From 4429db15527171294d23d66f2edab36f2dba4fae Mon Sep 17 00:00:00 2001 From: stravo1 Date: Tue, 28 Jul 2026 18:15:50 +0530 Subject: [PATCH 1/7] fix: keep autocomplete lists full while filtering Each getOptions implementation filtered and capped its own list. Once an item is selected the list showed no extra options. Add filterOptions, which puts the matches for a query first, fills the rest of the list with the other options, and stops at 20 items. Co-Authored-By: Claude Opus 5 --- .../TypographySection.ts | 54 ++++------- .../src/components/DynamicValueDropdown.vue | 8 +- .../components/Settings/AnalyticsFilters.vue | 13 +-- frontend/src/components/VisibilityInput.vue | 50 +++------- frontend/src/utils/autocompleteOptions.ts | 26 ++++++ frontend/src/utils/colorOptions.ts | 92 +++++++++---------- frontend/src/utils/cssMetadata.ts | 6 +- 7 files changed, 112 insertions(+), 137 deletions(-) create mode 100644 frontend/src/utils/autocompleteOptions.ts diff --git a/frontend/src/components/BlockPropertySections/TypographySection.ts b/frontend/src/components/BlockPropertySections/TypographySection.ts index 74c2d9642..a98ba50b0 100644 --- a/frontend/src/components/BlockPropertySections/TypographySection.ts +++ b/frontend/src/components/BlockPropertySections/TypographySection.ts @@ -5,6 +5,7 @@ import OptionToggle from "@/components/Controls/OptionToggle.vue"; import StylePropertyControl from "@/components/Controls/StylePropertyControl.vue"; import userFonts from "@/data/userFonts"; import { UserFont } from "@/types/doctypes"; +import { filterOptions, MAX_OPTIONS } from "@/utils/autocompleteOptions"; import blockController from "@/utils/blockController"; import { setFont as _setFont, fontListItems, getFontWeightOptions, loadFontList } from "@/utils/fontManager"; import { BOX_UNIT_OPTIONS } from "@/utils/unitOptions"; @@ -44,41 +45,24 @@ const typographySectionProperties = [ propertyKey: "fontFamily", getOptions: async (filterString: string) => { await loadFontList(); - const fontOptions = [] as { label: string; value: string }[]; - userFonts.data?.forEach((font: UserFont) => { - if (fontOptions.length >= 20) { - return; - } - const fontName = font.font_name as string; - if (fontName.toLowerCase().includes(filterString.toLowerCase()) || !filterString) { - fontOptions.push({ - label: fontName, - value: fontName, - }); - } - }); - if (fontOptions.length) { - fontOptions.unshift({ - label: "Custom", - value: "_separator_1", - }); - fontOptions.push({ - label: "Default", - value: "_separator_2", - }); - } - fontListItems.value.forEach((font) => { - if (fontOptions.length >= 20) { - return; - } - if (font.family.toLowerCase().includes(filterString.toLowerCase()) || !filterString) { - fontOptions.push({ - label: font.family, - value: font.family, - }); - } - }); - return fontOptions; + const toOption = (family: string) => ({ label: family, value: family }); + const userFontOptions = filterOptions( + (userFonts.data || []).map((font: UserFont) => toOption(font.font_name as string)), + filterString, + ); + const defaultFontOptions = filterOptions( + fontListItems.value.map((font) => toOption(font.family)), + filterString, + Math.max(MAX_OPTIONS - userFontOptions.length, 0), + ); + + if (!userFontOptions.length) return defaultFontOptions; + return [ + { label: "Custom", value: "_separator_1" }, + ...userFontOptions, + { label: "Default", value: "_separator_2" }, + ...defaultFontOptions, + ]; }, actionButton: { component: FontUploader, diff --git a/frontend/src/components/DynamicValueDropdown.vue b/frontend/src/components/DynamicValueDropdown.vue index cdf560e1e..453b4fb9e 100644 --- a/frontend/src/components/DynamicValueDropdown.vue +++ b/frontend/src/components/DynamicValueDropdown.vue @@ -30,6 +30,7 @@ import Autocomplete from "@/components/Controls/Autocomplete.vue"; import MiddleTruncate from "@/components/MiddleTruncate.vue"; import usePageStore from "@/stores/pageStore"; +import { filterOptions } from "@/utils/autocompleteOptions"; import blockController from "@/utils/blockController"; import { getDataArray, getDefaultPropsList, getParentProps, getRepeaterScopedData } from "@/utils/helpers"; import { computed, ref, watch } from "vue"; @@ -100,12 +101,7 @@ const dynamicOptions = computed(() => { return options; }); -const getOptions = async (query: string) => { - const normalizedQuery = query.trim().toLowerCase(); - return dynamicOptions.value.filter((option) => { - return !normalizedQuery || option.label.toLowerCase().includes(normalizedQuery); - }); -}; +const getOptions = async (query: string) => filterOptions(dynamicOptions.value, query); const handleModelValueUpdate = (value: string | null) => { if (value == null) { diff --git a/frontend/src/components/Settings/AnalyticsFilters.vue b/frontend/src/components/Settings/AnalyticsFilters.vue index ff4cda140..085a955bc 100644 --- a/frontend/src/components/Settings/AnalyticsFilters.vue +++ b/frontend/src/components/Settings/AnalyticsFilters.vue @@ -25,6 +25,7 @@ import Autocomplete from "@/components/Controls/Autocomplete.vue"; import { webPages } from "@/data/webPage"; import { BuilderPage } from "@/types/doctypes"; +import { filterOptions } from "@/utils/autocompleteOptions"; import { DateRangePicker, Select } from "frappe-ui"; import { computed } from "vue"; @@ -101,14 +102,10 @@ const getRouteOptions = async (query: string) => { await webPages.fetch(); } - const queryLower = query?.toLowerCase() || ""; - - return (webPages.data ?? []) + const routeOptions = (webPages.data ?? []) .filter((page: BuilderPage) => page.route && !page.dynamic_route) - .map((page: BuilderPage) => ({ value: page.route as string, label: page.route as string })) - .filter( - (option: { value: string; label: string }) => - !queryLower || option.label.toLowerCase().includes(queryLower), - ); + .map((page: BuilderPage) => ({ value: page.route as string, label: page.route as string })); + + return filterOptions(routeOptions, query || ""); }; diff --git a/frontend/src/components/VisibilityInput.vue b/frontend/src/components/VisibilityInput.vue index 8bd5022ea..b409413fc 100644 --- a/frontend/src/components/VisibilityInput.vue +++ b/frontend/src/components/VisibilityInput.vue @@ -11,6 +11,7 @@ import InlineInput from "@/components/Controls/InlineInput.vue"; import useCanvasStore from "@/stores/canvasStore"; import usePageStore from "@/stores/pageStore"; +import { filterOptions } from "@/utils/autocompleteOptions"; import blockController from "@/utils/blockController"; import componentController from "@/utils/componentController"; import { getDataArray, getDefaultPropsList, getParentProps, getRepeaterScopedData } from "@/utils/helpers"; @@ -66,46 +67,19 @@ const defaultProps = computed(() => { } return Object.keys(getDefaultPropsList(currentBlock.value)); }); -const getOptions = async (query: string) => { - let options: { label: string; value: string }[] = []; +const visibilityOptions = computed(() => { + const toOptions = (props: string[], comesFrom: string) => + props.map((prop) => ({ label: prop, value: `${prop}--${comesFrom}` })); + const propsList = [...new Set([...ownProps.value, ...parentProps.value, ...defaultProps.value])]; - pageDataArray.value.map((prop) => { - if (query.trim() == "" || prop.toLowerCase().includes(query.toLowerCase())) { - options.push({ - label: prop, - value: `${prop}--dataScript`, - }); - } - }); - componentDataArray.value.map((prop) => { - if (query.trim() == "" || prop.toLowerCase().includes(query.toLowerCase())) { - options.push({ - label: prop, - value: `${prop}--componentData`, - }); - } - }); - const combinedProps = [...new Set([...ownProps.value, ...parentProps.value])]; - - combinedProps.map((prop) => { - if (query.trim() == "" || prop.toLowerCase().includes(query.toLowerCase())) { - options.push({ - label: prop, - value: `${prop}--props`, - }); - } - }); - defaultProps.value.map((prop) => { - if (query.trim() == "" || prop.toLowerCase().includes(query.toLowerCase())) { - options.push({ - label: prop, - value: `${prop}--props`, - }); - } - }); + return [ + ...toOptions(pageDataArray.value, "dataScript"), + ...toOptions(componentDataArray.value, "componentData"), + ...toOptions(propsList, "props"), + ]; +}); - return options; -}; +const getOptions = async (query: string) => filterOptions(visibilityOptions.value, query); const handleModelValueUpdate = (value: string | null) => { if (value == null) { diff --git a/frontend/src/utils/autocompleteOptions.ts b/frontend/src/utils/autocompleteOptions.ts new file mode 100644 index 000000000..0eadca43c --- /dev/null +++ b/frontend/src/utils/autocompleteOptions.ts @@ -0,0 +1,26 @@ +const MAX_OPTIONS = 20; + +// keeps every match for the query in the list, then fills the rest of the list with the +// remaining options so the dropdown always offers something to browse +function filterOptions