@nocobase/client-v2 3.0.0-alpha.6 → 3.0.0-alpha.8
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/es/flow/actions/linkageRules.d.ts +24 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +4 -30
- package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
- package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +31 -0
- package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
- package/es/flow/components/field-value-variable/index.d.ts +11 -0
- package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
- package/es/flow/models/blocks/form/FormBlockModel.d.ts +6 -0
- package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
- package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
- package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
- package/es/index.mjs +106 -106
- package/lib/index.js +97 -97
- package/package.json +7 -7
- package/src/__tests__/app.test.tsx +122 -3
- package/src/__tests__/browserChecker.test.ts +37 -0
- package/src/__tests__/settings-shell.test.tsx +27 -1
- package/src/components/AppComponents.tsx +16 -7
- package/src/components/form/table/Table.tsx +81 -6
- package/src/components/form/table/__tests__/Table.columnWidth.test.tsx +433 -0
- package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
- package/src/flow/actions/linkageRules.tsx +25 -7
- package/src/flow/components/FieldAssignValueInput.tsx +38 -621
- package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
- package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +326 -0
- package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +380 -0
- package/src/flow/components/field-value-variable/dateValue.ts +223 -0
- package/src/flow/components/field-value-variable/index.ts +12 -0
- package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
- package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
- package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
- package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +28 -53
- package/src/flow/models/blocks/form/FormBlockModel.tsx +4 -0
- package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +8 -2
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
- package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
- package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
- package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +8 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +46 -0
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +1 -0
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +20 -2
- package/src/settings-app/SettingsShell.tsx +19 -4
|
@@ -8,9 +8,15 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { MultiRecordResource, SingleRecordResource } from '@nocobase/flow-engine';
|
|
11
|
+
import type { AxiosRequestConfig } from 'axios';
|
|
11
12
|
import { dispatchEventDeep } from '../../utils';
|
|
12
13
|
import { resolveAssignFieldValues } from '../blocks/assign-form/assignFieldValuesFlow';
|
|
13
14
|
|
|
15
|
+
type UpdateRecordActionParams = {
|
|
16
|
+
assignedValues?: unknown;
|
|
17
|
+
requestConfig?: AxiosRequestConfig;
|
|
18
|
+
};
|
|
19
|
+
|
|
14
20
|
export async function refreshLinkageRulesAfterUpdate(ctx: any) {
|
|
15
21
|
const blockModel = ctx?.blockModel || ctx?.model?.context?.blockModel || ctx?.model;
|
|
16
22
|
const actionModel = ctx?.model;
|
|
@@ -48,11 +54,11 @@ export async function refreshLinkageRulesAfterUpdate(ctx: any) {
|
|
|
48
54
|
|
|
49
55
|
export async function applyUpdateRecordAction(
|
|
50
56
|
ctx: any,
|
|
51
|
-
params:
|
|
57
|
+
params: UpdateRecordActionParams,
|
|
52
58
|
options?: {
|
|
53
59
|
settingsFlowKey?: string;
|
|
54
60
|
},
|
|
55
|
-
) {
|
|
61
|
+
): Promise<boolean> {
|
|
56
62
|
const settingsFlowKey = options?.settingsFlowKey || 'assignSettings';
|
|
57
63
|
|
|
58
64
|
// 统一接入二次确认:如果启用则弹窗;未配置时默认不启用
|
|
@@ -62,25 +68,31 @@ export async function applyUpdateRecordAction(
|
|
|
62
68
|
|
|
63
69
|
const assignedValues = await resolveAssignFieldValues(ctx, params?.assignedValues, 'UpdateRecordAction');
|
|
64
70
|
if (!assignedValues) {
|
|
65
|
-
return;
|
|
71
|
+
return false;
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
if (!assignedValues || typeof assignedValues !== 'object' || !Object.keys(assignedValues).length) {
|
|
69
75
|
ctx.message.warning(ctx.t('No assigned fields configured'));
|
|
70
|
-
return;
|
|
76
|
+
return false;
|
|
71
77
|
}
|
|
72
78
|
const collection = ctx.collection?.name;
|
|
73
79
|
const filterByTk = ctx.collection?.getFilterByTK?.(ctx.record);
|
|
74
80
|
if (!collection || typeof filterByTk === 'undefined' || filterByTk === null) {
|
|
75
81
|
ctx.message.error(ctx.t('Record is required to perform this action'));
|
|
76
|
-
return;
|
|
82
|
+
return false;
|
|
77
83
|
}
|
|
84
|
+
let updated = false;
|
|
78
85
|
if (ctx.resource instanceof SingleRecordResource) {
|
|
79
86
|
await ctx.resource.save(assignedValues, params.requestConfig);
|
|
87
|
+
updated = true;
|
|
80
88
|
} else if (ctx.resource instanceof MultiRecordResource) {
|
|
81
89
|
await ctx.resource.update(filterByTk, assignedValues, params.requestConfig);
|
|
90
|
+
updated = true;
|
|
91
|
+
}
|
|
92
|
+
if (!updated) {
|
|
93
|
+
return false;
|
|
82
94
|
}
|
|
83
95
|
|
|
84
96
|
await refreshLinkageRulesAfterUpdate(ctx);
|
|
85
|
-
|
|
97
|
+
return true;
|
|
86
98
|
}
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
11
|
-
import { MultiRecordResource } from '@nocobase/flow-engine';
|
|
11
|
+
import { FlowEngine, MultiRecordResource } from '@nocobase/flow-engine';
|
|
12
12
|
import { applyUpdateRecordAction } from '../UpdateRecordActionUtils';
|
|
13
|
+
import { UpdateRecordActionModel } from '../UpdateRecordActionModel';
|
|
13
14
|
import { dispatchEventDeep } from '../../../utils';
|
|
14
15
|
|
|
15
16
|
vi.mock('../../../utils', () => ({
|
|
@@ -21,7 +22,7 @@ describe('UpdateRecordActionModel apply action', () => {
|
|
|
21
22
|
vi.clearAllMocks();
|
|
22
23
|
});
|
|
23
24
|
|
|
24
|
-
it('dispatches paginationChange
|
|
25
|
+
it('dispatches paginationChange and returns success after updating the record', async () => {
|
|
25
26
|
const resource: any = Object.create(MultiRecordResource.prototype);
|
|
26
27
|
resource.update = vi.fn(async () => ({}));
|
|
27
28
|
resource.refresh = vi.fn(async () => {});
|
|
@@ -52,12 +53,13 @@ describe('UpdateRecordActionModel apply action', () => {
|
|
|
52
53
|
t: (value: string) => value,
|
|
53
54
|
};
|
|
54
55
|
|
|
55
|
-
await applyUpdateRecordAction(ctx, {
|
|
56
|
+
const updated = await applyUpdateRecordAction(ctx, {
|
|
56
57
|
assignedValues: {
|
|
57
58
|
marital_status: '已婚',
|
|
58
59
|
},
|
|
59
60
|
});
|
|
60
61
|
|
|
62
|
+
expect(updated).toBe(true);
|
|
61
63
|
expect(resource.update).toHaveBeenCalledWith(1, { marital_status: '已婚' }, undefined);
|
|
62
64
|
expect(resource.refresh).not.toHaveBeenCalled();
|
|
63
65
|
|
|
@@ -67,6 +69,76 @@ describe('UpdateRecordActionModel apply action', () => {
|
|
|
67
69
|
expect(paginationCalls.length).toBeGreaterThan(0);
|
|
68
70
|
expect(paginationCalls.some(([model]: [any]) => model === ctx.model)).toBe(true);
|
|
69
71
|
expect(paginationCalls.some(([model]: [any]) => model === blockModel)).toBe(true);
|
|
70
|
-
expect(ctx.message.success).
|
|
72
|
+
expect(ctx.message.success).not.toHaveBeenCalled();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('runs the configured after-success action only after a successful update', async () => {
|
|
76
|
+
const engine = new FlowEngine();
|
|
77
|
+
const action = new UpdateRecordActionModel({ uid: 'update-record-action', flowEngine: engine } as any);
|
|
78
|
+
action.setStepParams('assignSettings', 'confirm', { enable: false });
|
|
79
|
+
action.setStepParams('assignSettings', 'afterSuccess', {
|
|
80
|
+
successMessage: 'Record updated',
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const resource: any = Object.create(MultiRecordResource.prototype);
|
|
84
|
+
resource.update = vi.fn(async () => ({}));
|
|
85
|
+
const blockModel: any = { uid: 'details-block' };
|
|
86
|
+
const runAction = vi.fn(async () => {});
|
|
87
|
+
const ctx: any = {
|
|
88
|
+
model: action,
|
|
89
|
+
blockModel,
|
|
90
|
+
runAction,
|
|
91
|
+
collection: {
|
|
92
|
+
name: 'users',
|
|
93
|
+
getFilterByTK: vi.fn(() => 1),
|
|
94
|
+
},
|
|
95
|
+
record: { id: 1 },
|
|
96
|
+
resource,
|
|
97
|
+
message: {
|
|
98
|
+
success: vi.fn(),
|
|
99
|
+
warning: vi.fn(),
|
|
100
|
+
error: vi.fn(),
|
|
101
|
+
},
|
|
102
|
+
t: (value: string) => value,
|
|
103
|
+
};
|
|
104
|
+
const handler = action.getFlow('apply')?.getStep('apply')?.serialize().handler;
|
|
105
|
+
|
|
106
|
+
await handler(ctx, { assignedValues: { status: 'active' } });
|
|
107
|
+
|
|
108
|
+
expect(runAction).toHaveBeenNthCalledWith(1, 'confirm', { enable: false });
|
|
109
|
+
expect(runAction).toHaveBeenNthCalledWith(2, 'afterSuccess', {
|
|
110
|
+
successMessage: 'Record updated',
|
|
111
|
+
manualClose: false,
|
|
112
|
+
actionAfterSuccess: 'stay',
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('does not run the after-success action when no fields are assigned', async () => {
|
|
117
|
+
const engine = new FlowEngine();
|
|
118
|
+
const action = new UpdateRecordActionModel({ uid: 'update-record-action-empty', flowEngine: engine } as any);
|
|
119
|
+
const runAction = vi.fn(async () => {});
|
|
120
|
+
const ctx: any = {
|
|
121
|
+
model: action,
|
|
122
|
+
runAction,
|
|
123
|
+
collection: {
|
|
124
|
+
name: 'users',
|
|
125
|
+
getFilterByTK: vi.fn(() => 1),
|
|
126
|
+
},
|
|
127
|
+
record: { id: 1 },
|
|
128
|
+
resource: Object.create(MultiRecordResource.prototype),
|
|
129
|
+
message: {
|
|
130
|
+
success: vi.fn(),
|
|
131
|
+
warning: vi.fn(),
|
|
132
|
+
error: vi.fn(),
|
|
133
|
+
},
|
|
134
|
+
t: (value: string) => value,
|
|
135
|
+
};
|
|
136
|
+
const handler = action.getFlow('apply')?.getStep('apply')?.serialize().handler;
|
|
137
|
+
|
|
138
|
+
await handler(ctx, { assignedValues: {} });
|
|
139
|
+
|
|
140
|
+
expect(runAction).toHaveBeenCalledTimes(1);
|
|
141
|
+
expect(runAction).toHaveBeenCalledWith('confirm', { enable: false });
|
|
142
|
+
expect(ctx.message.warning).toHaveBeenCalledWith('No assigned fields configured');
|
|
71
143
|
});
|
|
72
144
|
});
|
|
@@ -10,23 +10,20 @@
|
|
|
10
10
|
import React from 'react';
|
|
11
11
|
import { Input } from 'antd';
|
|
12
12
|
import { define, observable } from '@formily/reactive';
|
|
13
|
-
import {
|
|
14
|
-
FlowModelRenderer,
|
|
15
|
-
FormItem,
|
|
16
|
-
VariableInput,
|
|
17
|
-
tExpr,
|
|
18
|
-
isVariableExpression,
|
|
19
|
-
parseValueToPath,
|
|
20
|
-
isRunJSValue,
|
|
21
|
-
EditableItemModel,
|
|
22
|
-
jioToJoiSchema,
|
|
23
|
-
} from '@nocobase/flow-engine';
|
|
13
|
+
import { FlowModelRenderer, FormItem, tExpr, EditableItemModel, jioToJoiSchema } from '@nocobase/flow-engine';
|
|
24
14
|
// 无需类型导入(避免未使用的类型)
|
|
25
15
|
import { FormItemModel } from '../form/FormItemModel';
|
|
26
16
|
import { EditFormModel } from '../form/EditFormModel';
|
|
27
17
|
import { customAlphabet as Alphabet } from 'nanoid';
|
|
28
18
|
import { ensureOptionsFromUiSchemaEnumIfAbsent } from '../../../internal/utils/enumOptionsUtils';
|
|
29
19
|
import { RunJSValueEditor } from '../../../components/RunJSValueEditor';
|
|
20
|
+
import {
|
|
21
|
+
DEFAULT_DATE_VARIABLE_COMPONENT_PROPS,
|
|
22
|
+
FieldValueVariableInput,
|
|
23
|
+
getFieldInterface,
|
|
24
|
+
isDateLikeField,
|
|
25
|
+
resolveDateVariableComponentProps,
|
|
26
|
+
} from '../../../components/field-value-variable';
|
|
30
27
|
|
|
31
28
|
type AssignFormTempOriginField = {
|
|
32
29
|
uid?: string;
|
|
@@ -285,7 +282,9 @@ export class AssignFormItemModel extends FormItemModel {
|
|
|
285
282
|
) : null;
|
|
286
283
|
};
|
|
287
284
|
|
|
288
|
-
const NullComponent: React.FC = () =>
|
|
285
|
+
const NullComponent: React.FC = () => (
|
|
286
|
+
<Input placeholder={`<${this.context.t?.('Null') ?? 'Null'}>`} readOnly style={{ width: '100%' }} />
|
|
287
|
+
);
|
|
289
288
|
|
|
290
289
|
const RunJSComponent: React.FC<any> = (inputProps: any) => {
|
|
291
290
|
return (
|
|
@@ -298,46 +297,18 @@ export class AssignFormItemModel extends FormItemModel {
|
|
|
298
297
|
);
|
|
299
298
|
};
|
|
300
299
|
|
|
301
|
-
const
|
|
302
|
-
renderInputComponent: (meta: any) => {
|
|
303
|
-
const firstPath = meta?.paths?.[0];
|
|
304
|
-
if (firstPath === 'constant') return ConstantValueEditor as any;
|
|
305
|
-
if (firstPath === 'null') return NullComponent as any;
|
|
306
|
-
if (firstPath === 'runjs') return RunJSComponent as any;
|
|
307
|
-
return undefined;
|
|
308
|
-
},
|
|
309
|
-
resolveValueFromPath: (item: any) => {
|
|
310
|
-
const firstPath = item?.paths?.[0];
|
|
311
|
-
if (firstPath === 'constant') return '';
|
|
312
|
-
if (firstPath === 'null') return null;
|
|
313
|
-
if (firstPath === 'runjs') return { code: '', version: 'v2' };
|
|
314
|
-
return undefined;
|
|
315
|
-
},
|
|
316
|
-
resolvePathFromValue: (currentValue: any) => {
|
|
317
|
-
if (currentValue === null) return ['null'];
|
|
318
|
-
if (isRunJSValue(currentValue)) return ['runjs'];
|
|
319
|
-
return isVariableExpression(currentValue) ? parseValueToPath(currentValue) : ['constant'];
|
|
320
|
-
},
|
|
321
|
-
} as any;
|
|
322
|
-
|
|
323
|
-
// 合并变量树:在最前面追加“常量/空值”两个选项
|
|
324
|
-
const mergedMetaTree = async () => {
|
|
300
|
+
const baseMetaTree = async () => {
|
|
325
301
|
const getTree = (this.context as any)?.getPropertyMetaTree;
|
|
326
|
-
|
|
327
|
-
return [
|
|
328
|
-
{
|
|
329
|
-
title: tExpr('Constant'),
|
|
330
|
-
name: 'constant',
|
|
331
|
-
type: 'string',
|
|
332
|
-
paths: ['constant'],
|
|
333
|
-
render: ConstantValueEditor,
|
|
334
|
-
},
|
|
335
|
-
{ title: tExpr('Null'), name: 'null', type: 'object', paths: ['null'], render: NullComponent },
|
|
336
|
-
{ title: tExpr('RunJS'), name: 'runjs', type: 'object', paths: ['runjs'], render: RunJSComponent },
|
|
337
|
-
...base,
|
|
338
|
-
];
|
|
302
|
+
return typeof getTree === 'function' ? await getTree() : [];
|
|
339
303
|
};
|
|
340
304
|
|
|
305
|
+
const targetCollectionField = collection?.getField?.(this.fieldPath);
|
|
306
|
+
const targetFieldInterface = getFieldInterface(targetCollectionField);
|
|
307
|
+
const dateLike = isDateLikeField(targetCollectionField, this.fieldPath?.split('.').slice(-1)[0]);
|
|
308
|
+
const dateComponentProps = dateLike
|
|
309
|
+
? resolveDateVariableComponentProps(targetCollectionField, targetFieldInterface)
|
|
310
|
+
: DEFAULT_DATE_VARIABLE_COMPONENT_PROPS;
|
|
311
|
+
|
|
341
312
|
// 计算 label:优先使用配置中的 label,其次集合字段标题,最后回退字段路径
|
|
342
313
|
let labelText = this.props?.label ?? this.fieldPath ?? '';
|
|
343
314
|
const cf = collection?.getField?.(this.fieldPath);
|
|
@@ -357,14 +328,18 @@ export class AssignFormItemModel extends FormItemModel {
|
|
|
357
328
|
const formValue = formBindingProps?.__assign_value__;
|
|
358
329
|
const mergedValue = typeof formValue === 'undefined' ? this.assignValue : formValue;
|
|
359
330
|
return (
|
|
360
|
-
<
|
|
331
|
+
<FieldValueVariableInput
|
|
361
332
|
value={mergedValue}
|
|
362
|
-
onChange={(v
|
|
333
|
+
onChange={(v) => {
|
|
363
334
|
this.assignValue = v;
|
|
364
335
|
formBindingProps?.__assign_trigger__?.(v);
|
|
365
336
|
}}
|
|
366
|
-
|
|
367
|
-
|
|
337
|
+
baseMetaTree={baseMetaTree}
|
|
338
|
+
constantComponent={ConstantValueEditor}
|
|
339
|
+
nullComponent={NullComponent}
|
|
340
|
+
runJSComponent={RunJSComponent}
|
|
341
|
+
isDateLikeField={dateLike}
|
|
342
|
+
dateComponentProps={dateComponentProps}
|
|
368
343
|
clearValue={''}
|
|
369
344
|
/>
|
|
370
345
|
);
|
|
@@ -77,6 +77,10 @@ export class FormBlockModel<
|
|
|
77
77
|
> extends CollectionBlockModel<T> {
|
|
78
78
|
formValueRuntime?: FormValueRuntime;
|
|
79
79
|
|
|
80
|
+
serialize() {
|
|
81
|
+
return { ...super.serialize(), variableContractType: { type: 'form', use: this.use } };
|
|
82
|
+
}
|
|
83
|
+
|
|
80
84
|
private userModifiedTopLevelFields = new Set<string>();
|
|
81
85
|
|
|
82
86
|
get form() {
|
|
@@ -21,6 +21,10 @@ export type DefaultFormGridStructure = {
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
export class FormGridModel<T extends DefaultFormGridStructure = DefaultFormGridStructure> extends GridModel<T> {
|
|
24
|
+
serialize() {
|
|
25
|
+
return { ...super.serialize(), variableContractType: { type: 'formGrid', use: this.use } };
|
|
26
|
+
}
|
|
27
|
+
|
|
24
28
|
itemFallback = (<Skeleton.Input block size="small" style={{ marginBottom: '0.5rem' }} />);
|
|
25
29
|
itemSettingsMenuLevel = 2;
|
|
26
30
|
itemFlowSettings = {
|
|
@@ -465,11 +465,17 @@ describe('FormBlockModel (form/formValues injection & server resolve anchors)',
|
|
|
465
465
|
|
|
466
466
|
it('builds non-empty contextParams for ctx.formValues.* deep association path', async () => {
|
|
467
467
|
const model = await setupFormModel();
|
|
468
|
+
const sessionPayload = Buffer.from(JSON.stringify({ userId: 1, signInTime: 'form-record-slots' })).toString(
|
|
469
|
+
'base64url',
|
|
470
|
+
);
|
|
468
471
|
// 注入 api mock 到引擎上下文,拦截 variables:resolve 的请求
|
|
469
472
|
const api = {
|
|
473
|
+
auth: { token: `test.${sessionPayload}.sig` },
|
|
470
474
|
request: vi.fn(async (config: any) => {
|
|
471
|
-
const
|
|
472
|
-
const batch =
|
|
475
|
+
const requestValues = config?.data?.values || {};
|
|
476
|
+
const batch = requestValues.batch || [];
|
|
477
|
+
expect(batch[0]?.rd).toEqual(expect.any(String));
|
|
478
|
+
expect(batch[0]?.template).toEqual({ who: '{{ ctx.formValues.assignees.org.name }}' });
|
|
473
479
|
const cp = batch[0]?.contextParams || {};
|
|
474
480
|
const keys = Object.keys(cp).sort();
|
|
475
481
|
// 聚合为单键,不再使用索引键
|