@nocobase/client-v2 2.2.0-beta.2 → 2.2.0-beta.5

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 (42) hide show
  1. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +2 -0
  2. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +6 -0
  3. package/es/flow/utils/dataScopeRowSnapshot.d.ts +59 -0
  4. package/es/flow/utils/dateTimeDisplayProps.d.ts +49 -0
  5. package/es/flow/utils/formValueDeps.d.ts +32 -0
  6. package/es/index.mjs +113 -93
  7. package/lib/index.js +104 -84
  8. package/package.json +7 -7
  9. package/src/components/AppComponents.tsx +19 -3
  10. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +1022 -0
  11. package/src/flow/actions/__tests__/subFormFieldLinkageRules.inputArgs.test.ts +2 -2
  12. package/src/flow/actions/dateTimeFormat.tsx +42 -28
  13. package/src/flow/actions/linkageRules.tsx +1 -1
  14. package/src/flow/actions/linkageRulesFormValueRefresh.ts +22 -130
  15. package/src/flow/admin-shell/admin-layout/HelpLite.tsx +1 -3
  16. package/src/flow/components/DynamicFlowsIcon.tsx +447 -126
  17. package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +148 -2
  18. package/src/flow/flows/editMarkdownFlow.tsx +1 -1
  19. package/src/flow/index.ts +1 -1
  20. package/src/flow/internal/utils/operatorSchemaHelper.ts +15 -1
  21. package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +56 -1
  22. package/src/flow/models/blocks/table/TableColumnModel.tsx +36 -3
  23. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +159 -1
  24. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +16 -4
  25. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +141 -19
  26. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +197 -0
  27. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +23 -0
  28. package/src/flow/models/fields/ClickableFieldModel.tsx +5 -0
  29. package/src/flow/models/fields/DisplayDateTimeFieldModel.tsx +29 -1
  30. package/src/flow/models/fields/InputFieldModel.tsx +48 -2
  31. package/src/flow/models/fields/SelectFieldModel.tsx +6 -4
  32. package/src/flow/models/fields/TextareaFieldModel.tsx +49 -3
  33. package/src/flow/models/fields/__tests__/DisplayDateTimeFieldModel.test.tsx +96 -0
  34. package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +100 -1
  35. package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +43 -0
  36. package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +100 -0
  37. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +258 -0
  38. package/src/flow/utils/dataScopeFormValueClear.ts +218 -81
  39. package/src/flow/utils/dataScopeRowSnapshot.ts +616 -0
  40. package/src/flow/utils/dateTimeDisplayProps.ts +135 -0
  41. package/src/flow/utils/formValueDeps.ts +170 -0
  42. package/src/settings-center/AdminSettingsLayout.tsx +7 -2
@@ -13,7 +13,7 @@ import { subFormFieldLinkageRules } from '../linkageRules';
13
13
  import { SubFormFieldModel, SubFormListFieldModel } from '../../models/fields/AssociationFieldModel/SubFormFieldModel';
14
14
 
