Skip to content

Commit

Permalink
#7111 Fix deactivation/delete actions for unsaved standalone mod comp…
Browse files Browse the repository at this point in the history
…onents (#7121)

* refactor rename useSaveExtension -> useSaveStandaloneModComponent

* refactor rename saveExtension -> saveStandaloneModComponent

* rename saveExtension -> saveModComponent

* add upsertStandaloneModComponent to rtk query endpoints

* update modal message

* add isSavedOnMod via selector

* create selector for isSavedOnCloud

* introduce modal constants

* refactor remove useDeactivateModComponent/useDeleteModComponent

* refactor use useRemoveModComponent

* change type of props for useRemoveModComponentFromStorage

* refactor rename useRemoveModComponent -> useRemoveModComponentFromStorage

* remove useDeactivate... useDelete... helper hooks

* update comment

* remove showConfiramtionModal prop

* update callback dependency props

* revert unused changes

* change config prop
  • Loading branch information
mnholtz authored Dec 15, 2023
1 parent 601b4b5 commit 983ae0c
Show file tree
Hide file tree
Showing 9 changed files with 151 additions and 150 deletions.
13 changes: 7 additions & 6 deletions src/components/ConfirmationModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import React, {
import { Modal, Button } from "react-bootstrap";
import { type ButtonVariant } from "react-bootstrap/types";

type ModalProps = {
export type ConfirmationModalProps = {
title?: string;
message: string | React.ReactElement;
submitVariant?: ButtonVariant;
Expand All @@ -34,7 +34,7 @@ type ModalProps = {
};

type ModalContextProps = {
showConfirmation: (modalProps: ModalProps) => Promise<boolean>;
showConfirmation: (modalProps: ConfirmationModalProps) => Promise<boolean>;
};

const initialModalState: ModalContextProps = {
Expand All @@ -46,7 +46,7 @@ const initialModalState: ModalContextProps = {
const ModalContext = createContext<ModalContextProps>(initialModalState);

const ConfirmationModal: React.FunctionComponent<
ModalProps & {
ConfirmationModalProps & {
onCancel: () => void;
onSubmit: () => void;
onExited: () => void;
Expand Down Expand Up @@ -87,14 +87,15 @@ const ConfirmationModal: React.FunctionComponent<

type Callback = (submit: boolean) => void;

const DEFAULT_MODAL_PROPS: ModalProps = {
const DEFAULT_MODAL_PROPS: ConfirmationModalProps = {
message: "Are you sure?",
};

export const ModalProvider: React.FunctionComponent<{
children: React.ReactNode;
}> = ({ children }) => {
const [modalProps, setModalProps] = useState<ModalProps>(DEFAULT_MODAL_PROPS);
const [modalProps, setModalProps] =
useState<ConfirmationModalProps>(DEFAULT_MODAL_PROPS);
const [callback, setCallback] = useState<Callback | null>();
const [isModalVisible, setIsModalVisible] = useState<boolean>(false);
useEffect(
Expand All @@ -106,7 +107,7 @@ export const ModalProvider: React.FunctionComponent<{
);

const showConfirmation = useCallback(
async (modalProps: ModalProps) => {
async (modalProps: ConfirmationModalProps) => {
// Cancel any previous modal that was showing
callback?.(false);

Expand Down
35 changes: 15 additions & 20 deletions src/pageEditor/hooks/useDeactivateMod.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import React, { useCallback } from "react";
import { useCallback } from "react";
import { type RegistryId } from "@/types/registryTypes";
import { useDeactivateModComponent } from "@/pageEditor/hooks/useRemoveModComponent";
import {
DEACTIVATE_MOD_MODAL_PROPS,
useRemoveModComponentFromStorage,
} from "@/pageEditor/hooks/useRemoveModComponentFromStorage";
import { useDispatch, useSelector } from "react-redux";
import { selectExtensions } from "@/store/extensionsSelectors";
import { selectElements } from "@/pageEditor/slices/editorSelectors";
Expand All @@ -37,28 +40,15 @@ type Config = {
*/
function useDeactivateMod(): (useDeactivateConfig: Config) => Promise<void> {
const dispatch = useDispatch();
const deactivateModComponent = useDeactivateModComponent();
const removeModComponentFromStorage = useRemoveModComponentFromStorage();
const extensions = useSelector(selectExtensions);
const elements = useSelector(selectElements);
const { showConfirmation } = useModals();

return useCallback(
async ({ modId, shouldShowConfirmation = true }) => {
if (shouldShowConfirmation) {
const confirmed = await showConfirmation({
title: "Deactivate Mod?",
message: (
<>
This action will deactivate the mod and remove it from the Page
Editor. You can reactivate or delete mods from the{" "}
<a href="/options.html" target="_blank">
PixieBrix Extension Console
</a>
.
</>
),
submitCaption: "Deactivate",
});
const confirmed = await showConfirmation(DEACTIVATE_MOD_MODAL_PROPS);

if (!confirmed) {
return;
Expand All @@ -72,9 +62,8 @@ function useDeactivateMod(): (useDeactivateConfig: Config) => Promise<void> {
);
await Promise.all(
extensionIds.map(async (extensionId) =>
deactivateModComponent({
removeModComponentFromStorage({
extensionId,
shouldShowConfirmation: false,
}),
),
);
Expand All @@ -85,7 +74,13 @@ function useDeactivateMod(): (useDeactivateConfig: Config) => Promise<void> {

dispatch(actions.removeRecipeData(modId));
},
[dispatch, elements, extensions, deactivateModComponent, showConfirmation],
[
dispatch,
elements,
extensions,
useRemoveModComponentFromStorage,
showConfirmation,
],
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@

import { renderHook } from "@/pageEditor/testHelpers";
import { removeExtensionsFromAllTabs } from "@/store/uninstallUtils";
import {
useDeactivateModComponent,
useDeleteModComponent,
} from "./useRemoveModComponent";
import { useRemoveModComponentFromStorage } from "./useRemoveModComponentFromStorage";
import { actions as editorActions } from "@/pageEditor/slices/editorSlice";
import { actions as extensionsActions } from "@/store/extensionsSlice";
import { clearDynamicElements } from "@/contentScript/messenger/api";
Expand All @@ -31,36 +28,32 @@ beforeEach(() => {
jest.resetAllMocks();
});

test("useRemoveModComponent", async () => {
test("useRemoveModComponentFromStorage", async () => {
const extensionId = uuidSequence(1);

// eslint-disable-next-line unicorn/no-array-for-each -- Better readability in this case, and performance is not a concern here
[useDeactivateModComponent, useDeleteModComponent].forEach(async (hook) => {
const {
result: { current: removeExtension },
getReduxStore,
} = renderHook(() => hook(), {
setupRedux(dispatch, { store }) {
jest.spyOn(store, "dispatch");
},
});
const {
result: { current: removeExtension },
getReduxStore,
} = renderHook(() => useRemoveModComponentFromStorage(), {
setupRedux(dispatch, { store }) {
jest.spyOn(store, "dispatch");
},
});

await removeExtension({
extensionId,
shouldShowConfirmation: false,
});
await removeExtension({
extensionId,
});

const { dispatch } = getReduxStore();
const { dispatch } = getReduxStore();

expect(dispatch).toHaveBeenCalledWith(
editorActions.removeElement(extensionId),
);
expect(dispatch).toHaveBeenCalledWith(
extensionsActions.removeExtension({ extensionId }),
);
expect(clearDynamicElements).toHaveBeenCalledWith(expect.any(Object), {
uuid: extensionId,
});
expect(removeExtensionsFromAllTabs).toHaveBeenCalledWith([extensionId]);
expect(dispatch).toHaveBeenCalledWith(
editorActions.removeElement(extensionId),
);
expect(dispatch).toHaveBeenCalledWith(
extensionsActions.removeExtension({ extensionId }),
);
expect(clearDynamicElements).toHaveBeenCalledWith(expect.any(Object), {
uuid: extensionId,
});
expect(removeExtensionsFromAllTabs).toHaveBeenCalledWith([extensionId]);
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@
import { type UUID } from "@/types/stringTypes";
import { useDispatch, useSelector } from "react-redux";
import { selectSessionId } from "@/pageEditor/slices/sessionSelectors";
import { useModals } from "@/components/ConfirmationModal";
import { useCallback } from "react";
import {
type ConfirmationModalProps,
useModals,
} from "@/components/ConfirmationModal";
import React, { useCallback } from "react";
import reportEvent from "@/telemetry/reportEvent";
import { Events } from "@/telemetry/events";
import notify from "@/utils/notify";
Expand All @@ -31,24 +34,64 @@ import { removeExtensionsFromAllTabs } from "@/store/uninstallUtils";

type Config = {
extensionId: UUID;
shouldShowConfirmation?: boolean;
// Show a confirmation modal with the specified modal props before removing the mod component if defined
showConfirmationModal?: ConfirmationModalProps;
};

export const DELETE_STARTER_BRICK_MODAL_PROPS: ConfirmationModalProps = {
title: "Delete starter brick?",
message: "This action cannot be undone.",
submitCaption: "Delete",
};

export const DELETE_STANDALONE_MOD_COMPONENT_MODAL_PROPS: ConfirmationModalProps =
{
title: "Delete mod?",
message: "This action cannot be undone.",
submitCaption: "Delete",
};

export const DEACTIVATE_MOD_MODAL_PROPS: ConfirmationModalProps = {
title: "Deactivate Mod?",
message: (
<>
Any unsaved changes will be lost. You can reactivate or delete mods from
the{" "}
<a href="/options.html" target="_blank">
PixieBrix Extension Console
</a>
.
</>
),
submitCaption: "Deactivate",
};

/**
* Returns a callback that removes a mod component from the Page Editor and Extension Storage.
*
* For mod components (packaged inside a mod), this callback will effectively delete the mod component.
* For standalone mods, this callback will simply deactivate the mod and remove it from the Page Editor.
* For mod components packaged inside a mod and standalone mod components not saved on the cloud, this callback will effectively delete the mod component.
* For saved standalone mods, this callback will simply deactivate the mod and remove it from the Page Editor.
*
* Prefer using `useDeactivateModComponent` or `useDeleteModComponent` instead of exporting this hook.
* In both cases, unsaved changes will be lost.
**/
function _useRemoveModComponent(): (extensionId: UUID) => Promise<void> {
export function useRemoveModComponentFromStorage(): (
useRemoveConfig: Config,
) => Promise<void> {
const dispatch = useDispatch();
const sessionId = useSelector(selectSessionId);
const { showConfirmation } = useModals();

return useCallback(
async (extensionId) => {
console.debug(`pageEditor: remove extension with id ${extensionId}`);
async ({ extensionId, showConfirmationModal }) => {
console.debug(`pageEditor: remove mod component with id ${extensionId}`);

if (showConfirmationModal) {
const confirm = await showConfirmation(showConfirmationModal);

if (!confirm) {
return;
}
}

reportEvent(Events.PAGE_EDITOR_REMOVE, {
sessionId,
Expand Down Expand Up @@ -81,59 +124,6 @@ function _useRemoveModComponent(): (extensionId: UUID) => Promise<void> {
});
}
},
[dispatch, sessionId],
[dispatch, sessionId, showConfirmation],
);
}

export const useDeactivateModComponent = (): ((
useRemoveConfig: Config,
) => Promise<void>) => {
const removeModComponent = _useRemoveModComponent();
const { showConfirmation } = useModals();

return useCallback(
async ({ extensionId, shouldShowConfirmation = true }) => {
if (shouldShowConfirmation) {
const confirm = await showConfirmation({
title: "Deactivate Mod?",
message:
"This action will deactivate the mod and remove it from the Page Editor. You can reactivate or delete mods from the PixieBrix Extension Console.",
submitCaption: "Deactivate",
});

if (!confirm) {
return;
}
}

await removeModComponent(extensionId);
},
[removeModComponent, showConfirmation],
);
};

export const useDeleteModComponent = (): ((
useRemoveConfig: Config,
) => Promise<void>) => {
const removeModComponent = _useRemoveModComponent();
const { showConfirmation } = useModals();

return useCallback(
async ({ extensionId, shouldShowConfirmation = true }) => {
if (shouldShowConfirmation) {
const confirm = await showConfirmation({
title: "Delete starter brick?",
message: "This action cannot be undone.",
submitCaption: "Delete",
});

if (!confirm) {
return;
}
}

await removeModComponent(extensionId);
},
[removeModComponent, showConfirmation],
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type ExtensionSaver = {
isSaving: boolean;
};

function useSaveExtension(): ExtensionSaver {
function useSaveStandaloneModComponent(): ExtensionSaver {
const [isSaving, setIsSaving] = useState(false);
const create = useUpsertFormElement();
const sessionId = useSelector(selectSessionId);
Expand Down Expand Up @@ -67,4 +67,4 @@ function useSaveExtension(): ExtensionSaver {
};
}

export default useSaveExtension;
export default useSaveStandaloneModComponent;
Loading

0 comments on commit 983ae0c

Please sign in to comment.