diff --git a/packages/app/src/api/app.ts b/packages/app/src/api/app.ts
index 43d281ad6f..9542a1c022 100644
--- a/packages/app/src/api/app.ts
+++ b/packages/app/src/api/app.ts
@@ -67,5 +67,6 @@ api.registerPlugin('plugin-pages', [path.resolve(__dirname, '../../../plugin-pag
api.registerPlugin('plugin-users', [path.resolve(__dirname, '../../../plugin-users'), {}]);
api.registerPlugin('plugin-file-manager', [path.resolve(__dirname, '../../../plugin-file-manager'), {}]);
api.registerPlugin('plugin-permissions', [path.resolve(__dirname, '../../../plugin-permissions'), {}]);
+api.registerPlugin('plugin-automations', [path.resolve(__dirname, '../../../plugin-automations'), {}]);
export default api;
diff --git a/packages/app/src/api/index.ts b/packages/app/src/api/index.ts
index 44923e15fd..e0bb237b61 100644
--- a/packages/app/src/api/index.ts
+++ b/packages/app/src/api/index.ts
@@ -80,6 +80,7 @@ api.resourcer.use(async (ctx: actions.Context, next) => {
await api.database.getModel('collections').load({where: {
name: 'users',
}});
+ await api.database.getModel('automations').load();
api.listen(process.env.HTTP_PORT, () => {
console.log(`http://localhost:${process.env.HTTP_PORT}/`);
});
diff --git a/packages/app/src/api/migrations/create-automations.ts b/packages/app/src/api/migrations/create-automations.ts
new file mode 100644
index 0000000000..8cefb956a1
--- /dev/null
+++ b/packages/app/src/api/migrations/create-automations.ts
@@ -0,0 +1,19 @@
+import api from '../app';
+import Database from '@nocobase/database';
+
+(async () => {
+ await api.loadPlugins();
+ const database: Database = api.database;
+ await api.database.sync({
+ tables: ['automations', 'automations_jobs', 'actions_scopes'],
+ });
+
+ const [Collection, User, Role] = database.getModels(['collections', 'users', 'roles']);
+
+ const tables = database.getTables(['automations', 'automations_jobs', 'actions_scopes']);
+
+ for (let table of tables) {
+ console.log(table.getName());
+ await Collection.import(table.getOptions(), { update: true, migrate: false });
+ }
+})();
diff --git a/packages/app/src/api/migrations/init.ts b/packages/app/src/api/migrations/init.ts
index 5ae5848b02..fda9139969 100644
--- a/packages/app/src/api/migrations/init.ts
+++ b/packages/app/src/api/migrations/init.ts
@@ -132,6 +132,15 @@ const data = [
sort: 120,
showInMenu: true,
},
+ {
+ title: '自动化配置',
+ type: 'collection',
+ collection: 'automations',
+ path: '/settings/automations',
+ icon: 'TableOutlined',
+ sort: 130,
+ showInMenu: true,
+ },
]
},
],
diff --git a/packages/app/src/components/form.fields/automations/index.tsx b/packages/app/src/components/form.fields/automations/index.tsx
new file mode 100644
index 0000000000..7660d6c5e7
--- /dev/null
+++ b/packages/app/src/components/form.fields/automations/index.tsx
@@ -0,0 +1,286 @@
+import React, { useEffect, useState } from 'react';
+import { connect } from '@formily/react-schema-renderer'
+import { Button, Select, DatePicker, Tag, InputNumber, TimePicker, Input } from 'antd';
+import {
+ Select as AntdSelect,
+ mapStyledProps,
+ mapTextComponent
+} from '../shared'
+import moment from 'moment';
+import './style.less';
+import api from '@/api-client';
+import { useRequest } from 'umi';
+
+export const DateTime = connect({
+ getProps: mapStyledProps,
+ getComponent: mapTextComponent,
+})((props) => {
+ const { associatedKey, automationType, filter, onChange } = props;
+ const [aKey, setaKey] = useState(associatedKey);
+ const [aType, setaType] = useState(automationType);
+ console.log('Automations.DateTime', aKey, associatedKey)
+ const [value, setValue] = useState(props.value||{});
+ const [offsetType, setOffsetType] = useState(() => {
+ if (!value.offset) {
+ return 'current';
+ }
+ if (value.offset > 0) {
+ return 'after';
+ }
+ if (value.offset < 0) {
+ return 'before';
+ }
+ return 'current';
+ })
+
+ useEffect(() => {
+ if (associatedKey !== aKey || automationType !== aType) {
+ setOffsetType('current');
+ setValue({
+ value: null,
+ byField: null,
+ offset: 0,
+ unit: undefined,
+ });
+ onChange({
+ value: null,
+ byField: null,
+ offset: 0,
+ unit: undefined,
+ });
+ setaKey(associatedKey)
+ }
+ }, [ associatedKey, automationType, aKey, aType ]);
+ const { data = [], loading = true } = useRequest(() => {
+ return associatedKey && automationType !== 'schedule' ? api.resource('collections.fields').list({
+ associatedKey,
+ filter: filter||{
+ type: 'date',
+ },
+ }) : Promise.resolve({data: []});
+ }, {
+ refreshDeps: [associatedKey, automationType, filter]
+ });
+ console.log({data});
+ return (
+
+
+ {automationType === 'schedule' ? (
+ {
+ onChange({value: m.toISOString()});
+ setValue({value: m.toISOString()});
+ // console.log('Automations.DateTime', m.toISOString(), {m, dateString})
+ }} defaultValue={(() => {
+ if (!value.value) {
+ return undefined;
+ }
+ const m = moment(value.value);
+ return m.isValid() ? m : undefined;
+ })()}/>
+ ) : (
+
+
+
+ {offsetType !== 'current' && (
+ {
+ const values = {
+ unit: 'day',...value,
+ }
+ if (offsetType === 'before') {
+ values.offset = -1 * Math.abs(offset);
+ } else if (offsetType === 'after') {
+ values.offset = Math.abs(offset);
+ }
+ setValue(values);
+ onChange(values);
+ console.log('Automations.DateTime', values)
+ // console.log(offsetType);
+ }} placeholder={'数字'}/>
+ )}
+ {offsetType !== 'current' && (
+
+ )}
+ {offsetType !== 'current' && value.unit && ['day', 'week', 'month'].indexOf(value.unit) !== -1 && (
+ {
+ const m = moment(value.time, 'HH:mm:ss');
+ return m.isValid() ? m : undefined;
+ })()} onChange={(m, dateString) => {
+ console.log('Automations.DateTime', m, dateString)
+ setValue({...value, time: dateString});
+ onChange({...value, time: dateString});
+ }}/>
+ )}
+
+ )}
+
+
+ )
+})
+
+const cronmap = {
+ none: '不重复',
+ everysecond: '每秒',
+ everyminute: '每分钟',
+ everyhour: '每小时',
+ everyday: '每天',
+ everyweek: '每周',
+ everymonth: '每月',
+ custom: '自定义',
+};
+
+export const Cron = connect({
+ getProps: mapStyledProps,
+ getComponent: mapTextComponent,
+})((props) => {
+ const { value, onChange } = props;
+
+ console.log('Automations.DateTime', {value})
+
+ const re = /every_(\d+)_(.+)/i;
+
+ const match = cronmap[value] ? null : re.exec(value);
+
+ const [unit, setUnit] = useState(match ? match[2] : 'days');
+ const [num, setNum] = useState(match ? parseInt(match[1]) : undefined);
+ const [cron, setCron] = useState(() => {
+ if (!value) {
+ return 'none';
+ }
+ return match ? 'custom' : cronmap[value];
+ })
+
+ return (
+
+
+
+ {cron === 'custom' && (
+ {
+ const v = parseInt(e.target.value);
+ setNum(v);
+ onChange(`every_${v}_${unit}`);
+ }} defaultValue={num} addonBefore={'每'}/>
+ )}
+ {cron === 'custom' && (
+
+ )}
+
+
+ )
+})
+
+export const EndMode = connect({
+ getProps: mapStyledProps,
+ getComponent: mapTextComponent,
+})((props) => {
+ const { value = 'none', onChange, automationType } = props;
+ const re = /after_(\d+)_times/i;
+ const match = re.exec(value);
+
+ const [mode, setMode] = useState(() => {
+ if (automationType === 'schedule' && value === 'byField') {
+ return 'none';
+ } else if (automationType === 'collections:schedule' && value === 'customTime') {
+ return 'none';
+ }
+
+ return match ? 'times' : value;
+ });
+
+ const [num, setNum] = useState(match ? parseInt(match[1]) : undefined);
+
+ useEffect(() => {
+ if (automationType === 'schedule' && value === 'byField') {
+ setMode('none')
+ onChange('none')
+ } else if (automationType === 'collections:schedule' && value === 'customTime') {
+ setMode('none')
+ onChange('none')
+ }
+ }, [automationType]);
+ console.log('Automations.DateTime', {value, automationType, mode})
+
+ return (
+
+
+
+ {mode === 'times' && {
+ const v = parseInt(e.target.value);
+ setNum(v);
+ onChange(`after_${v}_times`);
+ }} defaultValue={num} addonAfter={'次'}/>}
+
+
+ )
+})
+
+export const Automations = {
+ DateTime, Cron, EndMode
+};
+
diff --git a/packages/app/src/components/form.fields/automations/style.less b/packages/app/src/components/form.fields/automations/style.less
new file mode 100644
index 0000000000..6d89298814
--- /dev/null
+++ b/packages/app/src/components/form.fields/automations/style.less
@@ -0,0 +1,5 @@
+.ant-input-group.ant-input-group-compact {
+ .ant-input-group-wrapper {
+ width: 120px;
+ }
+}
\ No newline at end of file
diff --git a/packages/app/src/components/form.fields/filter/index.tsx b/packages/app/src/components/form.fields/filter/index.tsx
index eeb5d52013..95a349da49 100644
--- a/packages/app/src/components/form.fields/filter/index.tsx
+++ b/packages/app/src/components/form.fields/filter/index.tsx
@@ -11,7 +11,7 @@ import api from '@/api-client';
import { useRequest } from 'umi';
export function FilterGroup(props: any) {
- const { showDeleteButton = true, fields = [], onDelete, onChange, onAdd, dataSource = {} } = props;
+ const { showDeleteButton = true, fields = [], sourceFields = [], onDelete, onChange, onAdd, dataSource = {} } = props;
const { list, getKey, push, remove, replace } = useDynamicList(dataSource.list || [
{
type: 'item',
@@ -50,6 +50,7 @@ export function FilterGroup(props: any) {
{
1}
onChange={(value) => {
@@ -319,10 +320,11 @@ function NullControl(props) {
}
export function FilterItem(props: FilterItemProps) {
- const { index, fields = [], showDeleteButton = true, onDelete, onChange } = props;
+ const { index, fields = [], sourceFields = [], showDeleteButton = true, onDelete, onChange } = props;
const [type, setType] = useState('string');
const [field, setField] = useState({});
const [dataSource, setDataSource] = useState(props.dataSource||{});
+ const [valueType, setValueType] = useState('custom');
useEffect(() => {
const field = fields.find(field => field.name === props.dataSource.column);
if (field) {
@@ -334,6 +336,9 @@ export function FilterItem(props: FilterItemProps) {
setType(componentType);
}
setDataSource({...props.dataSource});
+ if (/^{{.+}}$/.test(props.dataSource.value)) {
+ setValueType('ref');
+ }
}, [
props.dataSource, type,
]);
@@ -347,7 +352,7 @@ export function FilterItem(props: FilterItemProps) {
// let multiple = true;
// if ()
const opOptions = op[type]||op.string;
- console.log({field, dataSource, type, ValueControl});
+ console.log({valueType});
return (
- {
- onChange({...dataSource, value: value});
- }}
- style={{ width: 180 }}
- />
+ {sourceFields.length > 0 && (
+
+ )}
+ {valueType !== 'ref' ? (
+ {
+ onChange({...dataSource, value: value});
+ }}
+ style={{ width: 180 }}
+ />
+ ) : (sourceFields.length > 0 ? (
+
+ ) : null)}
{showDeleteButton && (