diff --git a/src/pageEditor/sidebar/ActivatedModComponentListItem.test.tsx b/src/pageEditor/sidebar/ActivatedModComponentListItem.test.tsx
index a1a678c82d..7c0c4188cd 100644
--- a/src/pageEditor/sidebar/ActivatedModComponentListItem.test.tsx
+++ b/src/pageEditor/sidebar/ActivatedModComponentListItem.test.tsx
@@ -16,14 +16,14 @@
*/
import React from "react";
-import { waitForEffect } from "@/testUtils/testHelpers";
import { render } from "@/pageEditor/testHelpers";
import { actions as editorActions } from "@/pageEditor/slices/editorSlice";
-import { authActions } from "@/auth/authSlice";
import ActivatedModComponentListItem from "@/pageEditor/sidebar/ActivatedModComponentListItem";
import { modComponentFactory } from "@/testUtils/factories/modComponentFactories";
import { formStateFactory } from "@/testUtils/factories/pageEditorFactories";
-import { authStateFactory } from "@/testUtils/factories/authFactories";
+import { screen, waitFor } from "@testing-library/react";
+import { disableOverlay, enableOverlay } from "@/contentScript/messenger/api";
+import userEvent from "@testing-library/user-event";
jest.mock("@/pageEditor/starterBricks/adapter", () => {
const actual = jest.requireActual("@/pageEditor/starterBricks/adapter");
@@ -33,6 +33,14 @@ jest.mock("@/pageEditor/starterBricks/adapter", () => {
};
});
+jest.mock("@/contentScript/messenger/api", () => ({
+ enableOverlay: jest.fn(),
+ disableOverlay: jest.fn(),
+}));
+
+const enableOverlayMock = jest.mocked(enableOverlay);
+const disableOverlayMock = jest.mocked(disableOverlay);
+
beforeAll(() => {
// When a FontAwesomeIcon gets a title, it generates a random id, which breaks the snapshot.
jest.spyOn(global.Math, "random").mockImplementation(() => 0);
@@ -43,55 +51,76 @@ afterAll(() => {
});
describe("ActivatedModComponentListItem", () => {
- test("it renders not active element", async () => {
+ it("renders not active element", async () => {
const modComponent = modComponentFactory();
- const formState = formStateFactory();
- const { asFragment } = render(
- ,
+ render(
+ ,
+ );
+
+ const button = await screen.findByRole("button", {
+ name: modComponent.label,
+ });
+ expect(button).toBeVisible();
+ expect(button).not.toHaveClass("active");
+ });
+
+ it("renders active element", async () => {
+ // Note: This is a contrived situation, because in the real app, a
+ // form state element will always be created when a mod component
+ // is selected (active) in the sidebar
+ const modComponent = modComponentFactory();
+ const formState = formStateFactory({
+ uuid: modComponent.id,
+ });
+ render(
+ ,
{
- initialValues: formState,
setupRedux(dispatch) {
- dispatch(authActions.setAuth(authStateFactory()));
-
// The addElement also sets the active element
- dispatch(editorActions.addElement(formStateFactory()));
-
- // Add new element to deactivate the previous one
dispatch(editorActions.addElement(formState));
- // Remove the active element and stay with one inactive item
- dispatch(editorActions.removeElement(formState.uuid));
},
},
);
- await waitForEffect();
- expect(asFragment()).toMatchSnapshot();
+ const button = await screen.findByRole("button", {
+ name: modComponent.label,
+ });
+ expect(button).toBeVisible();
+ // Wait for Redux side effects
+ await waitFor(() => {
+ expect(button).toHaveClass("active");
+ });
});
- test("it renders active element", async () => {
+ it("shows not-available icon properly", async () => {
const modComponent = modComponentFactory();
- const formState = formStateFactory();
- const { asFragment } = render(
+ render(
,
- {
- initialValues: formState,
- setupRedux(dispatch) {
- dispatch(authActions.setAuth(authStateFactory()));
- // The addElement also sets the active element
- dispatch(editorActions.addElement(formState));
- },
- },
);
- await waitForEffect();
- expect(asFragment()).toMatchSnapshot();
+ expect(
+ await screen.findByRole("img", { name: "Not available on page" }),
+ ).toBeVisible();
+ });
+
+ it("handles mouseover action properly for button mod components", async () => {
+ const modComponent = modComponentFactory();
+ render(
+ ,
+ );
+
+ const button = await screen.findByRole("button", {
+ name: modComponent.label,
+ });
+ await userEvent.hover(button);
+
+ expect(enableOverlayMock).toHaveBeenCalledTimes(1);
+
+ await userEvent.unhover(button);
+
+ expect(disableOverlayMock).toHaveBeenCalledTimes(1);
});
});
diff --git a/src/pageEditor/sidebar/ActivatedModComponentListItem.tsx b/src/pageEditor/sidebar/ActivatedModComponentListItem.tsx
index 1a2a16ecf0..5e5f4b5e4b 100644
--- a/src/pageEditor/sidebar/ActivatedModComponentListItem.tsx
+++ b/src/pageEditor/sidebar/ActivatedModComponentListItem.tsx
@@ -28,11 +28,9 @@ import { actions } from "@/pageEditor/slices/editorSlice";
import reportError from "@/telemetry/reportError";
import { ListGroup } from "react-bootstrap";
import {
- NotAvailableIcon,
ExtensionIcon,
+ NotAvailableIcon,
} from "@/pageEditor/sidebar/ExtensionIcons";
-import { type ModDefinition } from "@/types/modDefinitionTypes";
-import { initRecipeOptionsIfNeeded } from "@/pageEditor/starterBricks/base";
import {
disableOverlay,
enableOverlay,
@@ -49,6 +47,9 @@ import {
} from "@/pageEditor/slices/editorSelectors";
import { type UUID } from "@/types/stringTypes";
import { type ModComponentBase } from "@/types/modComponentTypes";
+import { appApi } from "@/services/api";
+import { emptyModOptionsDefinitionFactory } from "@/utils/modUtils";
+import { type Schema } from "@/types/schemaTypes";
/**
* A sidebar menu entry corresponding to an untouched mod component
@@ -56,10 +57,9 @@ import { type ModComponentBase } from "@/types/modComponentTypes";
*/
const ActivatedModComponentListItem: React.FunctionComponent<{
modComponent: ModComponentBase;
- mods: ModDefinition[];
isAvailable: boolean;
isNested?: boolean;
-}> = ({ modComponent, mods, isAvailable, isNested = false }) => {
+}> = ({ modComponent, isAvailable, isNested = false }) => {
const sessionId = useSelector(selectSessionId);
const dispatch = useDispatch();
const [type] = useAsyncState(
@@ -67,14 +67,16 @@ const ActivatedModComponentListItem: React.FunctionComponent<{
[modComponent.extensionPointId],
);
- const activeRecipeId = useSelector(selectActiveRecipeId);
+ const [getModDefinition] = appApi.endpoints.getRecipe.useLazyQuery();
+
+ const activeModId = useSelector(selectActiveRecipeId);
const activeElement = useSelector(selectActiveElement);
const isActive = activeElement?.uuid === modComponent.id;
- // Get the selected recipe id, or the recipe id of the selected item
- const recipeId = activeRecipeId ?? activeElement?.recipe?.id;
+ // Get the selected mod id, or the mod id of the selected mod component
+ const modId = activeModId ?? activeElement?.recipe?.id;
// Set the alternate background if this item isn't active, but either its recipe or another item in its recipe is active
- const hasRecipeBackground =
- !isActive && recipeId && modComponent._recipe?.id === recipeId;
+ const hasActiveModBackground =
+ !isActive && modId && modComponent._recipe?.id === modId;
const selectHandler = useCallback(
async (modComponent: ModComponentBase) => {
@@ -84,10 +86,31 @@ const ActivatedModComponentListItem: React.FunctionComponent<{
extensionId: modComponent.id,
});
- const state = await extensionToFormState(modComponent);
- initRecipeOptionsIfNeeded(state, mods);
+ const modComponentFormState = await extensionToFormState(modComponent);
+
+ // Initialize mod options schema if needed
+ if (modComponent._recipe) {
+ const { data: modDefinition } = await getModDefinition(
+ { recipeId: modComponent._recipe.id },
+ true,
+ );
+ if (modDefinition) {
+ modComponentFormState.optionsDefinition =
+ modDefinition.options == null
+ ? emptyModOptionsDefinitionFactory()
+ : {
+ schema: modDefinition.options.schema.properties
+ ? modDefinition.options.schema
+ : ({
+ type: "object",
+ properties: modDefinition.options.schema,
+ } as Schema),
+ uiSchema: modDefinition.options.uiSchema,
+ };
+ }
+ }
- dispatch(actions.selectInstalled(state));
+ dispatch(actions.selectInstalled(modComponentFormState));
dispatch(actions.checkActiveElementAvailability());
if (type === "actionPanel") {
@@ -104,7 +127,7 @@ const ActivatedModComponentListItem: React.FunctionComponent<{
dispatch(actions.adapterError({ uuid: modComponent.id, error }));
}
},
- [dispatch, sessionId, mods, type],
+ [sessionId, dispatch, type, getModDefinition],
);
const isButton = type === "menuItem";
@@ -120,7 +143,7 @@ const ActivatedModComponentListItem: React.FunctionComponent<{
return (
.
*/
-import { type ModDefinition } from "@/types/modDefinitionTypes";
import React from "react";
-import { type ModComponentFormState } from "@/pageEditor/starterBricks/formStateTypes";
-import { isModComponentBase } from "./common";
+import { isModComponentBase, type ModComponentSidebarItem } from "./common";
import DynamicModComponentListItem from "./DynamicModComponentListItem";
import ActivatedModComponentListItem from "./ActivatedModComponentListItem";
-import { type ModComponentBase } from "@/types/modComponentTypes";
import { type UUID } from "@/types/stringTypes";
type ModComponentListItemProps = {
- modComponent: ModComponentBase | ModComponentFormState;
- mods: ModDefinition[];
+ modComponentSidebarItem: ModComponentSidebarItem;
availableInstalledIds: UUID[];
availableDynamicIds: UUID[];
isNested?: boolean;
@@ -35,29 +31,26 @@ type ModComponentListItemProps = {
const ModComponentListItem: React.FunctionComponent<
ModComponentListItemProps
> = ({
- modComponent,
- mods,
+ modComponentSidebarItem,
availableInstalledIds,
availableDynamicIds,
isNested = false,
}) =>
- isModComponentBase(modComponent) ? (
+ isModComponentBase(modComponentSidebarItem) ? (
) : (
diff --git a/src/pageEditor/sidebar/ModListItem.test.tsx b/src/pageEditor/sidebar/ModListItem.test.tsx
index eb4d03da2e..499fffd054 100644
--- a/src/pageEditor/sidebar/ModListItem.test.tsx
+++ b/src/pageEditor/sidebar/ModListItem.test.tsx
@@ -16,102 +16,121 @@
*/
import React from "react";
-import extensionsSlice from "@/store/extensionsSlice";
-import {
- createRenderFunctionWithRedux,
- type RenderFunctionWithRedux,
-} from "@/testUtils/testHelpers";
-import {
- editorSlice,
- initialState as editorInitialState,
-} from "@/pageEditor/slices/editorSlice";
-import ModListItem, { type ModListItemProps } from "./ModListItem";
-import { type EditorState } from "@/pageEditor/pageEditorTypes";
-import { type ModComponentState } from "@/store/extensionsTypes";
-import { validateSemVerString } from "@/types/helpers";
-import { defaultModDefinitionFactory } from "@/testUtils/factories/modDefinitionFactories";
-import { metadataFactory } from "@/testUtils/factories/metadataFactory";
+import ModListItem from "./ModListItem";
import { screen } from "@testing-library/react";
+import { modMetadataFactory } from "@/testUtils/factories/modComponentFactories";
+import { render } from "@/pageEditor/testHelpers";
+import { Accordion, ListGroup } from "react-bootstrap";
+import { appApiMock } from "@/testUtils/appApiMock";
+import { modDefinitionFactory } from "@/testUtils/factories/modDefinitionFactories";
+import { validateSemVerString } from "@/types/helpers";
-let renderModListItem: RenderFunctionWithRedux<
- {
- editor: EditorState;
- options: ModComponentState;
- },
- ModListItemProps
->;
+describe("ModListItem", () => {
+ it("renders expanded", async () => {
+ const modMetadata = modMetadataFactory();
+ appApiMock.onGet(`/api/recipes/${modMetadata.id}/`).reply(
+ 200,
+ modDefinitionFactory({
+ metadata: modMetadata,
+ }),
+ );
+ render(
+
+
+
+ test children
+
+
+ ,
+ );
-beforeEach(() => {
- const recipe = defaultModDefinitionFactory();
- const recipeId = recipe.metadata.id;
- // eslint-disable-next-line testing-library/no-render-in-lifecycle -- higher order function, not the actual render
- renderModListItem = createRenderFunctionWithRedux({
- reducer: {
- editor: editorSlice.reducer,
- options: extensionsSlice.reducer,
- },
- preloadedState: {
- editor: {
- ...editorInitialState,
- expandedRecipeId: recipeId,
- },
- },
- ComponentUnderTest: ModListItem,
- defaultProps: {
- recipe,
- children:
test children
,
- installedVersion: validateSemVerString("1.0.0"),
- onSave: jest.fn(),
- isSaving: false,
- onReset: jest.fn(),
- onDeactivate: jest.fn(),
- onClone: jest.fn(),
- },
+ expect(await screen.findByText(modMetadata.name)).toBeVisible();
+ // eslint-disable-next-line testing-library/no-node-access -- Accordion collapse state
+ expect(screen.getByText("test children").parentElement).toHaveClass(
+ "collapse show",
+ );
});
-});
-test("it renders", () => {
- const { asFragment } = renderModListItem();
+ it("renders not expanded", async () => {
+ const modMetadata = modMetadataFactory();
+ appApiMock.onGet(`/api/recipes/${modMetadata.id}/`).reply(
+ 200,
+ modDefinitionFactory({
+ metadata: modMetadata,
+ }),
+ );
+ render(
+
+
+
+ test children
+
+
+ ,
+ );
- expect(asFragment()).toMatchSnapshot();
-});
-
-test("renders with empty recipe", () => {
- const { asFragment } = renderModListItem({
- propsOverride: {
- recipe: undefined,
- },
+ expect(await screen.findByText(modMetadata.name)).toBeVisible();
+ // eslint-disable-next-line testing-library/no-node-access -- Accordion collapse state
+ expect(screen.getByText("test children").parentElement).toHaveClass(
+ "collapse",
+ );
+ // eslint-disable-next-line testing-library/no-node-access -- Accordion collapse state
+ expect(screen.getByText("test children").parentElement).not.toHaveClass(
+ "show",
+ );
});
- expect(asFragment()).toMatchSnapshot();
-});
-
-test("renders with empty metadata", () => {
- const recipe = defaultModDefinitionFactory({ metadata: null });
- const { asFragment } = renderModListItem({
- propsOverride: {
- recipe,
- },
- });
+ it("renders has-update icon properly", async () => {
+ const modMetadata = modMetadataFactory();
+ const modDefinition = modDefinitionFactory({
+ metadata: {
+ ...modMetadata,
+ version: validateSemVerString("1.0.1"),
+ },
+ });
+ appApiMock
+ .onGet(`/api/recipes/${encodeURIComponent(modMetadata.id)}/`)
+ .reply(200, {
+ config: modDefinition,
+ sharing: modDefinition.sharing,
+ updated_at: modDefinition.updated_at,
+ });
+ render(
+
+
+
+ test children
+
+
+ ,
+ );
- expect(asFragment()).toMatchSnapshot();
-});
+ const expectedMessage =
+ "You are editing version 1.0.0 of this mod, the latest version is 1.0.1.";
-test("renders the warning icon when has update", () => {
- const recipe = defaultModDefinitionFactory({
- metadata: metadataFactory({
- version: validateSemVerString("2.0.0"),
- }),
+ expect(
+ await screen.findByRole("img", { name: expectedMessage }),
+ ).toBeVisible();
});
- renderModListItem({
- propsOverride: {
- recipe,
- },
- });
-
- const warningIcon = screen.getByTitle(
- "You are editing version 1.0.0 of this mod, the latest version is 2.0.0.",
- );
-
- expect(warningIcon).toBeInTheDocument();
});
diff --git a/src/pageEditor/sidebar/ModListItem.tsx b/src/pageEditor/sidebar/ModListItem.tsx
index 03c1f91a77..ef6e45e084 100644
--- a/src/pageEditor/sidebar/ModListItem.tsx
+++ b/src/pageEditor/sidebar/ModListItem.tsx
@@ -16,7 +16,7 @@
*/
import React, { type PropsWithChildren } from "react";
-import { type SemVerString } from "@/types/registryTypes";
+import { type Metadata } from "@/types/registryTypes";
import styles from "./Entry.module.scss";
import {
RecipeHasUpdateIcon,
@@ -34,18 +34,17 @@ import { useDispatch, useSelector } from "react-redux";
import cx from "classnames";
import {
selectActiveElement,
+ selectActiveRecipeId,
selectDirtyMetadataForRecipeId,
selectExpandedRecipeId,
selectRecipeIsDirty,
} from "@/pageEditor/slices/editorSelectors";
-import { type ModDefinition } from "@/types/modDefinitionTypes";
import * as semver from "semver";
import ActionMenu from "@/pageEditor/sidebar/ActionMenu";
+import { useGetRecipeQuery } from "@/services/api";
export type ModListItemProps = PropsWithChildren<{
- recipe: ModDefinition | undefined;
- isActive?: boolean;
- installedVersion: SemVerString;
+ modMetadata: Metadata;
onSave: () => Promise;
isSaving: boolean;
onReset: () => Promise;
@@ -54,10 +53,8 @@ export type ModListItemProps = PropsWithChildren<{
}>;
const ModListItem: React.FC = ({
- recipe,
- isActive,
+ modMetadata,
children,
- installedVersion,
onSave,
isSaving,
onReset,
@@ -65,43 +62,43 @@ const ModListItem: React.FC = ({
onClone,
}) => {
const dispatch = useDispatch();
-
- const expandedRecipeId = useSelector(selectExpandedRecipeId);
+ const activeModId = useSelector(selectActiveRecipeId);
+ const expandedModId = useSelector(selectExpandedRecipeId);
const activeElement = useSelector(selectActiveElement);
- const {
- id: recipeId,
- name: savedName,
- version: latestRecipeVersion,
- } = recipe?.metadata ?? {};
+ const { id: modId, name: savedName, version: installedVersion } = modMetadata;
+ const isActive = activeModId === modId;
+
+ // TODO: Fix this so it pulls from registry, after registry single-item-api-fetch is implemented
+ // (See: https://github.com/pixiebrix/pixiebrix-extension/issues/7184)
+ const { data: modDefinition } = useGetRecipeQuery({ recipeId: modId });
+ const latestRecipeVersion = modDefinition?.metadata?.version;
// Set the alternate background if an extension in this recipe is active
- const hasRecipeBackground = activeElement?.recipe?.id === recipeId;
+ const hasRecipeBackground = activeElement?.recipe?.id === modId;
- const dirtyName = useSelector(selectDirtyMetadataForRecipeId(recipeId))?.name;
+ const dirtyName = useSelector(selectDirtyMetadataForRecipeId(modId))?.name;
const name = dirtyName ?? savedName ?? "Loading...";
- const isDirty = useSelector(selectRecipeIsDirty(recipeId));
+ const isDirty = useSelector(selectRecipeIsDirty(modId));
const hasUpdate =
latestRecipeVersion != null &&
installedVersion != null &&
semver.gt(latestRecipeVersion, installedVersion);
- const caretIcon = expandedRecipeId === recipeId ? faCaretDown : faCaretRight;
+ const caretIcon = expandedModId === modId ? faCaretDown : faCaretRight;
return (
<>
- recipeId != null && dispatch(actions.selectRecipeId(recipeId))
- }
+ key={`recipe-${modId}`}
+ onClick={() => modId != null && dispatch(actions.selectRecipeId(modId))}
>
@@ -130,7 +127,7 @@ const ModListItem: React.FC = ({
/>
)}
-
+
<>{children}>
>
diff --git a/src/pageEditor/sidebar/SidebarExpanded.tsx b/src/pageEditor/sidebar/SidebarExpanded.tsx
index 1ab060f8cc..4bad996e85 100644
--- a/src/pageEditor/sidebar/SidebarExpanded.tsx
+++ b/src/pageEditor/sidebar/SidebarExpanded.tsx
@@ -16,24 +16,25 @@
*/
import styles from "./Sidebar.module.scss";
-import React, { useEffect, useMemo, useState } from "react";
-import { sortBy } from "lodash";
+import React, { useMemo, useState } from "react";
import {
Accordion,
Button,
- // eslint-disable-next-line no-restricted-imports -- TODO: Fix over time
- Form,
+ FormControl,
InputGroup,
ListGroup,
} from "react-bootstrap";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import hash from "object-hash";
-import { isModComponentBase } from "@/pageEditor/sidebar/common";
+import {
+ getModComponentItemId,
+ isModSidebarItem,
+ type SidebarItem,
+} from "@/pageEditor/sidebar/common";
import { faAngleDoubleLeft } from "@fortawesome/free-solid-svg-icons";
import cx from "classnames";
import ModListItem from "@/pageEditor/sidebar/ModListItem";
import useFlags from "@/hooks/useFlags";
-import arrangeElements from "@/pageEditor/sidebar/arrangeElements";
+import arrangeSidebarItems from "@/pageEditor/sidebar/arrangeSidebarItems";
import {
selectActiveElementId,
selectActiveRecipeId,
@@ -43,11 +44,6 @@ import {
selectNotDeletedExtensions,
} from "@/pageEditor/slices/editorSelectors";
import { useDispatch, useSelector } from "react-redux";
-import {
- getRecipeById,
- getIdForElement,
- getModIdForElement,
-} from "@/pageEditor/utils";
import useSaveRecipe from "@/pageEditor/hooks/useSaveRecipe";
import useResetRecipe from "@/pageEditor/hooks/useResetRecipe";
import useDeactivateMod from "@/pageEditor/hooks/useDeactivateMod";
@@ -56,86 +52,56 @@ import ReloadButton from "./ReloadButton";
import AddStarterBrickButton from "./AddStarterBrickButton";
import ModComponentListItem from "./ModComponentListItem";
import { actions } from "@/pageEditor/slices/editorSlice";
-import { measureDurationFromAppStart } from "@/utils/performance";
-import { useAllModDefinitions } from "@/modDefinitions/modDefinitionHooks";
import { useDebounce } from "use-debounce";
+import { lowerCase } from "lodash";
+import filterSidebarItems from "@/pageEditor/sidebar/filterSidebarItems";
const SidebarExpanded: React.FunctionComponent<{
collapseSidebar: () => void;
}> = ({ collapseSidebar }) => {
- const { data: allModDefinitions, isLoading: isAllModDefinitionsLoading } =
- useAllModDefinitions();
-
- useEffect(() => {
- if (!isAllModDefinitionsLoading) {
- measureDurationFromAppStart("sidebarExpanded:allRecipesLoaded");
- }
- }, [isAllModDefinitionsLoading]);
-
const dispatch = useDispatch();
- const activeElementId = useSelector(selectActiveElementId);
- const activeRecipeId = useSelector(selectActiveRecipeId);
- const expandedRecipeId = useSelector(selectExpandedRecipeId);
- const activated = useSelector(selectNotDeletedExtensions);
- const elements = useSelector(selectNotDeletedElements);
+ const activeModComponentId = useSelector(selectActiveElementId);
+ const activeModId = useSelector(selectActiveRecipeId);
+ const expandedModId = useSelector(selectExpandedRecipeId);
+ const cleanModComponents = useSelector(selectNotDeletedExtensions);
+ const modComponentFormStates = useSelector(selectNotDeletedElements);
const { availableInstalledIds, availableDynamicIds } = useSelector(
selectExtensionAvailability,
);
- const mods = useMemo(() => {
- const activatedAndElements = [...activated, ...elements];
- return (
- allModDefinitions?.filter((recipe) =>
- activatedAndElements.some(
- (element) => getModIdForElement(element) === recipe.metadata.id,
- ),
- ) ?? []
- );
- }, [allModDefinitions, elements, activated]);
-
const { flagOn } = useFlags();
const showDeveloperUI =
process.env.ENVIRONMENT === "development" ||
flagOn("page-editor-developer");
- const [query, setQuery] = useState("");
-
- const [debouncedQuery] = useDebounce(query, 250, {
+ const [filterQuery, setFilterQuery] = useState("");
+ const [debouncedFilterQuery] = useDebounce(lowerCase(filterQuery), 250, {
trailing: true,
leading: false,
});
- const elementHash = hash(
- sortBy(
- elements.map(
- (formState) =>
- `${formState.uuid}-${formState.label}-${formState.recipe?.id ?? ""}`,
- ),
- ),
- );
- const recipeHash = hash(
- mods
- ? mods.map((recipe) => `${recipe.metadata.id}-${recipe.metadata.name}`)
- : "",
+ const sortedSidebarItems = useMemo(
+ () =>
+ arrangeSidebarItems({
+ modComponentFormStates,
+ cleanModComponents,
+ }),
+ [modComponentFormStates, cleanModComponents],
);
- const sortedElements = useMemo(
+
+ const filteredSidebarItems = useMemo(
() =>
- arrangeElements({
- elements,
- installed: activated,
- recipes: mods,
- activeElementId,
- activeRecipeId,
- query: debouncedQuery,
+ filterSidebarItems({
+ sidebarItems: sortedSidebarItems,
+ filterText: debouncedFilterQuery,
+ activeModId,
+ activeModComponentId,
}),
- // eslint-disable-next-line react-hooks/exhaustive-deps -- using elementHash and recipeHash to track changes
[
- activated,
- elementHash,
- recipeHash,
- activeElementId,
- activeRecipeId,
- debouncedQuery,
+ activeModComponentId,
+ activeModId,
+ debouncedFilterQuery,
+ sortedSidebarItems,
],
);
@@ -143,44 +109,31 @@ const SidebarExpanded: React.FunctionComponent<{
const resetRecipe = useResetRecipe();
const deactivateMod = useDeactivateMod();
- const listItems = sortedElements.map((item) => {
- if (Array.isArray(item)) {
- const [recipeId, elements] = item;
- const recipe = getRecipeById(mods, recipeId);
- const firstElement = elements[0];
- const installedVersion =
- firstElement == null
- ? // If there's no extensions in the Blueprint (empty Blueprint?), use the Blueprint's version
- recipe?.metadata?.version
- : isModComponentBase(firstElement)
- ? firstElement._recipe.version
- : firstElement.recipe.version;
-
+ const listItems = filteredSidebarItems.map((sidebarItem) => {
+ if (isModSidebarItem(sidebarItem)) {
+ const { modMetadata, modComponents } = sidebarItem;
return (
{
- await saveRecipe(activeRecipeId);
+ await saveRecipe(activeModId);
}}
isSaving={isSavingRecipe}
onReset={async () => {
- await resetRecipe(activeRecipeId);
+ await resetRecipe(activeModId);
}}
onDeactivate={async () => {
- await deactivateMod({ modId: activeRecipeId });
+ await deactivateMod({ modId: activeModId });
}}
onClone={async () => {
dispatch(actions.showCreateRecipeModal({ keepLocalCopy: true }));
}}
>
- {elements.map((element) => (
+ {modComponents.map((modComponentSidebarItem) => (
@@ -227,20 +179,20 @@ const SidebarExpanded: React.FunctionComponent<{
- {
- setQuery(target.value);
+ setFilterQuery(target.value);
}}
/>
- {query.length > 0 ? (
+ {filterQuery.length > 0 ? (