@nocobase/client-v2 2.2.0-beta.3 → 2.2.0-beta.6

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 (133) hide show
  1. package/es/RouteRepository.d.ts +21 -6
  2. package/es/components/form/JsonTextArea.d.ts +2 -1
  3. package/es/components/form/VariableJsonTextArea.d.ts +19 -0
  4. package/es/components/form/index.d.ts +1 -0
  5. package/es/components/index.d.ts +1 -0
  6. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
  7. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
  8. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +2 -0
  9. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
  10. package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +4 -0
  11. package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
  12. package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
  13. package/es/flow/models/base/GridModel.d.ts +1 -1
  14. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +5 -0
  15. package/es/flow/models/blocks/form/FormBlockModel.d.ts +4 -0
  16. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +21 -0
  17. package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +21 -0
  18. package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
  19. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +2 -0
  20. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +1 -0
  21. package/es/flow/utils/dateTimeDisplayProps.d.ts +49 -0
  22. package/es/flow-compat/FieldValidation.d.ts +2 -1
  23. package/es/index.d.ts +2 -1
  24. package/es/index.mjs +167 -121
  25. package/lib/index.js +167 -121
  26. package/package.json +7 -7
  27. package/src/RouteRepository.ts +126 -24
  28. package/src/__tests__/RouteRepository.test.ts +216 -0
  29. package/src/__tests__/exports.test.ts +16 -0
  30. package/src/__tests__/settings-center.test.tsx +33 -0
  31. package/src/components/form/JsonTextArea.tsx +21 -11
  32. package/src/components/form/VariableJsonTextArea.tsx +175 -0
  33. package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
  34. package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
  35. package/src/components/form/index.tsx +1 -0
  36. package/src/components/index.ts +1 -0
  37. package/src/flow/__tests__/FlowRoute.test.tsx +443 -5
  38. package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
  39. package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
  40. package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +19 -5
  41. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +92 -0
  42. package/src/flow/actions/customVariable.tsx +1 -2
  43. package/src/flow/actions/dateTimeFormat.tsx +42 -28
  44. package/src/flow/actions/linkageRules.tsx +122 -34
  45. package/src/flow/actions/runjs.tsx +2 -14
  46. package/src/flow/actions/validation.tsx +62 -30
  47. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +33 -10
  48. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +310 -0
  49. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +44 -8
  50. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
  51. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
  52. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
  53. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
  54. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +21 -8
  55. package/src/flow/admin-shell/admin-layout/HelpLite.tsx +1 -3
  56. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +188 -0
  57. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
  58. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +166 -2
  59. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
  60. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
  61. package/src/flow/components/DefaultValue.tsx +1 -2
  62. package/src/flow/components/DynamicFlowsIcon.tsx +447 -126
  63. package/src/flow/components/FieldAssignRulesEditor.tsx +62 -12
  64. package/src/flow/components/FlowRoute.tsx +56 -11
  65. package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +148 -2
  66. package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +81 -4
  67. package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
  68. package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
  69. package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
  70. package/src/flow/flows/editMarkdownFlow.tsx +1 -1
  71. package/src/flow/index.ts +1 -1
  72. package/src/flow/internal/utils/operatorSchemaHelper.ts +15 -1
  73. package/src/flow/models/actions/JSActionModel.tsx +2 -7
  74. package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
  75. package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
  76. package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
  77. package/src/flow/models/base/ActionModel.tsx +12 -1
  78. package/src/flow/models/base/GridModel.tsx +38 -7
  79. package/src/flow/models/base/PageModel/PageModel.tsx +4 -1
  80. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +28 -4
  81. package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
  82. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
  83. package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
  84. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +31 -3
  85. package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
  86. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +77 -0
  87. package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +56 -1
  88. package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
  89. package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
  90. package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
  91. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
  92. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +880 -102
  93. package/src/flow/models/blocks/form/value-runtime/rules.ts +445 -13
  94. package/src/flow/models/blocks/form/value-runtime/runtime.ts +257 -13
  95. package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
  96. package/src/flow/models/blocks/js-block/JSBlock.tsx +2 -7
  97. package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
  98. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +1 -1
  99. package/src/flow/models/blocks/table/TableBlockModel.tsx +13 -3
  100. package/src/flow/models/blocks/table/TableColumnModel.tsx +36 -3
  101. package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +54 -0
  102. package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
  103. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +159 -1
  104. package/src/flow/models/fields/AssociationFieldModel/AssociationFieldModel.tsx +1 -1
  105. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +29 -6
  106. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +16 -4
  107. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +24 -1
  108. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +13 -1
  109. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +12 -0
  110. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableFieldModel.reset.test.ts +180 -0
  111. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -4
  112. package/src/flow/models/fields/AssociationFieldModel/__tests__/AssociationFieldModel.updateAssociation.test.ts +72 -0
  113. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +23 -0
  114. package/src/flow/models/fields/ClickableFieldModel.tsx +5 -0
  115. package/src/flow/models/fields/DisplayDateTimeFieldModel.tsx +29 -1
  116. package/src/flow/models/fields/InputFieldModel.tsx +14 -22
  117. package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
  118. package/src/flow/models/fields/JSFieldModel.tsx +2 -7
  119. package/src/flow/models/fields/JSItemModel.tsx +2 -14
  120. package/src/flow/models/fields/SelectFieldModel.tsx +6 -4
  121. package/src/flow/models/fields/__tests__/DisplayDateTimeFieldModel.test.tsx +96 -0
  122. package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +17 -0
  123. package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +43 -0
  124. package/src/flow/models/topbar/TopbarActionModel.tsx +93 -10
  125. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +258 -0
  126. package/src/flow/utils/dateTimeDisplayProps.ts +135 -0
  127. package/src/flow-compat/FieldValidation.tsx +122 -60
  128. package/src/flow-compat/Popover.tsx +43 -4
  129. package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
  130. package/src/index.ts +8 -1
  131. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
  132. package/src/settings-center/AdminSettingsLayout.tsx +3 -0
  133. package/src/settings-center/plugin-manager/index.tsx +3 -0
