@nocobase/client-v2 2.2.0-alpha.1 → 2.2.0-alpha.2

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 (216) hide show
  1. package/es/PluginSettingsManager.d.ts +33 -0
  2. package/es/RouteRepository.d.ts +21 -6
  3. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  4. package/es/components/category-tabs/index.d.ts +9 -0
  5. package/es/components/form/JsonTextArea.d.ts +2 -1
  6. package/es/components/form/TypedVariableInput.d.ts +22 -4
  7. package/es/components/form/VariableJsonTextArea.d.ts +19 -0
  8. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  9. package/es/components/form/filter/index.d.ts +2 -0
  10. package/es/components/form/index.d.ts +1 -0
  11. package/es/components/index.d.ts +2 -0
  12. package/es/flow/actions/afterSuccess.d.ts +8 -1
  13. package/es/flow/actions/index.d.ts +1 -1
  14. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
  15. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
  16. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +2 -0
  17. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
  18. package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +5 -0
  19. package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
  20. package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
  21. package/es/flow/components/FieldAssignValueInput.d.ts +2 -0
  22. package/es/flow/index.d.ts +1 -0
  23. package/es/flow/models/base/GridModel.d.ts +1 -1
  24. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +5 -0
  25. package/es/flow/models/blocks/form/FormBlockModel.d.ts +4 -0
  26. package/es/flow/models/blocks/form/submitHandler.d.ts +1 -1
  27. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +21 -0
  28. package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +21 -0
  29. package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
  30. package/es/flow/models/blocks/table/TableActionsColumnModel.d.ts +1 -0
  31. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  32. package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +1 -1
  33. package/es/flow/models/blocks/table/dragSort/dragSortSettings.d.ts +2 -1
  34. package/es/flow/models/blocks/table/dragSort/dragSortUtils.d.ts +6 -4
  35. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +2 -0
  36. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +6 -0
  37. package/es/flow/resolveViewParamsToViewList.d.ts +1 -1
  38. package/es/flow/utils/dataScopeRowSnapshot.d.ts +59 -0
  39. package/es/flow/utils/dateTimeDisplayProps.d.ts +49 -0
  40. package/es/flow/utils/formValueDeps.d.ts +32 -0
  41. package/es/flow-compat/FieldValidation.d.ts +2 -1
  42. package/es/index.d.ts +2 -1
  43. package/es/index.mjs +233 -137
  44. package/lib/index.js +234 -138
  45. package/package.json +7 -7
  46. package/src/PluginSettingsManager.ts +53 -0
  47. package/src/RouteRepository.ts +126 -24
  48. package/src/__tests__/PluginSettingsManager.test.ts +13 -0
  49. package/src/__tests__/RouteRepository.test.ts +216 -0
  50. package/src/__tests__/browserChecker.test.ts +61 -0
  51. package/src/__tests__/exports.test.ts +16 -0
  52. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +199 -0
  53. package/src/__tests__/plugin-manager.test.tsx +44 -2
  54. package/src/__tests__/settings-center.test.tsx +40 -1
  55. package/src/collection-manager/field-configure.ts +1 -1
  56. package/src/collection-manager/interfaces/id.ts +1 -1
  57. package/src/collection-manager/interfaces/m2m.tsx +2 -2
  58. package/src/collection-manager/interfaces/m2o.tsx +2 -2
  59. package/src/collection-manager/interfaces/nanoid.ts +1 -1
  60. package/src/collection-manager/interfaces/o2m.tsx +2 -2
  61. package/src/collection-manager/interfaces/obo.tsx +2 -2
  62. package/src/collection-manager/interfaces/oho.tsx +2 -2
  63. package/src/collection-manager/interfaces/properties/index.ts +2 -2
  64. package/src/collection-manager/interfaces/uuid.ts +1 -1
  65. package/src/components/AppComponents.tsx +19 -3
  66. package/src/components/README.md +7 -1
  67. package/src/components/README.zh-CN.md +6 -1
  68. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  69. package/src/components/category-tabs/index.ts +10 -0
  70. package/src/components/form/JsonTextArea.tsx +21 -11
  71. package/src/components/form/TypedVariableInput.tsx +416 -93
  72. package/src/components/form/VariableJsonTextArea.tsx +175 -0
  73. package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
  74. package/src/components/form/__tests__/TypedVariableInput.test.tsx +266 -9
  75. package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
  76. package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
  77. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
  78. package/src/components/form/filter/index.ts +2 -0
  79. package/src/components/form/index.tsx +1 -0
  80. package/src/components/index.ts +2 -0
  81. package/src/flow/__tests__/FlowRoute.test.tsx +443 -5
  82. package/src/flow/__tests__/getKey.test.ts +7 -0
  83. package/src/flow/__tests__/resolveViewParamsToViewList.test.ts +34 -0
  84. package/src/flow/actions/__tests__/afterSuccess.test.ts +73 -0
  85. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +58 -0
  86. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +1022 -0
  87. package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
  88. package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
  89. package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +19 -5
  90. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +92 -0
  91. package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +104 -0
  92. package/src/flow/actions/__tests__/subFormFieldLinkageRules.inputArgs.test.ts +2 -2
  93. package/src/flow/actions/afterSuccess.tsx +142 -3
  94. package/src/flow/actions/customVariable.tsx +1 -2
  95. package/src/flow/actions/dataScopeFilter.ts +10 -0
  96. package/src/flow/actions/dateTimeFormat.tsx +42 -28
  97. package/src/flow/actions/index.ts +1 -1
  98. package/src/flow/actions/linkageRules.tsx +123 -35
  99. package/src/flow/actions/linkageRulesFormValueRefresh.ts +22 -130
  100. package/src/flow/actions/openView.tsx +38 -4
  101. package/src/flow/actions/runjs.tsx +2 -14
  102. package/src/flow/actions/validation.tsx +62 -30
  103. package/src/flow/admin-shell/BaseLayoutModel.tsx +25 -3
  104. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +5 -2
  105. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +13 -0
  106. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +34 -10
  107. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +310 -0
  108. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +44 -8
  109. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
  110. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
  111. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
  112. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
  113. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +23 -9
  114. package/src/flow/admin-shell/admin-layout/HelpLite.tsx +1 -3
  115. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +188 -0
  116. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
  117. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +82 -0
  118. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +166 -2
  119. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
  120. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
  121. package/src/flow/components/DefaultValue.tsx +1 -2
  122. package/src/flow/components/DynamicFlowsIcon.tsx +447 -126
  123. package/src/flow/components/FieldAssignRulesEditor.tsx +128 -21
  124. package/src/flow/components/FieldAssignValueInput.tsx +10 -5
  125. package/src/flow/components/FlowRoute.tsx +56 -11
  126. package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +148 -2
  127. package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +508 -4
  128. package/src/flow/components/__tests__/fieldAssignOptions.test.ts +151 -3
  129. package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
  130. package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
  131. package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
  132. package/src/flow/components/fieldAssignOptions.ts +155 -27
  133. package/src/flow/flows/editMarkdownFlow.tsx +1 -1
  134. package/src/flow/getViewDiffAndUpdateHidden.tsx +1 -0
  135. package/src/flow/index.ts +2 -1
  136. package/src/flow/internal/utils/operatorSchemaHelper.ts +15 -1
  137. package/src/flow/models/actions/JSActionModel.tsx +2 -7
  138. package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
  139. package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
  140. package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
  141. package/src/flow/models/base/ActionModel.tsx +12 -1
  142. package/src/flow/models/base/GridModel.tsx +38 -7
  143. package/src/flow/models/base/PageModel/PageModel.tsx +4 -1
  144. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +28 -4
  145. package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
  146. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
  147. package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
  148. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +31 -3
  149. package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
  150. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +77 -0
  151. package/src/flow/models/blocks/filter-form/fields/FilterFormCustomFieldModel.tsx +1 -1
  152. package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +56 -1
  153. package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
  154. package/src/flow/models/blocks/form/FormActionModel.tsx +1 -1
  155. package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
  156. package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
  157. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
  158. package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +54 -1
  159. package/src/flow/models/blocks/form/submitHandler.ts +17 -3
  160. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +880 -102
  161. package/src/flow/models/blocks/form/value-runtime/rules.ts +445 -13
  162. package/src/flow/models/blocks/form/value-runtime/runtime.ts +257 -13
  163. package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
  164. package/src/flow/models/blocks/js-block/JSBlock.tsx +2 -7
  165. package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
  166. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +36 -13
  167. package/src/flow/models/blocks/table/TableBlockModel.tsx +38 -17
  168. package/src/flow/models/blocks/table/TableColumnModel.tsx +36 -3
  169. package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +111 -1
  170. package/src/flow/models/blocks/table/__tests__/TableBlockModel.dragSort.test.ts +127 -0
  171. package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
  172. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +1 -0
  173. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +159 -1
  174. package/src/flow/models/blocks/table/dragSort/dragSortHooks.tsx +28 -17
  175. package/src/flow/models/blocks/table/dragSort/dragSortSettings.ts +13 -6
  176. package/src/flow/models/blocks/table/dragSort/dragSortUtils.ts +15 -2
  177. package/src/flow/models/fields/AssociationFieldModel/AssociationFieldModel.tsx +1 -1
  178. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +29 -6
  179. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +16 -4
  180. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +141 -19
  181. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +13 -1
  182. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +197 -0
  183. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableFieldModel.reset.test.ts +180 -0
  184. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -4
  185. package/src/flow/models/fields/AssociationFieldModel/__tests__/AssociationFieldModel.updateAssociation.test.ts +72 -0
  186. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +23 -0
  187. package/src/flow/models/fields/ClickableFieldModel.tsx +5 -0
  188. package/src/flow/models/fields/DisplayDateTimeFieldModel.tsx +29 -1
  189. package/src/flow/models/fields/InputFieldModel.tsx +40 -2
  190. package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
  191. package/src/flow/models/fields/JSFieldModel.tsx +2 -7
  192. package/src/flow/models/fields/JSItemModel.tsx +2 -14
  193. package/src/flow/models/fields/SelectFieldModel.tsx +6 -4
  194. package/src/flow/models/fields/TextareaFieldModel.tsx +49 -3
  195. package/src/flow/models/fields/__tests__/DisplayDateTimeFieldModel.test.tsx +96 -0
  196. package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +117 -1
  197. package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +43 -0
  198. package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +100 -0
  199. package/src/flow/models/topbar/TopbarActionModel.tsx +93 -10
  200. package/src/flow/resolveViewParamsToViewList.tsx +11 -3
  201. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +258 -0
  202. package/src/flow/utils/dataScopeFormValueClear.ts +218 -81
  203. package/src/flow/utils/dataScopeRowSnapshot.ts +616 -0
  204. package/src/flow/utils/dateTimeDisplayProps.ts +135 -0
  205. package/src/flow/utils/formValueDeps.ts +170 -0
  206. package/src/flow-compat/FieldValidation.tsx +122 -60
  207. package/src/flow-compat/Popover.tsx +43 -4
  208. package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
  209. package/src/index.ts +8 -1
  210. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
  211. package/src/nocobase-buildin-plugin/index.tsx +44 -14
  212. package/src/settings-center/AdminSettingsLayout.tsx +10 -2
  213. package/src/settings-center/SystemSettingsPage.tsx +1 -2
  214. package/src/settings-center/plugin-manager/PluginCard.tsx +2 -2
  215. package/src/settings-center/plugin-manager/index.tsx +3 -0
  216. package/src/settings-center/utils.tsx +0 -6
