@nocobase/client-v2 2.1.0-alpha.34 → 2.1.0-alpha.35

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 (88) hide show
  1. package/es/BaseApplication.d.ts +1 -0
  2. package/es/PluginManager.d.ts +1 -0
  3. package/es/components/form/DrawerFormLayout.d.ts +49 -0
  4. package/es/components/form/EnvVariableInput.d.ts +42 -0
  5. package/es/components/form/FileSizeInput.d.ts +27 -0
  6. package/es/components/form/createFormRegistry.d.ts +33 -0
  7. package/es/components/form/index.d.ts +13 -0
  8. package/es/components/index.d.ts +1 -1
  9. package/es/flow/components/FieldAssignRulesEditor.d.ts +1 -0
  10. package/es/flow/internal/utils/enumOptionsUtils.d.ts +5 -0
  11. package/es/flow/models/actions/AssociationActionUtils.d.ts +5 -0
  12. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +4 -0
  13. package/es/flow/models/blocks/filter-manager/FilterManager.d.ts +5 -1
  14. package/es/flow/models/blocks/shared/legacyDefaultValueMigrationBase.d.ts +1 -0
  15. package/es/flow/models/blocks/table/TableSelectModel.d.ts +8 -0
  16. package/es/flow/models/fields/DisplayEnumFieldModel.d.ts +6 -0
  17. package/es/flow/models/fields/DisplayTitleFieldModel.d.ts +1 -1
  18. package/es/flow/models/utils/displayValueUtils.d.ts +12 -0
  19. package/es/flow-compat/passwordUtils.d.ts +1 -1
  20. package/es/index.mjs +117 -106
  21. package/es/utils/remotePlugins.d.ts +0 -4
  22. package/lib/index.js +122 -111
  23. package/package.json +8 -5
  24. package/src/BaseApplication.tsx +14 -8
  25. package/src/PluginManager.ts +1 -0
  26. package/src/__tests__/app.test.tsx +28 -1
  27. package/src/__tests__/remotePlugins.test.ts +29 -18
  28. package/src/components/form/DrawerFormLayout.tsx +103 -0
  29. package/src/components/form/EnvVariableInput.tsx +126 -0
  30. package/src/components/form/FileSizeInput.tsx +105 -0
  31. package/src/components/form/createFormRegistry.ts +60 -0
  32. package/src/components/form/index.tsx +14 -0
  33. package/src/components/index.ts +1 -1
  34. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +92 -13
  35. package/src/flow/actions/__tests__/formAssignRules.legacyMigration.test.tsx +173 -0
  36. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +476 -1
  37. package/src/flow/actions/filterFormDefaultValues.tsx +30 -9
  38. package/src/flow/actions/formAssignRules.tsx +24 -9
  39. package/src/flow/actions/linkageRules.tsx +240 -258
  40. package/src/flow/actions/setTargetDataScope.tsx +32 -3
  41. package/src/flow/actions/titleField.tsx +1 -1
  42. package/src/flow/actions/validation.tsx +1 -1
  43. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +0 -4
  44. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +13 -5
  45. package/src/flow/components/FieldAssignRulesEditor.tsx +2 -0
  46. package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +81 -4
  47. package/src/flow/components/filter/LinkageFilterItem.tsx +9 -2
  48. package/src/flow/components/filter/VariableFilterItem.tsx +2 -6
  49. package/src/flow/components/filter/__tests__/LinkageFilterItem.test.tsx +71 -0
  50. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +48 -0
  51. package/src/flow/internal/utils/__tests__/enumOptionsUtils.test.ts +10 -1
  52. package/src/flow/internal/utils/enumOptionsUtils.ts +29 -0
  53. package/src/flow/models/actions/AssociateActionModel.tsx +2 -2
  54. package/src/flow/models/actions/AssociationActionUtils.ts +14 -0
  55. package/src/flow/models/actions/__tests__/AssociationActionModel.test.ts +63 -0
  56. package/src/flow/models/base/CollectionBlockModel.tsx +7 -0
  57. package/src/flow/models/base/PageModel/RootPageModel.tsx +1 -1
  58. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +33 -9
  59. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +53 -13
  60. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +63 -3
  61. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +33 -1
  62. package/src/flow/models/blocks/filter-form/__tests__/legacyDefaultValueMigration.test.ts +3 -0
  63. package/src/flow/models/blocks/filter-manager/FilterManager.ts +66 -2
  64. package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +270 -0
  65. package/src/flow/models/blocks/form/FormBlockModel.tsx +8 -5
  66. package/src/flow/models/blocks/form/FormItemModel.tsx +1 -1
  67. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +30 -0
  68. package/src/flow/models/blocks/form/__tests__/legacyDefaultValueMigration.test.ts +3 -0
  69. package/src/flow/models/blocks/form/value-runtime/rules.ts +6 -1
  70. package/src/flow/models/blocks/form/value-runtime/runtime.ts +6 -1
  71. package/src/flow/models/blocks/shared/legacyDefaultValueMigrationBase.ts +21 -5
  72. package/src/flow/models/blocks/table/TableBlockModel.tsx +11 -6
  73. package/src/flow/models/blocks/table/TableColumnModel.tsx +8 -2
  74. package/src/flow/models/blocks/table/TableSelectModel.tsx +36 -26
  75. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowClick.test.ts +69 -0
  76. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +132 -1
  77. package/src/flow/models/blocks/table/__tests__/TableSelectModel.test.ts +41 -0
  78. package/src/flow/models/fields/ClickableFieldModel.tsx +9 -4
  79. package/src/flow/models/fields/DisplayEnumFieldModel.tsx +44 -0
  80. package/src/flow/models/fields/DisplayTitleFieldModel.tsx +12 -4
  81. package/src/flow/models/fields/SelectFieldModel.tsx +31 -1
  82. package/src/flow/models/fields/__tests__/ClickableFieldModel.test.ts +23 -0
  83. package/src/flow/models/fields/__tests__/DisplayEnumFieldModel.test.tsx +39 -0
  84. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +2 -1
  85. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +7 -0
  86. package/src/flow/models/utils/displayValueUtils.ts +57 -0
  87. package/src/utils/globalDeps.ts +8 -0
  88. package/src/utils/remotePlugins.ts +7 -27
