@nocobase/client-v2 2.3.0-alpha.1 → 2.3.0-beta.11

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 (173) hide show
  1. package/es/APIClient.d.ts +12 -0
  2. package/es/components/form/ScanInput/useCodeScanner.d.ts +12 -2
  3. package/es/components/form/ScanInput/zxingWasmDecoder.d.ts +9 -0
  4. package/es/flow/actions/linkageRules.d.ts +25 -0
  5. package/es/flow/components/FieldAssignValueInput.d.ts +6 -30
  6. package/es/flow/components/FlowRoute.d.ts +2 -2
  7. package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
  8. package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +32 -0
  9. package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
  10. package/es/flow/components/field-value-variable/index.d.ts +11 -0
  11. package/es/flow/components/filter/VariableFilterItem.d.ts +7 -0
  12. package/es/flow/components/placeholders/BlockPlaceholder.d.ts +1 -0
  13. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  14. package/es/flow/internal/utils/operatorSchemaHelper.d.ts +2 -2
  15. package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
  16. package/es/flow/models/base/ActionModelCore.d.ts +4 -2
  17. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  18. package/es/flow/models/base/CollectionBlockModel.d.ts +3 -0
  19. package/es/flow/models/blocks/details/DetailsBlockModel.d.ts +3 -0
  20. package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
  21. package/es/flow/models/blocks/filter-form/FilterFormSubmitActionModel.d.ts +3 -1
  22. package/es/flow/models/blocks/form/FormBlockModel.d.ts +7 -0
  23. package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
  24. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
  25. package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
  26. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  27. package/es/flow/models/blocks/table/TableColumnModel.d.ts +12 -0
  28. package/es/flow/models/blocks/table/utils.d.ts +1 -0
  29. package/es/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.d.ts +2 -1
  30. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +0 -17
  31. package/es/flow/models/fields/AssociationFieldModel/recordSelectShared.d.ts +41 -0
  32. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  33. package/es/flow-compat/passwordUtils.d.ts +1 -1
  34. package/es/index.d.ts +1 -0
  35. package/es/index.mjs +148 -145
  36. package/es/ui-operation/index.d.ts +15 -0
  37. package/es/ui-operation/ui-operation-codec.d.ts +10 -0
  38. package/lib/index.js +170 -167
  39. package/lib/locale/languageCodes.js +1 -0
  40. package/package.json +10 -8
  41. package/src/APIClient.ts +92 -0
  42. package/src/Application.tsx +3 -1
  43. package/src/__tests__/APIClient.test.tsx +58 -0
  44. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +7 -2
  45. package/src/__tests__/settings-center.test.tsx +270 -6
  46. package/src/acl/ACLProvider.tsx +2 -0
  47. package/src/acl/__tests__/ACLProvider.test.tsx +92 -0
  48. package/src/collection-manager/__tests__/field-configure.test.ts +52 -0
  49. package/src/collection-manager/field-configure.ts +2 -2
  50. package/src/components/form/ScanInput/CodeScanner.tsx +64 -35
  51. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +107 -0
  52. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +294 -24
  53. package/src/components/form/ScanInput/__tests__/zxingWasmDecoder.test.ts +78 -0
  54. package/src/components/form/ScanInput/useCodeScanner.ts +281 -23
  55. package/src/components/form/ScanInput/zxingWasmDecoder.ts +64 -0
  56. package/src/components/form/filter/CollectionFilterItem.tsx +4 -2
  57. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +17 -0
  58. package/src/components/form/table/Table.tsx +81 -6
  59. package/src/components/form/table/__tests__/Table.columnWidth.test.tsx +433 -0
  60. package/src/flow/__tests__/FlowRoute.test.tsx +126 -8
  61. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  62. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  63. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  64. package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
  65. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +72 -0
  66. package/src/flow/actions/__tests__/validation.test.ts +48 -0
  67. package/src/flow/actions/linkageRules.tsx +59 -16
  68. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +78 -59
  69. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +228 -0
  70. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +42 -23
  71. package/src/flow/common/Markdown/style.ts +4 -0
  72. package/src/flow/components/BlockItemCard.tsx +2 -2
  73. package/src/flow/components/FieldAssignValueInput.tsx +42 -621
  74. package/src/flow/components/FlowRoute.tsx +71 -29
  75. package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
  76. package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +335 -0
  77. package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +389 -0
  78. package/src/flow/components/field-value-variable/dateValue.ts +223 -0
  79. package/src/flow/components/field-value-variable/index.ts +12 -0
  80. package/src/flow/components/filter/VariableFilterItem.tsx +23 -6
  81. package/src/flow/components/filter/__tests__/VariableFilterItem.rightMetaTree.test.tsx +158 -0
  82. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +125 -0
  83. package/src/flow/components/placeholders/BlockPlaceholder.tsx +70 -23
  84. package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +57 -7
  85. package/src/flow/index.ts +2 -0
  86. package/src/flow/internal/components/Markdown/DisplayMarkdown.tsx +14 -9
  87. package/src/flow/internal/components/Markdown/__tests__/DisplayMarkdown.test.tsx +46 -0
  88. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  89. package/src/flow/internal/utils/operatorSchemaHelper.ts +3 -2
  90. package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
  91. package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
  92. package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
  93. package/src/flow/models/base/ActionModelCore.tsx +11 -2
  94. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  95. package/src/flow/models/base/CollectionBlockModel.tsx +38 -3
  96. package/src/flow/models/base/GridModel.tsx +0 -1
  97. package/src/flow/models/base/PageModel/PageTabModel.tsx +67 -17
  98. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +509 -12
  99. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  100. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  101. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  102. package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +125 -9
  103. package/src/flow/models/blocks/assign-form/AssignFormGridModel.tsx +22 -1
  104. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +36 -50
  105. package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.editor.test.tsx +140 -0
  106. package/src/flow/models/blocks/details/DetailsBlockModel.tsx +8 -3
  107. package/src/flow/models/blocks/details/__tests__/DetailsBlockModel.initialPaginationChangeRefresh.test.ts +32 -1
  108. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +27 -3
  109. package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +36 -5
  110. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +129 -14
  111. package/src/flow/models/blocks/filter-form/FilterFormSubmitActionModel.tsx +57 -4
  112. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.onModelCreated.test.ts +174 -1
  113. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +29 -0
  114. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +359 -1
  115. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +72 -0
  116. package/src/flow/models/blocks/filter-form/__tests__/FilterFormSubmitActionModel.lifecycle.test.ts +90 -0
  117. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +32 -4
  118. package/src/flow/models/blocks/filter-form/fields/FieldComponentProps.tsx +1 -1
  119. package/src/flow/models/blocks/filter-form/fields/__tests__/FieldComponentProps.options.test.tsx +61 -0
  120. package/src/flow/models/blocks/filter-manager/FilterManager.ts +5 -0
  121. package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +40 -0
  122. package/src/flow/models/blocks/form/FormBlockModel.tsx +73 -5
  123. package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
  124. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +153 -2
  125. package/src/flow/models/blocks/form/__tests__/popupLinkage.test.tsx +175 -0
  126. package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
  127. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
  128. package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
  129. package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
  130. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  131. package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
  132. package/src/flow/models/blocks/table/TableBlockModel.tsx +28 -2
  133. package/src/flow/models/blocks/table/TableColumnModel.tsx +48 -9
  134. package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
  135. package/src/flow/models/blocks/table/__tests__/TableBlockModel.filterReset.test.ts +78 -0
  136. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  137. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
  138. package/src/flow/models/blocks/table/utils.ts +7 -0
  139. package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +33 -1
  140. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +12 -3
  141. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
  142. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
  143. package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +29 -3
  144. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +9 -136
  145. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +5 -1
  146. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +191 -3
  147. package/src/flow/models/fields/AssociationFieldModel/recordSelectShared.tsx +156 -0
  148. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  149. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  150. package/src/flow/models/fields/DisplayAssociationField/DisplaySubTableFieldModel.tsx +42 -7
  151. package/src/flow/models/fields/DisplayAssociationField/__tests__/DisplaySubTableFieldModel.test.tsx +351 -0
  152. package/src/flow/models/fields/DisplayHtmlFieldModel.tsx +2 -1
  153. package/src/flow/models/fields/DisplayNumberFieldModel.tsx +1 -1
  154. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  155. package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
  156. package/src/flow/models/fields/RichTextFieldModel/__tests__/RichTextFieldModel.test.tsx +74 -0
  157. package/src/flow/models/fields/RichTextFieldModel/index.tsx +27 -6
  158. package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
  159. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  160. package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
  161. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +6 -0
  162. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +27 -2
  163. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +139 -13
  164. package/src/flow/models/topbar/TopbarActionModel.tsx +5 -5
  165. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  166. package/src/index.ts +1 -0
  167. package/src/layout-manager/LayoutContentRoute.tsx +2 -1
  168. package/src/layout-manager/LayoutRoute.tsx +2 -1
  169. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
  170. package/src/locale/languageCodes.ts +1 -0
  171. package/src/settings-center/AdminSettingsLayout.tsx +23 -2
  172. package/src/ui-operation/index.ts +33 -0
  173. package/src/ui-operation/ui-operation-codec.ts +54 -0