@@ -0,0 +1,43 @@
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 React from 'react';
11
+ import { describe, expect, it } from 'vitest';
12
+ import { SelectFieldModel } from '../SelectFieldModel';
13
+
14
+ function mockT(text: string) {
15
+ if (text === '{{t("Yes")}}') return '是';
16
+ if (text === '{{t("No")}}') return '否';
17
+ return text;
18
+ }
19
+
20
+ describe('SelectFieldModel', () => {
21
+ it('translates enum fallback labels for selected values', () => {
22
+ const model = {
23
+ props: {
24
+ value: true,
25
+ },
26
+ context: {
27
+ collectionField: {
28
+ uiSchema: {
29
+ enum: [
30
+ { label: '{{t("Yes")}}', value: true },
31
+ { label: '{{t("No")}}', value: false },
32
+ ],
33
+ },
34
+ },
35
+ },
36
+ translate: mockT,
37
+ } as unknown as SelectFieldModel;
38
+
39
+ const element = SelectFieldModel.prototype.render.call(model) as React.ReactElement;
40
+
41
+ expect(element.props.value).toEqual({ label: '是', value: true });
42
+ });
43
+ });
@@ -14,8 +14,9 @@ import { Button, Dropdown, theme, Tooltip, type ButtonProps, type MenuProps } fr
14
14
  import React, { useEffect, useMemo, useState } from 'react';
15
15
  import { useHotkeys } from 'react-hotkeys-hook';
16
16
  import { useTranslation } from 'react-i18next';
17
- import { Link } from 'react-router-dom';
17
+ import { Link, useLocation } from 'react-router-dom';
18
18
  import { useACLRoleContext } from '../../../acl';