@@ -12,7 +12,8 @@ import { Typography } from 'antd';
12
12
  import { castArray } from 'lodash';
13
13
  import { css } from '@emotion/css';
14
14
  import React from 'react';
15
- import { FieldModel } from '../base/FieldModel';
15
+ import { FieldModel } from '../base';
16
+ import { hasDisplayValue, normalizeDisplayValue } from '../utils/displayValueUtils';
16
17
 
17
18
  export class DisplayTitleFieldModel extends FieldModel {
18
19
  get collectionField(): CollectionField {
@@ -55,13 +56,20 @@ export class DisplayTitleFieldModel extends FieldModel {
55
56
  };
56
57
  if (titleField) {
57
58
  const result = castArray(value).flatMap((v, idx) => {
58
- const result = this.renderComponent(v?.[titleField]);
59
- const node = v?.[titleField] ? result : 'N/A';
59
+ const titleCollectionField =
60
+ this.context.collectionField?.targetCollection?.getField?.(titleField) || this.context.collectionField;
61
+ const displayValue = normalizeDisplayValue(v?.[titleField], { collectionField: titleCollectionField });
62
+ const result = this.renderComponent(displayValue);
63
+ const node = hasDisplayValue(displayValue) ? result : 'N/A';
60
64
  return idx === 0 ? [node] : [<span key={`sep-${idx}`}>, </span>, node];
61
65
  });
62
66
  return <Typography.Text {...typographyProps}>{result}</Typography.Text>;
63
67
  } else {
64
- const textContent = <Typography.Text {...typographyProps}>{this.renderComponent(value)}</Typography.Text>;
68
+ const textContent = (
69
+ <Typography.Text {...typographyProps}>
70
+ {this.renderComponent(normalizeDisplayValue(value, { collectionField: this.context.collectionField }))}
71
+ </Typography.Text>
72
+ );
65
73
  return textContent;
66
74
  }
67
75
  }
@@ -12,25 +12,55 @@ import { Select, Tag, Tooltip } from 'antd';
12
12
  import React from 'react';
13
13
  import { FieldModel } from '../base/FieldModel';
14
14
  import { MobileSelect } from './mobile-components/MobileSelect';
15
+ import { enumToOptions, getSelectedEnumLabels } from '../../internal/utils/enumOptionsUtils';
16
+
17
+ const getOriginalEnumOptions = (model: SelectFieldModel) => {
18
+ const fromEnum = enumToOptions(model.context.collectionField?.uiSchema?.enum, (text) => text) || [];
19
+ if (fromEnum.length > 0) {
20
+ return fromEnum.map((option) => ({ ...option }));
21
+ }
22
+ const current = Array.isArray(model.props.options) ? model.props.options : [];
23
+ return current.map((option) => ({ ...option }));
24
+ };
15
25
 
16
26
  export class SelectFieldModel extends FieldModel {
17
27
  render() {
28
+ const fallbackOptions = getOriginalEnumOptions(this);
18
29
  const options = this.props.options?.map((v) => {
19
30
  return {
20
31
  ...v,
21
32
  label: this.translate(v.label),
22
33
  };
23
34
  });
35
+ const selectedLabels = getSelectedEnumLabels(this.props.value, fallbackOptions).map((item) => ({
36
+ ...item,
37
+ label: this.translate(item.label),
38
+ }));
39
+ const value = Array.isArray(this.props.value)
40
+ ? selectedLabels
41
+ : selectedLabels.length > 0
42
+ ? selectedLabels[0]
43
+ : this.props.value;
24
44
 
25
45
  // TODO: 移动端相关的代码需迁移到单独的插件中
26
46
  if (this.context.isMobileLayout) {
27
- return <MobileSelect {...this.props} options={options} />;
47
+ return <MobileSelect {...this.props} options={options} displayValue={value} />;
28
48
  }
29
49
 
30
50
  return (
31
51
  <Select
32
52
  {...this.props}
53
+ value={value}
54
+ labelInValue
55
+ onChange={(nextValue) => {
56
+ if (Array.isArray(nextValue)) {
57
+ this.props.onChange?.(nextValue.map((item: any) => item?.value));
58
+ return;
59
+ }
60
+ this.props.onChange?.(nextValue?.value);
61
+ }}
33
62
  options={options}
63
+ labelRender={(item) => item.label}
34
64
  maxTagCount="responsive"
35
65
  maxTagPlaceholder={(omittedValues) => (
36
66
  <Tooltip
@@ -9,7 +9,9 @@
9
9
 
10
10
  import { describe, expect, it, vi } from 'vitest';
11
11
  import { FlowEngine } from '@nocobase/flow-engine';
12
+ import { render, screen } from '@testing-library/react';
12
13
  import { ClickableFieldModel } from '../ClickableFieldModel';
14
+ import { DisplayTextFieldModel } from '../DisplayTextFieldModel';
13
15
 
14
16
  function createRolesFieldModel(sourceRecord: Record<string, any>) {
15
17
  const engine = new FlowEngine();
@@ -84,4 +86,25 @@ describe('ClickableFieldModel', () => {
84
86
  { debounce: true },
85
87
  );
86
88
  });
89
+
90
+ it('renders object title field values by configured target title field', () => {
91
+ const engine = new FlowEngine();
92
+ engine.registerModels({ DisplayTextFieldModel });
93
+
94
+ const model = engine.createModel<DisplayTextFieldModel>({
95
+ use: DisplayTextFieldModel,
96
+ uid: 'display-text-association-title',
97
+ });
98
+ model.context.defineProperty('collectionField', {
99
+ value: {
100
+ targetCollectionTitleFieldName: 'code',
101
+ isAssociationField: () => true,
102
+ },
103
+ });
104
+
105
+ render(model.renderInDisplayStyle({ id: 2, name: 'Sales', code: 'S-001' }, { id: 1 }, false));
106
+
107
+ expect(screen.getByText('S-001')).toBeInTheDocument();
108
+ expect(screen.queryByText('Sales')).not.toBeInTheDocument();
109
+ });
87
110
  });
@@ -0,0 +1,39 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { describe, expect, it } from 'vitest';
11
+ import { FlowEngine } from '@nocobase/flow-engine';
12
+ import { render, screen } from '@testing-library/react';
13
+ import { DisplayEnumFieldModel } from '../DisplayEnumFieldModel';
14
+
15
+ describe('DisplayEnumFieldModel', () => {
16
+ it('renders labels for multiple values (multipleSelect/checkboxGroup)', () => {
17
+ const engine = new FlowEngine();
18
+ engine.registerModels({ DisplayEnumFieldModel });
19
+
20
+ const model = engine.createModel<DisplayEnumFieldModel>({
21
+ use: DisplayEnumFieldModel,
22
+ uid: 'display-enum-multiple-values',
23
+ });
24
+
25
+ model.setProps({
26
+ value: ['a', 'b'],
27
+ options: [
28
+ { label: 'Option A', value: 'a' },
29
+ { label: 'Option B', value: 'b' },
30
+ ],
31
+ });
32
+
33
+ render(model.render());
34
+
35
+ expect(screen.getByText('Option A')).toBeInTheDocument();
36
+ expect(screen.getByText('Option B')).toBeInTheDocument();
37
+ expect(screen.queryByText('a, b')).not.toBeInTheDocument();
38
+ });
39
+ });
@@ -13,7 +13,7 @@ import { Button, CheckList, Popup, SearchBar } from 'antd-mobile';
13
13
  import React, { useEffect, useMemo, useState } from 'react';
14
14
 
15
15
  export function MobileSelect(props) {
16
- const { value, onChange, onChangeComplete, disabled, options = [], mode } = props;
16
+ const { value, displayValue, onChange, onChangeComplete, disabled, options = [], mode } = props;
17
17
  const ctx = useFlowModelContext();
18
18
  const t = ctx.t;
19
19
  const [visible, setVisible] = useState(false);
@@ -44,6 +44,7 @@ export function MobileSelect(props) {
44
44
  <div onClick={() => !disabled && setVisible(true)}>
45
45
  <Select
46
46
  {...props}
47
+ value={displayValue ?? value}
47
48
  open={false}
48
49
  dropdownStyle={{ display: 'none' }}
49
50
  showSearch={false}
@@ -178,6 +178,13 @@ describe('MobileSelect', () => {
178
178
  clickTrigger();
179
179
  expect(screen.queryByTestId('popup')).not.toBeInTheDocument();
180
180
  });
181
+
182
+ it('prefers displayValue for trigger rendering', () => {
183
+ const displayValue = [{ label: 'Published', value: 'published' }];
184
+ renderMobileSelect({ value: ['published'], displayValue, mode: 'multiple' });
185
+
186
+ expect(mockState.selectProps?.value).toEqual(displayValue);
187
+ });
181
188
  });
182
189
 
183
190
  function SubTableCellHarness({ value, onCommit, mode }: { value: any; onCommit: (value: any) => void; mode?: string }) {
@@ -0,0 +1,57 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { isValidElement } from 'react';
11
+
12
+ export function hasDisplayValue(value: any) {
13
+ return value !== undefined && value !== null && value !== '';
14
+ }
15
+
16
+ function getTitleFieldName(collectionField: any) {
17
+ const targetCollection = collectionField?.targetCollection;
18
+ return (
19
+ collectionField?.targetCollectionTitleFieldName ||
20
+ targetCollection?.titleCollectionField?.name ||
21
+ targetCollection?.titleField ||
22
+ targetCollection?.options?.titleField
23
+ );
24
+ }
25
+
26
+ function getTitleCollectionField(collectionField: any, titleFieldName: string) {
27
+ return (
28
+ collectionField?.targetCollection?.getField?.(titleFieldName) ||
29
+ collectionField?.targetCollection?.titleCollectionField
30
+ );
31
+ }
32
+
33
+ export function normalizeDisplayValue(value: any, options: { collectionField?: any } = {}): any {
34
+ if (!hasDisplayValue(value) || isValidElement(value)) {
35
+ return value;
36
+ }
37
+ if (['string', 'number', 'boolean'].includes(typeof value)) {
38
+ return value;
39
+ }
40
+ if (Array.isArray(value)) {
41
+ const parts = value.map((item) => normalizeDisplayValue(item, options)).filter(hasDisplayValue);
42
+ return parts.length ? parts.map(String).join(', ') : undefined;
43
+ }
44
+ if (value instanceof Date) {
45
+ return value.toISOString();
46
+ }
47
+ if (typeof value === 'object') {
48
+ const titleFieldName = getTitleFieldName(options.collectionField);
49
+ if (titleFieldName) {
50
+ return normalizeDisplayValue(value[titleFieldName], {
51
+ collectionField: getTitleCollectionField(options.collectionField, titleFieldName),
52
+ });
53
+ }
54
+ return undefined;
55
+ }
56
+ return value;
57
+ }
@@ -9,7 +9,9 @@
9
9
 
10
10
  import * as antdCssinjs from '@ant-design/cssinjs';
11
11
  import * as antdIcons from '@ant-design/icons';
12
+ import * as antdStyle from 'antd-style';
12
13
  import * as emotionCss from '@emotion/css';
14
+ import * as formilyAntdV5 from '@formily/antd-v5';
13
15
  import * as formilyCore from '@formily/core';
14
16
  import * as formilyReact from '@formily/react';
15
17
  import * as formilyReactive from '@formily/reactive';
@@ -19,6 +21,8 @@ import { dayjs } from '@nocobase/utils/client';
19
21
  import * as nocobaseFlowEngine from '@nocobase/flow-engine';
20
22
  import * as ahooks from 'ahooks';
21
23
  import * as antd from 'antd';
24
+ import * as FileSaver from 'file-saver';
25
+ import axios from 'axios';
22
26
  import * as i18next from 'i18next';
23
27
  import lodash from 'lodash';
24
28
  import React from 'react';
@@ -48,12 +52,14 @@ export function defineGlobalDeps(requirejs: RequireJS) {
48
52
  requirejs.define('antd', () => antd);
49
53
  requirejs.define('@ant-design/icons', () => antdIcons);
50
54
  requirejs.define('@ant-design/cssinjs', () => antdCssinjs);
55
+ requirejs.define('antd-style', () => antdStyle);
51
56
 
52
57
  // i18next
53
58
  requirejs.define('i18next', () => i18next);
54
59
  requirejs.define('react-i18next', () => reactI18next);
55
60
 
56
61
  // formily
62
+ requirejs.define('@formily/antd-v5', () => formilyAntdV5);
57
63
  requirejs.define('@formily/core', () => formilyCore);
58
64
  requirejs.define('@formily/react', () => formilyReact);
59
65
  requirejs.define('@formily/reactive', () => formilyReactive);
@@ -68,7 +74,9 @@ export function defineGlobalDeps(requirejs: RequireJS) {
68
74
 
69
75
  // utils
70
76
  requirejs.define('ahooks', () => ahooks);
77
+ requirejs.define('axios', () => axios);
71
78
  requirejs.define('dayjs', () => dayjs);
72
79
  requirejs.define('lodash', () => lodash);
73
80
  requirejs.define('@emotion/css', () => emotionCss);
81
+ requirejs.define('file-saver', () => FileSaver);
74
82
  }
@@ -12,33 +12,16 @@ import type { PluginClass } from '../PluginManager';
12
12
  import type { PluginData } from '../PluginManager';
13
13
  import type { RequireJS } from './requirejs';
14
14
 
15
- /**
16
- * @internal
17
- */
18
- export function defineDevPlugins(plugins: Record<string, PluginClass>) {
19
- Object.entries(plugins).forEach(([packageName, plugin]) => {
20
- window.define(`${packageName}/client-v2`, () => plugin);
21
- });
15
+ function getClientV2ModuleId(packageName: string) {
16
+ return `${packageName}/client-v2`;
22
17
  }
23
18
 
24
19
  /**
25
20
  * @internal
26
21
  */
27
- export function definePluginClient(packageName: string) {
28
- window.define(`${packageName}/client-v2`, ['exports', packageName], function (_exports: any, _pluginExports: any) {
29
- Object.defineProperty(_exports, '__esModule', {
30
- value: true,
31
- });
32
- Object.keys(_pluginExports).forEach(function (key) {
33
- if (key === '__esModule') return;
34
- if (key in _exports && _exports[key] === _pluginExports[key]) return;
35
- Object.defineProperty(_exports, key, {
36
- enumerable: true,
37
- get: function () {
38
- return _pluginExports[key];
39
- },
40
- });
41
- });
22
+ export function defineDevPlugins(plugins: Record<string, PluginClass>) {
23
+ Object.entries(plugins).forEach(([packageName, plugin]) => {
24
+ window.define(getClientV2ModuleId(packageName), () => plugin);
42
25
  });
43
26
  }
44
27
 
@@ -49,7 +32,7 @@ export function configRequirejs(requirejs: any, pluginData: PluginData[]) {
49
32
  requirejs.requirejs.config({
50
33
  waitSeconds: 120,
51
34
  paths: pluginData.reduce<Record<string, string>>((acc, cur) => {
52
- acc[cur.packageName] = cur.url;
35
+ acc[getClientV2ModuleId(cur.packageName)] = cur.url;
53
36
  return acc;
54
37
  }, {}),
55
38
  });
@@ -85,10 +68,7 @@ export function processRemotePlugins(pluginData: PluginData[], resolve: (plugins
85
68
  export function getRemotePlugins(requirejs: any, pluginData: PluginData[] = []): Promise<Array<[string, PluginClass]>> {
86
69
  configRequirejs(requirejs, pluginData);
87
70
 
88
- const packageNames = pluginData.map((item) => item.packageName);
89
- packageNames.forEach((packageName) => {
90
- definePluginClient(packageName);
91
- });
71
+ const packageNames = pluginData.map((item) => getClientV2ModuleId(item.packageName));
92
72
 
93
73
  return new Promise((resolve, reject) => {
94
74
  requirejs.requirejs(packageNames, processRemotePlugins(pluginData, resolve), reject);