UNPKG

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
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 };