@nocobase/client-v2 3.0.0-alpha.7 → 3.0.0-alpha.9
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/blocks/form/FormBlockModel.d.ts +7 -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/index.mjs +125 -134
- package/lib/index.js +119 -128
- package/package.json +7 -7
- package/src/__tests__/app.test.tsx +15 -6
- package/src/__tests__/settings-center.test.tsx +222 -21
- package/src/components/AppComponents.tsx +4 -4
- 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/blocks/assign-form/AssignFormItemModel.tsx +28 -53
- package/src/flow/models/blocks/form/FormBlockModel.tsx +47 -0
- package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
- package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
- 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/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/DisplayNumberFieldModel.tsx +1 -1
- package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
- package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
- package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -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-center/AdminSettingsLayout.tsx +38 -3
|
@@ -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
|
);
|
|
@@ -72,17 +72,64 @@ const flowKeepMobileHorizontalClassName = css`
|
|
|
72
72
|
function isGridDelegatedStep(flowKey: string, stepKey: string): boolean {
|
|
73
73
|
return !!GRID_DELEGATED_STEP_KEYS[flowKey]?.has(stepKey);
|
|
74
74
|
}
|
|
75
|
+
|
|
76
|
+
interface FormModelWithAvailableData {
|
|
77
|
+
hasAvailableData(): boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface FormModelWithSubmit {
|
|
81
|
+
submit(): Promise<unknown>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function hasAvailableDataCapability(model: object): model is FormModelWithAvailableData {
|
|
85
|
+
return 'hasAvailableData' in model && typeof model.hasAvailableData === 'function';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function hasSubmitCapability(model: object): model is FormModelWithSubmit {
|
|
89
|
+
return 'submit' in model && typeof model.submit === 'function';
|
|
90
|
+
}
|
|
91
|
+
|
|
75
92
|
export class FormBlockModel<
|
|
76
93
|
T extends DefaultCollectionBlockModelStructure = DefaultCollectionBlockModelStructure,
|
|
77
94
|
> extends CollectionBlockModel<T> {
|
|
78
95
|
formValueRuntime?: FormValueRuntime;
|
|
79
96
|
|
|
97
|
+
serialize() {
|
|
98
|
+
return { ...super.serialize(), variableContractType: { type: 'form', use: this.use } };
|
|
99
|
+
}
|
|
100
|
+
|
|
80
101
|
private userModifiedTopLevelFields = new Set<string>();
|
|
81
102
|
|
|
82
103
|
get form() {
|
|
83
104
|
return this.context.form as FormInstance;
|
|
84
105
|
}
|
|
85
106
|
|
|
107
|
+
submitFromRunJs() {
|
|
108
|
+
if (hasAvailableDataCapability(this) && !this.hasAvailableData()) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const submitAction = this.mapSubModels('actions', (action) => action).find((action) =>
|
|
113
|
+
action.getFlow('submitSettings'),
|
|
114
|
+
);
|
|
115
|
+
if (submitAction) {
|
|
116
|
+
return submitAction.onClick(undefined);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const isCoreForm = ['CreateFormModel', 'EditFormModel'].some((modelName) => {
|
|
120
|
+
const ModelClass = this.flowEngine.getModelClass(modelName);
|
|
121
|
+
return ModelClass && this instanceof ModelClass;
|
|
122
|
+
});
|
|
123
|
+
if (this.context.publicFormRuntime || !isCoreForm || !hasSubmitCapability(this)) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this.submit().catch((error: unknown) => {
|
|
128
|
+
this.context.message.error(this.context.t('Save failed'));
|
|
129
|
+
console.error('Form submission error:', error);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
86
133
|
_defaultCustomModelClasses = {
|
|
87
134
|
FormActionGroupModel: 'FormActionGroupModel',
|
|
88
135
|
FormItemModel: 'FormItemModel',
|
|
@@ -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 = {
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { FlowEngine } from '@nocobase/flow-engine';
|
|
11
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
12
|
+
import { CreateFormModel } from '../CreateFormModel';
|
|
13
|
+
import { EditFormModel } from '../EditFormModel';
|
|
14
|
+
import { FormActionModel } from '../FormActionModel';
|
|
15
|
+
|
|
16
|
+
function prepareFormModel<T extends CreateFormModel | EditFormModel>(model: T, publicFormRuntime = false) {
|
|
17
|
+
const engine = new FlowEngine();
|
|
18
|
+
engine.registerModels({ CreateFormModel, EditFormModel });
|
|
19
|
+
model.flowEngine = engine;
|
|
20
|
+
Object.defineProperty(model, 'context', {
|
|
21
|
+
value: {
|
|
22
|
+
publicFormRuntime,
|
|
23
|
+
message: { error: vi.fn() },
|
|
24
|
+
t: (value: string) => value,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
return model;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createSubmitAction() {
|
|
31
|
+
const onClick = vi.fn();
|
|
32
|
+
const action = {
|
|
33
|
+
getFlow: vi.fn((key: string) => (key === 'submitSettings' ? {} : undefined)),
|
|
34
|
+
onClick,
|
|
35
|
+
} as unknown as FormActionModel;
|
|
36
|
+
return { action, onClick };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('FormBlockModel.submitFromRunJs', () => {
|
|
40
|
+
it('dispatches the first action with a submitSettings flow', () => {
|
|
41
|
+
const { action, onClick } = createSubmitAction();
|
|
42
|
+
const { action: secondAction, onClick: secondOnClick } = createSubmitAction();
|
|
43
|
+
const blockModel = prepareFormModel(
|
|
44
|
+
Object.assign(Object.create(CreateFormModel.prototype), {
|
|
45
|
+
mapSubModels: (_key: string, callback: (item: FormActionModel) => FormActionModel) => [
|
|
46
|
+
callback(action),
|
|
47
|
+
callback(secondAction),
|
|
48
|
+
],
|
|
49
|
+
}) as CreateFormModel,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
blockModel.submitFromRunJs();
|
|
53
|
+
|
|
54
|
+
expect(onClick).toHaveBeenCalledWith(undefined);
|
|
55
|
+
expect(secondOnClick).not.toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('does not dispatch an edit submit action without an available record', () => {
|
|
59
|
+
const { action, onClick } = createSubmitAction();
|
|
60
|
+
const blockModel = prepareFormModel(
|
|
61
|
+
Object.assign(Object.create(EditFormModel.prototype), {
|
|
62
|
+
hasAvailableData: () => false,
|
|
63
|
+
mapSubModels: (_key: string, callback: (item: FormActionModel) => FormActionModel) => [callback(action)],
|
|
64
|
+
}) as EditFormModel,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
blockModel.submitFromRunJs();
|
|
68
|
+
|
|
69
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('falls back to the core form submit handler when no submit action exists', () => {
|
|
73
|
+
const submit = vi.fn().mockResolvedValue(undefined);
|
|
74
|
+
const blockModel = prepareFormModel(
|
|
75
|
+
Object.assign(Object.create(CreateFormModel.prototype), {
|
|
76
|
+
mapSubModels: vi.fn(() => []),
|
|
77
|
+
submit,
|
|
78
|
+
}) as CreateFormModel,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
blockModel.submitFromRunJs();
|
|
82
|
+
|
|
83
|
+
expect(submit).toHaveBeenCalledOnce();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('falls back to the core edit submit handler when a record exists', () => {
|
|
87
|
+
const submit = vi.fn().mockResolvedValue(undefined);
|
|
88
|
+
const blockModel = prepareFormModel(
|
|
89
|
+
Object.assign(Object.create(EditFormModel.prototype), {
|
|
90
|
+
hasAvailableData: () => true,
|
|
91
|
+
mapSubModels: vi.fn(() => []),
|
|
92
|
+
submit,
|
|
93
|
+
}) as EditFormModel,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
blockModel.submitFromRunJs();
|
|
97
|
+
|
|
98
|
+
expect(submit).toHaveBeenCalledOnce();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('uses the core fallback for specialized form subclasses', () => {
|
|
102
|
+
class SpecializedCreateFormModel extends CreateFormModel {}
|
|
103
|
+
|
|
104
|
+
const submit = vi.fn().mockResolvedValue(undefined);
|
|
105
|
+
const blockModel = prepareFormModel(
|
|
106
|
+
Object.assign(Object.create(SpecializedCreateFormModel.prototype), {
|
|
107
|
+
mapSubModels: vi.fn(() => []),
|
|
108
|
+
submit,
|
|
109
|
+
}) as SpecializedCreateFormModel,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
blockModel.submitFromRunJs();
|
|
113
|
+
|
|
114
|
+
expect(submit).toHaveBeenCalledOnce();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('does not use the core fallback for public forms without a submit action', () => {
|
|
118
|
+
const submit = vi.fn().mockResolvedValue(undefined);
|
|
119
|
+
const blockModel = prepareFormModel(
|
|
120
|
+
Object.assign(Object.create(CreateFormModel.prototype), {
|
|
121
|
+
mapSubModels: vi.fn(() => []),
|
|
122
|
+
submit,
|
|
123
|
+
}) as CreateFormModel,
|
|
124
|
+
true,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
blockModel.submitFromRunJs();
|
|
128
|
+
|
|
129
|
+
expect(submit).not.toHaveBeenCalled();
|
|
130
|
+
});
|
|
131
|
+
});
|