@nocobase/plugin-workflow 0.7.5-alpha.1 → 0.7.6-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client/components/CollectionFieldset.js +61 -36
- package/lib/client/nodes/index.js +3 -2
- package/lib/client/triggers/collection.js +1 -1
- package/lib/server/Plugin.d.ts +3 -1
- package/lib/server/Plugin.js +2 -1
- package/lib/server/Processor.d.ts +2 -1
- package/lib/server/actions/workflows.js +23 -2
- package/lib/server/instructions/create.js +1 -0
- package/lib/server/instructions/destroy.js +1 -0
- package/lib/server/instructions/query.js +1 -0
- package/lib/server/instructions/update.js +1 -0
- package/lib/server/triggers/collection.js +22 -9
- package/lib/server/triggers/schedule.d.ts +4 -2
- package/lib/server/triggers/schedule.js +175 -181
- package/package.json +8 -8
|
@@ -93,27 +93,61 @@ function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { va
|
|
|
93
93
|
|
|
94
94
|
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
function AssociationInput(props) {
|
|
97
|
+
var _data$config;
|
|
98
|
+
|
|
99
|
+
const _useCollectionManager = (0, _client().useCollectionManager)(),
|
|
100
|
+
getCollectionFields = _useCollectionManager.getCollectionFields;
|
|
101
|
+
|
|
102
|
+
const _useField = (0, _react2().useField)(),
|
|
103
|
+
path = _useField.path;
|
|
104
|
+
|
|
105
|
+
const fieldName = path.segments[path.segments.length - 1];
|
|
106
|
+
|
|
107
|
+
const _useForm = (0, _react2().useForm)(),
|
|
108
|
+
data = _useForm.values;
|
|
109
|
+
|
|
110
|
+
const fields = getCollectionFields(data === null || data === void 0 ? void 0 : (_data$config = data.config) === null || _data$config === void 0 ? void 0 : _data$config.collection);
|
|
111
|
+
|
|
112
|
+
const _fields$find = fields.find(item => item.name === fieldName),
|
|
113
|
+
type = _fields$find.type;
|
|
114
|
+
|
|
115
|
+
const value = Array.isArray(props.value) ? props.value.join(',') : props.value;
|
|
116
|
+
|
|
117
|
+
function onChange(ev) {
|
|
118
|
+
const trimed = ev.target.value.trim();
|
|
119
|
+
props.onChange(['belongsTo', 'hasOne'].includes(type) ? trimed : trimed.split(/[,\s]+/));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return _react().default.createElement(_antd().Input, _objectSpread(_objectSpread({}, props), {}, {
|
|
123
|
+
value: value,
|
|
124
|
+
onChange: onChange
|
|
125
|
+
}));
|
|
126
|
+
} // NOTE: observer for watching useProps
|
|
127
|
+
|
|
128
|
+
|
|
97
129
|
var _default = (0, _react2().observer)(({
|
|
98
130
|
value,
|
|
99
131
|
onChange: _onChange
|
|
100
132
|
}) => {
|
|
101
|
-
var _data$
|
|
133
|
+
var _data$config2;
|
|
102
134
|
|
|
103
135
|
const _useTranslation = (0, _reactI18next().useTranslation)(),
|
|
104
136
|
t = _useTranslation.t;
|
|
105
137
|
|
|
106
138
|
const compile = (0, _client().useCompile)();
|
|
107
139
|
|
|
108
|
-
const
|
|
109
|
-
getCollection =
|
|
110
|
-
getCollectionFields =
|
|
140
|
+
const _useCollectionManager2 = (0, _client().useCollectionManager)(),
|
|
141
|
+
getCollection = _useCollectionManager2.getCollection,
|
|
142
|
+
getCollectionFields = _useCollectionManager2.getCollectionFields;
|
|
111
143
|
|
|
112
|
-
const
|
|
113
|
-
data =
|
|
144
|
+
const _useForm2 = (0, _react2().useForm)(),
|
|
145
|
+
data = _useForm2.values;
|
|
114
146
|
|
|
115
|
-
const collectionName = data === null || data === void 0 ? void 0 : (_data$
|
|
116
|
-
const fields = getCollectionFields(
|
|
147
|
+
const collectionName = data === null || data === void 0 ? void 0 : (_data$config2 = data.config) === null || _data$config2 === void 0 ? void 0 : _data$config2.collection;
|
|
148
|
+
const fields = getCollectionFields(collectionName).filter(field => !field.hidden && (field.uiSchema ? !field.uiSchema['x-read-pretty'] : false) // && (!['linkTo', 'hasMany', 'hasOne', 'belongsToMany'].includes(field.type))
|
|
149
|
+
);
|
|
150
|
+
const unassignedFields = fields.filter(field => !(field.name in value));
|
|
117
151
|
return _react().default.createElement("fieldset", {
|
|
118
152
|
className: (0, _css().css)`
|
|
119
153
|
margin-top: .5em;
|
|
@@ -131,30 +165,20 @@ var _default = (0, _react2().observer)(({
|
|
|
131
165
|
}, fields.filter(field => field.name in value).map(field => {
|
|
132
166
|
var _field$uiSchema$title, _field$uiSchema;
|
|
133
167
|
|
|
134
|
-
const VTypes = _objectSpread({}, _calculators.VariableTypes)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
title: '{{t("Constant")}}',
|
|
142
|
-
value: 'constant',
|
|
143
|
-
options: undefined
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
operand = typeof value[field.name] === 'string' ? (0, _calculators.parseStringValue)(value[field.name], VTypes) : {
|
|
147
|
-
type: 'constant',
|
|
148
|
-
value: value[field.name]
|
|
149
|
-
};
|
|
150
|
-
} else {
|
|
151
|
-
delete VTypes.constant;
|
|
152
|
-
operand = typeof value[field.name] === 'string' ? (0, _calculators.parseStringValue)(value[field.name], VTypes) : {
|
|
153
|
-
type: '$context',
|
|
154
|
-
value: value[field.name]
|
|
155
|
-
};
|
|
156
|
-
} // TODO: try to use <ObjectField> to replace this map
|
|
168
|
+
const VTypes = _objectSpread(_objectSpread({}, ['linkTo', 'hasMany', 'belongsToMany'].includes(field.type) ? {} : _calculators.VariableTypes), {}, {
|
|
169
|
+
constant: {
|
|
170
|
+
title: '{{t("Constant")}}',
|
|
171
|
+
value: 'constant',
|
|
172
|
+
options: undefined
|
|
173
|
+
}
|
|
174
|
+
});
|
|
157
175
|
|
|
176
|
+
const operand = typeof value[field.name] === 'string' ? (0, _calculators.parseStringValue)(value[field.name], VTypes) : {
|
|
177
|
+
type: 'constant',
|
|
178
|
+
value: value[field.name]
|
|
179
|
+
}; // constant for associations to use Input, others to use CollectionField
|
|
180
|
+
// dynamic values only support belongsTo/hasOne association, other association type should disable
|
|
181
|
+
// TODO: try to use <ObjectField> to replace this map
|
|
158
182
|
|
|
159
183
|
return _react().default.createElement(_antd().Form.Item, {
|
|
160
184
|
key: field.name,
|
|
@@ -187,12 +211,13 @@ var _default = (0, _react2().observer)(({
|
|
|
187
211
|
type: 'void',
|
|
188
212
|
properties: {
|
|
189
213
|
[field.name]: {
|
|
190
|
-
'x-component': 'CollectionField'
|
|
214
|
+
'x-component': ['linkTo', 'belongsTo', 'hasOne', 'hasMany', 'belongsToMany'].includes(field.type) ? 'AssociationInput' : 'CollectionField'
|
|
191
215
|
}
|
|
192
216
|
}
|
|
193
217
|
},
|
|
194
218
|
components: {
|
|
195
|
-
CollectionField: _client().CollectionField
|
|
219
|
+
CollectionField: _client().CollectionField,
|
|
220
|
+
AssociationInput
|
|
196
221
|
}
|
|
197
222
|
}) // ? <SchemaComponent schema={{ ...field.uiSchema, name: field.name }} />
|
|
198
223
|
: null), _react().default.createElement(_antd().Button, {
|
|
@@ -206,7 +231,7 @@ var _default = (0, _react2().observer)(({
|
|
|
206
231
|
_onChange(rest);
|
|
207
232
|
}
|
|
208
233
|
})));
|
|
209
|
-
}),
|
|
234
|
+
}), unassignedFields.length ? _react().default.createElement(_antd().Dropdown, {
|
|
210
235
|
overlay: _react().default.createElement(_antd().Menu, {
|
|
211
236
|
onClick: ({
|
|
212
237
|
key
|
|
@@ -217,7 +242,7 @@ var _default = (0, _react2().observer)(({
|
|
|
217
242
|
max-height: 300px;
|
|
218
243
|
overflow-y: auto;
|
|
219
244
|
`
|
|
220
|
-
},
|
|
245
|
+
}, unassignedFields.map(field => {
|
|
221
246
|
var _field$uiSchema$title2, _field$uiSchema2;
|
|
222
247
|
|
|
223
248
|
return _react().default.createElement(_antd().Menu.Item, {
|
|
@@ -154,9 +154,10 @@ function useUpdateAction() {
|
|
|
154
154
|
_antd().message.error(t('Node in executed workflow cannot be modified'));
|
|
155
155
|
|
|
156
156
|
return;
|
|
157
|
-
}
|
|
157
|
+
} // TODO: how to do validation separately for each field? especially disabled for dynamic fields?
|
|
158
|
+
// await form.submit();
|
|
159
|
+
|
|
158
160
|
|
|
159
|
-
yield form.submit();
|
|
160
161
|
yield api.resource('flow_nodes', data.id).update({
|
|
161
162
|
filterByTk: data.id,
|
|
162
163
|
values: {
|
|
@@ -103,7 +103,7 @@ const FieldsSelect = (0, _react2().observer)(props => {
|
|
|
103
103
|
className: (0, _css().css)`
|
|
104
104
|
min-width: 6em;
|
|
105
105
|
`
|
|
106
|
-
}), fields.filter(field => !field.hidden && (field.uiSchema ? !field.uiSchema['x-read-pretty'] : true)).map(field => {
|
|
106
|
+
}), fields.filter(field => !field.hidden && (field.uiSchema ? !field.uiSchema['x-read-pretty'] : true) && !['linkTo', 'hasOne', 'hasMany', 'belongsToMany'].includes(field.type)).map(field => {
|
|
107
107
|
var _field$uiSchema;
|
|
108
108
|
|
|
109
109
|
return _react().default.createElement(_antd().Select.Option, {
|
package/lib/server/Plugin.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export default class WorkflowPlugin extends Plugin {
|
|
|
15
15
|
getName(): string;
|
|
16
16
|
load(): Promise<void>;
|
|
17
17
|
toggle(workflow: WorkflowModel, enable?: boolean): void;
|
|
18
|
-
trigger(workflow: WorkflowModel, context: Object, options?: Transactionable
|
|
18
|
+
trigger(workflow: WorkflowModel, context: Object, options?: Transactionable & {
|
|
19
|
+
context?: any;
|
|
20
|
+
}): Promise<ExecutionModel | null>;
|
|
19
21
|
createProcessor(execution: ExecutionModel, options?: {}): Processor;
|
|
20
22
|
}
|
package/lib/server/Plugin.js
CHANGED
|
@@ -272,7 +272,8 @@ class WorkflowPlugin extends _server().Plugin {
|
|
|
272
272
|
execution.workflow = workflow;
|
|
273
273
|
|
|
274
274
|
const processor = _this3.createProcessor(execution, {
|
|
275
|
-
transaction
|
|
275
|
+
transaction,
|
|
276
|
+
_context: options.context
|
|
276
277
|
});
|
|
277
278
|
|
|
278
279
|
yield processor.start(); // @ts-ignore
|
|
@@ -4,11 +4,12 @@ import ExecutionModel from './models/Execution';
|
|
|
4
4
|
import JobModel from './models/Job';
|
|
5
5
|
import FlowNodeModel from './models/FlowNode';
|
|
6
6
|
export interface ProcessorOptions extends Transactionable {
|
|
7
|
+
_context?: any;
|
|
7
8
|
plugin: Plugin;
|
|
8
9
|
}
|
|
9
10
|
export default class Processor {
|
|
10
11
|
execution: ExecutionModel;
|
|
11
|
-
|
|
12
|
+
options: ProcessorOptions;
|
|
12
13
|
static StatusMap: {
|
|
13
14
|
[x: number]: number;
|
|
14
15
|
};
|
|
@@ -75,7 +75,20 @@ function migrateConfig(config, oldToNew) {
|
|
|
75
75
|
return value.map(item => migrate(item));
|
|
76
76
|
|
|
77
77
|
case 'string':
|
|
78
|
-
|
|
78
|
+
const matcher = value.match(/(\{\{\$jobsMapByNodeId\.)([\w-]+)/);
|
|
79
|
+
|
|
80
|
+
if (!matcher) {
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const oldNodeId = Number.parseInt(matcher[2], 10);
|
|
85
|
+
const newNode = oldToNew.get(oldNodeId);
|
|
86
|
+
|
|
87
|
+
if (!newNode) {
|
|
88
|
+
throw new Error('node configurated for result is not existed');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return value.replace(matcher[0], `{{$jobsMapByNodeId.${newNode.id}`);
|
|
79
92
|
|
|
80
93
|
default:
|
|
81
94
|
return value;
|
|
@@ -171,10 +184,18 @@ function _revision() {
|
|
|
171
184
|
const oldNode = originalNodesMap.get(oldId);
|
|
172
185
|
const newUpstream = oldNode.upstreamId ? oldToNew.get(oldNode.upstreamId) : null;
|
|
173
186
|
const newDownstream = oldNode.downstreamId ? oldToNew.get(oldNode.downstreamId) : null;
|
|
187
|
+
let migratedConfig;
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
migratedConfig = migrateConfig(oldNode.config, oldToNew);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
return context.throw(400, err.message);
|
|
193
|
+
}
|
|
194
|
+
|
|
174
195
|
yield newNode.update({
|
|
175
196
|
upstreamId: (_newUpstream$id = newUpstream === null || newUpstream === void 0 ? void 0 : newUpstream.id) !== null && _newUpstream$id !== void 0 ? _newUpstream$id : null,
|
|
176
197
|
downstreamId: (_newDownstream$id = newDownstream === null || newDownstream === void 0 ? void 0 : newDownstream.id) !== null && _newDownstream$id !== void 0 ? _newDownstream$id : null,
|
|
177
|
-
config:
|
|
198
|
+
config: migratedConfig
|
|
178
199
|
}, {
|
|
179
200
|
transaction
|
|
180
201
|
});
|
|
@@ -27,6 +27,7 @@ var _default = {
|
|
|
27
27
|
const repo = node.constructor.database.getRepository(collection);
|
|
28
28
|
const options = processor.getParsedValue(params);
|
|
29
29
|
const result = yield repo.create(_objectSpread(_objectSpread({}, options), {}, {
|
|
30
|
+
context: processor.options._context,
|
|
30
31
|
transaction: processor.transaction
|
|
31
32
|
}));
|
|
32
33
|
return {
|
|
@@ -27,6 +27,7 @@ var _default = {
|
|
|
27
27
|
const repo = node.constructor.database.getRepository(collection);
|
|
28
28
|
const options = processor.getParsedValue(params);
|
|
29
29
|
const result = yield repo.destroy(_objectSpread(_objectSpread({}, options), {}, {
|
|
30
|
+
context: processor.options._context,
|
|
30
31
|
transaction: processor.transaction
|
|
31
32
|
}));
|
|
32
33
|
return {
|
|
@@ -28,6 +28,7 @@ var _default = {
|
|
|
28
28
|
const repo = node.constructor.database.getRepository(collection);
|
|
29
29
|
const options = processor.getParsedValue(params);
|
|
30
30
|
const result = yield (multiple ? repo.find : repo.findOne).call(repo, _objectSpread(_objectSpread({}, options), {}, {
|
|
31
|
+
context: processor.options._context,
|
|
31
32
|
transaction: processor.transaction
|
|
32
33
|
})); // NOTE: `toJSON()` to avoid getting undefined value from Proxied model instance (#380)
|
|
33
34
|
// e.g. Object.prototype.hasOwnProperty.call(result, 'id') // false
|
|
@@ -29,6 +29,7 @@ var _default = {
|
|
|
29
29
|
const repo = node.constructor.database.getRepository(collection);
|
|
30
30
|
const options = processor.getParsedValue(params);
|
|
31
31
|
const result = yield repo.update(_objectSpread(_objectSpread({}, options), {}, {
|
|
32
|
+
context: processor.options._context,
|
|
32
33
|
transaction: processor.transaction
|
|
33
34
|
}));
|
|
34
35
|
return {
|
|
@@ -37,6 +37,16 @@ MODE_BITMAP_EVENTS.set(MODE_BITMAP.DESTROY, 'afterDestroy');
|
|
|
37
37
|
|
|
38
38
|
function getHookId(workflow, type) {
|
|
39
39
|
return `${type}#${workflow.id}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getFieldRawName(collection, name) {
|
|
43
|
+
const field = collection.getField(name);
|
|
44
|
+
|
|
45
|
+
if (field && field.type === 'belongsTo') {
|
|
46
|
+
return field.foreignKey;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return name;
|
|
40
50
|
} // async function, should return promise
|
|
41
51
|
|
|
42
52
|
|
|
@@ -49,29 +59,31 @@ function _handler() {
|
|
|
49
59
|
var _condition$$and;
|
|
50
60
|
|
|
51
61
|
const _workflow$config3 = workflow.config,
|
|
52
|
-
|
|
62
|
+
collectionName = _workflow$config3.collection,
|
|
53
63
|
condition = _workflow$config3.condition,
|
|
54
|
-
changed = _workflow$config3.changed;
|
|
64
|
+
changed = _workflow$config3.changed;
|
|
65
|
+
const collection = data.constructor.database.getCollection(collectionName); // NOTE: if no configured fields changed, do not trigger
|
|
55
66
|
|
|
56
|
-
if (changed && changed.length && changed.every(name => !data.
|
|
57
|
-
//
|
|
67
|
+
if (changed && changed.length && changed.filter(name => !['linkTo', 'hasOne', 'hasMany', 'belongsToMany'].includes(collection.getField(name).type)).every(name => !data.changedWithAssociations(getFieldRawName(collection, name)))) {
|
|
68
|
+
// TODO: temp comment out
|
|
69
|
+
return;
|
|
58
70
|
} // NOTE: if no configured condition match, do not trigger
|
|
59
71
|
|
|
60
72
|
|
|
61
73
|
if (condition && ((_condition$$and = condition.$and) === null || _condition$$and === void 0 ? void 0 : _condition$$and.length)) {
|
|
62
74
|
// TODO: change to map filter format to calculation format
|
|
63
75
|
// const calculation = toCalculation(condition);
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const transaction = options.transaction;
|
|
76
|
+
const repository = collection.repository,
|
|
77
|
+
model = collection.model;
|
|
78
|
+
const transaction = options.transaction,
|
|
79
|
+
context = options.context;
|
|
69
80
|
const count = yield repository.count({
|
|
70
81
|
filter: {
|
|
71
82
|
$and: [condition, {
|
|
72
83
|
[model.primaryKeyAttribute]: data[model.primaryKeyAttribute]
|
|
73
84
|
}]
|
|
74
85
|
},
|
|
86
|
+
context,
|
|
75
87
|
transaction
|
|
76
88
|
});
|
|
77
89
|
|
|
@@ -83,6 +95,7 @@ function _handler() {
|
|
|
83
95
|
return this.plugin.trigger(workflow, {
|
|
84
96
|
data: data.get()
|
|
85
97
|
}, {
|
|
98
|
+
context: options.context,
|
|
86
99
|
transaction: options.transaction
|
|
87
100
|
});
|
|
88
101
|
});
|
|
@@ -15,9 +15,10 @@ export declare const SCHEDULE_MODE: {
|
|
|
15
15
|
readonly CONSTANT: 0;
|
|
16
16
|
readonly COLLECTION_FIELD: 1;
|
|
17
17
|
};
|
|
18
|
+
declare function matchNext(this: ScheduleTrigger, workflow: any, now: Date, range?: number): boolean;
|
|
18
19
|
export default class ScheduleTrigger extends Trigger {
|
|
19
|
-
static CacheRules: ((workflow: any, now: any) => any)[];
|
|
20
|
-
static TriggerRules: ((workflow: any, now: any) =>
|
|
20
|
+
static CacheRules: (typeof matchNext | ((workflow: any, now: any) => any))[];
|
|
21
|
+
static TriggerRules: ((workflow: any, now: any) => any)[];
|
|
21
22
|
events: Map<any, any>;
|
|
22
23
|
private timer;
|
|
23
24
|
private cache;
|
|
@@ -36,3 +37,4 @@ export default class ScheduleTrigger extends Trigger {
|
|
|
36
37
|
on(workflow: any): void;
|
|
37
38
|
off(workflow: any): void;
|
|
38
39
|
}
|
|
40
|
+
export {};
|
|
@@ -56,23 +56,26 @@ ScheduleModes.set(SCHEDULE_MODE.CONSTANT, {
|
|
|
56
56
|
endsOn = _workflow$config.endsOn,
|
|
57
57
|
repeat = _workflow$config.repeat;
|
|
58
58
|
const timestamp = now.getTime();
|
|
59
|
+
const startTime = Date.parse(startsOn);
|
|
59
60
|
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (!startTime || startTime > timestamp + this.cacheCycle) {
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
61
|
+
if (!startTime || startTime > timestamp + this.cacheCycle) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
66
64
|
|
|
65
|
+
if (repeat) {
|
|
67
66
|
if (typeof repeat === 'number' && repeat > this.cacheCycle && (timestamp - startTime) % repeat > this.cacheCycle) {
|
|
68
67
|
return false;
|
|
69
68
|
}
|
|
70
|
-
}
|
|
71
69
|
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
if (endsOn) {
|
|
71
|
+
const endTime = Date.parse(endsOn);
|
|
74
72
|
|
|
75
|
-
|
|
73
|
+
if (!endTime || endTime <= timestamp) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
if (startTime < timestamp) {
|
|
76
79
|
return false;
|
|
77
80
|
}
|
|
78
81
|
}
|
|
@@ -80,40 +83,45 @@ ScheduleModes.set(SCHEDULE_MODE.CONSTANT, {
|
|
|
80
83
|
return true;
|
|
81
84
|
},
|
|
82
85
|
|
|
83
|
-
trigger(workflow,
|
|
86
|
+
trigger(workflow, now) {
|
|
84
87
|
const _workflow$config2 = workflow.config,
|
|
85
88
|
startsOn = _workflow$config2.startsOn,
|
|
86
89
|
endsOn = _workflow$config2.endsOn,
|
|
87
90
|
repeat = _workflow$config2.repeat;
|
|
91
|
+
const timestamp = now.getTime();
|
|
92
|
+
const startTime = Math.floor(Date.parse(startsOn) / 1000) * 1000;
|
|
88
93
|
|
|
89
|
-
if (
|
|
90
|
-
|
|
94
|
+
if (repeat) {
|
|
95
|
+
if (typeof repeat === 'number') {
|
|
96
|
+
if (Math.round(timestamp - startTime) % repeat) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
91
100
|
|
|
92
|
-
if (
|
|
101
|
+
if (endsOn) {
|
|
102
|
+
const endTime = Date.parse(endsOn);
|
|
103
|
+
|
|
104
|
+
if (!endTime || endTime < timestamp) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
if (startTime !== timestamp) {
|
|
93
110
|
return;
|
|
94
111
|
}
|
|
95
112
|
}
|
|
96
113
|
|
|
97
114
|
return this.plugin.trigger(workflow, {
|
|
98
|
-
date
|
|
115
|
+
date: now
|
|
99
116
|
});
|
|
100
117
|
}
|
|
101
118
|
|
|
102
119
|
});
|
|
103
120
|
|
|
104
|
-
function
|
|
105
|
-
const timestamp = now.getTime();
|
|
106
|
-
const op = dir < 0 ? _sequelize().Op.lt : _sequelize().Op.gte;
|
|
107
|
-
|
|
121
|
+
function getOnTimestampWithOffset(on, now) {
|
|
108
122
|
switch (typeof on) {
|
|
109
123
|
case 'string':
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (!time || (dir < 0 ? timestamp < time : time <= timestamp)) {
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
break;
|
|
124
|
+
return Date.parse(on);
|
|
117
125
|
|
|
118
126
|
case 'object':
|
|
119
127
|
const field = on.field,
|
|
@@ -123,20 +131,17 @@ function getDateRangeFilter(on, now, dir) {
|
|
|
123
131
|
unit = _on$unit === void 0 ? 1000 : _on$unit;
|
|
124
132
|
|
|
125
133
|
if (!field) {
|
|
126
|
-
return
|
|
134
|
+
return null;
|
|
127
135
|
}
|
|
128
136
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
};
|
|
137
|
+
const timestamp = now.getTime(); // onDate + offset > now
|
|
138
|
+
// onDate > now - offset
|
|
139
|
+
|
|
140
|
+
return timestamp - offset * unit;
|
|
134
141
|
|
|
135
142
|
default:
|
|
136
|
-
|
|
143
|
+
return null;
|
|
137
144
|
}
|
|
138
|
-
|
|
139
|
-
return {};
|
|
140
145
|
}
|
|
141
146
|
|
|
142
147
|
function getDataOptionTime(data, on, dir = 1) {
|
|
@@ -151,7 +156,7 @@ function getDataOptionTime(data, on, dir = 1) {
|
|
|
151
156
|
offset = _on$offset2 === void 0 ? 0 : _on$offset2,
|
|
152
157
|
_on$unit2 = on.unit,
|
|
153
158
|
unit = _on$unit2 === void 0 ? 1000 : _on$unit2;
|
|
154
|
-
return data
|
|
159
|
+
return data.get(field) ? data.get(field).getTime() - offset * unit * dir : null;
|
|
155
160
|
|
|
156
161
|
default:
|
|
157
162
|
return null;
|
|
@@ -189,49 +194,49 @@ ScheduleModes.set(SCHEDULE_MODE.COLLECTION_FIELD, {
|
|
|
189
194
|
const event = `${collection}.afterSave`;
|
|
190
195
|
const name = getHookId(workflow, event);
|
|
191
196
|
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
var _ref = _asyncToGenerator(function* (data, options) {
|
|
196
|
-
const now = new Date();
|
|
197
|
-
now.setMilliseconds(0);
|
|
198
|
-
const timestamp = now.getTime();
|
|
199
|
-
const startTime = getDataOptionTime(data, startsOn);
|
|
200
|
-
const endTime = getDataOptionTime(data, endsOn, -1);
|
|
201
|
-
|
|
202
|
-
if (!startTime && !repeat) {
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
197
|
+
if (this.events.has(name)) {
|
|
198
|
+
return;
|
|
199
|
+
} // NOTE: toggle cache depends on new date
|
|
205
200
|
|
|
206
|
-
if (startTime && startTime > timestamp + _this.cacheCycle) {
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
201
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
202
|
+
const listener = /*#__PURE__*/function () {
|
|
203
|
+
var _ref = _asyncToGenerator(function* (data, options) {
|
|
204
|
+
const now = new Date();
|
|
205
|
+
now.setMilliseconds(0);
|
|
206
|
+
const timestamp = now.getTime();
|
|
207
|
+
const startTime = getDataOptionTime(data, startsOn);
|
|
208
|
+
const endTime = getDataOptionTime(data, endsOn, -1);
|
|
213
209
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
210
|
+
if (!startTime && !repeat) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
217
213
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
214
|
+
if (startTime && startTime > timestamp + _this.cacheCycle) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
221
217
|
|
|
222
|
-
|
|
218
|
+
if (endTime && endTime <= timestamp) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
223
221
|
|
|
224
|
-
|
|
225
|
-
|
|
222
|
+
if (!matchNext.call(_this, workflow, now)) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
226
225
|
|
|
227
|
-
|
|
228
|
-
return
|
|
229
|
-
}
|
|
230
|
-
}();
|
|
226
|
+
if (typeof repeat === 'number' && repeat > _this.cacheCycle && (timestamp - startTime) % repeat > _this.cacheCycle) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
231
229
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
230
|
+
_this.setCache(workflow);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
return function listener(_x, _x2) {
|
|
234
|
+
return _ref.apply(this, arguments);
|
|
235
|
+
};
|
|
236
|
+
}();
|
|
237
|
+
|
|
238
|
+
this.events.set(name, listener);
|
|
239
|
+
this.plugin.app.db.on(event, listener);
|
|
235
240
|
},
|
|
236
241
|
|
|
237
242
|
off(workflow) {
|
|
@@ -242,7 +247,7 @@ ScheduleModes.set(SCHEDULE_MODE.COLLECTION_FIELD, {
|
|
|
242
247
|
if (this.events.has(name)) {
|
|
243
248
|
const listener = this.events.get(name);
|
|
244
249
|
this.events.delete(name);
|
|
245
|
-
this.plugin.app.db.off(
|
|
250
|
+
this.plugin.app.db.off(event, listener);
|
|
246
251
|
}
|
|
247
252
|
},
|
|
248
253
|
|
|
@@ -250,34 +255,60 @@ ScheduleModes.set(SCHEDULE_MODE.COLLECTION_FIELD, {
|
|
|
250
255
|
var _this2 = this;
|
|
251
256
|
|
|
252
257
|
return _asyncToGenerator(function* () {
|
|
258
|
+
const db = _this2.plugin.app.db;
|
|
253
259
|
const _workflow$config4 = workflow.config,
|
|
254
260
|
startsOn = _workflow$config4.startsOn,
|
|
255
261
|
endsOn = _workflow$config4.endsOn,
|
|
256
262
|
repeat = _workflow$config4.repeat,
|
|
257
263
|
collection = _workflow$config4.collection;
|
|
258
|
-
const
|
|
264
|
+
const timestamp = now.getTime();
|
|
265
|
+
const startTimestamp = getOnTimestampWithOffset(startsOn, now);
|
|
259
266
|
|
|
260
|
-
if (!
|
|
267
|
+
if (!startTimestamp) {
|
|
261
268
|
return false;
|
|
262
269
|
}
|
|
263
270
|
|
|
264
|
-
const
|
|
271
|
+
const conditions = [{
|
|
272
|
+
[startsOn.field]: {
|
|
273
|
+
[_sequelize().Op.lt]: new Date(startTimestamp + _this2.cacheCycle)
|
|
274
|
+
}
|
|
275
|
+
}]; // when repeat is number, means repeat after startsOn
|
|
276
|
+
// (now - startsOn) % repeat <= cacheCycle
|
|
265
277
|
|
|
266
|
-
if (
|
|
267
|
-
|
|
268
|
-
}
|
|
278
|
+
if (repeat) {
|
|
279
|
+
const tsFn = DialectTimestampFnMap[db.options.dialect];
|
|
269
280
|
|
|
270
|
-
|
|
271
|
-
|
|
281
|
+
if (typeof repeat === 'number' && repeat > _this2.cacheCycle && tsFn) {
|
|
282
|
+
conditions.push((0, _sequelize().where)((0, _sequelize().fn)('MOD', (0, _sequelize().literal)(`${Math.round(timestamp / 1000)} - ${tsFn(startsOn.field)}`), Math.round(repeat / 1000)), {
|
|
283
|
+
[_sequelize().Op.lt]: Math.round(_this2.cacheCycle / 1000)
|
|
284
|
+
})); // conditions.push(literal(`mod(${timestamp} - ${tsFn(startsOn.field)} * 1000, ${repeat}) < ${this.cacheCycle}`));
|
|
285
|
+
}
|
|
272
286
|
|
|
273
|
-
|
|
274
|
-
|
|
287
|
+
if (endsOn) {
|
|
288
|
+
const endTimestamp = getOnTimestampWithOffset(endsOn, now);
|
|
275
289
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
290
|
+
if (!endTimestamp) {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (typeof endsOn === 'string') {
|
|
295
|
+
if (endTimestamp <= timestamp) {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
} else {
|
|
299
|
+
conditions.push({
|
|
300
|
+
[endsOn.field]: {
|
|
301
|
+
[_sequelize().Op.gte]: new Date(endTimestamp + _this2.interval)
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
conditions.push({
|
|
308
|
+
[startsOn.field]: {
|
|
309
|
+
[_sequelize().Op.gte]: new Date(startTimestamp)
|
|
310
|
+
}
|
|
311
|
+
});
|
|
281
312
|
}
|
|
282
313
|
|
|
283
314
|
const _db$getCollection = db.getCollection(collection),
|
|
@@ -292,35 +323,29 @@ ScheduleModes.set(SCHEDULE_MODE.COLLECTION_FIELD, {
|
|
|
292
323
|
})();
|
|
293
324
|
},
|
|
294
325
|
|
|
295
|
-
trigger(workflow,
|
|
326
|
+
trigger(workflow, now) {
|
|
296
327
|
var _this3 = this;
|
|
297
328
|
|
|
298
329
|
return _asyncToGenerator(function* () {
|
|
299
|
-
var _startsOn$offset, _startsOn$unit;
|
|
300
|
-
|
|
301
330
|
const _workflow$config5 = workflow.config,
|
|
302
|
-
collection = _workflow$config5.collection,
|
|
303
331
|
startsOn = _workflow$config5.startsOn,
|
|
332
|
+
repeat = _workflow$config5.repeat,
|
|
304
333
|
endsOn = _workflow$config5.endsOn,
|
|
305
|
-
|
|
334
|
+
collection = _workflow$config5.collection;
|
|
335
|
+
const timestamp = now.getTime();
|
|
336
|
+
const startTimestamp = getOnTimestampWithOffset(startsOn, now);
|
|
306
337
|
|
|
307
|
-
if (
|
|
308
|
-
return;
|
|
338
|
+
if (!startTimestamp) {
|
|
339
|
+
return false;
|
|
309
340
|
}
|
|
310
341
|
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
342
|
+
const conditions = [{
|
|
343
|
+
[startsOn.field]: {
|
|
344
|
+
[_sequelize().Op.lt]: new Date(startTimestamp + _this3.interval)
|
|
345
|
+
}
|
|
346
|
+
}];
|
|
314
347
|
|
|
315
|
-
if (
|
|
316
|
-
// startsOn exactly equal to now in 1s
|
|
317
|
-
conditions.push({
|
|
318
|
-
[startsOn.field]: {
|
|
319
|
-
[_sequelize().Op.gte]: new Date(startTimestamp),
|
|
320
|
-
[_sequelize().Op.lt]: new Date(startTimestamp + 1000)
|
|
321
|
-
}
|
|
322
|
-
});
|
|
323
|
-
} else {
|
|
348
|
+
if (repeat) {
|
|
324
349
|
// startsOn not after now
|
|
325
350
|
conditions.push({
|
|
326
351
|
[startsOn.field]: {
|
|
@@ -335,32 +360,32 @@ ScheduleModes.set(SCHEDULE_MODE.COLLECTION_FIELD, {
|
|
|
335
360
|
})); // conditions.push(literal(`MOD(CAST(${timestamp} AS BIGINT) - CAST((FLOOR(${tsFn(startsOn.field)}) AS BIGINT) * 1000), ${repeat}) = 0`));
|
|
336
361
|
}
|
|
337
362
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const endTime = Date.parse(endsOn);
|
|
341
|
-
|
|
342
|
-
if (!endTime || endTime <= timestamp) {
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
break;
|
|
363
|
+
if (endsOn) {
|
|
364
|
+
const endTimestamp = getOnTimestampWithOffset(endsOn, now);
|
|
347
365
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
366
|
+
if (!endTimestamp) {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
351
369
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
}
|
|
356
|
-
});
|
|
370
|
+
if (typeof endsOn === 'string') {
|
|
371
|
+
if (endTimestamp <= timestamp) {
|
|
372
|
+
return false;
|
|
357
373
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
374
|
+
} else {
|
|
375
|
+
conditions.push({
|
|
376
|
+
[endsOn.field]: {
|
|
377
|
+
[_sequelize().Op.gte]: new Date(endTimestamp + _this3.interval)
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
}
|
|
363
381
|
}
|
|
382
|
+
} else {
|
|
383
|
+
// startsOn exactly equal to now in 1s
|
|
384
|
+
conditions.push({
|
|
385
|
+
[startsOn.field]: {
|
|
386
|
+
[_sequelize().Op.gte]: new Date(startTimestamp)
|
|
387
|
+
}
|
|
388
|
+
});
|
|
364
389
|
}
|
|
365
390
|
|
|
366
391
|
const _this3$plugin$app$db$ = _this3.plugin.app.db.getCollection(collection),
|
|
@@ -373,12 +398,12 @@ ScheduleModes.set(SCHEDULE_MODE.COLLECTION_FIELD, {
|
|
|
373
398
|
});
|
|
374
399
|
|
|
375
400
|
if (instances.length) {
|
|
376
|
-
console.log(instances.length, 'rows trigger at',
|
|
401
|
+
console.log(instances.length, 'rows trigger at', now);
|
|
377
402
|
}
|
|
378
403
|
|
|
379
404
|
instances.forEach(item => {
|
|
380
405
|
_this3.plugin.trigger(workflow, {
|
|
381
|
-
date,
|
|
406
|
+
date: now,
|
|
382
407
|
data: item.get()
|
|
383
408
|
});
|
|
384
409
|
});
|
|
@@ -387,24 +412,16 @@ ScheduleModes.set(SCHEDULE_MODE.COLLECTION_FIELD, {
|
|
|
387
412
|
|
|
388
413
|
});
|
|
389
414
|
|
|
390
|
-
function
|
|
415
|
+
function matchNext(workflow, now, range = this.cacheCycle) {
|
|
391
416
|
const repeat = workflow.config.repeat; // no repeat means no need to rerun
|
|
392
417
|
// but if in current cycle, should be put in cache
|
|
393
418
|
// no repeat but in current cycle means startsOn has been configured
|
|
394
419
|
// so we need to more info to determine if necessary config items
|
|
395
420
|
|
|
396
|
-
if (
|
|
421
|
+
if (typeof repeat !== 'string') {
|
|
397
422
|
return true;
|
|
398
423
|
}
|
|
399
424
|
|
|
400
|
-
switch (typeof repeat) {
|
|
401
|
-
case 'string':
|
|
402
|
-
break;
|
|
403
|
-
|
|
404
|
-
default:
|
|
405
|
-
return true;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
425
|
const currentDate = new Date(now);
|
|
409
426
|
currentDate.setMilliseconds(-1);
|
|
410
427
|
const timestamp = now.getTime();
|
|
@@ -415,7 +432,7 @@ function nextInCycle(workflow, now) {
|
|
|
415
432
|
|
|
416
433
|
let next = interval.next(); // NOTE: cache all workflows will be matched in current cycle
|
|
417
434
|
|
|
418
|
-
if (next.getTime() - timestamp <=
|
|
435
|
+
if (next.getTime() - timestamp <= range) {
|
|
419
436
|
return true;
|
|
420
437
|
}
|
|
421
438
|
|
|
@@ -452,18 +469,20 @@ class ScheduleTrigger extends _.Trigger {
|
|
|
452
469
|
}
|
|
453
470
|
|
|
454
471
|
init() {
|
|
455
|
-
if (
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
this.timer = setTimeout(() => {
|
|
459
|
-
this.timer = setInterval(this.run, this.interval); // initially trigger
|
|
460
|
-
// this.onTick(now);
|
|
461
|
-
}, // NOTE:
|
|
462
|
-
// try to align to system time on each second starts,
|
|
463
|
-
// after at least 1 second initialized for anything to get ready.
|
|
464
|
-
// so jobs in 2 seconds will be missed at first start.
|
|
465
|
-
1000 - now.getMilliseconds());
|
|
472
|
+
if (this.timer) {
|
|
473
|
+
return;
|
|
466
474
|
}
|
|
475
|
+
|
|
476
|
+
const now = new Date(); // NOTE: assign to this.timer to avoid duplicated initialization
|
|
477
|
+
|
|
478
|
+
this.timer = setTimeout(() => {
|
|
479
|
+
this.timer = setInterval(this.run, this.interval); // initially trigger
|
|
480
|
+
// this.onTick(now);
|
|
481
|
+
}, // NOTE:
|
|
482
|
+
// try to align to system time on each second starts,
|
|
483
|
+
// after at least 1 second initialized for anything to get ready.
|
|
484
|
+
// so jobs in 2 seconds will be missed at first start.
|
|
485
|
+
1000 - now.getMilliseconds());
|
|
467
486
|
}
|
|
468
487
|
|
|
469
488
|
onTick(now) {
|
|
@@ -631,13 +650,10 @@ class ScheduleTrigger extends _.Trigger {
|
|
|
631
650
|
}
|
|
632
651
|
|
|
633
652
|
exports.default = ScheduleTrigger;
|
|
634
|
-
ScheduleTrigger.CacheRules = [
|
|
635
|
-
({
|
|
653
|
+
ScheduleTrigger.CacheRules = [({
|
|
636
654
|
config,
|
|
637
655
|
allExecuted
|
|
638
|
-
}) => config.limit ? allExecuted < config.limit : true, ({
|
|
639
|
-
config
|
|
640
|
-
}) => ['repeat', 'startsOn'].some(key => config[key]), nextInCycle, function (workflow, now) {
|
|
656
|
+
}) => (config.limit ? allExecuted < config.limit : true) && config.startsOn, matchNext, function (workflow, now) {
|
|
641
657
|
const mode = workflow.config.mode;
|
|
642
658
|
const modeHandlers = ScheduleModes.get(mode);
|
|
643
659
|
return modeHandlers.shouldCache.call(this, workflow, now);
|
|
@@ -645,28 +661,6 @@ ScheduleTrigger.CacheRules = [// ({ enabled }) => enabled,
|
|
|
645
661
|
ScheduleTrigger.TriggerRules = [({
|
|
646
662
|
config,
|
|
647
663
|
allExecuted
|
|
648
|
-
}) => config.limit ? allExecuted < config.limit : true, ({
|
|
649
|
-
|
|
650
|
-
}) => ['repeat', 'startsOn'].some(key => config[key]), function (workflow, now) {
|
|
651
|
-
const repeat = workflow.config.repeat;
|
|
652
|
-
|
|
653
|
-
if (typeof repeat !== 'string') {
|
|
654
|
-
return true;
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
const currentDate = new Date(now);
|
|
658
|
-
currentDate.setMilliseconds(-1);
|
|
659
|
-
const timestamp = now.getTime();
|
|
660
|
-
|
|
661
|
-
const interval = _cronParser().default.parseExpression(repeat, {
|
|
662
|
-
currentDate
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
let next = interval.next();
|
|
666
|
-
|
|
667
|
-
if (next.getTime() === timestamp) {
|
|
668
|
-
return true;
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
return false;
|
|
664
|
+
}) => (config.limit ? allExecuted < config.limit : true) && config.startsOn, function (workflow, now) {
|
|
665
|
+
return matchNext.call(this, workflow, now, 0);
|
|
672
666
|
}];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/plugin-workflow",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.6-alpha.1",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"licenses": [
|
|
@@ -10,17 +10,17 @@
|
|
|
10
10
|
}
|
|
11
11
|
],
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@nocobase/actions": "0.7.
|
|
14
|
-
"@nocobase/client": "0.7.
|
|
15
|
-
"@nocobase/database": "0.7.
|
|
16
|
-
"@nocobase/server": "0.7.
|
|
17
|
-
"@nocobase/utils": "0.7.
|
|
13
|
+
"@nocobase/actions": "0.7.6-alpha.1",
|
|
14
|
+
"@nocobase/client": "0.7.6-alpha.1",
|
|
15
|
+
"@nocobase/database": "0.7.6-alpha.1",
|
|
16
|
+
"@nocobase/server": "0.7.6-alpha.1",
|
|
17
|
+
"@nocobase/utils": "0.7.6-alpha.1",
|
|
18
18
|
"cron-parser": "4.4.0",
|
|
19
19
|
"json-templates": "^4.2.0",
|
|
20
20
|
"react-js-cron": "^1.4.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@nocobase/test": "0.7.
|
|
23
|
+
"@nocobase/test": "0.7.6-alpha.1"
|
|
24
24
|
},
|
|
25
|
-
"gitHead": "
|
|
25
|
+
"gitHead": "f20ce011a9ac516dc6aec110979f063a0e63f923"
|
|
26
26
|
}
|