@nocobase/client-v2 2.1.6 → 2.1.7

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 (37) 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 +1 -0
  3. package/es/flow/utils/dateTimeDisplayProps.d.ts +49 -0
  4. package/es/index.mjs +110 -90
  5. package/lib/index.js +89 -69
  6. package/package.json +7 -7
  7. package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +19 -5
  8. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +92 -0
  9. package/src/flow/actions/dateTimeFormat.tsx +42 -28
  10. package/src/flow/actions/linkageRules.tsx +45 -11
  11. package/src/flow/admin-shell/admin-layout/HelpLite.tsx +1 -3
  12. package/src/flow/components/DynamicFlowsIcon.tsx +447 -126
  13. package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +148 -2
  14. package/src/flow/flows/editMarkdownFlow.tsx +1 -1
  15. package/src/flow/index.ts +1 -1
  16. package/src/flow/internal/utils/operatorSchemaHelper.ts +15 -1
  17. package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +56 -1
  18. package/src/flow/models/blocks/table/TableColumnModel.tsx +36 -3
  19. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +159 -1
  20. package/src/flow/models/fields/AssociationFieldModel/AssociationFieldModel.tsx +1 -1
  21. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +29 -6
  22. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +16 -4
  23. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +24 -1
  24. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +13 -1
  25. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +12 -0
  26. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableFieldModel.reset.test.ts +180 -0
  27. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -4
  28. package/src/flow/models/fields/AssociationFieldModel/__tests__/AssociationFieldModel.updateAssociation.test.ts +72 -0
  29. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +23 -0
  30. package/src/flow/models/fields/ClickableFieldModel.tsx +5 -0
  31. package/src/flow/models/fields/DisplayDateTimeFieldModel.tsx +29 -1
  32. package/src/flow/models/fields/SelectFieldModel.tsx +6 -4
  33. package/src/flow/models/fields/__tests__/DisplayDateTimeFieldModel.test.tsx +96 -0
  34. package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +43 -0
  35. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +258 -0
  36. package/src/flow/utils/dateTimeDisplayProps.ts +135 -0
  37. package/src/settings-center/plugin-manager/index.tsx +3 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/client-v2",
3
- "version": "2.1.6",
3
+ "version": "2.1.7",
4
4
  "license": "Apache-2.0",
5
5
  "main": "lib/index.js",
6
6
  "module": "es/index.mjs",
@@ -27,11 +27,11 @@
27
27
  "@formily/antd-v5": "1.2.3",
28
28
  "@formily/react": "^2.2.27",
29
29
  "@formily/shared": "^2.2.27",
30
- "@nocobase/evaluators": "2.1.6",
31
- "@nocobase/flow-engine": "2.1.6",
32
- "@nocobase/sdk": "2.1.6",
33
- "@nocobase/shared": "2.1.6",
34
- "@nocobase/utils": "2.1.6",
30
+ "@nocobase/evaluators": "2.1.7",
31
+ "@nocobase/flow-engine": "2.1.7",
32
+ "@nocobase/sdk": "2.1.7",
33
+ "@nocobase/shared": "2.1.7",
34
+ "@nocobase/utils": "2.1.7",
35
35
  "ahooks": "^3.7.2",
36
36
  "antd": "5.24.2",
37
37
  "antd-style": "3.7.1",
@@ -46,5 +46,5 @@
46
46
  "react-i18next": "^11.15.1",
47
47
  "react-router-dom": "^6.30.1"
48
48
  },
49
- "gitHead": "4312d6580cc01378d008072db8285803626dc435"
49
+ "gitHead": "fc3bc8bf6ef0c185a456cb5241abc4716af8c20c"
50
50
  }
