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