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.
1,022 lines (1,021 loc) • 38.7 kB
JavaScript
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
import * as React from "react";
import { an as requiredArgs, ao as toInteger, ap as millisecondsInHour, aq as millisecondsInMinute, L as Layouts, j as useTracking, k as useNotification, Y as useAPIErrorHandler, i as useDoc, e as useDocumentLayout, g as useQueryParams, h as buildValidParams, ar as useGetAllDocumentsQuery, r as useDocumentRBAC, s as useStrapiApp, B as getTranslation, b as Page, as as BackButton, at as InjectionZone, au as SearchInput, av as Table, p as DocumentStatus, o as getDisplayName, aw as TableActions, ax as Pagination, ay as useTable, az as BulkActionsRenderer, al as ForwardRef$1d, u as useRBAC, D as DocumentRBAC, aA as useAuth, aB as useContentTypeSchema, aC as useAdminUsers, f as useGetContentTypeConfigurationQuery, aD as CREATOR_FIELDS, aE as getMainField, aF as Filters, v as useField, aG as ForwardRef$47, aH as ForwardRef$2h, aI as checkIfAttributeIsDisplayable, aJ as HOOKS, aK as convertListLayoutToFieldLayouts, P as PERMISSIONS } from "./index-DX9HaYlZ.mjs";
import { Typography, Flex, Button, useCollator, Combobox, ComboboxOption, Tooltip, Popover, IconButton, LinkButton, TextButton, Checkbox, Menu, Badge, Avatar, useNotifyAT, Loader } from "@strapi/design-system";
import isEqual from "lodash/isEqual";
import { stringify } from "qs";
import { useIntl } from "react-intl";
import { useNavigate, Link, useParams, NavLink } from "react-router-dom";
import { styled } from "styled-components";
import { c as usePrev, b as useDebounce, p as prefixFileUrlWithBackendUrl, g as getRelationLabel, u as useGetRelationsQuery } from "./useDebounce-DmuSJIF3-De6sMYL4.mjs";
import isEmpty from "lodash/isEmpty";
import toString from "lodash/toString";
import { u as useTypedSelector } from "./hooks-E5u1mcgM-ChYG_e5O.mjs";
function parseISO(argument, options) {
var _options$additionalDi;
requiredArgs(1, arguments);
var additionalDigits = toInteger((_options$additionalDi = void 0) !== null && _options$additionalDi !== void 0 ? _options$additionalDi : 2);
if (additionalDigits !== 2 && additionalDigits !== 1 && additionalDigits !== 0) {
throw new RangeError("additionalDigits must be 0, 1 or 2");
}
if (!(typeof argument === "string" || Object.prototype.toString.call(argument) === "[object String]")) {
return /* @__PURE__ */ new Date(NaN);
}
var dateStrings = splitDateString(argument);
var date;
if (dateStrings.date) {
var parseYearResult = parseYear(dateStrings.date, additionalDigits);
date = parseDate(parseYearResult.restDateString, parseYearResult.year);
}
if (!date || isNaN(date.getTime())) {
return /* @__PURE__ */ new Date(NaN);
}
var timestamp = date.getTime();
var time = 0;
var offset;
if (dateStrings.time) {
time = parseTime(dateStrings.time);
if (isNaN(time)) {
return /* @__PURE__ */ new Date(NaN);
}
}
if (dateStrings.timezone) {
offset = parseTimezone(dateStrings.timezone);
if (isNaN(offset)) {
return /* @__PURE__ */ new Date(NaN);
}
} else {
var dirtyDate = new Date(timestamp + time);
var result = /* @__PURE__ */ new Date(0);
result.setFullYear(dirtyDate.getUTCFullYear(), dirtyDate.getUTCMonth(), dirtyDate.getUTCDate());
result.setHours(dirtyDate.getUTCHours(), dirtyDate.getUTCMinutes(), dirtyDate.getUTCSeconds(), dirtyDate.getUTCMilliseconds());
return result;
}
return new Date(timestamp + time + offset);
}
var patterns = {
dateTimeDelimiter: /[T ]/,
timeZoneDelimiter: /[Z ]/i,
timezone: /([Z+-].*)$/
};
var dateRegex = /^-?(?:(\d{3})|(\d{2})(?:-?(\d{2}))?|W(\d{2})(?:-?(\d{1}))?|)$/;
var timeRegex = /^(\d{2}(?:[.,]\d*)?)(?::?(\d{2}(?:[.,]\d*)?))?(?::?(\d{2}(?:[.,]\d*)?))?$/;
var timezoneRegex = /^([+-])(\d{2})(?::?(\d{2}))?$/;
function splitDateString(dateString) {
var dateStrings = {};
var array = dateString.split(patterns.dateTimeDelimiter);
var timeString;
if (array.length > 2) {
return dateStrings;
}
if (/:/.test(array[0])) {
timeString = array[0];
} else {
dateStrings.date = array[0];
timeString = array[1];
if (patterns.timeZoneDelimiter.test(dateStrings.date)) {
dateStrings.date = dateString.split(patterns.timeZoneDelimiter)[0];
timeString = dateString.substr(dateStrings.date.length, dateString.length);
}
}
if (timeString) {
var token = patterns.timezone.exec(timeString);
if (token) {
dateStrings.time = timeString.replace(token[1], "");
dateStrings.timezone = token[1];
} else {
dateStrings.time = timeString;
}
}
return dateStrings;
}
function parseYear(dateString, additionalDigits) {
var regex = new RegExp("^(?:(\\d{4}|[+-]\\d{" + (4 + additionalDigits) + "})|(\\d{2}|[+-]\\d{" + (2 + additionalDigits) + "})$)");
var captures = dateString.match(regex);
if (!captures) return {
year: NaN,
restDateString: ""
};
var year = captures[1] ? parseInt(captures[1]) : null;
var century = captures[2] ? parseInt(captures[2]) : null;
return {
year: century === null ? year : century * 100,
restDateString: dateString.slice((captures[1] || captures[2]).length)
};
}
function parseDate(dateString, year) {
if (year === null) return /* @__PURE__ */ new Date(NaN);
var captures = dateString.match(dateRegex);
if (!captures) return /* @__PURE__ */ new Date(NaN);
var isWeekDate = !!captures[4];
var dayOfYear = parseDateUnit(captures[1]);
var month = parseDateUnit(captures[2]) - 1;
var day = parseDateUnit(captures[3]);
var week = parseDateUnit(captures[4]);
var dayOfWeek = parseDateUnit(captures[5]) - 1;
if (isWeekDate) {
if (!validateWeekDate(year, week, dayOfWeek)) {
return /* @__PURE__ */ new Date(NaN);
}
return dayOfISOWeekYear(year, week, dayOfWeek);
} else {
var date = /* @__PURE__ */ new Date(0);
if (!validateDate(year, month, day) || !validateDayOfYearDate(year, dayOfYear)) {
return /* @__PURE__ */ new Date(NaN);
}
date.setUTCFullYear(year, month, Math.max(dayOfYear, day));
return date;
}
}
function parseDateUnit(value) {
return value ? parseInt(value) : 1;
}
function parseTime(timeString) {
var captures = timeString.match(timeRegex);
if (!captures) return NaN;
var hours = parseTimeUnit(captures[1]);
var minutes = parseTimeUnit(captures[2]);
var seconds = parseTimeUnit(captures[3]);
if (!validateTime(hours, minutes, seconds)) {
return NaN;
}
return hours * millisecondsInHour + minutes * millisecondsInMinute + seconds * 1e3;
}
function parseTimeUnit(value) {
return value && parseFloat(value.replace(",", ".")) || 0;
}
function parseTimezone(timezoneString) {
if (timezoneString === "Z") return 0;
var captures = timezoneString.match(timezoneRegex);
if (!captures) return 0;
var sign = captures[1] === "+" ? -1 : 1;
var hours = parseInt(captures[2]);
var minutes = captures[3] && parseInt(captures[3]) || 0;
if (!validateTimezone(hours, minutes)) {
return NaN;
}
return sign * (hours * millisecondsInHour + minutes * millisecondsInMinute);
}
function dayOfISOWeekYear(isoWeekYear, week, day) {
var date = /* @__PURE__ */ new Date(0);
date.setUTCFullYear(isoWeekYear, 0, 4);
var fourthOfJanuaryDay = date.getUTCDay() || 7;
var diff = (week - 1) * 7 + day + 1 - fourthOfJanuaryDay;
date.setUTCDate(date.getUTCDate() + diff);
return date;
}
var daysInMonths = [31, null, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
function isLeapYearIndex(year) {
return year % 400 === 0 || year % 4 === 0 && year % 100 !== 0;
}
function validateDate(year, month, date) {
return month >= 0 && month <= 11 && date >= 1 && date <= (daysInMonths[month] || (isLeapYearIndex(year) ? 29 : 28));
}
function validateDayOfYearDate(year, dayOfYear) {
return dayOfYear >= 1 && dayOfYear <= (isLeapYearIndex(year) ? 366 : 365);
}
function validateWeekDate(_year, week, day) {
return week >= 1 && week <= 53 && day >= 0 && day <= 6;
}
function validateTime(hours, minutes, seconds) {
if (hours === 24) {
return minutes === 0 && seconds === 0;
}
return seconds >= 0 && seconds < 60 && minutes >= 0 && minutes < 60 && hours >= 0 && hours < 25;
}
function validateTimezone(_hours, minutes) {
return minutes >= 0 && minutes <= 59;
}
const NOT_ALLOWED_FILTERS = [
"json",
"component",
"media",
"richtext",
"dynamiczone",
"password",
"blocks"
];
const DEFAULT_ALLOWED_FILTERS = ["createdAt", "updatedAt"];
const USER_FILTER_ATTRIBUTES = [...CREATOR_FIELDS, "strapi_assignee"];
const FiltersImpl = ({ disabled, schema }) => {
const { attributes, uid: model, options } = schema;
const { formatMessage, locale } = useIntl();
const { trackUsage } = useTracking();
const allPermissions = useAuth("FiltersImpl", (state) => state.permissions);
const [{ query }] = useQueryParams();
const { schemas } = useContentTypeSchema();
const canReadAdminUsers = React.useMemo(
() => allPermissions.filter(
(permission) => permission.action === "admin::users.read" && permission.subject === null
).length > 0,
[allPermissions]
);
const selectedUserIds = (query?.filters?.$and ?? []).reduce((acc, filter) => {
const [key, value] = Object.entries(filter)[0];
if (typeof value.id !== "object") {
return acc;
}
const id = value.id.$eq || value.id.$ne;
if (id && USER_FILTER_ATTRIBUTES.includes(key) && !acc.includes(id)) {
acc.push(id);
}
return acc;
}, []);
const { data: userData, isLoading: isLoadingAdminUsers } = useAdminUsers(
{ filters: { id: { $in: selectedUserIds } } },
{
// fetch the list of admin users only if the filter contains users and the
// current user has permissions to display users
skip: selectedUserIds.length === 0 || !canReadAdminUsers
}
);
const { users = [] } = userData ?? {};
const { metadata } = useGetContentTypeConfigurationQuery(model, {
selectFromResult: ({ data }) => ({ metadata: data?.contentType.metadatas ?? {} })
});
const formatter = useCollator(locale, {
sensitivity: "base"
});
const displayedFilters = React.useMemo(() => {
const [{ properties: { fields = [] } = { fields: [] } }] = allPermissions.filter(
(permission) => permission.action === "plugin::content-manager.explorer.read" && permission.subject === model
);
const allowedFields = fields.filter((field) => {
const attribute = attributes[field] ?? {};
return attribute.type && !NOT_ALLOWED_FILTERS.includes(attribute.type);
});
return [
"id",
...allowedFields,
...DEFAULT_ALLOWED_FILTERS,
...canReadAdminUsers ? CREATOR_FIELDS : []
].map((name) => {
const attribute = attributes[name];
if (NOT_ALLOWED_FILTERS.includes(attribute.type)) {
return null;
}
const { mainField: mainFieldName = "", label } = metadata[name].list;
let filter = {
name,
label: label ?? "",
mainField: getMainField(attribute, mainFieldName, { schemas, components: {} }),
// @ts-expect-error – TODO: this is filtered out above in the `allowedFields` call but TS complains, is there a better way to solve this?
type: attribute.type
};
if (attribute.type === "relation" && "target" in attribute && attribute.target === "admin::user") {
filter = {
...filter,
input: AdminUsersFilter,
options: users.map((user) => ({
label: getDisplayName(user),
value: user.id.toString()
})),
operators: [
{
label: formatMessage({
id: "components.FilterOptions.FILTER_TYPES.$eq",
defaultMessage: "is"
}),
value: "$eq"
},
{
label: formatMessage({
id: "components.FilterOptions.FILTER_TYPES.$ne",
defaultMessage: "is not"
}),
value: "$ne"
}
],
mainField: {
name: "id",
type: "integer"
}
};
}
if (attribute.type === "enumeration") {
filter = {
...filter,
options: attribute.enum.map((value) => ({
label: value,
value
}))
};
}
return filter;
}).filter(Boolean).toSorted((a, b) => formatter.compare(a.label, b.label));
}, [
allPermissions,
canReadAdminUsers,
model,
attributes,
metadata,
schemas,
users,
formatMessage,
formatter
]);
const onOpenChange = (isOpen) => {
if (isOpen) {
trackUsage("willFilterEntries");
}
};
const handleFilterChange = (data) => {
const attribute = attributes[data.name];
if (attribute) {
trackUsage("didFilterEntries", {
useRelation: attribute.type === "relation"
});
}
};
return /* @__PURE__ */ jsxs(
Filters.Root,
{
disabled,
options: displayedFilters,
onOpenChange,
onChange: handleFilterChange,
children: [
/* @__PURE__ */ jsx(Filters.Trigger, {}),
/* @__PURE__ */ jsx(Filters.Popover, {}),
/* @__PURE__ */ jsx(Filters.List, {})
]
}
);
};
const AdminUsersFilter = ({ name }) => {
const [pageSize, setPageSize] = React.useState(10);
const [search, setSearch] = React.useState("");
const { formatMessage } = useIntl();
const debouncedSearch = useDebounce(search, 300);
const { data, isLoading } = useAdminUsers({
pageSize,
_q: debouncedSearch
});
const field = useField(name);
const handleOpenChange = (isOpen) => {
if (!isOpen) {
setPageSize(10);
}
};
const { users = [], pagination } = data ?? {};
const { pageCount = 1, page = 1 } = pagination ?? {};
return /* @__PURE__ */ jsx(
Combobox,
{
value: field.value,
"aria-label": formatMessage({
id: "content-manager.components.Filters.usersSelect.label",
defaultMessage: "Search and select a user to filter"
}),
onOpenChange: handleOpenChange,
onChange: (value) => field.onChange(name, value),
loading: isLoading,
onLoadMore: () => setPageSize(pageSize + 10),
hasMoreItems: page < pageCount,
onInputChange: (e) => {
setSearch(e.currentTarget.value);
},
children: users.map((user) => {
return /* @__PURE__ */ jsx(ComboboxOption, { value: user.id.toString(), children: getDisplayName(user) }, user.id);
})
}
);
};
const CellValue = ({ type, value }) => {
const { formatDate, formatTime, formatNumber } = useIntl();
let formattedValue = value;
if (type === "date") {
formattedValue = formatDate(parseISO(value), { dateStyle: "full" });
}
if (type === "datetime") {
formattedValue = formatDate(value, { dateStyle: "full", timeStyle: "short" });
}
if (type === "time") {
const [hour, minute, second] = value.split(":");
const date = /* @__PURE__ */ new Date();
date.setHours(hour);
date.setMinutes(minute);
date.setSeconds(second);
formattedValue = formatTime(date, {
timeStyle: "short"
});
}
if (["float", "decimal"].includes(type)) {
formattedValue = formatNumber(value, {
// Should be kept in sync with the corresponding value
// in the design-system/NumberInput: https://github.com/strapi/design-system/blob/main/packages/strapi-design-system/src/NumberInput/NumberInput.js#L53
maximumFractionDigits: 20
});
}
if (["integer", "biginteger"].includes(type)) {
formattedValue = formatNumber(value, { maximumFractionDigits: 0 });
}
return toString(formattedValue);
};
const SingleComponent = ({ content, mainField }) => {
if (!mainField) {
return null;
}
return /* @__PURE__ */ jsx(Tooltip, { label: content[mainField.name], children: /* @__PURE__ */ jsx(Typography, { maxWidth: "25rem", textColor: "neutral800", ellipsis: true, children: /* @__PURE__ */ jsx(CellValue, { type: mainField.type, value: content[mainField.name] }) }) });
};
const RepeatableComponent = ({ content, mainField }) => {
const { formatMessage } = useIntl();
if (!mainField) {
return null;
}
return /* @__PURE__ */ jsxs(Menu.Root, { children: [
/* @__PURE__ */ jsxs(Menu.Trigger, { onClick: (e) => e.stopPropagation(), children: [
/* @__PURE__ */ jsx(Badge, { children: content.length }),
formatMessage(
{
id: "content-manager.containers.list.items",
defaultMessage: "{number, plural, =0 {items} one {item} other {items}}"
},
{ number: content.length }
)
] }),
/* @__PURE__ */ jsx(Menu.Content, { children: content.map((item) => /* @__PURE__ */ jsx(Menu.Item, { disabled: true, children: /* @__PURE__ */ jsx(Typography, { maxWidth: "50rem", ellipsis: true, children: /* @__PURE__ */ jsx(CellValue, { type: mainField.type, value: item[mainField.name] }) }) }, item.id)) })
] });
};
const getFileExtension = (ext) => ext && ext[0] === "." ? ext.substring(1) : ext;
const MediaSingle = ({ url, mime, alternativeText, name, ext, formats }) => {
const fileURL = prefixFileUrlWithBackendUrl(url);
if (mime.includes("image")) {
const thumbnail = formats?.thumbnail?.url;
const mediaURL = prefixFileUrlWithBackendUrl(thumbnail) || fileURL;
return /* @__PURE__ */ jsx(
Avatar.Item,
{
src: mediaURL,
alt: alternativeText || name,
fallback: alternativeText || name,
preview: true
}
);
}
const fileExtension = getFileExtension(ext);
const fileName = name.length > 100 ? `${name.substring(0, 100)}...` : name;
return /* @__PURE__ */ jsx(Tooltip, { description: fileName, children: /* @__PURE__ */ jsx(FileWrapper, { children: fileExtension }) });
};
const FileWrapper = ({ children }) => {
return /* @__PURE__ */ jsx(
Flex,
{
tag: "span",
position: "relative",
borderRadius: "50%",
width: "26px",
height: "26px",
borderColor: "neutral200",
background: "neutral150",
paddingLeft: "1px",
justifyContent: "center",
alignItems: "center",
children: /* @__PURE__ */ jsx(FileTypography, { variant: "sigma", textColor: "neutral600", children })
}
);
};
const FileTypography = styled(Typography)`
font-size: 0.9rem;
line-height: 0.9rem;
`;
const MediaMultiple = ({ content }) => {
return /* @__PURE__ */ jsx(Avatar.Group, { children: content.map((file, index) => {
const key = `${file.id}${index}`;
if (index === 3) {
const remainingFiles = `+${content.length - 3}`;
return /* @__PURE__ */ jsx(FileWrapper, { children: remainingFiles }, key);
}
if (index > 3) {
return null;
}
return /* @__PURE__ */ jsx(MediaSingle, { ...file }, key);
}) });
};
const RelationSingle = ({ mainField, content }) => {
return /* @__PURE__ */ jsx(Typography, { maxWidth: "50rem", textColor: "neutral800", ellipsis: true, children: getRelationLabel(content, mainField) });
};
const RelationMultiple = ({ mainField, content, rowId, name }) => {
const { model } = useDoc();
const { formatMessage } = useIntl();
const { notifyStatus } = useNotifyAT();
const [isOpen, setIsOpen] = React.useState(false);
const [targetField] = name.split(".");
const { data, isLoading } = useGetRelationsQuery(
{
model,
id: rowId,
targetField
},
{
skip: !isOpen,
refetchOnMountOrArgChange: true
}
);
const contentCount = Array.isArray(content) ? content.length : content.count;
React.useEffect(() => {
if (data) {
notifyStatus(
formatMessage({
id: getTranslation("DynamicTable.relation-loaded"),
defaultMessage: "Relations have been loaded"
})
);
}
}, [data, formatMessage, notifyStatus]);
return /* @__PURE__ */ jsxs(Menu.Root, { onOpenChange: (isOpen2) => setIsOpen(isOpen2), children: [
/* @__PURE__ */ jsx(Menu.Trigger, { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsx(Typography, { style: { cursor: "pointer" }, textColor: "neutral800", fontWeight: "regular", children: contentCount > 0 ? formatMessage(
{
id: "content-manager.containers.list.items",
defaultMessage: "{number} {number, plural, =0 {items} one {item} other {items}}"
},
{ number: contentCount }
) : "-" }) }),
/* @__PURE__ */ jsxs(Menu.Content, { children: [
isLoading && /* @__PURE__ */ jsx(Menu.Item, { disabled: true, children: /* @__PURE__ */ jsx(Loader, { small: true, children: formatMessage({
id: getTranslation("ListViewTable.relation-loading"),
defaultMessage: "Relations are loading"
}) }) }),
data?.results && /* @__PURE__ */ jsxs(Fragment, { children: [
data.results.map((entry) => /* @__PURE__ */ jsx(Menu.Item, { disabled: true, children: /* @__PURE__ */ jsx(Typography, { maxWidth: "50rem", ellipsis: true, children: getRelationLabel(entry, mainField) }) }, entry.documentId)),
data?.pagination && data?.pagination.total > 10 && /* @__PURE__ */ jsx(
Menu.Item,
{
"aria-disabled": true,
"aria-label": formatMessage({
id: getTranslation("ListViewTable.relation-more"),
defaultMessage: "This relation contains more entities than displayed"
}),
children: /* @__PURE__ */ jsx(Typography, { children: "…" })
}
)
] })
] })
] });
};
const CellContent = ({ content, mainField, attribute, rowId, name }) => {
if (!hasContent(content, mainField, attribute)) {
return /* @__PURE__ */ jsx(
Typography,
{
textColor: "neutral800",
paddingLeft: attribute.type === "relation" ? "1.6rem" : 0,
paddingRight: attribute.type === "relation" ? "1.6rem" : 0,
children: "-"
}
);
}
switch (attribute.type) {
case "media":
if (!attribute.multiple) {
return /* @__PURE__ */ jsx(MediaSingle, { ...content });
}
return /* @__PURE__ */ jsx(MediaMultiple, { content });
case "relation": {
if (isSingleRelation(attribute.relation)) {
return /* @__PURE__ */ jsx(RelationSingle, { mainField, content });
}
return /* @__PURE__ */ jsx(RelationMultiple, { rowId, mainField, content, name });
}
case "component":
if (attribute.repeatable) {
return /* @__PURE__ */ jsx(RepeatableComponent, { mainField, content });
}
return /* @__PURE__ */ jsx(SingleComponent, { mainField, content });
case "string":
return /* @__PURE__ */ jsx(Tooltip, { description: content, children: /* @__PURE__ */ jsx(Typography, { maxWidth: "30rem", ellipsis: true, textColor: "neutral800", children: /* @__PURE__ */ jsx(CellValue, { type: attribute.type, value: content }) }) });
default:
return /* @__PURE__ */ jsx(Typography, { maxWidth: "30rem", ellipsis: true, textColor: "neutral800", children: /* @__PURE__ */ jsx(CellValue, { type: attribute.type, value: content }) });
}
};
const hasContent = (content, mainField, attribute) => {
if (attribute.type === "component") {
if (attribute.repeatable || !mainField) {
return content?.length > 0;
}
const value = content?.[mainField.name];
if (mainField.name === "id" && ![void 0, null].includes(value)) {
return true;
}
return !isEmpty(value);
}
if (attribute.type === "relation") {
if (isSingleRelation(attribute.relation)) {
return !isEmpty(content);
}
if (Array.isArray(content)) {
return content.length > 0;
}
return content?.count > 0;
}
if (["integer", "decimal", "float", "number"].includes(attribute.type)) {
return typeof content === "number";
}
if (attribute.type === "boolean") {
return content !== null;
}
return !isEmpty(content);
};
const isSingleRelation = (type) => ["oneToOne", "manyToOne", "oneToOneMorph"].includes(type);
const ViewSettingsMenu = (props) => {
const permissions = useTypedSelector(
(state) => state.admin_app.permissions.contentManager?.collectionTypesConfigurations ?? []
);
const [{ query }] = useQueryParams();
const { formatMessage } = useIntl();
const {
allowedActions: { canConfigureView }
} = useRBAC(permissions);
return /* @__PURE__ */ jsxs(Popover.Root, { children: [
/* @__PURE__ */ jsx(Popover.Trigger, { children: /* @__PURE__ */ jsx(
IconButton,
{
label: formatMessage({
id: "components.ViewSettings.tooltip",
defaultMessage: "View Settings"
}),
children: /* @__PURE__ */ jsx(ForwardRef$47, {})
}
) }),
/* @__PURE__ */ jsx(Popover.Content, { side: "bottom", align: "end", sideOffset: 4, children: /* @__PURE__ */ jsxs(Flex, { alignItems: "stretch", direction: "column", padding: 3, gap: 3, children: [
canConfigureView ? /* @__PURE__ */ jsx(
LinkButton,
{
size: "S",
startIcon: /* @__PURE__ */ jsx(ForwardRef$2h, {}),
variant: "secondary",
tag: NavLink,
to: {
pathname: "configurations/list",
search: query.plugins ? stringify({ plugins: query.plugins }, { encode: false }) : ""
},
children: formatMessage({
id: "app.links.configure-view",
defaultMessage: "Configure the view"
})
}
) : null,
/* @__PURE__ */ jsx(FieldPicker, { ...props })
] }) })
] });
};
const FieldPicker = ({ headers = [], resetHeaders, setHeaders }) => {
const { trackUsage } = useTracking();
const { formatMessage, locale } = useIntl();
const { schema, model } = useDoc();
const { list } = useDocumentLayout(model);
const formatter = useCollator(locale, {
sensitivity: "base"
});
const attributes = schema?.attributes ?? {};
const columns = Object.keys(attributes).filter((name) => checkIfAttributeIsDisplayable(attributes[name])).map((name) => ({
name,
label: list.metadatas[name]?.label ?? ""
})).sort((a, b) => formatter.compare(a.label, b.label));
const handleChange = (name) => {
trackUsage("didChangeDisplayedFields");
const newHeaders = headers.includes(name) ? headers.filter((header) => header !== name) : [...headers, name];
setHeaders(newHeaders);
};
const handleReset = () => {
resetHeaders();
};
return /* @__PURE__ */ jsxs(
Flex,
{
tag: "fieldset",
direction: "column",
alignItems: "stretch",
gap: 3,
borderWidth: 0,
maxHeight: "240px",
overflow: "scroll",
children: [
/* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", children: [
/* @__PURE__ */ jsx(Typography, { tag: "legend", variant: "pi", fontWeight: "bold", children: formatMessage({
id: "containers.list.displayedFields",
defaultMessage: "Displayed fields"
}) }),
/* @__PURE__ */ jsx(TextButton, { onClick: handleReset, children: formatMessage({
id: "app.components.Button.reset",
defaultMessage: "Reset"
}) })
] }),
/* @__PURE__ */ jsx(Flex, { direction: "column", alignItems: "stretch", children: columns.map((header) => {
const isActive = headers.includes(header.name);
return /* @__PURE__ */ jsx(
Flex,
{
wrap: "wrap",
gap: 2,
background: isActive ? "primary100" : "transparent",
hasRadius: true,
padding: 2,
children: /* @__PURE__ */ jsx(
Checkbox,
{
onCheckedChange: () => handleChange(header.name),
checked: isActive,
name: header.name,
children: /* @__PURE__ */ jsx(Typography, { fontSize: 1, children: header.label })
}
)
},
header.name
);
}) })
]
}
);
};
const { INJECT_COLUMN_IN_TABLE } = HOOKS;
const LayoutsHeaderCustom = styled(Layouts.Header)`
overflow-wrap: anywhere;
`;
const ListViewPage = () => {
const { trackUsage } = useTracking();
const navigate = useNavigate();
const { formatMessage } = useIntl();
const { toggleNotification } = useNotification();
const { _unstableFormatAPIError: formatAPIError } = useAPIErrorHandler(getTranslation);
const { collectionType, model, schema } = useDoc();
const { list } = useDocumentLayout(model);
const [displayedHeaders, setDisplayedHeaders] = React.useState([]);
const listLayout = usePrev(list.layout);
React.useEffect(() => {
if (!isEqual(listLayout, list.layout)) {
setDisplayedHeaders(list.layout);
}
}, [list.layout, listLayout]);
const handleSetHeaders = (headers) => {
setDisplayedHeaders(
convertListLayoutToFieldLayouts(headers, schema.attributes, list.metadatas)
);
};
const [{ query }] = useQueryParams({
page: "1",
pageSize: list.settings.pageSize.toString(),
sort: list.settings.defaultSortBy ? `${list.settings.defaultSortBy}:${list.settings.defaultSortOrder}` : ""
});
const params = React.useMemo(() => buildValidParams(query), [query]);
const { data, error, isFetching } = useGetAllDocumentsQuery({
model,
params
});
React.useEffect(() => {
if (error) {
toggleNotification({
type: "danger",
message: formatAPIError(error)
});
}
}, [error, formatAPIError, toggleNotification]);
const { results = [], pagination } = data ?? {};
React.useEffect(() => {
if (pagination && pagination.pageCount > 0 && pagination.page > pagination.pageCount) {
navigate(
{
search: stringify({
...query,
page: pagination.pageCount
})
},
{ replace: true }
);
}
}, [pagination, formatMessage, query, navigate]);
const { canCreate } = useDocumentRBAC("ListViewPage", ({ canCreate: canCreate2 }) => ({
canCreate: canCreate2
}));
const runHookWaterfall = useStrapiApp("ListViewPage", ({ runHookWaterfall: runHookWaterfall2 }) => runHookWaterfall2);
const tableHeaders = React.useMemo(() => {
const headers = runHookWaterfall(INJECT_COLUMN_IN_TABLE, {
displayedHeaders,
layout: list
});
const formattedHeaders = headers.displayedHeaders.map((header) => {
const translation = typeof header.label === "string" ? {
id: `content-manager.content-types.${model}.${header.name}`,
defaultMessage: header.label
} : header.label;
return {
...header,
label: formatMessage(translation),
name: `${header.name}${header.mainField?.name ? `.${header.mainField.name}` : ""}`
};
});
if (schema?.options?.draftAndPublish) {
formattedHeaders.push({
attribute: {
type: "custom"
},
name: "status",
label: formatMessage({
id: getTranslation(`containers.list.table-headers.status`),
defaultMessage: "status"
}),
searchable: false,
sortable: false
});
}
return formattedHeaders;
}, [
displayedHeaders,
formatMessage,
list,
runHookWaterfall,
schema?.options?.draftAndPublish,
model
]);
if (isFetching) {
return /* @__PURE__ */ jsx(Page.Loading, {});
}
if (error) {
return /* @__PURE__ */ jsx(Page.Error, {});
}
const contentTypeTitle = schema?.info.displayName ?? "Untitled";
const handleRowClick = (id) => () => {
trackUsage("willEditEntryFromList");
navigate({
pathname: id.toString(),
search: stringify({ plugins: query.plugins })
});
};
return /* @__PURE__ */ jsxs(Page.Main, { children: [
/* @__PURE__ */ jsx(Page.Title, { children: `${contentTypeTitle}` }),
/* @__PURE__ */ jsx(
LayoutsHeaderCustom,
{
primaryAction: canCreate ? /* @__PURE__ */ jsx(CreateButton, {}) : null,
subtitle: formatMessage(
{
id: getTranslation("pages.ListView.header-subtitle"),
defaultMessage: "{number, plural, =0 {# entries} one {# entry} other {# entries}} found"
},
{ number: pagination?.total }
),
title: contentTypeTitle,
navigationAction: /* @__PURE__ */ jsx(BackButton, {})
}
),
/* @__PURE__ */ jsx(
Layouts.Action,
{
endActions: /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx(InjectionZone, { area: "listView.actions" }),
/* @__PURE__ */ jsx(
ViewSettingsMenu,
{
setHeaders: handleSetHeaders,
resetHeaders: () => setDisplayedHeaders(list.layout),
headers: displayedHeaders.map((header) => header.name)
}
)
] }),
startActions: /* @__PURE__ */ jsxs(Fragment, { children: [
list.settings.searchable && /* @__PURE__ */ jsx(
SearchInput,
{
disabled: results.length === 0,
label: formatMessage(
{ id: "app.component.search.label", defaultMessage: "Search for {target}" },
{ target: contentTypeTitle }
),
placeholder: formatMessage({
id: "global.search",
defaultMessage: "Search"
}),
trackedEvent: "didSearch"
}
),
list.settings.filterable && schema ? /* @__PURE__ */ jsx(FiltersImpl, { disabled: results.length === 0, schema }) : null
] })
}
),
/* @__PURE__ */ jsx(Layouts.Content, { children: /* @__PURE__ */ jsxs(Flex, { gap: 4, direction: "column", alignItems: "stretch", children: [
/* @__PURE__ */ jsxs(Table.Root, { rows: results, headers: tableHeaders, isLoading: isFetching, children: [
/* @__PURE__ */ jsx(TableActionsBar, {}),
/* @__PURE__ */ jsxs(Table.Content, { children: [
/* @__PURE__ */ jsxs(Table.Head, { children: [
/* @__PURE__ */ jsx(Table.HeaderCheckboxCell, {}),
tableHeaders.map((header) => /* @__PURE__ */ jsx(Table.HeaderCell, { ...header }, header.name))
] }),
/* @__PURE__ */ jsx(Table.Loading, {}),
/* @__PURE__ */ jsx(Table.Empty, { action: canCreate ? /* @__PURE__ */ jsx(CreateButton, { variant: "secondary" }) : null }),
/* @__PURE__ */ jsx(Table.Body, { children: results.map((row) => {
return /* @__PURE__ */ jsxs(
Table.Row,
{
cursor: "pointer",
onClick: handleRowClick(row.documentId),
children: [
/* @__PURE__ */ jsx(Table.CheckboxCell, { id: row.id }),
tableHeaders.map(({ cellFormatter, ...header }) => {
if (header.name === "status") {
const { status } = row;
return /* @__PURE__ */ jsx(Table.Cell, { children: /* @__PURE__ */ jsx(DocumentStatus, { status, maxWidth: "min-content" }) }, header.name);
}
if (["createdBy", "updatedBy"].includes(header.name.split(".")[0])) {
return /* @__PURE__ */ jsx(Table.Cell, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral800", children: row[header.name.split(".")[0]] ? getDisplayName(row[header.name.split(".")[0]]) : "-" }) }, header.name);
}
if (typeof cellFormatter === "function") {
return /* @__PURE__ */ jsx(Table.Cell, { children: cellFormatter(row, header, { collectionType, model }) }, header.name);
}
return /* @__PURE__ */ jsx(Table.Cell, { children: /* @__PURE__ */ jsx(
CellContent,
{
content: row[header.name.split(".")[0]],
rowId: row.documentId,
...header
}
) }, header.name);
}),
/* @__PURE__ */ jsx(ActionsCell, { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsx(TableActions, { document: row }) })
]
},
row.id
);
}) })
] })
] }),
/* @__PURE__ */ jsxs(
Pagination.Root,
{
...pagination,
onPageSizeChange: () => trackUsage("willChangeNumberOfEntriesPerPage"),
children: [
/* @__PURE__ */ jsx(Pagination.PageSize, {}),
/* @__PURE__ */ jsx(Pagination.Links, {})
]
}
)
] }) })
] });
};
const ActionsCell = styled(Table.Cell)`
display: flex;
justify-content: flex-end;
`;
const TableActionsBar = () => {
const selectRow = useTable("TableActionsBar", (state) => state.selectRow);
const [{ query }] = useQueryParams();
const locale = query?.plugins?.i18n?.locale;
const prevLocale = usePrev(locale);
React.useEffect(() => {
if (prevLocale !== locale) {
selectRow([]);
}
}, [selectRow, prevLocale, locale]);
return /* @__PURE__ */ jsx(Table.ActionBar, { children: /* @__PURE__ */ jsx(BulkActionsRenderer, {}) });
};
const CreateButton = ({ variant }) => {
const { formatMessage } = useIntl();
const { trackUsage } = useTracking();
const [{ query }] = useQueryParams();
return /* @__PURE__ */ jsx(
Button,
{
variant,
tag: Link,
onClick: () => {
trackUsage("willCreateEntry", { status: "draft" });
},
startIcon: /* @__PURE__ */ jsx(ForwardRef$1d, {}),
style: { textDecoration: "none" },
to: {
pathname: "create",
search: stringify({ plugins: query.plugins })
},
minWidth: "max-content",
marginLeft: 2,
children: formatMessage({
id: getTranslation("HeaderLayout.button.label-add-entry"),
defaultMessage: "Create new entry"
})
}
);
};
const ProtectedListViewPage = () => {
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(Page.Error, {});
}
return /* @__PURE__ */ jsx(Page.Protect, { permissions, children: ({ permissions: permissions2 }) => /* @__PURE__ */ jsx(DocumentRBAC, { permissions: permissions2, children: /* @__PURE__ */ jsx(ListViewPage, {}) }) });
};
export {
ListViewPage,
ProtectedListViewPage
};