├── .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 | ![Alt Text](https://media.giphy.com/media/ziKkXCGDftGmvLs1lJ/giphy.gif) 8 | 9 | Choose the date and time of publication and choose when to archive your page 10 | 11 | ![Alt Text](https://media.giphy.com/media/CpFm7AC67Mkul8VTFI/giphy.gif) 12 | 13 | That's it! 14 | 15 | ![Alt Text](https://media.giphy.com/media/gvMbDw1bOhals0hI3g/giphy.gif) 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 --------------------------------------------------------------------------------