Skip to content

Commit a5f0d2b

Browse files
authored
Categorize setting items (#338)
* Basic setting panel rework * refactor * Style the setting item * Reject invalid value * nit * nit * Sort settings by label * info chip as icon * nit
1 parent 02d7f91 commit a5f0d2b

File tree

10 files changed

+379
-109
lines changed

10 files changed

+379
-109
lines changed

src/components/dialog/GlobalDialog.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@
1212
@unmaximize="maximized = false"
1313
>
1414
<template #header>
15-
<h3>{{ dialogStore.title || ' ' }}</h3>
15+
<component
16+
v-if="dialogStore.headerComponent"
17+
:is="dialogStore.headerComponent"
18+
/>
19+
<h3 v-else>{{ dialogStore.title || ' ' }}</h3>
1620
</template>
1721

1822
<component
Lines changed: 96 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,116 +1,119 @@
11
<template>
2-
<table class="comfy-modal-content comfy-table">
3-
<tbody>
4-
<tr v-for="setting in sortedSettings" :key="setting.id">
5-
<td>
6-
<span>
7-
{{ setting.name }}
8-
</span>
9-
<Chip
10-
v-if="setting.tooltip"
11-
icon="pi pi-info-circle"
12-
severity="secondary"
13-
v-tooltip="setting.tooltip"
14-
class="info-chip"
15-
/>
16-
</td>
17-
<td>
18-
<component
19-
:is="markRaw(getSettingComponent(setting))"
20-
:id="setting.id"
21-
:modelValue="settingStore.get(setting.id)"
22-
@update:modelValue="updateSetting(setting, $event)"
23-
v-bind="getSettingAttrs(setting)"
24-
/>
25-
</td>
26-
</tr>
27-
</tbody>
28-
</table>
2+
<div class="settings-container">
3+
<div class="settings-sidebar">
4+
<Listbox
5+
v-model="activeCategory"
6+
:options="categories"
7+
optionLabel="label"
8+
scrollHeight="100%"
9+
:pt="{ root: { class: 'border-none' } }"
10+
/>
11+
</div>
12+
<Divider layout="vertical" />
13+
<div class="settings-content" v-if="activeCategory">
14+
<Tabs :value="activeCategory.label">
15+
<TabPanels>
16+
<TabPanel
17+
v-for="category in categories"
18+
:key="category.key"
19+
:value="category.label"
20+
>
21+
<SettingGroup
22+
v-for="group in sortedGroups(category)"
23+
:key="group.label"
24+
:group="{
25+
label: group.label,
26+
settings: flattenTree<SettingParams>(group)
27+
}"
28+
/>
29+
</TabPanel>
30+
</TabPanels>
31+
</Tabs>
32+
</div>
33+
</div>
2934
</template>
3035