@@ -18,7 +18,7 @@ import { useTranslation } from 'react-i18next';
18
18
  import { FIELDS_VALIDATION_OPTIONS, REQUIRED_RULE_KEY } from './fieldValidationConstants';
19
19
 
20
20
  interface ValidationRule {
21
- key: string;
21
+ key?: string;
22
22
  name: string;
23
23
  args?: {
24
24
  [key: string]: any;
@@ -34,14 +34,23 @@ interface ValidationData {
34
34
  interface FieldValidationProps {
35
35
  value?: ValidationData;
36
36
  onChange?: (value: ValidationData) => void;
37
+ inheritedValue?: ValidationData;
37
38
  type?: string;
38
39
  availableValidationOptions?: string[];
39
40
  excludeValidationOptions?: string[];
40
41
  isAssociation?: boolean;
41
42
  }
42
43
 
44
+ function formatFallbackRuleLabel(name: string) {
45
+ if (!name) {
46
+ return name;
47
+ }
48
+ return `${name.slice(0, 1).toUpperCase()}${name.slice(1)}`;
49
+ }
50
+
43
51
  export const FieldValidation = observer((props: FieldValidationProps) => {
44
- const { value, onChange, type, availableValidationOptions, excludeValidationOptions, isAssociation } = props;
52
+ const { value, onChange, inheritedValue, type, availableValidationOptions, excludeValidationOptions, isAssociation } =
53
+ props;
45
54
  const { t } = useTranslation();
46
55
  const { token } = theme.useToken();
47
56
  const noCascadeCls = css`
@@ -57,8 +66,9 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
57
66
  const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
58
67
  const formilyField: any = useField?.() as any;
59
68
 
60
- const validationType = value?.type || type;
61
- const rules = value?.rules || [];
69
+ const validationType = value?.type || inheritedValue?.type || type;
70
+ const rules = useMemo(() => value?.rules || [], [value?.rules]);
71
+ const inheritedRules = useMemo(() => inheritedValue?.rules || [], [inheritedValue?.rules]);
62
72
 
63
73
  const normalizeArgsForDomainLike = useCallback((ruleName: string, args: Record<string, any> | undefined) => {
64
74
  if (!args) return args;
@@ -163,19 +173,19 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
163
173
  let hasError = false;
164
174
  const invalidRuleKeys: string[] = [];
165
175
  try {
166
- rules.forEach((rule) => {
176
+ rules.forEach((rule, index) => {
167
177
  const opt = validationOptions.find((o) => o.key === rule.name);
168
178
  if (!opt || !opt.params) return;
169
179
  let ruleInvalid = false;
170
180
  opt.params.forEach((param) => {
171
181
  if (!param.required) return;
172
- const currentValue = (rule.args?.[param.key] ?? param.defaultValue) as any;
173
- if (isValueEmpty(param.componentType as any, currentValue)) {
182
+ const currentValue = rule.args?.[param.key] ?? param.defaultValue;
183
+ if (isValueEmpty(param.componentType, currentValue)) {
174
184
  hasError = true;
175
185
  ruleInvalid = true;
176
186
  }
177
187
  });
178
- if (ruleInvalid) invalidRuleKeys.push(rule.key);
188
+ if (ruleInvalid) invalidRuleKeys.push(getRuleKey(rule, index, 'ui'));
179
189
  });
180
190
  } catch (err) {
181
191
  hasError = true;
@@ -198,17 +208,18 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
198
208
 
199
209
  const handleAddRule = (ruleType: string) => {
200
210
  const option = validationOptions.find((opt) => opt.key === ruleType);
211
+ const args: Record<string, any> = {};
201
212
  const newRule: ValidationRule = {
202
213
  key: `r_${Date.now()}`,
203
214
  name: ruleType,
204
- args: {},
215
+ args,
205
216
  paramsType: option?.paramsType,
206
217
  };
207
218
 
208
219
  if (option?.params) {
209
220
  option.params.forEach((param) => {
210
221
  if (param.defaultValue !== undefined) {
211
- newRule.args![param.key] = param.defaultValue;
222
+ args[param.key] = param.defaultValue;
212
223
  }
213
224
  });
214
225
  }
@@ -225,7 +236,7 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
225
236
  };
226
237
 
227
238
  const handleRemoveRule = (ruleKey: string) => {
228
- const newRules = rules.filter((rule) => rule.key !== ruleKey);
239
+ const newRules = rules.filter((rule, index) => getRuleKey(rule, index, 'ui') !== ruleKey);
229
240
  onChange?.({
230
241
  type: validationType,
231
242
  rules: newRules,
@@ -233,8 +244,8 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
233
244
  };
234
245
 
235
246
  const handleRuleValueChange = (ruleKey: string, argKey: string, newValue: any) => {
236
- const newRules = rules.map((rule) => {
237
- if (rule.key === ruleKey) {
247
+ const newRules = rules.map((rule, index) => {
248
+ if (getRuleKey(rule, index, 'ui') === ruleKey) {
238
249
  let nextArgs = { ...rule.args, [argKey]: newValue } as Record<string, any>;
239
250
  nextArgs = normalizeArgsForDomainLike(rule.name, nextArgs);
240
251
  const option = validationOptions.find((item) => item.key === rule.name);
@@ -261,7 +272,10 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
261
272
  }
262
273
  };
263
274
 
264
- const renderValidationForm = (rule: ValidationRule) => {
275
+ const getRuleKey = (rule: ValidationRule, index: number, source: 'field' | 'ui') =>
276
+ rule.key || `${source}-${rule.name}-${index}`;
277
+
278
+ const renderValidationForm = (rule: ValidationRule, ruleKey: string, readOnly: boolean) => {
265
279
  const option = validationOptions.find((opt) => opt.key === rule.name);
266
280
  if (!option) return null;
267
281
 
@@ -282,7 +296,7 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
282
296
  ) {
283
297
  currentValue = deriveTldsModeFromArgs(rule.args);
284
298
  }
285
- const isInvalid = !!param.required && isValueEmpty(param.componentType as any, currentValue);
299
+ const isInvalid = !readOnly && !!param.required && isValueEmpty(param.componentType, currentValue);
286
300
 
287
301
  return (
288
302
  <Form.Item
@@ -296,33 +310,35 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
296
310
  >
297
311
  {param.componentType === 'checkbox' ? (
298
312
  <Checkbox
313
+ disabled={readOnly}
299
314
  checked={currentValue || false}
300
- onChange={(e) => handleRuleValueChange(rule.key, param.key, e.target.checked)}
315
+ onChange={(e) => handleRuleValueChange(ruleKey, param.key, e.target.checked)}
301
316
  >
302
317
  {t(param.label)}
303
318
  </Checkbox>
304
319
  ) : param.componentType === 'radio' ? (
305
320
  <div>
306
321
  <Radio.Group
322
+ disabled={readOnly}
307
323
  value={currentValue}
308
324
  onChange={(e) => {
309
325
  if (param.key === 'tlds' && (option.key === 'email' || option.key === 'domain')) {
310
326
  const val = e.target.value as 'iana' | 'disable' | 'allow' | 'deny';
311
327
  if (val === 'iana') {
312
- handleRuleValueChange(rule.key, 'tlds', { allow: true });
328
+ handleRuleValueChange(ruleKey, 'tlds', { allow: true });
313
329
  } else if (val === 'disable') {
314
- handleRuleValueChange(rule.key, 'tlds', false);
330
+ handleRuleValueChange(ruleKey, 'tlds', false);
315
331
  } else if (val === 'allow') {
316
332
  const prev = rule.args?.tlds;
317
333
  const arr = Array.isArray(prev?.allow) ? prev.allow : [];
318
- handleRuleValueChange(rule.key, 'tlds', { allow: arr });
334
+ handleRuleValueChange(ruleKey, 'tlds', { allow: arr });
319
335
  } else if (val === 'deny') {
320
336
  const prev = rule.args?.tlds;
321
337
  const arr = Array.isArray(prev?.deny) ? prev.deny : [];
322
- handleRuleValueChange(rule.key, 'tlds', { deny: arr });
338
+ handleRuleValueChange(ruleKey, 'tlds', { deny: arr });
323
339
  }
324
340
  } else {
325
- handleRuleValueChange(rule.key, param.key, e.target.value);
341
+ handleRuleValueChange(ruleKey, param.key, e.target.value);
326
342
  }
327
343
  }}
328
344
  >
@@ -349,6 +365,7 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
349
365
  : [];
350
366
  return (
351
367
  <Select
368
+ disabled={readOnly}
352
369
  mode="tags"
353
370
  value={arrValue}
354
371
  onChange={(vals) => {
@@ -356,9 +373,9 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
356
373
  new Set((vals as string[]).map((s) => String(s).trim()).filter(Boolean)),
357
374
  );
358
375
  if (item.value === 'allow') {
359
- handleRuleValueChange(rule.key, 'tlds', { allow: cleaned });
376
+ handleRuleValueChange(ruleKey, 'tlds', { allow: cleaned });
360
377
  } else {
361
- handleRuleValueChange(rule.key, 'tlds', { deny: cleaned });
378
+ handleRuleValueChange(ruleKey, 'tlds', { deny: cleaned });
362
379
  }
363
380
  }}
364
381
  tokenSeparators={[',', ' ', '\n', '\t']}
@@ -370,9 +387,10 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
370
387
 
371
388
  return (
372
389
  <Input
390
+ disabled={readOnly}
373
391
  value=""
374
392
  onChange={(e) =>
375
- handleRuleValueChange(rule.key, `${param.key}_${item.value}`, e.target.value)
393
+ handleRuleValueChange(ruleKey, `${param.key}_${item.value}`, e.target.value)
376
394
  }
377
395
  style={{ width: '100%' }}
378
396
  />
@@ -380,30 +398,33 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
380
398
  })()
381
399
  ) : item.componentType === 'inputNumber' ? (
382
400
  <InputNumber
401
+ disabled={readOnly}
383
402
  precision={allowDecimalInputNumber ? undefined : 0}
384
403
  step={allowDecimalInputNumber ? 0.1 : 1}
385
404
  value={rule.args?.[`${param.key}_${item.value}`] || ''}
386
- onChange={(val) => handleRuleValueChange(rule.key, `${param.key}_${item.value}`, val)}
405
+ onChange={(val) => handleRuleValueChange(ruleKey, `${param.key}_${item.value}`, val)}
387
406
  style={{ width: '100%' }}
388
407
  />
389
408
  ) : item.componentType === 'checkbox' ? (
390
409
  <Checkbox
410
+ disabled={readOnly}
391
411
  checked={rule.args?.[`${param.key}_${item.value}`] || false}
392
412
  onChange={(e) =>
393
- handleRuleValueChange(rule.key, `${param.key}_${item.value}`, e.target.checked)
413
+ handleRuleValueChange(ruleKey, `${param.key}_${item.value}`, e.target.checked)
394
414
  }
395
415
  >
396
416
  {t(item.label)}
397
417
  </Checkbox>
398
418
  ) : item.componentType === 'datePicker' ? (
399
419
  <DatePicker
420
+ disabled={readOnly}
400
421
  value={
401
422
  rule.args?.[`${param.key}_${item.value}`]
402
423
  ? dayjs(rule.args[`${param.key}_${item.value}`])
403
424
  : null
404
425
  }
405
426
  onChange={(date) =>
406
- handleRuleValueChange(rule.key, `${param.key}_${item.value}`, date?.toISOString())
427
+ handleRuleValueChange(ruleKey, `${param.key}_${item.value}`, date?.toISOString())
407
428
  }
408
429
  placeholder={t('Select date')}
409
430
  style={{ width: '100%' }}
@@ -417,8 +438,9 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
417
438
  </div>
418
439
  ) : param.componentType === 'singleSelect' ? (
419
440
  <Select
441
+ disabled={readOnly}
420
442
  value={currentValue}
421
- onChange={(val) => handleRuleValueChange(rule.key, param.key, val)}
443
+ onChange={(val) => handleRuleValueChange(ruleKey, param.key, val)}
422
444
  placeholder={t('Please select')}
423
445
  style={{ width: '100%', height: '100%' }}
424
446
  allowClear
@@ -426,9 +448,10 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
426
448
  />
427
449
  ) : param.componentType === 'multipleSelect' ? (
428
450
  <Select
451
+ disabled={readOnly}
429
452
  mode="multiple"
430
453
  value={currentValue}
431
- onChange={(val) => handleRuleValueChange(rule.key, param.key, val)}
454
+ onChange={(val) => handleRuleValueChange(ruleKey, param.key, val)}
432
455
  placeholder={t('Please select')}
433
456
  style={{ width: '100%' }}
434
457
  allowClear
@@ -436,24 +459,27 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
436
459
  />
437
460
  ) : param.componentType === 'inputNumber' ? (
438
461
  <InputNumber
462
+ disabled={readOnly}
439
463
  precision={allowDecimalInputNumber ? undefined : 0}
440
464
  step={allowDecimalInputNumber ? 0.1 : 1}
441
465
  value={currentValue !== undefined ? currentValue : ''}
442
- onChange={(val) => handleRuleValueChange(rule.key, param.key, val)}
466
+ onChange={(val) => handleRuleValueChange(ruleKey, param.key, val)}
443
467
  placeholder={t('Enter value')}
444
468
  style={{ width: '100%' }}
445
469
  />
446
470
  ) : param.componentType === 'datePicker' ? (
447
471
  <DatePicker
472
+ disabled={readOnly}
448
473
  value={currentValue ? dayjs(currentValue) : null}
449
- onChange={(date) => handleRuleValueChange(rule.key, param.key, date?.toISOString())}
474
+ onChange={(date) => handleRuleValueChange(ruleKey, param.key, date?.toISOString())}
450
475
  placeholder={t('Select date')}
451
476
  style={{ width: '100%' }}
452
477
  />
453
478
  ) : (
454
479
  <Input
480
+ disabled={readOnly}
455
481
  value={currentValue !== undefined ? currentValue : ''}
456
- onChange={(e) => handleRuleValueChange(rule.key, param.key, e.target.value)}
482
+ onChange={(e) => handleRuleValueChange(ruleKey, param.key, e.target.value)}
457
483
  placeholder={t('Enter value')}
458
484
  />
459
485
  )}
@@ -465,40 +491,61 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
465
491
  };
466
492
 
467
493
  const menuItems: MenuProps['items'] = useMemo(() => {
468
- const addedRuleNames = new Set(rules.map((rule) => rule.name));
494
+ const addedRuleNames = new Set([...inheritedRules, ...rules].map((rule) => rule.name));
469
495
  return validationOptions
470
496
  .filter((option) => !addedRuleNames.has(option.key))
471
497
  .map((option) => ({
472
498
  key: option.key,
473
499
  label: t(option.label),
474
500
  }));
475
- }, [rules, t, validationOptions]);
501
+ }, [inheritedRules, rules, t, validationOptions]);
476
502
 
477
503
  const menu: MenuProps = {
478
504
  items: menuItems,
479
505
  onClick: ({ key }) => handleAddRule(key as string),
480
506
  };
481
507
 
482
- return (
483
- <div className={noCascadeCls} style={{ marginBottom: token.marginLG }}>
484
- {rules.length > 0 && (
508
+ const renderSectionTitle = (title: string, extraStyle?: React.CSSProperties) => (
509
+ <div
510
+ style={{
511
+ alignItems: 'center',
512
+ color: token.colorTextSecondary,
513
+ display: 'flex',
514
+ fontSize: token.fontSize,
515
+ fontWeight: 500,
516
+ lineHeight: token.lineHeight,
517
+ marginBottom: token.marginXS,
518
+ ...extraStyle,
519
+ }}
520
+ >
521
+ {title}
522
+ </div>
523
+ );
524
+
525
+ const renderRuleList = (dataSource: ValidationRule[], readOnly: boolean, source: 'field' | 'ui') => {
526
+ if (!dataSource.length) {
527
+ return null;
528
+ }
529
+
530
+ return (
531
+ <div style={{ marginBottom: token.marginSM }}>
485
532
  <List
486
533
  size="small"
487
534
  style={{
488
- backgroundColor: token.colorFillAlter,
489
- border: `1px solid ${token.colorBorder}`,
535
+ backgroundColor: readOnly ? token.colorFillQuaternary : token.colorFillAlter,
536
+ border: `1px solid ${readOnly ? token.colorBorderSecondary : token.colorBorder}`,
490
537
  borderRadius: token.borderRadius,
491
- marginBottom: token.marginSM,
492
538
  }}
493
- dataSource={rules}
494
- renderItem={(rule) => {
539
+ dataSource={dataSource}
540
+ renderItem={(rule, index) => {
541
+ const ruleKey = getRuleKey(rule, index, source);
495
542
  const option = validationOptions.find((opt) => opt.key === rule.name);
496
- const ruleLabel = option ? t(option.label) : rule.name;
543
+ const ruleLabel = option ? t(option.label) : formatFallbackRuleLabel(rule.name);
497
544
  const hasParams = option?.hasValue && option.params.length > 0;
498
- const isExpanded = expandedKeys.includes(rule.key);
545
+ const isExpanded = expandedKeys.includes(ruleKey);
499
546
 
500
547
  return (
501
- <List.Item style={{ display: 'block', padding: 0 }}>
548
+ <List.Item key={ruleKey} style={{ display: 'block', padding: 0 }}>
502
549
  <div
503
550
  style={{
504
551
  display: 'flex',
@@ -506,10 +553,10 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
506
553
  alignItems: 'center',
507
554
  padding: `${token.paddingXS}px ${token.paddingSM}px`,
508
555
  cursor: hasParams ? 'pointer' : 'default',
509
- backgroundColor: token.colorBgContainer,
556
+ backgroundColor: readOnly ? token.colorFillQuaternary : token.colorBgContainer,
510
557
  borderRadius: 6,
511
558
  }}
512
- onClick={() => hasParams && handleToggleExpand(rule.key)}
559
+ onClick={() => hasParams && handleToggleExpand(ruleKey)}
513
560
  >
514
561
  <div style={{ display: 'flex', alignItems: 'center', gap: token.marginXS }}>
515
562
  <div
@@ -533,34 +580,49 @@ export const FieldValidation = observer((props: FieldValidationProps) => {
533
580
  )}
534
581
  </span>
535
582
  </div>
536
- <Button
537
- type="text"
538
- size="small"
539
- icon={<DeleteOutlined />}
540
- onClick={(e) => {
541
- e.stopPropagation();
542
- handleRemoveRule(rule.key);
543
- }}
544
- style={{ color: token.colorTextSecondary }}
545
- />
583
+ {!readOnly && (
584
+ <Button
585
+ type="text"
586
+ size="small"
587
+ icon={<DeleteOutlined />}
588
+ onClick={(e) => {
589
+ e.stopPropagation();
590
+ handleRemoveRule(ruleKey);
591
+ }}
592
+ style={{ color: token.colorTextSecondary }}
593
+ />
594
+ )}
546
595
  </div>
547
596
  {hasParams && isExpanded && (
548
597
  <div
549
598
  style={{
550
599
  padding: token.paddingSM,
551
- backgroundColor: token.colorBgContainer,
552
- borderTop: `1px solid ${token.colorBorder}`,
600
+ backgroundColor: readOnly ? token.colorFillQuaternary : token.colorBgContainer,
601
+ borderTop: `1px solid ${readOnly ? token.colorBorderSecondary : token.colorBorder}`,
553
602
  borderRadius: '0 0 6px 6px',
554
603
  }}
555
604
  >
556
- {renderValidationForm(rule)}
605
+ {renderValidationForm(rule, ruleKey, readOnly)}
557
606
  </div>
558
607
  )}
559
608
  </List.Item>
560
609
  );
561
610
  }}
562
611
  />
612
+ </div>
613
+ );
614
+ };
615
+
616
+ return (
617
+ <div className={noCascadeCls} style={{ marginBottom: token.marginLG }}>
618
+ {inheritedRules.length > 0 && (
619
+ <>
620
+ {renderSectionTitle(t('Server-side field validation rules'))}
621
+ {renderRuleList(inheritedRules, true, 'field')}
622
+ {renderSectionTitle(t('Client-side validation rules'), { marginTop: token.margin })}
623
+ </>
563
624
  )}
625
+ {renderRuleList(rules, false, 'ui')}
564
626
 
565
627
  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
566
628
  <div>
@@ -8,15 +8,54 @@
8
8
  */
9
9
 
10
10
  import { Popover as AntdPopover, type PopoverProps } from 'antd';
11
- import React, { useCallback, useRef } from 'react';
11
+ import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
12
12
  import { getZIndex, useZIndexContext, zIndexContext } from './zIndexContext';
13
13
 
14
14
  export const ICON_POPUP_Z_INDEX = 2000;
15
15
 
16
+ function parseZIndex(value?: string | null) {
17
+ if (!value || value === 'auto') {
18
+ return;
19
+ }
20
+
21
+ const zIndex = Number(value);
22
+ return Number.isFinite(zIndex) ? zIndex : undefined;
23
+ }
24
+
25
+ function getElementZIndex(element: HTMLElement) {
26
+ return parseZIndex(window.getComputedStyle(element).zIndex) ?? parseZIndex(element.style.zIndex);
27
+ }
28
+
29
+ function getContainingPopupZIndex(element: HTMLElement | null) {
30
+ if (!element || typeof window === 'undefined') {
31
+ return 0;
32
+ }
33
+
34
+ let current: HTMLElement | null = element;
35
+ let maxZIndex = 0;
36
+
37
+ while (current && current !== document.documentElement) {
38
+ const zIndex = getElementZIndex(current);
39
+ if (typeof zIndex === 'number' && zIndex > maxZIndex) {
40
+ maxZIndex = zIndex;
41
+ }
42
+ current = current.parentElement;
43
+ }
44
+
45
+ return maxZIndex;
46
+ }
47
+
16
48
  export const StablePopover = (props: PopoverProps) => {
17
49
  const parentZIndex = useZIndexContext();
18
- const zIndex = getZIndex('drawer', parentZIndex, 1);
19
- const target = useRef(null);
50
+ const [containingPopupZIndex, setContainingPopupZIndex] = useState(parentZIndex);
51
+ const target = useRef<EventTarget | null>(null);
52
+ const triggerRef = useRef<HTMLDivElement | null>(null);
53
+ const zIndex = getZIndex('drawer', Math.max(parentZIndex, containingPopupZIndex), 1);
54
+
55
+ useLayoutEffect(() => {
56
+ const nextZIndex = Math.max(parentZIndex, getContainingPopupZIndex(triggerRef.current));
57
+ setContainingPopupZIndex((previousZIndex) => (previousZIndex === nextZIndex ? previousZIndex : nextZIndex));
58
+ }, [parentZIndex]);
20
59
 
21
60
  const avoidClose = useCallback((e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
22
61
  target.current = e.target;
@@ -36,7 +75,7 @@ export const StablePopover = (props: PopoverProps) => {
36
75
  );
37
76
 
38
77
  return (
39
- <div className="popover-with-stop-propagation" onClick={avoidClose}>
78
+ <div ref={triggerRef} className="popover-with-stop-propagation" onClick={avoidClose}>
40
79
  <zIndexContext.Provider value={zIndex}>
41
80
  <AntdPopover {...props} zIndex={zIndex} onOpenChange={onOpenChange} />
42
81
  </zIndexContext.Provider>
@@ -0,0 +1,34 @@
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 { render, screen, waitFor } from '@testing-library/react';
11
+ import React from 'react';
12
+ import { describe, expect, it } from 'vitest';
13
+ import { StablePopover } from '../Popover';
14
+
15
+ describe('StablePopover', () => {
16
+ it('renders above the containing popup layer', async () => {
17
+ render(
18
+ <div style={{ position: 'relative', zIndex: 5000 }}>
19
+ <StablePopover open content={<div>Icon picker popup</div>}>
20
+ <button type="button">Select icon</button>
21
+ </StablePopover>
22
+ </div>,
23
+ );
24
+
25
+ expect(screen.getByText('Icon picker popup')).toBeInTheDocument();
26
+
27
+ await waitFor(() => {
28
+ const popover = document.querySelector<HTMLElement>('.ant-popover');
29
+
30
+ expect(popover).toBeInTheDocument();
31
+ expect(Number(popover?.style.zIndex)).toBeGreaterThan(5000);
32
+ });
33
+ });
34
+ });
package/src/index.ts CHANGED
@@ -39,5 +39,12 @@ export * from './collection-manager/interfaces';
39
39
  export * from './collection-manager/template-fields';
40
40
  export * from './data-source';
41
41
  export * from './flow';
42
- export { DEFAULT_DATA_SOURCE_KEY, isTitleField, isTitleFieldInterface } from './flow-compat';
42
+ export {
43
+ DEFAULT_DATA_SOURCE_KEY,
44
+ IconPicker,
45
+ isTitleField,
46
+ isTitleFieldInterface,
47
+ NocoBaseDesktopRouteType,
48
+ } from './flow-compat';
49
+ export type { NocoBaseDesktopRoute } from './flow-compat';
43
50
  export { default as AntdAppProvider } from './theme/AntdAppProvider';
@@ -11,7 +11,7 @@ import { FlowEngine, FlowEngineProvider, observer } from '@nocobase/flow-engine'
11
11
  import { render, screen, waitFor } from '@testing-library/react';
12
12
  import React from 'react';
13
13
  import { createMemoryRouter, Outlet, RouterProvider, useOutlet } from 'react-router-dom';
14
- import { describe, expect, it } from 'vitest';
14
+ import { describe, expect, it, vi } from 'vitest';
15
15
  import { BaseLayoutModel } from '../../flow/admin-shell/BaseLayoutModel';
16
16
  import { LayoutContentRoute } from '../LayoutContentRoute';
17
17
  import { LayoutRoute } from '../LayoutRoute';
@@ -106,6 +106,46 @@ describe('LayoutRoute', () => {
106
106
  });
107
107
  });
108
108
 
109
+ it('does not activate desktop route loading for generic layouts', async () => {
110
+ const activateLayout = vi.fn(() => vi.fn());
111
+ const engine = new FlowEngine();
112
+ engine.registerModels({ TestLayoutModel });
113
+ engine.context.defineProperty('routeRepository', {
114
+ value: {
115
+ activateLayout,
116
+ },
117
+ });
118
+ engine.context.defineProperty('app', {
119
+ value: {
120
+ layoutManager: {
121
+ getLayout: () => layout,
122
+ },
123
+ },
124
+ });
125
+
126
+ const router = createMemoryRouter(
127
+ [
128
+ {
129
+ id: layout.routeName,
130
+ path: layout.routePath,
131
+ element: <LayoutRoute layoutRouteName="test" />,
132
+ },
133
+ ],
134
+ {
135
+ initialEntries: ['/test'],
136
+ },
137
+ );
138
+
139
+ render(
140
+ <FlowEngineProvider engine={engine}>
141
+ <RouterProvider router={router} />
142
+ </FlowEngineProvider>,
143
+ );
144
+
145
+ expect(await screen.findByTestId('layout-route')).toHaveTextContent('test');
146
+ expect(activateLayout).not.toHaveBeenCalled();
147
+ });
148
+
109
149
  it('syncs nested page route before the layout renders its outlet', async () => {
110
150
  const nestedLayout: LayoutDefinition = {
111
151
  ...layout,