@@ -8,9 +8,11 @@
8
8
  */
9
9
 
10
10
  import { type FlowEngine, useFlowContext, useFlowEngine } from '@nocobase/flow-engine';
11
+ import { Button, Result } from 'antd';
11
12
  import React, { useEffect, useMemo, useRef, useState } from 'react';
12
- import { deviceType } from 'react-device-detect';
13
+ import { useTranslation } from 'react-i18next';
13
14
  import { useLocation, useNavigate, useParams } from 'react-router-dom';
15
+ import { getModernClientPrefix, stripModernClientPrefix } from '../../authRedirect';
14
16
  import { useApp } from '../../hooks/useApp';
15
17
  import { NocoBaseDesktopRouteType, type NocoBaseDesktopRoute } from '../../flow-compat';
16
18
  import {
@@ -22,12 +24,14 @@ import { getLayoutModel, type BaseLayoutModel } from '../admin-shell/BaseLayoutM
22
24
  import { useLayoutRoutePage } from '../admin-shell/useLayoutRoutePage';
23
25
  import { AppNotFound } from '../../components';
24
26
  import { useKeepAlive } from '../../components/KeepAlive';
27
+ import { registerDeviceTypeContext } from '../internal/registerDeviceTypeContext';
25
28
 
26
29
  type FlowRouteGuardState = {
27
30
  pageUid?: string;
28
31
  pending: boolean;
29
32
  allowBridge: boolean;
30
33
  notFound: boolean;
34
+ legacyPageUnsupported?: boolean;
31
35
  };
32
36
 
33
37
  export type LegacyPageBehavior = 'redirect' | 'notFound' | 'bridge';
@@ -156,31 +160,9 @@ const BridgeFlowRoute = ({
156
160
  const layoutContentRef = useRef<HTMLDivElement>(null);
157
161
 
158
162
  useEffect(() => {
159
- flowEngine.context.defineProperty('deviceType', {
160
- get: () => (deviceType === 'browser' ? 'computer' : deviceType),
161
- cache: false,
162
- meta: {
163
- type: 'string',
164
- title: flowEngine.translate('Current device type'),
165
- interface: 'select',
166
- uiSchema: {
167
- enum: [
168
- { label: flowEngine.translate('Computer'), value: 'computer' },
169
- { label: flowEngine.translate('Mobile'), value: 'mobile' },
170
- { label: flowEngine.translate('Tablet'), value: 'tablet' },
171
- { label: flowEngine.translate('SmartTv'), value: 'smarttv' },
172
- { label: flowEngine.translate('Console'), value: 'console' },
173
- { label: flowEngine.translate('Wearable'), value: 'wearable' },
174
- { label: flowEngine.translate('Embedded'), value: 'embedded' },
175
- ],
176
- 'x-component': 'Select',
177
- },
178
- },
179
- info: {
180
- description: 'Current device type (computer/mobile/tablet/...).',
181
- detail: 'string',
182
- },
183
- });
163
+ if (!flowEngine.context.getPropertyOptions('deviceType')) {
164
+ registerDeviceTypeContext(flowEngine);
165
+ }
184
166
  }, [flowEngine]);
185
167
 
186
168
  useLayoutRoutePage({
@@ -195,11 +177,58 @@ const BridgeFlowRoute = ({
195
177
  return <div ref={layoutContentRef} />;
196
178
  };
197
179
 
180
+ type RouteLocation = {
181
+ pathname: string;
182
+ search: string;
183
+ hash: string;
184
+ };
185
+
186
+ const getLegacyPageHref = (app: { getPublicPath: () => string }, location: RouteLocation) => {
187
+ const modernPublicPath = app.getPublicPath();
188
+ const browserLocationMatchesPublicPath = window.location.pathname.startsWith(modernPublicPath);
189
+ const currentLocation = browserLocationMatchesPublicPath ? window.location : location;
190
+ const pathWithinModernClient = currentLocation.pathname.startsWith(modernPublicPath)
191
+ ? currentLocation.pathname.slice(modernPublicPath.length)
192
+ : currentLocation.pathname.replace(/^\/+/, '');
193
+ return `${stripModernClientPrefix(modernPublicPath)}${pathWithinModernClient}${currentLocation.search}${
194
+ currentLocation.hash
195
+ }`;
196
+ };
197
+
198
+ const LegacyPageUnsupported = ({
199
+ app,
200
+ location,
201
+ }: {
202
+ app: { getPublicPath: () => string };
203
+ location: RouteLocation;
204
+ }) => {
205
+ const { t } = useTranslation();
206
+ const modernClientPath = `/${getModernClientPrefix()}/`;
207
+ const withModernClientPath = (message: string) => message.replaceAll('{{modernClientPath}}', modernClientPath);
208
+
209
+ return (
210
+ <Result
211
+ status="warning"
212
+ title={withModernClientPath(t('This page is not supported in the {{modernClientPath}} branch'))}
213
+ subTitle={withModernClientPath(
214
+ t(
215
+ 'The {{modernClientPath}} branch only supports new pages. This page is a legacy page. Please open it from the original entry.',
216
+ ),
217
+ )}
218
+ extra={
219
+ <Button href={getLegacyPageHref(app, location)} type="primary">
220
+ {t('Open from the original entry')}
221
+ </Button>
222
+ }
223
+ />
224
+ );
225
+ };
226
+
198
227
  /**
199
228
  * 管理后台动态页面路由组件。
200
229
  *
201
- * 负责读取当前路由页面 UID,补充运行时设备变量,
202
- * 并把页面生命周期桥接到 AdminLayout host model。
230
+ * 负责读取当前路由页面 UID,并把页面生命周期桥接到 AdminLayout host model。
231
+ * 设备变量通常由 PluginFlowEngine 共享初始化提供;独立渲染时会在挂载后补充注册。
203
232
  *
204
233
  * @example
205
234
  * ```tsx
@@ -377,7 +406,13 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
377
406
 
378
407
  if (target.reason === 'unsupportedV2Runtime') {
379
408
  if (active && requestId === requestIdRef.current) {
380
- setGuardState({ pageUid, pending: false, allowBridge: false, notFound: true });
409
+ setGuardState({
410
+ pageUid,
411
+ pending: false,
412
+ allowBridge: false,
413
+ notFound: false,
414
+ legacyPageUnsupported: true,
415
+ });
381
416
  }
382
417
  return;
383
418
  }
@@ -414,6 +449,10 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
414
449
  return <AppNotFound />;
415
450
  }
416
451
 
452
+ if (guardState.legacyPageUnsupported) {
453
+ return <LegacyPageUnsupported app={app} location={location} />;
454
+ }
455
+
417
456
  if (!guardState.allowBridge) {
418
457
  return null;
419
458
  }
@@ -421,11 +460,14 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
421
460
  return <BridgeFlowRoute pageUid={pageUid} active={active} getLayoutModel={getLayoutModel} />;
422
461
  }, [
423
462
  active,
463
+ app,
424
464
  getLayoutModel,
425
465
  guardState.allowBridge,
466
+ guardState.legacyPageUnsupported,
426
467
  guardState.notFound,
427
468
  guardState.pageUid,
428
469
  guardState.pending,
470
+ location,
429
471
  pageUid,
430
472
  ]);
431
473
 
@@ -0,0 +1,181 @@
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 { useFlowContext, type CtxDateExpressionConfig, type CtxDateRelativeUnit } from '@nocobase/flow-engine';
11
+ import { AutoComplete, Input, InputNumber, Select, Space } from 'antd';
12
+ import type { Dayjs } from 'dayjs';
13
+ import React from 'react';
14
+ import { FieldAssignExactDatePicker, type ExactDatePickerValue } from '../FieldAssignExactDatePicker';
15
+ import { toExactPickerDisplayValue, type DateVariableComponentProps } from './dateValue';
16
+
17
+ const DATE_FORMAT_OPTIONS = [
18
+ 'YYYY-MM-DD',
19
+ 'YYYY-MM-DD HH:mm:ss',
20
+ 'YYYY/MM/DD',
21
+ 'MM/DD/YYYY',
22
+ 'DD/MM/YYYY',
23
+ 'YYYYMMDD',
24
+ ].map((value) => ({ value }));
25
+
26
+ const PRESET_LABELS: Record<string, string> = {
27
+ now: 'Now',
28
+ today: 'Today',
29
+ yesterday: 'Yesterday',
30
+ tomorrow: 'Tomorrow',
31
+ thisWeek: 'This Week',
32
+ lastWeek: 'Last Week',
33
+ nextWeek: 'Next Week',
34
+ thisMonth: 'This Month',
35
+ lastMonth: 'Last Month',
36
+ nextMonth: 'Next Month',
37
+ thisQuarter: 'This Quarter',
38
+ lastQuarter: 'Last Quarter',
39
+ nextQuarter: 'Next Quarter',
40
+ thisYear: 'This Year',
41
+ lastYear: 'Last Year',
42
+ nextYear: 'Next Year',
43
+ };
44
+
45
+ export function getDefaultDateVariableFormat(config: CtxDateExpressionConfig): string {
46
+ return config.kind === 'preset' && config.preset === 'now' ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD';
47
+ }
48
+
49
+ export function serializeExactDatePickerValue(
50
+ value: ExactDatePickerValue,
51
+ showTime: boolean,
52
+ ): string | [string, string] {
53
+ const serialize = (item: Dayjs) => (showTime ? item.toISOString() : item.format('YYYY-MM-DD'));
54
+ if (Array.isArray(value)) {
55
+ return [serialize(value[0]), serialize(value[1])];
56
+ }
57
+ return value ? serialize(value) : '';
58
+ }
59
+
60
+ type DateVariableEditorProps = {
61
+ value?: CtxDateExpressionConfig;
62
+ onChange?: (value: CtxDateExpressionConfig) => void;
63
+ isDateLikeField: boolean;
64
+ dateComponentProps: DateVariableComponentProps;
65
+ style?: React.CSSProperties;
66
+ disabled?: boolean;
67
+ };
68
+
69
+ const DateFormatEditor: React.FC<{
70
+ value: string;
71
+ placeholder: string;
72
+ onChange: (value: string) => void;
73
+ disabled?: boolean;
74
+ }> = ({ value, placeholder, onChange, disabled }) => {
75
+ const ctx = useFlowContext();
76
+ return (
77
+ <Space.Compact style={{ width: '100%' }}>
78
+ <Input
79
+ value={ctx.t('Format')}
80
+ readOnly
81
+ disabled={disabled}
82
+ tabIndex={-1}
83
+ aria-hidden
84
+ style={{ width: 88, pointerEvents: 'none' }}
85
+ />
86
+ <AutoComplete
87
+ value={value}
88
+ placeholder={placeholder}
89
+ options={DATE_FORMAT_OPTIONS}
90
+ onChange={(nextValue) => onChange(nextValue.slice(0, 128))}
91
+ disabled={disabled}
92
+ style={{ flex: 1, minWidth: 0 }}
93
+ aria-label={ctx.t('Format')}
94
+ />
95
+ </Space.Compact>
96
+ );
97
+ };
98
+
99
+ export const DateVariableEditor: React.FC<DateVariableEditorProps> = ({
100
+ value,
101
+ onChange,
102
+ isDateLikeField,
103
+ dateComponentProps,
104
+ style,
105
+ disabled = false,
106
+ }) => {
107
+ const ctx = useFlowContext();
108
+ if (!value) return null;
109
+
110
+ const updateFormat = (format: string) => onChange?.({ ...value, format });
111
+ const format = value.format || '';
112
+
113
+ const renderValueEditor = () => {
114
+ if (value.kind === 'exact') {
115
+ const isRange = Array.isArray(value.value);
116
+ return (
117
+ <FieldAssignExactDatePicker
118
+ picker={dateComponentProps.picker}
119
+ showTime={dateComponentProps.showTime}
120
+ timeFormat={dateComponentProps.timeFormat}
121
+ format={dateComponentProps.format}
122
+ isRange={isRange}
123
+ value={toExactPickerDisplayValue(value.value, {
124
+ format: dateComponentProps.format,
125
+ isRange,
126
+ })}
127
+ onChange={(nextValue) => {
128
+ onChange?.({ ...value, value: serializeExactDatePickerValue(nextValue, dateComponentProps.showTime) });
129
+ }}
130
+ disabled={disabled}
131
+ style={{ width: '100%' }}
132
+ />
133
+ );
134
+ }
135
+
136
+ if (value.kind === 'relative') {
137
+ return (
138
+ <Space.Compact style={{ width: '100%' }}>
139
+ <InputNumber
140
+ min={1}
141
+ precision={0}
142
+ value={value.amount}
143
+ disabled={disabled}
144
+ onChange={(amount) => onChange?.({ ...value, amount: typeof amount === 'number' ? amount : 1 })}
145
+ aria-label={ctx.t(value.direction === 'past' ? 'Past' : 'Next')}
146
+ style={{ flex: '1 1 auto', minWidth: 80 }}
147
+ />
148
+ <Select
149
+ value={value.unit}
150
+ disabled={disabled}
151
+ onChange={(unit: CtxDateRelativeUnit) => onChange?.({ ...value, unit })}
152
+ aria-label={ctx.t('Unit')}
153
+ style={{ flex: '0 0 130px', minWidth: 130 }}
154
+ options={[
155
+ { value: 'day', label: ctx.t('Day') },
156
+ { value: 'week', label: ctx.t('Calendar week') },
157
+ { value: 'month', label: ctx.t('Calendar Month') },
158
+ { value: 'year', label: ctx.t('Calendar Year') },
159
+ ]}
160
+ />
161
+ </Space.Compact>
162
+ );
163
+ }
164
+
165
+ return <Input value={ctx.t(PRESET_LABELS[value.preset] || value.preset)} readOnly disabled={disabled} />;
166
+ };
167
+
168
+ return (
169
+ <div style={{ display: 'flex', flex: 1, flexDirection: 'column', gap: 8, width: '100%', minWidth: 0, ...style }}>
170
+ {renderValueEditor()}
171
+ {!isDateLikeField && (
172
+ <DateFormatEditor
173
+ value={format}
174
+ placeholder={getDefaultDateVariableFormat(value)}
175
+ onChange={updateFormat}
176
+ disabled={disabled}
177
+ />
178
+ )}
179
+ </div>
180
+ );
181
+ };
@@ -0,0 +1,335 @@
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 {
11
+ isRunJSValue,
12
+ isVariableExpression,
13
+ parseCtxDateExpressionConfig,
14
+ parseValueToPath,
15
+ serializeCtxDateExpressionConfig,
16
+ tExpr,
17
+ VariableInput,
18
+ type CtxDateExpressionConfig,
19
+ type CtxDatePreset,
20
+ type MetaTreeNode,
21
+ type VariableInputProps,
22
+ } from '@nocobase/flow-engine';
23
+ import { dayjs } from '@nocobase/utils/client';
24
+ import type { Dayjs } from 'dayjs';
25
+ import React from 'react';
26
+ import { DateVariableEditor, getDefaultDateVariableFormat } from './DateVariableEditor';
27
+ import { normalizeDateVariableExactValue, type DateVariableComponentProps } from './dateValue';
28
+
29
+ const DATE_PRESET_NODES: Array<{ name: CtxDatePreset; title: string }> = [
30
+ { name: 'now', title: 'Now' },
31
+ { name: 'today', title: 'Today' },
32
+ { name: 'yesterday', title: 'Yesterday' },
33
+ { name: 'tomorrow', title: 'Tomorrow' },
34
+ { name: 'thisWeek', title: 'This Week' },
35
+ { name: 'lastWeek', title: 'Last Week' },
36
+ { name: 'nextWeek', title: 'Next Week' },
37
+ { name: 'thisMonth', title: 'This Month' },
38
+ { name: 'lastMonth', title: 'Last Month' },
39
+ { name: 'nextMonth', title: 'Next Month' },
40
+ { name: 'thisQuarter', title: 'This Quarter' },
41
+ { name: 'lastQuarter', title: 'Last Quarter' },
42
+ { name: 'nextQuarter', title: 'Next Quarter' },
43
+ { name: 'thisYear', title: 'This Year' },
44
+ { name: 'lastYear', title: 'Last Year' },
45
+ { name: 'nextYear', title: 'Next Year' },
46
+ ];
47
+
48
+ type ValueEditorComponent = React.ComponentType<{
49
+ value?: unknown;
50
+ onChange?: (value: unknown) => void;
51
+ style?: React.CSSProperties;
52
+ disabled?: boolean;
53
+ }>;
54
+
55
+ const DATE_VARIABLE_CONFIG_MARK = Symbol('field-value-date-variable-config');
56
+
57
+ type DateVariableEditConfig = CtxDateExpressionConfig & {
58
+ [DATE_VARIABLE_CONFIG_MARK]: 'editable' | 'restored';
59
+ };
60
+
61
+ export type FieldValueVariableInputProps = Omit<
62
+ VariableInputProps,
63
+ 'value' | 'onChange' | 'metaTree' | 'converters'
64
+ > & {
65
+ value?: unknown;
66
+ onChange: (value: unknown) => void;
67
+ baseMetaTree: MetaTreeNode[] | (() => MetaTreeNode[] | Promise<MetaTreeNode[]>);
68
+ constantComponent: ValueEditorComponent;
69
+ nullComponent: ValueEditorComponent;
70
+ runJSComponent: ValueEditorComponent;
71
+ isDateLikeField: boolean;
72
+ dateComponentProps: DateVariableComponentProps;
73
+ allowRunJS?: boolean;
74
+ allowDateVariables?: boolean;
75
+ converters?: VariableInputProps['converters'];
76
+ };
77
+
78
+ function createDateVariableEditConfig(
79
+ config: CtxDateExpressionConfig,
80
+ source: DateVariableEditConfig[typeof DATE_VARIABLE_CONFIG_MARK] = 'editable',
81
+ ): DateVariableEditConfig {
82
+ return { ...config, [DATE_VARIABLE_CONFIG_MARK]: source } as DateVariableEditConfig;
83
+ }
84
+
85
+ function isDateVariableEditConfig(value: unknown): value is DateVariableEditConfig {
86
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
87
+ const source = (value as Partial<DateVariableEditConfig>)[DATE_VARIABLE_CONFIG_MARK];
88
+ return source === 'editable' || source === 'restored';
89
+ }
90
+
91
+ function getDateNodeName(config: CtxDateExpressionConfig): string {
92
+ if (config.kind === 'exact') return 'exact';
93
+ if (config.kind === 'relative') return config.direction;
94
+ return config.preset;
95
+ }
96
+
97
+ function isDayjsValue(value: unknown): value is Dayjs {
98
+ return dayjs.isDayjs(value);
99
+ }
100
+
101
+ function createInitialDateConfig(
102
+ nodeName: string,
103
+ isDateLikeField: boolean,
104
+ componentProps: DateVariableComponentProps,
105
+ ): DateVariableEditConfig | undefined {
106
+ let config: CtxDateExpressionConfig | undefined;
107
+
108
+ if (nodeName === 'exact') {
109
+ const normalized = normalizeDateVariableExactValue(dayjs(), componentProps);
110
+ const value = isDayjsValue(normalized) ? normalized.format('YYYY-MM-DD') : String(normalized || '');
111
+ config = { kind: 'exact', value };
112
+ } else if (nodeName === 'past' || nodeName === 'next') {
113
+ config = { kind: 'relative', direction: nodeName, amount: 1, unit: 'day' };
114
+ } else if (
115
+ DATE_PRESET_NODES.some((item) => item.name === nodeName) &&
116
+ !(nodeName === 'now' && componentProps.exactNormalizeMode === 'date')
117
+ ) {
118
+ config = { kind: 'preset', preset: nodeName as CtxDatePreset };
119
+ }
120
+
121
+ if (!config) return undefined;
122
+ const initialConfig = isDateLikeField ? config : { ...config, format: getDefaultDateVariableFormat(config) };
123
+ return createDateVariableEditConfig(initialConfig);
124
+ }
125
+
126
+ function normalizeDateConfigForStore(
127
+ config: CtxDateExpressionConfig,
128
+ componentProps: DateVariableComponentProps,
129
+ omitFormat: boolean,
130
+ ): CtxDateExpressionConfig {
131
+ const rawFormat = typeof config.format === 'string' ? config.format : '';
132
+ const normalizedConfig = omitFormat
133
+ ? { ...config, format: undefined }
134
+ : { ...config, format: rawFormat.trim() ? rawFormat : getDefaultDateVariableFormat(config) };
135
+ if (normalizedConfig.kind !== 'exact') return normalizedConfig;
136
+ const normalized = normalizeDateVariableExactValue(normalizedConfig.value, componentProps);
137
+ if (Array.isArray(normalized)) {
138
+ return { ...normalizedConfig, value: [String(normalized[0]), String(normalized[1])] };
139
+ }
140
+ return { ...normalizedConfig, value: String(normalized || '') };
141
+ }
142
+
143
+ export const FieldValueVariableInput: React.FC<FieldValueVariableInputProps> = ({
144
+ value,
145
+ onChange,
146
+ baseMetaTree,
147
+ constantComponent: ConstantComponent,
148
+ nullComponent: NullComponent,
149
+ runJSComponent: RunJSComponent,
150
+ isDateLikeField,
151
+ dateComponentProps,
152
+ allowRunJS = true,
153
+ allowDateVariables = true,
154
+ converters,
155
+ clearValue = '',
156
+ disabled = false,
157
+ ...variableInputProps
158
+ }) => {
159
+ const DateEditor = React.useMemo<ValueEditorComponent>(() => {
160
+ const Component: ValueEditorComponent = (props) => (
161
+ <DateVariableEditor
162
+ value={isDateVariableEditConfig(props.value) ? props.value : undefined}
163
+ onChange={(nextConfig) => props.onChange?.(createDateVariableEditConfig(nextConfig))}
164
+ isDateLikeField={isDateLikeField}
165
+ dateComponentProps={dateComponentProps}
166
+ style={props.style}
167
+ disabled={props.disabled}
168
+ />
169
+ );
170
+ return Component;
171
+ }, [dateComponentProps, isDateLikeField]);
172
+
173
+ const parsedDateConfig = allowDateVariables ? parseCtxDateExpressionConfig(value) : undefined;
174
+ const restoreLegacyNowForPureDate =
175
+ dateComponentProps.exactNormalizeMode === 'date' &&
176
+ parsedDateConfig?.kind === 'preset' &&
177
+ parsedDateConfig.preset === 'now';
178
+
179
+ const metaTree = React.useMemo<() => Promise<MetaTreeNode[]>>(() => {
180
+ return async () => {
181
+ const base = typeof baseMetaTree === 'function' ? await baseMetaTree() : baseMetaTree;
182
+ const presetNodes =
183
+ dateComponentProps.exactNormalizeMode === 'date' && !restoreLegacyNowForPureDate
184
+ ? DATE_PRESET_NODES.filter((item) => item.name !== 'now')
185
+ : DATE_PRESET_NODES;
186
+ const dateChildren: MetaTreeNode[] = [
187
+ {
188
+ title: tExpr('Exact day'),
189
+ name: 'exact',
190
+ type: 'date',
191
+ paths: ['date', 'exact'],
192
+ render: (props) => <DateEditor {...props} />,
193
+ },
194
+ {
195
+ title: tExpr('Past'),
196
+ name: 'past',
197
+ type: 'date',
198
+ paths: ['date', 'past'],
199
+ render: (props) => <DateEditor {...props} />,
200
+ },
201
+ {
202
+ title: tExpr('Next'),
203
+ name: 'next',
204
+ type: 'date',
205
+ paths: ['date', 'next'],
206
+ render: (props) => <DateEditor {...props} />,
207
+ },
208
+ ...presetNodes.map<MetaTreeNode>(({ name, title }) => ({
209
+ title: tExpr(title),
210
+ name,
211
+ type: 'date',
212
+ paths: ['date', name],
213
+ ...(name === 'now' && dateComponentProps.exactNormalizeMode === 'date'
214
+ ? { disabled: true, selectable: false }
215
+ : {}),
216
+ render: (props) => <DateEditor {...props} />,
217
+ })),
218
+ ];
219
+
220
+ const specialNodes: MetaTreeNode[] = [
221
+ {
222
+ title: tExpr('Constant'),
223
+ name: 'constant',
224
+ type: 'string',
225
+ paths: ['constant'],
226
+ render: (props) => <ConstantComponent {...props} />,
227
+ },
228
+ {
229
+ title: tExpr('Null'),
230
+ name: 'null',
231
+ type: 'object',
232
+ paths: ['null'],
233
+ render: (props) => <NullComponent {...props} />,
234
+ },
235
+ ...(allowDateVariables
236
+ ? [
237
+ {
238
+ title: tExpr('Date'),
239
+ name: 'date',
240
+ type: 'date',
241
+ paths: ['date'],
242
+ selectable: false,
243
+ children: dateChildren,
244
+ } satisfies MetaTreeNode,
245
+ ]
246
+ : []),
247
+ ...(allowRunJS
248
+ ? [
249
+ {
250
+ title: tExpr('RunJS'),
251
+ name: 'runjs',
252
+ type: 'object',
253
+ paths: ['runjs'],
254
+ render: (props) => <RunJSComponent {...props} />,
255
+ } satisfies MetaTreeNode,
256
+ ]
257
+ : []),
258
+ ];
259
+ return [...specialNodes, ...(Array.isArray(base) ? base : [])];
260
+ };
261
+ }, [
262
+ ConstantComponent,
263
+ DateEditor,
264
+ NullComponent,
265
+ RunJSComponent,
266
+ allowDateVariables,
267
+ allowRunJS,
268
+ baseMetaTree,
269
+ dateComponentProps.exactNormalizeMode,
270
+ restoreLegacyNowForPureDate,
271
+ ]);
272
+
273
+ const displayValue = parsedDateConfig ? createDateVariableEditConfig(parsedDateConfig, 'restored') : value;
274
+
275
+ const handleChange = React.useCallback(
276
+ (nextValue: unknown) => {
277
+ if (disabled) return;
278
+ if (!isDateVariableEditConfig(nextValue)) {
279
+ onChange(nextValue);
280
+ return;
281
+ }
282
+ if (nextValue[DATE_VARIABLE_CONFIG_MARK] === 'restored') return;
283
+ const normalized = normalizeDateConfigForStore(nextValue, dateComponentProps, isDateLikeField);
284
+ onChange(serializeCtxDateExpressionConfig(normalized) || '');
285
+ },
286
+ [dateComponentProps, disabled, isDateLikeField, onChange],
287
+ );
288
+
289
+ return (
290
+ <VariableInput
291
+ {...variableInputProps}
292
+ value={displayValue}
293
+ onChange={handleChange}
294
+ metaTree={metaTree}
295
+ clearValue={clearValue}
296
+ disabled={disabled}
297
+ converters={{
298
+ renderInputComponent: (meta) => {
299
+ const external = converters?.renderInputComponent?.(meta ?? null);
300
+ if (external) return external;
301
+ const firstPath = meta?.paths?.[0];
302
+ if (firstPath === 'constant') return ConstantComponent;
303
+ if (firstPath === 'null') return NullComponent;
304
+ if (allowDateVariables && firstPath === 'date') return DateEditor;
305
+ if (allowRunJS && firstPath === 'runjs') return RunJSComponent;
306
+ return null;
307
+ },
308
+ resolveValueFromPath: (item) => {
309
+ const external = converters?.resolveValueFromPath?.(item);
310
+ if (external !== undefined) return external;
311
+ const firstPath = item?.paths?.[0];
312
+ if (firstPath === 'constant') return '';
313
+ if (firstPath === 'null') return null;
314
+ if (allowDateVariables && firstPath === 'date') {
315
+ return createInitialDateConfig(item.paths[1], isDateLikeField, dateComponentProps);
316
+ }
317
+ if (allowRunJS && firstPath === 'runjs') return { code: '', version: 'v2' };
318
+ return undefined;
319
+ },
320
+ resolvePathFromValue: (currentValue) => {
321
+ const external = converters?.resolvePathFromValue?.(currentValue);
322
+ if (external !== undefined) return external;
323
+ if (currentValue === null) return ['null'];
324
+ if (allowRunJS && isRunJSValue(currentValue)) return ['runjs'];
325
+ if (allowDateVariables && isDateVariableEditConfig(currentValue)) {
326
+ return ['date', getDateNodeName(currentValue)];
327
+ }
328
+ return typeof currentValue === 'string' && isVariableExpression(currentValue)
329
+ ? parseValueToPath(currentValue)
330
+ : ['constant'];
331
+ },
332
+ }}
333
+ />
334
+ );
335
+ };