Skip to content

Commit

Permalink
feat(app-page-builder): add duplicate page list and bulk action (#3876)
Browse files Browse the repository at this point in the history
  • Loading branch information
leopuleo authored Feb 23, 2024
1 parent 7e364f3 commit 52030fe
Show file tree
Hide file tree
Showing 19 changed files with 476 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ const PublishRevisionDecorator = createDecorator(
);

const PublishPageMenuOptionDecorator = createDecorator(
Components.PageDetails.Toolbar.PublishRevision,
Components.PageDetails.Revisions.Actions.PublishRevision,
OriginalRenderer => {
return function PageReview() {
return function PageReview(props) {
const { revision } = Components.PageDetails.Revisions.useRevision();
const contentReviewId = useContentReviewId(revision.id);
const navigate = useNavigate();
Expand All @@ -57,7 +57,7 @@ const PublishPageMenuOptionDecorator = createDecorator(
}

if (!contentReviewId) {
return <OriginalRenderer />;
return <OriginalRenderer {...props} />;
}

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React, { useMemo } from "react";
import { ReactComponent as Duplicate } from "@material-design-icons/svg/outlined/library_add.svg";
import { useRecords } from "@webiny/app-aco";
import { observer } from "mobx-react-lite";
import { PageListConfig } from "~/admin/config/pages";
import { getPagesLabel } from "~/admin/components/BulkActions/BulkActions";
import { useDuplicatePageCase } from "~/admin/views/Pages/hooks/useDuplicatePage";
import { makeDecoratable } from "@webiny/react-composition";

export const ActionDuplicate = makeDecoratable(
"BulkActionDuplicate",
observer(() => {
const { duplicatePage } = useDuplicatePageCase();
const { getRecord } = useRecords();

const { useWorker, useButtons, useDialog } = PageListConfig.Browser.BulkAction;
const { IconButton } = useButtons();
const worker = useWorker();
const { showConfirmationDialog, showResultsDialog } = useDialog();

const pagesLabel = useMemo(() => {
return getPagesLabel(worker.items.length);
}, [worker.items.length]);

const openDuplicatePagesDialog = () =>
showConfirmationDialog({
title: "Duplicate pages",
message: `You are about to duplicate ${pagesLabel}. Are you sure you want to continue?`,
loadingLabel: `Processing ${pagesLabel}`,
execute: async () => {
await worker.processInSeries(async ({ item, report }) => {
try {
const data = await duplicatePage({ page: item });

await getRecord(data.pid);

report.success({
title: `${item.data.title}`,
message: "Page successfully duplicated."
});
} catch (e) {
report.error({
title: `${item.data.title}`,
message: e.message
});
}
});

worker.resetItems();

showResultsDialog({
results: worker.results,
title: "Duplicate pages",
message: "Finished duplicating pages! See full report below:"
});
}
});

return (
<IconButton
icon={<Duplicate />}
onAction={openDuplicatePagesDialog}
label={`Duplicate ${pagesLabel}`}
tooltipPlacement={"bottom"}
/>
);
})
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React, { useMemo } from "react";
import { useFolders } from "@webiny/app-aco";
import { observer } from "mobx-react-lite";
import { PageListConfig } from "~/admin/config/pages";
import { usePagesPermissions } from "~/hooks/permissions";
import { ActionDuplicate } from "~/admin/components/BulkActions/ActionDuplicate";
import { createDecorator } from "@webiny/react-composition";

export const SecureActionDuplicate = createDecorator(ActionDuplicate, Original => {
return observer(() => {
const { canWrite: pagesCanWrite } = usePagesPermissions();
const { folderLevelPermissions: flp } = useFolders();

const { useWorker } = PageListConfig.Browser.BulkAction;
const worker = useWorker();

const canDuplicateAll = useMemo(() => {
return worker.items.every(item => {
return (
pagesCanWrite(item.data.createdBy.id) &&
flp.canManageContent(item.location?.folderId)
);
});
}, [worker.items]);

if (!canDuplicateAll) {
console.log("You don't have permissions to duplicate pages.");
return null;
}

return <Original />;
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ export { ActionDelete } from "./ActionDelete";
export { SecureActionDelete } from "./SecureActionDelete";
export { ActionExport } from "./ActionExport";
export { ActionMove } from "./ActionMove";
export { ActionDuplicate } from "./ActionDuplicate";
export { SecureActionDuplicate } from "./SecureActionDuplicate";
export { SecureActionMove } from "./SecureActionMove";
export { ActionPublish } from "./ActionPublish";
export { SecureActionPublish } from "./SecureActionPublish";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React, { useCallback } from "react";
import { ReactComponent as Duplicate } from "@material-design-icons/svg/outlined/library_add.svg";
import { makeDecoratable } from "@webiny/react-composition";
import { PageListConfig } from "~/admin/config/pages";
import { usePage } from "~/admin/views/Pages/hooks/usePage";
import { useDuplicatePage } from "~/admin/views/Pages/hooks/useDuplicatePage";

export const DuplicatePage = makeDecoratable("TableActionDuplicatePage", () => {
const { page } = usePage();
const { duplicatePage } = useDuplicatePage();
const { OptionsMenuItem } = PageListConfig.Browser.PageAction;

const onAction = useCallback(async () => {
await duplicatePage({ page });
}, [page]);

return (
<OptionsMenuItem
icon={<Duplicate />}
label={"Duplicate"}
onAction={onAction}
data-testid={"aco.actions.pb.page.duplicate"}
/>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React, { useMemo } from "react";
import { useFolders } from "@webiny/app-aco";
import { createDecorator } from "@webiny/react-composition";
import { usePagesPermissions } from "~/hooks/permissions";
import { usePage } from "~/admin/views/Pages/hooks/usePage";
import { DuplicatePage } from "./DuplicatePage";

export const SecureDuplicatePage = createDecorator(DuplicatePage, Original => {
return function SecureDuplicatePageRenderer() {
const { page } = usePage();
const { folderLevelPermissions: flp } = useFolders();
const { canWrite: pagesCanWrite } = usePagesPermissions();

const { folderId } = page.location;

const canDuplicate = useMemo(() => {
return pagesCanWrite(page.data.createdBy.id) && flp.canManageContent(folderId);
}, [flp, folderId]);

if (!canDuplicate) {
return null;
}

return <Original />;
};
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ export * from "./ChangePageStatus";
export * from "./SecureChangePageStatus";
export * from "./DeletePage";
export * from "./SecureDeletePage";
export * from "./DuplicatePage";
export * from "./SecureDuplicatePage";
export * from "./EditPage";
export * from "./SecureEditPage";
export * from "./MovePage";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React, { useCallback } from "react";
import { useRouter } from "@webiny/react-router";
import { usePage } from "~/admin/views/Pages/PageDetails";
import { DuplicatePageMenuItem } from "./DuplicatePageMenuItem";
import { useDuplicatePage } from "~/admin/views/Pages/hooks/useDuplicatePage";

interface DefaultDuplicatePageProps {
label: React.ReactNode;
icon: React.ReactElement;
}

export const DefaultDuplicatePage = (props: DefaultDuplicatePageProps) => {
const { page } = usePage();
const { duplicatePage } = useDuplicatePage();
const { history } = useRouter();

const onClick = useCallback(async () => {
await duplicatePage({
page,
onSuccess: page => {
history.push(`/page-builder/pages?id=${encodeURIComponent(page.id)}`);
}
});
}, [page]);

return <DuplicatePageMenuItem icon={props.icon} onClick={onClick} label={props.label} />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from "react";
import { ReactComponent as DuplicateIcon } from "@material-design-icons/svg/filled/library_add.svg";
import { makeDecoratable } from "@webiny/app-admin";
import { DefaultDuplicatePage } from "./DefaultDuplicatePage";
import { DuplicatePageMenuItem } from "./DuplicatePageMenuItem";

export interface DuplicatePageProps {
icon?: React.ReactElement;
label?: React.ReactNode;
onClick?: () => void;
}

export const DuplicatePage = makeDecoratable("DuplicatePage", (props: DuplicatePageProps) => {
const duplicateButtonLabel = "Duplicate";

if (!props.onClick) {
return (
<DefaultDuplicatePage
label={props.label ?? duplicateButtonLabel}
icon={props.icon ?? <DuplicateIcon />}
/>
);
}

return (
<DuplicatePageMenuItem
label={props.label ?? duplicateButtonLabel}
icon={props.icon ?? <DuplicateIcon />}
onClick={props.onClick}
/>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from "react";
import { MenuItem } from "@webiny/ui/Menu";
import { ListItemGraphic } from "@webiny/ui/List";
import { Icon } from "@webiny/ui/Icon";
import { usePagesPermissions } from "~/hooks/permissions";

export interface DuplicatePageMenuItemProps {
onClick: () => void;
label: React.ReactNode;
icon: React.ReactElement;
}

export const DuplicatePageMenuItem = (props: DuplicatePageMenuItemProps) => {
const { canWrite } = usePagesPermissions();

if (!canWrite) {
return null;
}

return (
<MenuItem onClick={props.onClick}>
<ListItemGraphic>
<Icon
data-testid="pb-page-details-header-page-options-menu-duplicate"
icon={props.icon}
/>
</ListItemGraphic>
{props.label}
</MenuItem>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React, { useMemo } from "react";
import { useFolders } from "@webiny/app-aco";
import { createDecorator } from "@webiny/react-composition";
import { usePage } from "~/admin/views/Pages/PageDetails";
import { usePagesPermissions } from "~/hooks/permissions";
import { DuplicatePage } from "./DuplicatePage";

export const SecureDuplicatePage = createDecorator(DuplicatePage, Original => {
return function SecurePageDetailsDuplicatePageRenderer() {
const { page } = usePage();
const { folderLevelPermissions: flp } = useFolders();
const { canWrite: pagesCanWrite } = usePagesPermissions();

const canDuplicate = useMemo(() => {
if (!page || Object.keys(page).length === 0) {
// Page data is not available yet
return false;
}

return (
pagesCanWrite(page.createdBy.id) &&
flp.canManageContent(page.wbyAco_location.folderId)
);
}, [flp, page]);

if (!canDuplicate) {
return null;
}

return <Original />;
};
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./DefaultDuplicatePage";
export * from "./DuplicatePage";
export * from "./DuplicatePageMenuItem";
export * from "./SecureDuplicatePage";
Loading

0 comments on commit 52030fe

Please sign in to comment.