@nocobase/client-v2 2.1.0-beta.26 → 2.1.0-beta.29
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/components/form/JsonTextArea.d.ts +18 -0
- package/es/components/index.d.ts +1 -0
- package/es/flow/actions/dateRangeLimit.d.ts +9 -0
- package/es/flow/actions/index.d.ts +1 -0
- package/es/flow/components/code-editor/types.d.ts +1 -0
- package/es/flow/models/base/PageModel/PageModel.d.ts +4 -0
- package/es/flow/models/base/PageModel/RootPageModel.d.ts +9 -0
- package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +15 -6
- package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +7 -0
- package/es/flow/models/blocks/shared/filterOperators.d.ts +9 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +2 -0
- package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +20 -0
- package/es/flow/models/fields/JSEditableFieldModel.d.ts +4 -0
- package/es/flow-compat/data.d.ts +9 -2
- package/es/flow-compat/index.d.ts +1 -1
- package/es/index.d.ts +1 -1
- package/es/index.mjs +97 -90
- package/lib/index.js +99 -92
- package/package.json +6 -5
- package/src/BaseApplication.tsx +1 -1
- package/src/__tests__/app.test.tsx +23 -6
- package/src/components/form/JsonTextArea.tsx +129 -0
- package/src/components/index.ts +1 -0
- package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +478 -0
- package/src/flow/actions/__tests__/pattern.test.ts +190 -0
- package/src/flow/actions/dateRangeLimit.tsx +66 -0
- package/src/flow/actions/index.ts +1 -0
- package/src/flow/actions/linkageRules.tsx +117 -19
- package/src/flow/actions/openView.tsx +2 -1
- package/src/flow/actions/pattern.tsx +25 -2
- package/src/flow/actions/titleField.tsx +8 -3
- package/src/flow/admin-shell/AdminLayoutRouteCoordinator.ts +7 -1
- package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +117 -0
- package/src/flow/components/FieldAssignValueInput.tsx +1 -0
- package/src/flow/components/code-editor/__tests__/linter.test.ts +18 -0
- package/src/flow/components/code-editor/__tests__/runjsDiagnostics.test.ts +23 -0
- package/src/flow/components/code-editor/index.tsx +18 -17
- package/src/flow/components/code-editor/linter.ts +222 -158
- package/src/flow/components/code-editor/runjsDiagnostics.ts +161 -97
- package/src/flow/components/code-editor/types.ts +1 -0
- package/src/flow/components/filter/LinkageFilterItem.tsx +6 -5
- package/src/flow/components/filter/VariableFilterItem.tsx +14 -13
- package/src/flow/components/filter/__tests__/LinkageFilterItem.test.tsx +33 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +48 -5
- package/src/flow/internal/utils/__tests__/titleFieldQuickSync.test.ts +1 -0
- package/src/flow/internal/utils/titleFieldQuickSync.ts +2 -2
- package/src/flow/models/actions/FilterActionModel.tsx +17 -9
- package/src/flow/models/base/PageModel/PageModel.tsx +15 -3
- package/src/flow/models/base/PageModel/RootPageModel.tsx +37 -2
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +73 -0
- package/src/flow/models/base/PageModel/__tests__/RootPageModel.test.ts +116 -0
- package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +200 -36
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +270 -1
- package/src/flow/models/blocks/filter-form/__tests__/customFieldOperators.test.tsx +23 -0
- package/src/flow/models/blocks/filter-form/customFieldOperators.ts +12 -1
- package/src/flow/models/blocks/filter-form/fields/FieldComponentProps.tsx +22 -8
- package/src/flow/models/blocks/filter-form/fields/__tests__/FilterFormCustomFieldModel.recordSelect.test.tsx +18 -0
- package/src/flow/models/blocks/filter-manager/FilterManager.ts +51 -1
- package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +75 -0
- package/src/flow/models/blocks/form/FormItemModel.tsx +48 -28
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +167 -1
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +103 -11
- package/src/flow/models/blocks/shared/filterOperators.ts +14 -0
- package/src/flow/models/blocks/table/TableBlockModel.tsx +19 -3
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +27 -3
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +47 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +42 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableField.refresh.test.tsx +122 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +2 -0
- package/src/flow/models/fields/ClickableFieldModel.tsx +21 -9
- package/src/flow/models/fields/DateTimeFieldModel/DateOnlyFieldModel.tsx +9 -0
- package/src/flow/models/fields/DateTimeFieldModel/DateTimeFieldModel.tsx +4 -0
- package/src/flow/models/fields/DateTimeFieldModel/DateTimeNoTzFieldModel.tsx +9 -0
- package/src/flow/models/fields/DateTimeFieldModel/DateTimeTzFieldModel.tsx +9 -0
- package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +242 -0
- package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +152 -0
- package/src/flow/models/fields/DividerItemModel.tsx +30 -15
- package/src/flow/models/fields/JSEditableFieldModel.tsx +110 -14
- package/src/flow/models/fields/__tests__/ClickableFieldModel.test.ts +87 -0
- package/src/flow/models/fields/__tests__/JSEditableFieldModel.test.tsx +210 -0
- package/src/flow-compat/data.ts +25 -3
- package/src/flow-compat/index.ts +7 -1
- package/src/index.ts +1 -1
|
@@ -22,6 +22,7 @@ import { FieldModel } from '../../base/FieldModel';
|
|
|
22
22
|
import { DetailsItemModel } from '../details/DetailsItemModel';
|
|
23
23
|
import { EditFormModel } from './EditFormModel';
|
|
24
24
|
import _ from 'lodash';
|
|
25
|
+
import { Tooltip } from 'antd';
|
|
25
26
|
import { coerceForToOneField } from '../../../internal/utils/associationValueCoercion';
|
|
26
27
|
import { buildDynamicNamePath } from './dynamicNamePath';
|
|
27
28
|
|
|
@@ -99,54 +100,63 @@ export class FormItemModel<T extends DefaultStructure = DefaultStructure> extend
|
|
|
99
100
|
|
|
100
101
|
renderItem() {
|
|
101
102
|
const fieldModel = this.subModels.field as FieldModel;
|
|
102
|
-
// 行索引(来自数组子表单)
|
|
103
103
|
const idx = this.context.fieldIndex;
|
|
104
104
|
const fieldKey = this.context.fieldKey;
|
|
105
105
|
const parentFieldPathArray = this.parent?.context.fieldPathArray || [];
|
|
106
|
+
const isHiddenReservedValuePreview =
|
|
107
|
+
!!this.context.flowSettingsEnabled && !!this.props.hidden && !this.hidden && !this.forbidden;
|
|
106
108
|
|
|
107
|
-
// 嵌套场景下继续传透,为字段子模型创建 fork
|
|
108
109
|
const modelForRender =
|
|
109
|
-
idx != null
|
|
110
|
+
idx != null || isHiddenReservedValuePreview
|
|
110
111
|
? (() => {
|
|
111
|
-
const
|
|
112
|
-
fork.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
get: () => fieldKey,
|
|
117
|
-
});
|
|
118
|
-
if (this.context.currentObject) {
|
|
119
|
-
fork.context.defineProperty('currentObject', {
|
|
120
|
-
get: () => this.context.currentObject,
|
|
112
|
+
const forkKey = isHiddenReservedValuePreview ? `${this.uid}:config-visible` : `${fieldKey}`;
|
|
113
|
+
const fork = fieldModel.createFork({}, forkKey);
|
|
114
|
+
if (idx != null) {
|
|
115
|
+
fork.context.defineProperty('fieldIndex', {
|
|
116
|
+
get: () => idx,
|
|
121
117
|
});
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (this.context.item) {
|
|
125
|
-
const { value: _value, ...rest } = (itemOptions || {}) as any;
|
|
126
|
-
fork.context.defineProperty('item', {
|
|
127
|
-
...rest,
|
|
128
|
-
get: () => this.context.item,
|
|
129
|
-
cache: false,
|
|
118
|
+
fork.context.defineProperty('fieldKey', {
|
|
119
|
+
get: () => fieldKey,
|
|
130
120
|
});
|
|
121
|
+
if (this.context.currentObject) {
|
|
122
|
+
fork.context.defineProperty('currentObject', {
|
|
123
|
+
get: () => this.context.currentObject,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
const itemOptions = this.context.getPropertyOptions('item');
|
|
127
|
+
if (this.context.item) {
|
|
128
|
+
const { value: _value, ...rest } = (itemOptions || {}) as any;
|
|
129
|
+
fork.context.defineProperty('item', {
|
|
130
|
+
...rest,
|
|
131
|
+
get: () => this.context.item,
|
|
132
|
+
cache: false,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (this.context.pattern) {
|
|
136
|
+
fork.context.defineProperty('pattern', {
|
|
137
|
+
get: () => this.context.pattern,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
131
140
|
}
|
|
132
|
-
if (
|
|
133
|
-
fork.
|
|
134
|
-
get: () => this.context.pattern,
|
|
135
|
-
});
|
|
141
|
+
if (isHiddenReservedValuePreview) {
|
|
142
|
+
fork.setProps({ hidden: false });
|
|
136
143
|
}
|
|
137
144
|
return fork;
|
|
138
145
|
})()
|
|
139
146
|
: fieldModel;
|
|
140
147
|
const mergedProps = this.context.pattern ? { ...this.props, pattern: this.context.pattern } : this.props;
|
|
141
|
-
const { initialValue, ...mergedPropsWithoutInitial } = mergedProps as any;
|
|
148
|
+
const { initialValue, hidden, ...mergedPropsWithoutInitial } = mergedProps as any;
|
|
149
|
+
const formItemProps = isHiddenReservedValuePreview
|
|
150
|
+
? mergedPropsWithoutInitial
|
|
151
|
+
: { hidden, ...mergedPropsWithoutInitial };
|
|
142
152
|
const fieldPath = buildDynamicNamePath(this.props.name, idx);
|
|
143
153
|
this.context.defineProperty('fieldPathArray', {
|
|
144
154
|
value: [...parentFieldPathArray, ..._.castArray(fieldPath)],
|
|
145
155
|
});
|
|
146
156
|
const record = this.context.item?.value || this.context.record;
|
|
147
|
-
|
|
157
|
+
const content = (
|
|
148
158
|
<FormItem
|
|
149
|
-
{...
|
|
159
|
+
{...formItemProps}
|
|
150
160
|
name={fieldPath}
|
|
151
161
|
validateFirst={true}
|
|
152
162
|
disabled={
|
|
@@ -158,6 +168,16 @@ export class FormItemModel<T extends DefaultStructure = DefaultStructure> extend
|
|
|
158
168
|
<FieldModelRenderer model={modelForRender} name={fieldPath} />
|
|
159
169
|
</FormItem>
|
|
160
170
|
);
|
|
171
|
+
|
|
172
|
+
if (isHiddenReservedValuePreview) {
|
|
173
|
+
return (
|
|
174
|
+
<Tooltip title={this.context.t('The field is hidden and only visible when the UI Editor is active')}>
|
|
175
|
+
<div style={{ opacity: 0.3 }}>{content}</div>
|
|
176
|
+
</Tooltip>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return content;
|
|
161
181
|
}
|
|
162
182
|
}
|
|
163
183
|
|
|
@@ -159,12 +159,141 @@ describe('FormValueRuntime (default rules)', () => {
|
|
|
159
159
|
await runtime.setFormValues(fieldCtx, [{ path: ['b'], value: 'Y' }], { source: 'user' });
|
|
160
160
|
await waitFor(() => expect(formStub.getFieldValue(['a'])).toBe('Y'));
|
|
161
161
|
|
|
162
|
+
// change dependency again -> default keeps following while target is still the last default
|
|
163
|
+
await runtime.setFormValues(fieldCtx, [{ path: ['b'], value: 'Z' }], { source: 'user' });
|
|
164
|
+
await waitFor(() => expect(formStub.getFieldValue(['a'])).toBe('Z'));
|
|
165
|
+
|
|
162
166
|
// user changes target -> default should be disabled permanently
|
|
163
167
|
await runtime.setFormValues(fieldCtx, [{ path: ['a'], value: 'user' }], { source: 'user' });
|
|
164
|
-
await runtime.setFormValues(fieldCtx, [{ path: ['b'], value: '
|
|
168
|
+
await runtime.setFormValues(fieldCtx, [{ path: ['b'], value: 'W' }], { source: 'user' });
|
|
165
169
|
expect(formStub.getFieldValue(['a'])).toBe('user');
|
|
166
170
|
});
|
|
167
171
|
|
|
172
|
+
it('allows direct default patches to keep following until target is explicitly changed', async () => {
|
|
173
|
+
const engineEmitter = new EventEmitter();
|
|
174
|
+
const blockEmitter = new EventEmitter();
|
|
175
|
+
const formStub = createFormStub({ roleUid: 'role-uid-1', roleName: '' });
|
|
176
|
+
|
|
177
|
+
const blockModel: any = {
|
|
178
|
+
uid: 'form-direct-default-patch',
|
|
179
|
+
flowEngine: { emitter: engineEmitter },
|
|
180
|
+
emitter: blockEmitter,
|
|
181
|
+
dispatchEvent: vi.fn(),
|
|
182
|
+
getAclActionName: () => 'create',
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
|
|
186
|
+
runtime.mount({ sync: true });
|
|
187
|
+
|
|
188
|
+
const blockCtx = createFieldContext(runtime);
|
|
189
|
+
blockModel.context = blockCtx;
|
|
190
|
+
|
|
191
|
+
expect(runtime.canApplyDefaultValuePatch(['roleName'], 'role-uid-1')).toBe(true);
|
|
192
|
+
await runtime.setFormValues(blockCtx, [{ path: ['roleName'], value: 'role-uid-1' }], {
|
|
193
|
+
source: 'linkage',
|
|
194
|
+
txId: 'tx-linkage-1',
|
|
195
|
+
linkageTxId: 'tx-linkage-1',
|
|
196
|
+
});
|
|
197
|
+
runtime.recordDefaultValuePatch(['roleName'], 'role-uid-1');
|
|
198
|
+
|
|
199
|
+
await runtime.setFormValues(blockCtx, [{ path: ['roleUid'], value: 'role-uid-2' }], { source: 'user' });
|
|
200
|
+
expect(runtime.canApplyDefaultValuePatch(['roleName'], 'role-uid-2')).toBe(true);
|
|
201
|
+
await runtime.setFormValues(blockCtx, [{ path: ['roleName'], value: 'role-uid-2' }], {
|
|
202
|
+
source: 'linkage',
|
|
203
|
+
txId: 'tx-linkage-2',
|
|
204
|
+
linkageTxId: 'tx-linkage-2',
|
|
205
|
+
});
|
|
206
|
+
runtime.recordDefaultValuePatch(['roleName'], 'role-uid-2');
|
|
207
|
+
|
|
208
|
+
await runtime.setFormValues(blockCtx, [{ path: ['roleUid'], value: 'role-uid-3' }], { source: 'user' });
|
|
209
|
+
expect(runtime.canApplyDefaultValuePatch(['roleName'], 'role-uid-3')).toBe(true);
|
|
210
|
+
|
|
211
|
+
await runtime.setFormValues(blockCtx, [{ path: ['roleName'], value: 'manual' }], { source: 'user' });
|
|
212
|
+
expect(runtime.canApplyDefaultValuePatch(['roleName'], 'role-uid-4')).toBe(false);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('keeps direct default patches active when an unchanged default value is carried by a row change', async () => {
|
|
216
|
+
const engineEmitter = new EventEmitter();
|
|
217
|
+
const blockEmitter = new EventEmitter();
|
|
218
|
+
const formStub = createFormStub({ roles: [{ uid: '1', title: '' }] });
|
|
219
|
+
|
|
220
|
+
const blockModel: any = {
|
|
221
|
+
uid: 'form-direct-default-patch-row',
|
|
222
|
+
flowEngine: { emitter: engineEmitter },
|
|
223
|
+
emitter: blockEmitter,
|
|
224
|
+
dispatchEvent: vi.fn(),
|
|
225
|
+
getAclActionName: () => 'create',
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
|
|
229
|
+
runtime.mount({ sync: true });
|
|
230
|
+
|
|
231
|
+
const blockCtx = createFieldContext(runtime);
|
|
232
|
+
blockModel.context = blockCtx;
|
|
233
|
+
|
|
234
|
+
expect(runtime.canApplyDefaultValuePatch(['roles', 0, 'title'], '1')).toBe(true);
|
|
235
|
+
await runtime.setFormValues(blockCtx, [{ path: ['roles', 0, 'title'], value: '1' }], {
|
|
236
|
+
source: 'linkage',
|
|
237
|
+
txId: 'tx-linkage-row-1',
|
|
238
|
+
linkageTxId: 'tx-linkage-row-1',
|
|
239
|
+
});
|
|
240
|
+
runtime.recordDefaultValuePatch(['roles', 0, 'title'], '1');
|
|
241
|
+
|
|
242
|
+
lodashSet((runtime as any).valuesMirror, ['roles', 0, 'title'], undefined);
|
|
243
|
+
runtime.handleFormFieldsChange([{ name: ['roles', 0, 'title'], touched: true } as any]);
|
|
244
|
+
expect((runtime as any).findExplicitHit('roles[0].title')).toBeNull();
|
|
245
|
+
expect(runtime.canApplyDefaultValuePatch(['roles', 0, 'title'], '12')).toBe(true);
|
|
246
|
+
|
|
247
|
+
(runtime as any).markExplicit('roles');
|
|
248
|
+
expect((runtime as any).findExplicitHit('roles[0].title')).toBeNull();
|
|
249
|
+
expect(runtime.canApplyDefaultValuePatch(['roles', 0, 'title'], '12')).toBe(true);
|
|
250
|
+
|
|
251
|
+
lodashSet((runtime as any).valuesMirror, ['roles', 0, 'title'], undefined);
|
|
252
|
+
|
|
253
|
+
lodashSet((formStub as any).__store, ['roles', 0, 'uid'], '12');
|
|
254
|
+
runtime.handleFormValuesChange({ roles: formStub.getFieldValue(['roles']) }, formStub.getFieldsValue());
|
|
255
|
+
|
|
256
|
+
expect((runtime as any).findExplicitHit('roles[0].title')).toBeNull();
|
|
257
|
+
expect(runtime.canApplyDefaultValuePatch(['roles', 0, 'title'], '12')).toBe(true);
|
|
258
|
+
await runtime.setFormValues(blockCtx, [{ path: ['roles', 0, 'title'], value: '12' }], {
|
|
259
|
+
source: 'linkage',
|
|
260
|
+
txId: 'tx-linkage-row-2',
|
|
261
|
+
linkageTxId: 'tx-linkage-row-2',
|
|
262
|
+
});
|
|
263
|
+
runtime.recordDefaultValuePatch(['roles', 0, 'title'], '12');
|
|
264
|
+
expect(formStub.getFieldValue(['roles', 0, 'title'])).toBe('12');
|
|
265
|
+
|
|
266
|
+
await runtime.setFormValues(blockCtx, [{ path: ['roles', 0, 'title'], value: 'manual' }], { source: 'user' });
|
|
267
|
+
expect(runtime.canApplyDefaultValuePatch(['roles', 0, 'title'], '13')).toBe(false);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('does not mark omitted sibling fields from partial top-level changedValues as explicit', async () => {
|
|
271
|
+
const engineEmitter = new EventEmitter();
|
|
272
|
+
const blockEmitter = new EventEmitter();
|
|
273
|
+
const formStub = createFormStub({
|
|
274
|
+
roles: [{ __is_new__: true, __index__: 'row-1', name: '1', title: '1' }],
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const blockModel: any = {
|
|
278
|
+
uid: 'form-direct-default-patch-partial-row',
|
|
279
|
+
flowEngine: { emitter: engineEmitter },
|
|
280
|
+
emitter: blockEmitter,
|
|
281
|
+
dispatchEvent: vi.fn(),
|
|
282
|
+
getAclActionName: () => 'create',
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
|
|
286
|
+
runtime.mount({ sync: true });
|
|
287
|
+
|
|
288
|
+
runtime.recordDefaultValuePatch(['roles', 0, 'title'], '1');
|
|
289
|
+
|
|
290
|
+
lodashSet((formStub as any).__store, ['roles', 0, 'name'], '12');
|
|
291
|
+
runtime.handleFormValuesChange({ roles: [{ name: '12' }] }, formStub.getFieldsValue());
|
|
292
|
+
|
|
293
|
+
expect((runtime as any).findExplicitHit('roles[0].title')).toBeNull();
|
|
294
|
+
expect(runtime.canApplyDefaultValuePatch(['roles', 0, 'title'], '12')).toBe(true);
|
|
295
|
+
});
|
|
296
|
+
|
|
168
297
|
it('handles onFieldsChange name as string and triggers default recompute', async () => {
|
|
169
298
|
const engineEmitter = new EventEmitter();
|
|
170
299
|
const blockEmitter = new EventEmitter();
|
|
@@ -3267,4 +3396,41 @@ describe('FormValueRuntime (form assign rules)', () => {
|
|
|
3267
3396
|
expect(payload.txId).toBe('tx-current');
|
|
3268
3397
|
expect(payload.linkageTxId).toBe('tx-root');
|
|
3269
3398
|
});
|
|
3399
|
+
|
|
3400
|
+
it('does not mark linkage writes explicit so later user edits can keep following default linkage', async () => {
|
|
3401
|
+
const engineEmitter = new EventEmitter();
|
|
3402
|
+
const blockEmitter = new EventEmitter();
|
|
3403
|
+
const formStub = createFormStub({ roleUid: 'role-uid-1', roleName: '' });
|
|
3404
|
+
|
|
3405
|
+
const blockModel: any = {
|
|
3406
|
+
uid: 'form-linkage-linkage-not-explicit',
|
|
3407
|
+
flowEngine: { emitter: engineEmitter },
|
|
3408
|
+
emitter: blockEmitter,
|
|
3409
|
+
dispatchEvent: vi.fn(),
|
|
3410
|
+
getAclActionName: () => 'create',
|
|
3411
|
+
};
|
|
3412
|
+
|
|
3413
|
+
const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
|
|
3414
|
+
runtime.mount({ sync: true });
|
|
3415
|
+
|
|
3416
|
+
const blockCtx = createFieldContext(runtime);
|
|
3417
|
+
blockModel.context = blockCtx;
|
|
3418
|
+
|
|
3419
|
+
await runtime.setFormValues(blockCtx, [{ path: ['roleName'], value: 'role-uid-1' }], {
|
|
3420
|
+
source: 'linkage',
|
|
3421
|
+
txId: 'tx-linkage-1',
|
|
3422
|
+
linkageTxId: 'tx-linkage-1',
|
|
3423
|
+
linkageScopeDepth: 0,
|
|
3424
|
+
});
|
|
3425
|
+
|
|
3426
|
+
expect(formStub.getFieldValue(['roleName'])).toBe('role-uid-1');
|
|
3427
|
+
expect((runtime as any).findExplicitHit('roleName')).toBeNull();
|
|
3428
|
+
|
|
3429
|
+
await runtime.setFormValues(blockCtx, [{ path: ['roleUid'], value: 'role-uid-2' }], {
|
|
3430
|
+
source: 'user',
|
|
3431
|
+
txId: 'tx-user-1',
|
|
3432
|
+
});
|
|
3433
|
+
expect(formStub.getFieldValue(['roleUid'])).toBe('role-uid-2');
|
|
3434
|
+
expect((runtime as any).findExplicitHit('roleName')).toBeNull();
|
|
3435
|
+
});
|
|
3270
3436
|
});
|
|
@@ -137,6 +137,42 @@ export class FormValueRuntime {
|
|
|
137
137
|
return this.getForm().getFieldsValue(true);
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
canApplyDefaultValuePatch(namePath: NamePath, resolved: any) {
|
|
141
|
+
if (!namePath?.length) return false;
|
|
142
|
+
if (typeof resolved === 'undefined') return false;
|
|
143
|
+
|
|
144
|
+
const pathKey = namePathToPathKey(namePath);
|
|
145
|
+
const current = this.getFormValueAtPath(namePath);
|
|
146
|
+
const last = this.lastDefaultValueByPathKey.get(pathKey);
|
|
147
|
+
const nextSnapshot = isObservable(resolved) ? toJS(resolved) : resolved;
|
|
148
|
+
const currentSnapshot = isObservable(current) ? toJS(current) : current;
|
|
149
|
+
const currentEqualsLastDefault = typeof last !== 'undefined' && _.isEqual(currentSnapshot, last);
|
|
150
|
+
const explicitHit = this.findExplicitHit(pathKey);
|
|
151
|
+
|
|
152
|
+
if (explicitHit && !currentEqualsLastDefault) return false;
|
|
153
|
+
|
|
154
|
+
const canOverwrite = isEmptyValue(current) || currentEqualsLastDefault;
|
|
155
|
+
if (!canOverwrite && _.isEqual(current, nextSnapshot)) {
|
|
156
|
+
this.lastDefaultValueByPathKey.set(pathKey, nextSnapshot);
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return canOverwrite;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
recordDefaultValuePatch(namePath: NamePath, value?: any) {
|
|
164
|
+
if (!namePath?.length) return;
|
|
165
|
+
const pathKey = namePathToPathKey(namePath);
|
|
166
|
+
const snapshot =
|
|
167
|
+
arguments.length >= 2 ? (isObservable(value) ? toJS(value) : value) : this.getFormValueAtPath(namePath);
|
|
168
|
+
this.lastDefaultValueByPathKey.set(pathKey, snapshot);
|
|
169
|
+
const current = this.getFormValueAtPath(namePath);
|
|
170
|
+
const currentSnapshot = isObservable(current) ? toJS(current) : current;
|
|
171
|
+
if (_.isEqual(currentSnapshot, snapshot)) {
|
|
172
|
+
this.clearExplicitForDefaultPatch(pathKey);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
140
176
|
private getFormValueAtPath(namePath: NamePath) {
|
|
141
177
|
const form: any = this.getForm?.();
|
|
142
178
|
if (form && typeof form.getFieldValue === 'function') {
|
|
@@ -247,11 +283,7 @@ export class FormValueRuntime {
|
|
|
247
283
|
let bumpedWriteSeq = false;
|
|
248
284
|
for (const field of changedFields || []) {
|
|
249
285
|
const name = field?.name;
|
|
250
|
-
const namePath
|
|
251
|
-
? (name as NamePath)
|
|
252
|
-
: typeof name === 'string' || typeof name === 'number'
|
|
253
|
-
? ([name] as NamePath)
|
|
254
|
-
: null;
|
|
286
|
+
const namePath = this.normalizeObservedNamePath(name);
|
|
255
287
|
if (!namePath?.length) continue;
|
|
256
288
|
const nextValue = form.getFieldValue(namePath as any);
|
|
257
289
|
const prevValue = _.get(this.valuesMirror, namePath);
|
|
@@ -265,8 +297,11 @@ export class FormValueRuntime {
|
|
|
265
297
|
const isMeaningfulTouched =
|
|
266
298
|
field?.touched === true && !this.shouldIgnoreSyntheticTouchedInit(namePath, prevValue, nextValue);
|
|
267
299
|
if (isMeaningfulTouched) {
|
|
268
|
-
|
|
269
|
-
|
|
300
|
+
const pathKey = namePathToPathKey(namePath);
|
|
301
|
+
if (!this.shouldKeepDefaultPathValueEnabled(namePath, nextValue)) {
|
|
302
|
+
touchedChangedPathKeys.add(pathKey);
|
|
303
|
+
hasMeaningfulTouchedChange = true;
|
|
304
|
+
}
|
|
270
305
|
}
|
|
271
306
|
}
|
|
272
307
|
|
|
@@ -526,6 +561,10 @@ export class FormValueRuntime {
|
|
|
526
561
|
}
|
|
527
562
|
|
|
528
563
|
private getObservedChangedValue(path: NamePath, changedValues: any, snapshot: any) {
|
|
564
|
+
if (_.has(snapshot, path as any)) {
|
|
565
|
+
return _.get(snapshot, path as any);
|
|
566
|
+
}
|
|
567
|
+
|
|
529
568
|
if (path?.length === 1) {
|
|
530
569
|
const key = path[0];
|
|
531
570
|
if (
|
|
@@ -546,6 +585,7 @@ export class FormValueRuntime {
|
|
|
546
585
|
|
|
547
586
|
let observed = snapshot;
|
|
548
587
|
for (const key of Object.keys(changedValues)) {
|
|
588
|
+
if (_.has(observed, [key])) continue;
|
|
549
589
|
if (observed === snapshot) {
|
|
550
590
|
observed = Array.isArray(snapshot) ? [...snapshot] : { ...snapshot };
|
|
551
591
|
}
|
|
@@ -573,7 +613,15 @@ export class FormValueRuntime {
|
|
|
573
613
|
this.lastObservedChangedPaths = null;
|
|
574
614
|
this.lastObservedSource = null;
|
|
575
615
|
|
|
576
|
-
|
|
616
|
+
// 子表格的 changedValues 可能只包含局部行对象,diff 必须以 form 当前完整快照为准,
|
|
617
|
+
// 否则缺失的 sibling 字段会被误判为用户清空。
|
|
618
|
+
const formSnapshot = this.getFormValuesSnapshot();
|
|
619
|
+
const rawSnapshot =
|
|
620
|
+
formSnapshot && typeof formSnapshot === 'object'
|
|
621
|
+
? formSnapshot
|
|
622
|
+
: allValues && typeof allValues === 'object'
|
|
623
|
+
? allValues
|
|
624
|
+
: changedValues;
|
|
577
625
|
const snapshot = this.getObservedSnapshot(changedValues, rawSnapshot);
|
|
578
626
|
this.reconcileArrayItemState([...rawChangedPaths, ...changedValuePaths], changedValues, snapshot);
|
|
579
627
|
this.pruneDeletedArrayItemState(snapshot);
|
|
@@ -602,8 +650,16 @@ export class FormValueRuntime {
|
|
|
602
650
|
this.bumpChangeTick();
|
|
603
651
|
}
|
|
604
652
|
|
|
605
|
-
|
|
653
|
+
const explicitPathsToMark: NamePath[] = [];
|
|
606
654
|
for (const p of explicitPaths) {
|
|
655
|
+
if (this.shouldKeepDefaultPathEnabled(p, snapshot)) {
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
explicitPathsToMark.push(p);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// 非 default 来源写入:需要使默认值永久失效(explicit)
|
|
662
|
+
for (const p of explicitPathsToMark) {
|
|
607
663
|
this.markExplicit(namePathToPathKey(p));
|
|
608
664
|
}
|
|
609
665
|
|
|
@@ -733,7 +789,7 @@ export class FormValueRuntime {
|
|
|
733
789
|
const source: ValueSource = options?.source ?? 'system';
|
|
734
790
|
const triggerEvent = options?.triggerEvent !== false;
|
|
735
791
|
const txId = options?.txId ?? createTxId();
|
|
736
|
-
const markExplicit = options?.markExplicit ?? source !== 'default';
|
|
792
|
+
const markExplicit = options?.markExplicit ?? (source !== 'default' && source !== 'linkage');
|
|
737
793
|
const ownsTxId = typeof options?.txId === 'undefined';
|
|
738
794
|
|
|
739
795
|
const linkageScopeDepth =
|
|
@@ -1071,10 +1127,12 @@ export class FormValueRuntime {
|
|
|
1071
1127
|
private markExplicit(pathKey: string) {
|
|
1072
1128
|
if (this.explicitSet.has(pathKey)) return;
|
|
1073
1129
|
this.explicitSet.add(pathKey);
|
|
1130
|
+
const preserveDescendantDefaults = this.shouldPreserveDescendantDefaultsOnExplicit(pathKey);
|
|
1074
1131
|
|
|
1075
1132
|
// explicit 后默认值永远失效:清理该路径及其子路径的 lastDefault 记录,避免误判“仍是默认值”
|
|
1076
1133
|
for (const k of Array.from(this.lastDefaultValueByPathKey.keys())) {
|
|
1077
|
-
|
|
1134
|
+
const isDescendant = k.startsWith(`${pathKey}.`) || k.startsWith(`${pathKey}[`);
|
|
1135
|
+
if (k === pathKey || (!preserveDescendantDefaults && isDescendant)) {
|
|
1078
1136
|
this.lastDefaultValueByPathKey.delete(k);
|
|
1079
1137
|
}
|
|
1080
1138
|
}
|
|
@@ -1087,6 +1145,40 @@ export class FormValueRuntime {
|
|
|
1087
1145
|
}
|
|
1088
1146
|
}
|
|
1089
1147
|
|
|
1148
|
+
private normalizeObservedNamePath(name: NamePath | string | number | undefined): NamePath | null {
|
|
1149
|
+
if (Array.isArray(name)) return name as NamePath;
|
|
1150
|
+
if (typeof name === 'number') return [name];
|
|
1151
|
+
if (typeof name !== 'string' || !name) return null;
|
|
1152
|
+
const parsed = pathKeyToNamePath(name);
|
|
1153
|
+
return parsed.length ? parsed : [name];
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
private clearExplicitForDefaultPatch(pathKey: string) {
|
|
1157
|
+
for (const key of Array.from(this.explicitSet)) {
|
|
1158
|
+
if (key === pathKey || key.startsWith(`${pathKey}.`) || key.startsWith(`${pathKey}[`)) {
|
|
1159
|
+
this.explicitSet.delete(key);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
private shouldPreserveDescendantDefaultsOnExplicit(pathKey: string) {
|
|
1165
|
+
const value = this.getFormValueAtPath(pathKeyToNamePath(pathKey));
|
|
1166
|
+
return Array.isArray(value);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
private shouldKeepDefaultPathEnabled(namePath: NamePath, snapshot: any) {
|
|
1170
|
+
return this.shouldKeepDefaultPathValueEnabled(namePath, _.get(snapshot, namePath as any));
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
private shouldKeepDefaultPathValueEnabled(namePath: NamePath, value: any) {
|
|
1174
|
+
const pathKey = namePathToPathKey(namePath);
|
|
1175
|
+
const last = this.lastDefaultValueByPathKey.get(pathKey);
|
|
1176
|
+
if (typeof last === 'undefined') return false;
|
|
1177
|
+
|
|
1178
|
+
const nextSnapshot = isObservable(value) ? toJS(value) : value;
|
|
1179
|
+
return _.isEqual(nextSnapshot, last);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1090
1182
|
private isExplicit(pathKey: string) {
|
|
1091
1183
|
return !!this.findExplicitHit(pathKey);
|
|
1092
1184
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
export function isArrayLikeField(field: any) {
|
|
11
|
+
return (
|
|
12
|
+
['multipleSelect', 'checkboxGroup'].includes(field?.interface) || ['array', 'json', 'jsonb'].includes(field?.type)
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -563,6 +563,9 @@ export class TableBlockModel extends CollectionBlockModel<TableBlockModelStructu
|
|
|
563
563
|
columns={this.columns.value}
|
|
564
564
|
pagination={this.pagination()}
|
|
565
565
|
highlightedRowKey={highlightedRowKey}
|
|
566
|
+
selectedRowKeysFromResource={this.resource
|
|
567
|
+
.getSelectedRows()
|
|
568
|
+
.map((row) => getRowKey(row, this.collection.filterTargetKey))}
|
|
566
569
|
defaultExpandAllRows={this.props.defaultExpandAllRows}
|
|
567
570
|
expandedRowKeys={this.props.expandedRowKeys}
|
|
568
571
|
heightMode={heightMode}
|
|
@@ -581,6 +584,7 @@ const TableBlockContent = (props: {
|
|
|
581
584
|
columns: any;
|
|
582
585
|
pagination: any;
|
|
583
586
|
highlightedRowKey: string;
|
|
587
|
+
selectedRowKeysFromResource: string[];
|
|
584
588
|
defaultExpandAllRows?: boolean;
|
|
585
589
|
expandedRowKeys?: any[];
|
|
586
590
|
heightMode?: string;
|
|
@@ -594,6 +598,7 @@ const TableBlockContent = (props: {
|
|
|
594
598
|
columns,
|
|
595
599
|
pagination,
|
|
596
600
|
highlightedRowKey,
|
|
601
|
+
selectedRowKeysFromResource,
|
|
597
602
|
defaultExpandAllRows,
|
|
598
603
|
expandedRowKeys,
|
|
599
604
|
heightMode,
|
|
@@ -620,6 +625,7 @@ const TableBlockContent = (props: {
|
|
|
620
625
|
columns={columns}
|
|
621
626
|
pagination={pagination}
|
|
622
627
|
highlightedRowKey={highlightedRowKey}
|
|
628
|
+
selectedRowKeysFromResource={selectedRowKeysFromResource}
|
|
623
629
|
defaultExpandAllRows={defaultExpandAllRows}
|
|
624
630
|
expandedRowKeys={expandedRowKeys}
|
|
625
631
|
tableScroll={tableScroll}
|
|
@@ -829,6 +835,7 @@ const HighPerformanceTable = React.memo(
|
|
|
829
835
|
columns: any;
|
|
830
836
|
pagination: any;
|
|
831
837
|
highlightedRowKey: string;
|
|
838
|
+
selectedRowKeysFromResource: string[];
|
|
832
839
|
defaultExpandAllRows?: boolean;
|
|
833
840
|
expandedRowKeys?: any[];
|
|
834
841
|
tableScroll;
|
|
@@ -841,6 +848,7 @@ const HighPerformanceTable = React.memo(
|
|
|
841
848
|
columns,
|
|
842
849
|
pagination: _pagination,
|
|
843
850
|
highlightedRowKey,
|
|
851
|
+
selectedRowKeysFromResource,
|
|
844
852
|
defaultExpandAllRows,
|
|
845
853
|
expandedRowKeys,
|
|
846
854
|
tableScroll,
|
|
@@ -848,9 +856,17 @@ const HighPerformanceTable = React.memo(
|
|
|
848
856
|
const dataSourceRef = useRef(dataSource);
|
|
849
857
|
dataSourceRef.current = dataSource;
|
|
850
858
|
|
|
851
|
-
const [selectedRowKeys, setSelectedRowKeys] = React.useState<string[]>(
|
|
852
|
-
|
|
853
|
-
)
|
|
859
|
+
const [selectedRowKeys, setSelectedRowKeys] = React.useState<string[]>(selectedRowKeysFromResource || []);
|
|
860
|
+
|
|
861
|
+
useEffect(() => {
|
|
862
|
+
const nextSelectedRowKeys = selectedRowKeysFromResource || [];
|
|
863
|
+
setSelectedRowKeys((prev) => {
|
|
864
|
+
if (_.isEqual(prev, nextSelectedRowKeys)) {
|
|
865
|
+
return prev;
|
|
866
|
+
}
|
|
867
|
+
return nextSelectedRowKeys;
|
|
868
|
+
});
|
|
869
|
+
}, [selectedRowKeysFromResource]);
|
|
854
870
|
|
|
855
871
|
const getRowKeyFunc = useCallback(
|
|
856
872
|
(record) => {
|
package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx
CHANGED
|
@@ -177,6 +177,25 @@ const MemoFieldRenderer = React.memo(FieldModelRenderer, (prev, next) => {
|
|
|
177
177
|
return prev.value === next.value && prev.model === next.model;
|
|
178
178
|
});
|
|
179
179
|
|
|
180
|
+
export function buildRowPathFromFieldIndex(fieldIndex: unknown): Array<string | number> | null {
|
|
181
|
+
if (!Array.isArray(fieldIndex) || !fieldIndex.length) return null;
|
|
182
|
+
const out: Array<string | number> = [];
|
|
183
|
+
for (const entry of fieldIndex) {
|
|
184
|
+
if (typeof entry !== 'string') continue;
|
|
185
|
+
const [fieldName, indexStr] = entry.split(':');
|
|
186
|
+
const index = Number(indexStr);
|
|
187
|
+
if (!fieldName || !Number.isFinite(index)) continue;
|
|
188
|
+
out.push(fieldName, index);
|
|
189
|
+
}
|
|
190
|
+
return out.length ? out : null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function getLatestSubTableRowRecord(form: any, fieldIndex: unknown, fallbackRecord: any): any {
|
|
194
|
+
const latestRowPath = buildRowPathFromFieldIndex(fieldIndex);
|
|
195
|
+
const latestRecord = latestRowPath ? form?.getFieldValue?.(latestRowPath) : undefined;
|
|
196
|
+
return typeof latestRecord === 'undefined' ? fallbackRecord : latestRecord;
|
|
197
|
+
}
|
|
198
|
+
|
|
180
199
|
function shouldCommitImmediately(value: any) {
|
|
181
200
|
if (Array.isArray(value)) {
|
|
182
201
|
return true;
|
|
@@ -574,6 +593,9 @@ export class SubTableColumnModel<
|
|
|
574
593
|
const rowForkKey = `row:${baseIndexKey}:${rowIdentity}:${String(rowIdx)}`;
|
|
575
594
|
const rowFork: any = (() => {
|
|
576
595
|
const fork = this.createFork({}, rowForkKey);
|
|
596
|
+
fork.context.defineProperty('subTableRowFork', {
|
|
597
|
+
value: true,
|
|
598
|
+
});
|
|
577
599
|
const associationFieldPath =
|
|
578
600
|
(this.parent as any)?.fieldPath ??
|
|
579
601
|
(this.parent as any)?.context?.fieldPath ??
|
|
@@ -592,9 +614,11 @@ export class SubTableColumnModel<
|
|
|
592
614
|
}
|
|
593
615
|
fork.context.defineProperty('item', {
|
|
594
616
|
get: () => {
|
|
617
|
+
const form = (fork.context as any)?.form || (this.context?.blockModel as any)?.context?.form;
|
|
618
|
+
const rowRecord = getLatestSubTableRowRecord(form, fork.context.fieldIndex, record);
|
|
595
619
|
const parentItemCtx = (parentItem ?? this.context?.item) as any;
|
|
596
|
-
const isNew =
|
|
597
|
-
const isStored =
|
|
620
|
+
const isNew = rowRecord?.__is_new__;
|
|
621
|
+
const isStored = rowRecord?.__is_stored__;
|
|
598
622
|
const list = (this.parent as any)?.props?.value;
|
|
599
623
|
const length = Array.isArray(list) ? list.length : undefined;
|
|
600
624
|
return {
|
|
@@ -602,7 +626,7 @@ export class SubTableColumnModel<
|
|
|
602
626
|
length,
|
|
603
627
|
__is_new__: isNew,
|
|
604
628
|
__is_stored__: isStored,
|
|
605
|
-
value:
|
|
629
|
+
value: rowRecord,
|
|
606
630
|
parentItem: parentItemCtx,
|
|
607
631
|
};
|
|
608
632
|
},
|