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
JavaScript
"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;