diff --git a/apps/builder/app/builder/features/components/components.tsx b/apps/builder/app/builder/features/components/components.tsx index 6440c512b349..bb45a65752b3 100644 --- a/apps/builder/app/builder/features/components/components.tsx +++ b/apps/builder/app/builder/features/components/components.tsx @@ -1,9 +1,11 @@ -import { useMemo, useState } from "react"; +import { useState } from "react"; +import { matchSorter } from "match-sorter"; +import { computed } from "nanostores"; import { useStore } from "@nanostores/react"; import { XIcon } from "@webstudio-is/icons"; +import { isFeatureEnabled } from "@webstudio-is/feature-flags"; import { type WsComponentMeta, - blockComponent, collectionComponent, componentCategories, } from "@webstudio-is/react-sdk"; @@ -29,59 +31,63 @@ import { CollapsibleSection } from "~/builder/shared/collapsible-section"; import { dragItemAttribute, useDraggable } from "./use-draggable"; import { MetaIcon } from "~/builder/shared/meta-icon"; import { $registeredComponentMetas } from "~/shared/nano-states"; -import { - getMetaMaps, - type MetaByCategory, - type ComponentNamesByMeta, -} from "./get-meta-maps"; import { findClosestInsertable, getComponentTemplateData, getInstanceLabel, insertWebstudioFragmentAt, } from "~/shared/instance-utils"; -import { isFeatureEnabled } from "@webstudio-is/feature-flags"; -import { matchSorter } from "match-sorter"; -import { parseComponentName } from "@webstudio-is/sdk"; import type { Publish } from "~/shared/pubsub"; import { $selectedPage } from "~/shared/awareness"; - -const matchComponents = ( - metas: Array, - componentNamesByMeta: ComponentNamesByMeta, - search: string -) => { - const getKey = (meta: WsComponentMeta) => { - if (meta.label) { - return meta.label.toLowerCase(); - } - const component = componentNamesByMeta.get(meta); - if (component) { - const [_namespace, name] = parseComponentName(component); - return name.toLowerCase(); - } - return ""; - }; - - return matchSorter(metas, search, { - keys: [getKey], - }); +import { mapGroupBy } from "~/shared/shim"; + +type Meta = { + name: string; + category: string; + order: undefined | number; + label: string; + description: undefined | string; + icon: string; }; +const $metas = computed([$registeredComponentMetas], (componentMetas) => { + const availableComponents = new Set(); + const metas: Meta[] = []; + for (const [name, componentMeta] of componentMetas) { + availableComponents.add(name); + metas.push({ + name, + category: componentMeta.category ?? "hidden", + order: componentMeta.order, + label: getInstanceLabel({ component: name }, componentMeta), + description: componentMeta.description, + icon: componentMeta.icon, + }); + } + const metasByCategory = mapGroupBy(metas, (meta) => meta.category); + for (const meta of metasByCategory.values()) { + meta.sort((metaA, metaB) => { + return ( + (metaA.order ?? Number.MAX_SAFE_INTEGER) - + (metaB.order ?? Number.MAX_SAFE_INTEGER) + ); + }); + } + return { metasByCategory, availableComponents }; +}); + type Groups = Array<{ category: Exclude | "found"; - metas: Array; + metas: Array; }>; const filterAndGroupComponents = ({ documentType = "html", - metaByCategory, - componentNamesByMeta, + metasByCategory, search, }: { documentType?: "html" | "xml"; - metaByCategory: MetaByCategory; - componentNamesByMeta: ComponentNamesByMeta; + metasByCategory: Map>; search: string; }): Groups => { const categories = componentCategories.filter((category) => { @@ -109,42 +115,22 @@ const filterAndGroupComponents = ({ }); let groups: Groups = categories.map((category) => { - const metas = (metaByCategory.get(category) ?? []).filter((meta) => { - const component = componentNamesByMeta.get(meta); - - if (component === undefined) { - return false; - } - + const metas = (metasByCategory.get(category) ?? []).filter((meta) => { if (documentType === "xml" && meta.category === "data") { - return component === collectionComponent; + return meta.name === collectionComponent; } - - if (component === "RemixForm" && isFeatureEnabled("filters") === false) { - return false; - } - - if (component === "ContentEmbed" && isFeatureEnabled("cms") === false) { - return false; - } - - if ( - component === blockComponent && - isFeatureEnabled("contentEditableMode") === false - ) { - return false; - } - return true; }); return { category, metas }; }); - if (search.length !== 0) { - let metas = groups.map((group) => group.metas).flat(); - metas = matchComponents(metas, componentNamesByMeta, search); - groups = [{ category: "found", metas }]; + if (search.length > 0) { + const metas = groups.map((group) => group.metas).flat(); + const matched = matchSorter(metas, search, { + keys: ["label"], + }); + groups = [{ category: "found", metas: matched }]; } groups = groups.filter((group) => group.metas.length > 0); @@ -152,19 +138,13 @@ const filterAndGroupComponents = ({ return groups; }; -const findComponentIndex = ( - groups: Groups, - componentNamesByMeta: ComponentNamesByMeta, - selectedComponent?: string -) => { +const findComponentIndex = (groups: Groups, selectedComponent?: string) => { if (selectedComponent === undefined) { return { index: -1, metas: groups[0].metas }; } for (const { metas } of groups) { - const index = metas.findIndex((meta) => { - return componentNamesByMeta.get(meta) === selectedComponent; - }); + const index = metas.findIndex((meta) => meta.name === selectedComponent); if (index === -1) { continue; } @@ -181,12 +161,10 @@ export const ComponentsPanel = ({ publish: Publish; onClose: () => void; }) => { - const metaByComponentName = useStore($registeredComponentMetas); const selectedPage = useStore($selectedPage); const [selectedComponent, setSelectedComponent] = useState(); const handleInsert = (component: string) => { - onClose(); const fragment = getComponentTemplateData(component); if (fragment) { const insertable = findClosestInsertable(fragment); @@ -194,6 +172,7 @@ export const ComponentsPanel = ({ insertWebstudioFragmentAt(fragment, insertable); } } + onClose(); }; const resetSelectedComponent = () => { @@ -204,7 +183,7 @@ export const ComponentsPanel = ({ // When user didn't select a component but they have search input, // we want to always have the first component selected, so that user can just hit enter. if (selectedComponent === undefined && searchFieldProps.value) { - return componentNamesByMeta.get(groups[0].metas[0]); + return groups[0].metas[0].name; } return selectedComponent; }; @@ -221,35 +200,26 @@ export const ComponentsPanel = ({ return; } - const { index, metas } = findComponentIndex( - groups, - componentNamesByMeta, - selectedComponent - ); + const { index, metas } = findComponentIndex(groups, selectedComponent); const nextIndex = findNextListItemIndex(index, metas.length, direction); - const nextComponent = componentNamesByMeta.get(metas[nextIndex]); - + const nextComponent = metas[nextIndex]?.name; if (nextComponent) { setSelectedComponent(nextComponent); } }, }); - const { metaByCategory, componentNamesByMeta } = useMemo( - () => getMetaMaps(metaByComponentName), - [metaByComponentName] - ); + const { metasByCategory, availableComponents } = useStore($metas); const { dragCard, draggableContainerRef } = useDraggable({ publish, - metaByComponentName, + availableComponents, }); const groups = filterAndGroupComponents({ documentType: selectedPage?.meta.documentType, - metaByCategory, - componentNamesByMeta, + metasByCategory, search: searchFieldProps.value, }); @@ -296,39 +266,27 @@ export const ComponentsPanel = ({ overflow: "auto", }} > - {group.metas.map((meta: WsComponentMeta, index) => { - const component = componentNamesByMeta.get(meta); - - if (component === undefined) { - return; - } - - return ( - { - handleInsert(component); - }} - onFocus={() => { - setSelectedComponent(component); - }} - > - } - /> - - ); - })} + {group.metas.map((meta, index) => ( + handleInsert(meta.name)} + onFocus={() => setSelectedComponent(meta.name)} + > + } + /> + + ))} {dragCard} {group.metas.length === 0 && ( diff --git a/apps/builder/app/builder/features/components/get-meta-maps.test.ts b/apps/builder/app/builder/features/components/get-meta-maps.test.ts deleted file mode 100644 index f68304651bca..000000000000 --- a/apps/builder/app/builder/features/components/get-meta-maps.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { type WsComponentMeta } from "@webstudio-is/react-sdk"; -import { describe, expect, test } from "vitest"; -import { getMetaMaps } from "./get-meta-maps"; - -const metaByComponentName: Map = new Map([ - [ - "Box", - { - category: "general", - label: "Box", - type: "container", - order: 0, - icon: "", - }, - ], - [ - "Box1", - { - category: "general", - label: "Box1", - type: "container", - order: 1, - icon: "", - }, - ], -]); - -describe("getMetaMaps", () => { - test("sorts meta by order", () => { - const { metaByCategory, componentNamesByMeta } = - getMetaMaps(metaByComponentName); - expect(metaByCategory).toMatchInlineSnapshot(` - Map { - "general" => [ - { - "category": "general", - "icon": "", - "label": "Box", - "order": 0, - "type": "container", - }, - { - "category": "general", - "icon": "", - "label": "Box1", - "order": 1, - "type": "container", - }, - ], - } - `); - expect(componentNamesByMeta).toMatchInlineSnapshot(` - Map { - { - "category": "general", - "icon": "", - "label": "Box", - "order": 0, - "type": "container", - } => "Box", - { - "category": "general", - "icon": "", - "label": "Box1", - "order": 1, - "type": "container", - } => "Box1", - } - `); - }); -}); diff --git a/apps/builder/app/builder/features/components/get-meta-maps.ts b/apps/builder/app/builder/features/components/get-meta-maps.ts deleted file mode 100644 index 5127b33373bc..000000000000 --- a/apps/builder/app/builder/features/components/get-meta-maps.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { type WsComponentMeta } from "@webstudio-is/react-sdk"; - -export type MetaByCategory = Map< - WsComponentMeta["category"], - Array ->; - -export type ComponentNamesByMeta = Map; - -export const getMetaMaps = ( - metaByComponentName: Map -) => { - const metaByCategory: MetaByCategory = new Map(); - const componentNamesByMeta: ComponentNamesByMeta = new Map(); - - for (const [name, meta] of metaByComponentName) { - if (meta.category === undefined || meta.category === "hidden") { - continue; - } - let categoryMetas = metaByCategory.get(meta.category); - if (categoryMetas === undefined) { - categoryMetas = []; - metaByCategory.set(meta.category, categoryMetas); - } - categoryMetas.push(meta); - metaByComponentName.set(name, meta); - componentNamesByMeta.set(meta, name); - } - - for (const meta of metaByCategory.values()) { - meta.sort((metaA, metaB) => { - return ( - (metaA.order ?? Number.MAX_SAFE_INTEGER) - - (metaB.order ?? Number.MAX_SAFE_INTEGER) - ); - }); - } - - return { metaByCategory, componentNamesByMeta }; -}; diff --git a/apps/builder/app/builder/features/components/use-draggable.tsx b/apps/builder/app/builder/features/components/use-draggable.tsx index db6eb6ebfde2..aa4ca6805085 100644 --- a/apps/builder/app/builder/features/components/use-draggable.tsx +++ b/apps/builder/app/builder/features/components/use-draggable.tsx @@ -2,7 +2,6 @@ import { useState } from "react"; import { useStore } from "@nanostores/react"; import { createPortal } from "react-dom"; import type { Instance } from "@webstudio-is/sdk"; -import type { WsComponentMeta } from "@webstudio-is/react-sdk"; import { type Point, Flex, @@ -73,7 +72,7 @@ const toCanvasCoordinates = ( const elementToComponentName = ( element: Element, - metaByComponentName: Map + availableComponents: Set ) => { // If drag doesn't start on the button element directly but on one of its children, // we need to trace back to the button that has the data. @@ -81,7 +80,7 @@ const elementToComponentName = ( if (parentWithData instanceof HTMLElement) { const dragComponent = parentWithData.dataset.dragComponent as string; - if (metaByComponentName.has(dragComponent)) { + if (availableComponents.has(dragComponent)) { return dragComponent; } } @@ -90,10 +89,10 @@ const elementToComponentName = ( export const useDraggable = ({ publish, - metaByComponentName, + availableComponents, }: { publish: Publish; - metaByComponentName: Map; + availableComponents: Set; }) => { const [dragComponent, setDragComponent] = useState(); const [point, setPoint] = useState({ x: 0, y: 0 }); @@ -104,7 +103,7 @@ export const useDraggable = ({ const dragHandlers = useDrag({ elementToData(element) { - return elementToComponentName(element, metaByComponentName); + return elementToComponentName(element, availableComponents); }, onStart({ data: componentName }) { setDragComponent(componentName);