diff --git a/platform/wab/src/wab/client/components/cms/CmsEntryDetails.tsx b/platform/wab/src/wab/client/components/cms/CmsEntryDetails.tsx index f35be68efb5..3d7d3a21f20 100644 --- a/platform/wab/src/wab/client/components/cms/CmsEntryDetails.tsx +++ b/platform/wab/src/wab/client/components/cms/CmsEntryDetails.tsx @@ -13,6 +13,8 @@ import { renderMaybeLocalizedInput, } from "@/wab/client/components/cms/CmsInputs"; import { isCmsTextLike } from "@/wab/client/components/cms/utils"; +import Button from "@/wab/client/components/widgets/Button"; +import { Modal } from "@/wab/client/components/widgets/Modal"; import { useApi, useAppCtx } from "@/wab/client/contexts/AppContexts"; import { DefaultCmsEntryDetailsProps, @@ -33,7 +35,15 @@ import { spawn } from "@/wab/shared/common"; import { DEVFLAGS } from "@/wab/shared/devflags"; import { substituteUrlParams } from "@/wab/shared/utils/url-utils"; import { HTMLElementRefOf } from "@plasmicapp/react-web"; -import { Drawer, Form, Menu, message, notification, Tooltip } from "antd"; +import { + Drawer, + Form, + Input, + Menu, + message, + notification, + Tooltip, +} from "antd"; import { useForm } from "antd/lib/form/Form"; import { isEqual, isNil, mapValues, pickBy } from "lodash"; import * as React from "react"; @@ -175,6 +185,9 @@ function CmsEntryDetailsForm_( const [hasUnpublishedChanges, setHasUnpublishedChanges] = React.useState( !!row?.draftData ); + const [showCopyModal, setShowCopyModal] = React.useState(false); + const [inputCopyIdentifier, setInputCopyIdentifier] = React.useState(""); + const [revision, setRevision] = React.useState(row.revision); const [inConflict, setInConflict] = React.useState(false); const mutateRow_ = useMutateRow(); @@ -572,16 +585,27 @@ function CmsEntryDetailsForm_( content: "Reverted.", }); }} - disabled={isSaving || isPublishing} + disabled={isSaving} > Revert to published entry )} - )} + { + setInputCopyIdentifier( + row.identifier ? `Copy of ${row.identifier}` : "" + ); + setShowCopyModal(true); + }} + disabled={isSaving || isPublishing} + > + Duplicate entry + { @@ -632,6 +656,72 @@ function CmsEntryDetailsForm_( }} /> + {showCopyModal && ( + setShowCopyModal(false)} + > + + + {`Duplicate the CMS entry with identifier "${row.identifier}"?`} +
+ {`This will create an unpublished copy of the entry and all its data.`} +
+ setInputCopyIdentifier(e.target.value)} + /> +
+ + + + +
+ )} ); } diff --git a/platform/wab/src/wab/server/AppServer.ts b/platform/wab/src/wab/server/AppServer.ts index 778dea11f90..2267c76570c 100644 --- a/platform/wab/src/wab/server/AppServer.ts +++ b/platform/wab/src/wab/server/AppServer.ts @@ -61,6 +61,7 @@ import { import { cloneDatabase, cmsFileUpload, + copyRow, createDatabase, createRows, createTable, @@ -826,6 +827,7 @@ export function addCmsEditorRoutes(app: express.Application) { app.get("/api/v1/cmse/rows/:rowId/revisions", withNext(listRowRevisions)); app.put("/api/v1/cmse/rows/:rowId", withNext(updateRow)); app.delete("/api/v1/cmse/rows/:rowId", withNext(deleteRow)); + app.post("/api/v1/cmse/rows/:rowId/copy", withNext(copyRow)); app.get("/api/v1/cmse/row-revisions/:revId", withNext(getRowRevision)); app.post( diff --git a/platform/wab/src/wab/server/db/DbMgr.ts b/platform/wab/src/wab/server/db/DbMgr.ts index 12327ea3b11..d2ddcf92529 100644 --- a/platform/wab/src/wab/server/db/DbMgr.ts +++ b/platform/wab/src/wab/server/db/DbMgr.ts @@ -7342,6 +7342,22 @@ export class DbMgr implements MigrationDbMgr { await this.entMgr.save(row); } + async copyCmsRow( + tableId: CmsTableId, + rowId: CmsRowId, + opts: { + identifier?: string; + } + ) { + await this.checkCmsRowPerms(rowId, "content"); + const row = await this.getCmsRowById(rowId); + const copiedRow = await this.createCmsRow(tableId, { + identifier: opts.identifier || undefined, + draftData: row.draftData || row.data, + }); + return await this.entMgr.save(copiedRow); + } + // TODO We are always querying just the default locale. async queryCmsRows( tableId: CmsTableId, diff --git a/platform/wab/src/wab/server/routes/cmse.ts b/platform/wab/src/wab/server/routes/cmse.ts index b4b49511ab1..0bcc9f1d759 100644 --- a/platform/wab/src/wab/server/routes/cmse.ts +++ b/platform/wab/src/wab/server/routes/cmse.ts @@ -269,6 +269,26 @@ export async function deleteRow(req: Request, res: Response) { res.json({}); } +export async function copyRow(req: Request, res: Response) { + const mgr = userDbMgr(req); + const row = await mgr.getCmsRowById(req.params.rowId as CmsRowId); + userAnalytics(req).track({ + event: "Copy cms row", + properties: { + rowId: row.id as CmsRowId, + tableId: row.tableId, + tableName: row.table?.name, + databaseId: row.table?.databaseId, + }, + }); + const copiedRow = await mgr.copyCmsRow( + row.tableId as CmsTableId, + req.params.rowId as CmsRowId, + req.body + ); + res.json(copiedRow); +} + export async function updateRow(req: Request, res: Response) { const mgr = userDbMgr(req); const row = await mgr.updateCmsRow(req.params.rowId as CmsRowId, req.body); diff --git a/platform/wab/src/wab/shared/SharedApi.ts b/platform/wab/src/wab/shared/SharedApi.ts index 3cfa7a2ddae..cba25f95d2e 100644 --- a/platform/wab/src/wab/shared/SharedApi.ts +++ b/platform/wab/src/wab/shared/SharedApi.ts @@ -1724,6 +1724,15 @@ export abstract class SharedApi { return (await this.put(`/cmse/rows/${rowId}`, opts)) as ApiCmseRow; } + async copyCmsRow( + rowId: CmsRowId, + opts: { + identifier: string; + } + ) { + return (await this.post(`/cmse/rows/${rowId}/copy`, opts)) as ApiCmseRow; + } + async deleteCmsRow(rowId: CmsRowId) { return await this.delete(`/cmse/rows/${rowId}`); }