Skip to content

Commit

Permalink
#9242: migrate unsaved standalone mod components
Browse files Browse the repository at this point in the history
  • Loading branch information
twschiller committed Oct 7, 2024
1 parent 99f9601 commit ff96cba
Show file tree
Hide file tree
Showing 28 changed files with 299 additions and 302 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ import { validateOutputKey } from "@/runtime/runtimeTypes";
import { toExpression } from "@/utils/expressionUtils";
import { normalizeAvailability } from "@/bricks/available";
import { StarterBrickTypes } from "@/types/starterBrickTypes";
import { emptyModVariablesDefinitionFactory } from "@/utils/modUtils";
import {
createNewUnsavedModMetadata,
emptyModVariablesDefinitionFactory,
} from "@/utils/modUtils";

describe("selectVariables", () => {
test("selects nothing when no services used", () => {
Expand Down Expand Up @@ -224,7 +227,7 @@ describe("selectVariables", () => {
permissions: emptyPermissionsFactory(),
optionsArgs: {},
variablesDefinition: emptyModVariablesDefinitionFactory(),
modMetadata: undefined,
modMetadata: createNewUnsavedModMetadata({ modName: "Document Mod" }),
modComponent: {
brickPipeline: [
{
Expand Down
19 changes: 10 additions & 9 deletions src/pageEditor/hooks/useAddNewModComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,19 +89,20 @@ function useAddNewModComponent(modMetadata?: ModMetadata): AddNewModComponent {
setInsertingStarterBrickType(null);
}

const url = await getCurrentInspectedURL();
const metadata = internalStarterBrickMetaFactory();
const initialFormState = fromNativeElement(url, metadata, element);
const initialFormState = fromNativeElement({
url: await getCurrentInspectedURL(),
starterBrickMetadata: internalStarterBrickMetaFactory(),
modMetadata:
modMetadata ??
createNewUnsavedModMetadata({
modName: generateFreshModName(),
}),
element,
});

initialFormState.modComponent.brickPipeline =
getExampleBrickPipeline(starterBrickType);

initialFormState.modMetadata =
modMetadata ??
createNewUnsavedModMetadata({
modName: generateFreshModName(),
});

return initialFormState as ModComponentFormState;
},
[
Expand Down
2 changes: 1 addition & 1 deletion src/pageEditor/hooks/useClearModChanges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ function useClearModChanges(): (modId: RegistryId) => Promise<void> {

await Promise.all(
modComponentFormStates
.filter((x) => x.modMetadata?.id === modId)
.filter((x) => x.modMetadata.id === modId)
.map(async (modComponentFormState) =>
clearModComponentChanges({
modComponentId: modComponentFormState.uuid,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ const ActivatedModComponentListItem: React.FunctionComponent<{
);
const isActive = activeModComponentFormState?.uuid === modComponent.id;
// Get the selected mod id, or the mod id of the selected mod component
const modId = activeModId ?? activeModComponentFormState?.modMetadata?.id;
const modId = activeModId ?? activeModComponentFormState?.modMetadata.id;
// Set the alternate background if this item isn't active, but either its mod or another item in its mod is active
const hasActiveModBackground =
!isActive && modId && modComponent._recipe?.id === modId;
Expand Down
50 changes: 10 additions & 40 deletions src/pageEditor/modListingPanel/DraftModComponentListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,9 @@ import ModComponentActionMenu from "@/pageEditor/modListingPanel/ModComponentAct
import useClearModComponentChanges from "@/pageEditor/hooks/useClearModComponentChanges";
import {
useRemoveModComponentFromStorage,
DEACTIVATE_MOD_MODAL_PROPS,
DELETE_STANDALONE_MOD_COMPONENT_MODAL_PROPS,
DELETE_STARTER_BRICK_MODAL_PROPS,
} from "@/pageEditor/hooks/useRemoveModComponentFromStorage";
import { selectIsModComponentSavedOnCloud } from "@/store/modComponents/modComponentSelectors";
import { inspectedTab } from "@/pageEditor/context/connection";
import { StarterBrickTypes } from "@/types/starterBrickTypes";

Expand All @@ -78,19 +76,16 @@ const DraftModComponentListItem: React.FunctionComponent<

const isActive =
activeModComponentFormState?.uuid === modComponentFormState.uuid;
const modId = modComponentFormState.modMetadata?.id;
const isSiblingOfActiveListItem = activeModComponentFormState?.modMetadata?.id
? modId === activeModComponentFormState?.modMetadata?.id
const modId = modComponentFormState.modMetadata.id;
const isSiblingOfActiveListItem = activeModComponentFormState?.modMetadata.id
? modId === activeModComponentFormState?.modMetadata.id
: false;
const isChildOfActiveListItem = modId === activeModId;
const isRelativeOfActiveListItem =
!isActive && (isChildOfActiveListItem || isSiblingOfActiveListItem);
const isDirty = useSelector(
selectModComponentIsDirty(modComponentFormState.uuid),
);
const isSavedOnCloud = useSelector(
selectIsModComponentSavedOnCloud(modComponentFormState.uuid),
);
const removeModComponentFromStorage = useRemoveModComponentFromStorage();
const isButton =
modComponentFormState.starterBrick.definition.type ===
Expand All @@ -114,12 +109,6 @@ const DraftModComponentListItem: React.FunctionComponent<
: DELETE_STANDALONE_MOD_COMPONENT_MODAL_PROPS,
});

const deactivateModComponent = async () =>
removeModComponentFromStorage({
modComponentId: modComponentFormState.uuid,
showConfirmationModal: DEACTIVATE_MOD_MODAL_PROPS,
});

const onSave = useMemo(() => {
if (modComponentFormState.modMetadata == null) {
return async () => {
Expand All @@ -135,10 +124,6 @@ const DraftModComponentListItem: React.FunctionComponent<
const onClearChanges = async () =>
clearModComponentChanges({ modComponentId: modComponentFormState.uuid });

const onDelete = modId || !isSavedOnCloud ? deleteModComponent : undefined;

const onDeactivate = onDelete ? undefined : deactivateModComponent;

return (
<ListGroup.Item
className={cx(styles.root, {
Expand Down Expand Up @@ -202,7 +187,7 @@ const DraftModComponentListItem: React.FunctionComponent<
isActive={isActive}
isDirty={isDirty}
labelRoot={getLabel(modComponentFormState)}
onDeactivate={onDeactivate}
onDelete={deleteModComponent}
onDuplicate={async () => {
dispatch(
actions.duplicateActiveModComponent({
Expand All @@ -214,27 +199,12 @@ const DraftModComponentListItem: React.FunctionComponent<
onClearChanges={
modComponentFormState.installed ? onClearChanges : undefined
}
onMoveToMod={
modComponentFormState.modMetadata
? async () => {
dispatch(
actions.showMoveCopyToModModal({ moveOrCopy: "move" }),
);
}
: undefined
}
onCopyToMod={
modComponentFormState.modMetadata
? async () => {
dispatch(
actions.showMoveCopyToModModal({ moveOrCopy: "copy" }),
);
}
: undefined
}
// TODO: https://github.com/pixiebrix/pixiebrix-extension/issues/9242, remove standalone mod component actions
onSave={onSave}
onDelete={onDelete}
onMoveToMod={async () => {
dispatch(actions.showMoveCopyToModModal({ moveOrCopy: "move" }));
}}
onCopyToMod={async () => {
dispatch(actions.showMoveCopyToModModal({ moveOrCopy: "copy" }));
}}
/>
</ListGroup.Item>
);
Expand Down
41 changes: 8 additions & 33 deletions src/pageEditor/modListingPanel/ModComponentActionMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,44 +20,36 @@ import {
faClone,
faFileExport,
faHistory,
faTimes,
faTrash,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import styles from "./ActionMenu.module.scss";
import EllipsisMenu, {
type EllipsisMenuItem,
} from "@/components/ellipsisMenu/EllipsisMenu";
import SaveButton from "@/pageEditor/modListingPanel/SaveButton";

type OptionalAction = (() => Promise<void>) | undefined;

type ActionMenuProps = {
labelRoot: string;
isDirty: boolean;
isActive: boolean;
onDelete: OptionalAction;
onDuplicate: OptionalAction;
onDelete: () => Promise<void>;
onDuplicate: () => Promise<void>;
onClearChanges: OptionalAction;
onMoveToMod: OptionalAction;
onCopyToMod: OptionalAction;
// TODO: https://github.com/pixiebrix/pixiebrix-extension/issues/9242, remove standalone mod component actions
onSave: OptionalAction;
onDeactivate: OptionalAction;
onMoveToMod: () => Promise<void>;
onCopyToMod: () => Promise<void>;
};

const ModComponentActionMenu: React.FC<ActionMenuProps> = ({
isActive,
labelRoot,
isDirty,
onDelete = null,
onDuplicate = null,
onDelete,
onDuplicate,
onClearChanges = null,
onMoveToMod = null,
onCopyToMod = null,
// Standalone Mod Component Actions
onSave = null,
onDeactivate = null,
onMoveToMod,
onCopyToMod,
}) => {
const menuItems: EllipsisMenuItem[] = [
{
Expand All @@ -71,7 +63,6 @@ const ModComponentActionMenu: React.FC<ActionMenuProps> = ({
title: "Duplicate",
icon: <FontAwesomeIcon icon={faClone} fixedWidth />,
action: onDuplicate,
hide: !onDuplicate,
},
{
title: "Move to mod",
Expand All @@ -83,7 +74,6 @@ const ModComponentActionMenu: React.FC<ActionMenuProps> = ({
/>
),
action: onMoveToMod,
hide: !onMoveToMod,
},
{
title: "Copy to mod",
Expand All @@ -95,31 +85,16 @@ const ModComponentActionMenu: React.FC<ActionMenuProps> = ({
/>
),
action: onCopyToMod,
hide: !onCopyToMod,
},
{
title: "Delete",
icon: <FontAwesomeIcon icon={faTrash} fixedWidth />,
action: onDelete,
hide: !onDelete,
},
{
title: "Deactivate",
icon: <FontAwesomeIcon icon={faTimes} fixedWidth />,
action: onDeactivate,
hide: !onDeactivate,
},
];

return (
<div className={styles.root}>
{onSave != null && (
<SaveButton
ariaLabel={labelRoot ? `${labelRoot} - Save` : undefined}
onClick={onSave}
disabled={!isDirty}
/>
)}
{isActive && (
<EllipsisMenu
portal
Expand Down
2 changes: 1 addition & 1 deletion src/pageEditor/modListingPanel/ModListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const ModListItem: React.FC<ModListItemProps> = ({

// Set the alternate background if a mod component in this mod is active
const hasModBackground =
activeModComponentFormState?.modMetadata?.id === modId;
activeModComponentFormState?.modMetadata.id === modId;

const dirtyName = useSelector(selectDirtyMetadataForModId(modId))?.name;
const name = dirtyName ?? savedName ?? "Loading...";
Expand Down
2 changes: 1 addition & 1 deletion src/pageEditor/modListingPanel/modals/CreateModModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ const CreateModModalBody: React.FC = () => {
// `selectActiveModId` returns the mod id if a mod entry is selected (not a mod component within the mod)
const directlyActiveModId = useSelector(selectActiveModId);
const activeModId =
directlyActiveModId ?? activeModComponentFormState?.modMetadata?.id;
directlyActiveModId ?? activeModComponentFormState?.modMetadata.id;

const { data: activeModDefinition, isFetching: isModDefinitionFetching } =
useOptionalModDefinition(activeModId);
Expand Down
29 changes: 21 additions & 8 deletions src/pageEditor/starterBricks/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ import { hasInnerStarterBrickRef } from "@/registry/hydrateInnerDefinitions";
import { normalizePipelineForEditor } from "./pipelineMapping";
import { emptyPermissionsFactory } from "@/permissions/permissionsUtils";
import { type ApiVersion } from "@/types/runtimeTypes";
import { type ModComponentBase } from "@/types/modComponentTypes";
import {
type ModComponentBase,
type ModMetadata,
} from "@/types/modComponentTypes";
import { type Schema } from "@/types/schemaTypes";
import { type SafeString, type UUID } from "@/types/stringTypes";
import { isExpression } from "@/utils/expressionUtils";
Expand All @@ -68,6 +71,7 @@ import {
} from "@/types/availabilityTypes";
import { normalizeAvailability } from "@/bricks/available";
import { registry } from "@/background/messenger/api";
import { assertNotNullish } from "@/utils/nullishUtils";

export interface WizardStep {
step: string;
Expand Down Expand Up @@ -129,6 +133,11 @@ export function baseFromModComponent<T extends StarterBrickType>(
| "variablesDefinition"
| "modMetadata"
> & { type: T } {
assertNotNullish(
config._recipe,
"Standalone mod components are not supported",
);

return {
uuid: config.id,
apiVersion: config.apiVersion,
Expand All @@ -152,9 +161,9 @@ export function initModOptionsIfNeeded<TFormState extends BaseFormState>(
modComponentFormState: TFormState,
modDefinitions: ModDefinition[],
) {
if (modComponentFormState.modMetadata?.id) {
if (modComponentFormState.modMetadata.id) {
const mod = modDefinitions?.find(
(x) => x.metadata.id === modComponentFormState.modMetadata?.id,
(x) => x.metadata.id === modComponentFormState.modMetadata.id,
);

if (mod?.options == null) {
Expand Down Expand Up @@ -209,20 +218,24 @@ export function baseSelectModComponent({
};
}

export function makeInitialBaseState(
uuid: UUID = uuidv4(),
): Except<BaseFormState, "label" | "starterBrick"> {
export function makeInitialBaseState({
modMetadata,
modComponentId = uuidv4(),
}: {
modComponentId?: UUID;
modMetadata: ModMetadata;
}): Except<BaseFormState, "label" | "starterBrick"> {
return {
uuid,
uuid: modComponentId,
apiVersion: PAGE_EDITOR_DEFAULT_BRICK_API_VERSION,
modMetadata,
integrationDependencies: [],
permissions: emptyPermissionsFactory(),
optionsArgs: {},
variablesDefinition: emptyModVariablesDefinitionFactory(),
modComponent: {
brickPipeline: [],
},
modMetadata: undefined,
};
}

Expand Down
Loading

0 comments on commit ff96cba

Please sign in to comment.