15
15
  describe('subFormFieldLinkageRules action', () => {
16
- it('passes inputArgs to fork runtime context', async () => {
16
+ it('passes inputArgs to fork runtime context when grid forks are stored in a Set', async () => {
17
17
  const linkageAssignHandler = vi.fn();
18
18
  const engine = new FlowEngine();
19
19
  const forkModel = new FlowModel({ uid: 'fork-model', flowEngine: engine }) as any;
@@ -38,7 +38,7 @@ describe('subFormFieldLinkageRules action', () => {
38
38
  hidden: false,
39
39
  subModels: {
40
40
  grid: {
41
- forks: [forkModel],
41
+ forks: new Set([forkModel]),
42
42
  },
43
43
  },
44
44
  },
@@ -11,13 +11,36 @@ import { css } from '@emotion/css';
11
11
  import { defineAction, tExpr } from '@nocobase/flow-engine';
12
12
  import { getPickerFormat } from '@nocobase/utils/client';
13
13
  import { DateFormatCom, ExpiresRadio } from '../components';
14
+ import {
15
+ getDateTimeFormatCollectionField,
16
+ isDateOnlyCollectionField,
17
+ isTimeCollectionField,
18
+ resolveDateTimeDisplayProps,
19
+ } from '../utils/dateTimeDisplayProps';
20
+
21
+ const isTableColumnFieldSubModel = (model) => {
22
+ const parent = model?.parent;
23
+ return (
24
+ parent?.subModels?.field === model &&
25
+ (parent?.use === 'TableColumnModel' || parent?.constructor?.name === 'TableColumnModel')
26
+ );
27
+ };
28
+
29
+ const syncTableColumnDateTimeFormatProps = (ctx, props) => {
30
+ const model = ctx.model;
31
+ if (!isTableColumnFieldSubModel(model) || !model?.parent?.collectionField?.isAssociationField?.()) {
32
+ return;
33
+ }
34
+
35
+ model.parent.setProps(props);
36
+ };
14
37
 
15
38
  export const dateTimeFormat = defineAction({
16
39
  title: tExpr('Date display format'),
17
40
  name: 'dateDisplayFormat',
18
41
  uiSchema: (ctx) => {
19
- const { collectionField } = ctx.model.context as any;
20
- const isTimeField = collectionField.type === 'time' || collectionField.interface === 'time';
42
+ const collectionField = getDateTimeFormatCollectionField({ model: ctx.model });
43
+ const isTimeField = isTimeCollectionField(collectionField);
21
44
  const timeFormatField = {
22
45
  type: 'string',
23
46
  title: '{{t("Time format")}}',
@@ -43,7 +66,7 @@ export const dateTimeFormat = defineAction({
43
66
  (field) => {
44
67
  if (!isTimeField) {
45
68
  const { showTime, picker } = field.form.values || {};
46
- field.hidden = !showTime || picker !== 'date';
69
+ field.hidden = isDateOnlyCollectionField(collectionField) || !showTime || picker !== 'date';
47
70
  }
48
71
  },
49
72
  ],
@@ -146,10 +169,11 @@ export const dateTimeFormat = defineAction({
146
169
  },
147
170
  },
148
171
  (field) => {
149
- const { collectionField } = ctx.model.context as any;
172
+ const collectionField = getDateTimeFormatCollectionField({ model: ctx.model });
150
173
  const { picker } = field.form.values || {};
151
- field.hidden = collectionField.type === 'dateOnly' || picker !== 'date';
152
- if (picker !== 'date') {
174
+ const isDateOnlyField = isDateOnlyCollectionField(collectionField);
175
+ field.hidden = isDateOnlyField || picker !== 'date';
176
+ if (isDateOnlyField || picker !== 'date') {
153
177
  field.value = false;
154
178
  }
155
179
  },
@@ -159,34 +183,24 @@ export const dateTimeFormat = defineAction({
159
183
  };
160
184
  },
161
185
  defaultParams: (ctx: any) => {
162
- const { showTime, dateFormat, format, timeFormat, picker }: any = {
163
- ...ctx.model.context.collectionField.getComponentProps(),
164
- ...ctx.model.props,
165
- };
166
- const collectionField = ctx.model.context.collectionField;
167
- const isTimeField = collectionField.type === 'time' || collectionField.interface === 'time';
186
+ const { showTime, dateFormat, timeFormat, picker } = resolveDateTimeDisplayProps({
187
+ model: ctx.model,
188
+ withDefaults: true,
189
+ });
168
190
  return {
169
191
  picker: picker || 'date',
170
192
  dateFormat: dateFormat || 'YYYY-MM-DD',
171
- timeFormat: timeFormat || (isTimeField ? format : undefined) || 'HH:mm:ss',
193
+ timeFormat: timeFormat || 'HH:mm:ss',
172
194
  showTime,
173
195
  };
174
196
  },
197
+ async beforeParamsSave(ctx: any, params) {
198
+ const props = resolveDateTimeDisplayProps({ model: ctx.model, params });
199
+ ctx.model.setProps(props);
200
+ syncTableColumnDateTimeFormatProps(ctx, props);
201
+ await ctx.model.save?.();
202
+ },
175
203
  handler(ctx: any, params) {
176
- const { collectionField } = ctx.model.context as any;
177
- const isTimeField = collectionField.type === 'time' || collectionField.interface === 'time';
178
- if (isTimeField) {
179
- const timeFormat = params?.timeFormat || params?.format || 'HH:mm:ss';
180
- ctx.model.setProps({
181
- ...params,
182
- timeFormat,
183
- format: timeFormat,
184
- });
185
- } else {
186
- ctx.model.setProps({
187
- ...params,
188
- format: params?.showTime ? `${params.dateFormat} ${params.timeFormat}` : params.dateFormat,
189
- });
190
- }
204
+ ctx.model.setProps(resolveDateTimeDisplayProps({ model: ctx.model, params }));
191
205
  },
192
206
  });
@@ -2842,7 +2842,7 @@ export const subFormFieldLinkageRules = defineAction({
2842
2842
  }
2843
2843
  } else {
2844
2844
  await Promise.all(
2845
- (grid.forks || []).map(async (forkModel: FlowModel) => {
2845
+ Array.from(grid.forks || []).map(async (forkModel: FlowModel) => {
2846
2846
  if (forkModel.hidden) {
2847
2847
  return;
2848
2848
  }
@@ -15,17 +15,29 @@ import {
15
15
  isRunJSValue,
16
16
  } from '@nocobase/flow-engine';
17
17
  import _ from 'lodash';
18
- import { namePathToPathKey, parsePathString, pathKeyToNamePath } from '../models/blocks/form/value-runtime/path';
18
+ import { namePathToPathKey } from '../models/blocks/form/value-runtime/path';
19
19
  import {
20
20
  collectStaticDepsFromRunJSValue,
21
21
  collectStaticDepsFromTemplateValue,
22
22
  recordDep,
23
23
  type DepCollector,
24
24
  } from '../models/blocks/form/value-runtime/deps';
25
+ import {
26
+ buildItemListRootPath,
27
+ buildItemRowPath,
28
+ dedupeNamePaths,
29
+ findFormValueChangeSource,
30
+ getChangedPathsFromPayload,
31
+ getFieldIndexEntriesFromContext,
32
+ isNamePathPrefix,
33
+ isSameNamePath,
34
+ minimizeNamePaths,
35
+ parseDependencyPath,
36
+ parsePathKey,
37
+ type NamePath,
38
+ } from '../utils/formValueDeps';
25
39
  import { linkageRulesRefresh } from './linkageRulesRefresh';
26
40
 
27
- type NamePath = Array<string | number>;
28
-
29
41
  type LinkageRefreshDeps = {
30
42
  wildcard: boolean;
31
43
  valuePaths: NamePath[];
@@ -40,75 +52,11 @@ type LinkageRefreshBinding = {
40
52
  dispose: () => void;
41
53
  };
42
54
 
43
- type FieldIndexEntry = {
44
- name: string;
45
- index: number;
46
- };
47
-
48
55
  const FORM_VALUES_CHANGE_EVENT = 'formValuesChange';
49
56
  const LINKAGE_REFRESH_BINDINGS_KEY = '__formValueDrivenLinkageRefreshBindings';
50
57
 
51
- function isSameNamePath(a: NamePath, b: NamePath) {
52
- return a.length === b.length && a.every((seg, index) => seg === b[index]);
53
- }
54
-
55
- function isNamePathPrefix(prefix: NamePath, path: NamePath) {
56
- if (prefix.length > path.length) return false;
57
- return prefix.every((seg, index) => seg === path[index]);
58
- }
59
-
60
- function dedupeNamePaths(paths: NamePath[]) {
61
- const byKey = new Map<string, NamePath>();
62
- for (const path of paths) {
63
- if (!path?.length) continue;
64
- byKey.set(namePathToPathKey(path), path);
65
- }
66
- return Array.from(byKey.values());
67
- }
68
-
69
- function minimizeValueNamePaths(paths: NamePath[]) {
70
- const deduped = dedupeNamePaths(paths);
71
- return deduped.filter((path, index) => {
72
- return !deduped.some((other, otherIndex) => otherIndex !== index && isNamePathPrefix(path, other));
73
- });
74
- }
75
-
76
- function parseFieldIndexEntries(fieldIndex: unknown): FieldIndexEntry[] {
77
- const arr = Array.isArray(fieldIndex) ? fieldIndex : [];
78
- const entries: FieldIndexEntry[] = [];
79
- for (const it of arr) {
80
- if (typeof it !== 'string') continue;
81
- const [name, indexStr] = it.split(':');
82
- const index = Number(indexStr);
83
- if (!name || Number.isNaN(index)) continue;
84
- entries.push({ name, index });
85
- }
86
- return entries;
87
- }
88
-
89
- function getFieldIndexEntriesFromContext(ctx: any): FieldIndexEntry[] {
90
- return parseFieldIndexEntries(ctx?.model?.context?.fieldIndex ?? ctx?.fieldIndex);
91
- }
92
-
93
- function buildItemRowPath(entries: FieldIndexEntry[], parentDepth: number): NamePath | null {
94
- const targetIndex = entries.length - 1 - parentDepth;
95
- if (targetIndex < 0) return null;
96
-
97
- const out: NamePath = [];
98
- for (let i = 0; i <= targetIndex; i++) {
99
- out.push(entries[i].name, entries[i].index);
100
- }
101
- return out;
102
- }
103
-
104
- function buildItemListRootPath(entries: FieldIndexEntry[], parentDepth: number): NamePath | null {
105
- const rowPath = buildItemRowPath(entries, parentDepth);
106
- if (!rowPath?.length) return null;
107
- return rowPath.slice(0, -1);
108
- }
109
-
110
58
  function resolveItemDependencyPath(ctx: FlowContext, depPath: NamePath): LinkageRefreshDeps {
111
- const entries = getFieldIndexEntriesFromContext(ctx as any);
59
+ const entries = getFieldIndexEntriesFromContext(ctx);
112
60
  if (!entries.length) {
113
61
  return { wildcard: true, valuePaths: [], structuralPaths: [] };
114
62
  }
@@ -171,8 +119,7 @@ function addRunjsUsageToCollector(script: string, collector: DepCollector) {
171
119
  collector.wildcard = true;
172
120
  continue;
173
121
  }
174
- const segs = parsePathString(String(subPath)).filter((seg) => typeof seg !== 'object') as NamePath;
175
- recordDep(segs, collector);
122
+ recordDep(parseDependencyPath(String(subPath)), collector);
176
123
  continue;
177
124
  }
178
125
  if (varName === 'item') {
@@ -233,13 +180,13 @@ function collectLinkageRefreshDeps(ctx: FlowContext, params: any): LinkageRefres
233
180
  wildcard = true;
234
181
  continue;
235
182
  }
236
- valuePaths.push(pathKeyToNamePath(inner));
183
+ valuePaths.push(parsePathKey(inner));
237
184
  continue;
238
185
  }
239
186
 
240
187
  if (depKey === 'ctx:item' || depKey.startsWith('ctx:item:')) {
241
188
  const subPath = depKey === 'ctx:item' ? '' : depKey.slice('ctx:item:'.length);
242
- const depPath = subPath ? (parsePathString(subPath).filter((seg) => typeof seg !== 'object') as NamePath) : [];
189
+ const depPath = subPath ? parseDependencyPath(subPath) : [];
243
190
  const resolved = resolveItemDependencyPath(ctx, depPath);
244
191
  wildcard ||= resolved.wildcard;
245
192
  valuePaths.push(...resolved.valuePaths);
@@ -249,7 +196,7 @@ function collectLinkageRefreshDeps(ctx: FlowContext, params: any): LinkageRefres
249
196
 
250
197
  return {
251
198
  wildcard,
252
- valuePaths: minimizeValueNamePaths(valuePaths),
199
+ valuePaths: minimizeNamePaths(valuePaths),
253
200
  structuralPaths: dedupeNamePaths(structuralPaths),
254
201
  };
255
202
  }
@@ -258,44 +205,9 @@ function hasLinkageRefreshDeps(deps: LinkageRefreshDeps) {
258
205
  return deps.wildcard || deps.valuePaths.length > 0 || deps.structuralPaths.length > 0;
259
206
  }
260
207
 
261
- function getChangedPathsFromPayload(payload: any): NamePath[] {
262
- const rawChangedPaths = Array.isArray(payload?.changedPaths) ? payload.changedPaths : [];
263
- const out: NamePath[] = [];
264
-
265
- for (const path of rawChangedPaths) {
266
- if (Array.isArray(path)) {
267
- if (path.length === 1 && typeof path[0] === 'string') {
268
- const namePath = pathKeyToNamePath(path[0]);
269
- if (namePath.length) out.push(namePath);
270
- continue;
271
- }
272
- const segs = path.filter((seg) => typeof seg === 'string' || typeof seg === 'number') as NamePath;
273
- if (segs.length) out.push(segs);
274
- continue;
275
- }
276
- if (typeof path === 'string' && path) {
277
- out.push(pathKeyToNamePath(path));
278
- }
279
- }
280
-
281
- if (out.length) {
282
- return out;
283
- }
284
-
285
- const changedValues = payload?.changedValues;
286
- if (changedValues && typeof changedValues === 'object') {
287
- for (const key of Object.keys(changedValues)) {
288
- const namePath = pathKeyToNamePath(key);
289
- if (namePath.length) out.push(namePath);
290
- }
291
- }
292
-
293
- return out;
294
- }
295
-
296
208
  function linkageRefreshDepsMatchPayload(deps: LinkageRefreshDeps, payload: any) {
297
209
  if (!hasLinkageRefreshDeps(deps)) return false;
298
- const changedPaths = getChangedPathsFromPayload(payload);
210
+ const changedPaths = getChangedPathsFromPayload(payload, { includeArrayChangedValues: true });
299
211
  if (deps.wildcard) return true;
300
212
  if (!changedPaths.length) return true;
301
213
 
@@ -330,28 +242,8 @@ function getLinkageRefreshBindings(model: any): Map<string, LinkageRefreshBindin
330
242
  return (model[LINKAGE_REFRESH_BINDINGS_KEY] ||= new Map<string, LinkageRefreshBinding>());
331
243
  }
332
244
 
333
- function isFormBlockForLinkageRefresh(model: any) {
334
- if (!model || typeof model !== 'object') return false;
335
- if (!model.emitter || typeof model.emitter.on !== 'function' || typeof model.emitter.off !== 'function') return false;
336
- return !!model.formValueRuntime || !!model.context?.form || typeof model.context?.setFormValues === 'function';
337
- }
338
-
339
245
  function findFormBlockForLinkageRefresh(ctx: FlowContext): any | null {
340
- const candidates: any[] = [];
341
- const push = (model: any) => {
342
- if (model && !candidates.includes(model)) candidates.push(model);
343
- };
344
-
345
- push((ctx.model as any)?.context?.blockModel);
346
- push(ctx.model);
347
-
348
- let cursor: any = (ctx.model as any)?.parent;
349
- while (cursor) {
350
- push(cursor);
351
- cursor = cursor?.parent;
352
- }
353
-
354
- return candidates.find(isFormBlockForLinkageRefresh) || null;
246
+ return findFormValueChangeSource(ctx) as any;
355
247
  }
356
248
 
357
249
  function disposeLinkageRefreshBinding(model: any, key: string) {
@@ -70,9 +70,7 @@ const SettingsMenu: React.FC = () => {
70
70
  key: 'userManual',
71
71
  label: (
72
72
  <a
73
- href={
74
- isSimplifiedChinese ? 'https://v2.docs.nocobase.com/cn/guide/' : 'https://v2.docs.nocobase.com/guide/'
75
- }
73
+ href={isSimplifiedChinese ? 'https://docs.nocobase.com/cn/guide/' : 'https://docs.nocobase.com/guide/'}
76
74
  target="_blank"
77
75
  rel="noreferrer"
78
76
  >