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&nbsp;
+            <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&nbsp;
+            <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&nbsp;
-            <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&nbsp;
-            <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