From 15e44f9ce2a0f033ddda63eea3129f2bc03b4041 Mon Sep 17 00:00:00 2001 From: Jesse Baker <jesse.baker@acquia.com> Date: Mon, 17 Feb 2025 15:54:13 +0000 Subject: [PATCH 01/13] proofs of concepts --- .../dynamicComponentsGroupsList.tsx | 197 ++++++++++++++++++ .../components/form/components/Accordion.tsx | 11 +- ui/src/components/topbar/Topbar.tsx | 7 +- .../components/topbar/menu/TopbarPopover.tsx | 4 +- 4 files changed, 210 insertions(+), 9 deletions(-) create mode 100644 ui/src/components/dynamicComponents/dynamicComponentsGroupsList.tsx diff --git a/ui/src/components/dynamicComponents/dynamicComponentsGroupsList.tsx b/ui/src/components/dynamicComponents/dynamicComponentsGroupsList.tsx new file mode 100644 index 0000000000..493104b84d --- /dev/null +++ b/ui/src/components/dynamicComponents/dynamicComponentsGroupsList.tsx @@ -0,0 +1,197 @@ +import { + Flex, + Heading, + Link, + Grid, + Spinner, + Text, + Box, +} from '@radix-ui/themes'; +import { + ChevronRightIcon, + ExternalLinkIcon, + LightningBoltIcon, +} from '@radix-ui/react-icons'; +import { handleNonWorkingBtn } from '@/utils/function-utils'; +import React, { useState } from 'react'; +import ErrorCard from '@/components/error/ErrorCard'; +import { useGetComponentsQuery } from '@/services/components'; +import { setOpenLayoutItem } from '@/features/ui/primaryPanelSlice'; +import { + AccordionDetails, + AccordionRoot, +} from '@/components/form/components/Accordion'; +import styles from '@/components/sidebar/Library.module.css'; +import ErrorBoundary from '@/components/error/ErrorBoundary'; +import Library from '@/components/sidebar/Library'; +import ComponentList from '@/components/list/ComponentList'; + +interface DynamicComponentsGroupsPopoverProps {} + +function toLowercaseKey(str) { + // Remove spaces and convert the entire string to lowercase + return str.replace(/\s+/g, '').toLowerCase(); +} + +const DynamicComponentsGroupsList: React.FC< + DynamicComponentsGroupsPopoverProps +> = () => { + const { data, isError, isLoading, refetch } = useGetComponentsQuery(); + + if (data) { + const dynamicComponentsGroups = Object.fromEntries( + Object.entries(data).filter(([key, value]) => + value.id.startsWith('block.'), + ), + ); + return ( + <DynamicComponentsGroupsListDisplay + dynamicComponentsGroups={dynamicComponentsGroups || []} + isLoading={isLoading} + isError={isError} + refetch={refetch} + /> + ); + } + + return <Spinner />; +}; + +interface DynamicComponentsGroupsListDisplayProps { + dynamicComponentsGroups: Object<any>; + isLoading: boolean; + isError: boolean; + refetch: () => void; +} + +const DynamicComponentsGroupsListDisplay: React.FC< + DynamicComponentsGroupsListDisplayProps +> = ({ dynamicComponentsGroups, isLoading, isError, refetch }) => { + const groupsSet = new Set(); + Object.entries(dynamicComponentsGroups).forEach(([id, group]) => { + groupsSet.add(group.category); + }); + const groups = Array.from(groupsSet).map((groupName) => { + return { name: groupName, id: toLowercaseKey(groupName) }; + }); + const [openGroups, setOpenGroups] = useState([]); + + const handleGroupClick = (groupName, open) => { + setOpenGroups((state) => { + if (!state.includes(groupName)) { + return [...state, groupName]; + } + return state.filter((stateName) => stateName !== groupName); + }); + }; + + return ( + <> + <Flex justify="between"> + <Heading as="h3" size="3" mb="4"> + Dynamic Components + </Heading> + + <Flex justify="end" asChild> + <Link + size="1" + href="" + target="_blank" + onClick={(e: React.MouseEvent<HTMLAnchorElement>) => { + e.preventDefault(); + handleNonWorkingBtn(); + }} + > + Go to CMS + <ExternalLinkIcon /> + </Link> + </Flex> + </Flex> + + <Library /> + {isError && ( + <ErrorCard + error="Cannot display dynamicComponentsGroups, please try again." + resetErrorBoundary={refetch} + resetButtonText="Try again" + title="Error loading dynamicComponentsGroups" + /> + )} + {isLoading && ( + <Flex justify="center"> + <Spinner /> + </Flex> + )} + {!isError && dynamicComponentsGroups && ( + <AccordionRoot + value={openGroups} + // onValueChange={() => setOpenLayoutItem} + > + {groups.map((group) => ( + <AccordionDetails + key={group.id} + value={group.id} + title={ + <Flex align="center"> + <Flex + style={{ + border: '1px solid #ccc', + height: '60px', + width: '60px', + borderRadius: '4px', + }} + m="2" + p="2" + justify="center" + align="center" + > + <LightningBoltIcon /> + </Flex> + <Text>{group.name}</Text> + </Flex> + } + onTriggerClick={() => handleGroupClick(group.id)} + className={styles.accordionDetails} + triggerClassName={styles.accordionDetailsTrigger} + > + <ErrorBoundary title="An unexpected error has occurred while fetching code components."> + <Text>{group.name}</Text> + <ComponentList /> + </ErrorBoundary> + </AccordionDetails> + ))} + </AccordionRoot> + )} + {/*{!isError && dynamicComponentsGroups && (*/} + {/* <Grid columns="1" gap="3">*/} + {/* {groups.map((group) => (*/} + {/* <Flex align="center">*/} + {/* <Flex*/} + {/* style={{*/} + {/* border: '1px solid #ccc',*/} + {/* height: '60px',*/} + {/* width: '60px',*/} + {/* borderRadius: '4px',*/} + {/* }}*/} + {/* m="2"*/} + {/* p="2"*/} + {/* justify="center"*/} + {/* align="center"*/} + {/* >*/} + {/* <LightningBoltIcon />*/} + {/* </Flex>*/} + {/* <Text>{group}</Text>*/} + {/* <Box ml="auto">*/} + {/* <ChevronRightIcon />*/} + {/* </Box>*/} + {/* </Flex>*/} + {/* ))}*/} + {/* </Grid>*/} + {/*)}*/} + </> + ); +}; + +export { DynamicComponentsGroupsListDisplay }; + +export default DynamicComponentsGroupsList; diff --git a/ui/src/components/form/components/Accordion.tsx b/ui/src/components/form/components/Accordion.tsx index 7852daa52d..f5448f68ea 100644 --- a/ui/src/components/form/components/Accordion.tsx +++ b/ui/src/components/form/components/Accordion.tsx @@ -58,9 +58,14 @@ const AccordionDetails = ({ className={clsx(styles.trigger, triggerClassName)} onClick={onTriggerClick} > - <Text size="2" weight="medium" {...summaryAttributes}> - {title} - </Text> + {typeof title === 'string' ? ( + <Text size="2" weight="medium" {...summaryAttributes}> + {title} + </Text> + ) : ( + title + )} + <ChevronRightIcon className={styles.chevron} aria-hidden /> </Accordion.Trigger> </Flex> diff --git a/ui/src/components/topbar/Topbar.tsx b/ui/src/components/topbar/Topbar.tsx index 783d601dec..8f303f510c 100644 --- a/ui/src/components/topbar/Topbar.tsx +++ b/ui/src/components/topbar/Topbar.tsx @@ -23,6 +23,7 @@ import type React from 'react'; import { handleNonWorkingBtn } from '@/utils/function-utils'; import TopbarPopover from '@/components/topbar/menu/TopbarPopover'; import topBarStyles from '@/components/topbar/Topbar.module.css'; +import DynamicComponentsGroupsList from '@/components/dynamicComponents/dynamicComponentsGroupsList'; const PREVIOUS_URL_STORAGE_KEY = 'XBPreviousURL'; @@ -101,16 +102,12 @@ const Topbar = () => { color="gray" size="2" className={clsx(styles.topBarButton)} - onClick={(e: React.MouseEvent<HTMLButtonElement>) => { - e.preventDefault(); - handleNonWorkingBtn(); - }} > <CMSIcon height="24" width="auto" /> </Button> } > - <Text>Not yet supported</Text> + <DynamicComponentsGroupsList /> </TopbarPopover> </Flex> <Flex align="center" justify="center" gap="2"> diff --git a/ui/src/components/topbar/menu/TopbarPopover.tsx b/ui/src/components/topbar/menu/TopbarPopover.tsx index 8cf06e9761..a33101b27f 100644 --- a/ui/src/components/topbar/menu/TopbarPopover.tsx +++ b/ui/src/components/topbar/menu/TopbarPopover.tsx @@ -22,7 +22,9 @@ const TopbarPopover: React.FC<TopbarPopoverProps> = ({ <Popover.Trigger>{trigger}</Popover.Trigger> </Tooltip> <Popover.Content asChild> - <Panel className={clsx(styles.content, 'xb-app')}>{children}</Panel> + <Panel className={clsx(styles.content, 'xb-app')} maxHeight="50vh"> + {children} + </Panel> </Popover.Content> </Popover.Root> ); -- GitLab From c7144bbafcb745300ba61dab637a6b251060b633 Mon Sep 17 00:00:00 2001 From: Jesse Baker <jesse.baker@acquia.com> Date: Fri, 21 Feb 2025 11:00:50 +0000 Subject: [PATCH 02/13] moving things about --- .../dynamicComponents/DynamicComponents.tsx | 57 +++++ .../DynamicComponentsLibrary.tsx | 79 +++++++ .../dynamicComponentsGroupsList.tsx | 197 ------------------ ui/src/components/topbar/Topbar.tsx | 4 +- 4 files changed, 138 insertions(+), 199 deletions(-) create mode 100644 ui/src/components/dynamicComponents/DynamicComponents.tsx create mode 100644 ui/src/components/dynamicComponents/DynamicComponentsLibrary.tsx delete mode 100644 ui/src/components/dynamicComponents/dynamicComponentsGroupsList.tsx diff --git a/ui/src/components/dynamicComponents/DynamicComponents.tsx b/ui/src/components/dynamicComponents/DynamicComponents.tsx new file mode 100644 index 0000000000..21a263e627 --- /dev/null +++ b/ui/src/components/dynamicComponents/DynamicComponents.tsx @@ -0,0 +1,57 @@ +import { + Flex, + Heading, + Link, + Grid, + Spinner, + Text, + Box, + ScrollArea, +} from '@radix-ui/themes'; +import { + ChevronRightIcon, + ExternalLinkIcon, + LightningBoltIcon, +} from '@radix-ui/react-icons'; +import { handleNonWorkingBtn } from '@/utils/function-utils'; +import type React from 'react'; + +import DynamicComponentsLibrary from '@/components/dynamicComponents/DynamicComponentsLibrary'; + +interface DynamicComponentsGroupsPopoverProps {} + +const DynamicComponents: React.FC<DynamicComponentsGroupsPopoverProps> = () => { + return ( + <> + <Flex justify="between"> + <Heading as="h3" size="3" mb="4"> + Dynamic Components + </Heading> + + <Flex justify="end" asChild> + <Link + size="1" + href="" + target="_blank" + onClick={(e: React.MouseEvent<HTMLAnchorElement>) => { + e.preventDefault(); + handleNonWorkingBtn(); + }} + > + Go to CMS + <ExternalLinkIcon /> + </Link> + </Flex> + </Flex> + <Box mr="-4"> + <ScrollArea style={{ maxHeight: '380px', width: '100%' }} type="scroll"> + <Box pr="4"> + <DynamicComponentsLibrary /> + </Box> + </ScrollArea> + </Box> + </> + ); +}; + +export default DynamicComponents; diff --git a/ui/src/components/dynamicComponents/DynamicComponentsLibrary.tsx b/ui/src/components/dynamicComponents/DynamicComponentsLibrary.tsx new file mode 100644 index 0000000000..b221e9bf35 --- /dev/null +++ b/ui/src/components/dynamicComponents/DynamicComponentsLibrary.tsx @@ -0,0 +1,79 @@ +import { + AccordionRoot, + AccordionDetails, +} from '@/components/form/components/Accordion'; +import ComponentList from '@/components/list/ComponentList'; +import ErrorBoundary from '@/components/error/ErrorBoundary'; +import { setOpenLayoutItem } from '@/features/ui/primaryPanelSlice'; +import styles from '@/components/sidebar/Library.module.css'; +import { useState } from 'react'; +import { useGetComponentsQuery } from '@/services/components'; + +function toLowercaseKey(str: string) { + // Remove spaces and convert the entire string to lowercase + return str.replace(/\s+/g, '').toLowerCase(); +} + +const Library = () => { + const { data, isError, isLoading, refetch } = useGetComponentsQuery(); + + const dynamicComponentsGroups = data + ? Object.fromEntries( + Object.entries(data).filter(([key, value]) => + value.id.startsWith('block.'), + ), + ) + : {}; + + console.log(dynamicComponentsGroups); + + const groupsSet = new Set<string>(); + Object.entries(dynamicComponentsGroups).forEach(([id, group]) => { + groupsSet.add(group.category); + }); + const groups = Array.from(groupsSet).map((groupName) => { + return { name: groupName, id: toLowercaseKey(groupName) }; + }); + + // const groupsSet = new Set<string>(); + // groupsSet.add('foo'); + // groupsSet.add('bar'); + // const groups = Array.from(groupsSet).map((groupName: string) => { + // return { name: groupName, id: toLowercaseKey(groupName) }; + // }); + const [openGroups, setOpenGroups] = useState<string[]>([]); + + const onClickHandler = (groupName: string) => { + setOpenGroups((state) => { + if (!state.includes(groupName)) { + return [...state, groupName]; + } + return state.filter((stateName) => stateName !== groupName); + }); + }; + + return ( + <> + <AccordionRoot value={openGroups} onValueChange={() => setOpenLayoutItem}> + {groups.map((group) => ( + <AccordionDetails + key={group.id} + value={group.id} + title={group.name} + onTriggerClick={() => onClickHandler(group.id)} + className={styles.accordionDetails} + triggerClassName={styles.accordionDetailsTrigger} + > + <ErrorBoundary + title={`An unexpected error has occurred while fetching ${group.name}.`} + > + <ComponentList /> + </ErrorBoundary> + </AccordionDetails> + ))} + </AccordionRoot> + </> + ); +}; + +export default Library; diff --git a/ui/src/components/dynamicComponents/dynamicComponentsGroupsList.tsx b/ui/src/components/dynamicComponents/dynamicComponentsGroupsList.tsx deleted file mode 100644 index 493104b84d..0000000000 --- a/ui/src/components/dynamicComponents/dynamicComponentsGroupsList.tsx +++ /dev/null @@ -1,197 +0,0 @@ -import { - Flex, - Heading, - Link, - Grid, - Spinner, - Text, - Box, -} from '@radix-ui/themes'; -import { - ChevronRightIcon, - ExternalLinkIcon, - LightningBoltIcon, -} from '@radix-ui/react-icons'; -import { handleNonWorkingBtn } from '@/utils/function-utils'; -import React, { useState } from 'react'; -import ErrorCard from '@/components/error/ErrorCard'; -import { useGetComponentsQuery } from '@/services/components'; -import { setOpenLayoutItem } from '@/features/ui/primaryPanelSlice'; -import { - AccordionDetails, - AccordionRoot, -} from '@/components/form/components/Accordion'; -import styles from '@/components/sidebar/Library.module.css'; -import ErrorBoundary from '@/components/error/ErrorBoundary'; -import Library from '@/components/sidebar/Library'; -import ComponentList from '@/components/list/ComponentList'; - -interface DynamicComponentsGroupsPopoverProps {} - -function toLowercaseKey(str) { - // Remove spaces and convert the entire string to lowercase - return str.replace(/\s+/g, '').toLowerCase(); -} - -const DynamicComponentsGroupsList: React.FC< - DynamicComponentsGroupsPopoverProps -> = () => { - const { data, isError, isLoading, refetch } = useGetComponentsQuery(); - - if (data) { - const dynamicComponentsGroups = Object.fromEntries( - Object.entries(data).filter(([key, value]) => - value.id.startsWith('block.'), - ), - ); - return ( - <DynamicComponentsGroupsListDisplay - dynamicComponentsGroups={dynamicComponentsGroups || []} - isLoading={isLoading} - isError={isError} - refetch={refetch} - /> - ); - } - - return <Spinner />; -}; - -interface DynamicComponentsGroupsListDisplayProps { - dynamicComponentsGroups: Object<any>; - isLoading: boolean; - isError: boolean; - refetch: () => void; -} - -const DynamicComponentsGroupsListDisplay: React.FC< - DynamicComponentsGroupsListDisplayProps -> = ({ dynamicComponentsGroups, isLoading, isError, refetch }) => { - const groupsSet = new Set(); - Object.entries(dynamicComponentsGroups).forEach(([id, group]) => { - groupsSet.add(group.category); - }); - const groups = Array.from(groupsSet).map((groupName) => { - return { name: groupName, id: toLowercaseKey(groupName) }; - }); - const [openGroups, setOpenGroups] = useState([]); - - const handleGroupClick = (groupName, open) => { - setOpenGroups((state) => { - if (!state.includes(groupName)) { - return [...state, groupName]; - } - return state.filter((stateName) => stateName !== groupName); - }); - }; - - return ( - <> - <Flex justify="between"> - <Heading as="h3" size="3" mb="4"> - Dynamic Components - </Heading> - - <Flex justify="end" asChild> - <Link - size="1" - href="" - target="_blank" - onClick={(e: React.MouseEvent<HTMLAnchorElement>) => { - e.preventDefault(); - handleNonWorkingBtn(); - }} - > - Go to CMS - <ExternalLinkIcon /> - </Link> - </Flex> - </Flex> - - <Library /> - {isError && ( - <ErrorCard - error="Cannot display dynamicComponentsGroups, please try again." - resetErrorBoundary={refetch} - resetButtonText="Try again" - title="Error loading dynamicComponentsGroups" - /> - )} - {isLoading && ( - <Flex justify="center"> - <Spinner /> - </Flex> - )} - {!isError && dynamicComponentsGroups && ( - <AccordionRoot - value={openGroups} - // onValueChange={() => setOpenLayoutItem} - > - {groups.map((group) => ( - <AccordionDetails - key={group.id} - value={group.id} - title={ - <Flex align="center"> - <Flex - style={{ - border: '1px solid #ccc', - height: '60px', - width: '60px', - borderRadius: '4px', - }} - m="2" - p="2" - justify="center" - align="center" - > - <LightningBoltIcon /> - </Flex> - <Text>{group.name}</Text> - </Flex> - } - onTriggerClick={() => handleGroupClick(group.id)} - className={styles.accordionDetails} - triggerClassName={styles.accordionDetailsTrigger} - > - <ErrorBoundary title="An unexpected error has occurred while fetching code components."> - <Text>{group.name}</Text> - <ComponentList /> - </ErrorBoundary> - </AccordionDetails> - ))} - </AccordionRoot> - )} - {/*{!isError && dynamicComponentsGroups && (*/} - {/* <Grid columns="1" gap="3">*/} - {/* {groups.map((group) => (*/} - {/* <Flex align="center">*/} - {/* <Flex*/} - {/* style={{*/} - {/* border: '1px solid #ccc',*/} - {/* height: '60px',*/} - {/* width: '60px',*/} - {/* borderRadius: '4px',*/} - {/* }}*/} - {/* m="2"*/} - {/* p="2"*/} - {/* justify="center"*/} - {/* align="center"*/} - {/* >*/} - {/* <LightningBoltIcon />*/} - {/* </Flex>*/} - {/* <Text>{group}</Text>*/} - {/* <Box ml="auto">*/} - {/* <ChevronRightIcon />*/} - {/* </Box>*/} - {/* </Flex>*/} - {/* ))}*/} - {/* </Grid>*/} - {/*)}*/} - </> - ); -}; - -export { DynamicComponentsGroupsListDisplay }; - -export default DynamicComponentsGroupsList; diff --git a/ui/src/components/topbar/Topbar.tsx b/ui/src/components/topbar/Topbar.tsx index 8f303f510c..e3cc568faf 100644 --- a/ui/src/components/topbar/Topbar.tsx +++ b/ui/src/components/topbar/Topbar.tsx @@ -23,7 +23,7 @@ import type React from 'react'; import { handleNonWorkingBtn } from '@/utils/function-utils'; import TopbarPopover from '@/components/topbar/menu/TopbarPopover'; import topBarStyles from '@/components/topbar/Topbar.module.css'; -import DynamicComponentsGroupsList from '@/components/dynamicComponents/dynamicComponentsGroupsList'; +import DynamicComponents from '@/components/dynamicComponents/DynamicComponents'; const PREVIOUS_URL_STORAGE_KEY = 'XBPreviousURL'; @@ -107,7 +107,7 @@ const Topbar = () => { </Button> } > - <DynamicComponentsGroupsList /> + <DynamicComponents /> </TopbarPopover> </Flex> <Flex align="center" justify="center" gap="2"> -- GitLab From 651e9b274fb815d4e6a080480862f7e8b06900c0 Mon Sep 17 00:00:00 2001 From: Jesse Baker <jesse.baker@acquia.com> Date: Fri, 21 Feb 2025 16:32:22 +0000 Subject: [PATCH 03/13] filtering the lists --- .../DynamicComponentsLibrary.tsx | 83 ++++++++++--------- ui/src/components/list/ComponentList.tsx | 9 +- ui/src/types/Component.ts | 6 ++ 3 files changed, 57 insertions(+), 41 deletions(-) diff --git a/ui/src/components/dynamicComponents/DynamicComponentsLibrary.tsx b/ui/src/components/dynamicComponents/DynamicComponentsLibrary.tsx index b221e9bf35..be251962d1 100644 --- a/ui/src/components/dynamicComponents/DynamicComponentsLibrary.tsx +++ b/ui/src/components/dynamicComponents/DynamicComponentsLibrary.tsx @@ -8,66 +8,71 @@ import { setOpenLayoutItem } from '@/features/ui/primaryPanelSlice'; import styles from '@/components/sidebar/Library.module.css'; import { useState } from 'react'; import { useGetComponentsQuery } from '@/services/components'; - -function toLowercaseKey(str: string) { - // Remove spaces and convert the entire string to lowercase - return str.replace(/\s+/g, '').toLowerCase(); -} +import List from '@/components/list/List'; const Library = () => { const { data, isError, isLoading, refetch } = useGetComponentsQuery(); - const dynamicComponentsGroups = data - ? Object.fromEntries( - Object.entries(data).filter(([key, value]) => - value.id.startsWith('block.'), - ), - ) - : {}; + // Convert dynamicComponents to an array + const dynamicComponents = data + ? Object.entries(data) + .filter(([key, value]) => value.library === 'dynamic_components') + .map(([key, value]) => ({ id: key, ...value })) // Convert to array of objects + : []; - console.log(dynamicComponentsGroups); + console.log(dynamicComponents); - const groupsSet = new Set<string>(); - Object.entries(dynamicComponentsGroups).forEach(([id, group]) => { - groupsSet.add(group.category); - }); - const groups = Array.from(groupsSet).map((groupName) => { - return { name: groupName, id: toLowercaseKey(groupName) }; + const categoriesSet = new Set<string>(); + dynamicComponents.forEach((component) => { + categoriesSet.add(component.category); }); - // const groupsSet = new Set<string>(); - // groupsSet.add('foo'); - // groupsSet.add('bar'); - // const groups = Array.from(groupsSet).map((groupName: string) => { - // return { name: groupName, id: toLowercaseKey(groupName) }; - // }); - const [openGroups, setOpenGroups] = useState<string[]>([]); + const categories = Array.from(categoriesSet) + .map((categoryName) => { + return { + name: categoryName.replace(/\w/, (c) => c.toUpperCase()), + id: categoryName, + }; + }) + .sort((a, b) => a.name.localeCompare(b.name)); + + const [opencategories, setOpencategories] = useState<string[]>([]); - const onClickHandler = (groupName: string) => { - setOpenGroups((state) => { - if (!state.includes(groupName)) { - return [...state, groupName]; + const onClickHandler = (categoryName: string) => { + setOpencategories((state) => { + if (!state.includes(categoryName)) { + return [...state, categoryName]; } - return state.filter((stateName) => stateName !== groupName); + return state.filter((stateName) => stateName !== categoryName); }); }; return ( <> - <AccordionRoot value={openGroups} onValueChange={() => setOpenLayoutItem}> - {groups.map((group) => ( + <AccordionRoot + value={opencategories} + onValueChange={() => setOpenLayoutItem} + > + {categories.map((category) => ( <AccordionDetails - key={group.id} - value={group.id} - title={group.name} - onTriggerClick={() => onClickHandler(group.id)} + key={category.id} + value={category.id} + title={category.name} + onTriggerClick={() => onClickHandler(category.id)} className={styles.accordionDetails} triggerClassName={styles.accordionDetailsTrigger} > <ErrorBoundary - title={`An unexpected error has occurred while fetching ${group.name}.`} + title={`An unexpected error has occurred while fetching ${category.name}.`} > - <ComponentList /> + <List + items={dynamicComponents.filter( + (c) => c.category === category.id, + )} + isLoading={isLoading} + type="component" + label="Components" + /> </ErrorBoundary> </AccordionDetails> ))} diff --git a/ui/src/components/list/ComponentList.tsx b/ui/src/components/list/ComponentList.tsx index eb40cff78b..eed1e7d38d 100644 --- a/ui/src/components/list/ComponentList.tsx +++ b/ui/src/components/list/ComponentList.tsx @@ -14,8 +14,13 @@ const ComponentList = () => { } }, [error, showBoundary]); - const sortedComponents = components - ? toArray(components).sort((a, b) => a.name.localeCompare(b.name)) + // @todo This currently is a little too basic - it just renders everything except the dynamic_components + const filteredComponents = toArray(components).filter((component) => { + return component.library !== 'dynamic_components'; + }); + + const sortedComponents = filteredComponents + ? toArray(filteredComponents).sort((a, b) => a.name.localeCompare(b.name)) : {}; return ( diff --git a/ui/src/types/Component.ts b/ui/src/types/Component.ts index 5693cff6d0..a0fbb44fd9 100644 --- a/ui/src/types/Component.ts +++ b/ui/src/types/Component.ts @@ -32,6 +32,12 @@ export interface SimpleComponent { js_footer: string; // The source plugin label. source: string; + category: string; + library: + | 'primary_components' + | 'dynamic_components' + | 'elements' + | 'extension_components'; } export interface PropSourceComponent extends SimpleComponent { -- GitLab From 6bd31aa12f748296558316501eba49f0e472acf9 Mon Sep 17 00:00:00 2001 From: Jesse Baker <jesse.baker@acquia.com> Date: Mon, 24 Feb 2025 11:02:17 +0000 Subject: [PATCH 04/13] Big sort out of types for all the different things coming from API --- ui/src/components/ComponentPreview.tsx | 12 +++--- .../DynamicComponentsLibrary.tsx | 28 +++++++------ ui/src/components/list/ExposedJsComponent.tsx | 6 +-- ui/src/components/list/List.tsx | 28 ++----------- ui/src/components/list/ListItem.tsx | 23 +++++------ ui/src/hooks/usePreviewSortable.ts | 30 +++++++------- ui/src/services/sections.ts | 3 +- ui/src/types/Component.ts | 39 ++++++++++++------- ui/src/types/Section.ts | 10 +++-- 9 files changed, 89 insertions(+), 90 deletions(-) diff --git a/ui/src/components/ComponentPreview.tsx b/ui/src/components/ComponentPreview.tsx index fb33b82f4c..0a682c04db 100644 --- a/ui/src/components/ComponentPreview.tsx +++ b/ui/src/components/ComponentPreview.tsx @@ -3,13 +3,15 @@ import { useEffect } from 'react'; import styles from './ComponentPreview.module.css'; import clsx from 'clsx'; import Panel from '@/components/Panel'; -import type { - ComponentListItem, - SectionListItem, -} from '@/components/list/List'; +import type { Component } from '@/types/Component'; +import type { Section } from '@/types/Section'; +// import type { +// ComponentListItem, +// SectionListItem, +// } from '@/components/list/List'; interface ComponentPreviewProps { - componentListItem: ComponentListItem | SectionListItem; + componentListItem: Component | Section; } const { drupalSettings } = window; diff --git a/ui/src/components/dynamicComponents/DynamicComponentsLibrary.tsx b/ui/src/components/dynamicComponents/DynamicComponentsLibrary.tsx index be251962d1..6bb6fc5c59 100644 --- a/ui/src/components/dynamicComponents/DynamicComponentsLibrary.tsx +++ b/ui/src/components/dynamicComponents/DynamicComponentsLibrary.tsx @@ -2,28 +2,31 @@ import { AccordionRoot, AccordionDetails, } from '@/components/form/components/Accordion'; -import ComponentList from '@/components/list/ComponentList'; import ErrorBoundary from '@/components/error/ErrorBoundary'; import { setOpenLayoutItem } from '@/features/ui/primaryPanelSlice'; import styles from '@/components/sidebar/Library.module.css'; import { useState } from 'react'; -import { useGetComponentsQuery } from '@/services/components'; +import { useGetComponentsQuery } from '@/services/componentAndLayout'; import List from '@/components/list/List'; +import type { ComponentsList } from '@/types/Component'; const Library = () => { const { data, isError, isLoading, refetch } = useGetComponentsQuery(); - // Convert dynamicComponents to an array - const dynamicComponents = data - ? Object.entries(data) - .filter(([key, value]) => value.library === 'dynamic_components') - .map(([key, value]) => ({ id: key, ...value })) // Convert to array of objects - : []; + // Filter and maintain dynamicComponents as a keyed object + const dynamicComponents: ComponentsList = data + ? Object.fromEntries( + Object.entries(data).filter( + ([key, value]) => + value.library && value.library === 'dynamic_components', + ), + ) + : {}; console.log(dynamicComponents); const categoriesSet = new Set<string>(); - dynamicComponents.forEach((component) => { + Object.values(dynamicComponents).forEach((component) => { categoriesSet.add(component.category); }); @@ -66,8 +69,11 @@ const Library = () => { title={`An unexpected error has occurred while fetching ${category.name}.`} > <List - items={dynamicComponents.filter( - (c) => c.category === category.id, + // Pass filtered dynamicComponents directly + items={Object.fromEntries( + Object.entries(dynamicComponents).filter( + ([key, component]) => component.category === category.id, + ), )} isLoading={isLoading} type="component" diff --git a/ui/src/components/list/ExposedJsComponent.tsx b/ui/src/components/list/ExposedJsComponent.tsx index 41e59d5491..bec622c064 100644 --- a/ui/src/components/list/ExposedJsComponent.tsx +++ b/ui/src/components/list/ExposedJsComponent.tsx @@ -1,4 +1,3 @@ -import type { ComponentListItem } from '@/components/list/List'; import SidebarNode from '@/components/sidebar/SidebarNode'; import type React from 'react'; import { useEffect } from 'react'; @@ -18,6 +17,7 @@ import type { CodeComponentSerialized } from '@/types/CodeComponent'; import { selectLayout } from '@/features/layout/layoutModelSlice'; import { componentExistsInLayout } from '@/features/layout/layoutUtils'; import { useErrorBoundary } from 'react-error-boundary'; +import type { JSComponent } from '@/types/Component'; function removeJsPrefix(input: string): string { if (input.startsWith('js.')) { @@ -26,9 +26,7 @@ function removeJsPrefix(input: string): string { return input; } -const ExposedJsComponent: React.FC<{ component: ComponentListItem }> = ( - props, -) => { +const ExposedJsComponent: React.FC<{ component: JSComponent }> = (props) => { const dispatch = useAppDispatch(); const { component } = props; const machineName = removeJsPrefix(component.id); diff --git a/ui/src/components/list/List.tsx b/ui/src/components/list/List.tsx index 8f00d90570..e6aa217ccd 100644 --- a/ui/src/components/list/List.tsx +++ b/ui/src/components/list/List.tsx @@ -11,42 +11,20 @@ import { useAppDispatch, useAppSelector } from '@/app/hooks'; import { Box, Flex, Spinner } from '@radix-ui/themes'; import clsx from 'clsx'; import ListItem from '@/components/list/ListItem'; -import type { LayoutModelPiece } from '@/features/layout/layoutModelSlice'; import { isDropTargetBetweenTwoElementsOfSameComponent, isDropTargetInSlotAllowedByEdgeDistance, } from '@/features/sortable/sortableUtils'; -import type { TransformConfig } from '@/utils/transforms'; +import type { ComponentsList } from '@/types/Component'; +import { SectionsList } from '@/types/Section'; export interface ListProps { - items: ListData | undefined; + items: ComponentsList | SectionsList | undefined; isLoading: boolean; type: 'component' | 'section'; label: string; } -export interface ListItemBase { - id: string; - name: string; - metadata: Record<string, any>; - default_markup: string; - css: string; - js_header: string; - js_footer: string; -} -export interface ComponentListItem extends ListItemBase { - field_data: Record<string, any>; - source: string; - transforms: TransformConfig; -} -export interface SectionListItem extends ListItemBase { - layoutModel: LayoutModelPiece; -} - -interface ListData { - [key: string]: ComponentListItem | SectionListItem; -} - const List: React.FC<ListProps> = (props) => { const { items, isLoading, type } = props; const dispatch = useAppDispatch(); diff --git a/ui/src/components/list/ListItem.tsx b/ui/src/components/list/ListItem.tsx index 9e7f5d9d96..f631c1bedd 100644 --- a/ui/src/components/list/ListItem.tsx +++ b/ui/src/components/list/ListItem.tsx @@ -1,8 +1,6 @@ import type React from 'react'; -import type { - ComponentListItem, - SectionListItem, -} from '@/components/list/List'; +import type { Component, JSComponent } from '@/types/Component'; +import type { Section } from '@/types/Section'; import { useState } from 'react'; import clsx from 'clsx'; import styles from '@/components/list/List.module.css'; @@ -24,14 +22,14 @@ import useXbParams from '@/hooks/useXbParams'; import { DEFAULT_REGION } from '@/features/ui/uiSlice'; const ListItem: React.FC<{ - item: ComponentListItem | SectionListItem; + item: Component | Section; type: 'component' | 'section'; }> = (props) => { const { item, type } = props; const dispatch = useAppDispatch(); const layout = useAppSelector(selectLayout); const [previewingComponent, setPreviewingComponent] = useState< - ComponentListItem | SectionListItem + Component | Section >(); const { componentId: selectedComponent, @@ -55,7 +53,7 @@ const ListItem: React.FC<{ addNewComponentToLayout( { to: newPath, - component: item as ComponentListItem, + component: item as Component, }, setSelectedComponent, ), @@ -65,7 +63,7 @@ const ListItem: React.FC<{ addNewSectionToLayout( { to: newPath, - layoutModel: (item as SectionListItem).layoutModel, + layoutModel: (item as Section).layoutModel, }, setSelectedComponent, ), @@ -74,23 +72,22 @@ const ListItem: React.FC<{ } }; - const handleMouseEnter = (component: ComponentListItem | SectionListItem) => { + const handleMouseEnter = (component: Component | Section) => { setPreviewingComponent(component); }; const renderItem = () => { if ( type === 'component' && - (item as ComponentListItem).source === 'Code component' + (item as JSComponent).source === 'Code component' ) { - return <ExposedJsComponent component={item as ComponentListItem} />; + return <ExposedJsComponent component={item as JSComponent} />; } return ( <SidebarNode title={item.name} variant={ - type === 'component' && - (item as ComponentListItem).source === 'Blocks' + type === 'component' && (item as Component).source === 'Blocks' ? 'blockComponent' : type } diff --git a/ui/src/hooks/usePreviewSortable.ts b/ui/src/hooks/usePreviewSortable.ts index d458f34fb9..fea63b9414 100644 --- a/ui/src/hooks/usePreviewSortable.ts +++ b/ui/src/hooks/usePreviewSortable.ts @@ -115,19 +115,23 @@ function usePreviewSortable( ); } } else if (type === 'section') { - // Adding a section template. - ev.item.innerHTML = '<p>Loading section...</p>'; - dispatch( - addNewSectionToLayout( - { - to: newPath, - layoutModel: - sectionsRef?.current?.[ev.clone.dataset.xbComponentId] - .layoutModel, - }, - setSelectedComponent, - ), - ); + if ( + sectionsRef?.current?.[ev.clone.dataset.xbComponentId].layoutModel + ) { + // Adding a section template. + ev.item.innerHTML = '<p>Loading section...</p>'; + dispatch( + addNewSectionToLayout( + { + to: newPath, + layoutModel: + sectionsRef.current[ev.clone.dataset.xbComponentId] + .layoutModel, + }, + setSelectedComponent, + ), + ); + } } } } diff --git a/ui/src/services/sections.ts b/ui/src/services/sections.ts index cbd07afa3e..707f271c80 100644 --- a/ui/src/services/sections.ts +++ b/ui/src/services/sections.ts @@ -2,6 +2,7 @@ import { createApi } from '@reduxjs/toolkit/query/react'; import { baseQuery } from '@/services/baseQuery'; import type { LayoutModelPiece } from '@/features/layout/layoutModelSlice'; +import type { SectionsList } from '@/types/Section'; interface SaveSectionData extends LayoutModelPiece { name: string; @@ -13,7 +14,7 @@ export const sectionApi = createApi({ baseQuery, tagTypes: ['Sections'], endpoints: (builder) => ({ - getSections: builder.query<any, void>({ + getSections: builder.query<SectionsList, void>({ query: () => `/xb/api/config/pattern`, providesTags: () => [{ type: 'Sections', id: 'LIST' }], }), diff --git a/ui/src/types/Component.ts b/ui/src/types/Component.ts index a0fbb44fd9..cab4eadf2c 100644 --- a/ui/src/types/Component.ts +++ b/ui/src/types/Component.ts @@ -23,24 +23,34 @@ export interface FieldDataItem { [x: string | number | symbol]: unknown; } -export interface SimpleComponent { - name: string; +interface BaseComponent { id: string; + name: string; + library: string; + category: string; + source: string; default_markup: string; css: string; js_header: string; js_footer: string; - // The source plugin label. - source: string; - category: string; - library: - | 'primary_components' - | 'dynamic_components' - | 'elements' - | 'extension_components'; } -export interface PropSourceComponent extends SimpleComponent { +// BlockComponent Interface +export interface BlockComponent extends BaseComponent { + library: 'dynamic_components'; +} + +// JSComponent Interface +export interface JSComponent extends BaseComponent { + library: 'primary_components'; + source: 'Code component'; + dynamic_prop_source_candidates: any[]; + transforms: any[]; +} + +// PropSourceComponent Interface +export interface PropSourceComponent extends BaseComponent { + library: 'elements' | 'extension_components'; // @todo rename this to propSources - https://www.drupal.org/project/experience_builder/issues/3504421 field_data: FieldData; metadata: { @@ -52,12 +62,13 @@ export interface PropSourceComponent extends SimpleComponent { }; [key: string]: any; }; - source: string; + dynamic_prop_source_candidates: Record<string, any>; transforms: TransformConfig; } +// Union type for any component +export type Component = BlockComponent | JSComponent | PropSourceComponent; -export type Component = PropSourceComponent | SimpleComponent; - +// ComponentsList type representing the API response export interface ComponentsList { [key: string]: Component; } diff --git a/ui/src/types/Section.ts b/ui/src/types/Section.ts index 250d57d392..f26fb258d6 100644 --- a/ui/src/types/Section.ts +++ b/ui/src/types/Section.ts @@ -1,14 +1,16 @@ -import type { LayoutModelSliceState } from '@/features/layout/layoutModelSlice'; -import type { Component } from '@/types/Component'; +import type { LayoutModelPiece } from '@/features/layout/layoutModelSlice'; export interface Section { + layoutModel: LayoutModelPiece; name: string; id: string; default_markup: string; - components: Component[]; - layoutModel: LayoutModelSliceState; + css: string; + js_header: string; + js_footer: string; } +// Type for the API response, an object keyed by section ID export interface SectionsList { [key: string]: Section; } -- GitLab From 240ebee6a0855176786919ca5b7d2da8962d9b2c Mon Sep 17 00:00:00 2001 From: Jesse Baker <jesse.baker@acquia.com> Date: Mon, 24 Feb 2025 11:03:53 +0000 Subject: [PATCH 05/13] Rename Componnet to XBComponent. Too many things are called component in React land --- ui/src/components/ComponentPreview.tsx | 4 ++-- ui/src/components/DummyPropsEditForm.tsx | 4 ++-- ui/src/components/form/inputBehaviors.tsx | 4 ++-- ui/src/components/list/ListItem.tsx | 12 ++++++------ ui/src/features/layout/layoutModelSlice.ts | 6 +++--- ui/src/hooks/useGetComponentName.ts | 4 ++-- ui/src/types/Component.ts | 8 ++++---- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/ui/src/components/ComponentPreview.tsx b/ui/src/components/ComponentPreview.tsx index 0a682c04db..b1a0c0f1ff 100644 --- a/ui/src/components/ComponentPreview.tsx +++ b/ui/src/components/ComponentPreview.tsx @@ -3,7 +3,7 @@ import { useEffect } from 'react'; import styles from './ComponentPreview.module.css'; import clsx from 'clsx'; import Panel from '@/components/Panel'; -import type { Component } from '@/types/Component'; +import type { XBComponent } from '@/types/Component'; import type { Section } from '@/types/Section'; // import type { // ComponentListItem, @@ -11,7 +11,7 @@ import type { Section } from '@/types/Section'; // } from '@/components/list/List'; interface ComponentPreviewProps { - componentListItem: Component | Section; + componentListItem: XBComponent | Section; } const { drupalSettings } = window; diff --git a/ui/src/components/DummyPropsEditForm.tsx b/ui/src/components/DummyPropsEditForm.tsx index d95db000f6..33e8c04f9e 100644 --- a/ui/src/components/DummyPropsEditForm.tsx +++ b/ui/src/components/DummyPropsEditForm.tsx @@ -21,7 +21,7 @@ import { useDrupalBehaviors } from '@/hooks/useDrupalBehaviors'; import useXbParams from '@/hooks/useXbParams'; import { clearFieldValues } from '@/features/form/formStateSlice'; import type { FieldData } from '@/types/Component'; -import type { Component } from '@/types/Component'; +import type { XBComponent } from '@/types/Component'; import { componentHasFieldData } from '@/types/Component'; import type { AjaxUpdateFormStateEvent } from '@/types/Ajax'; import { AJAX_UPDATE_FORM_STATE_EVENT } from '@/types/Ajax'; @@ -229,7 +229,7 @@ const DummyPropsEditForm: React.FC<DummyPropsEditFormProps> = () => { const buildPreparedModel = ( model: ComponentModel, - component: Component, + component: XBComponent, ): ComponentModel => { if (!componentHasFieldData(component)) { return model; diff --git a/ui/src/components/form/inputBehaviors.tsx b/ui/src/components/form/inputBehaviors.tsx index c890cd7e4c..68461d5f18 100644 --- a/ui/src/components/form/inputBehaviors.tsx +++ b/ui/src/components/form/inputBehaviors.tsx @@ -42,7 +42,7 @@ import { clearFieldError, } from '@/features/form/formStateSlice'; import type { ErrorObject } from 'ajv/dist/types'; -import type { Component } from '@/types/Component'; +import type { XBComponent } from '@/types/Component'; import { componentHasFieldData } from '@/types/Component'; import { FORM_TYPES } from '@/features/form/constants'; import { useUpdateComponentMutation } from '@/services/preview'; @@ -462,7 +462,7 @@ export default InputBehaviors; export const syncPropSourcesToResolvedValues = ( sources: Sources, - component: Component, + component: XBComponent, resolvedValues: ResolvedValues, ): Sources => { if (!componentHasFieldData(component)) { diff --git a/ui/src/components/list/ListItem.tsx b/ui/src/components/list/ListItem.tsx index f631c1bedd..a4d998a0d2 100644 --- a/ui/src/components/list/ListItem.tsx +++ b/ui/src/components/list/ListItem.tsx @@ -1,5 +1,5 @@ import type React from 'react'; -import type { Component, JSComponent } from '@/types/Component'; +import type { XBComponent, JSComponent } from '@/types/Component'; import type { Section } from '@/types/Section'; import { useState } from 'react'; import clsx from 'clsx'; @@ -22,14 +22,14 @@ import useXbParams from '@/hooks/useXbParams'; import { DEFAULT_REGION } from '@/features/ui/uiSlice'; const ListItem: React.FC<{ - item: Component | Section; + item: XBComponent | Section; type: 'component' | 'section'; }> = (props) => { const { item, type } = props; const dispatch = useAppDispatch(); const layout = useAppSelector(selectLayout); const [previewingComponent, setPreviewingComponent] = useState< - Component | Section + XBComponent | Section >(); const { componentId: selectedComponent, @@ -53,7 +53,7 @@ const ListItem: React.FC<{ addNewComponentToLayout( { to: newPath, - component: item as Component, + component: item as XBComponent, }, setSelectedComponent, ), @@ -72,7 +72,7 @@ const ListItem: React.FC<{ } }; - const handleMouseEnter = (component: Component | Section) => { + const handleMouseEnter = (component: XBComponent | Section) => { setPreviewingComponent(component); }; @@ -87,7 +87,7 @@ const ListItem: React.FC<{ <SidebarNode title={item.name} variant={ - type === 'component' && (item as Component).source === 'Blocks' + type === 'component' && (item as XBComponent).source === 'Blocks' ? 'blockComponent' : type } diff --git a/ui/src/features/layout/layoutModelSlice.ts b/ui/src/features/layout/layoutModelSlice.ts index 9a211faa22..f810216053 100644 --- a/ui/src/features/layout/layoutModelSlice.ts +++ b/ui/src/features/layout/layoutModelSlice.ts @@ -1,6 +1,6 @@ // cspell:ignore uuidv import type { AppDispatch, RootState } from '@/app/store'; -import type { Component } from '@/types/Component'; +import type { XBComponent } from '@/types/Component'; import { componentHasFieldData } from '@/types/Component'; import type { UUID } from '@/types/UUID'; import type { PayloadAction } from '@reduxjs/toolkit'; @@ -112,7 +112,7 @@ type InsertMultipleNodesPayload = { type AddNewNodePayload = { to: number[]; - component: Component; + component: XBComponent; }; type AddNewSectionPayload = { @@ -403,7 +403,7 @@ export const addNewComponentToLayout = (dispatch: AppDispatch) => { const { to, component } = payload; // Populate the model data with the default values - const buildInitialData = (component: Component): ComponentModel => { + const buildInitialData = (component: XBComponent): ComponentModel => { if (componentHasFieldData(component)) { const initialData: EvaluatedComponentModel = { name: component.name, diff --git a/ui/src/hooks/useGetComponentName.ts b/ui/src/hooks/useGetComponentName.ts index c369e28a8c..df82a887f3 100644 --- a/ui/src/hooks/useGetComponentName.ts +++ b/ui/src/hooks/useGetComponentName.ts @@ -4,7 +4,7 @@ import type { LayoutChildNode, LayoutNode, } from '@/features/layout/layoutModelSlice'; -import type { Component } from '@/types/Component'; +import type { XBComponent } from '@/types/Component'; import { componentHasFieldData } from '@/types/Component'; const useGetComponentName = ( @@ -15,7 +15,7 @@ const useGetComponentName = ( const findPresentationSlotName = ( slotName: string | undefined, - parentComponent: Component, + parentComponent: XBComponent, ) => { if ( slotName && diff --git a/ui/src/types/Component.ts b/ui/src/types/Component.ts index cab4eadf2c..961eec2ca3 100644 --- a/ui/src/types/Component.ts +++ b/ui/src/types/Component.ts @@ -66,17 +66,17 @@ export interface PropSourceComponent extends BaseComponent { transforms: TransformConfig; } // Union type for any component -export type Component = BlockComponent | JSComponent | PropSourceComponent; +export type XBComponent = BlockComponent | JSComponent | PropSourceComponent; // ComponentsList type representing the API response export interface ComponentsList { - [key: string]: Component; + [key: string]: XBComponent; } /** * Type predicate. * - * @param {Component | undefined} component + * @param {XBComponent | undefined} component * Component to test. * * @return boolean @@ -85,7 +85,7 @@ export interface ComponentsList { * @todo rename this to componentHasPropSources in https://www.drupal.org/project/experience_builder/issues/3504421 */ export const componentHasFieldData = ( - component: Component | undefined, + component: XBComponent | undefined, ): component is PropSourceComponent => { return component !== undefined && 'field_data' in component; }; -- GitLab From 3c1c5539ae17f5829216303f51f11b0921e420ba Mon Sep 17 00:00:00 2001 From: Jesse Baker <jesse.baker@acquia.com> Date: Mon, 24 Feb 2025 11:12:57 +0000 Subject: [PATCH 06/13] small refactor --- .../dynamicComponents/DynamicComponents.tsx | 2 +- .../DynamicComponentsLibrary.tsx | 15 ++++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/ui/src/components/dynamicComponents/DynamicComponents.tsx b/ui/src/components/dynamicComponents/DynamicComponents.tsx index 21a263e627..8be61de5e1 100644 --- a/ui/src/components/dynamicComponents/DynamicComponents.tsx +++ b/ui/src/components/dynamicComponents/DynamicComponents.tsx @@ -45,7 +45,7 @@ const DynamicComponents: React.FC<DynamicComponentsGroupsPopoverProps> = () => { </Flex> <Box mr="-4"> <ScrollArea style={{ maxHeight: '380px', width: '100%' }} type="scroll"> - <Box pr="4"> + <Box pr="4" mt="3"> <DynamicComponentsLibrary /> </Box> </ScrollArea> diff --git a/ui/src/components/dynamicComponents/DynamicComponentsLibrary.tsx b/ui/src/components/dynamicComponents/DynamicComponentsLibrary.tsx index 6bb6fc5c59..ed40f7074c 100644 --- a/ui/src/components/dynamicComponents/DynamicComponentsLibrary.tsx +++ b/ui/src/components/dynamicComponents/DynamicComponentsLibrary.tsx @@ -11,9 +11,10 @@ import List from '@/components/list/List'; import type { ComponentsList } from '@/types/Component'; const Library = () => { - const { data, isError, isLoading, refetch } = useGetComponentsQuery(); + const { data, isLoading } = useGetComponentsQuery(); + const [openCategories, setOpenCategories] = useState<string[]>([]); - // Filter and maintain dynamicComponents as a keyed object + // Filter only dynamic_components const dynamicComponents: ComponentsList = data ? Object.fromEntries( Object.entries(data).filter( @@ -23,8 +24,6 @@ const Library = () => { ) : {}; - console.log(dynamicComponents); - const categoriesSet = new Set<string>(); Object.values(dynamicComponents).forEach((component) => { categoriesSet.add(component.category); @@ -39,10 +38,8 @@ const Library = () => { }) .sort((a, b) => a.name.localeCompare(b.name)); - const [opencategories, setOpencategories] = useState<string[]>([]); - const onClickHandler = (categoryName: string) => { - setOpencategories((state) => { + setOpenCategories((state) => { if (!state.includes(categoryName)) { return [...state, categoryName]; } @@ -53,7 +50,7 @@ const Library = () => { return ( <> <AccordionRoot - value={opencategories} + value={openCategories} onValueChange={() => setOpenLayoutItem} > {categories.map((category) => ( @@ -69,7 +66,7 @@ const Library = () => { title={`An unexpected error has occurred while fetching ${category.name}.`} > <List - // Pass filtered dynamicComponents directly + // filtered dynamicComponents that match the current category items={Object.fromEntries( Object.entries(dynamicComponents).filter( ([key, component]) => component.category === category.id, -- GitLab From f81d84422cbf740ca77469961f92884cf0cac5b0 Mon Sep 17 00:00:00 2001 From: Jesse Baker <jesse.baker@acquia.com> Date: Mon, 24 Feb 2025 14:42:46 +0000 Subject: [PATCH 07/13] Fixed tests and lint --- .../dynamicComponents/DynamicComponents.tsx | 17 +------- ui/src/components/list/List.tsx | 2 +- ui/src/components/topbar/Topbar.tsx | 3 -- ui/tests/e2e/extension.cy.js | 43 +++++++++++-------- 4 files changed, 27 insertions(+), 38 deletions(-) diff --git a/ui/src/components/dynamicComponents/DynamicComponents.tsx b/ui/src/components/dynamicComponents/DynamicComponents.tsx index 8be61de5e1..028c9c97da 100644 --- a/ui/src/components/dynamicComponents/DynamicComponents.tsx +++ b/ui/src/components/dynamicComponents/DynamicComponents.tsx @@ -1,18 +1,5 @@ -import { - Flex, - Heading, - Link, - Grid, - Spinner, - Text, - Box, - ScrollArea, -} from '@radix-ui/themes'; -import { - ChevronRightIcon, - ExternalLinkIcon, - LightningBoltIcon, -} from '@radix-ui/react-icons'; +import { Flex, Heading, Link, Box, ScrollArea } from '@radix-ui/themes'; +import { ExternalLinkIcon } from '@radix-ui/react-icons'; import { handleNonWorkingBtn } from '@/utils/function-utils'; import type React from 'react'; diff --git a/ui/src/components/list/List.tsx b/ui/src/components/list/List.tsx index e6aa217ccd..df59c2ed16 100644 --- a/ui/src/components/list/List.tsx +++ b/ui/src/components/list/List.tsx @@ -16,7 +16,7 @@ import { isDropTargetInSlotAllowedByEdgeDistance, } from '@/features/sortable/sortableUtils'; import type { ComponentsList } from '@/types/Component'; -import { SectionsList } from '@/types/Section'; +import type { SectionsList } from '@/types/Section'; export interface ListProps { items: ComponentsList | SectionsList | undefined; diff --git a/ui/src/components/topbar/Topbar.tsx b/ui/src/components/topbar/Topbar.tsx index 225d614751..c1bf75eacb 100644 --- a/ui/src/components/topbar/Topbar.tsx +++ b/ui/src/components/topbar/Topbar.tsx @@ -5,7 +5,6 @@ import { Flex, Grid, SegmentedControl, - Text, Tooltip, } from '@radix-ui/themes'; import Panel from '@/components/Panel'; @@ -19,8 +18,6 @@ import clsx from 'clsx'; import UnpublishedChanges from '@/components/review/UnpublishedChanges'; import PageInfo from '../pageInfo/PageInfo'; import ExtensionsList from '@/components/extensions/ExtensionsList'; -import type React from 'react'; -import { handleNonWorkingBtn } from '@/utils/function-utils'; import TopbarPopover from '@/components/topbar/menu/TopbarPopover'; import topBarStyles from '@/components/topbar/Topbar.module.css'; import DynamicComponents from '@/components/dynamicComponents/DynamicComponents'; diff --git a/ui/tests/e2e/extension.cy.js b/ui/tests/e2e/extension.cy.js index 5a26d4cccb..c0491b4e26 100644 --- a/ui/tests/e2e/extension.cy.js +++ b/ui/tests/e2e/extension.cy.js @@ -22,30 +22,35 @@ describe('extending experience builder', () => { // component select dropdown provided by the extension. cy.get('.primaryPanelContent [data-state="open"] [data-xb-name]').then( ($components) => { + const availableComponents = []; + $components.each((index, item) => { - availableComponents.push(item.textContent); + availableComponents.push(item.textContent.trim()); + }); + + cy.findByTestId('ex-select-component').then(($select) => { + const extensionComponents = []; + // Get all the items with values in the extension component list, which + // will be compared to the component list from the XB UI. + $select.find('option').each((index, item) => { + if (item.value) { + extensionComponents.push(item.textContent.trim()); + } + }); + + // Check if every available component is included in the extension components + const allAvailableComponentsExist = availableComponents.every( + (component) => extensionComponents.includes(component), + ); + + expect( + allAvailableComponentsExist, + 'All library components exist in the extension component dropdown', + ).to.be.true; }); }, ); - cy.findByTestId('ex-select-component').then(($select) => { - const extensionComponents = []; - // Get all the items with values in the extension component list, which - // will be compared to the component list from the XB UI. - $select.find('option').each((index, item) => { - if (item.value) { - extensionComponents.push(item.textContent); - } - }); - - // Comparing these two arrays as strings works reliably was opposed to - // deep equal. - expect( - extensionComponents.sort().join(), - 'The extension provides a components dropdown with every available component in the XB UI', - ).to.equal(availableComponents.sort().join()); - }); - cy.log( 'Confirm that an extension can select an item in the layout, focus it, then delete it', ); -- GitLab From bdc80c6f91b5b35a570ae0dbb9d9617965c2e1e2 Mon Sep 17 00:00:00 2001 From: Jesse Baker <jesse.baker@acquia.com> Date: Mon, 24 Feb 2025 15:50:00 +0000 Subject: [PATCH 08/13] lint fix --- ui/tests/e2e/extension.cy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/tests/e2e/extension.cy.js b/ui/tests/e2e/extension.cy.js index c0491b4e26..1959ca20f9 100644 --- a/ui/tests/e2e/extension.cy.js +++ b/ui/tests/e2e/extension.cy.js @@ -15,7 +15,7 @@ describe('extending experience builder', () => { it('Insert, focus, delete a component', () => { cy.loadURLandWaitForXBLoaded(); cy.openLibraryPanel(); - const availableComponents = []; + cy.get('.primaryPanelContent [data-state="open"]').contains('Components'); // Get the components list from the sidebar so it can be compared to the -- GitLab From 34e409f1e1726db25076f69f84275ba74852e704 Mon Sep 17 00:00:00 2001 From: Jesse Baker <jesse.baker@acquia.com> Date: Tue, 25 Feb 2025 16:26:48 +0000 Subject: [PATCH 09/13] tidy, and fixed alphabetical sorting of items in library and dynamic components dropdown --- ui/src/components/ComponentPreview.tsx | 4 ---- ui/src/components/list/ComponentList.tsx | 24 +++++++++++++++++------- ui/src/components/list/List.tsx | 15 ++++++++++++--- ui/src/types/Component.ts | 2 +- 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/ui/src/components/ComponentPreview.tsx b/ui/src/components/ComponentPreview.tsx index b1a0c0f1ff..fc0141c1f7 100644 --- a/ui/src/components/ComponentPreview.tsx +++ b/ui/src/components/ComponentPreview.tsx @@ -5,10 +5,6 @@ import clsx from 'clsx'; import Panel from '@/components/Panel'; import type { XBComponent } from '@/types/Component'; import type { Section } from '@/types/Section'; -// import type { -// ComponentListItem, -// SectionListItem, -// } from '@/components/list/List'; interface ComponentPreviewProps { componentListItem: XBComponent | Section; diff --git a/ui/src/components/list/ComponentList.tsx b/ui/src/components/list/ComponentList.tsx index a258223c29..b28664fde4 100644 --- a/ui/src/components/list/ComponentList.tsx +++ b/ui/src/components/list/ComponentList.tsx @@ -3,6 +3,7 @@ import { useErrorBoundary } from 'react-error-boundary'; import { useGetComponentsQuery } from '@/services/componentAndLayout'; import List from '@/components/list/List'; import { toArray } from 'lodash'; +import type { ComponentsList } from '@/types/Component'; const ComponentList = () => { const { data: components, error, isLoading } = useGetComponentsQuery(); @@ -14,18 +15,27 @@ const ComponentList = () => { } }, [error, showBoundary]); - // @todo This currently is a little too basic - it just renders everything except the dynamic_components - const filteredComponents = toArray(components).filter((component) => { - return component.library !== 'dynamic_components'; - }); + // This currently is a little simplistic, it just renders everything filtering out the dynamic_components + // const filteredComponents = toArray(components).filter((component) => { + // return component.library !== 'dynamic_components'; + // }); - const sortedComponents = filteredComponents - ? toArray(filteredComponents).sort((a, b) => a.name.localeCompare(b.name)) + const filteredComponents: ComponentsList = components + ? Object.fromEntries( + Object.entries(components).filter( + ([key, value]) => + value.library && value.library !== 'dynamic_components', + ), + ) : {}; + // const sortedComponents = filteredComponents + // ? toArray(filteredComponents).sort((a, b) => a.name.localeCompare(b.name)) + // : {}; + return ( <List - items={sortedComponents} + items={filteredComponents} isLoading={isLoading} type="component" label="Components" diff --git a/ui/src/components/list/List.tsx b/ui/src/components/list/List.tsx index df59c2ed16..728a6f6b23 100644 --- a/ui/src/components/list/List.tsx +++ b/ui/src/components/list/List.tsx @@ -1,4 +1,4 @@ -import type React from 'react'; +import React, { useMemo } from 'react'; import { useEffect, useRef, useCallback } from 'react'; import styles from './List.module.css'; import { @@ -32,6 +32,15 @@ const List: React.FC<ListProps> = (props) => { const listElRef = useRef<HTMLDivElement>(null); const { isDragging } = useAppSelector(selectDragging); + // Sort items and convert to array. + const sortedItems = useMemo(() => { + return items + ? Object.entries(items).sort(([, a], [, b]) => + a.name.localeCompare(b.name), + ) + : []; + }, [items]); + const handleDragStart = useCallback(() => { dispatch(setListDragging(true)); }, [dispatch]); @@ -102,8 +111,8 @@ const List: React.FC<ListProps> = (props) => { <Box className={isDragging ? 'list-dragging' : ''}> <Spinner loading={isLoading}> <Flex direction="column" width="100%" ref={listElRef}> - {items && - Object.entries(items).map(([id, item]) => ( + {sortedItems && + sortedItems.map(([id, item]) => ( <ListItem item={item} key={id} type={type} /> ))} </Flex> diff --git a/ui/src/types/Component.ts b/ui/src/types/Component.ts index 961eec2ca3..349fa5fc33 100644 --- a/ui/src/types/Component.ts +++ b/ui/src/types/Component.ts @@ -68,7 +68,7 @@ export interface PropSourceComponent extends BaseComponent { // Union type for any component export type XBComponent = BlockComponent | JSComponent | PropSourceComponent; -// ComponentsList type representing the API response +// ComponentsList representing the API response export interface ComponentsList { [key: string]: XBComponent; } -- GitLab From 1d7fe189cd9cd5cbfe1f896732092a39623e02c5 Mon Sep 17 00:00:00 2001 From: Jesse Baker <jesse.baker@acquia.com> Date: Tue, 25 Feb 2025 16:42:42 +0000 Subject: [PATCH 10/13] lint fix --- ui/src/components/list/ComponentList.tsx | 10 ---------- ui/src/components/list/List.tsx | 3 ++- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/ui/src/components/list/ComponentList.tsx b/ui/src/components/list/ComponentList.tsx index b28664fde4..5e3903c141 100644 --- a/ui/src/components/list/ComponentList.tsx +++ b/ui/src/components/list/ComponentList.tsx @@ -2,7 +2,6 @@ import { useEffect } from 'react'; import { useErrorBoundary } from 'react-error-boundary'; import { useGetComponentsQuery } from '@/services/componentAndLayout'; import List from '@/components/list/List'; -import { toArray } from 'lodash'; import type { ComponentsList } from '@/types/Component'; const ComponentList = () => { @@ -15,11 +14,6 @@ const ComponentList = () => { } }, [error, showBoundary]); - // This currently is a little simplistic, it just renders everything filtering out the dynamic_components - // const filteredComponents = toArray(components).filter((component) => { - // return component.library !== 'dynamic_components'; - // }); - const filteredComponents: ComponentsList = components ? Object.fromEntries( Object.entries(components).filter( @@ -29,10 +23,6 @@ const ComponentList = () => { ) : {}; - // const sortedComponents = filteredComponents - // ? toArray(filteredComponents).sort((a, b) => a.name.localeCompare(b.name)) - // : {}; - return ( <List items={filteredComponents} diff --git a/ui/src/components/list/List.tsx b/ui/src/components/list/List.tsx index 728a6f6b23..3e5835cb4e 100644 --- a/ui/src/components/list/List.tsx +++ b/ui/src/components/list/List.tsx @@ -1,4 +1,5 @@ -import React, { useMemo } from 'react'; +import type React from 'react'; +import { useMemo } from 'react'; import { useEffect, useRef, useCallback } from 'react'; import styles from './List.module.css'; import { -- GitLab From e708ed3cdafb52187318723bfb28c261d2ec8d70 Mon Sep 17 00:00:00 2001 From: Jesse Baker <jesse.baker@acquia.com> Date: Wed, 26 Feb 2025 11:21:57 +0000 Subject: [PATCH 11/13] fixed test data --- ui/tests/e2e/primary-panel.cy.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ui/tests/e2e/primary-panel.cy.js b/ui/tests/e2e/primary-panel.cy.js index 87d88c589c..430ff63ae1 100644 --- a/ui/tests/e2e/primary-panel.cy.js +++ b/ui/tests/e2e/primary-panel.cy.js @@ -20,7 +20,11 @@ describe('Primary panel', () => { .reduce((acc, _, index) => { const paddedIndex = String(index + 1).padStart(2, '0'); const id = `experience_builder:component_${paddedIndex}`; - acc[id] = { id, name: `Component ${paddedIndex}` }; + acc[id] = { + id, + name: `Component ${paddedIndex}`, + library: 'elements', + }; return acc; }, {}), }).as('getComponents'); -- GitLab From 8a77bd63a726970e1cee94abaa137ff7d74c2442 Mon Sep 17 00:00:00 2001 From: Jesse Baker <jesse.baker@acquia.com> Date: Thu, 27 Feb 2025 09:45:49 +0000 Subject: [PATCH 12/13] Move filtering of component list to API, rename CMS to Dynamic Components --- .../dynamicComponents/DynamicComponents.tsx | 47 +++++++++++-------- .../DynamicComponentsLibrary.tsx | 24 ++++------ ui/src/components/list/ComponentList.tsx | 12 +---- ui/src/components/topbar/Topbar.tsx | 2 +- ui/src/services/componentAndLayout.ts | 35 +++++++++++++- ui/src/types/Component.ts | 6 +-- 6 files changed, 75 insertions(+), 51 deletions(-) diff --git a/ui/src/components/dynamicComponents/DynamicComponents.tsx b/ui/src/components/dynamicComponents/DynamicComponents.tsx index 028c9c97da..a391f9be39 100644 --- a/ui/src/components/dynamicComponents/DynamicComponents.tsx +++ b/ui/src/components/dynamicComponents/DynamicComponents.tsx @@ -1,39 +1,48 @@ -import { Flex, Heading, Link, Box, ScrollArea } from '@radix-ui/themes'; -import { ExternalLinkIcon } from '@radix-ui/react-icons'; -import { handleNonWorkingBtn } from '@/utils/function-utils'; +import { Flex, Heading, Box, ScrollArea, Spinner } from '@radix-ui/themes'; import type React from 'react'; import DynamicComponentsLibrary from '@/components/dynamicComponents/DynamicComponentsLibrary'; +import { useGetDynamicComponentsQuery } from '@/services/componentAndLayout'; +import ErrorCard from '@/components/error/ErrorCard'; interface DynamicComponentsGroupsPopoverProps {} const DynamicComponents: React.FC<DynamicComponentsGroupsPopoverProps> = () => { + const { + data: dynamicComponents, + isLoading, + isError, + isFetching, + refetch, + } = useGetDynamicComponentsQuery(); + + if (isLoading || isFetching) { + return ( + <Flex justify="center" align="center"> + <Spinner /> + </Flex> + ); + } + return ( <> <Flex justify="between"> <Heading as="h3" size="3" mb="4"> Dynamic Components </Heading> - - <Flex justify="end" asChild> - <Link - size="1" - href="" - target="_blank" - onClick={(e: React.MouseEvent<HTMLAnchorElement>) => { - e.preventDefault(); - handleNonWorkingBtn(); - }} - > - Go to CMS - <ExternalLinkIcon /> - </Link> - </Flex> </Flex> <Box mr="-4"> <ScrollArea style={{ maxHeight: '380px', width: '100%' }} type="scroll"> <Box pr="4" mt="3"> - <DynamicComponentsLibrary /> + {isError && ( + <ErrorCard + title="Error fetching Dynamic component list." + resetErrorBoundary={refetch} + /> + )} + {!isError && dynamicComponents && ( + <DynamicComponentsLibrary dynamicComponents={dynamicComponents} /> + )} </Box> </ScrollArea> </Box> diff --git a/ui/src/components/dynamicComponents/DynamicComponentsLibrary.tsx b/ui/src/components/dynamicComponents/DynamicComponentsLibrary.tsx index ed40f7074c..a054adb559 100644 --- a/ui/src/components/dynamicComponents/DynamicComponentsLibrary.tsx +++ b/ui/src/components/dynamicComponents/DynamicComponentsLibrary.tsx @@ -5,24 +5,16 @@ import { import ErrorBoundary from '@/components/error/ErrorBoundary'; import { setOpenLayoutItem } from '@/features/ui/primaryPanelSlice'; import styles from '@/components/sidebar/Library.module.css'; -import { useState } from 'react'; -import { useGetComponentsQuery } from '@/services/componentAndLayout'; +import React, { useState } from 'react'; import List from '@/components/list/List'; -import type { ComponentsList } from '@/types/Component'; +import { ComponentsList } from '@/types/Component'; -const Library = () => { - const { data, isLoading } = useGetComponentsQuery(); - const [openCategories, setOpenCategories] = useState<string[]>([]); +interface LibraryProps { + dynamicComponents: ComponentsList; +} - // Filter only dynamic_components - const dynamicComponents: ComponentsList = data - ? Object.fromEntries( - Object.entries(data).filter( - ([key, value]) => - value.library && value.library === 'dynamic_components', - ), - ) - : {}; +const Library: React.FC<LibraryProps> = ({ dynamicComponents }) => { + const [openCategories, setOpenCategories] = useState<string[]>([]); const categoriesSet = new Set<string>(); Object.values(dynamicComponents).forEach((component) => { @@ -72,7 +64,7 @@ const Library = () => { ([key, component]) => component.category === category.id, ), )} - isLoading={isLoading} + isLoading={false} type="component" label="Components" /> diff --git a/ui/src/components/list/ComponentList.tsx b/ui/src/components/list/ComponentList.tsx index 5e3903c141..b44107aeb5 100644 --- a/ui/src/components/list/ComponentList.tsx +++ b/ui/src/components/list/ComponentList.tsx @@ -2,7 +2,6 @@ import { useEffect } from 'react'; import { useErrorBoundary } from 'react-error-boundary'; import { useGetComponentsQuery } from '@/services/componentAndLayout'; import List from '@/components/list/List'; -import type { ComponentsList } from '@/types/Component'; const ComponentList = () => { const { data: components, error, isLoading } = useGetComponentsQuery(); @@ -14,18 +13,9 @@ const ComponentList = () => { } }, [error, showBoundary]); - const filteredComponents: ComponentsList = components - ? Object.fromEntries( - Object.entries(components).filter( - ([key, value]) => - value.library && value.library !== 'dynamic_components', - ), - ) - : {}; - return ( <List - items={filteredComponents} + items={components} isLoading={isLoading} type="component" label="Components" diff --git a/ui/src/components/topbar/Topbar.tsx b/ui/src/components/topbar/Topbar.tsx index c1bf75eacb..85064b74e6 100644 --- a/ui/src/components/topbar/Topbar.tsx +++ b/ui/src/components/topbar/Topbar.tsx @@ -92,7 +92,7 @@ const Topbar = () => { <ExtensionsList /> </TopbarPopover> <TopbarPopover - tooltip="CMS" + tooltip="Dynamic components" trigger={ <Button variant="ghost" diff --git a/ui/src/services/componentAndLayout.ts b/ui/src/services/componentAndLayout.ts index 595ff0a3f3..72f0fe3e7a 100644 --- a/ui/src/services/componentAndLayout.ts +++ b/ui/src/services/componentAndLayout.ts @@ -8,11 +8,42 @@ import { setPageData } from '@/features/pageData/pageDataSlice'; export const componentAndLayoutApi = createApi({ reducerPath: 'componentAndLayoutApi', baseQuery, - tagTypes: ['Components', 'CodeComponents', 'CodeComponentAutoSave', 'Layout'], + tagTypes: [ + 'Components', + 'CodeComponents', + 'CodeComponentAutoSave', + 'Layout', + 'DynamicComponents', + 'AllComponents', + ], endpoints: (builder) => ({ getComponents: builder.query<ComponentsList, void>({ query: () => `xb/api/config/component`, providesTags: () => [{ type: 'Components', id: 'LIST' }], + transformResponse: (response: ComponentsList) => { + // Filter the response to exclude dynamic_components + return Object.fromEntries( + Object.entries(response).filter( + ([, value]) => value.library !== 'dynamic_components', + ), + ); + }, + }), + getDynamicComponents: builder.query<ComponentsList, void>({ + query: () => `xb/api/config/component`, + providesTags: () => [{ type: 'DynamicComponents', id: 'LIST' }], + transformResponse: (response: ComponentsList) => { + // Filter the response to only include dynamic_components + return Object.fromEntries( + Object.entries(response).filter( + ([, value]) => value.library === 'dynamic_components', + ), + ); + }, + }), + getAllComponents: builder.query<ComponentsList, void>({ + query: () => `xb/api/config/component`, + providesTags: () => [{ type: 'AllComponents', id: 'LIST' }], }), getLayoutById: builder.query< RootLayoutModel & { @@ -131,6 +162,8 @@ export const componentAndLayoutApi = createApi({ export const { useGetComponentsQuery, + useGetAllComponentsQuery, + useGetDynamicComponentsQuery, useGetLayoutByIdQuery, useLazyGetLayoutByIdQuery, useGetCodeComponentsQuery, diff --git a/ui/src/types/Component.ts b/ui/src/types/Component.ts index 349fa5fc33..fe34baae10 100644 --- a/ui/src/types/Component.ts +++ b/ui/src/types/Component.ts @@ -35,8 +35,8 @@ interface BaseComponent { js_footer: string; } -// BlockComponent Interface -export interface BlockComponent extends BaseComponent { +// For now, these are only Blocks. Later, it will be more. +export interface DynamicComponent extends BaseComponent { library: 'dynamic_components'; } @@ -66,7 +66,7 @@ export interface PropSourceComponent extends BaseComponent { transforms: TransformConfig; } // Union type for any component -export type XBComponent = BlockComponent | JSComponent | PropSourceComponent; +export type XBComponent = DynamicComponent | JSComponent | PropSourceComponent; // ComponentsList representing the API response export interface ComponentsList { -- GitLab From 6b69ded908cb0fe00a849359dfc2a542cf9ef7c2 Mon Sep 17 00:00:00 2001 From: Jesse Baker <jesse.baker@acquia.com> Date: Thu, 27 Feb 2025 15:06:46 +0000 Subject: [PATCH 13/13] refactor again to pass arg to getComponents rather than separate queries --- .../dynamicComponents/DynamicComponents.tsx | 7 ++- .../DynamicComponentsLibrary.tsx | 5 +- ui/src/components/list/ComponentList.tsx | 9 ++- ui/src/services/componentAndLayout.ts | 55 ++++++++----------- ui/src/types/Component.ts | 6 ++ 5 files changed, 44 insertions(+), 38 deletions(-) diff --git a/ui/src/components/dynamicComponents/DynamicComponents.tsx b/ui/src/components/dynamicComponents/DynamicComponents.tsx index a391f9be39..09c985d5c5 100644 --- a/ui/src/components/dynamicComponents/DynamicComponents.tsx +++ b/ui/src/components/dynamicComponents/DynamicComponents.tsx @@ -2,7 +2,7 @@ import { Flex, Heading, Box, ScrollArea, Spinner } from '@radix-ui/themes'; import type React from 'react'; import DynamicComponentsLibrary from '@/components/dynamicComponents/DynamicComponentsLibrary'; -import { useGetDynamicComponentsQuery } from '@/services/componentAndLayout'; +import { useGetComponentsQuery } from '@/services/componentAndLayout'; import ErrorCard from '@/components/error/ErrorCard'; interface DynamicComponentsGroupsPopoverProps {} @@ -14,7 +14,10 @@ const DynamicComponents: React.FC<DynamicComponentsGroupsPopoverProps> = () => { isError, isFetching, refetch, - } = useGetDynamicComponentsQuery(); + } = useGetComponentsQuery({ + mode: 'include', + libraries: ['dynamic_components'], + }); if (isLoading || isFetching) { return ( diff --git a/ui/src/components/dynamicComponents/DynamicComponentsLibrary.tsx b/ui/src/components/dynamicComponents/DynamicComponentsLibrary.tsx index a054adb559..0f7241e2e6 100644 --- a/ui/src/components/dynamicComponents/DynamicComponentsLibrary.tsx +++ b/ui/src/components/dynamicComponents/DynamicComponentsLibrary.tsx @@ -1,3 +1,4 @@ +import type React from 'react'; import { AccordionRoot, AccordionDetails, @@ -5,9 +6,9 @@ import { import ErrorBoundary from '@/components/error/ErrorBoundary'; import { setOpenLayoutItem } from '@/features/ui/primaryPanelSlice'; import styles from '@/components/sidebar/Library.module.css'; -import React, { useState } from 'react'; +import { useState } from 'react'; import List from '@/components/list/List'; -import { ComponentsList } from '@/types/Component'; +import type { ComponentsList } from '@/types/Component'; interface LibraryProps { dynamicComponents: ComponentsList; diff --git a/ui/src/components/list/ComponentList.tsx b/ui/src/components/list/ComponentList.tsx index b44107aeb5..56d47f092e 100644 --- a/ui/src/components/list/ComponentList.tsx +++ b/ui/src/components/list/ComponentList.tsx @@ -4,7 +4,14 @@ import { useGetComponentsQuery } from '@/services/componentAndLayout'; import List from '@/components/list/List'; const ComponentList = () => { - const { data: components, error, isLoading } = useGetComponentsQuery(); + const { + data: components, + error, + isLoading, + } = useGetComponentsQuery({ + mode: 'exclude', + libraries: ['dynamic_components'], + }); const { showBoundary } = useErrorBoundary(); useEffect(() => { diff --git a/ui/src/services/componentAndLayout.ts b/ui/src/services/componentAndLayout.ts index 72f0fe3e7a..03bf52e5d4 100644 --- a/ui/src/services/componentAndLayout.ts +++ b/ui/src/services/componentAndLayout.ts @@ -1,50 +1,41 @@ import { createApi } from '@reduxjs/toolkit/query/react'; import { baseQuery } from '@/services/baseQuery'; import type { CodeComponentSerialized } from '@/types/CodeComponent'; -import type { ComponentsList } from '@/types/Component'; +import type { ComponentsList, libraryTypes } from '@/types/Component'; import type { RootLayoutModel } from '@/features/layout/layoutModelSlice'; import { setPageData } from '@/features/pageData/pageDataSlice'; +type getComponentsQueryOptions = { + libraries: libraryTypes[]; + mode: 'include' | 'exclude'; +}; + export const componentAndLayoutApi = createApi({ reducerPath: 'componentAndLayoutApi', baseQuery, - tagTypes: [ - 'Components', - 'CodeComponents', - 'CodeComponentAutoSave', - 'Layout', - 'DynamicComponents', - 'AllComponents', - ], + tagTypes: ['Components', 'CodeComponents', 'CodeComponentAutoSave', 'Layout'], endpoints: (builder) => ({ - getComponents: builder.query<ComponentsList, void>({ + getComponents: builder.query< + ComponentsList, + getComponentsQueryOptions | void + >({ query: () => `xb/api/config/component`, providesTags: () => [{ type: 'Components', id: 'LIST' }], - transformResponse: (response: ComponentsList) => { - // Filter the response to exclude dynamic_components - return Object.fromEntries( - Object.entries(response).filter( - ([, value]) => value.library !== 'dynamic_components', - ), - ); - }, - }), - getDynamicComponents: builder.query<ComponentsList, void>({ - query: () => `xb/api/config/component`, - providesTags: () => [{ type: 'DynamicComponents', id: 'LIST' }], - transformResponse: (response: ComponentsList) => { - // Filter the response to only include dynamic_components + transformResponse: (response: ComponentsList, meta, arg) => { + if (!arg || !Array.isArray(arg.libraries)) { + // If no filter is provided, return all components. + return response; + } + + // Filter the response based on the include/exclude and the list of library types passed. return Object.fromEntries( - Object.entries(response).filter( - ([, value]) => value.library === 'dynamic_components', - ), + Object.entries(response).filter(([, value]) => { + const isIncluded = arg.libraries.includes(value.library); + return arg.mode === 'include' ? isIncluded : !isIncluded; + }), ); }, }), - getAllComponents: builder.query<ComponentsList, void>({ - query: () => `xb/api/config/component`, - providesTags: () => [{ type: 'AllComponents', id: 'LIST' }], - }), getLayoutById: builder.query< RootLayoutModel & { entity_form_fields: {}; @@ -162,8 +153,6 @@ export const componentAndLayoutApi = createApi({ export const { useGetComponentsQuery, - useGetAllComponentsQuery, - useGetDynamicComponentsQuery, useGetLayoutByIdQuery, useLazyGetLayoutByIdQuery, useGetCodeComponentsQuery, diff --git a/ui/src/types/Component.ts b/ui/src/types/Component.ts index fe34baae10..9680d4e386 100644 --- a/ui/src/types/Component.ts +++ b/ui/src/types/Component.ts @@ -35,6 +35,12 @@ interface BaseComponent { js_footer: string; } +export type libraryTypes = + | 'dynamic_components' + | 'primary_components' + | 'extension_components' + | 'elements'; + // For now, these are only Blocks. Later, it will be more. export interface DynamicComponent extends BaseComponent { library: 'dynamic_components'; -- GitLab