Skip to content

Commit

Permalink
Categorize setting items (#338)
Browse files Browse the repository at this point in the history
* Basic setting panel rework

* refactor

* Style the setting item

* Reject invalid value

* nit

* nit

* Sort settings by label

* info chip as icon

* nit
  • Loading branch information
huchenlei authored Aug 8, 2024
1 parent 02d7f91 commit a5f0d2b
Show file tree
Hide file tree
Showing 10 changed files with 379 additions and 109 deletions.
6 changes: 5 additions & 1 deletion src/components/dialog/GlobalDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
@unmaximize="maximized = false"
>
<template #header>
<h3>{{ dialogStore.title || ' ' }}</h3>
<component
v-if="dialogStore.headerComponent"
:is="dialogStore.headerComponent"
/>
<h3 v-else>{{ dialogStore.title || ' ' }}</h3>
</template>

<component
Expand Down
189 changes: 96 additions & 93 deletions src/components/dialog/content/SettingDialogContent.vue
Original file line number Diff line number Diff line change
@@ -1,116 +1,119 @@
<template>
<table class="comfy-modal-content comfy-table">
<tbody>
<tr v-for="setting in sortedSettings" :key="setting.id">
<td>
<span>
{{ setting.name }}
</span>
<Chip
v-if="setting.tooltip"
icon="pi pi-info-circle"
severity="secondary"
v-tooltip="setting.tooltip"
class="info-chip"
/>
</td>
<td>
<component
:is="markRaw(getSettingComponent(setting))"
:id="setting.id"
:modelValue="settingStore.get(setting.id)"
@update:modelValue="updateSetting(setting, $event)"
v-bind="getSettingAttrs(setting)"
/>
</td>
</tr>
</tbody>
</table>
<div class="settings-container">
<div class="settings-sidebar">
<Listbox
v-model="activeCategory"
:options="categories"
optionLabel="label"
scrollHeight="100%"
:pt="{ root: { class: 'border-none' } }"
/>
</div>
<Divider layout="vertical" />
<div class="settings-content" v-if="activeCategory">
<Tabs :value="activeCategory.label">
<TabPanels>
<TabPanel
v-for="category in categories"
:key="category.key"
:value="category.label"
>
<SettingGroup
v-for="group in sortedGroups(category)"
:key="group.label"
:group="{
label: group.label,
settings: flattenTree<SettingParams>(group)
}"
/>
</TabPanel>
</TabPanels>
</Tabs>
</div>
</div>
</template>

