diff --git a/packages/core/utils/src/parse-filter.ts b/packages/core/utils/src/parse-filter.ts index 21a001e29b..014f64c3c3 100644 --- a/packages/core/utils/src/parse-filter.ts +++ b/packages/core/utils/src/parse-filter.ts @@ -260,9 +260,15 @@ export function utc2unit(options: Utc2unitOptions) { const r = fn[unit]?.(); return timezone ? r + timezone : r; } - +type ToUnitParams = { + now?: any; + timezone?: string | number; + field?: { + timezone?: string | number; + }; +}; export const toUnit = (unit, offset?: number) => { - return ({ now, timezone, field }) => { + return ({ now, timezone, field }: ToUnitParams) => { if (field?.timezone) { timezone = field?.timezone; } @@ -271,7 +277,7 @@ export const toUnit = (unit, offset?: number) => { }; const toDays = (offset: number) => { - return ({ now, timezone, field }) => { + return ({ now, timezone, field }: ToUnitParams) => { if (field?.timezone) { timezone = field?.timezone; } diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/variable.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/variable.tsx index b473ab0f45..03bd2019de 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/variable.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/variable.tsx @@ -8,6 +8,7 @@ */ import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; import { uniqBy } from 'lodash'; import { Variable, parseCollectionName, useApp, useCompile, usePlugin } from '@nocobase/client'; @@ -53,6 +54,114 @@ export type UseVariableOptions = { export const defaultFieldNames = { label: 'label', value: 'value', children: 'children' } as const; +const getDateOptions = (t) => [ + { + key: 'yesterday', + value: 'yesterday', + label: t('Yesterday'), + }, + { + key: 'today', + value: 'today', + label: t('Today'), + }, + { + key: 'tomorrow', + value: 'tomorrow', + label: t('Tomorrow'), + }, + { + key: 'lastWeek', + value: 'lastWeek', + label: t('Last week'), + }, + { + key: 'thisWeek', + value: 'thisWeek', + label: t('This week'), + }, + { + key: 'nextWeek', + value: 'nextWeek', + label: t('Next week'), + }, + { + key: 'lastMonth', + value: 'lastMonth', + label: t('Last month'), + }, + { + key: 'thisMonth', + value: 'thisMonth', + label: t('This month'), + }, + { + key: 'nextMonth', + value: 'nextMonth', + label: t('Next month'), + }, + { + key: 'lastQuarter', + value: 'lastQuarter', + label: t('Last quarter'), + }, + { + key: 'thisQuarter', + value: 'thisQuarter', + label: t('This quarter'), + }, + { + key: 'nextQuarter', + value: 'nextQuarter', + label: t('Next quarter'), + }, + { + key: 'lastYear', + value: 'lastYear', + label: t('Last year'), + }, + { + key: 'thisYear', + value: 'thisYear', + label: t('This year'), + }, + { + key: 'nextYear', + value: 'nextYear', + label: t('Next year'), + }, + { + key: 'last7Days', + value: 'last7Days', + label: t('Last 7 days'), + }, + { + key: 'next7Days', + value: 'next7Days', + label: t('Next 7 days'), + }, + { + key: 'last30Days', + value: 'last30Days', + label: t('Last 30 days'), + }, + { + key: 'next30Days', + value: 'next30Days', + label: t('Next 30 days'), + }, + { + key: 'last90Days', + value: 'last90Days', + label: t('Last 90 days'), + }, + { + key: 'next90Days', + value: 'next90Days', + label: t('Next 90 days'), + }, +]; + export const nodesOptions = { label: `{{t("Node result", { ns: "${NAMESPACE}" })}}`, value: '$jobsMapByNodeKey', @@ -113,6 +222,7 @@ export const systemOptions = { label: `{{t("System variables", { ns: "${NAMESPACE}" })}}`, value: '$system', useOptions({ types, fieldNames = defaultFieldNames }: UseVariableOptions) { + const { t } = useTranslation(); return [ ...(!types || types.includes('date') ? [ @@ -121,6 +231,12 @@ export const systemOptions = { [fieldNames.label]: lang('System time'), [fieldNames.value]: 'now', }, + { + key: 'dateRange', + [fieldNames.label]: lang('Date range'), + [fieldNames.value]: 'dateRange', + children: getDateOptions(t), + }, ] : []), ]; diff --git a/packages/plugins/@nocobase/plugin-workflow/src/locale/ko_KR.json b/packages/plugins/@nocobase/plugin-workflow/src/locale/ko_KR.json index c14334c9b9..cb78d8879a 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/locale/ko_KR.json +++ b/packages/plugins/@nocobase/plugin-workflow/src/locale/ko_KR.json @@ -87,6 +87,7 @@ "System variables": "시스템 변수", "System time": "시스템 시간", "Date variables": "날짜 변수", + "Date range": "날짜 범위", "Executed at": "실행된 시각", "Queueing": "대기 중", "On going": "진행 중", diff --git a/packages/plugins/@nocobase/plugin-workflow/src/locale/zh-CN.json b/packages/plugins/@nocobase/plugin-workflow/src/locale/zh-CN.json index bcba78e0ac..623fd49d69 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/locale/zh-CN.json +++ b/packages/plugins/@nocobase/plugin-workflow/src/locale/zh-CN.json @@ -104,6 +104,7 @@ "System variables": "系统变量", "System time": "系统时间", "Date variables": "日期变量", + "Date range": "日期范围", "Executed at": "执行于", "Queueing": "队列中", diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/Processor.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/Processor.ts index b7eb11a8b5..6bb4df094a 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/Processor.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/Processor.ts @@ -11,6 +11,7 @@ import { Model, Transaction, Transactionable } from '@nocobase/database'; import { appendArrayColumn } from '@nocobase/evaluators'; import { Logger } from '@nocobase/logger'; import { parse } from '@nocobase/utils'; +import set from 'lodash/set'; import type Plugin from './Plugin'; import { EXECUTION_STATUS, JOB_STATUS } from './constants'; import { Runner } from './instructions'; @@ -381,7 +382,7 @@ export default class Processor { node, }; for (const [name, fn] of this.options.plugin.functions.getEntities()) { - systemFns[name] = fn.bind(scope); + set(systemFns, name, fn.bind(scope)); } const $scopes = {}; diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/function.test.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/function.test.ts new file mode 100644 index 0000000000..946b4d1bc4 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/function.test.ts @@ -0,0 +1,628 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import dayjs from 'dayjs'; +import { Application } from '@nocobase/server'; +import Database from '@nocobase/database'; +import { parse } from '@nocobase/utils'; +import { dateRangeFns } from '@nocobase/plugin-workflow'; +import { getApp, sleep } from '@nocobase/plugin-workflow-test'; + +const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; +let last2days; +let yesterday; +let startOfYesterday; +let endOfYesterday; + +let today; +let startOfToday; +let endOfToday; + +let tomorrow; +let startOfTomorrow; +let endOfTomorrow; + +let theDayAfterTomorrow; + +let lastWeek; +let lastWeekFirstDay; +let lastWeekLastDay; +let thisWeekFirstDay; +let thisWeekLastDay; +let nextWeek; +let nextWeekFirstDay; +let nextWeekLastDay; + +let lastMonth; +let lastMonthFirstDay; +let lastMonthLastDay; +let thisMonthFirstDay; +let thisMonthLastDay; +let nextMonth; +let nextMonthFirstDay; +let nextMonthLastDay; + +let lastQuarter; +let lastQuarterFirstDay; +let lastQuarterLastDay; +let thisQuarterFirstDay; +let thisQuarterLastDay; +let nextQuarter; +let nextQuarterFirstDay; +let nextQuarterLastDay; + +let lastYear; +let lastYearFirstDay; +let lastYearLastDay; +let thisYearFirstDay; +let thisYearLastDay; +let nextYear; +let nextYearFirstDay; +let nextYearLastDay; + +describe('workflow > functions > system variable', () => { + let app: Application; + let db: Database; + let PostCollection; + let PostRepo; + let TagModel; + let CommentRepo; + let WorkflowModel; + let workflow; + + beforeEach(async () => { + app = await getApp(); + + db = app.db; + PostCollection = db.getCollection('posts'); + PostRepo = PostCollection.repository; + + last2days = dayjs().tz(timezone).subtract(2, 'day'); + + yesterday = dayjs().tz(timezone).subtract(1, 'day'); + startOfYesterday = yesterday.clone().startOf('day'); + endOfYesterday = yesterday.clone().endOf('day'); + + today = dayjs().tz(timezone); + startOfToday = today.clone().startOf('day'); + endOfToday = today.clone().endOf('day'); + + tomorrow = dayjs().tz(timezone).add(1, 'day'); + startOfTomorrow = tomorrow.clone().startOf('day'); + endOfTomorrow = tomorrow.clone().endOf('day'); + + theDayAfterTomorrow = dayjs().tz(timezone).add(2, 'day'); + + lastWeek = dayjs().tz(timezone).subtract(1, 'week'); + lastWeekFirstDay = lastWeek.clone().startOf('week'); + lastWeekLastDay = lastWeek.clone().endOf('week'); + thisWeekFirstDay = today.clone().tz(timezone).startOf('week'); + thisWeekLastDay = today.clone().endOf('week'); + nextWeek = dayjs().tz(timezone).add(1, 'week'); + nextWeekFirstDay = nextWeek.clone().startOf('week'); + nextWeekLastDay = nextWeek.clone().endOf('week'); + + lastMonth = dayjs().tz(timezone).subtract(1, 'month'); + lastMonthFirstDay = lastMonth.clone().startOf('month'); + lastMonthLastDay = lastMonth.clone().endOf('month'); + thisMonthFirstDay = today.clone().startOf('month'); + thisMonthLastDay = today.clone().endOf('month'); + nextMonth = dayjs().tz(timezone).add(1, 'month'); + nextMonthFirstDay = nextMonth.clone().startOf('month'); + nextMonthLastDay = nextMonth.clone().endOf('month'); + + lastQuarter = dayjs().tz(timezone).subtract(1, 'quarter'); + lastQuarterFirstDay = lastQuarter.clone().startOf('quarter'); + lastQuarterLastDay = lastQuarter.clone().endOf('quarter'); + thisQuarterFirstDay = today.clone().startOf('quarter'); + thisQuarterLastDay = today.clone().endOf('quarter'); + nextQuarter = dayjs().tz(timezone).add(1, 'quarter'); + nextQuarterFirstDay = nextQuarter.clone().startOf('quarter'); + nextQuarterLastDay = nextQuarter.clone().endOf('quarter'); + + lastYear = dayjs().tz(timezone).subtract(1, 'year'); + lastYearFirstDay = lastYear.clone().startOf('year'); + lastYearLastDay = lastYear.clone().endOf('year'); + thisYearFirstDay = today.clone().startOf('year'); + thisYearLastDay = today.clone().endOf('year'); + nextYear = dayjs().tz(timezone).add(1, 'year'); + nextYearFirstDay = nextYear.clone().startOf('year'); + nextYearLastDay = nextYear.clone().endOf('year'); + }); + + afterEach(() => app.destroy()); + + describe('system variable should', () => { + it('filter yesterday record', async () => { + const filter = parse({ + updatedAt: { + $dateOn: '{{$system.dateRange.yesterday}}', + }, + })({ + $system: { dateRange: dateRangeFns }, + }); + + const posts = await PostRepo.createMany({ + records: [ + { title: 'a year ago', updatedAt: lastYear.toDate() }, + { title: 'yesterday', updatedAt: yesterday.toDate() }, + { title: 'start of yesterday', updatedAt: startOfYesterday.toDate() }, + { + title: 'the time before start of yesterday 1s', + updatedAt: startOfYesterday.clone().subtract(1, 's').toDate(), + }, + { title: 'end of yesterday', updatedAt: endOfYesterday.toDate() }, + { title: 'the time after end of yesterday 1s', updatedAt: endOfYesterday.clone().add(1, 's').toDate() }, + ], + silent: true, + }); + + await sleep(500); + + const result = await PostRepo.find({ filter }); + expect(result.length).toBe(3); + expect(result[0].title).toBe(posts[1].title); + expect(result[1].title).toBe(posts[2].title); + expect(result[2].title).toBe(posts[4].title); + }); + it('filter today record', async () => { + const filter = parse({ + updatedAt: { + $dateOn: '{{$system.dateRange.today}}', + }, + })({ + $system: { dateRange: dateRangeFns }, + }); + + const posts = await PostRepo.createMany({ + records: [ + { title: 'a year ago', updatedAt: lastYear.toDate() }, + { title: 'today', updatedAt: today.toDate() }, + { title: 'start of today', updatedAt: startOfToday.toDate() }, + { title: 'the time before start of today 1s', updatedAt: startOfToday.clone().subtract(1, 's').toDate() }, + { title: 'end of today', updatedAt: endOfToday.toDate() }, + { title: 'the time after end of today 1s', updatedAt: endOfToday.clone().add(1, 's').toDate() }, + ], + silent: true, + }); + + await sleep(500); + + const result = await PostRepo.find({ filter }); + expect(result.length).toBe(3); + expect(result[0].title).toBe(posts[1].title); + expect(result[1].title).toBe(posts[2].title); + expect(result[2].title).toBe(posts[4].title); + }); + it('filter tomorrow record', async () => { + const filter = parse({ + updatedAt: { + $dateOn: '{{$system.dateRange.tomorrow}}', + }, + })({ + $system: { dateRange: dateRangeFns }, + }); + + const posts = await PostRepo.createMany({ + records: [ + { title: 'a year ago', updatedAt: lastYear.toDate() }, + { title: 'tomorrow', updatedAt: tomorrow.toDate() }, + { title: 'start of tomorrow', updatedAt: startOfTomorrow.toDate() }, + { + title: 'the time before start of tomorrow 1s', + updatedAt: startOfTomorrow.clone().subtract(1, 's').toDate(), + }, + { title: 'end of tomorrow', updatedAt: endOfTomorrow.toDate() }, + { title: 'the time after end of tomorrow 1s', updatedAt: endOfTomorrow.clone().add(1, 's').toDate() }, + ], + silent: true, + }); + + await sleep(500); + + const result = await PostRepo.find({ filter }); + expect(result.length).toBe(3); + expect(result[0].title).toBe(posts[1].title); + expect(result[1].title).toBe(posts[2].title); + expect(result[2].title).toBe(posts[4].title); + }); + it('filter last week record', async () => { + const filter = parse({ + updatedAt: { + $dateOn: '{{$system.dateRange.lastWeek}}', + }, + })({ + $system: { dateRange: dateRangeFns }, + }); + + const posts = await PostRepo.createMany({ + records: [ + { title: 'a year ago', updatedAt: lastYear.toDate() }, + { title: 'last week', updatedAt: lastWeek.toDate() }, + { title: 'start of last week', updatedAt: lastWeekFirstDay.toDate() }, + { + title: 'the time before start of last week 1s', + updatedAt: lastWeekFirstDay.clone().subtract(1, 's').toDate(), + }, + { title: 'end of last week', updatedAt: lastWeekLastDay.toDate() }, + { title: 'the time after end of last week 1s', updatedAt: lastWeekLastDay.clone().add(1, 's').toDate() }, + ], + silent: true, + }); + + await sleep(500); + + const result = await PostRepo.find({ filter }); + expect(result.length).toBe(3); + expect(result[0].title).toBe(posts[1].title); + expect(result[1].title).toBe(posts[2].title); + expect(result[2].title).toBe(posts[4].title); + }); + it('filter this week record', async () => { + const filter = parse({ + updatedAt: { + $dateOn: '{{$system.dateRange.thisWeek}}', + }, + })({ + $system: { dateRange: dateRangeFns }, + }); + + const posts = await PostRepo.createMany({ + records: [ + { title: 'a year ago', updatedAt: lastYear.toDate() }, + { title: 'this week', updatedAt: today.toDate() }, + { title: 'start of this week', updatedAt: thisWeekFirstDay.toDate() }, + { + title: 'the time before start of this week 1s', + updatedAt: thisWeekFirstDay.clone().subtract(1, 's').toDate(), + }, + { title: 'end of this week', updatedAt: thisWeekLastDay.toDate() }, + { title: 'the time after end of this week 1s', updatedAt: thisWeekLastDay.clone().add(1, 's').toDate() }, + ], + silent: true, + }); + + await sleep(500); + + const result = await PostRepo.find({ filter }); + expect(result.length).toBe(3); + expect(result[0].title).toBe(posts[1].title); + expect(result[1].title).toBe(posts[2].title); + expect(result[2].title).toBe(posts[4].title); + }); + it('filter next week record', async () => { + const filter = parse({ + updatedAt: { + $dateOn: '{{$system.dateRange.nextWeek}}', + }, + })({ + $system: { dateRange: dateRangeFns }, + }); + + const posts = await PostRepo.createMany({ + records: [ + { title: 'a year ago', updatedAt: lastYear.toDate() }, + { title: 'next week', updatedAt: nextWeek.toDate() }, + { title: 'start of next week', updatedAt: nextWeekFirstDay.toDate() }, + { + title: 'the time before start of next week 1s', + updatedAt: nextWeekFirstDay.clone().subtract(1, 's').toDate(), + }, + { title: 'end of next week', updatedAt: nextWeekLastDay.toDate() }, + { title: 'the time after end of next week 1s', updatedAt: nextWeekLastDay.clone().add(1, 's').toDate() }, + ], + silent: true, + }); + + await sleep(500); + + const result = await PostRepo.find({ filter }); + expect(result.length).toBe(3); + expect(result[0].title).toBe(posts[1].title); + expect(result[1].title).toBe(posts[2].title); + expect(result[2].title).toBe(posts[4].title); + }); + it('filter last month record', async () => { + const filter = parse({ + updatedAt: { + $dateOn: '{{$system.dateRange.lastMonth}}', + }, + })({ + $system: { dateRange: dateRangeFns }, + }); + + const posts = await PostRepo.createMany({ + records: [ + { title: 'a year ago', updatedAt: lastYear.toDate() }, + { title: 'last month', updatedAt: lastMonth.toDate() }, + { title: 'start of last month', updatedAt: lastMonthFirstDay.toDate() }, + { + title: 'the time before start of last month 1s', + updatedAt: lastMonthFirstDay.clone().subtract(1, 's').toDate(), + }, + { title: 'end of last month', updatedAt: lastMonthLastDay.toDate() }, + { title: 'the time after end of last month 1s', updatedAt: lastMonthLastDay.clone().add(1, 's').toDate() }, + ], + silent: true, + }); + + await sleep(500); + + const result = await PostRepo.find({ filter }); + expect(result.length).toBe(3); + expect(result[0].title).toBe(posts[1].title); + expect(result[1].title).toBe(posts[2].title); + expect(result[2].title).toBe(posts[4].title); + }); + it('filter this month record', async () => { + const filter = parse({ + updatedAt: { + $dateOn: '{{$system.dateRange.thisMonth}}', + }, + })({ + $system: { dateRange: dateRangeFns }, + }); + + const posts = await PostRepo.createMany({ + records: [ + { title: 'a year ago', updatedAt: lastYear.toDate() }, + { title: 'this month', updatedAt: today.toDate() }, + { title: 'start of this month', updatedAt: thisMonthFirstDay.toDate() }, + { + title: 'the time before start of this month 1s', + updatedAt: thisMonthFirstDay.clone().subtract(1, 's').toDate(), + }, + { title: 'end of this month', updatedAt: thisMonthLastDay.toDate() }, + { title: 'the time after end of this month 1s', updatedAt: thisMonthLastDay.clone().add(1, 's').toDate() }, + ], + silent: true, + }); + + await sleep(500); + + const result = await PostRepo.find({ filter }); + expect(result.length).toBe(3); + expect(result[0].title).toBe(posts[1].title); + expect(result[1].title).toBe(posts[2].title); + expect(result[2].title).toBe(posts[4].title); + }); + it('filter next month record', async () => { + const filter = parse({ + updatedAt: { + $dateOn: '{{$system.dateRange.nextMonth}}', + }, + })({ + $system: { dateRange: dateRangeFns }, + }); + + const posts = await PostRepo.createMany({ + records: [ + { title: 'a year ago', updatedAt: lastYear.toDate() }, + { title: 'next month', updatedAt: nextMonth.toDate() }, + { title: 'start of next month', updatedAt: nextMonthFirstDay.toDate() }, + { + title: 'the time before start of next month 1s', + updatedAt: nextMonthFirstDay.clone().subtract(1, 's').toDate(), + }, + { title: 'end of next month', updatedAt: nextMonthLastDay.toDate() }, + { title: 'the time after end of next month 1s', updatedAt: nextMonthLastDay.clone().add(1, 's').toDate() }, + ], + silent: true, + }); + + await sleep(500); + + const result = await PostRepo.find({ filter }); + expect(result.length).toBe(3); + expect(result[0].title).toBe(posts[1].title); + expect(result[1].title).toBe(posts[2].title); + expect(result[2].title).toBe(posts[4].title); + }); + it('filter last quarter record', async () => { + const filter = parse({ + updatedAt: { + $dateOn: '{{$system.dateRange.lastQuarter}}', + }, + })({ + $system: { dateRange: dateRangeFns }, + }); + + const posts = await PostRepo.createMany({ + records: [ + { title: 'a year ago', updatedAt: lastYear.toDate() }, + { title: 'last quarter', updatedAt: lastQuarter.toDate() }, + { title: 'start of last quarter', updatedAt: lastQuarterFirstDay.toDate() }, + { + title: 'the time before start of last quarter 1s', + updatedAt: lastQuarterFirstDay.clone().subtract(1, 's').toDate(), + }, + { title: 'end of last quarter', updatedAt: lastQuarterLastDay.toDate() }, + { + title: 'the time after end of last quarter 1s', + updatedAt: lastQuarterLastDay.clone().add(1, 's').toDate(), + }, + ], + silent: true, + }); + + await sleep(500); + + const result = await PostRepo.find({ filter }); + expect(result.length).toBe(3); + expect(result[0].title).toBe(posts[1].title); + expect(result[1].title).toBe(posts[2].title); + expect(result[2].title).toBe(posts[4].title); + }); + it('filter this quarter record', async () => { + const filter = parse({ + updatedAt: { + $dateOn: '{{$system.dateRange.thisQuarter}}', + }, + })({ + $system: { dateRange: dateRangeFns }, + }); + + const posts = await PostRepo.createMany({ + records: [ + { title: 'a year ago', updatedAt: lastYear.toDate() }, + { title: 'this quarter', updatedAt: today.toDate() }, + { title: 'start of this quarter', updatedAt: thisQuarterFirstDay.toDate() }, + { + title: 'the time before start of this quarter 1s', + updatedAt: thisQuarterFirstDay.clone().subtract(1, 's').toDate(), + }, + { title: 'end of this quarter', updatedAt: thisQuarterLastDay.toDate() }, + { + title: 'the time after end of this quarter 1s', + updatedAt: thisQuarterLastDay.clone().add(1, 's').toDate(), + }, + ], + silent: true, + }); + + await sleep(500); + + const result = await PostRepo.find({ filter }); + expect(result.length).toBe(3); + expect(result[0].title).toBe(posts[1].title); + expect(result[1].title).toBe(posts[2].title); + expect(result[2].title).toBe(posts[4].title); + }); + it('filter next quarter record', async () => { + const filter = parse({ + updatedAt: { + $dateOn: '{{$system.dateRange.nextQuarter}}', + }, + })({ + $system: { dateRange: dateRangeFns }, + }); + + const posts = await PostRepo.createMany({ + records: [ + { title: 'a year ago', updatedAt: lastYear.toDate() }, + { title: 'next quarter', updatedAt: nextQuarter.toDate() }, + { title: 'start of next quarter', updatedAt: nextQuarterFirstDay.toDate() }, + { + title: 'the time before start of next quarter 1s', + updatedAt: nextQuarterFirstDay.clone().subtract(1, 's').toDate(), + }, + { title: 'end of next quarter', updatedAt: nextQuarterLastDay.toDate() }, + { + title: 'the time after end of next quarter 1s', + updatedAt: nextQuarterLastDay.clone().add(1, 's').toDate(), + }, + ], + silent: true, + }); + + await sleep(500); + + const result = await PostRepo.find({ filter }); + expect(result.length).toBe(3); + expect(result[0].title).toBe(posts[1].title); + expect(result[1].title).toBe(posts[2].title); + expect(result[2].title).toBe(posts[4].title); + }); + it('filter last year record', async () => { + const filter = parse({ + updatedAt: { + $dateOn: '{{$system.dateRange.lastYear}}', + }, + })({ + $system: { dateRange: dateRangeFns }, + }); + + const posts = await PostRepo.createMany({ + records: [ + { title: 'last year', updatedAt: lastYear.toDate() }, + { title: 'start of last year', updatedAt: lastYearFirstDay.toDate() }, + { + title: 'the time before start of last year 1s', + updatedAt: lastYearFirstDay.clone().subtract(1, 's').toDate(), + }, + { title: 'end of last year', updatedAt: lastYearLastDay.toDate() }, + { title: 'the time after end of last year 1s', updatedAt: lastYearLastDay.clone().add(1, 's').toDate() }, + ], + silent: true, + }); + + await sleep(500); + + const result = await PostRepo.find({ filter }); + expect(result.length).toBe(3); + expect(result[0].title).toBe(posts[0].title); + expect(result[1].title).toBe(posts[1].title); + expect(result[2].title).toBe(posts[3].title); + }); + it('filter this year record', async () => { + const filter = parse({ + updatedAt: { + $dateOn: '{{$system.dateRange.thisYear}}', + }, + })({ + $system: { dateRange: dateRangeFns }, + }); + + const posts = await PostRepo.createMany({ + records: [ + { title: 'a year ago', updatedAt: lastYear.toDate() }, + { title: 'this year', updatedAt: today.toDate() }, + { title: 'start of this year', updatedAt: thisYearFirstDay.toDate() }, + { + title: 'the time before start of this year 1s', + updatedAt: thisYearFirstDay.clone().subtract(1, 's').toDate(), + }, + { title: 'end of this year', updatedAt: thisYearLastDay.toDate() }, + { title: 'the time after end of this year 1s', updatedAt: thisYearLastDay.clone().add(1, 's').toDate() }, + ], + silent: true, + }); + + await sleep(500); + + const result = await PostRepo.find({ filter }); + expect(result.length).toBe(3); + expect(result[0].title).toBe(posts[1].title); + expect(result[1].title).toBe(posts[2].title); + expect(result[2].title).toBe(posts[4].title); + }); + it('filter next year record', async () => { + const filter = parse({ + updatedAt: { + $dateOn: '{{$system.dateRange.nextYear}}', + }, + })({ + $system: { dateRange: dateRangeFns }, + }); + + const posts = await PostRepo.createMany({ + records: [ + { title: 'a year ago', updatedAt: lastYear.toDate() }, + { title: 'next year', updatedAt: nextYear.toDate() }, + { title: 'start of next year', updatedAt: nextYearFirstDay.toDate() }, + { + title: 'the time before start of next year 1s', + updatedAt: nextYearFirstDay.clone().subtract(1, 's').toDate(), + }, + { title: 'end of next year', updatedAt: nextYearLastDay.toDate() }, + { title: 'the time after end of next year 1s', updatedAt: nextYearLastDay.clone().add(1, 's').toDate() }, + ], + silent: true, + }); + + await sleep(500); + + const result = await PostRepo.find({ filter }); + expect(result.length).toBe(3); + expect(result[0].title).toBe(posts[1].title); + expect(result[1].title).toBe(posts[2].title); + expect(result[2].title).toBe(posts[4].title); + }); + }); +}); diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/functions/index.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/functions/index.ts index 48a596a4f5..8ebec54006 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/functions/index.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/functions/index.ts @@ -7,18 +7,102 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ +import { utc2unit, getDateVars } from '@nocobase/utils'; +import dayjs, { Dayjs } from 'dayjs'; import Plugin from '..'; import type { ExecutionModel, FlowNodeModel } from '../types'; export type CustomFunction = (this: { execution: ExecutionModel; node?: FlowNodeModel }) => any; +function getTimezone() { + const offset = new Date().getTimezoneOffset(); + const hours = String(Math.floor(Math.abs(offset) / 60)).padStart(2, '0'); + const minutes = String(Math.abs(offset) % 60).padStart(2, '0'); + const sign = offset <= 0 ? '+' : '-'; + return `${sign}${hours}:${minutes}`; +} +const getRangeByDay = (offset: number) => + utc2unit({ now: new Date(), unit: 'day', offset: offset, timezone: getTimezone() }); + +const getOffsetFromMS = (start, end) => Math.floor((end - start) / 1000 / 60 / 60 / 24); + function now() { return new Date(); } +const dateVars = getDateVars(); +export const dateRangeFns = { + yesterday() { + return dateVars.yesterday({ now: new Date(), timezone: getTimezone() }); + }, + today() { + return dateVars.today({ now: new Date(), timezone: getTimezone() }); + }, + tomorrow() { + return dateVars.tomorrow({ now: new Date(), timezone: getTimezone() }); + }, + lastWeek() { + return dateVars.lastWeek({ now: new Date(), timezone: getTimezone() }); + }, + thisWeek() { + return dateVars.thisWeek({ now: new Date(), timezone: getTimezone() }); + }, + nextWeek() { + return dateVars.nextWeek({ now: new Date(), timezone: getTimezone() }); + }, + lastMonth() { + return dateVars.lastMonth({ now: new Date(), timezone: getTimezone() }); + }, + thisMonth() { + return dateVars.thisMonth({ now: new Date(), timezone: getTimezone() }); + }, + nextMonth() { + return dateVars.nextMonth({ now: new Date(), timezone: getTimezone() }); + }, + lastQuarter() { + return dateVars.lastQuarter({ now: new Date(), timezone: getTimezone() }); + }, + thisQuarter() { + return dateVars.thisQuarter({ now: new Date(), timezone: getTimezone() }); + }, + nextQuarter() { + return dateVars.nextQuarter({ now: new Date(), timezone: getTimezone() }); + }, + lastYear() { + return dateVars.lastYear({ now: new Date(), timezone: getTimezone() }); + }, + thisYear() { + return dateVars.thisYear({ now: new Date(), timezone: getTimezone() }); + }, + nextYear() { + return dateVars.nextYear({ now: new Date(), timezone: getTimezone() }); + }, + last7Days() { + return dateVars.last7Days({ now: new Date(), timezone: getTimezone() }); + }, + next7Days() { + return dateVars.next7Days({ now: new Date(), timezone: getTimezone() }); + }, + last30Days() { + return dateVars.last30Days({ now: new Date(), timezone: getTimezone() }); + }, + next30Days() { + return dateVars.next30Days({ now: new Date(), timezone: getTimezone() }); + }, + last90Days() { + return dateVars.last90Days({ now: new Date(), timezone: getTimezone() }); + }, + next90Days() { + return dateVars.next90Days({ now: new Date(), timezone: getTimezone() }); + }, +}; export default function ({ functions }: Plugin, more: { [key: string]: CustomFunction } = {}) { functions.register('now', now); + Object.keys(dateRangeFns).forEach((key) => { + functions.register(`dateRange.${key}`, dateRangeFns[key]); + }); + for (const [name, fn] of Object.entries(more)) { functions.register(name, fn); } diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/index.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/index.ts index 9403e535eb..d7baac1bf2 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/index.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/index.ts @@ -10,6 +10,7 @@ export * from './utils'; export * from './constants'; export * from './instructions'; +export * from './functions'; export { Trigger } from './triggers'; export { default as Processor } from './Processor'; export { default } from './Plugin';