@nocobase/client-v2 2.1.0-beta.34 → 2.1.0-beta.36

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.
Files changed (76) hide show
  1. package/es/BaseApplication.d.ts +7 -1
  2. package/es/PluginManager.d.ts +2 -0
  3. package/es/components/PoweredBy.d.ts +18 -0
  4. package/es/components/SwitchLanguage.d.ts +11 -0
  5. package/es/components/form/DialogFormLayout.d.ts +75 -0
  6. package/es/components/form/DrawerFormLayout.d.ts +11 -11
  7. package/es/components/form/PasswordInput.d.ts +40 -0
  8. package/es/components/form/RemoteSelect.d.ts +79 -0
  9. package/es/components/form/index.d.ts +3 -0
  10. package/es/components/form/table/styles.d.ts +10 -0
  11. package/es/components/index.d.ts +2 -0
  12. package/es/flow/models/base/ActionModelCore.d.ts +6 -0
  13. package/es/flow/models/base/GridModel.d.ts +2 -0
  14. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +9 -1
  15. package/es/flow/utils/dataScopeFormValueClear.d.ts +14 -0
  16. package/es/flow-compat/passwordUtils.d.ts +1 -1
  17. package/es/hooks/index.d.ts +2 -0
  18. package/es/hooks/useCurrentAppInfo.d.ts +9 -0
  19. package/es/index.mjs +117 -105
  20. package/es/json-logic/globalOperators.d.ts +11 -0
  21. package/es/nocobase-buildin-plugin/index.d.ts +25 -0
  22. package/es/utils/appVersionHTML.d.ts +10 -0
  23. package/es/utils/globalDeps.d.ts +7 -0
  24. package/es/utils/index.d.ts +1 -0
  25. package/es/utils/remotePlugins.d.ts +4 -1
  26. package/lib/index.js +120 -108
  27. package/package.json +7 -6
  28. package/src/BaseApplication.tsx +11 -3
  29. package/src/PluginManager.ts +2 -0
  30. package/src/PluginSettingsManager.ts +2 -1
  31. package/src/__tests__/PluginSettingsManager.test.ts +19 -0
  32. package/src/__tests__/PoweredBy.test.tsx +130 -0
  33. package/src/__tests__/app.test.tsx +39 -0
  34. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +39 -72
  35. package/src/__tests__/remotePlugins.test.ts +203 -0
  36. package/src/__tests__/useCurrentRoles.test.tsx +100 -0
  37. package/src/components/PoweredBy.tsx +71 -0
  38. package/src/components/README.md +314 -0
  39. package/src/components/README.zh-CN.md +312 -0
  40. package/src/components/SwitchLanguage.tsx +48 -0
  41. package/src/components/form/DialogFormLayout.tsx +111 -0
  42. package/src/components/form/DrawerFormLayout.tsx +13 -32
  43. package/src/components/form/PasswordInput.tsx +211 -0
  44. package/src/components/form/RemoteSelect.tsx +137 -0
  45. package/src/components/form/index.tsx +3 -0
  46. package/src/components/form/table/Table.tsx +2 -1
  47. package/src/components/form/table/styles.ts +19 -0
  48. package/src/components/index.ts +2 -0
  49. package/src/css-variable/CSSVariableProvider.tsx +10 -1
  50. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +96 -0
  51. package/src/flow/actions/dataScope.tsx +3 -0
  52. package/src/flow/actions/filterFormDefaultValues.tsx +1 -2
  53. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -4
  54. package/src/flow/admin-shell/admin-layout/HelpLite.tsx +7 -33
  55. package/src/flow/components/BlockItemCard.tsx +2 -2
  56. package/src/flow/models/base/ActionModel.tsx +8 -7
  57. package/src/flow/models/base/ActionModelCore.tsx +15 -7
  58. package/src/flow/models/base/GridModel.tsx +93 -36
  59. package/src/flow/models/base/__tests__/GridModel.visibleLayout.test.ts +83 -11
  60. package/src/flow/models/blocks/details/DetailsItemModel.tsx +2 -0
  61. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +329 -5
  62. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +337 -0
  63. package/src/flow/models/blocks/form/FormItemModel.tsx +5 -3
  64. package/src/flow/models/blocks/form/__tests__/FormItemModel.defineChildren.test.ts +108 -0
  65. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +5 -0
  66. package/src/flow/models/blocks/table/TableColumnModel.tsx +2 -0
  67. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +2 -0
  68. package/src/flow/utils/dataScopeFormValueClear.ts +278 -0
  69. package/src/hooks/index.ts +2 -0
  70. package/src/hooks/useCurrentAppInfo.ts +36 -0
  71. package/src/json-logic/globalOperators.js +731 -0
  72. package/src/nocobase-buildin-plugin/index.tsx +70 -16
  73. package/src/utils/appVersionHTML.ts +28 -0
  74. package/src/utils/globalDeps.ts +47 -31
  75. package/src/utils/index.tsx +2 -0
  76. package/src/utils/remotePlugins.ts +119 -13
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { SettingOutlined } from '@ant-design/icons';
11
11
  import { FormButtonGroup } from '@formily/antd-v5';
