UNPKG

strapi-plugin-publisher

Version:

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

672 lines (671 loc) 20.4 kB
"use strict"; const react = require("react"); const PropTypes = require("prop-types"); const jsxRuntime = require("react/jsx-runtime"); const reactRouterDom = require("react-router-dom"); const admin = require("@strapi/strapi/admin"); const reactQuery = require("react-query"); const reactIntl = require("react-intl"); const designSystem = require("@strapi/design-system"); const icons = require("@strapi/icons"); const _interopDefault = (e) => e && e.__esModule ? e : { default: e }; const PropTypes__default = /* @__PURE__ */ _interopDefault(PropTypes); 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$1 = { "./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: exports$1, author, maintainers, homepage, repository, bugs, dependencies, devDependencies, peerDependencies, strapi, engines, keywords, license }; const pluginId = pluginPkg.strapi.name; const Initializer = ({ setPlugin }) => { const ref = react.useRef(); ref.current = setPlugin; react.useEffect(() => { ref.current(pluginId); }, []); return null; }; Initializer.propTypes = { setPlugin: PropTypes__default.default.func.isRequired }; const useSettings = () => { const { get } = admin.useFetchClient(); function getSettings() { return reactQuery.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 } = reactIntl.useIntl(); const [locale, setLocale] = react.useState(browserLocale); const [step, setStep] = react.useState(1); const { getSettings } = useSettings(); function handleDateChange(date) { if (onChange) { onChange(date); } } const { isLoading, data, isRefetching } = getSettings(); react.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__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [ /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "action-date-time-picker", children: [ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", textColor: "neutral600", children: formatMessage({ id: getTrad(`action.header.${mode}.title`), defaultMessage: `${mode} Date` }) }), /* @__PURE__ */ jsxRuntime.jsx( designSystem.DateTimePicker, { "aria-label": "datetime picker", onChange: handleDateChange, value: executeAt ? new Date(executeAt) : null, disabled: !isCreating, step, locale } ) ] }), /* @__PURE__ */ jsxRuntime.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__default.default.oneOfType([PropTypes__default.default.string, PropTypes__default.default.instanceOf(Date)]), onChange: PropTypes__default.default.func, mode: PropTypes__default.default.string.isRequired, isCreating: PropTypes__default.default.bool.isRequired, isEditing: PropTypes__default.default.bool.isRequired }; const ActionButtons = ({ mode, isEditing, onEdit, onCreate, isCreating, executeAt, onDelete, onSave, canPublish, isLoading }) => { const { formatMessage } = reactIntl.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__ */ jsxRuntime.jsx( designSystem.Button, { disabled: isLoading || !executeAt, fullWidth: true, variant: "success-light", startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.PaperPlane, {}), onClick: handleSaveChange, style: { minHeight: "auto" }, children: formatMessage({ id: getTrad(`action.footer.${mode}.button.save`), defaultMessage: `Save` }) } ); } if (isEditing) { return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [ /* @__PURE__ */ jsxRuntime.jsx( designSystem.Button, { onClick: handleEditChange, fullWidth: true, variant: "tertiary", startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Pencil, {}), style: { minHeight: "auto" }, children: formatMessage({ id: getTrad(`action.footer.${mode}.button.edit`), defaultMessage: `Edit` }) } ), /* @__PURE__ */ jsxRuntime.jsx( designSystem.Button, { onClick: handleDeleteChange, fullWidth: true, variant: "danger-light", startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {}), style: { minHeight: "auto" }, children: formatMessage({ id: getTrad(`action.footer.${mode}.button.delete`), defaultMessage: `Delete` }) } ) ] }); } return /* @__PURE__ */ jsxRuntime.jsx( designSystem.Button, { fullWidth: true, variant: mode === "publish" ? "default" : "secondary", startIcon: mode === "publish" ? /* @__PURE__ */ jsxRuntime.jsx(icons.Check, {}) : /* @__PURE__ */ jsxRuntime.jsx(icons.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__default.default.string.isRequired, executeAt: PropTypes__default.default.oneOfType([PropTypes__default.default.string, PropTypes__default.default.instanceOf(Date)]), isEditing: PropTypes__default.default.bool.isRequired, onEdit: PropTypes__default.default.func, onCreate: PropTypes__default.default.func, isCreating: PropTypes__default.default.bool.isRequired, isLoading: PropTypes__default.default.bool.isRequired, onDelete: PropTypes__default.default.func, onSave: PropTypes__default.default.func, canPublish: PropTypes__default.default.bool.isRequired }; const buildQueryKey = (args) => args.filter((a) => a); const usePublisher = () => { const { toggleNotification } = admin.useNotification(); const setErrors = admin.useForm("PublishAction", (state) => state.setErrors); const { _unstableFormatValidationErrors: formatValidationErrors } = admin.useAPIErrorHandler(); const { del, post, put, get } = admin.useFetchClient(); const queryClient = reactQuery.useQueryClient(); const { formatMessage } = reactIntl.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 reactQuery.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 } = reactQuery.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 } = reactQuery.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 } = reactQuery.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] = react.useState(0); const [isEditing, setIsEditing] = react.useState(false); const [executeAt, setExecuteAt] = react.useState(null); const [isCreating, setIsCreating] = react.useState(false); const [isLoading, setIsLoading] = react.useState(false); const [canPublish, setCanPublish] = react.useState(true); const { isLoading: isLoadingPermissions, allowedActions } = admin.useRBAC({ publish: [{ action: "plugin::content-manager.explorer.publish", subject: entitySlug }] }); react.useEffect(() => { if (!isLoadingPermissions) { setCanPublish(allowedActions.canPublish); } }, [isLoadingPermissions]); const { isLoading: isLoadingAction, data, isRefetching: isRefetchingAction } = getAction({ mode, entityId: documentId, entitySlug }); react.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__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: { initial: 2 }, direction: { initial: "column" }, children: [ /* @__PURE__ */ jsxRuntime.jsx( ActionDateTimePicker, { onChange: handleDateChange, executeAt, isCreating, isEditing, mode } ), /* @__PURE__ */ jsxRuntime.jsx( ActionButtons, { mode, onEdit: handleOnEdit, isEditing, isCreating, isLoading, executeAt, canPublish, onCreate: handleOnCreate, onSave: handleOnSave, onDelete: handleOnDelete } ) ] }); }; Action.propTypes = { mode: PropTypes__default.default.string.isRequired, documentId: PropTypes__default.default.string.isRequired, entitySlug: PropTypes__default.default.string.isRequired, locale: PropTypes__default.default.string }; const actionModes = ["publish", "unpublish"]; const ActionManager = () => { const entity = admin.unstable_useContentManagerContext(); const location = reactRouterDom.useLocation(); const params = new URLSearchParams(location.search); const currentLocale = params.get("plugins[i18n][locale]"); const { document } = admin.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] = react.useState(true); react.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__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: actionModes.map((mode) => /* @__PURE__ */ jsxRuntime.jsx("div", { style: { width: "100%" }, children: /* @__PURE__ */ jsxRuntime.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": () => Promise.resolve().then(() => require("../_chunks/de-88fz-6HR.js")), "./translations/en.json": () => Promise.resolve().then(() => require("../_chunks/en-CDmKULOK.js")), "./translations/fr.json": () => Promise.resolve().then(() => require("../_chunks/fr-z9QMKyCj.js")), "./translations/nl.json": () => Promise.resolve().then(() => require("../_chunks/nl-DxcQ3n4D.js")) }), `./translations/${locale}.json`, 3).then(({ default: data }) => { return { data: prefixPluginTranslations(data, pluginId), locale }; }).catch(() => { return { data: {}, locale }; }); }) ); return Promise.resolve(importedTrads); } }; module.exports = index;