<script setup lang="ts">
import { type Component, computed, markRaw } from 'vue'
import InputText from 'primevue/inputtext'
import InputNumber from 'primevue/inputnumber'
import Select from 'primevue/select'
import Chip from 'primevue/chip'
import ToggleSwitch from 'primevue/toggleswitch'
import { useSettingStore } from '@/stores/settingStore'
import { ref, computed, onMounted, watch } from 'vue'
import Listbox from 'primevue/listbox'
import Tabs from 'primevue/tabs'
import TabPanels from 'primevue/tabpanels'
import TabPanel from 'primevue/tabpanel'
import Divider from 'primevue/divider'
import { SettingTreeNode, useSettingStore } from '@/stores/settingStore'
import { SettingParams } from '@/types/settingTypes'
import CustomSettingValue from '@/components/dialog/content/setting/CustomSettingValue.vue'
import InputSlider from '@/components/dialog/content/setting/InputSlider.vue'
import SettingGroup from './setting/SettingGroup.vue'
import { flattenTree } from '@/utils/treeUtil'
const settingStore = useSettingStore()
const sortedSettings = computed<SettingParams[]>(() => {
return Object.values(settingStore.settings)
.filter((setting: SettingParams) => setting.type !== 'hidden')
.sort((a, b) => a.name.localeCompare(b.name))
})
function getSettingAttrs(setting: SettingParams) {
const attrs = { ...(setting.attrs || {}) }
const settingType = setting.type
if (typeof settingType === 'function') {
attrs['renderFunction'] = () =>
settingType(
setting.name,
(v) => updateSetting(setting, v),
settingStore.get(setting.id),
setting.attrs
)
}
switch (setting.type) {
case 'combo':
attrs['options'] = setting.options
if (typeof setting.options[0] !== 'string') {
attrs['optionLabel'] = 'text'
attrs['optionValue'] = 'value'
}
break
}
const settingRoot = computed<SettingTreeNode>(() => settingStore.settingTree)
const categories = computed<SettingTreeNode[]>(
() => settingRoot.value.children || []
)
attrs['class'] += ' comfy-vue-setting-input'
return attrs
}
const activeCategory = ref<SettingTreeNode | null>(null)
function getSettingComponent(setting: SettingParams): Component {
if (typeof setting.type === 'function') {
// return setting.type(
// setting.name, (v) => updateSetting(setting, v), settingStore.get(setting.id), setting.attrs)
return CustomSettingValue
}
switch (setting.type) {
case 'boolean':
return ToggleSwitch
case 'number':
return InputNumber
case 'slider':
return InputSlider
case 'combo':
return Select
default:
return InputText
watch(activeCategory, (newCategory, oldCategory) => {
if (newCategory === null) {
activeCategory.value = oldCategory
}
}
})
const updateSetting = (setting: SettingParams, value: any) => {
if (setting.onChange) setting.onChange(value, settingStore.get(setting.id))
onMounted(() => {
activeCategory.value = categories.value[0]
})
settingStore.set(setting.id, value)
const sortedGroups = (category: SettingTreeNode) => {
return [...(category.children || [])].sort((a, b) =>
a.label.localeCompare(b.label)
)
}
</script>

<style>
.info-chip {
background: transparent !important;
}
.comfy-vue-setting-input {
width: 100%;
/* Remove after we have tailwind setup */
.border-none {
border: none !important;
}
</style>

<style scoped>
.comfy-table {
.settings-container {
display: flex;
height: 80vh;
width: 60vw;
max-width: 1000px;
overflow: hidden;
/* Prevents container from scrolling */
}
.settings-sidebar {
width: 250px;
flex-shrink: 0;
/* Prevents sidebar from shrinking */
overflow-y: auto;
padding: 10px;
}
.settings-content {
flex-grow: 1;
overflow-y: auto;
/* Allows vertical scrolling */
}
/* Ensure the Listbox takes full width of the sidebar */
.settings-sidebar :deep(.p-listbox) {
width: 100%;
}
/* Optional: Style scrollbars for webkit browsers */
.settings-sidebar::-webkit-scrollbar,
.settings-content::-webkit-scrollbar {
width: 1px;
}
.settings-sidebar::-webkit-scrollbar-thumb,
.settings-content::-webkit-scrollbar-thumb {
background-color: transparent;
}
</style>
55 changes: 42 additions & 13 deletions src/components/dialog/content/setting/InputSlider.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,68 @@
@update:modelValue="updateValue"
class="slider-part"
:class="sliderClass"
v-bind="$attrs"
:min="min"
:max="max"
:step="step"
/>
<InputText
:value="modelValue"
@input="updateValue"
<InputNumber
:modelValue="modelValue"
@update:modelValue="updateValue"
class="input-part"
:class="inputClass"
:min="min"
:max="max"
:step="step"
/>
</div>
</template>

<script setup lang="ts">
import InputText from 'primevue/inputtext'
import { ref, watch } from 'vue'
import InputNumber from 'primevue/inputnumber'
import Slider from 'primevue/slider'
const props = defineProps<{
modelValue: number
inputClass?: string
sliderClass?: string
min?: number
max?: number
step?: number
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: number): void
}>()
const updateValue = (newValue: string | number) => {
const numValue =
typeof newValue === 'string' ? parseFloat(newValue) : newValue
if (!isNaN(numValue)) {
emit('update:modelValue', numValue)
const localValue = ref(props.modelValue)
watch(
() => props.modelValue,
(newValue) => {
localValue.value = newValue
}
)
const updateValue = (newValue: number | null) => {
if (newValue === null) {
// If the input is cleared, reset to the minimum value or 0
newValue = Number(props.min) || 0
}
const min = Number(props.min) || Number.NEGATIVE_INFINITY
const max = Number(props.max) || Number.POSITIVE_INFINITY
const step = Number(props.step) || 1
// Ensure the value is within the allowed range
newValue = Math.max(min, Math.min(max, newValue))
// Round to the nearest step
newValue = Math.round(newValue / step) * step
// Update local value and emit change
localValue.value = newValue
emit('update:modelValue', newValue)
}
</script>

Expand All @@ -44,15 +75,13 @@ const updateValue = (newValue: string | number) => {
display: flex;
align-items: center;
gap: 1rem;
/* Adjust this value to control space between slider and input */
}
.slider-part {
flex-grow: 1;
}
.input-part {
width: 5rem;
/* Adjust this value to control input width */
width: 5rem !important;
}
</style>
Loading

0 comments on commit a5f0d2b

Please sign in to comment.