├── .prettierignore
├── admin
└── src
│ ├── translations
│ ├── nl.json
│ └── en.json
│ ├── utils
│ └── getTrad.ts
│ ├── pluginId.ts
│ ├── components
│ ├── PluginIcon
│ │ └── index.tsx
│ ├── date-time-picker-wrapper
│ │ └── index.tsx
│ ├── Initializer
│ │ └── index.tsx
│ └── Scheduler
│ │ └── index.tsx
│ └── index.tsx
├── server
├── config
│ └── index.ts
├── services
│ ├── index.ts
│ └── scheduler.ts
├── destroy.ts
├── register.ts
├── content-types
│ ├── index.ts
│ └── scheduler
│ │ └── schema.json
├── controllers
│ ├── index.ts
│ ├── config.ts
│ └── scheduler.ts
├── routes
│ └── index.ts
├── index.ts
└── bootstrap.ts
├── strapi-server.js
├── strapi-admin.js
├── custom.d.ts
├── tsconfig.json
├── tsconfig.server.json
├── .github
└── workflows
│ └── jira.yml
├── README.md
├── package.json
└── .gitignore
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
--------------------------------------------------------------------------------
/admin/src/translations/nl.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/server/config/index.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | default: {},
3 | validator() {}
4 | };
5 |
--------------------------------------------------------------------------------
/strapi-server.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = require('./dist/server');
4 |
--------------------------------------------------------------------------------
/strapi-admin.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = require('./admin/src').default;
4 |
--------------------------------------------------------------------------------
/server/services/index.ts:
--------------------------------------------------------------------------------
1 | import schedulerService from './scheduler';
2 |
3 | export default {
4 | scheduler: schedulerService
5 | };
6 |
--------------------------------------------------------------------------------
/server/destroy.ts:
--------------------------------------------------------------------------------
1 | import { Strapi } from '@strapi/strapi';
2 |
3 | export default ({ strapi }: { strapi: Strapi }) => {
4 | // destroy phase
5 | };
6 |
--------------------------------------------------------------------------------
/admin/src/utils/getTrad.ts:
--------------------------------------------------------------------------------
1 | import pluginId from '../pluginId';
2 |
3 | const getTrad = (id: string) => `${pluginId}.${id}`;
4 |
5 | export default getTrad;
6 |
--------------------------------------------------------------------------------
/server/register.ts:
--------------------------------------------------------------------------------
1 | import { Strapi } from '@strapi/strapi';
2 |
3 | export default ({ strapi }: { strapi: Strapi }) => {
4 | // registeration phase
5 | };
6 |
--------------------------------------------------------------------------------
/server/content-types/index.ts:
--------------------------------------------------------------------------------
1 | const scheduler = require('./scheduler/schema.json');
2 |
3 | export default {
4 | scheduler: {
5 | schema: scheduler
6 | }
7 | };
8 |
--------------------------------------------------------------------------------
/admin/src/pluginId.ts:
--------------------------------------------------------------------------------
1 | import pluginPkg from '../../package.json';
2 |
3 | const pluginId = pluginPkg.name.replace(/^(@[^-,.][\w,-]+\/|strapi-)plugin-/i, '');
4 |
5 | export default pluginId;
6 |
--------------------------------------------------------------------------------
/custom.d.ts:
--------------------------------------------------------------------------------
1 | declare module '@strapi/design-system/*';
2 | declare module '@strapi/design-system';
3 | declare module '@strapi/icons';
4 | declare module '@strapi/icons/*';
5 | declare module '@strapi/helper-plugin';
6 |
--------------------------------------------------------------------------------
/server/controllers/index.ts:
--------------------------------------------------------------------------------
1 | import configController from './config';
2 | import schedulerController from './scheduler';
3 |
4 | export default {
5 | config: configController,
6 | scheduler: schedulerController
7 | };
8 |
--------------------------------------------------------------------------------
/admin/src/translations/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "Settings.scheduler.schedule.success": "Post publish has been scheduled",
3 | "Settings.scheduler.depublish.success": "Post unpublish has been scheduled",
4 | "Settings.scheduler.alreadyExist": "This locale already exists"
5 | }
6 |
--------------------------------------------------------------------------------
/admin/src/components/PluginIcon/index.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * PluginIcon
4 | *
5 | */
6 |
7 | import React from 'react';
8 | import { Puzzle } from '@strapi/icons';
9 |
10 | const PluginIcon: React.VoidFunctionComponent = () => ;
11 |
12 | export default PluginIcon;
13 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@strapi/typescript-utils/tsconfigs/admin",
3 |
4 | "compilerOptions": {
5 | "target": "ESNext",
6 | "strict": true
7 | },
8 |
9 | "include": ["admin", "custom.d.ts"],
10 |
11 | "exclude": [
12 | "node_modules/",
13 | "dist/",
14 |
15 | // Do not include server files in the server compilation
16 | "server/",
17 | // Do not include test files
18 | "**/*.test.ts"
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/server/routes/index.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | admin: {
3 | type: 'admin',
4 | routes: [
5 | {
6 | method: 'GET',
7 | path: '/config',
8 | handler: 'config.getGlobalConfig'
9 | },
10 | {
11 | method: 'GET',
12 | path: '/config/:uid',
13 | handler: 'config.getContentTypeConfig'
14 | },
15 | {
16 | method: 'GET',
17 | path: '/scheduler/:uid/:entryId',
18 | handler: 'scheduler.getByUidAndEntryId'
19 | }
20 | ]
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/server/index.ts:
--------------------------------------------------------------------------------
1 | import register from './register';
2 | import bootstrap from './bootstrap';
3 | import destroy from './destroy';
4 | import config from './config';
5 | import contentTypes from './content-types';
6 | import controllers from './controllers';
7 | import routes from './routes';
8 | import services from './services';
9 |
10 | export default {
11 | register,
12 | bootstrap,
13 | destroy,
14 | config,
15 | controllers,
16 | routes,
17 | services,
18 | contentTypes
19 | };
20 |
--------------------------------------------------------------------------------
/admin/src/components/date-time-picker-wrapper/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 |
3 | import { DateTimePicker } from '@strapi/design-system';
4 |
5 | const component = ({ ...props }: any) => {
6 | return ;
7 | };
8 |
9 | const DateTimePickerWrapper = memo(component, arePropsEqual);
10 |
11 | export default DateTimePickerWrapper;
12 |
13 | function arePropsEqual(oldProps: any, newProps: any) {
14 | return oldProps?.value === newProps?.value;
15 | }
16 |
--------------------------------------------------------------------------------
/admin/src/components/Initializer/index.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * Initializer
4 | *
5 | */
6 |
7 | import React, { useEffect, useRef } from 'react';
8 | import pluginId from '../../pluginId';
9 |
10 | type InitializerProps = {
11 | setPlugin: (id: string) => void;
12 | };
13 |
14 | const Initializer = ({ setPlugin }: InitializerProps) => {
15 | const ref = useRef(setPlugin);
16 |
17 | useEffect(() => {
18 | ref.current(pluginId);
19 | }, []);
20 |
21 | return null;
22 | };
23 |
24 | export default Initializer;
25 |
--------------------------------------------------------------------------------
/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@strapi/typescript-utils/tsconfigs/server",
3 |
4 | "compilerOptions": {
5 | "outDir": "dist",
6 | "rootDir": ".",
7 | "moduleResolution": "node",
8 | "esModuleInterop": true,
9 | "module": "CommonJS"
10 | },
11 |
12 | "include": [
13 | // Include the root directory
14 | "server",
15 | // Force the JSON files in the src folder to be included
16 | "server/**/*.json"
17 | ],
18 |
19 | "exclude": [
20 | "node_modules/",
21 | "dist/",
22 |
23 | // Do not include admin files in the server compilation
24 | "admin/",
25 | // Do not include test files
26 | "**/*.test.ts"
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/server/content-types/scheduler/schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "info": {
3 | "collectionName": "scheduler",
4 | "singularName": "scheduler",
5 | "pluralName": "scheduler",
6 | "displayName": "scheduler",
7 | "description": ""
8 | },
9 | "pluginOptions": {
10 | "content-manager": {
11 | "visible": false
12 | },
13 | "content-type-builder": {
14 | "visible": false
15 | }
16 | },
17 | "attributes": {
18 | "uid": {
19 | "required": true,
20 | "type": "string"
21 | },
22 | "entryId": {
23 | "required": true,
24 | "type": "biginteger"
25 | },
26 | "type": {
27 | "required": true,
28 | "type": "enumeration",
29 | "enum": ["publish", "archive"]
30 | },
31 | "datetime": {
32 | "type": "datetime"
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/server/controllers/config.ts:
--------------------------------------------------------------------------------
1 | import { Strapi } from '@strapi/strapi';
2 |
3 | export interface IConfig {
4 | initialPublishAtDate?: string;
5 | initialArchiveAtDate?: string;
6 | }
7 |
8 | export interface IConfigControllerReturn {
9 | data: IConfig | null;
10 | }
11 |
12 | export default ({ strapi }: { strapi: Strapi }) => ({
13 | getGlobalConfig(): IConfigControllerReturn {
14 | const config = strapi.config.get('plugin.scheduler');
15 |
16 | return {
17 | data: config ?? null
18 | };
19 | },
20 | getContentTypeConfig(ctx: any): IConfigControllerReturn {
21 | const uid = ctx.params?.uid;
22 |
23 | if (!uid) {
24 | throw new Error();
25 | }
26 |
27 | const contentTypeConfigs = strapi.plugin('scheduler').config('contentTypes');
28 |
29 | const contentTypeConfig = contentTypeConfigs?.[uid];
30 |
31 | return {
32 | data: contentTypeConfig ?? null
33 | };
34 | }
35 | });
36 |
--------------------------------------------------------------------------------
/.github/workflows/jira.yml:
--------------------------------------------------------------------------------
1 | name: JIRA Issue Code Check
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - develop
7 | jobs:
8 | check-jira-issue-code:
9 | if: |
10 | !startsWith(github.head_ref, 'dependabot/') &&
11 | github.base_ref != 'release' &&
12 | github.base_ref != 'master' &&
13 | github.base_ref != 'main'
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: JIRA login
17 | uses: atlassian/gajira-login@master
18 | env:
19 | JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
20 | JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}
21 | JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
22 | - name: Find JIRA issue code in branch name
23 | uses: atlassian/gajira-find-issue-key@master
24 | with:
25 | string: ${{ github.head_ref }}, ${{ github.event.pull_request.title }}, ${{ github.event.pull_request.body }}
26 |
--------------------------------------------------------------------------------
/server/controllers/scheduler.ts:
--------------------------------------------------------------------------------
1 | import { Strapi } from '@strapi/strapi';
2 |
3 | export interface IScheduler {
4 | uid: string;
5 | entryId: number;
6 | publishAt: string;
7 | archiveAt: string;
8 | }
9 |
10 | export interface ISchedulerControllerReturn {
11 | data: IScheduler | null;
12 | }
13 |
14 | export default ({ strapi }: { strapi: Strapi }) => ({
15 | async getByUidAndEntryId(ctx: any): Promise {
16 | const { uid, entryId } = ctx.params;
17 |
18 | if (!uid) {
19 | throw new Error();
20 | }
21 |
22 | if (!entryId) {
23 | throw new Error();
24 | }
25 |
26 | const result = await strapi.plugin('scheduler').service('scheduler').getByUidAndEntryId(uid, entryId);
27 |
28 | const existingPublish = result.find((entry: any) => entry.type === 'publish');
29 | const existingArchive = result.find((entry: any) => entry.type === 'archive');
30 |
31 | const data = {
32 | uid,
33 | entryId,
34 | publishAt: existingPublish?.datetime,
35 | archiveAt: existingArchive?.datetime
36 | };
37 |
38 | return {
39 | data
40 | };
41 | }
42 | });
43 |
--------------------------------------------------------------------------------
/admin/src/index.tsx:
--------------------------------------------------------------------------------
1 | import { prefixPluginTranslations } from '@strapi/helper-plugin';
2 | import pluginPkg from '../../package.json';
3 | import pluginId from './pluginId';
4 | import Initializer from './components/Initializer';
5 | import Scheduler from './components/Scheduler';
6 |
7 | const name = pluginPkg.strapi.name;
8 |
9 | export default {
10 | register(app: any) {
11 | const plugin = {
12 | id: pluginId,
13 | initializer: Initializer,
14 | isReady: false,
15 | name
16 | };
17 |
18 | app.registerPlugin(plugin);
19 | },
20 | bootstrap(app: any) {
21 | app.injectContentManagerComponent('editView', 'right-links', {
22 | name: 'scheduler',
23 | Component: Scheduler
24 | });
25 | },
26 | async registerTrads(app: any) {
27 | const { locales } = app;
28 |
29 | const importedTrads = await Promise.all(
30 | locales.map((locale: string) => {
31 | return import(`./translations/${locale}.json`)
32 | .then(({ default: data }) => {
33 | return {
34 | data: prefixPluginTranslations(data, pluginId),
35 | locale
36 | };
37 | })
38 | .catch(() => {
39 | return {
40 | data: {},
41 | locale
42 | };
43 | });
44 | })
45 | );
46 |
47 | return Promise.resolve(importedTrads);
48 | }
49 | };
50 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Strapi plugin scheduler
2 |
3 | Strapi Plugin to schedule publish and depublish actions for any collection type.
4 |
5 | Schedule when you want to publish your content
6 |
7 | 
8 |
9 | Choose the date and time of publication and choose when to archive your page
10 |
11 | 
12 |
13 | That's it!
14 |
15 | 
16 |
17 | # Installation
18 |
19 | 1. To install the plugin run `npm i @webbio/strapi-plugin-scheduler` or `yarn add @webbio/strapi-plugin-scheduler`.
20 |
21 | 2. After the plugin is installed, add the plugin to the plugins.js file in your config folder.
22 |
23 | ```
24 | scheduler: {
25 | enabled: true,
26 | config: {
27 | contentTypes: {
28 | 'api::page.page': {}
29 | }
30 | }
31 | },
32 | ```
33 |
34 | # Set initial dates
35 |
36 | Set the initial archive date and initial publish date in the plugin settings. These dates will automatically be set when creating a new page.
37 |
38 | ```
39 | scheduler: {
40 | enabled: true,
41 | resolve: './src/plugins/strapi-plugin-scheduler',
42 | config: {
43 | 'api::page.page': {
44 | initialPublishAtDate: setMonth(
45 | new Date(),
46 | new Date().getMonth() + 1
47 | ).toDateString(),
48 | initialArchiveAtDate: setMonth(
49 | new Date(),
50 | new Date().getMonth() + 3
51 | ).toDateString(),
52 | },
53 | },
54 | },
55 | ```
56 |
57 | Now when you run your application, the addon will be added to the sidebar. You can choose a date and time to publish or archive your article.
58 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@webbio/strapi-plugin-scheduler",
3 | "private": false,
4 | "version": "1.1.0",
5 | "description": "A plugin to publish or depublish content types in the future.",
6 | "scripts": {
7 | "develop": "tsc -p tsconfig.server.json -w",
8 | "build": "tsc -p tsconfig.server.json",
9 | "prepublish": "tsc -p tsconfig.server.json",
10 | "format": "prettier --write ."
11 | },
12 | "main": "dist/server/index.js",
13 | "types": "dist/types.js",
14 | "strapi": {
15 | "name": "scheduler",
16 | "description": "A plugin to publish or depublish content types in the future.",
17 | "kind": "plugin"
18 | },
19 | "dependencies": {
20 | "@strapi/design-system": "^1.8.2",
21 | "@strapi/helper-plugin": "^4.12.5",
22 | "@strapi/icons": "^1.8.2",
23 | "@strapi/strapi": "4.12.5",
24 | "@strapi/typescript-utils": "^4.12.5",
25 | "@strapi/utils": "^4.12.5"
26 | },
27 | "devDependencies": {
28 | "@strapi/typescript-utils": "^4.12.5",
29 | "@types/node": "^22.15.18",
30 | "@types/react": "^17.0.53",
31 | "@types/react-dom": "^18.0.28",
32 | "@types/react-router-dom": "^5.3.3",
33 | "@types/styled-components": "^5.1.26",
34 | "react": "^18.2.0",
35 | "react-dom": "^18.2.0",
36 | "react-router-dom": "^5.3.4",
37 | "styled-components": "^5.3.6",
38 | "typescript": "5.1.6"
39 | },
40 | "peerDependencies": {
41 | "@strapi/strapi": "^4.12.5",
42 | "react": "^17.0.0 || ^18.0.0",
43 | "react-dom": "^17.0.0 || ^18.0.0",
44 | "react-router-dom": "^5.3.4",
45 | "styled-components": "^5.3.6"
46 | },
47 | "author": {
48 | "name": "Webbio "
49 | },
50 | "maintainers": [
51 | {
52 | "name": "Webbio"
53 | }
54 | ],
55 | "engines": {
56 | "node": ">=14.19.1 <=18.x.x",
57 | "npm": ">=6.0.0"
58 | },
59 | "license": "MIT",
60 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
61 | }
62 |
--------------------------------------------------------------------------------
/server/bootstrap.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import { Strapi } from '@strapi/strapi';
3 |
4 | const submitSchedulerData = async (event) => {
5 | const { model, state, result } = event;
6 |
7 | const uid = model.uid;
8 | const entryId = result.id;
9 |
10 | const schedulerData = {
11 | uid,
12 | entryId,
13 | publishAt: state.publishAt,
14 | archiveAt: state.archiveAt
15 | };
16 |
17 | try {
18 | const schedulerService = strapi.service('plugin::scheduler.scheduler');
19 | await schedulerService.schedule(schedulerData);
20 | } catch (error) {
21 | console.error(error);
22 | }
23 | };
24 |
25 | export default ({ strapi }: { strapi: Strapi }) => {
26 | // Lifecycle hooks
27 | const userCreatedContentTypesWithDraftAndPublish = Object.values(strapi.contentTypes)
28 | .filter((model) => model.uid.startsWith('api::') && model.options?.draftAndPublish === true)
29 | .map((model) => model.uid);
30 |
31 | strapi.db.lifecycles.subscribe({
32 | models: userCreatedContentTypesWithDraftAndPublish,
33 | async beforeCreate(event) {
34 | event.state.publishAt = event.params.data?.publishAt;
35 | event.state.archiveAt = event.params.data?.archiveAt;
36 | },
37 | async beforeUpdate(event) {
38 | event.state.publishAt = event.params.data?.publishAt;
39 | event.state.archiveAt = event.params.data?.archiveAt;
40 | },
41 | async afterCreate(event) {
42 | if (event.state.publishAt !== undefined || event.state.archiveAt !== undefined) {
43 | await submitSchedulerData(event);
44 | }
45 | },
46 | async afterUpdate(event) {
47 | if (event.state.publishAt !== undefined || event.state.archiveAt !== undefined) {
48 | await submitSchedulerData(event);
49 | }
50 | }
51 | });
52 |
53 | // Cron
54 | strapi.cron.add({
55 | scheduler: {
56 | task: async ({ strapi }) => {
57 | await strapi.service('plugin::scheduler.scheduler').runCronTask();
58 | },
59 | options: {
60 | rule: '* * * * *'
61 | }
62 | }
63 | });
64 | };
65 |
--------------------------------------------------------------------------------
/server/services/scheduler.ts:
--------------------------------------------------------------------------------
1 | import { Strapi } from "@strapi/strapi";
2 |
3 | export default ({ strapi }: { strapi: Strapi }) => ({
4 | async create(data) {
5 | return await strapi.entityService.create(
6 | "plugin::scheduler.scheduler",
7 | {
8 | data,
9 | },
10 | );
11 | },
12 | async update(id, data) {
13 | return await strapi.entityService.update(
14 | "plugin::scheduler.scheduler",
15 | id,
16 | {
17 | data,
18 | },
19 | );
20 | },
21 | async findOne(id) {
22 | return strapi.entityService.findOne("plugin::scheduler.scheduler", id);
23 | },
24 | async delete(id) {
25 | return await strapi.entityService.delete(
26 | "plugin::scheduler.scheduler",
27 | id,
28 | );
29 | },
30 | async schedule(data) {
31 | const existingEntries = await this.getByUidAndEntryId(
32 | data.uid,
33 | data.entryId,
34 | );
35 | const existingPublishEntry = existingEntries.find(
36 | (entry) => entry.type === "publish",
37 | );
38 | const existingArchiveEntry = existingEntries.find(
39 | (entry) => entry.type === "archive",
40 | );
41 |
42 | if (existingPublishEntry) {
43 | await this.update(existingPublishEntry.id, {
44 | uid: data.uid,
45 | entryId: data.entryId,
46 | type: "publish",
47 | datetime: data.publishAt,
48 | });
49 | } else {
50 | await this.create({
51 | uid: data.uid,
52 | entryId: data.entryId,
53 | type: "publish",
54 | datetime: data.publishAt,
55 | });
56 | }
57 |
58 | if (existingPublishEntry && !data?.publishAt) {
59 | return await this.delete(existingPublishEntry.id);
60 | }
61 |
62 | if (existingArchiveEntry && !data?.archiveAt) {
63 | return await this.delete(existingArchiveEntry.id);
64 | }
65 |
66 | if (existingArchiveEntry) {
67 | await this.update(existingArchiveEntry.id, {
68 | uid: data.uid,
69 | entryId: data.entryId,
70 | type: "archive",
71 | datetime: data.archiveAt,
72 | });
73 | } else {
74 | await this.create({
75 | uid: data.uid,
76 | entryId: data.entryId,
77 | type: "archive",
78 | datetime: data.archiveAt,
79 | });
80 | }
81 | },
82 | async getByUidAndEntryId(uid, entryId) {
83 | const result = await strapi
84 | .query("plugin::scheduler.scheduler")
85 | .findMany({ where: { uid, entryId } });
86 |
87 | return result;
88 | },
89 | async findItemsPastCurrentDate() {
90 | const currentDate = new Date();
91 |
92 | const result = await strapi
93 | .query("plugin::scheduler.scheduler")
94 | .findMany({
95 | where: {
96 | datetime: { $lte: currentDate },
97 | },
98 | });
99 |
100 | return result;
101 | },
102 | async publishEntry(schedulerEntry) {
103 | return strapi.db.query(schedulerEntry.uid).update({
104 | where: {
105 | id: schedulerEntry.entryId,
106 | },
107 | data: {
108 | publishedAt: new Date(),
109 | },
110 | });
111 | },
112 | async archiveEntry(schedulerEntry) {
113 | return strapi.db.query(schedulerEntry.uid).update({
114 | where: {
115 | id: schedulerEntry.entryId,
116 | },
117 | data: {
118 | publishedAt: null,
119 | },
120 | });
121 | },
122 | async runCronTask() {
123 | const entries = await this.findItemsPastCurrentDate();
124 |
125 | for await (const entry of entries) {
126 | try {
127 | if (entry.type === "publish") {
128 | await this.publishEntry(entry);
129 | }
130 |
131 | if (entry.type === "archive") {
132 | await this.archiveEntry(entry);
133 | }
134 | } catch (error) {
135 | console.error("Error publishing or archiving entry:", error);
136 | } finally {
137 | await this.delete(entry.id);
138 | }
139 | }
140 | },
141 | });
142 |
--------------------------------------------------------------------------------
/admin/src/components/Scheduler/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, memo, useMemo } from 'react';
2 | import { Box, Stack, Divider, Typography } from '@strapi/design-system';
3 | import { useCMEditViewDataManager, useFetchClient } from '@strapi/helper-plugin';
4 |
5 | import { IScheduler, ISchedulerControllerReturn } from '../../../../server/controllers/scheduler';
6 | import { IConfig, IConfigControllerReturn } from '../../../../server/controllers/config';
7 | import DateTimePickerWrapper from '../date-time-picker-wrapper';
8 |
9 | const DATETIME_PICKER_STEP_SIZE = 5;
10 |
11 | const Scheduler = () => {
12 | const { onChange, layout, initialData, modifiedData, isCreatingEntry } = useCMEditViewDataManager();
13 | const { get } = useFetchClient();
14 |
15 | const [isLoadingConfig, setIsLoadingConfig] = useState(true);
16 | const [isLoadingScheduler, setIsLoadingScheduler] = useState(true);
17 |
18 | const [config, setConfig] = useState();
19 | const [scheduler, setScheduler] = useState(undefined);
20 |
21 | const publishAt = useMemo(() => {
22 | const newDate = modifiedData?.publishAt;
23 | return newDate ? new Date(newDate) : null;
24 | }, [initialData.publishAt, modifiedData.publishAt]);
25 |
26 | const archiveAt = useMemo(() => {
27 | const newDate = modifiedData?.archiveAt
28 | return newDate ? new Date(newDate) : null;
29 | }, [initialData.archiveAt, modifiedData.archiveAt]);
30 |
31 | const updateFormValue = (name: string, value: Date | null, initialValue = false) => {
32 | const isoDate = value ? value.toISOString() : null;
33 |
34 | onChange(
35 | {
36 | target: { name, value: isoDate, type: 'string' }
37 | },
38 | initialValue
39 | );
40 | };
41 |
42 | const handleChangePublishAt = (value: Date | null) => {
43 | updateFormValue('publishAt', value, false);
44 | };
45 |
46 | const onClearPublishAt = () => {
47 | updateFormValue('publishAt', null, false);
48 | };
49 |
50 | const handleChangeArchiveAt = (value: Date | null) => {
51 | updateFormValue('archiveAt', value, false);
52 | };
53 |
54 | const onClearArchiveAt = () => {
55 | updateFormValue('archiveAt', null, false);
56 | };
57 |
58 | const fetchConfig = async (uid: string) => {
59 | try {
60 | const configFetchResult: { data?: IConfigControllerReturn } = await get(`/scheduler/config/${uid}`);
61 | const { data: config } = configFetchResult?.data || {};
62 | setConfig(config);
63 | } catch (error) {
64 | } finally {
65 | setIsLoadingConfig(false);
66 | }
67 | };
68 |
69 | const fetchScheduler = async (uid: string, entryId: string) => {
70 | try {
71 | const schedulerFetchResult: { data: ISchedulerControllerReturn } = await get(
72 | `/scheduler/scheduler/${uid}/${entryId}`
73 | );
74 | const { data: scheduler } = schedulerFetchResult?.data;
75 | setScheduler(scheduler);
76 | } catch (error) {
77 | console.error(error);
78 | } finally {
79 | setIsLoadingScheduler(false);
80 | }
81 | };
82 |
83 | useEffect(() => {
84 | const uid = layout.uid;
85 | const entryId = initialData?.id;
86 | fetchConfig(uid);
87 |
88 | if (entryId) {
89 | fetchScheduler(uid, entryId);
90 | }
91 |
92 | if (isCreatingEntry) {
93 | setIsLoadingScheduler(false);
94 | }
95 | }, []);
96 |
97 | useEffect(() => {
98 | if (isLoadingConfig === false && isLoadingScheduler === false) {
99 | const initialPublishAt = getInitialPublishAt(scheduler, config);
100 | const initialArchiveAt = getInitialArchiveAt(scheduler, config);
101 |
102 | if (initialPublishAt !== undefined) {
103 | const value = initialPublishAt === null ? null : new Date(initialPublishAt);
104 | updateFormValue('publishAt', value, true);
105 | }
106 |
107 | if (initialArchiveAt !== undefined) {
108 | const value = initialArchiveAt === null ? null : new Date(initialArchiveAt);
109 | updateFormValue('archiveAt', value, true);
110 | }
111 | }
112 | }, [isLoadingConfig, isLoadingScheduler]);
113 |
114 | if (!config) {
115 | return null;
116 | }
117 |
118 | return (
119 |
131 |
132 | Scheduler
133 |
134 |
135 |
136 |
137 |
138 |
149 |
160 |
161 |
162 | );
163 | };
164 |
165 | export default Scheduler;
166 |
167 | const getInitialPublishAt = (scheduler?: IScheduler | null, config?: IConfig | null) => {
168 | if (scheduler?.publishAt === null || scheduler?.publishAt !== undefined) {
169 | return scheduler.publishAt;
170 | }
171 |
172 | if (config?.initialPublishAtDate === null || config?.initialPublishAtDate !== undefined) {
173 | return config.initialPublishAtDate;
174 | }
175 |
176 | return undefined;
177 | };
178 |
179 | const getInitialArchiveAt = (scheduler?: IScheduler | null, config?: IConfig | null) => {
180 | if (scheduler?.archiveAt === null || scheduler?.archiveAt !== undefined) {
181 | return scheduler.archiveAt;
182 | }
183 |
184 | if (config?.initialArchiveAtDate === null || config?.initialArchiveAtDate !== undefined) {
185 | return config.initialArchiveAtDate;
186 | }
187 |
188 | return undefined;
189 | };
190 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/macos,visualstudiocode,node,jetbrains
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,visualstudiocode,node,jetbrains
3 |
4 | ### JetBrains ###
5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
7 |
8 | # User-specific stuff
9 | .idea/**/workspace.xml
10 | .idea/**/tasks.xml
11 | .idea/**/usage.statistics.xml
12 | .idea/**/dictionaries
13 | .idea/**/shelf
14 |
15 | # AWS User-specific
16 | .idea/**/aws.xml
17 |
18 | # Generated files
19 | .idea/**/contentModel.xml
20 |
21 | # Sensitive or high-churn files
22 | .idea/**/dataSources/
23 | .idea/**/dataSources.ids
24 | .idea/**/dataSources.local.xml
25 | .idea/**/sqlDataSources.xml
26 | .idea/**/dynamic.xml
27 | .idea/**/uiDesigner.xml
28 | .idea/**/dbnavigator.xml
29 |
30 | # Gradle
31 | .idea/**/gradle.xml
32 | .idea/**/libraries
33 |
34 | # Gradle and Maven with auto-import
35 | # When using Gradle or Maven with auto-import, you should exclude module files,
36 | # since they will be recreated, and may cause churn. Uncomment if using
37 | # auto-import.
38 | # .idea/artifacts
39 | # .idea/compiler.xml
40 | # .idea/jarRepositories.xml
41 | # .idea/modules.xml
42 | # .idea/*.iml
43 | # .idea/modules
44 | # *.iml
45 | # *.ipr
46 |
47 | # CMake
48 | cmake-build-*/
49 |
50 | # Mongo Explorer plugin
51 | .idea/**/mongoSettings.xml
52 |
53 | # File-based project format
54 | *.iws
55 |
56 | # IntelliJ
57 | out/
58 |
59 | # mpeltonen/sbt-idea plugin
60 | .idea_modules/
61 |
62 | # JIRA plugin
63 | atlassian-ide-plugin.xml
64 |
65 | # Cursive Clojure plugin
66 | .idea/replstate.xml
67 |
68 | # SonarLint plugin
69 | .idea/sonarlint/
70 |
71 | # Crashlytics plugin (for Android Studio and IntelliJ)
72 | com_crashlytics_export_strings.xml
73 | crashlytics.properties
74 | crashlytics-build.properties
75 | fabric.properties
76 |
77 | # Editor-based Rest Client
78 | .idea/httpRequests
79 |
80 | # Android studio 3.1+ serialized cache file
81 | .idea/caches/build_file_checksums.ser
82 |
83 | ### JetBrains Patch ###
84 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
85 |
86 | # *.iml
87 | # modules.xml
88 | # .idea/misc.xml
89 | # *.ipr
90 |
91 | # Sonarlint plugin
92 | # https://plugins.jetbrains.com/plugin/7973-sonarlint
93 | .idea/**/sonarlint/
94 |
95 | # SonarQube Plugin
96 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
97 | .idea/**/sonarIssues.xml
98 |
99 | # Markdown Navigator plugin
100 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
101 | .idea/**/markdown-navigator.xml
102 | .idea/**/markdown-navigator-enh.xml
103 | .idea/**/markdown-navigator/
104 |
105 | # Cache file creation bug
106 | # See https://youtrack.jetbrains.com/issue/JBR-2257
107 | .idea/$CACHE_FILE$
108 |
109 | # CodeStream plugin
110 | # https://plugins.jetbrains.com/plugin/12206-codestream
111 | .idea/codestream.xml
112 |
113 | ### macOS ###
114 | # General
115 | .DS_Store
116 | .AppleDouble
117 | .LSOverride
118 |
119 | # Icon must end with two \r
120 | Icon
121 |
122 |
123 | # Thumbnails
124 | ._*
125 |
126 | # Files that might appear in the root of a volume
127 | .DocumentRevisions-V100
128 | .fseventsd
129 | .Spotlight-V100
130 | .TemporaryItems
131 | .Trashes
132 | .VolumeIcon.icns
133 | .com.apple.timemachine.donotpresent
134 |
135 | # Directories potentially created on remote AFP share
136 | .AppleDB
137 | .AppleDesktop
138 | Network Trash Folder
139 | Temporary Items
140 | .apdisk
141 |
142 | ### Node ###
143 | # Logs
144 | logs
145 | *.log
146 | npm-debug.log*
147 | yarn-debug.log*
148 | yarn-error.log*
149 | lerna-debug.log*
150 | .pnpm-debug.log*
151 |
152 | # Diagnostic reports (https://nodejs.org/api/report.html)
153 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
154 |
155 | # Runtime data
156 | pids
157 | *.pid
158 | *.seed
159 | *.pid.lock
160 |
161 | # Directory for instrumented libs generated by jscoverage/JSCover
162 | lib-cov
163 |
164 | # Coverage directory used by tools like istanbul
165 | coverage
166 | *.lcov
167 |
168 | # nyc test coverage
169 | .nyc_output
170 |
171 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
172 | .grunt
173 |
174 | # Bower dependency directory (https://bower.io/)
175 | bower_components
176 |
177 | # node-waf configuration
178 | .lock-wscript
179 |
180 | # Compiled binary addons (https://nodejs.org/api/addons.html)
181 | build/Release
182 |
183 | # Dependency directories
184 | node_modules/
185 | jspm_packages/
186 |
187 | # Snowpack dependency directory (https://snowpack.dev/)
188 | web_modules/
189 |
190 | # TypeScript cache
191 | *.tsbuildinfo
192 |
193 | # Optional npm cache directory
194 | .npm
195 |
196 | # Optional eslint cache
197 | .eslintcache
198 |
199 | # Optional stylelint cache
200 | .stylelintcache
201 |
202 | # Microbundle cache
203 | .rpt2_cache/
204 | .rts2_cache_cjs/
205 | .rts2_cache_es/
206 | .rts2_cache_umd/
207 |
208 | # Optional REPL history
209 | .node_repl_history
210 |
211 | # Output of 'npm pack'
212 | *.tgz
213 |
214 | # Yarn Integrity file
215 | .yarn-integrity
216 |
217 | # dotenv environment variable files
218 | .env
219 | .env.development.local
220 | .env.test.local
221 | .env.production.local
222 | .env.local
223 |
224 | # parcel-bundler cache (https://parceljs.org/)
225 | .cache
226 | .parcel-cache
227 |
228 | # Next.js build output
229 | .next
230 | out
231 |
232 | # Nuxt.js build / generate output
233 | .nuxt
234 | dist
235 |
236 | # Gatsby files
237 | .cache/
238 | # Comment in the public line in if your project uses Gatsby and not Next.js
239 | # https://nextjs.org/blog/next-9-1#public-directory-support
240 | # public
241 |
242 | # vuepress build output
243 | .vuepress/dist
244 |
245 | # vuepress v2.x temp and cache directory
246 | .temp
247 |
248 | # Docusaurus cache and generated files
249 | .docusaurus
250 |
251 | # Serverless directories
252 | .serverless/
253 |
254 | # FuseBox cache
255 | .fusebox/
256 |
257 | # DynamoDB Local files
258 | .dynamodb/
259 |
260 | # TernJS port file
261 | .tern-port
262 |
263 | # Stores VSCode versions used for testing VSCode extensions
264 | .vscode-test
265 |
266 | # yarn v2
267 | .yarn/cache
268 | .yarn/unplugged
269 | .yarn/build-state.yml
270 | .yarn/install-state.gz
271 | .pnp.*
272 |
273 | ### Node Patch ###
274 | # Serverless Webpack directories
275 | .webpack/
276 |
277 | # Optional stylelint cache
278 |
279 | # SvelteKit build / generate output
280 | .svelte-kit
281 |
282 | ### VisualStudioCode ###
283 | .vscode/*
284 | !.vscode/settings.json
285 | !.vscode/tasks.json
286 | !.vscode/launch.json
287 | !.vscode/extensions.json
288 | !.vscode/*.code-snippets
289 |
290 | # Local History for Visual Studio Code
291 | .history/
292 |
293 | # Built Visual Studio Code Extensions
294 | *.vsix
295 |
296 | ### VisualStudioCode Patch ###
297 | # Ignore all local history of files
298 | .history
299 | .ionide
300 |
301 | # Support for Project snippet scope
302 |
303 | # End of https://www.toptal.com/developers/gitignore/api/macos,visualstudiocode,node,jetbrains
--------------------------------------------------------------------------------