Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 18 additions & 35 deletions frontend/src/components/BlockPropertySections/TypographySection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } 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";
Expand Down Expand Up @@ -44,41 +45,23 @@ 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,
);
Comment thread
greptile-apps[bot] marked this conversation as resolved.

if (!userFontOptions.length) return defaultFontOptions;
return [
{ label: "Custom", value: "_separator_1" },
...userFontOptions,
{ label: "Default", value: "_separator_2" },
...defaultFontOptions,
];
},
actionButton: {
component: FontUploader,
Expand Down
8 changes: 2 additions & 6 deletions frontend/src/components/DynamicValueDropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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) {
Expand Down
13 changes: 5 additions & 8 deletions frontend/src/components/Settings/AnalyticsFilters.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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 || "");
};
</script>
50 changes: 12 additions & 38 deletions frontend/src/components/VisibilityInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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) {
Expand Down
23 changes: 23 additions & 0 deletions frontend/src/utils/autocompleteOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const MAX_OPTIONS = 20;

// typing filters to matching options; an exact match (the current selection)
// shows the list windowed around it, with about a third of the window above it
function filterOptions<Option extends { label: string }>(
options: Option[],
query: string,
limit = MAX_OPTIONS,
): Option[] {
const normalizedQuery = query.trim().toLowerCase();
if (!normalizedQuery) return options.slice(0, limit);

const selectedIndex = options.findIndex((option) => option.label.toLowerCase() === normalizedQuery);
if (selectedIndex === -1) {
return options.filter((option) => option.label.toLowerCase().includes(normalizedQuery)).slice(0, limit);
}

const maxStart = Math.max(options.length - limit, 0);
const start = Math.min(Math.max(selectedIndex - Math.floor(limit / 3), 0), maxStart);
return options.slice(start, start + limit);
}

export { filterOptions, MAX_OPTIONS };
115 changes: 67 additions & 48 deletions frontend/src/utils/colorOptions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BuilderVariable } from "@/types/doctypes";
import { filterOptions } from "@/utils/autocompleteOptions";
import { defineComponent, h, shallowRef } from "vue";

export function getColorVariableOptions(
Expand All @@ -8,55 +9,73 @@ export function getColorVariableOptions(
isDark: boolean,
onEdit?: (variable: BuilderVariable) => void,
) {
let processedQuery = query.replace(/^(--|var|\s+)/, "");
processedQuery = processedQuery.replace(/^--|\(|\s+/g, "");
// strip var(--...) syntax, keep the rest of the query as typed
let processedQuery = query
.replace(/^\s*(var\()?\s*(--)?/, "")
.replace(/\)\s*$/, "")
.trim();

return variables
.filter((builderVariable: BuilderVariable) => {
const label = (builderVariable.variable_name || "").toLowerCase();
const group = (builderVariable.group || "").toLowerCase();
const queryLower = processedQuery.toLowerCase();
return queryLower === "" || label.includes(queryLower) || group.includes(queryLower);
})
.map((builderVariable: BuilderVariable) => {
const varName = `var(--${builderVariable.name})`;
const resolvedLightColor = resolveVariableValue(varName);
const resolvedDarkColor = resolveVariableValue(varName, true);
const searchableLabel = (builderVariable: BuilderVariable) =>
`${builderVariable.variable_name || ""} ${builderVariable.group || ""}`;

return {
label: `${builderVariable?.variable_name || ""}`,
value: varName,
prefix: shallowRef(
defineComponent({
setup() {
return () =>
h("div", {
class: "h-4 w-4 rounded shadow-sm border border-outline-gray-1 flex-shrink-0",
style: { background: isDark ? resolvedDarkColor : resolvedLightColor },
});
},
}),
),
suffix:
!builderVariable.is_standard && onEdit
? shallowRef(
defineComponent({
setup() {
return () =>
h(
"Button",
{
class: "hidden group-hover:inline-block",
onClick: (e: Event) => {
onEdit(builderVariable);
},
// the group name is searchable along with the variable name
const searchableOptions = variables
.map((builderVariable: BuilderVariable) => ({
label: searchableLabel(builderVariable),
variable: builderVariable,
}))
// alphabetical, so the options around a match are related ones
.sort((a, b) => (a.variable.variable_name || "").localeCompare(b.variable.variable_name || ""));

// the query is the current selection when it is the var() value or the
// variable's display name (what ColorInput shows on focus): use its full
// label so filterOptions windows the list around it
const normalizedQuery = processedQuery.toLowerCase();
const selectedVariable = variables.find(
(v) =>
query.trim() === `var(--${v.name})` || (v.variable_name || "").toLowerCase() === normalizedQuery,
);
if (selectedVariable) processedQuery = searchableLabel(selectedVariable);

return filterOptions(searchableOptions, processedQuery).map(({ variable: builderVariable }) => {
const varName = `var(--${builderVariable.name})`;
const resolvedLightColor = resolveVariableValue(varName);
const resolvedDarkColor = resolveVariableValue(varName, true);

return {
label: `${builderVariable.variable_name || ""}`,
value: varName,
prefix: shallowRef(
defineComponent({
setup() {
return () =>
h("div", {
class: "h-4 w-4 rounded shadow-sm border border-outline-gray-1 flex-shrink-0",
style: { background: isDark ? resolvedDarkColor : resolvedLightColor },
});
},
}),
),
suffix:
!builderVariable.is_standard && onEdit
? shallowRef(
defineComponent({
setup() {
return () =>
h(
"Button",
{
class: "hidden group-hover:inline-block",
onClick: (e: Event) => {
onEdit(builderVariable);
},
"Edit",
);
},
}),
)
: undefined,
};
});
},
"Edit",
);
},
}),
)
: undefined,
};
});
}
6 changes: 2 additions & 4 deletions frontend/src/utils/cssMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Autocomplete from "@/components/Controls/Autocomplete.vue";
import ColorInput from "@/components/Controls/ColorInput.vue";
import RangeInput from "@/components/Controls/RangeInput.vue";
import cssPropertyMetadata from "@/data/cssPropertyMetadata.json";
import { filterOptions } from "@/utils/autocompleteOptions";
import {
BORDER_UNIT_OPTIONS,
BOX_UNIT_OPTIONS,
Expand Down Expand Up @@ -149,10 +150,7 @@ const getCSSPropertyOptions = (query: string, excludedProperties = new Set<strin
};

const getCSSValueOptions = (property: string, query: string) => {
const normalizedQuery = query.toLowerCase();
return getKeywordOptions(property)
.filter((option) => !normalizedQuery || option.label.toLowerCase().includes(normalizedQuery))
.slice(0, MAX_SEARCH_RESULTS);
return filterOptions(getKeywordOptions(property), query, MAX_SEARCH_RESULTS);
};

export type { StyleControlConfig };
Expand Down
Loading