@@ -27,12 +27,12 @@ describe('linkageRules hidden state propagation', () => {
27
27
  const hostModel: any = {
28
28
  uid: 'host',
29
29
  hidden: false,
30
- context: { blockModel },
30
+ context: { blockModel, fieldPathArray: ['children'] },
31
31
  __allModels: [],
32
32
  setProps: vi.fn(),
33
33
  getFlow: vi.fn(() => ({})),
34
34
  getStepParams: vi.fn((_flowKey: string, stepKey: string) => {
35
- if (stepKey === 'init') return { fieldPath: 'org_m2o' };
35
+ if (stepKey === 'init') return { fieldPath: 'children' };
36
36
  return undefined;
37
37
  }),
38
38
  translate: (s: string) => s,
@@ -41,10 +41,22 @@ describe('linkageRules hidden state propagation', () => {
41
41
  const sameTargetModel: any = {
42
42
  uid: 'same-target',
43
43
  hidden: false,
44
- context: { blockModel },
44
+ context: { blockModel, fieldPathArray: ['children'] },
45
45
  setProps: vi.fn(),
46
46
  getStepParams: vi.fn((_flowKey: string, stepKey: string) => {
47
- if (stepKey === 'init') return { fieldPath: 'org_m2o' };
47
+ if (stepKey === 'init') return { fieldPath: 'children' };
48
+ return undefined;
49
+ }),
50
+ translate: (s: string) => s,
51
+ };
52
+
53
+ const nestedColumnModel: any = {
54
+ uid: 'nested-column',
55
+ hidden: true,
56
+ context: { blockModel, fieldPathArray: ['children'] },
57
+ setProps: vi.fn(),
58
+ getStepParams: vi.fn((_flowKey: string, stepKey: string) => {
59
+ if (stepKey === 'init') return { fieldPath: 'children.children' };
48
60
  return undefined;
49
61
  }),
50
62
  translate: (s: string) => s,
@@ -52,7 +64,7 @@ describe('linkageRules hidden state propagation', () => {
52
64
 
53
65
  const engine = {
54
66
  forEachModel: (visitor: (m: any) => void) => {
55
- [hostModel, sameTargetModel].forEach(visitor);
67
+ [hostModel, sameTargetModel, nestedColumnModel].forEach(visitor);
56
68
  },
57
69
  };
58
70
 
@@ -92,9 +104,11 @@ describe('linkageRules hidden state propagation', () => {
92
104
  });
93
105
  expect(hostModel.hidden).toBe(true);
94
106
  expect(sameTargetModel.hidden).toBe(true);
107
+ expect(nestedColumnModel.hidden).toBe(true);
95
108
 
96
109
  await actionLinkageRules.handler(ctx, { value: [] });
97
110
  expect(hostModel.hidden).toBe(false);
98
111
  expect(sameTargetModel.hidden).toBe(false);
112
+ expect(nestedColumnModel.hidden).toBe(true);
99
113
  });
100
114
  });
@@ -728,6 +728,98 @@ describe('linkageSetFieldProps action', () => {
728
728
  expect(form.setFieldValue).not.toHaveBeenCalled();
729
729
  });
730
730
 
731
+ it('should not clear the parent field when a nested same-name field inherits the parent path', async () => {
732
+ const setFormValues = vi.fn(async () => undefined);
733
+ const form = {
734
+ getFieldValue: vi.fn((path: Array<string | number>) => {
735
+ if (JSON.stringify(path) === JSON.stringify(['children', 'children'])) {
736
+ return [{ title: 'nested' }];
737
+ }
738
+ if (JSON.stringify(path) === JSON.stringify(['children'])) {
739
+ return [{ title: 'parent' }];
740
+ }
741
+ return undefined;
742
+ }),
743
+ setFieldValue: vi.fn(),
744
+ };
745
+ const fieldModel: any = {
746
+ uid: 'nested-children-field',
747
+ hidden: false,
748
+ context: {
749
+ form,
750
+ fieldPathArray: ['children'],
751
+ },
752
+ props: {
753
+ label: 'Children',
754
+ },
755
+ getStepParams: vi.fn((flowKey: string, stepKey: string) => {
756
+ if (flowKey === 'fieldSettings' && stepKey === 'init') {
757
+ return { fieldPath: 'children.children' };
758
+ }
759
+ }),
760
+ setProps(key: any, value?: any) {
761
+ if (typeof key === 'string') {
762
+ this.props[key] = value;
763
+ } else {
764
+ this.props = { ...this.props, ...key };
765
+ }
766
+ },
767
+ };
768
+ const ctx: any = {
769
+ app: {
770
+ jsonLogic: {
771
+ apply: vi.fn(() => true),
772
+ },
773
+ },
774
+ model: {
775
+ context: { form },
776
+ subModels: {
777
+ grid: {
778
+ subModels: {
779
+ items: [fieldModel],
780
+ },
781
+ },
782
+ },
783
+ },
784
+ setFormValues,
785
+ getAction: (name: string) => (name === 'linkageSetFieldProps' ? linkageSetFieldProps : null),
786
+ resolveJsonTemplate: vi.fn(async (value) => value),
787
+ };
788
+
789
+ await fieldLinkageRules.handler(ctx, {
790
+ value: [
791
+ {
792
+ key: 'rule-1',
793
+ enable: true,
794
+ condition: { logic: '$and', items: [] },
795
+ actions: [
796
+ {
797
+ key: 'action-1',
798
+ name: 'linkageSetFieldProps',
799
+ params: {
800
+ value: {
801
+ fields: ['nested-children-field'],
802
+ state: 'hidden',
803
+ },
804
+ },
805
+ },
806
+ ],
807
+ },
808
+ ],
809
+ });
810
+
811
+ expect(fieldModel.hidden).toBe(true);
812
+ expect(setFormValues).toHaveBeenCalledWith(
813
+ [{ path: ['children', 'children'], value: undefined }],
814
+ expect.objectContaining({ source: 'linkage' }),
815
+ );
816
+ expect(setFormValues).not.toHaveBeenCalledWith(
817
+ [{ path: ['children'], value: undefined }],
818
+ expect.objectContaining({ source: 'linkage' }),
819
+ );
820
+ expect(form.setFieldValue).not.toHaveBeenCalled();
821
+ });
822
+
731
823
  it('should keep hidden clear after same-round assignment for the same field', async () => {
732
824
  const formValues: { name?: string } = {};
733
825
  const setFormValues = vi.fn(async (patches: Array<{ path: Array<string | number>; value: string | undefined }>) => {
@@ -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
  });
@@ -1948,6 +1948,21 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
1948
1948
  >;
1949
1949
  return normalized.length ? normalized : null;
1950
1950
  };
1951
+ const pathKeysEqual = (
1952
+ a: Array<string | number> | null | undefined,
1953
+ b: Array<string | number> | null | undefined,
1954
+ ) => {
1955
+ if (!a || !b) return false;
1956
+ return namePathToPathKey(a) === namePathToPathKey(b);
1957
+ };
1958
+ const namePathEndsWith = (
1959
+ namePath: Array<string | number> | null | undefined,
1960
+ suffix: Array<string | number> | null | undefined,
1961
+ ) => {
1962
+ if (!namePath || !suffix || suffix.length > namePath.length) return false;
1963
+ const offset = namePath.length - suffix.length;
1964
+ return suffix.every((seg, index) => namePath[offset + index] === seg);
1965
+ };
1951
1966
  const getFieldIndexEntries = (fieldIndex: any): Array<{ name: string; index: number }> => {
1952
1967
  if (!Array.isArray(fieldIndex)) return [];
1953
1968
  return fieldIndex
@@ -1996,17 +2011,35 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
1996
2011
 
1997
2012
  return out;
1998
2013
  };
2014
+ const getTrustedFieldPathArray = (
2015
+ fieldPathArray: Array<string | number> | null,
2016
+ targetPath: string | null,
2017
+ fieldIndex: unknown,
2018
+ ): Array<string | number> | null => {
2019
+ if (!fieldPathArray || !targetPath) return null;
2020
+
2021
+ const targetNamePath = normalizeNamePathForKey(
2022
+ parsePathString(targetPath).filter((seg) => typeof seg === 'string' || typeof seg === 'number'),
2023
+ );
2024
+ const resolvedTargetPath = normalizeNamePathForKey(resolveDynamicNamePath(targetPath, fieldIndex));
2025
+ const indexedRelativePath = resolveIndexedRelativePath(targetPath, fieldIndex);
2026
+
2027
+ if (
2028
+ pathKeysEqual(fieldPathArray, resolvedTargetPath) ||
2029
+ pathKeysEqual(fieldPathArray, indexedRelativePath) ||
2030
+ namePathEndsWith(fieldPathArray, targetNamePath)
2031
+ ) {
2032
+ return fieldPathArray;
2033
+ }
2034
+
2035
+ return null;
2036
+ };
1999
2037
  const getModelTargetPathForHiddenClear = (model: any): string | Array<string | number> | null => {
2000
2038
  const fieldPathArray = normalizeNamePathForKey(model?.context?.fieldPathArray);
2001
2039
  const targetPath = getModelTargetPathForPatch(model);
2002
- if (fieldPathArray) {
2003
- const targetPathLastString = targetPath
2004
- ? ([...parsePathString(targetPath)].reverse().find((seg) => typeof seg === 'string') as string | undefined)
2005
- : undefined;
2006
- const fieldPathArrayLastString = [...fieldPathArray].reverse().find((seg) => typeof seg === 'string');
2007
- if (!targetPathLastString || targetPathLastString === fieldPathArrayLastString) {
2008
- return fieldPathArray;
2009
- }
2040
+ const trustedFieldPathArray = getTrustedFieldPathArray(fieldPathArray, targetPath, model?.context?.fieldIndex);
2041
+ if (trustedFieldPathArray) {
2042
+ return trustedFieldPathArray;
2010
2043
  }
2011
2044
 
2012
2045
  if (!targetPath) return null;
@@ -2016,12 +2049,13 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
2016
2049
  const getModelTargetPathKeys = (model: any): Set<string> => {
2017
2050
  const keys = new Set<string>();
2018
2051
  const fieldPathArray = normalizeNamePathForKey(model?.context?.fieldPathArray);
2019
- if (fieldPathArray) {
2020
- keys.add(namePathToPathKey(fieldPathArray));
2052
+ const targetPath = getModelTargetPathForPatch(model);
2053
+ const trustedFieldPathArray = getTrustedFieldPathArray(fieldPathArray, targetPath, model?.context?.fieldIndex);
2054
+ if (trustedFieldPathArray) {
2055
+ keys.add(namePathToPathKey(trustedFieldPathArray));
2021
2056
  return keys;
2022
2057
  }
2023
2058
 
2024
- const targetPath = getModelTargetPathForPatch(model);
2025
2059
  if (targetPath) {
2026
2060
  const fieldIndexEntries = getFieldIndexEntries(model?.context?.fieldIndex);
2027
2061
  if (!fieldIndexEntries.length) {
@@ -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
  >