strapi-plugin-preview-button
Version:
A plugin for Strapi CMS that adds a preview button and live view button to the content manager edit view.
973 lines (971 loc) • 37.6 kB
JavaScript
import { jsx, Fragment, jsxs } from "react/jsx-runtime";
import * as React from "react";
import { c as contentManagerApi, C as COLLECTION_TYPES, a as createContext, u as useRBAC, P as PERMISSIONS, b as Page, D as DocumentRBAC, d as useDocument, e as useDocumentLayout, f as useGetContentTypeConfigurationQuery, g as useQueryParams, h as buildValidParams, i as useDoc, L as Layouts, F as Form, j as useTracking, k as useNotification, l as ForwardRef$55, m as ConfirmDialog, n as ForwardRef$3, o as getDisplayName, R as RelativeTime, p as DocumentStatus, q as useForm, r as useDocumentRBAC, s as useStrapiApp, t as useDocLayout, M as MemoizedInputRenderer, v as useField } from "./index-DX9HaYlZ.mjs";
import { Alert, Link, Portal, FocusTrap, Box, Flex, Main, Divider, Typography, Dialog, Button, Grid, Field, Tooltip } from "@strapi/design-system";
import { stringify } from "qs";
import { useIntl } from "react-intl";
import { useParams, Navigate, NavLink, useNavigate, Link as Link$1 } from "react-router-dom";
import pipe from "lodash/fp/pipe";
import { u as useTypedSelector } from "./hooks-E5u1mcgM-ChYG_e5O.mjs";
import { r as removeFieldsThatDontExistOnSchema, p as prepareTempKeys, u as useDynamicZone, a as useLazyComponents, b as useFieldHint, N as NotAllowedInput, M as MemoizedUIDInput, c as MemoizedWysiwyg, D as DynamicZone, d as MemoizedComponentInput, e as MemoizedBlocksInput } from "./Field-hD0Y00Mt-8YPhpk8A.mjs";
import { styled } from "styled-components";
import { g as getRelationLabel } from "./useDebounce-DmuSJIF3-De6sMYL4.mjs";
const StyledAlert = styled(Alert).attrs({ closeLabel: "Close", onClose: () => {
}, shadow: "none" })`
button {
display: none;
}
`;
const LinkEllipsis = styled(Link)`
display: block;
& > span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
}
`;
const CustomRelationInput = (props) => {
const { formatMessage } = useIntl();
const field = useField(props.name);
let formattedFieldValue;
if (field) {
formattedFieldValue = Array.isArray(field.value) ? { results: field.value, meta: { missingCount: 0 } } : field.value;
}
if (!formattedFieldValue || formattedFieldValue.results.length === 0 && formattedFieldValue.meta.missingCount === 0) {
return /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx(Field.Label, { action: props.labelAction, children: props.label }),
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(StyledAlert, { variant: "default", children: formatMessage({
id: "content-manager.history.content.no-relations",
defaultMessage: "No relations."
}) }) })
] });
}
const { results, meta } = formattedFieldValue;
return /* @__PURE__ */ jsxs(Box, { children: [
/* @__PURE__ */ jsx(Field.Label, { children: props.label }),
results.length > 0 && /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 2, marginTop: 1, alignItems: "stretch", children: results.map((relationData) => {
const { targetModel } = props.attribute;
const href = `../${COLLECTION_TYPES}/${targetModel}/${relationData.documentId}`;
const label = getRelationLabel(relationData, props.mainField);
const isAdminUserRelation = targetModel === "admin::user";
return /* @__PURE__ */ jsxs(
Flex,
{
paddingTop: 2,
paddingBottom: 2,
paddingLeft: 4,
paddingRight: 4,
hasRadius: true,
borderColor: "neutral200",
background: "neutral150",
justifyContent: "space-between",
children: [
/* @__PURE__ */ jsx(Box, { minWidth: 0, paddingTop: 1, paddingBottom: 1, paddingRight: 4, children: /* @__PURE__ */ jsx(Tooltip, { label, children: isAdminUserRelation ? /* @__PURE__ */ jsx(Typography, { children: label }) : /* @__PURE__ */ jsx(LinkEllipsis, { tag: NavLink, to: href, children: label }) }) }),
/* @__PURE__ */ jsx(DocumentStatus, { status: relationData.status })
]
},
relationData.documentId ?? relationData.id
);
}) }),
meta.missingCount > 0 && /* @ts-expect-error – we dont need closeLabel */
/* @__PURE__ */ jsx(
StyledAlert,
{
marginTop: 1,
variant: "warning",
title: formatMessage(
{
id: "content-manager.history.content.missing-relations.title",
defaultMessage: "{number, plural, =1 {Missing relation} other {{number} missing relations}}"
},
{ number: meta.missingCount }
),
children: formatMessage(
{
id: "content-manager.history.content.missing-relations.message",
defaultMessage: "{number, plural, =1 {It has} other {They have}} been deleted and can't be restored."
},
{ number: meta.missingCount }
)
}
)
] });
};
const CustomMediaInput = (props) => {
const { value } = useField(props.name);
const results = value ? value.results : [];
const meta = value ? value.meta : { missingCount: 0 };
const { formatMessage } = useIntl();
const fields = useStrapiApp("CustomMediaInput", (state) => state.fields);
const MediaLibrary = fields.media;
return /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 2, alignItems: "stretch", children: [
/* @__PURE__ */ jsx(Form, { method: "PUT", disabled: true, initialValues: { [props.name]: results }, children: /* @__PURE__ */ jsx(MediaLibrary, { ...props, disabled: true, multiple: results.length > 1 }) }),
meta.missingCount > 0 && /* @__PURE__ */ jsx(
StyledAlert,
{
variant: "warning",
closeLabel: "Close",
onClose: () => {
},
title: formatMessage(
{
id: "content-manager.history.content.missing-assets.title",
defaultMessage: "{number, plural, =1 {Missing asset} other {{number} missing assets}}"
},
{ number: meta.missingCount }
),
children: formatMessage(
{
id: "content-manager.history.content.missing-assets.message",
defaultMessage: "{number, plural, =1 {It has} other {They have}} been deleted in the Media Library and can't be restored."
},
{ number: meta.missingCount }
)
}
)
] });
};
const getLabelAction = (labelAction) => {
if (!React.isValidElement(labelAction)) {
return labelAction;
}
const labelActionTitleId = labelAction.props.title.id;
if (labelActionTitleId === "i18n.Field.localized") {
return React.cloneElement(labelAction, {
...labelAction.props,
title: {
id: "history.content.localized",
defaultMessage: "This value is specific to this locale. If you restore this version, the content will not be replaced for other locales."
}
});
}
if (labelActionTitleId === "i18n.Field.not-localized") {
return React.cloneElement(labelAction, {
...labelAction.props,
title: {
id: "history.content.not-localized",
defaultMessage: "This value is common to all locales. If you restore this version and save the changes, the content will be replaced for all locales."
}
});
}
return labelAction;
};
const VersionInputRenderer = ({
visible,
hint: providedHint,
shouldIgnoreRBAC = false,
labelAction,
...props
}) => {
const customLabelAction = getLabelAction(labelAction);
const { formatMessage } = useIntl();
const version = useHistoryContext("VersionContent", (state) => state.selectedVersion);
const configuration = useHistoryContext("VersionContent", (state) => state.configuration);
const fieldSizes = useTypedSelector((state) => state["content-manager"].app.fieldSizes);
const { id, components } = useDoc();
const isFormDisabled = useForm("InputRenderer", (state) => state.disabled);
const isInDynamicZone = useDynamicZone("isInDynamicZone", (state) => state.isInDynamicZone);
const canCreateFields = useDocumentRBAC("InputRenderer", (rbac) => rbac.canCreateFields);
const canReadFields = useDocumentRBAC("InputRenderer", (rbac) => rbac.canReadFields);
const canUpdateFields = useDocumentRBAC("InputRenderer", (rbac) => rbac.canUpdateFields);
const canUserAction = useDocumentRBAC("InputRenderer", (rbac) => rbac.canUserAction);
const editableFields = id ? canUpdateFields : canCreateFields;
const readableFields = id ? canReadFields : canCreateFields;
const canUserReadField = canUserAction(props.name, readableFields, props.type);
const canUserEditField = canUserAction(props.name, editableFields, props.type);
const fields = useStrapiApp("InputRenderer", (app) => app.fields);
const { lazyComponentStore } = useLazyComponents(
attributeHasCustomFieldProperty(props.attribute) ? [props.attribute.customField] : void 0
);
const hint = useFieldHint(providedHint, props.attribute);
const {
edit: { components: componentsLayout }
} = useDocLayout();
if (!visible) {
return null;
}
if (!shouldIgnoreRBAC && !canUserReadField && !isInDynamicZone) {
return /* @__PURE__ */ jsx(NotAllowedInput, { hint, ...props });
}
const fieldIsDisabled = !canUserEditField && !isInDynamicZone || props.disabled || isFormDisabled;
const addedAttributes = version.meta.unknownAttributes.added;
if (Object.keys(addedAttributes).includes(props.name)) {
return /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "flex-start", gap: 1, children: [
/* @__PURE__ */ jsx(Field.Label, { children: props.label }),
/* @__PURE__ */ jsx(
StyledAlert,
{
width: "100%",
closeLabel: "Close",
onClose: () => {
},
variant: "warning",
title: formatMessage({
id: "content-manager.history.content.new-field.title",
defaultMessage: "New field"
}),
children: formatMessage({
id: "content-manager.history.content.new-field.message",
defaultMessage: "This field didn't exist when this version was saved. If you restore this version, it will be empty."
})
}
)
] });
}
if (attributeHasCustomFieldProperty(props.attribute)) {
const CustomInput = lazyComponentStore[props.attribute.customField];
if (CustomInput) {
return /* @__PURE__ */ jsx(
CustomInput,
{
...props,
hint,
labelAction: customLabelAction,
disabled: fieldIsDisabled
}
);
}
return /* @__PURE__ */ jsx(
MemoizedInputRenderer,
{
...props,
hint,
labelAction: customLabelAction,
type: props.attribute.customField,
disabled: fieldIsDisabled
}
);
}
if (props.type === "media") {
return /* @__PURE__ */ jsx(CustomMediaInput, { ...props, labelAction: customLabelAction, disabled: fieldIsDisabled });
}
const addedInputTypes = Object.keys(fields);
if (!attributeHasCustomFieldProperty(props.attribute) && addedInputTypes.includes(props.type)) {
const CustomInput = fields[props.type];
return /* @__PURE__ */ jsx(
CustomInput,
{
...props,
hint,
labelAction: customLabelAction,
disabled: fieldIsDisabled
}
);
}
switch (props.type) {
case "blocks":
return /* @__PURE__ */ jsx(MemoizedBlocksInput, { ...props, hint, type: props.type, disabled: fieldIsDisabled });
case "component":
const { layout } = componentsLayout[props.attribute.component];
const [remainingFieldsLayout] = getRemaingFieldsLayout({
layout: [layout],
metadatas: configuration.components[props.attribute.component].metadatas,
fieldSizes,
schemaAttributes: components[props.attribute.component].attributes
});
return /* @__PURE__ */ jsx(
MemoizedComponentInput,
{
...props,
layout: [...layout, ...remainingFieldsLayout || []],
hint,
labelAction: customLabelAction,
disabled: fieldIsDisabled,
children: (inputProps) => /* @__PURE__ */ jsx(VersionInputRenderer, { ...inputProps, shouldIgnoreRBAC: true })
}
);
case "dynamiczone":
return /* @__PURE__ */ jsx(
DynamicZone,
{
...props,
hint,
labelAction: customLabelAction,
disabled: fieldIsDisabled
}
);
case "relation":
return /* @__PURE__ */ jsx(
CustomRelationInput,
{
...props,
hint,
labelAction: customLabelAction,
disabled: fieldIsDisabled
}
);
case "richtext":
return /* @__PURE__ */ jsx(
MemoizedWysiwyg,
{
...props,
hint,
type: props.type,
labelAction: customLabelAction,
disabled: fieldIsDisabled
}
);
case "uid":
return /* @__PURE__ */ jsx(
MemoizedUIDInput,
{
...props,
hint,
type: props.type,
labelAction: customLabelAction,
disabled: fieldIsDisabled
}
);
case "enumeration":
return /* @__PURE__ */ jsx(
MemoizedInputRenderer,
{
...props,
hint,
labelAction: customLabelAction,
options: props.attribute.enum.map((value) => ({ value })),
type: props.customField ? "custom-field" : props.type,
disabled: fieldIsDisabled
}
);
default:
const { unique: _unique, mainField: _mainField, ...restProps } = props;
return /* @__PURE__ */ jsx(
MemoizedInputRenderer,
{
...restProps,
hint,
labelAction: customLabelAction,
type: props.customField ? "custom-field" : props.type,
disabled: fieldIsDisabled
}
);
}
};
const attributeHasCustomFieldProperty = (attribute) => "customField" in attribute && typeof attribute.customField === "string";
const createLayoutFromFields = (fields) => {
return fields.reduce((rows, field) => {
if (field.type === "dynamiczone") {
rows.push([field]);
return rows;
}
if (!rows[rows.length - 1]) {
rows.push([]);
}
rows[rows.length - 1].push(field);
return rows;
}, []).map((row) => [row]);
};
function getRemaingFieldsLayout({
layout,
metadatas,
schemaAttributes,
fieldSizes
}) {
const fieldsInLayout = layout.flatMap(
(panel) => panel.flatMap((row) => row.flatMap((field) => field.name))
);
const remainingFields = Object.entries(metadatas).reduce(
(currentRemainingFields, [name, field]) => {
if (!fieldsInLayout.includes(name) && field.edit.visible === true) {
const attribute = schemaAttributes[name];
currentRemainingFields.push({
attribute,
type: attribute.type,
visible: true,
disabled: true,
label: field.edit.label || name,
name,
size: fieldSizes[attribute.type].default ?? 12
});
}
return currentRemainingFields;
},
[]
);
return createLayoutFromFields(remainingFields);
}
const FormPanel = ({ panel }) => {
if (panel.some((row) => row.some((field) => field.type === "dynamiczone"))) {
const [row] = panel;
const [field] = row;
return /* @__PURE__ */ jsx(Grid.Root, { gap: 4, children: /* @__PURE__ */ jsx(Grid.Item, { col: 12, s: 12, xs: 12, direction: "column", alignItems: "stretch", children: /* @__PURE__ */ jsx(VersionInputRenderer, { ...field }) }) }, field.name);
}
return /* @__PURE__ */ jsx(
Box,
{
hasRadius: true,
background: "neutral0",
shadow: "tableShadow",
paddingLeft: 6,
paddingRight: 6,
paddingTop: 6,
paddingBottom: 6,
borderColor: "neutral150",
children: /* @__PURE__ */ jsx(Flex, { direction: "column", alignItems: "stretch", gap: 6, children: panel.map((row, gridRowIndex) => /* @__PURE__ */ jsx(Grid.Root, { gap: 4, children: row.map(({ size, ...field }) => {
return /* @__PURE__ */ jsx(
Grid.Item,
{
col: size,
s: 12,
xs: 12,
direction: "column",
alignItems: "stretch",
children: /* @__PURE__ */ jsx(VersionInputRenderer, { ...field })
},
field.name
);
}) }, gridRowIndex)) })
}
);
};
const VersionContent = () => {
const { formatMessage } = useIntl();
const { fieldSizes } = useTypedSelector((state) => state["content-manager"].app);
const version = useHistoryContext("VersionContent", (state) => state.selectedVersion);
const layout = useHistoryContext("VersionContent", (state) => state.layout);
const configuration = useHistoryContext("VersionContent", (state) => state.configuration);
const schema = useHistoryContext("VersionContent", (state) => state.schema);
const removedAttributes = version.meta.unknownAttributes.removed;
const removedAttributesAsFields = Object.entries(removedAttributes).map(
([attributeName, attribute]) => {
const field = {
attribute,
shouldIgnoreRBAC: true,
type: attribute.type,
visible: true,
disabled: true,
label: attributeName,
name: attributeName,
size: fieldSizes[attribute.type].default ?? 12
};
return field;
}
);
const unknownFieldsLayout = createLayoutFromFields(removedAttributesAsFields);
const remainingFieldsLayout = getRemaingFieldsLayout({
metadatas: configuration.contentType.metadatas,
layout,
schemaAttributes: schema.attributes,
fieldSizes
});
const { components } = useDoc();
const transformedData = React.useMemo(() => {
const transform = (schemaAttributes, components2 = {}) => (document) => {
const schema2 = { attributes: schemaAttributes };
const transformations = pipe(
removeFieldsThatDontExistOnSchema(schema2),
prepareTempKeys(schema2, components2)
);
return transformations(document);
};
return transform(version.schema, components)(version.data);
}, [components, version.data, version.schema]);
return /* @__PURE__ */ jsxs(Layouts.Content, { children: [
/* @__PURE__ */ jsx(Box, { paddingBottom: 8, children: /* @__PURE__ */ jsx(Form, { disabled: true, method: "PUT", initialValues: transformedData, children: /* @__PURE__ */ jsx(Flex, { direction: "column", alignItems: "stretch", gap: 6, position: "relative", children: [...layout, ...remainingFieldsLayout].map((panel, index) => {
return /* @__PURE__ */ jsx(FormPanel, { panel }, index);
}) }) }) }),
removedAttributesAsFields.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx(Divider, {}),
/* @__PURE__ */ jsxs(Box, { paddingTop: 8, children: [
/* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "flex-start", paddingBottom: 6, gap: 1, children: [
/* @__PURE__ */ jsx(Typography, { variant: "delta", children: formatMessage({
id: "content-manager.history.content.unknown-fields.title",
defaultMessage: "Unknown fields"
}) }),
/* @__PURE__ */ jsx(Typography, { variant: "pi", children: formatMessage(
{
id: "content-manager.history.content.unknown-fields.message",
defaultMessage: "These fields have been deleted or renamed in the Content-Type Builder. <b>These fields will not be restored.</b>"
},
{
b: (chunks) => /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", children: chunks })
}
) })
] }),
/* @__PURE__ */ jsx(Form, { disabled: true, method: "PUT", initialValues: version.data, children: /* @__PURE__ */ jsx(Flex, { direction: "column", alignItems: "stretch", gap: 6, position: "relative", children: unknownFieldsLayout.map((panel, index) => {
return /* @__PURE__ */ jsx(FormPanel, { panel }, index);
}) }) })
] })
] })
] });
};
const historyVersionsApi = contentManagerApi.injectEndpoints({
endpoints: (builder) => ({
getHistoryVersions: builder.query({
query(params) {
return {
url: `/content-manager/history-versions`,
method: "GET",
config: {
params
}
};
},
providesTags: ["HistoryVersion"]
}),
restoreVersion: builder.mutation({
query({ params, body }) {
return {
url: `/content-manager/history-versions/${params.versionId}/restore`,
method: "PUT",
data: body
};
},
invalidatesTags: (_res, _error, { documentId, collectionType, params }) => {
return [
"HistoryVersion",
{
type: "Document",
id: collectionType === COLLECTION_TYPES ? `${params.contentType}_${documentId}` : params.contentType
}
];
}
})
})
});
const { useGetHistoryVersionsQuery, useRestoreVersionMutation } = historyVersionsApi;
const VersionHeader = ({ headerId }) => {
const [isConfirmDialogOpen, setIsConfirmDialogOpen] = React.useState(false);
const navigate = useNavigate();
const { formatMessage, formatDate } = useIntl();
const { trackUsage } = useTracking();
const { toggleNotification } = useNotification();
const [{ query }] = useQueryParams();
const { collectionType, slug } = useParams();
const [restoreVersion, { isLoading }] = useRestoreVersionMutation();
const { allowedActions } = useRBAC(PERMISSIONS.map((action) => ({ action, subject: slug })));
const version = useHistoryContext("VersionHeader", (state) => state.selectedVersion);
const mainField = useHistoryContext("VersionHeader", (state) => state.mainField);
const schema = useHistoryContext("VersionHeader", (state) => state.schema);
const isCurrentVersion = useHistoryContext(
"VersionHeader",
(state) => state.page === 1 && state.versions.data[0].id === state.selectedVersion.id
);
const mainFieldValue = version.data[mainField];
const getNextNavigation = () => {
const pluginsQueryParams = stringify({ plugins: query.plugins }, { encode: false });
return {
pathname: "..",
search: pluginsQueryParams
};
};
const handleRestore = async () => {
try {
const response = await restoreVersion({
documentId: version.relatedDocumentId,
collectionType,
params: {
versionId: version.id,
contentType: version.contentType
},
body: { contentType: version.contentType }
});
if ("data" in response) {
navigate(getNextNavigation(), { relative: "path" });
toggleNotification({
type: "success",
title: formatMessage({
id: "content-manager.restore.success.title",
defaultMessage: "Version restored."
}),
message: formatMessage({
id: "content-manager.restore.success.message",
defaultMessage: "A past version of the content was restored."
})
});
trackUsage("didRestoreHistoryVersion");
}
if ("error" in response) {
toggleNotification({
type: "danger",
message: formatMessage({
id: "content-manager.history.restore.error.message",
defaultMessage: "Could not restore version."
})
});
}
} catch (error) {
toggleNotification({
type: "danger",
message: formatMessage({ id: "notification.error", defaultMessage: "An error occurred" })
});
}
};
return /* @__PURE__ */ jsxs(Dialog.Root, { open: isConfirmDialogOpen, onOpenChange: setIsConfirmDialogOpen, children: [
/* @__PURE__ */ jsx(
Layouts.BaseHeader,
{
id: headerId,
title: formatDate(new Date(version.createdAt), {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric"
}),
subtitle: /* @__PURE__ */ jsx(Typography, { variant: "epsilon", textColor: "neutral600", children: formatMessage(
{
id: "content-manager.history.version.subtitle",
defaultMessage: "{hasLocale, select, true {{subtitle}, in {locale}} other {{subtitle}}}"
},
{
hasLocale: Boolean(version.locale),
subtitle: `${mainFieldValue || ""} (${schema.info.singularName})`.trim(),
locale: version.locale?.name
}
) }),
navigationAction: /* @__PURE__ */ jsx(
Link,
{
startIcon: /* @__PURE__ */ jsx(ForwardRef$55, {}),
tag: NavLink,
to: getNextNavigation(),
relative: "path",
isExternal: false,
children: formatMessage({
id: "global.back",
defaultMessage: "Back"
})
}
),
sticky: false,
primaryAction: /* @__PURE__ */ jsx(Dialog.Trigger, { children: /* @__PURE__ */ jsx(
Button,
{
disabled: !allowedActions.canUpdate || isCurrentVersion,
onClick: () => {
setIsConfirmDialogOpen(true);
},
children: formatMessage({
id: "content-manager.history.restore.confirm.button",
defaultMessage: "Restore"
})
}
) })
}
),
/* @__PURE__ */ jsx(
ConfirmDialog,
{
onConfirm: handleRestore,
endAction: /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: handleRestore, loading: isLoading, children: formatMessage({
id: "content-manager.history.restore.confirm.button",
defaultMessage: "Restore"
}) }),
children: /* @__PURE__ */ jsxs(
Flex,
{
direction: "column",
alignItems: "center",
justifyContent: "center",
gap: 2,
textAlign: "center",
children: [
/* @__PURE__ */ jsx(Flex, { justifyContent: "center", children: /* @__PURE__ */ jsx(ForwardRef$3, { width: "24px", height: "24px", fill: "danger600" }) }),
/* @__PURE__ */ jsx(Typography, { children: formatMessage({
id: "content-manager.history.restore.confirm.title",
defaultMessage: "Are you sure you want to restore this version?"
}) }),
/* @__PURE__ */ jsx(Typography, { children: formatMessage(
{
id: "content-manager.history.restore.confirm.message",
defaultMessage: "{isDraft, select, true {The restored content will override your draft.} other {The restored content won't be published, it will override the draft and be saved as pending changes. You'll be able to publish the changes at anytime.}}"
},
{
isDraft: version.status === "draft"
}
) })
]
}
)
}
)
] });
};
const BlueText = (children) => /* @__PURE__ */ jsx(Typography, { textColor: "primary600", variant: "pi", children });
const VersionCard = ({ version, isCurrent }) => {
const { formatDate, formatMessage } = useIntl();
const [{ query }] = useQueryParams();
const isActive = query.id === version.id.toString();
const author = version.createdBy && getDisplayName(version.createdBy);
return /* @__PURE__ */ jsxs(
Flex,
{
direction: "column",
alignItems: "flex-start",
gap: 3,
hasRadius: true,
borderWidth: "1px",
borderStyle: "solid",
borderColor: isActive ? "primary600" : "neutral200",
color: "neutral800",
padding: 5,
tag: Link$1,
to: `?${stringify({ ...query, id: version.id })}`,
style: { textDecoration: "none" },
children: [
/* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 1, alignItems: "flex-start", children: [
/* @__PURE__ */ jsx(Typography, { tag: "h3", fontWeight: "semiBold", children: formatDate(version.createdAt, {
day: "numeric",
month: "numeric",
year: "numeric",
hour: "2-digit",
minute: "2-digit"
}) }),
/* @__PURE__ */ jsx(Typography, { tag: "p", variant: "pi", textColor: "neutral600", children: formatMessage(
{
id: "content-manager.history.sidebar.versionDescription",
defaultMessage: "{distanceToNow}{isAnonymous, select, true {} other { by {author}}}{isCurrent, select, true { <b>(current)</b>} other {}}"
},
{
distanceToNow: /* @__PURE__ */ jsx(RelativeTime, { timestamp: new Date(version.createdAt) }),
author,
isAnonymous: !Boolean(version.createdBy),
isCurrent,
b: BlueText
}
) })
] }),
version.status && /* @__PURE__ */ jsx(DocumentStatus, { status: version.status, size: "XS" })
]
}
);
};
const PaginationButton = ({ page, children }) => {
const [{ query }] = useQueryParams();
const { id: _id, ...queryRest } = query;
return /* @__PURE__ */ jsx(Link$1, { to: { search: stringify({ ...queryRest, page }) }, style: { textDecoration: "none" }, children: /* @__PURE__ */ jsx(Typography, { variant: "omega", textColor: "primary600", children }) });
};
const VersionsList = () => {
const { formatMessage } = useIntl();
const { versions, page } = useHistoryContext("VersionsList", (state) => ({
versions: state.versions,
page: state.page
}));
return /* @__PURE__ */ jsxs(
Flex,
{
shrink: 0,
direction: "column",
alignItems: "stretch",
width: "320px",
height: "100vh",
background: "neutral0",
borderColor: "neutral200",
borderWidth: "0 0 0 1px",
borderStyle: "solid",
tag: "aside",
children: [
/* @__PURE__ */ jsxs(
Flex,
{
direction: "row",
justifyContent: "space-between",
padding: 4,
borderColor: "neutral200",
borderWidth: "0 0 1px",
borderStyle: "solid",
tag: "header",
children: [
/* @__PURE__ */ jsx(Typography, { tag: "h2", variant: "omega", fontWeight: "semiBold", children: formatMessage({
id: "content-manager.history.sidebar.title",
defaultMessage: "Versions"
}) }),
/* @__PURE__ */ jsx(Box, { background: "neutral150", hasRadius: true, padding: 1, children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: versions.meta.pagination.total }) })
]
}
),
/* @__PURE__ */ jsxs(Box, { flex: 1, overflow: "auto", children: [
versions.meta.pagination.page > 1 && /* @__PURE__ */ jsx(Box, { paddingTop: 4, textAlign: "center", children: /* @__PURE__ */ jsx(PaginationButton, { page: page - 1, children: formatMessage({
id: "content-manager.history.sidebar.show-newer",
defaultMessage: "Show newer versions"
}) }) }),
/* @__PURE__ */ jsx(Flex, { direction: "column", gap: 3, padding: 4, tag: "ul", alignItems: "stretch", children: versions.data.map((version, index) => /* @__PURE__ */ jsx(
"li",
{
"aria-label": formatMessage({
id: "content-manager.history.sidebar.title.version-card.aria-label",
defaultMessage: "Version card"
}),
children: /* @__PURE__ */ jsx(VersionCard, { version, isCurrent: page === 1 && index === 0 })
},
version.id
)) }),
versions.meta.pagination.page < versions.meta.pagination.pageCount && /* @__PURE__ */ jsx(Box, { paddingBottom: 4, textAlign: "center", children: /* @__PURE__ */ jsx(PaginationButton, { page: page + 1, children: formatMessage({
id: "content-manager.history.sidebar.show-older",
defaultMessage: "Show older versions"
}) }) })
] })
]
}
);
};
const [HistoryProvider, useHistoryContext] = createContext("HistoryPage");
const HistoryPage = () => {
const headerId = React.useId();
const { formatMessage } = useIntl();
const {
slug,
id: documentId,
collectionType
} = useParams();
const { isLoading: isLoadingDocument, schema } = useDocument({
collectionType,
model: slug
});
const {
isLoading: isLoadingLayout,
edit: {
layout,
settings: { displayName, mainField }
}
} = useDocumentLayout(slug);
const { data: configuration, isLoading: isLoadingConfiguration } = useGetContentTypeConfigurationQuery(slug);
const [{ query }] = useQueryParams();
const { id: selectedVersionId, ...queryWithoutId } = query;
const validQueryParamsWithoutId = buildValidParams(queryWithoutId);
const page = validQueryParamsWithoutId.page ? Number(validQueryParamsWithoutId.page) : 1;
const versionsResponse = useGetHistoryVersionsQuery(
{
contentType: slug,
...documentId ? { documentId } : {},
// Omit id since it's not needed by the endpoint and caused extra refetches
...validQueryParamsWithoutId
},
{ refetchOnMountOrArgChange: true }
);
const initialRequestId = React.useRef(versionsResponse.requestId);
const isStaleRequest = versionsResponse.requestId === initialRequestId.current;
if (!slug || collectionType === COLLECTION_TYPES && !documentId) {
return /* @__PURE__ */ jsx(Navigate, { to: "/content-manager" });
}
if (isLoadingDocument || isLoadingLayout || versionsResponse.isFetching || isStaleRequest || isLoadingConfiguration) {
return /* @__PURE__ */ jsx(Page.Loading, {});
}
if (!versionsResponse.isError && !versionsResponse.data?.data?.length) {
return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(
Page.NoData,
{
action: /* @__PURE__ */ jsx(
Link,
{
tag: NavLink,
to: `/content-manager/${collectionType}/${slug}${documentId ? `/${documentId}` : ""}`,
children: formatMessage({
id: "global.back",
defaultMessage: "Back"
})
}
)
}
) });
}
if (versionsResponse.data?.data?.length && !selectedVersionId) {
return /* @__PURE__ */ jsx(
Navigate,
{
to: { search: stringify({ ...query, id: versionsResponse.data.data[0].id }) },
replace: true
}
);
}
const selectedVersion = versionsResponse.data?.data?.find(
(version) => version.id.toString() === selectedVersionId
);
if (versionsResponse.isError || !layout || !schema || !selectedVersion || !configuration || // This should not happen as it's covered by versionsResponse.isError, but we need it for TS
versionsResponse.data.error) {
return /* @__PURE__ */ jsx(Page.Error, {});
}
return /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx(Page.Title, { children: formatMessage(
{
id: "content-manager.history.page-title",
defaultMessage: "{contentType} history"
},
{
contentType: displayName
}
) }),
/* @__PURE__ */ jsx(
HistoryProvider,
{
contentType: slug,
id: documentId,
schema,
layout,
configuration,
selectedVersion,
versions: versionsResponse.data,
page,
mainField,
children: /* @__PURE__ */ jsxs(Flex, { direction: "row", alignItems: "flex-start", children: [
/* @__PURE__ */ jsxs(
Main,
{
grow: 1,
height: "100vh",
background: "neutral100",
paddingBottom: 6,
overflow: "auto",
labelledBy: headerId,
children: [
/* @__PURE__ */ jsx(VersionHeader, { headerId }),
/* @__PURE__ */ jsx(VersionContent, {})
]
}
),
/* @__PURE__ */ jsx(VersionsList, {})
] })
}
)
] });
};
const ProtectedHistoryPageImpl = () => {
const { slug } = useParams();
const {
permissions = [],
isLoading,
error
} = useRBAC(PERMISSIONS.map((action) => ({ action, subject: slug })));
if (isLoading) {
return /* @__PURE__ */ jsx(Page.Loading, {});
}
if (error || !slug) {
return /* @__PURE__ */ jsx(
Box,
{
height: "100vh",
width: "100vw",
position: "fixed",
top: 0,
left: 0,
zIndex: 2,
background: "neutral0",
children: /* @__PURE__ */ jsx(Page.Error, {})
}
);
}
return /* @__PURE__ */ jsx(
Box,
{
height: "100vh",
width: "100vw",
position: "fixed",
top: 0,
left: 0,
zIndex: 2,
background: "neutral0",
children: /* @__PURE__ */ jsx(Page.Protect, { permissions, children: ({ permissions: permissions2 }) => /* @__PURE__ */ jsx(DocumentRBAC, { permissions: permissions2, children: /* @__PURE__ */ jsx(HistoryPage, {}) }) })
}
);
};
const ProtectedHistoryPage = () => {
return /* @__PURE__ */ jsx(Portal, { children: /* @__PURE__ */ jsx(FocusTrap, { children: /* @__PURE__ */ jsx(ProtectedHistoryPageImpl, {}) }) });
};
export {
HistoryProvider,
ProtectedHistoryPage,
useHistoryContext
};