UNPKG

strapi-plugin-publisher

Version:

A plugin for Strapi Headless CMS that provides the ability to schedule publishing for any content type.

671 lines (670 loc) 19.3 kB
import { useRef, useEffect, useState } from "react"; import PropTypes from "prop-types"; import { jsxs, Fragment, jsx } from "react/jsx-runtime"; import { useLocation } from "react-router-dom"; import { useFetchClient, useNotification, useForm, useAPIErrorHandler, useRBAC, unstable_useContentManagerContext, unstable_useDocument } from "@strapi/strapi/admin"; import { useQuery, useQueryClient, useMutation } from "react-query"; import { useIntl } from "react-intl"; import { Typography, DateTimePicker, Button, Flex } from "@strapi/design-system"; import { PaperPlane, Pencil, Trash, Check, Cross } from "@strapi/icons"; const __variableDynamicImportRuntimeHelper = (glob, path, segs) => { const v = glob[path]; if (v) { return typeof v === "function" ? v() : Promise.resolve(v); } return new Promise((_, reject) => { (typeof queueMicrotask === "function" ? queueMicrotask : setTimeout)( reject.bind( null, new Error( "Unknown variable dynamic import: " + path + (path.split("/").length !== segs ? ". Note that variables only represent file names one level deep." : "") ) ) ); }); }; const $schema = "https://json.schemastore.org/package"; const name$1 = "strapi-plugin-publisher"; const version = "2.0.2"; const description = "A plugin for Strapi Headless CMS that provides the ability to schedule publishing for any content type."; const scripts = { lint: "eslint . --fix", format: "prettier --write **/*.{ts,js,json,yml}", build: "strapi-plugin build", watch: "strapi-plugin watch", "watch:link": "strapi-plugin watch:link" }; const exports = { "./strapi-admin": { source: "./admin/src/index.ts", "import": "./dist/admin/index.mjs", require: "./dist/admin/index.js", "default": "./dist/admin/index.js" }, "./strapi-server": { source: "./server/index.js", "import": "./dist/server/index.mjs", require: "./dist/server/index.js", "default": "./dist/server/index.js" }, "./package.json": "./package.json" }; const author = { name: "@ComfortablyCoding", url: "https://github.com/ComfortablyCoding" }; const maintainers = [ { name: "@PluginPal", url: "https://github.com/PluginPal" } ]; const homepage = "https://github.com/PluginPal/strapi-plugin-publisher#readme"; const repository = { type: "git", url: "https://github.com/PluginPal/strapi-plugin-publisher.git" }; const bugs = { url: "https://github.com/PluginPal/strapi-plugin-publisher/issues" }; const dependencies = { lodash: "^4.17.21", "prop-types": "^15.8.1", "react-intl": "^6.6.2", "react-query": "^3.39.3" }; const devDependencies = { "@babel/core": "^7.23.3", "@babel/eslint-parser": "^7.23.3", "@babel/preset-react": "^7.23.3", "@strapi/sdk-plugin": "^5.2.7", "@strapi/strapi": "^5.2.0", eslint: "^8.53.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-node": "^11.1.0", "eslint-plugin-react": "^7.33.2", prettier: "^3.1.0", react: "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.0.0", "styled-components": "^6.0.0" }; const peerDependencies = { "@strapi/design-system": "^2.0.0-rc.11", "@strapi/icons": "^2.0.0-rc.11", "@strapi/strapi": "^5.2.0", "@strapi/utils": "^5.2.0", react: "^17.0.0 || ^18.0.0", "react-router-dom": "^6.0.0", "styled-components": "^6.0.0" }; const strapi = { displayName: "Publisher", name: "publisher", description: "A plugin for Strapi Headless CMS that provides the ability to schedule publishing for any content type.", kind: "plugin" }; const engines = { node: ">=18.0.0 <=22.x.x", npm: ">=6.0.0" }; const keywords = [ "strapi", "strapi-plugin", "plugin", "strapi plugin", "publishing", "schedule publish" ]; const license = "MIT"; const pluginPkg = { $schema, name: name$1, version, description, scripts, exports, author, maintainers, homepage, repository, bugs, dependencies, devDependencies, peerDependencies, strapi, engines, keywords, license }; const pluginId = pluginPkg.strapi.name; const Initializer = ({ setPlugin }) => { const ref = useRef(); ref.current = setPlugin; useEffect(() => { ref.current(pluginId); }, []); return null; }; Initializer.propTypes = { setPlugin: PropTypes.func.isRequired }; const useSettings = () => { const { get } = useFetchClient(); function getSettings() { return useQuery({ queryKey: [pluginId, "settings"], queryFn: function() { return get(`/${pluginId}/settings`); }, select: function({ data }) { return data.data || false; } }); } return { getSettings }; }; const getTrad = (id) => `${pluginId}.${id}`; const ActionDateTimePicker = ({ executeAt, mode, isCreating, isEditing, onChange }) => { const { formatMessage, locale: browserLocale } = useIntl(); const [locale, setLocale] = useState(browserLocale); const [step, setStep] = useState(1); const { getSettings } = useSettings(); function handleDateChange(date) { if (onChange) { onChange(date); } } const { isLoading, data, isRefetching } = getSettings(); useEffect(() => { if (!isLoading && !isRefetching) { if (data) { setStep(data.components.dateTimePicker.step); const customLocale = data.components.dateTimePicker.locale; try { new Intl.DateTimeFormat(customLocale); setLocale(customLocale); } catch (error) { console.warn( `'${customLocale}' is not a valid locale format. Falling back to browser locale: '${browserLocale}'` ); setLocale(browserLocale); } } } }, [isLoading, isRefetching]); if (!isCreating && !isEditing) { return null; } return /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsxs("div", { id: "action-date-time-picker", children: [ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: formatMessage({ id: getTrad(`action.header.${mode}.title`), defaultMessage: `${mode} Date` }) }), /* @__PURE__ */ jsx( DateTimePicker, { "aria-label": "datetime picker", onChange: handleDateChange, value: executeAt ? new Date(executeAt) : null, disabled: !isCreating, step, locale } ) ] }), /* @__PURE__ */ jsx("style", { children: ` #action-date-time-picker { width: 100% !important; } #action-date-time-picker > div { flex-direction: column !important; } #action-date-time-picker > div > div { width: 100% !important; } ` }) ] }); }; ActionDateTimePicker.propTypes = { executeAt: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]), onChange: PropTypes.func, mode: PropTypes.string.isRequired, isCreating: PropTypes.bool.isRequired, isEditing: PropTypes.bool.isRequired }; const ActionButtons = ({ mode, isEditing, onEdit, onCreate, isCreating, executeAt, onDelete, onSave, canPublish, isLoading }) => { const { formatMessage } = useIntl(); function handleEditChange() { if (onEdit) { onEdit(); } } function handleCreateChange() { if (onCreate) { onCreate(); } } function handleSaveChange() { if (onSave) { onSave(); } } function handleDeleteChange() { if (onDelete) { onDelete(); } } if ((isCreating || isEditing) && !canPublish) { return null; } if (isCreating) { return /* @__PURE__ */ jsx( Button, { disabled: isLoading || !executeAt, fullWidth: true, variant: "success-light", startIcon: /* @__PURE__ */ jsx(PaperPlane, {}), onClick: handleSaveChange, style: { minHeight: "auto" }, children: formatMessage({ id: getTrad(`action.footer.${mode}.button.save`), defaultMessage: `Save` }) } ); } if (isEditing) { return /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx( Button, { onClick: handleEditChange, fullWidth: true, variant: "tertiary", startIcon: /* @__PURE__ */ jsx(Pencil, {}), style: { minHeight: "auto" }, children: formatMessage({ id: getTrad(`action.footer.${mode}.button.edit`), defaultMessage: `Edit` }) } ), /* @__PURE__ */ jsx( Button, { onClick: handleDeleteChange, fullWidth: true, variant: "danger-light", startIcon: /* @__PURE__ */ jsx(Trash, {}), style: { minHeight: "auto" }, children: formatMessage({ id: getTrad(`action.footer.${mode}.button.delete`), defaultMessage: `Delete` }) } ) ] }); } return /* @__PURE__ */ jsx( Button, { fullWidth: true, variant: mode === "publish" ? "default" : "secondary", startIcon: mode === "publish" ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Cross, {}), onClick: handleCreateChange, disabled: !canPublish || isLoading, style: { minHeight: "auto" }, children: formatMessage({ id: getTrad(`action.footer.${mode}.button.add`), defaultMessage: `${mode} date` }) } ); }; ActionButtons.propTypes = { mode: PropTypes.string.isRequired, executeAt: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]), isEditing: PropTypes.bool.isRequired, onEdit: PropTypes.func, onCreate: PropTypes.func, isCreating: PropTypes.bool.isRequired, isLoading: PropTypes.bool.isRequired, onDelete: PropTypes.func, onSave: PropTypes.func, canPublish: PropTypes.bool.isRequired }; const buildQueryKey = (args) => args.filter((a) => a); const usePublisher = () => { const { toggleNotification } = useNotification(); const setErrors = useForm("PublishAction", (state) => state.setErrors); const { _unstableFormatValidationErrors: formatValidationErrors } = useAPIErrorHandler(); const { del, post, put, get } = useFetchClient(); const queryClient = useQueryClient(); const { formatMessage } = useIntl(); function onSuccessHandler({ queryKey, notification }) { queryClient.invalidateQueries(queryKey); toggleNotification({ type: notification.type, message: formatMessage({ id: getTrad(notification.tradId), defaultMessage: "Action completed successfully" }) }); } function onErrorHandler(error) { toggleNotification({ type: "danger", message: error.response?.data?.error?.message || error.message || formatMessage({ id: "notification.error", defaultMessage: "An unexpected error occurred" }) }); if (error.response?.data?.error?.name === "ValidationError") { setErrors(formatValidationErrors(error.response?.data?.error)); } } function getAction(filters = {}) { return useQuery({ queryKey: buildQueryKey([ pluginId, "entity-action", filters.documentId, filters.entitySlug, filters.mode ]), queryFn: function() { return get(`/${pluginId}/actions`, { params: { filters } }); }, select: function({ data }) { return data.data[0] || false; } }); } const { mutateAsync: createAction } = useMutation({ mutationFn: function(body) { return post(`/${pluginId}/actions`, { data: body }); }, onSuccess: ({ data: response }) => { const { data } = response; const queryKey = buildQueryKey([ pluginId, "entity-action", data.documentId, data.entitySlug, data.mode ]); onSuccessHandler({ queryKey, notification: { type: "success", tradId: `action.notification.${data.mode}.create.success` } }); }, onError: onErrorHandler }); const { mutateAsync: updateAction } = useMutation({ mutationFn: function({ id, body }) { return put(`/${pluginId}/actions/${id}`, { data: body }); }, onSuccess: ({ data: response }) => { const { data } = response; const queryKey = buildQueryKey([ pluginId, "entity-action", data.documentId, data.entitySlug, data.mode ]); onSuccessHandler({ queryKey, notification: { type: "success", tradId: `action.notification.${data.mode}.update.success` } }); }, onError: onErrorHandler }); const { mutateAsync: deleteAction } = useMutation({ mutationFn: function({ id }) { return del(`/${pluginId}/actions/${id}`); }, onSuccess: (_response, actionMode) => { const { mode } = actionMode; const queryKey = buildQueryKey([ pluginId, "entity-action" ]); onSuccessHandler({ queryKey, notification: { type: "success", tradId: `action.notification.${mode}.delete.success` } }); }, onError: onErrorHandler }); return { getAction, createAction, updateAction, deleteAction }; }; const Action = ({ mode, documentId, entitySlug, locale }) => { const { createAction, getAction, updateAction, deleteAction } = usePublisher(); const [actionId, setActionId] = useState(0); const [isEditing, setIsEditing] = useState(false); const [executeAt, setExecuteAt] = useState(null); const [isCreating, setIsCreating] = useState(false); const [isLoading, setIsLoading] = useState(false); const [canPublish, setCanPublish] = useState(true); const { isLoading: isLoadingPermissions, allowedActions } = useRBAC({ publish: [{ action: "plugin::content-manager.explorer.publish", subject: entitySlug }] }); useEffect(() => { if (!isLoadingPermissions) { setCanPublish(allowedActions.canPublish); } }, [isLoadingPermissions]); const { isLoading: isLoadingAction, data, isRefetching: isRefetchingAction } = getAction({ mode, entityId: documentId, entitySlug }); useEffect(() => { setIsLoading(true); if (!isLoadingAction && !isRefetchingAction) { setIsLoading(false); if (data) { setActionId(data.documentId); setExecuteAt(data.executeAt); setIsEditing(true); } else { setActionId(0); } } }, [isLoadingAction, isRefetchingAction]); function handleDateChange(date) { setExecuteAt(date); } const handleOnEdit = () => { setIsCreating(true); setIsEditing(false); }; const handleOnCreate = () => { setIsCreating(true); }; const handleOnSave = async () => { setIsLoading(true); try { if (!actionId) { const { data: response } = await createAction({ mode, entityId: documentId, entitySlug, executeAt, locale }); if (response.data && response.data.id) { setActionId(response.data.documentId); } } else { await updateAction({ id: actionId, body: { executeAt } }); } setIsCreating(false); setIsEditing(true); setIsLoading(false); } catch (error) { console.error("Error saving action:", error); setIsLoading(false); } }; const handleOnDelete = async () => { setIsLoading(true); await deleteAction({ id: actionId, mode }); setActionId(0); setExecuteAt(null); setIsCreating(false); setIsEditing(false); setIsLoading(false); }; return /* @__PURE__ */ jsxs(Flex, { gap: { initial: 2 }, direction: { initial: "column" }, children: [ /* @__PURE__ */ jsx( ActionDateTimePicker, { onChange: handleDateChange, executeAt, isCreating, isEditing, mode } ), /* @__PURE__ */ jsx( ActionButtons, { mode, onEdit: handleOnEdit, isEditing, isCreating, isLoading, executeAt, canPublish, onCreate: handleOnCreate, onSave: handleOnSave, onDelete: handleOnDelete } ) ] }); }; Action.propTypes = { mode: PropTypes.string.isRequired, documentId: PropTypes.string.isRequired, entitySlug: PropTypes.string.isRequired, locale: PropTypes.string }; const actionModes = ["publish", "unpublish"]; const ActionManager = () => { const entity = unstable_useContentManagerContext(); const location = useLocation(); const params = new URLSearchParams(location.search); const currentLocale = params.get("plugins[i18n][locale]"); const { document } = unstable_useDocument({ documentId: entity?.id, model: entity?.model, collectionType: entity?.collectionType, params: { locale: currentLocale } }); const { getSettings } = useSettings(); const { isLoading, data, isRefetching } = getSettings(); const [show, setShow] = useState(true); useEffect(() => { if (!isLoading && !isRefetching) { const allowedList = data?.contentTypes || []; const isAllowed = allowedList.length === 0 || allowedList.includes(entity.slug); setShow(isAllowed); } }, [isLoading, isRefetching, data?.contentTypes, entity.slug]); if (!entity.hasDraftAndPublish || entity.isCreatingEntry) { return null; } if (!document || !entity) { return null; } if (!isLoading && !isRefetching && !show) { return null; } return { title: "Publisher", content: /* @__PURE__ */ jsx(Fragment, { children: actionModes.map((mode) => /* @__PURE__ */ jsx("div", { style: { width: "100%" }, children: /* @__PURE__ */ jsx( Action, { mode, documentId: document.documentId, entitySlug: entity.model, locale: currentLocale } ) }, mode)) }) }; }; const prefixPluginTranslations = (trad, pluginId2) => { return Object.keys(trad).reduce((acc, current) => { acc[`${pluginId2}.${current}`] = trad[current]; return acc; }, {}); }; const name = pluginPkg.strapi.name; const index = { register(app) { app.registerPlugin({ id: pluginId, initializer: Initializer, isReady: false, name }); }, bootstrap(app) { app.getPlugin("content-manager").apis.addEditViewSidePanel([ActionManager]); }, async registerTrads(app) { const { locales } = app; const importedTrads = await Promise.all( locales.map((locale) => { return __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/de.json": () => import("../_chunks/de-CyfPTspv.mjs"), "./translations/en.json": () => import("../_chunks/en-jNnggUI8.mjs"), "./translations/fr.json": () => import("../_chunks/fr-CsuQ9P6n.mjs"), "./translations/nl.json": () => import("../_chunks/nl-B0_tes-O.mjs") }), `./translations/${locale}.json`, 3).then(({ default: data }) => { return { data: prefixPluginTranslations(data, pluginId), locale }; }).catch(() => { return { data: {}, locale }; }); }) ); return Promise.resolve(importedTrads); } }; export { index as default };