19
+ import type { BaseApplication } from '../../../BaseApplication';
19
20
  import type { PluginSettingsPageType } from '../../../PluginSettingsManager';
20
21
  import { useApp } from '../../../hooks/useApp';
21
22
  import {
@@ -64,6 +65,89 @@ const topbarActionTriggerClassName = css`
64
65
  height: 100%;
65
66
  `;
66
67
 
68
+ type TopbarSettingsAppLike = Pick<BaseApplication<any>, 'router' | 'getPublicPath'>;
69
+
70
+ const normalizeTopbarPath = (pathname?: string) => {
71
+ const trimmed = pathname?.trim();
72
+ if (!trimmed || trimmed === '/') {
73
+ return '/';
74
+ }
75
+ return `/${trimmed.replace(/^\/+/, '')}`;
76
+ };
77
+
78
+ const normalizeTopbarBasePath = (pathname?: string) => {
79
+ const normalized = normalizeTopbarPath(pathname).replace(/\/+$/, '');
80
+ return normalized === '' || normalized === '/' ? '' : normalized;
81
+ };
82
+
83
+ const getTopbarRouterBasePath = (app: TopbarSettingsAppLike | undefined) => {
84
+ return app?.router?.getBasename?.() || app?.router?.basename || app?.getPublicPath?.() || '';
85
+ };
86
+
87
+ const stripTopbarRouterBasePath = (pathname: string, basename?: string) => {
88
+ const normalizedPath = normalizeTopbarPath(pathname);
89
+ const normalizedBase = normalizeTopbarBasePath(basename);
90
+
91
+ if (!normalizedBase) {
92
+ return normalizedPath;
93
+ }
94
+
95
+ if (normalizedPath === normalizedBase) {
96
+ return '/';
97
+ }
98
+
99
+ if (normalizedPath.startsWith(`${normalizedBase}/`)) {
100
+ return normalizeTopbarPath(normalizedPath.slice(normalizedBase.length));
101
+ }
102
+
103
+ return normalizedPath;
104
+ };
105
+
106
+ const isAdminRuntimePath = (pathname: string) => {
107
+ return pathname === '/admin' || pathname.startsWith('/admin/');
108
+ };
109
+
110
+ const buildTopbarDocumentHref = (targetPath: string, basename?: string) => {
111
+ const normalizedTarget = normalizeTopbarPath(targetPath);
112
+ const normalizedBase = normalizeTopbarBasePath(basename);
113
+
114
+ if (!normalizedBase || normalizedTarget === normalizedBase || normalizedTarget.startsWith(`${normalizedBase}/`)) {
115
+ return normalizedTarget;
116
+ }
117
+
118
+ return `${normalizedBase}${normalizedTarget}`;
119
+ };
120
+
121
+ function TopbarExternalSettingsLabel(props: { title: React.ReactNode; link: string }) {
122
+ return (
123
+ <div
124
+ onClick={() => {
125
+ window.open(props.link, '_blank', 'noopener,noreferrer');
126
+ }}
127
+ >
128
+ {props.title}
129
+ </div>
130
+ );
131
+ }
132
+
133
+ function TopbarInternalSettingsLabel(props: { title: React.ReactNode; path?: string }) {
134
+ const app = useApp();
135
+ const location = useLocation();
136
+ const targetPath = props.path || '/admin/settings';
137
+ const basename = getTopbarRouterBasePath(app);
138
+ const currentPath = stripTopbarRouterBasePath(location.pathname, basename);
139
+
140
+ if (isAdminRuntimePath(currentPath)) {
141
+ return <Link to={targetPath}>{props.title}</Link>;
142
+ }
143
+
144
+ return (
145
+ <a href={buildTopbarDocumentHref(targetPath, basename)} target="_blank" rel="noopener noreferrer">
146
+ {props.title}
147
+ </a>
148
+ );
149
+ }
150
+
67
151
  /**
68
152
  * 将 settings 顶部菜单转换成右上角 dropdown items。
69
153
  *
@@ -112,15 +196,9 @@ export function getTopbarPluginSettingsItems(options: {
112
196
  title: targetTitle,
113
197
  icon: item.icon,
114
198
  label: targetLink ? (
115
- <div
116
- onClick={() => {
117
- window.open(targetLink, '_blank', 'noopener,noreferrer');
118
- }}
119
- >
120
- {targetTitle}
121
- </div>
199
+ <TopbarExternalSettingsLabel title={targetTitle} link={targetLink} />
122
200
  ) : (
123
- <Link to={targetPath || '/admin/settings'}>{targetTitle}</Link>
201
+ <TopbarInternalSettingsLabel title={targetTitle} path={targetPath} />
124
202
  ),
125
203
  };
126
204
  });
@@ -131,7 +209,12 @@ export function getTopbarPluginSettingsItems(options: {
131
209
  items.push({
132
210
  key: pluginManagerSetting.key,
133
211
  icon: pluginManagerSetting.icon || <ApiOutlined />,
134
- label: <Link to={pluginManagerSetting.path}>{pluginManagerSetting.title || t('Plugin manager')}</Link>,
212
+ label: (
213
+ <TopbarInternalSettingsLabel
214
+ title={pluginManagerSetting.title || t('Plugin manager')}
215
+ path={pluginManagerSetting.path}
216
+ />
217
+ ),
135
218
  });
136
219
  }
137
220
 
@@ -48,6 +48,264 @@ describe('dateTimeFormat', () => {
48
48
  });
49
49
  });
50
50
 
51
+ it('uses the association title field when deciding the format schema', () => {
52
+ const ctx = {
53
+ model: {
54
+ props: {
55
+ titleField: 'shipmentsTime',
56
+ },
57
+ context: {
58
+ collectionField: {
59
+ type: 'belongsTo',
60
+ targetCollection: {
61
+ getField: (name) =>
62
+ name === 'shipmentsTime'
63
+ ? {
64
+ type: 'time',
65
+ interface: 'time',
66
+ }
67
+ : null,
68
+ },
69
+ },
70
+ },
71
+ },
72
+ };
73
+
74
+ expect(Object.keys(dateTimeFormat.uiSchema(ctx))).toEqual(['timeFormat']);
75
+ });
76
+
77
+ it('saves association title time field format as a time format', () => {
78
+ const setProps = vi.fn();
79
+ const ctx = {
80
+ model: {
81
+ props: {
82
+ titleField: 'shipmentsTime',
83
+ },
84
+ context: {
85
+ collectionField: {
86
+ type: 'belongsTo',
87
+ targetCollection: {
88
+ getField: (name) =>
89
+ name === 'shipmentsTime'
90
+ ? {
91
+ type: 'time',
92
+ interface: 'time',
93
+ }
94
+ : null,
95
+ },
96
+ },
97
+ },
98
+ setProps,
99
+ },
100
+ };
101
+
102
+ dateTimeFormat.handler(ctx, { timeFormat: 'hh:mm:ss a' });
103
+
104
+ expect(setProps).toHaveBeenCalledWith({
105
+ timeFormat: 'hh:mm:ss a',
106
+ format: 'hh:mm:ss a',
107
+ });
108
+ });
109
+
110
+ it('applies and persists date time format params when settings are saved', async () => {
111
+ const setProps = vi.fn();
112
+ const save = vi.fn();
113
+ const ctx = {
114
+ model: {
115
+ props: {
116
+ titleField: 'shipmentsDatetime',
117
+ },
118
+ context: {
119
+ collectionField: {
120
+ type: 'belongsTo',
121
+ targetCollection: {
122
+ getField: (name) =>
123
+ name === 'shipmentsDatetime'
124
+ ? {
125
+ type: 'datetime',
126
+ interface: 'datetime',
127
+ }
128
+ : null,
129
+ },
130
+ },
131
+ },
132
+ setProps,
133
+ save,
134
+ },
135
+ };
136
+
137
+ await dateTimeFormat.beforeParamsSave(ctx, {
138
+ picker: 'date',
139
+ dateFormat: 'YYYY-MM-DD',
140
+ showTime: true,
141
+ timeFormat: 'hh:mm:ss a',
142
+ });
143
+
144
+ expect(setProps).toHaveBeenCalledWith({
145
+ picker: 'date',
146
+ dateFormat: 'YYYY-MM-DD',
147
+ showTime: true,
148
+ timeFormat: 'hh:mm:ss a',
149
+ format: 'YYYY-MM-DD hh:mm:ss a',
150
+ });
151
+ expect(save).toHaveBeenCalled();
152
+ });
153
+
154
+ it('syncs table association column props when title date time format settings are saved', async () => {
155
+ const setProps = vi.fn();
156
+ const save = vi.fn();
157
+ const setParentProps = vi.fn();
158
+ const model = {
159
+ props: {
160
+ titleField: 'shipmentsDatetime',
161
+ },
162
+ context: {
163
+ collectionField: {
164
+ type: 'belongsTo',
165
+ targetCollection: {
166
+ getField: (name) =>
167
+ name === 'shipmentsDatetime'
168
+ ? {
169
+ type: 'datetime',
170
+ interface: 'datetime',
171
+ }
172
+ : null,
173
+ },
174
+ },
175
+ },
176
+ setProps,
177
+ save,
178
+ parent: {
179
+ use: 'TableColumnModel',
180
+ collectionField: {
181
+ isAssociationField: () => true,
182
+ },
183
+ setProps: setParentProps,
184
+ },
185
+ };
186
+ model.parent['subModels'] = {
187
+ field: model,
188
+ };
189
+
190
+ await dateTimeFormat.beforeParamsSave(
191
+ { model },
192
+ {
193
+ picker: 'date',
194
+ dateFormat: 'YYYY-MM-DD',
195
+ showTime: true,
196
+ timeFormat: 'hh:mm:ss a',
197
+ },
198
+ );
199
+
200
+ expect(setParentProps).toHaveBeenCalledWith({
201
+ picker: 'date',
202
+ dateFormat: 'YYYY-MM-DD',
203
+ showTime: true,
204
+ timeFormat: 'hh:mm:ss a',
205
+ format: 'YYYY-MM-DD hh:mm:ss a',
206
+ });
207
+ expect(save).toHaveBeenCalled();
208
+ });
209
+
210
+ it('hides time format for association title date-only fields', () => {
211
+ const ctx = {
212
+ model: {
213
+ props: {
214
+ titleField: 'shipmentsDateOnly',
215
+ showTime: true,
216
+ },
217
+ context: {
218
+ collectionField: {
219
+ type: 'belongsTo',
220
+ targetCollection: {
221
+ getField: (name) =>
222
+ name === 'shipmentsDateOnly'
223
+ ? {
224
+ type: 'dateOnly',
225
+ interface: 'date',
226
+ getComponentProps: () => ({
227
+ dateOnly: true,
228
+ showTime: false,
229
+ }),
230
+ }
231
+ : null,
232
+ },
233
+ },
234
+ },
235
+ },
236
+ };
237
+ const schema = dateTimeFormat.uiSchema(ctx);
238
+ const timeFormatField: any = schema.timeFormat;
239
+ const showTimeField: any = schema.showTime;
240
+ const timeFormatState = {
241
+ hidden: false,
242
+ form: {
243
+ values: {
244
+ picker: 'date',
245
+ showTime: true,
246
+ },
247
+ },
248
+ };
249
+ const showTimeState = {
250
+ hidden: false,
251
+ value: true,
252
+ form: {
253
+ values: {
254
+ picker: 'date',
255
+ },
256
+ },
257
+ };
258
+
259
+ timeFormatField['x-reactions'][0](timeFormatState);
260
+ showTimeField['x-reactions'][1](showTimeState);
261
+
262
+ expect(timeFormatState.hidden).toBe(true);
263
+ expect(showTimeState.hidden).toBe(true);
264
+ expect(showTimeState.value).toBe(false);
265
+ expect(dateTimeFormat.defaultParams(ctx)).toMatchObject({
266
+ showTime: false,
267
+ });
268
+ });
269
+
270
+ it('saves association title date-only field format without time even when params contain showTime', () => {
271
+ const setProps = vi.fn();
272
+ const ctx = {
273
+ model: {
274
+ props: {
275
+ titleField: 'shipmentsDateOnly',
276
+ },
277
+ context: {
278
+ collectionField: {
279
+ type: 'belongsTo',
280
+ targetCollection: {
281
+ getField: (name) =>
282
+ name === 'shipmentsDateOnly'
283
+ ? {
284
+ type: 'dateOnly',
285
+ interface: 'date',
286
+ }
287
+ : null,
288
+ },
289
+ },
290
+ },
291
+ setProps,
292
+ },
293
+ };
294
+
295
+ dateTimeFormat.handler(ctx, {
296
+ dateFormat: 'YYYY-MM-DD',
297
+ showTime: true,
298
+ timeFormat: 'HH:mm:ss',
299
+ });
300
+
301
+ expect(setProps).toHaveBeenCalledWith({
302
+ dateFormat: 'YYYY-MM-DD',
303
+ showTime: false,
304
+ timeFormat: 'HH:mm:ss',
305
+ format: 'YYYY-MM-DD',
306
+ });
307
+ });
308
+
51
309
  it('uses format as the default time format when timeFormat is missing', () => {
52
310
  const ctx = {
53
311
  model: {
@@ -0,0 +1,135 @@
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 type { FlowModelContext } from '@nocobase/flow-engine';
11
+
12
+ type DateTimeDisplayProps = {
13
+ dateOnly?: boolean;
14
+ dateFormat?: string;
15
+ format?: string;
16
+ picker?: string;
17
+ showTime?: boolean;
18
+ timeFormat?: string;
19
+ };
20
+
21
+ type DateTimeCollectionField = {
22
+ type?: string;
23
+ interface?: string;
24
+ getComponentProps?: () => DateTimeDisplayProps;
25
+ targetCollection?: {
26
+ getField?: (name?: string) => DateTimeCollectionField | undefined;
27
+ };
28
+ };
29
+
30
+ type DateTimeModelContext = FlowModelContext & {
31
+ collectionField?: DateTimeCollectionField;
32
+ };
33
+
34
+ type DateTimeModel = {
35
+ props?: DateTimeDisplayProps & {
36
+ titleField?: string;
37
+ };
38
+ context?: DateTimeModelContext;
39
+ getStepParams?: (flowKey: string, stepKey: string) => DateTimeDisplayProps | undefined;
40
+ };
41
+
42
+ type ResolveDateTimeDisplayPropsOptions = {
43
+ model?: DateTimeModel;
44
+ collectionField?: DateTimeCollectionField;
45
+ titleField?: string;
46
+ currentProps?: DateTimeDisplayProps;
47
+ params?: DateTimeDisplayProps;
48
+ withDefaults?: boolean;
49
+ };
50
+
51
+ const dateTimeDisplayPropKeys: Array<keyof DateTimeDisplayProps> = [
52
+ 'dateOnly',
53
+ 'dateFormat',
54
+ 'format',
55
+ 'picker',
56
+ 'showTime',
57
+ 'timeFormat',
58
+ ];
59
+
60
+ const pickDateTimeDisplayProps = (source?: DateTimeDisplayProps) => {
61
+ if (!source) {
62
+ return {};
63
+ }
64
+
65
+ const result: DateTimeDisplayProps = {};
66
+ for (const key of dateTimeDisplayPropKeys) {
67
+ if (key === 'dateOnly' || key === 'showTime') {
68
+ if (typeof source[key] !== 'undefined') {
69
+ result[key] = source[key];
70
+ }
71
+ } else if (typeof source[key] !== 'undefined') {
72
+ result[key] = source[key];
73
+ }
74
+ }
75
+ return result;
76
+ };
77
+
78
+ const stripUndefined = (props: DateTimeDisplayProps) =>
79
+ Object.fromEntries(Object.entries(props).filter(([, value]) => typeof value !== 'undefined')) as DateTimeDisplayProps;
80
+
81
+ const getModelCollectionField = (model?: DateTimeModel) => model?.context?.collectionField;
82
+
83
+ export const getDateTimeFormatCollectionField = (options: ResolveDateTimeDisplayPropsOptions) => {
84
+ const collectionField = options.collectionField || getModelCollectionField(options.model);
85
+ const titleField = options.titleField || options.model?.props?.titleField;
86
+ return collectionField?.targetCollection?.getField?.(titleField) || collectionField;
87
+ };
88
+
89
+ export const isTimeCollectionField = (collectionField?: DateTimeCollectionField) =>
90
+ collectionField?.type === 'time' || collectionField?.interface === 'time';
91
+
92
+ export const isDateOnlyCollectionField = (collectionField?: DateTimeCollectionField) =>
93
+ collectionField?.type === 'dateOnly' || collectionField?.interface === 'dateOnly';
94
+
95
+ export const getSavedDateTimeFormatParams = (model?: DateTimeModel) =>
96
+ model?.getStepParams?.('datetimeSettings', 'dateFormat') || model?.getStepParams?.('timeSettings', 'dateFormat');
97
+
98
+ export const resolveDateTimeDisplayProps = (options: ResolveDateTimeDisplayPropsOptions) => {
99
+ const collectionField = options.collectionField || getModelCollectionField(options.model);
100
+ const targetCollectionField = getDateTimeFormatCollectionField(options);
101
+ const mergedProps = {
102
+ ...pickDateTimeDisplayProps(collectionField?.getComponentProps?.()),
103
+ ...pickDateTimeDisplayProps(
104
+ targetCollectionField !== collectionField ? targetCollectionField?.getComponentProps?.() : undefined,
105
+ ),
106
+ ...pickDateTimeDisplayProps(options.currentProps || options.model?.props),
107
+ ...pickDateTimeDisplayProps(getSavedDateTimeFormatParams(options.model)),
108
+ ...pickDateTimeDisplayProps(options.params),
109
+ };
110
+
111
+ if (isTimeCollectionField(targetCollectionField)) {
112
+ const timeFormat = mergedProps.timeFormat || mergedProps.format || 'HH:mm:ss';
113
+ return stripUndefined({
114
+ ...mergedProps,
115
+ timeFormat,
116
+ format: timeFormat,
117
+ });
118
+ }
119
+
120
+ const picker = mergedProps.picker || (options.withDefaults ? 'date' : undefined);
121
+ const dateFormat = mergedProps.dateFormat || (options.withDefaults ? 'YYYY-MM-DD' : undefined);
122
+ const timeFormat = mergedProps.timeFormat || (options.withDefaults ? 'HH:mm:ss' : undefined);
123
+ const showTime = isDateOnlyCollectionField(targetCollectionField) ? false : mergedProps.showTime;
124
+ const finalDateFormat = dateFormat || 'YYYY-MM-DD';
125
+ const finalTimeFormat = timeFormat || 'HH:mm:ss';
126
+
127
+ return stripUndefined({
128
+ ...mergedProps,
129
+ picker,
130
+ dateFormat,
131
+ timeFormat,
132
+ showTime,
133
+ format: showTime ? `${finalDateFormat} ${finalTimeFormat}` : finalDateFormat,
134
+ });
135
+ };