From dacb59f5d32791521f168b5747d62cbc017cb582 Mon Sep 17 00:00:00 2001 From: Chenlei Hu Date: Thu, 17 Apr 2025 15:55:16 -0400 Subject: [PATCH] [Refactor] Extract setting dialog logic into composables (#3490) --- .../dialog/content/SettingDialogContent.vue | 176 +++--------------- src/composables/setting/useSettingSearch.ts | 123 ++++++++++++ src/composables/setting/useSettingUI.ts | 79 ++++++++ 3 files changed, 233 insertions(+), 145 deletions(-) create mode 100644 src/composables/setting/useSettingSearch.ts create mode 100644 src/composables/setting/useSettingUI.ts diff --git a/src/components/dialog/content/SettingDialogContent.vue b/src/components/dialog/content/SettingDialogContent.vue index 352ecd6b4..81cd578a7 100644 --- a/src/components/dialog/content/SettingDialogContent.vue +++ b/src/components/dialog/content/SettingDialogContent.vue @@ -73,19 +73,13 @@ import Listbox from 'primevue/listbox' import ScrollPanel from 'primevue/scrollpanel' import TabPanels from 'primevue/tabpanels' import Tabs from 'primevue/tabs' -import { computed, defineAsyncComponent, onMounted, ref, watch } from 'vue' -import { useI18n } from 'vue-i18n' +import { computed, defineAsyncComponent, onMounted, watch } from 'vue' import SearchBox from '@/components/common/SearchBox.vue' -import { st } from '@/i18n' -import { - SettingTreeNode, - getSettingInfo, - useSettingStore -} from '@/stores/settingStore' +import { useSettingSearch } from '@/composables/setting/useSettingSearch' +import { useSettingUI } from '@/composables/setting/useSettingUI' +import { SettingTreeNode, useSettingStore } from '@/stores/settingStore' import { ISettingGroup, SettingParams } from '@/types/settingTypes' -import { isElectron } from '@/utils/envUtil' -import { normalizeI18nKey } from '@/utils/formatUtil' import { flattenTree } from '@/utils/treeUtil' import AboutPanel from './setting/AboutPanel.vue' @@ -95,7 +89,7 @@ import FirstTimeUIMessage from './setting/FirstTimeUIMessage.vue' import PanelTemplate from './setting/PanelTemplate.vue' import SettingsPanel from './setting/SettingsPanel.vue' -const props = defineProps<{ +const { defaultPanel } = defineProps<{ defaultPanel?: 'about' | 'keybinding' | 'extension' | 'server-config' }>() @@ -109,71 +103,35 @@ const ServerConfigPanel = defineAsyncComponent( () => import('./setting/ServerConfigPanel.vue') ) -const aboutPanelNode: SettingTreeNode = { - key: 'about', - label: 'About', - children: [] -} - -const keybindingPanelNode: SettingTreeNode = { - key: 'keybinding', - label: 'Keybinding', - children: [] -} - -const extensionPanelNode: SettingTreeNode = { - key: 'extension', - label: 'Extension', - children: [] -} - -const serverConfigPanelNode: SettingTreeNode = { - key: 'server-config', - label: 'Server-Config', - children: [] -} - -/** - * Server config panel is only available in Electron. We might want to support - * it in the web version in the future. - */ -const serverConfigPanelNodeList = computed(() => { - return isElectron() ? [serverConfigPanelNode] : [] -}) - const settingStore = useSettingStore() const settingRoot = computed(() => settingStore.settingTree) const settingCategories = computed( () => settingRoot.value.children ?? [] ) -const { t } = useI18n() + +const { activeCategory, getDefaultCategory, createTranslatedCategories } = + useSettingUI(defaultPanel) + +const { + searchQuery, + searchResultsCategories, + queryIsEmpty, + inSearch, + handleSearch: handleSearchBase, + getSearchResults +} = useSettingSearch() + +// Create categories with translated labels const categories = computed(() => - [ - ...settingCategories.value, - keybindingPanelNode, - extensionPanelNode, - ...serverConfigPanelNodeList.value, - aboutPanelNode - ].map((node) => ({ - ...node, - translatedLabel: t( - `settingsCategories.${normalizeI18nKey(node.label)}`, - node.label - ) - })) + createTranslatedCategories(settingCategories.value) ) -const activeCategory = ref(null) -const getDefaultCategory = () => { - return props.defaultPanel - ? categories.value.find((x) => x.key === props.defaultPanel) ?? - categories.value[0] - : categories.value[0] -} +// Initialize active category on mount onMounted(() => { - activeCategory.value = getDefaultCategory() + activeCategory.value = getDefaultCategory(categories.value) }) +// Sort groups for a category const sortedGroups = (category: SettingTreeNode): ISettingGroup[] => { return [...(category.children ?? [])] .sort((a, b) => a.label.localeCompare(b.label)) @@ -183,92 +141,20 @@ const sortedGroups = (category: SettingTreeNode): ISettingGroup[] => { })) } -const searchQuery = ref('') -const filteredSettingIds = ref([]) -const searchInProgress = ref(false) -watch(searchQuery, () => (searchInProgress.value = true)) - -const searchResults = computed(() => { - const groupedSettings: { [key: string]: SettingParams[] } = {} - - filteredSettingIds.value.forEach((id) => { - const setting = settingStore.settingsById[id] - const info = getSettingInfo(setting) - const groupLabel = info.subCategory - - if ( - activeCategory.value === null || - activeCategory.value.label === info.category - ) { - if (!groupedSettings[groupLabel]) { - groupedSettings[groupLabel] = [] - } - groupedSettings[groupLabel].push(setting) - } - }) - - return Object.entries(groupedSettings).map(([label, settings]) => ({ - label, - settings - })) -}) - -/** - * Settings categories that contains at least one setting in search results. - */ -const searchResultsCategories = computed>(() => { - return new Set( - filteredSettingIds.value.map( - (id) => getSettingInfo(settingStore.settingsById[id]).category - ) - ) -}) - const handleSearch = (query: string) => { - if (!query) { - filteredSettingIds.value = [] - activeCategory.value ??= getDefaultCategory() - return - } - - const queryLower = query.toLocaleLowerCase() - const allSettings = flattenTree(settingRoot.value) - const filteredSettings = allSettings.filter((setting) => { - const idLower = setting.id.toLowerCase() - const nameLower = setting.name.toLowerCase() - const translatedName = st( - `settings.${normalizeI18nKey(setting.id)}.name`, - setting.name - ).toLocaleLowerCase() - const info = getSettingInfo(setting) - const translatedCategory = st( - `settingsCategories.${normalizeI18nKey(info.category)}`, - info.category - ).toLocaleLowerCase() - const translatedSubCategory = st( - `settingsCategories.${normalizeI18nKey(info.subCategory)}`, - info.subCategory - ).toLocaleLowerCase() - - return ( - idLower.includes(queryLower) || - nameLower.includes(queryLower) || - translatedName.includes(queryLower) || - translatedCategory.includes(queryLower) || - translatedSubCategory.includes(queryLower) - ) - }) - - filteredSettingIds.value = filteredSettings.map((x) => x.id) - searchInProgress.value = false - activeCategory.value = null + handleSearchBase(query, settingRoot.value) + activeCategory.value = query ? null : getDefaultCategory(categories.value) } -const queryIsEmpty = computed(() => searchQuery.value.length === 0) -const inSearch = computed(() => !queryIsEmpty.value && !searchInProgress.value) +// Get search results +const searchResults = computed(() => + getSearchResults(activeCategory.value) +) + const tabValue = computed(() => inSearch.value ? 'Search Results' : activeCategory.value?.label ?? '' ) + // Don't allow null category to be set outside of search. // In search mode, the active category can be null to show all search results. watch(activeCategory, (_, oldValue) => { diff --git a/src/composables/setting/useSettingSearch.ts b/src/composables/setting/useSettingSearch.ts new file mode 100644 index 000000000..3cc601dd4 --- /dev/null +++ b/src/composables/setting/useSettingSearch.ts @@ -0,0 +1,123 @@ +import { computed, ref, watch } from 'vue' + +import { st } from '@/i18n' +import { + SettingTreeNode, + getSettingInfo, + useSettingStore +} from '@/stores/settingStore' +import { ISettingGroup, SettingParams } from '@/types/settingTypes' +import { normalizeI18nKey } from '@/utils/formatUtil' +import { flattenTree } from '@/utils/treeUtil' + +export function useSettingSearch() { + const settingStore = useSettingStore() + + const searchQuery = ref('') + const filteredSettingIds = ref([]) + const searchInProgress = ref(false) + + watch(searchQuery, () => (searchInProgress.value = true)) + + /** + * Settings categories that contains at least one setting in search results. + */ + const searchResultsCategories = computed>(() => { + return new Set( + filteredSettingIds.value.map( + (id) => getSettingInfo(settingStore.settingsById[id]).category + ) + ) + }) + + /** + * Check if the search query is empty + */ + const queryIsEmpty = computed(() => searchQuery.value.length === 0) + + /** + * Check if we're in search mode + */ + const inSearch = computed( + () => !queryIsEmpty.value && !searchInProgress.value + ) + + /** + * Handle search functionality + */ + const handleSearch = (query: string, settingRoot: SettingTreeNode) => { + if (!query) { + filteredSettingIds.value = [] + return + } + + const queryLower = query.toLocaleLowerCase() + const allSettings = flattenTree(settingRoot) + const filteredSettings = allSettings.filter((setting) => { + const idLower = setting.id.toLowerCase() + const nameLower = setting.name.toLowerCase() + const translatedName = st( + `settings.${normalizeI18nKey(setting.id)}.name`, + setting.name + ).toLocaleLowerCase() + const info = getSettingInfo(setting) + const translatedCategory = st( + `settingsCategories.${normalizeI18nKey(info.category)}`, + info.category + ).toLocaleLowerCase() + const translatedSubCategory = st( + `settingsCategories.${normalizeI18nKey(info.subCategory)}`, + info.subCategory + ).toLocaleLowerCase() + + return ( + idLower.includes(queryLower) || + nameLower.includes(queryLower) || + translatedName.includes(queryLower) || + translatedCategory.includes(queryLower) || + translatedSubCategory.includes(queryLower) + ) + }) + + filteredSettingIds.value = filteredSettings.map((x) => x.id) + searchInProgress.value = false + } + + /** + * Get search results grouped by category + */ + const getSearchResults = ( + activeCategory: SettingTreeNode | null + ): ISettingGroup[] => { + const groupedSettings: { [key: string]: SettingParams[] } = {} + + filteredSettingIds.value.forEach((id) => { + const setting = settingStore.settingsById[id] + const info = getSettingInfo(setting) + const groupLabel = info.subCategory + + if (activeCategory === null || activeCategory.label === info.category) { + if (!groupedSettings[groupLabel]) { + groupedSettings[groupLabel] = [] + } + groupedSettings[groupLabel].push(setting) + } + }) + + return Object.entries(groupedSettings).map(([label, settings]) => ({ + label, + settings + })) + } + + return { + searchQuery, + filteredSettingIds, + searchInProgress, + searchResultsCategories, + queryIsEmpty, + inSearch, + handleSearch, + getSearchResults + } +} diff --git a/src/composables/setting/useSettingUI.ts b/src/composables/setting/useSettingUI.ts new file mode 100644 index 000000000..cdd17650f --- /dev/null +++ b/src/composables/setting/useSettingUI.ts @@ -0,0 +1,79 @@ +import { computed, ref } from 'vue' +import { useI18n } from 'vue-i18n' + +import { SettingTreeNode } from '@/stores/settingStore' +import { isElectron } from '@/utils/envUtil' +import { normalizeI18nKey } from '@/utils/formatUtil' + +export function useSettingUI( + defaultPanel?: 'about' | 'keybinding' | 'extension' | 'server-config' +) { + const { t } = useI18n() + const activeCategory = ref(null) + + // Define panel nodes + const aboutPanelNode: SettingTreeNode = { + key: 'about', + label: 'About', + children: [] + } + + const keybindingPanelNode: SettingTreeNode = { + key: 'keybinding', + label: 'Keybinding', + children: [] + } + + const extensionPanelNode: SettingTreeNode = { + key: 'extension', + label: 'Extension', + children: [] + } + + const serverConfigPanelNode: SettingTreeNode = { + key: 'server-config', + label: 'Server-Config', + children: [] + } + + /** + * Server config panel is only available in Electron + */ + const serverConfigPanelNodeList = computed(() => { + return isElectron() ? [serverConfigPanelNode] : [] + }) + + /** + * Get the default category to show when the dialog is opened. + */ + const getDefaultCategory = (categories: SettingTreeNode[]) => { + return defaultPanel + ? categories.find((x) => x.key === defaultPanel) ?? categories[0] + : categories[0] + } + + /** + * Create translated categories with labels + */ + const createTranslatedCategories = (settingCategories: SettingTreeNode[]) => { + return [ + ...settingCategories, + keybindingPanelNode, + extensionPanelNode, + ...serverConfigPanelNodeList.value, + aboutPanelNode + ].map((node) => ({ + ...node, + translatedLabel: t( + `settingsCategories.${normalizeI18nKey(node.label)}`, + node.label + ) + })) + } + + return { + activeCategory, + getDefaultCategory, + createTranslatedCategories + } +}