12
+ import type { CollectionField, PropertyMeta, PropertyMetaFactory } from '@nocobase/flow-engine';
12
13
  import {
13
14
  AddSubModelButton,
14
15
  DndProvider,
@@ -22,6 +23,7 @@ import {
22
23
  FlowSettingsButton,
23
24
  } from '@nocobase/flow-engine';
24
25
  import { Form } from 'antd';
26
+ import { isEqual } from 'lodash';
25
27
  import React from 'react';
26
28
  import { commonConditionHandler, ConditionBuilder } from '../../../components/ConditionBuilder';
27
29
  import {
@@ -31,6 +33,7 @@ import {
31
33
  import { BlockSceneEnum } from '../../base/BlockModel';
32
34
  import { FilterBlockModel } from '../../base/FilterBlockModel';
33
35
  import { FormComponent } from '../form/FormBlockModel';
36
+ import { evaluateCondition } from '../form/value-runtime/conditions';
34
37
  import { isEmptyValue } from '../form/value-runtime/utils';
35
38
  import { FilterManager, type RefreshTargetsByFilterOptions } from '../filter-manager/FilterManager';
36
39
  import { FilterFormItemModel } from './FilterFormItemModel';
@@ -40,6 +43,177 @@ import { FormItemModel } from '../form/FormItemModel';
40
43
  import { getDefaultOperator } from '../filter-manager/utils';
41
44
  import { normalizeFilterValueByOperator } from './valueNormalization';
42
45
 
46
+ const RELATION_FIELD_TYPES = ['belongsTo', 'hasOne', 'hasMany', 'belongsToMany', 'belongsToArray'];
47
+ const NUMERIC_FIELD_TYPES = ['integer', 'float', 'double', 'decimal'];
48
+
49
+ function getFilterFormFieldMetaType(field: CollectionField) {
50
+ if (RELATION_FIELD_TYPES.includes(field.type)) {
51
+ return 'object';
52
+ }
53
+
54
+ if (NUMERIC_FIELD_TYPES.includes(field.type)) {
55
+ return 'number';
56
+ }
57
+
58
+ switch (field.type) {
59
+ case 'boolean':
60
+ return 'boolean';
61
+ case 'json':
62
+ return 'object';
63
+ case 'array':
64
+ return 'array';
65
+ default:
66
+ return 'string';
67
+ }
68
+ }
69
+
70
+ function shouldShowFilterFormFieldMeta(field: CollectionField) {
71
+ return Boolean(field?.interface);
72
+ }
73
+
74
+ function createFilterFormFieldMeta(field: CollectionField): PropertyMeta {
75
+ const baseMeta = {
76
+ title: field.title || field.name,
77
+ interface: field.interface,
78
+ options: field.options,
79
+ uiSchema: field.uiSchema || {},
80
+ };
81
+
82
+ if (!field.isAssociationField?.()) {
83
+ return {
84
+ type: getFilterFormFieldMetaType(field),
85
+ ...baseMeta,
86
+ };
87
+ }
88
+
89
+ const targetCollection = field.targetCollection;
90
+ if (!targetCollection) {
91
+ return {
92
+ type: 'object',
93
+ ...baseMeta,
94
+ };
95
+ }
96
+
97
+ return {
98
+ type: 'object',
99
+ ...baseMeta,
100
+ properties: async () => {
101
+ const properties: Record<string, PropertyMeta> = {};
102
+ targetCollection.fields.forEach((subField) => {
103
+ if (shouldShowFilterFormFieldMeta(subField)) {
104
+ properties[subField.name] = createFilterFormFieldMeta(subField);
105
+ }
106
+ });
107
+ return properties;
108
+ },
109
+ };
110
+ }
111
+
112
+ function getFilterFormItemFieldName(itemModel: any) {
113
+ const name = itemModel?.props?.name;
114
+ if (typeof name === 'string' && name) {
115
+ return name;
116
+ }
117
+
118
+ return itemModel?.fieldPath && itemModel?.uid ? `${itemModel.fieldPath}_${itemModel.uid}` : undefined;
119
+ }
120
+
121
+ function toFilterByTk(value: any, primaryKey: string | string[] | undefined) {
122
+ if (value == null) return undefined;
123
+ if (Array.isArray(primaryKey)) {
124
+ if (typeof value !== 'object') return undefined;
125
+ const filterByTk: Record<string, any> = {};
126
+ for (const key of primaryKey) {
127
+ const item = value?.[key];
128
+ if (item == null) return undefined;
129
+ filterByTk[key] = item;
130
+ }
131
+ return filterByTk;
132
+ }
133
+ if (typeof value !== 'object') return value;
134
+ const key = Array.isArray(primaryKey) ? primaryKey[0] : primaryKey;
135
+ return key ? value?.[key] : value?.id;
136
+ }
137
+
138
+ function setValueByPath(target: Record<string, any>, path: string, value: any) {
139
+ const segments = path.split('.').filter(Boolean);
140
+ if (!segments.length) return;
141
+
142
+ let cursor = target;
143
+ segments.forEach((segment, index) => {
144
+ if (index === segments.length - 1) {
145
+ cursor[segment] = value;
146
+ return;
147
+ }
148
+
149
+ if (!cursor[segment] || typeof cursor[segment] !== 'object' || Array.isArray(cursor[segment])) {
150
+ cursor[segment] = {};
151
+ }
152
+ cursor = cursor[segment];
153
+ });
154
+ }
155
+
156
+ function setMetaByPath(target: Record<string, PropertyMeta>, path: string, meta: PropertyMeta) {
157
+ const segments = path.split('.').filter(Boolean);
158
+ if (!segments.length) return;
159
+
160
+ let cursor = target;
161
+ segments.forEach((segment, index) => {
162
+ if (index === segments.length - 1) {
163
+ cursor[segment] = meta;
164
+ return;
165
+ }
166
+
167
+ const current = cursor[segment];
168
+ if (!current || typeof current !== 'object') {
169
+ cursor[segment] = {
170
+ type: 'object',
171
+ title: segment,
172
+ properties: {},
173
+ };
174
+ }
175
+ const properties = cursor[segment].properties;
176
+ if (!properties || typeof properties === 'function') {
177
+ cursor[segment].properties = {};
178
+ }
179
+ cursor = cursor[segment].properties as Record<string, PropertyMeta>;
180
+ });
181
+ }
182
+
183
+ function getFilterFormValues(form: any, items: any[]) {
184
+ const formValues = form?.getFieldsValue?.() || {};
185
+ const values = { ...formValues };
186
+
187
+ for (const itemModel of items) {
188
+ const fieldName = getFilterFormItemFieldName(itemModel);
189
+ if (!fieldName || !fieldName.includes('.') || !(fieldName in formValues)) {
190
+ continue;
191
+ }
192
+ setValueByPath(values, fieldName, formValues[fieldName]);
193
+ }
194
+
195
+ return values;
196
+ }
197
+
198
+ function isFilterFormFieldSubPath(fieldName: string, subPath: string) {
199
+ return subPath === fieldName || subPath.startsWith(`${fieldName}.`) || subPath.startsWith(`${fieldName}[`);
200
+ }
201
+
202
+ function isFilterFormFieldDeepSubPath(fieldName: string, subPath: string) {
203
+ return subPath.startsWith(`${fieldName}.`) || subPath.startsWith(`${fieldName}[`);
204
+ }
205
+
206
+ function findFilterFormItemByVariableSubPath(items: any[], subPath: string) {
207
+ if (!subPath) return null;
208
+
209
+ return (
210
+ items.find((itemModel) => {
211
+ const fieldName = getFilterFormItemFieldName(itemModel);
212
+ return fieldName && isFilterFormFieldSubPath(fieldName, subPath);
213
+ }) || null
214
+ );
215
+ }
216
+
43
217
  export class FilterFormBlockModel extends FilterBlockModel<{
44
218
  subModels: {
45
219
  grid: any; // Replace with actual type if available
@@ -56,6 +230,8 @@ export class FilterFormBlockModel extends FilterBlockModel<{
56
230
  private removeTargetBlockListener?: () => void;
57
231
  private initialDefaultsPromise?: Promise<void>;
58
232
  private initialRefreshHandledTargetIds = new Set<string>();
233
+ private lastDefaultValueByFieldName = new Map<string, any>();
234
+ private defaultValuesRefreshSeq = 0;
59
235
 
60
236
  get form() {
61
237
  return this.context.form;
@@ -65,10 +241,98 @@ export class FilterFormBlockModel extends FilterBlockModel<{
65
241
  return 'Filter form';
66
242
  }
67
243
 
244
+ protected createFormValuesMetaFactory(): PropertyMetaFactory {
245
+ const factory: PropertyMetaFactory = async () => ({
246
+ type: 'object',
247
+ title: this.translate('Current form'),
248
+ properties: async () => {
249
+ const properties: Record<string, PropertyMeta> = {};
250
+ const items = this.subModels?.grid?.subModels?.items || [];
251
+
252
+ for (const itemModel of items) {
253
+ const fieldName = getFilterFormItemFieldName(itemModel);
254
+ const collectionField = itemModel?.subModels?.field?.context?.collectionField || itemModel?.collectionField;
255
+ if (!fieldName || !collectionField || !shouldShowFilterFormFieldMeta(collectionField)) {
256
+ continue;
257
+ }
258
+ setMetaByPath(properties, fieldName, createFilterFormFieldMeta(collectionField));
259
+ }
260
+
261
+ return properties;
262
+ },
263
+ buildVariablesParams: () => {
264
+ const formValues = this.form?.getFieldsValue?.() || {};
265
+ const items = this.subModels?.grid?.subModels?.items || [];
266
+ const params: Record<string, any> = {};
267
+
268
+ for (const itemModel of items) {
269
+ const fieldName = getFilterFormItemFieldName(itemModel);
270
+ const collectionField = itemModel?.subModels?.field?.context?.collectionField || itemModel?.collectionField;
271
+ if (!fieldName || !collectionField?.isAssociationField?.()) {
272
+ continue;
273
+ }
274
+
275
+ const targetCollection = collectionField.targetCollection;
276
+ const target = collectionField.target;
277
+ if (!targetCollection || !target) {
278
+ continue;
279
+ }
280
+
281
+ const fieldValue = formValues[fieldName];
282
+ const primaryKey = targetCollection.filterTargetKey;
283
+ if (Array.isArray(fieldValue)) {
284
+ const filterByTk = fieldValue.map((item) => toFilterByTk(item, primaryKey)).filter((item) => item != null);
285
+ if (filterByTk.length) {
286
+ setValueByPath(params, fieldName, {
287
+ collection: target,
288
+ dataSourceKey: targetCollection.dataSourceKey,
289
+ filterByTk,
290
+ });
291
+ }
292
+ continue;
293
+ }
294
+
295
+ const filterByTk = toFilterByTk(fieldValue, primaryKey);
296
+ if (filterByTk != null) {
297
+ setValueByPath(params, fieldName, {
298
+ collection: target,
299
+ dataSourceKey: targetCollection.dataSourceKey,
300
+ filterByTk,
301
+ });
302
+ }
303
+ }
304
+
305
+ return params;
306
+ },
307
+ });
308
+ factory.title = this.translate('Current form');
309
+ return factory;
310
+ }
311
+
68
312
  useHooksBeforeRender() {
69
313
  // eslint-disable-next-line react-hooks/rules-of-hooks
70
314
  const [form] = Form.useForm();
71
315
  this.context.defineProperty('form', { get: () => form, cache: false });
316
+ this.context.defineProperty('formValues', {
317
+ get: () => getFilterFormValues(this.form, this.subModels?.grid?.subModels?.items || []),
318
+ cache: false,
319
+ meta: this.createFormValuesMetaFactory(),
320
+ resolveOnServer: (subPath: string) => {
321
+ const items = this.subModels?.grid?.subModels?.items || [];
322
+ const itemModel = findFilterFormItemByVariableSubPath(items, subPath);
323
+ if (!itemModel) return false;
324
+
325
+ const fieldName = getFilterFormItemFieldName(itemModel);
326
+ const collectionField = itemModel?.subModels?.field?.context?.collectionField || itemModel?.collectionField;
327
+ return Boolean(
328
+ fieldName &&
329
+ isFilterFormFieldDeepSubPath(fieldName, subPath) &&
330
+ collectionField?.isAssociationField?.() &&
331
+ collectionField?.targetCollection,
332
+ );
333
+ },
334
+ serverOnlyWhenContextParams: true,
335
+ });
72
336
  }
73
337
 
74
338
  async saveStepParams() {
@@ -165,14 +429,38 @@ export class FilterFormBlockModel extends FilterBlockModel<{
165
429
  }
166
430
  }
167
431
 
168
- async applyFormDefaultValues(options?: { force?: boolean }) {
432
+ private canApplyFormDefaultValue(name: string, current: any, force?: boolean) {
433
+ if (force) return true;
434
+ if (isEmptyValue(current)) return true;
435
+ if (!this.lastDefaultValueByFieldName.has(name)) return false;
436
+ return isEqual(current, this.lastDefaultValueByFieldName.get(name));
437
+ }
438
+
439
+ private async matchDefaultValueCondition(condition: any) {
440
+ if (!condition) return true;
441
+
442
+ let resolvedCondition = condition;
443
+ try {
444
+ const nextCondition = await (this.context as any).resolveJsonTemplate?.(condition);
445
+ if (typeof nextCondition !== 'undefined') {
446
+ resolvedCondition = nextCondition;
447
+ }
448
+ } catch {
449
+ resolvedCondition = condition;
450
+ }
451
+
452
+ return evaluateCondition(this.context, resolvedCondition);
453
+ }
454
+
455
+ async applyFormDefaultValues(options?: { force?: boolean; refreshSeq?: number }) {
456
+ const appliedValues: Record<string, any> = {};
169
457
  const form = this.form;
170
- if (!form) return;
458
+ if (!form) return appliedValues;
171
459
 
172
460
  const force = options?.force === true;
173
461
  const params = this.getStepParams?.('formFilterBlockModelSettings', 'defaultValues');
174
462
  const rules = (params?.value || []) as any[];
175
- if (!Array.isArray(rules) || rules.length === 0) return;
463
+ if (!Array.isArray(rules) || rules.length === 0) return appliedValues;
176
464
 
177
465
  const resolveValue = async (raw: any) => {
178
466
  // RunJS support
@@ -188,7 +476,8 @@ export class FilterFormBlockModel extends FilterBlockModel<{
188
476
  for (const rule of rules) {
189
477
  if (!rule || typeof rule !== 'object') continue;
190
478
  if (rule.enable === false) continue;
191
- if (rule.mode && String(rule.mode) !== 'default') continue;
479
+ if (!(await this.matchDefaultValueCondition(rule.condition))) continue;
480
+ if (options?.refreshSeq && options.refreshSeq !== this.defaultValuesRefreshSeq) return appliedValues;
192
481
 
193
482
  const targetPath = rule.targetPath ? String(rule.targetPath).trim() : '';
194
483
  const fieldUid = rule.field ? String(rule.field).trim() : '';
@@ -203,20 +492,54 @@ export class FilterFormBlockModel extends FilterBlockModel<{
203
492
  if (!name) continue;
204
493
 
205
494
  const current = (form as any).getFieldValue?.(name);
206
- if (!force && !isEmptyValue(current)) continue;
207
495
 
208
496
  const resolved = await resolveValue(rule.value);
497
+ if (options?.refreshSeq && options.refreshSeq !== this.defaultValuesRefreshSeq) return appliedValues;
209
498
  if (typeof resolved === 'undefined') continue;
210
499
 
211
500
  const operator = getDefaultOperator(itemModel as any);
212
501
  const normalized = normalizeFilterValueByOperator(operator, resolved);
502
+ const mode = String(rule.mode || 'default') === 'assign' ? 'assign' : 'default';
503
+ if (mode === 'default' && !this.canApplyFormDefaultValue(String(name), current, force)) continue;
504
+ if (isEqual(current, normalized)) {
505
+ if (mode === 'default') {
506
+ this.lastDefaultValueByFieldName.set(String(name), normalized);
507
+ } else {
508
+ this.lastDefaultValueByFieldName.delete(String(name));
509
+ }
510
+ continue;
511
+ }
213
512
 
214
513
  if (typeof (form as any).setFieldValue === 'function') {
215
514
  (form as any).setFieldValue(name, normalized);
216
515
  } else {
217
516
  (form as any).setFieldsValue?.({ [String(name)]: normalized });
218
517
  }
518
+ if (mode === 'default') {
519
+ this.lastDefaultValueByFieldName.set(String(name), normalized);
520
+ } else {
521
+ this.lastDefaultValueByFieldName.delete(String(name));
522
+ }
523
+ appliedValues[String(name)] = normalized;
219
524
  }
525
+
526
+ return appliedValues;
527
+ }
528
+
529
+ private handleFilterFormValuesChange(changedValues: any, allValues: any) {
530
+ const refreshSeq = ++this.defaultValuesRefreshSeq;
531
+ void (async () => {
532
+ const appliedValues = await this.applyFormDefaultValues({ refreshSeq });
533
+ if (refreshSeq !== this.defaultValuesRefreshSeq) return;
534
+
535
+ const finalChangedValues = { ...(changedValues || {}), ...(appliedValues || {}) };
536
+ const finalAllValues = this.form?.getFieldsValue?.() || allValues;
537
+ const payload = { changedValues: finalChangedValues, allValues: finalAllValues };
538
+ this.dispatchEvent('formValuesChange', payload, { debounce: true });
539
+ this.emitter.emit('formValuesChange', payload);
540
+ })().catch((error) => {
541
+ console.error('Failed to refresh filter form default values:', error);
542
+ });
220
543
  }
221
544
 
222
545
  private async handleTargetBlockRemoved(targetUid: string) {
@@ -264,6 +587,7 @@ export class FilterFormBlockModel extends FilterBlockModel<{
264
587
  onFinish={() => {
265
588
  this.context.refreshTargets();
266
589
  }}
590
+ onValuesChange={(changedValues, allValues) => this.handleFilterFormValuesChange(changedValues, allValues)}
267
591
  layoutProps={{ colon, labelAlign, labelWidth, labelWrap, layout }}
268
592
  >
269
593
  <FlowModelRenderer model={this.subModels.grid} showFlowSettings={false} />