3136
<script setup lang="ts">
32-
import { type Component, computed, markRaw } from 'vue'
33-
import InputText from 'primevue/inputtext'
34-
import InputNumber from 'primevue/inputnumber'
35-
import Select from 'primevue/select'
36-
import Chip from 'primevue/chip'
37-
import ToggleSwitch from 'primevue/toggleswitch'
38-
import { useSettingStore } from '@/stores/settingStore'
37+
import { ref, computed, onMounted, watch } from 'vue'
38+
import Listbox from 'primevue/listbox'
39+
import Tabs from 'primevue/tabs'
40+
import TabPanels from 'primevue/tabpanels'
41+
import TabPanel from 'primevue/tabpanel'
42+
import Divider from 'primevue/divider'
43+
import { SettingTreeNode, useSettingStore } from '@/stores/settingStore'
3944
import { SettingParams } from '@/types/settingTypes'
40-
import CustomSettingValue from '@/components/dialog/content/setting/CustomSettingValue.vue'
41-
import InputSlider from '@/components/dialog/content/setting/InputSlider.vue'
45+
import SettingGroup from './setting/SettingGroup.vue'
46+
import { flattenTree } from '@/utils/treeUtil'
4247
4348
const settingStore = useSettingStore()
44-
const sortedSettings = computed<SettingParams[]>(() => {
45-
return Object.values(settingStore.settings)
46-
.filter((setting: SettingParams) => setting.type !== 'hidden')
47-
.sort((a, b) => a.name.localeCompare(b.name))
48-
})
49-
50-
function getSettingAttrs(setting: SettingParams) {
51-
const attrs = { ...(setting.attrs || {}) }
52-
const settingType = setting.type
53-
if (typeof settingType === 'function') {
54-
attrs['renderFunction'] = () =>
55-
settingType(
56-
setting.name,
57-
(v) => updateSetting(setting, v),
58-
settingStore.get(setting.id),
59-
setting.attrs
60-
)
61-
}
62-
switch (setting.type) {
63-
case 'combo':
64-
attrs['options'] = setting.options
65-
if (typeof setting.options[0] !== 'string') {
66-
attrs['optionLabel'] = 'text'
67-
attrs['optionValue'] = 'value'
68-
}
69-
break
70-
}
49+
const settingRoot = computed<SettingTreeNode>(() => settingStore.settingTree)
50+
const categories = computed<SettingTreeNode[]>(
51+
() => settingRoot.value.children || []
52+
)
7153
72-
attrs['class'] += ' comfy-vue-setting-input'
73-
return attrs
74-
}
54+
const activeCategory = ref<SettingTreeNode | null>(null)
7555
76-
function getSettingComponent(setting: SettingParams): Component {
77-
if (typeof setting.type === 'function') {
78-
// return setting.type(
79-
// setting.name, (v) => updateSetting(setting, v), settingStore.get(setting.id), setting.attrs)
80-
return CustomSettingValue
81-
}
82-
switch (setting.type) {
83-
case 'boolean':
84-
return ToggleSwitch
85-
case 'number':
86-
return InputNumber
87-
case 'slider':
88-
return InputSlider
89-
case 'combo':
90-
return Select
91-
default:
92-
return InputText
56+
watch(activeCategory, (newCategory, oldCategory) => {
57+
if (newCategory === null) {
58+
activeCategory.value = oldCategory
9359
}
94-
}
60+
})
9561
96-
const updateSetting = (setting: SettingParams, value: any) => {
97-
if (setting.onChange) setting.onChange(value, settingStore.get(setting.id))
62+
onMounted(() => {
63+
activeCategory.value = categories.value[0]
64+
})
9865
99-
settingStore.set(setting.id, value)
66+
const sortedGroups = (category: SettingTreeNode) => {
67+
return [...(category.children || [])].sort((a, b) =>
68+
a.label.localeCompare(b.label)
69+
)
10070
}
10171
</script>
10272

10373
<style>
104-
.info-chip {
105-
background: transparent !important;
106-
}
107-
.comfy-vue-setting-input {
108-
width: 100%;
74+
/* Remove after we have tailwind setup */
75+
.border-none {
76+
border: none !important;
10977
}
11078
</style>
11179

11280
<style scoped>
113-
.comfy-table {
81+
.settings-container {
82+
display: flex;
83+
height: 80vh;
84+
width: 60vw;
85+
max-width: 1000px;
86+
overflow: hidden;
87+
/* Prevents container from scrolling */
88+
}
89+
90+
.settings-sidebar {
91+
width: 250px;
92+
flex-shrink: 0;
93+
/* Prevents sidebar from shrinking */
94+
overflow-y: auto;
95+
padding: 10px;
96+
}
97+
98+
.settings-content {
99+
flex-grow: 1;
100+
overflow-y: auto;
101+
/* Allows vertical scrolling */
102+
}
103+
104+
/* Ensure the Listbox takes full width of the sidebar */
105+
.settings-sidebar :deep(.p-listbox) {
114106
width: 100%;
115107
}
108+
109+
/* Optional: Style scrollbars for webkit browsers */
110+
.settings-sidebar::-webkit-scrollbar,
111+
.settings-content::-webkit-scrollbar {
112+
width: 1px;
113+
}
114+
115+
.settings-sidebar::-webkit-scrollbar-thumb,
116+
.settings-content::-webkit-scrollbar-thumb {
117+
background-color: transparent;
118+
}
116119
</style>

src/components/dialog/content/setting/InputSlider.vue

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,37 +5,68 @@
55
@update:modelValue="updateValue"
66
class="slider-part"
77
:class="sliderClass"
8-
v-bind="$attrs"
8+
:min="min"
9+
:max="max"
10+
:step="step"
911
/>
10-
<InputText
11-
:value="modelValue"
12-
@input="updateValue"
12+
<InputNumber
13+
:modelValue="modelValue"
14+
@update:modelValue="updateValue"
1315
class="input-part"
1416
:class="inputClass"
17+
:min="min"
18+
:max="max"
19+
:step="step"
1520
/>
1621
</div>
1722
</template>
1823

1924
<script setup lang="ts">
20-
import InputText from 'primevue/inputtext'
25+
import { ref, watch } from 'vue'
26+
import InputNumber from 'primevue/inputnumber'
2127
import Slider from 'primevue/slider'
2228
2329
const props = defineProps<{
2430
modelValue: number
2531
inputClass?: string
2632
sliderClass?: string
33+
min?: number
34+
max?: number
35+
step?: number
2736
}>()
2837
2938
const emit = defineEmits<{
3039
(e: 'update:modelValue', value: number): void
3140
}>()
3241
33-
const updateValue = (newValue: string | number) => {
34-
const numValue =
35-
typeof newValue === 'string' ? parseFloat(newValue) : newValue
36-
if (!isNaN(numValue)) {
37-
emit('update:modelValue', numValue)
42+
const localValue = ref(props.modelValue)
43+
44+
watch(
45+
() => props.modelValue,
46+
(newValue) => {
47+
localValue.value = newValue
48+
}
49+
)
50+
51+
const updateValue = (newValue: number | null) => {
52+
if (newValue === null) {
53+
// If the input is cleared, reset to the minimum value or 0
54+
newValue = Number(props.min) || 0
3855
}
56+
57+
const min = Number(props.min) || Number.NEGATIVE_INFINITY
58+
const max = Number(props.max) || Number.POSITIVE_INFINITY
59+
const step = Number(props.step) || 1
60+
61+
// Ensure the value is within the allowed range
62+
newValue = Math.max(min, Math.min(max, newValue))
63+
64+
// Round to the nearest step
65+
newValue = Math.round(newValue / step) * step
66+
67+
// Update local value and emit change
68+
localValue.value = newValue
69+
emit('update:modelValue', newValue)
3970
}
4071
</script>
4172

@@ -44,15 +75,13 @@ const updateValue = (newValue: string | number) => {
4475
display: flex;
4576
align-items: center;
4677
gap: 1rem;
47-
/* Adjust this value to control space between slider and input */
4878
}
4979
5080
.slider-part {
5181
flex-grow: 1;
5282
}
5383
5484
.input-part {
55-
width: 5rem;
56-
/* Adjust this value to control input width */
85+
width: 5rem !important;
5786
}
5887
</style>

0 commit comments

Comments
 (0)