Skip to content

Commit

Permalink
#7183 Refactor page editor sidebar to completely avoid fetching mod d…
Browse files Browse the repository at this point in the history
…efinitions (#7173)

* refactor page editor sidebar

* fix merge conflict artifact

* some more name cleanup

* reimplement update icon feature using RTK query for now

* cleanup snapshots

* inline var

* extract filter logic and add tests

* make test name better

* move type guard to common module

---------

Co-authored-by: Ben Loe <[email protected]>
  • Loading branch information
BLoe and Ben Loe authored Dec 28, 2023
1 parent 14cde02 commit 025cd65
Show file tree
Hide file tree
Showing 15 changed files with 765 additions and 853 deletions.
101 changes: 65 additions & 36 deletions src/pageEditor/sidebar/ActivatedModComponentListItem.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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);
Expand All @@ -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(
<ActivatedModComponentListItem
modComponent={modComponent}
mods={[]}
isAvailable
/>,
render(
<ActivatedModComponentListItem modComponent={modComponent} isAvailable />,
);

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(
<ActivatedModComponentListItem modComponent={modComponent} isAvailable />,
{
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(
<ActivatedModComponentListItem
modComponent={modComponent}
mods={[]}
isAvailable
isAvailable={false}
/>,
{
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(
<ActivatedModComponentListItem modComponent={modComponent} isAvailable />,
);

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);
});
});
53 changes: 38 additions & 15 deletions src/pageEditor/sidebar/ActivatedModComponentListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -49,32 +47,36 @@ 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
* @see DynamicModComponentListItem
*/
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(
async () => selectType(modComponent),
[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) => {
Expand All @@ -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") {
Expand All @@ -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";
Expand All @@ -120,7 +143,7 @@ const ActivatedModComponentListItem: React.FunctionComponent<{
return (
<ListGroup.Item
className={cx(styles.root, {
[styles.recipeBackground ?? ""]: hasRecipeBackground,
[styles.recipeBackground ?? ""]: hasActiveModBackground,
})}
action
active={isActive}
Expand Down
25 changes: 9 additions & 16 deletions src/pageEditor/sidebar/ModComponentListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,14 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

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;
Expand All @@ -35,29 +31,26 @@ type ModComponentListItemProps = {
const ModComponentListItem: React.FunctionComponent<
ModComponentListItemProps
> = ({
modComponent,
mods,
modComponentSidebarItem,
availableInstalledIds,
availableDynamicIds,
isNested = false,
}) =>
isModComponentBase(modComponent) ? (
isModComponentBase(modComponentSidebarItem) ? (
<ActivatedModComponentListItem
key={`installed-${modComponent.id}`}
modComponent={modComponent}
mods={mods}
modComponent={modComponentSidebarItem}
isAvailable={
!availableInstalledIds ||
availableInstalledIds.includes(modComponent.id)
availableInstalledIds.includes(modComponentSidebarItem.id)
}
isNested={isNested}
/>
) : (
<DynamicModComponentListItem
key={`dynamic-${modComponent.uuid}`}
modComponentFormState={modComponent}
modComponentFormState={modComponentSidebarItem}
isAvailable={
!availableDynamicIds || availableDynamicIds.includes(modComponent.uuid)
!availableDynamicIds ||
availableDynamicIds.includes(modComponentSidebarItem.uuid)
}
isNested={isNested}
/>
Expand Down
Loading

0 comments on commit 025cd65

Please sign in to comment.