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.
689 lines (682 loc) • 24.8 kB
JavaScript
import { jsxs, jsx } from "react/jsx-runtime";
import * as React from "react";
import { N as ForwardRef$3N, aL as ForwardRef$1r, z as ForwardRef$3$, aM as create, aN as create$1, aO as create$2, j as useTracking, k as useNotification, Y as useAPIErrorHandler, i as useDoc, t as useDocLayout, aP as useUpdateContentTypeConfigurationMutation, S as SINGLE_TYPES, b as Page, L as Layouts, F as Form, q as useForm, as as BackButton, C as COLLECTION_TYPES, B as getTranslation, aQ as capitalise, M as MemoizedInputRenderer, f as useGetContentTypeConfigurationQuery, aI as checkIfAttributeIsDisplayable, al as ForwardRef$1d, v as useField } from "./index-DX9HaYlZ.mjs";
import { Flex, Typography, Box, Main, Divider, Button, useCollator, Grid, Menu, VisuallyHidden, useComposedRefs, Modal } from "@strapi/design-system";
import { useIntl } from "react-intl";
import { Navigate, useParams } from "react-router-dom";
import { u as useTypedSelector } from "./hooks-E5u1mcgM-ChYG_e5O.mjs";
import { s as setIn, u as useDragAndDrop, I as ItemTypes, g as getEmptyImage } from "./objects-D6yBsdmx-CoUb3Ffa.mjs";
import { styled } from "styled-components";
import { F as FieldTypeIcon } from "./FieldTypeIcon-CMlNO8PE-DzNhzYsL.mjs";
const CardDragPreview = ({ label, isSibling = false }) => {
return /* @__PURE__ */ jsxs(
FieldContainer$1,
{
background: isSibling ? "neutral100" : "primary100",
display: "inline-flex",
gap: 3,
hasRadius: true,
justifyContent: "space-between",
$isSibling: isSibling,
"max-height": `3.2rem`,
maxWidth: "min-content",
children: [
/* @__PURE__ */ jsxs(Flex, { gap: 3, children: [
/* @__PURE__ */ jsx(DragButton$1, { alignItems: "center", cursor: "all-scroll", padding: 3, children: /* @__PURE__ */ jsx(ForwardRef$3N, {}) }),
/* @__PURE__ */ jsx(
Typography,
{
textColor: isSibling ? void 0 : "primary600",
fontWeight: "bold",
ellipsis: true,
maxWidth: "7.2rem",
children: label
}
)
] }),
/* @__PURE__ */ jsxs(Flex, { children: [
/* @__PURE__ */ jsx(ActionBox, { alignItems: "center", children: /* @__PURE__ */ jsx(ForwardRef$1r, {}) }),
/* @__PURE__ */ jsx(ActionBox, { alignItems: "center", children: /* @__PURE__ */ jsx(ForwardRef$3$, {}) })
] })
]
}
);
};
const ActionBox = styled(Flex)`
height: ${({ theme }) => theme.spaces[7]};
&:last-child {
padding: 0 ${({ theme }) => theme.spaces[3]};
}
`;
const DragButton$1 = styled(ActionBox)`
border-right: 1px solid ${({ theme }) => theme.colors.primary200};
svg {
width: 1.2rem;
height: 1.2rem;
}
`;
const FieldContainer$1 = styled(Flex)`
border: 1px solid
${({ theme, $isSibling }) => $isSibling ? theme.colors.neutral150 : theme.colors.primary200};
svg {
width: 1rem;
height: 1rem;
path {
fill: ${({ theme, $isSibling }) => $isSibling ? void 0 : theme.colors.primary600};
}
}
`;
const Header = ({ name }) => {
const { formatMessage } = useIntl();
const params = useParams();
const modified = useForm("Header", (state) => state.modified);
const isSubmitting = useForm("Header", (state) => state.isSubmitting);
return /* @__PURE__ */ jsx(
Layouts.Header,
{
navigationAction: /* @__PURE__ */ jsx(BackButton, { fallback: `../${COLLECTION_TYPES}/${params.slug}` }),
primaryAction: /* @__PURE__ */ jsx(Button, { size: "S", disabled: !modified, type: "submit", loading: isSubmitting, children: formatMessage({ id: "global.save", defaultMessage: "Save" }) }),
subtitle: formatMessage({
id: getTranslation("components.SettingsViewWrapper.pluginHeader.description.list-settings"),
defaultMessage: "Define the settings of the list view."
}),
title: formatMessage(
{
id: getTranslation("components.SettingsViewWrapper.pluginHeader.title"),
defaultMessage: "Configure the view - {name}"
},
{ name: capitalise(name) }
)
}
);
};
const EXCLUDED_SORT_ATTRIBUTE_TYPES = [
"media",
"richtext",
"dynamiczone",
"relation",
"component",
"json",
"blocks"
];
const Settings = () => {
const { formatMessage, locale } = useIntl();
const formatter = useCollator(locale, {
sensitivity: "base"
});
const { schema } = useDoc();
const layout = useForm("Settings", (state) => state.values.layout ?? []);
const currentSortBy = useForm(
"Settings",
(state) => state.values.settings.defaultSortBy
);
const onChange = useForm("Settings", (state) => state.onChange);
const sortOptions = React.useMemo(
() => Object.values(layout).reduce((acc, field) => {
if (schema && !EXCLUDED_SORT_ATTRIBUTE_TYPES.includes(schema.attributes[field.name].type)) {
acc.push({
value: field.name,
label: typeof field.label !== "string" ? formatMessage(field.label) : field.label
});
}
return acc;
}, []),
[formatMessage, layout, schema]
);
const sortOptionsSorted = sortOptions.sort((a, b) => formatter.compare(a.label, b.label));
React.useEffect(() => {
if (sortOptionsSorted.findIndex((opt) => opt.value === currentSortBy) === -1) {
onChange("settings.defaultSortBy", sortOptionsSorted[0]?.value);
}
}, [currentSortBy, onChange, sortOptionsSorted]);
const formLayout = React.useMemo(
() => SETTINGS_FORM_LAYOUT.map(
(row) => row.map((field) => {
if (field.type === "enumeration") {
return {
...field,
hint: field.hint ? formatMessage(field.hint) : void 0,
label: formatMessage(field.label),
options: field.name === "settings.defaultSortBy" ? sortOptionsSorted : field.options
};
} else {
return {
...field,
hint: field.hint ? formatMessage(field.hint) : void 0,
label: formatMessage(field.label)
};
}
})
),
[formatMessage, sortOptionsSorted]
);
return /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 4, children: [
/* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h2", children: formatMessage({
id: getTranslation("containers.SettingPage.settings"),
defaultMessage: "Settings"
}) }),
/* @__PURE__ */ jsx(Grid.Root, { gap: 4, children: formLayout.map(
(row) => row.map(({ size, ...field }) => /* @__PURE__ */ jsx(Grid.Item, { s: 12, col: size, direction: "column", alignItems: "stretch", children: /* @__PURE__ */ jsx(MemoizedInputRenderer, { ...field }) }, field.name))
) }, "bottom")
] });
};
const SETTINGS_FORM_LAYOUT = [
[
{
label: {
id: getTranslation("form.Input.search"),
defaultMessage: "Enable search"
},
name: "settings.searchable",
size: 4,
type: "boolean"
},
{
label: {
id: getTranslation("form.Input.filters"),
defaultMessage: "Enable filters"
},
name: "settings.filterable",
size: 4,
type: "boolean"
},
{
label: {
id: getTranslation("form.Input.bulkActions"),
defaultMessage: "Enable bulk actions"
},
name: "settings.bulkable",
size: 4,
type: "boolean"
}
],
[
{
hint: {
id: getTranslation("form.Input.pageEntries.inputDescription"),
defaultMessage: "Note: You can override this value in the Collection Type settings page."
},
label: {
id: getTranslation("form.Input.pageEntries"),
defaultMessage: "Entries per page"
},
name: "settings.pageSize",
options: ["10", "20", "50", "100"].map((value) => ({ value, label: value })),
size: 6,
type: "enumeration"
},
{
label: {
id: getTranslation("form.Input.defaultSort"),
defaultMessage: "Default sort attribute"
},
name: "settings.defaultSortBy",
options: [],
size: 3,
type: "enumeration"
},
{
label: {
id: getTranslation("form.Input.sort.order"),
defaultMessage: "Default sort order"
},
name: "settings.defaultSortOrder",
options: ["ASC", "DESC"].map((value) => ({ value, label: value })),
size: 3,
type: "enumeration"
}
]
];
const FIELD_SCHEMA = create().shape({
label: create$1().required(),
sortable: create$2()
});
const EditFieldForm = ({ attribute, name, onClose }) => {
const { formatMessage } = useIntl();
const { toggleNotification } = useNotification();
const { value, onChange } = useField(name);
if (!value) {
console.error(
"You've opened a field to edit without it being part of the form, this is likely a bug with Strapi. Please open an issue."
);
toggleNotification({
message: formatMessage({
id: "content-manager.containers.list-settings.modal-form.error",
defaultMessage: "An error occurred while trying to open the form."
}),
type: "danger"
});
return null;
}
let shouldDisplaySortToggle = !["media", "relation"].includes(attribute.type);
if ("relation" in attribute && ["oneWay", "oneToOne", "manyToOne"].includes(attribute.relation)) {
shouldDisplaySortToggle = true;
}
return /* @__PURE__ */ jsx(Modal.Content, { children: /* @__PURE__ */ jsxs(
Form,
{
method: "PUT",
initialValues: value,
validationSchema: FIELD_SCHEMA,
onSubmit: (data) => {
onChange(name, data);
onClose();
},
children: [
/* @__PURE__ */ jsx(Modal.Header, { children: /* @__PURE__ */ jsxs(HeaderContainer, { children: [
/* @__PURE__ */ jsx(FieldTypeIcon, { type: attribute.type }),
/* @__PURE__ */ jsx(Modal.Title, { children: formatMessage(
{
id: getTranslation("containers.list-settings.modal-form.label"),
defaultMessage: "Edit {fieldName}"
},
{ fieldName: capitalise(value.label) }
) })
] }) }),
/* @__PURE__ */ jsx(Modal.Body, { children: /* @__PURE__ */ jsx(Grid.Root, { gap: 4, children: [
{
name: "label",
label: formatMessage({
id: getTranslation("form.Input.label"),
defaultMessage: "Label"
}),
hint: formatMessage({
id: getTranslation("form.Input.label.inputDescription"),
defaultMessage: "This value overrides the label displayed in the table's head"
}),
size: 6,
type: "string"
},
{
label: formatMessage({
id: getTranslation("form.Input.sort.field"),
defaultMessage: "Enable sort on this field"
}),
name: "sortable",
size: 6,
type: "boolean"
}
].filter(
(field) => field.name !== "sortable" || field.name === "sortable" && shouldDisplaySortToggle
).map(({ size, ...field }) => /* @__PURE__ */ jsx(
Grid.Item,
{
s: 12,
col: size,
direction: "column",
alignItems: "stretch",
children: /* @__PURE__ */ jsx(MemoizedInputRenderer, { ...field })
},
field.name
)) }) }),
/* @__PURE__ */ jsxs(Modal.Footer, { children: [
/* @__PURE__ */ jsx(Button, { onClick: onClose, variant: "tertiary", children: formatMessage({ id: "app.components.Button.cancel", defaultMessage: "Cancel" }) }),
/* @__PURE__ */ jsx(Button, { type: "submit", children: formatMessage({ id: "global.finish", defaultMessage: "Finish" }) })
] })
]
}
) });
};
const HeaderContainer = styled(Flex)`
svg {
width: 3.2rem;
margin-right: ${({ theme }) => theme.spaces[3]};
}
`;
const DraggableCard = ({
attribute,
index,
isDraggingSibling,
label,
name,
onMoveField,
onRemoveField,
setIsDraggingSibling
}) => {
const [isModalOpen, setIsModalOpen] = React.useState(false);
const { formatMessage } = useIntl();
const [, forceRerenderAfterDnd] = React.useState(false);
const [{ isDragging }, objectRef, dropRef, dragRef, dragPreviewRef] = useDragAndDrop(true, {
type: ItemTypes.FIELD,
item: { index, label, name },
index,
onMoveItem: onMoveField,
onEnd: () => setIsDraggingSibling(false)
});
React.useEffect(() => {
dragPreviewRef(getEmptyImage(), { captureDraggingState: false });
}, [dragPreviewRef]);
React.useEffect(() => {
if (isDragging) {
setIsDraggingSibling(true);
}
}, [isDragging, setIsDraggingSibling]);
React.useEffect(() => {
if (!isDraggingSibling) {
forceRerenderAfterDnd((prev) => !prev);
}
}, [isDraggingSibling]);
const composedRefs = useComposedRefs(
dropRef,
objectRef
);
return /* @__PURE__ */ jsxs(FieldWrapper, { ref: composedRefs, children: [
isDragging && /* @__PURE__ */ jsx(CardDragPreview, { label }),
!isDragging && isDraggingSibling && /* @__PURE__ */ jsx(CardDragPreview, { isSibling: true, label }),
!isDragging && !isDraggingSibling && /* @__PURE__ */ jsxs(
FieldContainer,
{
borderColor: "neutral150",
background: "neutral100",
hasRadius: true,
justifyContent: "space-between",
onClick: () => setIsModalOpen(true),
children: [
/* @__PURE__ */ jsxs(Flex, { gap: 3, children: [
/* @__PURE__ */ jsx(
DragButton,
{
ref: dragRef,
"aria-label": formatMessage(
{
id: getTranslation("components.DraggableCard.move.field"),
defaultMessage: "Move {item}"
},
{ item: label }
),
onClick: (e) => e.stopPropagation(),
children: /* @__PURE__ */ jsx(ForwardRef$3N, {})
}
),
/* @__PURE__ */ jsx(Typography, { fontWeight: "bold", children: label })
] }),
/* @__PURE__ */ jsxs(Flex, { paddingLeft: 3, onClick: (e) => e.stopPropagation(), children: [
/* @__PURE__ */ jsxs(Modal.Root, { open: isModalOpen, onOpenChange: setIsModalOpen, children: [
/* @__PURE__ */ jsx(Modal.Trigger, { children: /* @__PURE__ */ jsx(
ActionButton,
{
onClick: (e) => {
e.stopPropagation();
},
"aria-label": formatMessage(
{
id: getTranslation("components.DraggableCard.edit.field"),
defaultMessage: "Edit {item}"
},
{ item: label }
),
type: "button",
children: /* @__PURE__ */ jsx(ForwardRef$1r, { width: "1.2rem", height: "1.2rem" })
}
) }),
/* @__PURE__ */ jsx(
EditFieldForm,
{
attribute,
name: `layout.${index}`,
onClose: () => {
setIsModalOpen(false);
}
}
)
] }),
/* @__PURE__ */ jsx(
ActionButton,
{
onClick: onRemoveField,
"data-testid": `delete-${name}`,
"aria-label": formatMessage(
{
id: getTranslation("components.DraggableCard.delete.field"),
defaultMessage: "Delete {item}"
},
{ item: label }
),
type: "button",
children: /* @__PURE__ */ jsx(ForwardRef$3$, { width: "1.2rem", height: "1.2rem" })
}
)
] })
]
}
)
] });
};
const ActionButton = styled.button`
display: flex;
align-items: center;
height: ${({ theme }) => theme.spaces[7]};
color: ${({ theme }) => theme.colors.neutral600};
&:hover {
color: ${({ theme }) => theme.colors.neutral700};
}
&:last-child {
padding: 0 ${({ theme }) => theme.spaces[3]};
}
`;
const DragButton = styled(ActionButton)`
padding: 0 ${({ theme }) => theme.spaces[3]};
border-right: 1px solid ${({ theme }) => theme.colors.neutral150};
cursor: all-scroll;
`;
const FieldContainer = styled(Flex)`
max-height: 3.2rem;
cursor: pointer;
`;
const FieldWrapper = styled(Box)`
&:last-child {
padding-right: ${({ theme }) => theme.spaces[3]};
}
`;
const SortDisplayedFields = () => {
const { formatMessage } = useIntl();
const { model, schema } = useDoc();
const [isDraggingSibling, setIsDraggingSibling] = React.useState(false);
const [lastAction, setLastAction] = React.useState(null);
const scrollableContainerRef = React.useRef(null);
const values = useForm(
"SortDisplayedFields",
(state) => state.values.layout ?? []
);
const addFieldRow = useForm("SortDisplayedFields", (state) => state.addFieldRow);
const removeFieldRow = useForm("SortDisplayedFields", (state) => state.removeFieldRow);
const moveFieldRow = useForm("SortDisplayedFields", (state) => state.moveFieldRow);
const { metadata: allMetadata } = useGetContentTypeConfigurationQuery(model, {
selectFromResult: ({ data }) => ({ metadata: data?.contentType.metadatas ?? {} })
});
const nonDisplayedFields = React.useMemo(() => {
if (!schema) {
return [];
}
const displayedFieldNames = values.map((field) => field.name);
return Object.entries(schema.attributes).reduce(
(acc, [name, attribute]) => {
if (!displayedFieldNames.includes(name) && checkIfAttributeIsDisplayable(attribute)) {
const { list: metadata } = allMetadata[name];
acc.push({
name,
label: metadata.label || name,
sortable: metadata.sortable
});
}
return acc;
},
[]
);
}, [allMetadata, values, schema]);
const handleAddField = (field) => {
setLastAction("add");
addFieldRow("layout", field);
};
const handleRemoveField = (index) => {
setLastAction("remove");
removeFieldRow("layout", index);
};
const handleMoveField = (dragIndex, hoverIndex) => {
moveFieldRow("layout", dragIndex, hoverIndex);
};
React.useEffect(() => {
if (lastAction === "add" && scrollableContainerRef?.current) {
scrollableContainerRef.current.scrollLeft = scrollableContainerRef.current.scrollWidth;
}
}, [lastAction]);
return /* @__PURE__ */ jsxs(Flex, { alignItems: "stretch", direction: "column", gap: 4, children: [
/* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h2", children: formatMessage({
id: getTranslation("containers.SettingPage.view"),
defaultMessage: "View"
}) }),
/* @__PURE__ */ jsxs(Flex, { padding: 4, borderColor: "neutral300", borderStyle: "dashed", borderWidth: "1px", hasRadius: true, children: [
/* @__PURE__ */ jsx(Box, { flex: "1", overflow: "auto hidden", ref: scrollableContainerRef, children: /* @__PURE__ */ jsx(Flex, { gap: 3, children: values.map((field, index) => /* @__PURE__ */ jsx(
DraggableCard,
{
index,
isDraggingSibling,
onMoveField: handleMoveField,
onRemoveField: () => handleRemoveField(index),
setIsDraggingSibling,
...field,
attribute: schema.attributes[field.name],
label: typeof field.label === "object" ? formatMessage(field.label) : field.label
},
field.name
)) }) }),
/* @__PURE__ */ jsxs(Menu.Root, { children: [
/* @__PURE__ */ jsxs(
Menu.Trigger,
{
paddingLeft: 2,
paddingRight: 2,
justifyContent: "center",
endIcon: null,
disabled: nonDisplayedFields.length === 0,
variant: "tertiary",
children: [
/* @__PURE__ */ jsx(VisuallyHidden, { tag: "span", children: formatMessage({
id: getTranslation("components.FieldSelect.label"),
defaultMessage: "Add a field"
}) }),
/* @__PURE__ */ jsx(ForwardRef$1d, { "aria-hidden": true, focusable: false, style: { position: "relative", top: 2 } })
]
}
),
/* @__PURE__ */ jsx(Menu.Content, { children: nonDisplayedFields.map((field) => /* @__PURE__ */ jsx(Menu.Item, { onSelect: () => handleAddField(field), children: typeof field.label === "object" ? formatMessage(field.label) : field.label }, field.name)) })
] })
] })
] });
};
const ListConfiguration = () => {
const { formatMessage } = useIntl();
const { trackUsage } = useTracking();
const { toggleNotification } = useNotification();
const { _unstableFormatAPIError: formatAPIError } = useAPIErrorHandler();
const { model, collectionType } = useDoc();
const { isLoading: isLoadingLayout, list, edit } = useDocLayout();
const [updateContentTypeConfiguration] = useUpdateContentTypeConfigurationMutation();
const handleSubmit = async (data) => {
try {
trackUsage("willSaveContentTypeLayout");
const layoutData = data.layout ?? [];
const meta = Object.entries(edit.metadatas).reduce((acc, [name, editMeta]) => {
const { mainField: _mainField, ...listMeta } = list.metadatas[name];
const { label, sortable } = layoutData.find((field) => field.name === name) ?? {};
acc[name] = {
edit: editMeta,
list: {
...listMeta,
label: label || listMeta.label,
sortable: sortable || listMeta.sortable
}
};
return acc;
}, {});
const res = await updateContentTypeConfiguration({
layouts: {
edit: edit.layout.flatMap(
(panel) => panel.map((row) => row.map(({ name, size }) => ({ name, size })))
),
list: layoutData.map((field) => field.name)
},
settings: setIn(data.settings, "displayName", void 0),
metadatas: meta,
uid: model
});
if ("data" in res) {
trackUsage("didEditListSettings");
toggleNotification({
type: "success",
message: formatMessage({ id: "notification.success.saved", defaultMessage: "Saved" })
});
} else {
toggleNotification({
type: "danger",
message: formatAPIError(res.error)
});
}
} catch (err) {
console.error(err);
toggleNotification({
type: "danger",
message: formatMessage({ id: "notification.error", defaultMessage: "An error occurred" })
});
}
};
const initialValues = React.useMemo(() => {
return {
layout: list.layout.map(({ label, sortable, name }) => ({
label: typeof label === "string" ? label : formatMessage(label),
sortable,
name
})),
settings: list.settings
};
}, [formatMessage, list.layout, list.settings]);
if (collectionType === SINGLE_TYPES) {
return /* @__PURE__ */ jsx(Navigate, { to: `/single-types/${model}` });
}
if (isLoadingLayout) {
return /* @__PURE__ */ jsx(Page.Loading, {});
}
return /* @__PURE__ */ jsxs(Layouts.Root, { children: [
/* @__PURE__ */ jsx(Page.Title, { children: `Configure ${list.settings.displayName} List View` }),
/* @__PURE__ */ jsx(Main, { children: /* @__PURE__ */ jsxs(Form, { initialValues, onSubmit: handleSubmit, method: "PUT", children: [
/* @__PURE__ */ jsx(
Header,
{
collectionType,
model,
name: list.settings.displayName ?? ""
}
),
/* @__PURE__ */ jsx(Layouts.Content, { children: /* @__PURE__ */ jsxs(
Flex,
{
alignItems: "stretch",
background: "neutral0",
direction: "column",
gap: 6,
hasRadius: true,
shadow: "tableShadow",
paddingTop: 6,
paddingBottom: 6,
paddingLeft: 7,
paddingRight: 7,
children: [
/* @__PURE__ */ jsx(Settings, {}),
/* @__PURE__ */ jsx(Divider, {}),
/* @__PURE__ */ jsx(SortDisplayedFields, {})
]
}
) })
] }) })
] });
};
const ProtectedListConfiguration = () => {
const permissions = useTypedSelector(
(state) => state.admin_app.permissions.contentManager?.collectionTypesConfigurations
);
return /* @__PURE__ */ jsx(Page.Protect, { permissions, children: /* @__PURE__ */ jsx(ListConfiguration, {}) });
};
export {
ListConfiguration,
ProtectedListConfiguration
};