@nocobase/client-v2 2.1.0-alpha.40 → 2.1.0-alpha.45

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 (278) hide show
  1. package/es/Application.d.ts +7 -0
  2. package/es/BaseApplication.d.ts +13 -0
  3. package/es/RouterManager.d.ts +1 -0
  4. package/es/collection-field-interface/CollectionFieldInterface.d.ts +51 -15
  5. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +82 -3
  6. package/es/collection-manager/field-configure.d.ts +80 -0
  7. package/es/collection-manager/field-validation.d.ts +43 -0
  8. package/es/collection-manager/filter-operators/index.d.ts +46 -0
  9. package/es/collection-manager/filter-operators/operators.d.ts +30 -0
  10. package/es/collection-manager/interfaces/checkbox.d.ts +1 -41
  11. package/es/collection-manager/interfaces/checkboxGroup.d.ts +12 -44
  12. package/es/collection-manager/interfaces/collection.d.ts +12 -51
  13. package/es/collection-manager/interfaces/color.d.ts +1 -16
  14. package/es/collection-manager/interfaces/createdAt.d.ts +1 -44
  15. package/es/collection-manager/interfaces/createdBy.d.ts +0 -4
  16. package/es/collection-manager/interfaces/dateOnly.d.ts +7 -44
  17. package/es/collection-manager/interfaces/datetime.d.ts +1 -44
  18. package/es/collection-manager/interfaces/datetimeNoTz.d.ts +1 -44
  19. package/es/collection-manager/interfaces/email.d.ts +1 -29
  20. package/es/collection-manager/interfaces/id.d.ts +1 -16
  21. package/es/collection-manager/interfaces/index.d.ts +2 -3
  22. package/es/collection-manager/interfaces/input.d.ts +1 -102
  23. package/es/collection-manager/interfaces/integer.d.ts +1 -95
  24. package/es/collection-manager/interfaces/json.d.ts +16 -7
  25. package/es/collection-manager/interfaces/m2m.d.ts +11 -19
  26. package/es/collection-manager/interfaces/m2o.d.ts +11 -19
  27. package/es/collection-manager/interfaces/markdown.d.ts +1 -63
  28. package/es/collection-manager/interfaces/multipleSelect.d.ts +12 -44
  29. package/es/collection-manager/interfaces/nanoid.d.ts +1 -34
  30. package/es/collection-manager/interfaces/number.d.ts +1 -87
  31. package/es/collection-manager/interfaces/o2m.d.ts +12 -24
  32. package/es/collection-manager/interfaces/obo.d.ts +207 -0
  33. package/es/collection-manager/interfaces/oho.d.ts +207 -0
  34. package/es/collection-manager/interfaces/password.d.ts +1 -56
  35. package/es/collection-manager/interfaces/percent.d.ts +1 -84
  36. package/es/collection-manager/interfaces/phone.d.ts +1 -25
  37. package/es/collection-manager/interfaces/properties/index.d.ts +0 -28
  38. package/es/collection-manager/interfaces/radioGroup.d.ts +1 -29
  39. package/es/collection-manager/interfaces/richText.d.ts +1 -63
  40. package/es/collection-manager/interfaces/select.d.ts +12 -44
  41. package/es/collection-manager/interfaces/snowflake-id.d.ts +1 -34
  42. package/es/collection-manager/interfaces/tableoid.d.ts +1 -10
  43. package/es/collection-manager/interfaces/textarea.d.ts +1 -51
  44. package/es/collection-manager/interfaces/time.d.ts +1 -16
  45. package/es/collection-manager/interfaces/types.d.ts +3 -12
  46. package/es/collection-manager/interfaces/unixTimestamp.d.ts +1 -44
  47. package/es/collection-manager/interfaces/updatedAt.d.ts +1 -44
  48. package/es/collection-manager/interfaces/updatedBy.d.ts +0 -4
  49. package/es/collection-manager/interfaces/url.d.ts +1 -20
  50. package/es/collection-manager/interfaces/uuid.d.ts +1 -34
  51. package/es/collection-manager/template-fields.d.ts +53 -0
  52. package/es/components/KeepAlive.d.ts +22 -0
  53. package/es/components/RouterBridge.d.ts +9 -0
  54. package/es/components/form/DialogFormLayout.d.ts +5 -29
  55. package/es/components/form/VariableInput.d.ts +53 -2
  56. package/es/components/form/filter/CollectionFilter.d.ts +49 -0
  57. package/es/components/form/filter/CollectionFilterItem.d.ts +49 -0
  58. package/es/components/form/filter/DateFilterDynamicComponent.d.ts +57 -0
  59. package/es/components/form/filter/FilterValueInput.d.ts +29 -0
  60. package/es/components/form/filter/index.d.ts +11 -0
  61. package/es/components/form/filter/useFilterActionProps.d.ts +96 -0
  62. package/es/components/form/index.d.ts +1 -0
  63. package/es/data-source/ExtendCollectionsProvider.d.ts +50 -0
  64. package/es/data-source/index.d.ts +9 -0
  65. package/es/flow/FlowPage.d.ts +2 -1
  66. package/es/flow/admin-shell/AdminLayoutRouteCoordinator.d.ts +8 -40
  67. package/es/flow/admin-shell/BaseLayoutModel.d.ts +89 -0
  68. package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +74 -0
  69. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +12 -0
  70. package/es/flow/admin-shell/admin-layout/AdminLayoutModel.d.ts +7 -92
  71. package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +11 -0
  72. package/es/flow/admin-shell/admin-layout/index.d.ts +3 -0
  73. package/es/flow/admin-shell/admin-layout/useApplications.d.ts +3 -2
  74. package/es/flow/admin-shell/useAdminLayoutRoutePage.d.ts +2 -2
  75. package/es/flow/admin-shell/useLayoutRoutePage.d.ts +23 -0
  76. package/es/flow/components/FlowRoute.d.ts +10 -1
  77. package/es/flow/components/filter/index.d.ts +2 -0
  78. package/es/flow/components/filter/useFilterOptions.d.ts +66 -0
  79. package/es/flow/index.d.ts +4 -0
  80. package/es/flow/models/base/PageModel/PageModel.d.ts +3 -1
  81. package/es/flow/models/blocks/assign-form/assignFieldValuesFlow.d.ts +84 -0
  82. package/es/flow/models/blocks/assign-form/index.d.ts +1 -0
  83. package/es/flow/models/blocks/form/FormActionGroupModel.d.ts +1 -0
  84. package/es/flow/models/blocks/form/FormActionModel.d.ts +9 -2
  85. package/es/flow/models/blocks/table/TableBlockModel.d.ts +10 -0
  86. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.d.ts +1 -1
  87. package/es/flow-compat/passwordUtils.d.ts +1 -1
  88. package/es/index.d.ts +6 -0
  89. package/es/index.mjs +552 -459
  90. package/es/layout-manager/LayoutContentRoute.d.ts +14 -0
  91. package/es/layout-manager/LayoutManager.d.ts +22 -0
  92. package/es/layout-manager/LayoutRoute.d.ts +14 -0
  93. package/es/layout-manager/index.d.ts +13 -0
  94. package/es/layout-manager/types.d.ts +20 -0
  95. package/es/layout-manager/utils.d.ts +14 -0
  96. package/es/nocobase-buildin-plugin/index.d.ts +3 -10
  97. package/es/settings-center/index.d.ts +1 -1
  98. package/es/settings-center/plugin-manager/BulkEnableButton.d.ts +15 -0
  99. package/es/settings-center/plugin-manager/PluginCard.d.ts +15 -0
  100. package/es/settings-center/plugin-manager/PluginDetail.d.ts +16 -0
  101. package/es/settings-center/{PluginManagerPage.d.ts → plugin-manager/index.d.ts} +1 -7
  102. package/es/settings-center/plugin-manager/types.d.ts +34 -0
  103. package/lib/index.js +552 -459
  104. package/package.json +8 -7
  105. package/src/Application.tsx +51 -12
  106. package/src/BaseApplication.tsx +32 -0
  107. package/src/PluginSettingsManager.ts +1 -1
  108. package/src/RouterManager.tsx +17 -1
  109. package/src/__tests__/PluginSettingsManager.test.ts +41 -2
  110. package/src/__tests__/app.test.tsx +17 -1
  111. package/src/__tests__/globalDeps.test.ts +1 -0
  112. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +45 -2
  113. package/src/__tests__/plugin-manager.test.tsx +177 -0
  114. package/src/__tests__/settings-center.test.tsx +24 -2
  115. package/src/collection-field-interface/CollectionFieldInterface.ts +71 -77
  116. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +201 -4
  117. package/src/collection-manager/field-configure.ts +548 -0
  118. package/src/collection-manager/field-validation.ts +195 -0
  119. package/src/collection-manager/filter-operators/index.ts +176 -0
  120. package/src/collection-manager/{interfaces/properties → filter-operators}/operators.ts +24 -13
  121. package/src/collection-manager/interfaces/checkbox.ts +2 -9
  122. package/src/collection-manager/interfaces/checkboxGroup.ts +2 -10
  123. package/src/collection-manager/interfaces/collection.ts +2 -15
  124. package/src/collection-manager/interfaces/color.ts +2 -2
  125. package/src/collection-manager/interfaces/createdAt.ts +2 -2
  126. package/src/collection-manager/interfaces/createdBy.ts +1 -12
  127. package/src/collection-manager/interfaces/dateOnly.ts +8 -2
  128. package/src/collection-manager/interfaces/datetime.ts +2 -2
  129. package/src/collection-manager/interfaces/datetimeNoTz.ts +2 -2
  130. package/src/collection-manager/interfaces/email.ts +2 -9
  131. package/src/collection-manager/interfaces/id.ts +1 -2
  132. package/src/collection-manager/interfaces/index.ts +2 -3
  133. package/src/collection-manager/interfaces/input.ts +2 -133
  134. package/src/collection-manager/interfaces/integer.ts +2 -71
  135. package/src/collection-manager/interfaces/json.tsx +17 -11
  136. package/src/collection-manager/interfaces/m2m.tsx +0 -21
  137. package/src/collection-manager/interfaces/m2o.tsx +0 -22
  138. package/src/collection-manager/interfaces/markdown.ts +2 -51
  139. package/src/collection-manager/interfaces/multipleSelect.ts +2 -14
  140. package/src/collection-manager/interfaces/nanoid.ts +2 -2
  141. package/src/collection-manager/interfaces/number.ts +2 -85
  142. package/src/collection-manager/interfaces/o2m.tsx +1 -22
  143. package/src/collection-manager/interfaces/obo.tsx +145 -0
  144. package/src/collection-manager/interfaces/oho.tsx +145 -0
  145. package/src/collection-manager/interfaces/password.ts +2 -44
  146. package/src/collection-manager/interfaces/percent.ts +2 -74
  147. package/src/collection-manager/interfaces/phone.ts +2 -2
  148. package/src/collection-manager/interfaces/properties/index.ts +0 -133
  149. package/src/collection-manager/interfaces/radioGroup.ts +2 -2
  150. package/src/collection-manager/interfaces/richText.ts +2 -51
  151. package/src/collection-manager/interfaces/select.ts +2 -14
  152. package/src/collection-manager/interfaces/snowflake-id.ts +2 -2
  153. package/src/collection-manager/interfaces/tableoid.ts +1 -2
  154. package/src/collection-manager/interfaces/textarea.ts +2 -51
  155. package/src/collection-manager/interfaces/time.ts +2 -2
  156. package/src/collection-manager/interfaces/types.ts +4 -12
  157. package/src/collection-manager/interfaces/unixTimestamp.tsx +2 -2
  158. package/src/collection-manager/interfaces/updatedAt.ts +2 -2
  159. package/src/collection-manager/interfaces/updatedBy.ts +1 -12
  160. package/src/collection-manager/interfaces/url.ts +2 -4
  161. package/src/collection-manager/interfaces/uuid.ts +2 -2
  162. package/src/collection-manager/template-fields.ts +109 -0
  163. package/src/components/KeepAlive.tsx +131 -0
  164. package/src/components/README.md +90 -6
  165. package/src/components/README.zh-CN.md +90 -7
  166. package/src/components/RouterBridge.tsx +28 -4
  167. package/src/components/__tests__/KeepAlive.test.tsx +63 -0
  168. package/src/components/__tests__/RouterBridge.test.tsx +27 -0
  169. package/src/components/form/DialogFormLayout.tsx +5 -29
  170. package/src/components/form/VariableInput.tsx +101 -28
  171. package/src/components/form/__tests__/VariableInput.test.ts +85 -0
  172. package/src/components/form/filter/CollectionFilter.tsx +111 -0
  173. package/src/components/form/filter/CollectionFilterItem.tsx +184 -0
  174. package/src/components/form/filter/DateFilterDynamicComponent.tsx +283 -0
  175. package/src/components/form/filter/FilterValueInput.tsx +198 -0
  176. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +247 -0
  177. package/src/components/form/filter/__tests__/DateFilterDynamicComponent.test.tsx +148 -0
  178. package/src/components/form/filter/__tests__/FilterValueInput.test.tsx +243 -0
  179. package/src/components/form/filter/__tests__/compileFilterGroup.test.ts +146 -0
  180. package/src/components/form/filter/index.ts +13 -0
  181. package/src/components/form/filter/useFilterActionProps.ts +203 -0
  182. package/src/components/form/index.tsx +1 -0
  183. package/src/data-source/ExtendCollectionsProvider.tsx +144 -0
  184. package/src/data-source/__tests__/ExtendCollectionsProvider.test.tsx +264 -0
  185. package/src/data-source/index.ts +10 -0
  186. package/src/flow/FlowPage.tsx +35 -7
  187. package/src/flow/__tests__/FlowPage.test.tsx +79 -0
  188. package/src/flow/__tests__/FlowRoute.test.tsx +529 -2
  189. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +191 -0
  190. package/src/flow/actions/__tests__/openView.subModelKey.test.tsx +33 -0
  191. package/src/flow/actions/aclCheck.tsx +4 -0
  192. package/src/flow/actions/aclCheckRefresh.tsx +4 -0
  193. package/src/flow/actions/dateTimeFormat.tsx +12 -8
  194. package/src/flow/actions/linkageRules.tsx +122 -0
  195. package/src/flow/actions/openView.tsx +28 -4
  196. package/src/flow/admin-shell/AdminLayoutRouteCoordinator.ts +11 -329
  197. package/src/flow/admin-shell/BaseLayoutModel.tsx +455 -0
  198. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +502 -0
  199. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +547 -3
  200. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +35 -7
  201. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +160 -0
  202. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +0 -12
  203. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +28 -201
  204. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +11 -2
  205. package/src/flow/admin-shell/admin-layout/AppListRender.tsx +139 -0
  206. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +1 -26
  207. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +149 -27
  208. package/src/flow/admin-shell/admin-layout/index.ts +3 -0
  209. package/src/flow/admin-shell/admin-layout/useApplications.tsx +34 -1
  210. package/src/flow/admin-shell/useAdminLayoutRoutePage.ts +10 -26
  211. package/src/flow/admin-shell/useLayoutRoutePage.ts +61 -0
  212. package/src/flow/components/AdminLayout.tsx +4 -154
  213. package/src/flow/components/FlowRoute.tsx +105 -15
  214. package/src/flow/components/filter/index.ts +3 -0
  215. package/src/flow/components/filter/useFilterOptions.ts +102 -0
  216. package/src/flow/index.ts +4 -0
  217. package/src/flow/models/actions/UpdateRecordActionModel.tsx +14 -95
  218. package/src/flow/models/actions/UpdateRecordActionUtils.ts +4 -7
  219. package/src/flow/models/actions/__tests__/AssignFormRefill.test.ts +26 -1
  220. package/src/flow/models/base/ActionModel.tsx +8 -1
  221. package/src/flow/models/base/PageModel/PageModel.tsx +51 -18
  222. package/src/flow/models/base/PageModel/RootPageModel.tsx +6 -13
  223. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +102 -1
  224. package/src/flow/models/base/RouteModel.tsx +1 -1
  225. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +63 -2
  226. package/src/flow/models/blocks/assign-form/assignFieldValuesFlow.tsx +206 -0
  227. package/src/flow/models/blocks/assign-form/index.ts +1 -0
  228. package/src/flow/models/blocks/form/FormActionGroupModel.tsx +14 -0
  229. package/src/flow/models/blocks/form/FormActionModel.tsx +30 -3
  230. package/src/flow/models/blocks/form/FormItemModel.tsx +8 -1
  231. package/src/flow/models/blocks/form/__tests__/FormActionGroupModel.test.ts +46 -0
  232. package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +71 -0
  233. package/src/flow/models/blocks/form/submitHandler.ts +8 -1
  234. package/src/flow/models/blocks/form/submitValues.ts +4 -1
  235. package/src/flow/models/blocks/table/TableBlockModel.tsx +118 -16
  236. package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +114 -0
  237. package/src/flow/models/fields/AssociationFieldModel/SubFormFieldModel.tsx +7 -1
  238. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +1 -1
  239. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -5
  240. package/src/flow/models/fields/ClickableFieldModel.tsx +9 -1
  241. package/src/flow/models/fields/CollectionSelectorFieldModel.tsx +8 -2
  242. package/src/flow/models/fields/DisplayEnumFieldModel.tsx +8 -2
  243. package/src/flow/models/fields/DisplayTimeFieldModel.tsx +1 -1
  244. package/src/flow/models/fields/TimeFieldModel.tsx +1 -1
  245. package/src/flow/models/fields/__tests__/TimeFieldModel.test.tsx +61 -0
  246. package/src/flow/models/fields/mobile-components/MobileDatePicker.tsx +19 -3
  247. package/src/flow/models/fields/mobile-components/__tests__/MobileDatePicker.test.tsx +94 -0
  248. package/src/flow/models/topbar/TopbarActionModel.tsx +1 -1
  249. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +91 -0
  250. package/src/index.ts +6 -0
  251. package/src/layout-manager/LayoutContentRoute.tsx +90 -0
  252. package/src/layout-manager/LayoutManager.tsx +185 -0
  253. package/src/layout-manager/LayoutRoute.tsx +138 -0
  254. package/src/layout-manager/__tests__/LayoutManager.test.tsx +335 -0
  255. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +473 -0
  256. package/src/layout-manager/index.ts +14 -0
  257. package/src/layout-manager/types.ts +22 -0
  258. package/src/layout-manager/utils.ts +37 -0
  259. package/src/nocobase-buildin-plugin/index.tsx +69 -67
  260. package/src/nocobase-buildin-plugin/plugins/LocalePlugin.ts +1 -0
  261. package/src/settings-center/index.ts +1 -1
  262. package/src/settings-center/plugin-manager/BulkEnableButton.tsx +111 -0
  263. package/src/settings-center/plugin-manager/PluginCard.tsx +270 -0
  264. package/src/settings-center/plugin-manager/PluginDetail.tsx +195 -0
  265. package/src/settings-center/plugin-manager/index.tsx +254 -0
  266. package/src/settings-center/plugin-manager/types.ts +35 -0
  267. package/src/settings-center/utils.tsx +8 -1
  268. package/src/theme/__tests__/globalStyles.test.ts +24 -0
  269. package/src/theme/globalStyles.ts +10 -0
  270. package/src/utils/globalDeps.ts +2 -0
  271. package/es/collection-manager/interfaces/linkTo.d.ts +0 -90
  272. package/es/collection-manager/interfaces/o2o.d.ts +0 -621
  273. package/es/collection-manager/interfaces/properties/operators.d.ts +0 -294
  274. package/es/collection-manager/interfaces/subTable.d.ts +0 -172
  275. package/src/collection-manager/interfaces/linkTo.ts +0 -120
  276. package/src/collection-manager/interfaces/o2o.tsx +0 -561
  277. package/src/collection-manager/interfaces/subTable.ts +0 -218
  278. package/src/settings-center/PluginManagerPage.tsx +0 -162
@@ -0,0 +1,146 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { describe, expect, it } from 'vitest';
11
+ import { compileFilterGroup } from '../useFilterActionProps';
12
+
13
+ describe('compileFilterGroup', () => {
14
+ it('returns undefined for an empty group so callers can drop the filter param', () => {
15
+ expect(compileFilterGroup(undefined)).toBeUndefined();
16
+ expect(compileFilterGroup({ logic: '$and', items: [] })).toBeUndefined();
17
+ });
18
+
19
+ it('compiles a single condition into the NocoBase {path: {op: val}} envelope', () => {
20
+ const out = compileFilterGroup({
21
+ logic: '$and',
22
+ items: [{ path: 'lockReason', operator: '$includes', value: 'abuse' }],
23
+ });
24
+ expect(out).toEqual({ $and: [{ lockReason: { $includes: 'abuse' } }] });
25
+ });
26
+
27
+ it('preserves the parent logic ($and / $or)', () => {
28
+ const out = compileFilterGroup({
29
+ logic: '$or',
30
+ items: [
31
+ { path: 'lockReason', operator: '$eq', value: 'abuse' },
32
+ { path: 'lockReason', operator: '$eq', value: 'spam' },
33
+ ],
34
+ });
35
+ expect(out).toEqual({
36
+ $or: [{ lockReason: { $eq: 'abuse' } }, { lockReason: { $eq: 'spam' } }],
37
+ });
38
+ });
39
+
40
+ it('compiles nested groups recursively', () => {
41
+ const out = compileFilterGroup({
42
+ logic: '$and',
43
+ items: [
44
+ { path: 'lockReason', operator: '$eq', value: 'abuse' },
45
+ {
46
+ logic: '$or',
47
+ items: [
48
+ { path: 'lockedTs', operator: '$dateAfter', value: '2026-01-01' },
49
+ { path: 'lockedTs', operator: '$dateBefore', value: '2026-12-31' },
50
+ ],
51
+ },
52
+ ],
53
+ });
54
+ expect(out).toEqual({
55
+ $and: [
56
+ { lockReason: { $eq: 'abuse' } },
57
+ {
58
+ $or: [{ lockedTs: { $dateAfter: '2026-01-01' } }, { lockedTs: { $dateBefore: '2026-12-31' } }],
59
+ },
60
+ ],
61
+ });
62
+ });
63
+
64
+ it('drops items with empty values (undefined / "" / [] / {}) so half-edited rows do not 500 the server', () => {
65
+ // Mirrors v1's `removeNullCondition` behaviour. A user who selected a field + operator but hasn't typed a value yet must NOT cause `{path:{operator:undefined}}` to fly out — the server rejects empty operator bodies on `$dateOn` etc.
66
+ const out = compileFilterGroup({
67
+ logic: '$and',
68
+ items: [
69
+ { path: 'lockedTs', operator: '$dateOn', value: undefined },
70
+ { path: 'lockReason', operator: '$eq', value: '' },
71
+ { path: 'lockReason', operator: '$in', value: [] },
72
+ { path: 'lockedTs', operator: '$dateOn', value: {} },
73
+ { path: 'lockReason', operator: '$eq', value: 'kept' },
74
+ ],
75
+ });
76
+ expect(out).toEqual({ $and: [{ lockReason: { $eq: 'kept' } }] });
77
+ });
78
+
79
+ it('expands dotted association paths into nested objects (matches v1 payload shape)', () => {
80
+ // `user.createdBy.password` must reach the server as a nested object so the filter resolver walks the association chain. Flattened-key form (`{ "user.createdBy.password": ... }`) leaves the server treating the whole string as one column name.
81
+ const out = compileFilterGroup({
82
+ logic: '$and',
83
+ items: [{ path: 'user.createdBy.password', operator: '$includes', value: '123' }],
84
+ });
85
+ expect(out).toEqual({
86
+ $and: [{ user: { createdBy: { password: { $includes: '123' } } } }],
87
+ });
88
+ });
89
+
90
+ it('keeps relative date descriptors (non-empty plain objects) intact', () => {
91
+ // `{ type: 'today' }` is an empty-keys-only check away from being pruned by accident. Confirm it survives — that's the server-readable shape for relative-date filters.
92
+ const out = compileFilterGroup({
93
+ logic: '$and',
94
+ items: [{ path: 'lockedTs', operator: '$dateOn', value: { type: 'today' } }],
95
+ });
96
+ expect(out).toEqual({ $and: [{ lockedTs: { $dateOn: { type: 'today' } } }] });
97
+ });
98
+
99
+ it('drops items missing path or operator so half-typed rows do not break the query', () => {
100
+ const out = compileFilterGroup({
101
+ logic: '$and',
102
+ items: [
103
+ { path: '', operator: '$eq', value: 'orphan' },
104
+ { path: 'lockReason', operator: '', value: 'orphan' },
105
+ { path: 'lockReason', operator: '$eq', value: 'kept' },
106
+ ],
107
+ });
108
+ expect(out).toEqual({ $and: [{ lockReason: { $eq: 'kept' } }] });
109
+ });
110
+
111
+ it('drops empty nested groups so a half-built sub-group does not produce { $or: [] }', () => {
112
+ const out = compileFilterGroup({
113
+ logic: '$and',
114
+ items: [
115
+ { path: 'lockReason', operator: '$eq', value: 'abuse' },
116
+ { logic: '$or', items: [] },
117
+ ],
118
+ });
119
+ expect(out).toEqual({ $and: [{ lockReason: { $eq: 'abuse' } }] });
120
+ });
121
+
122
+ it('returns undefined when every item drops out', () => {
123
+ const out = compileFilterGroup({
124
+ logic: '$and',
125
+ items: [
126
+ { path: '', operator: '', value: '' },
127
+ { logic: '$or', items: [] },
128
+ ],
129
+ });
130
+ expect(out).toBeUndefined();
131
+ });
132
+
133
+ it('passes complex value shapes (date descriptors, arrays) through unchanged', () => {
134
+ const dateDescriptor = { type: 'past', number: 3, unit: 'day' };
135
+ const out = compileFilterGroup({
136
+ logic: '$and',
137
+ items: [
138
+ { path: 'lockedTs', operator: '$dateOn', value: dateDescriptor },
139
+ { path: 'lockReason', operator: '$in', value: ['abuse', 'spam'] },
140
+ ],
141
+ });
142
+ expect(out).toEqual({
143
+ $and: [{ lockedTs: { $dateOn: dateDescriptor } }, { lockReason: { $in: ['abuse', 'spam'] } }],
144
+ });
145
+ });
146
+ });
@@ -0,0 +1,13 @@
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
+ // Higher-level filter compositions for non-schema surfaces (settings pages, panels, side drawers). The low-level primitives — `FilterContainer`, `FilterGroup`, `FilterItem`, `fieldsToOptions`, `useFilterOptions` — live under `src/flow/components/filter/`; this layer composes them with a `Collection` binding and exposes the hook/component pair callers actually reach for. The dependency direction is form/filter → flow/components/filter only.
11
+ export { CollectionFilter } from './CollectionFilter';
12
+ export type { CollectionFilterProps } from './CollectionFilter';
13
+ export type { CompiledFilter } from './useFilterActionProps';
@@ -0,0 +1,203 @@
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 { Collection, observable } from '@nocobase/flow-engine';
11
+ import { useMemoizedFn } from 'ahooks';
12
+ import { useMemo, useRef } from 'react';
13
+ import { FilterOption, useFilterOptions, UseFilterOptionsArgs } from '../../../flow/components/filter/useFilterOptions';
14
+ import { CollectionFilterItemValue, createCollectionFilterItem } from './CollectionFilterItem';
15
+
16
+ /** A single condition row (`{ path, operator, value }`) or a nested group. */
17
+ export type FilterGroupItem = CollectionFilterItemValue | FilterGroupValue;
18
+
19
+ /**
20
+ * Reactive shape consumed by `FilterContainer` / `FilterGroup`. `logic` is the join (`$and` / `$or`) and `items` is a heterogeneous list of leaf conditions and nested groups.
21
+ */
22
+ export type FilterGroupValue = {
23
+ logic: '$and' | '$or';
24
+ items: FilterGroupItem[];
25
+ };
26
+
27
+ /** Compiled filter param accepted by NocoBase resource `list`. */
28
+ export type CompiledFilter = Record<string, unknown> | undefined;
29
+
30
+ interface FilterCtxModel {
31
+ translate: (key: string) => string;
32
+ dispatchEvent: (event: 'submit' | 'reset' | (string & {})) => void;
33
+ }
34
+
35
+ export interface FilterCtx {
36
+ model: FilterCtxModel;
37
+ }
38
+
39
+ const isGroup = (item: FilterGroupItem): item is FilterGroupValue =>
40
+ Array.isArray((item as FilterGroupValue).items) && typeof (item as FilterGroupValue).logic === 'string';
41
+
42
+ const isCondition = (item: FilterGroupItem): item is CollectionFilterItemValue =>
43
+ typeof (item as CollectionFilterItemValue).path === 'string' &&
44
+ Object.prototype.hasOwnProperty.call(item, 'operator');
45
+
46
+ /**
47
+ * `true` when the rhs of a condition is "no real value yet" — covers `undefined` / `null` / empty string / empty array / empty plain object. Mirrors v1's `removeNullCondition` `isEmpty` predicate so half-filled rows ("Locked time → is → (no date picked yet)") get dropped on Submit instead of being sent to the server as `{lockedTs:{}}` and triggering a 500.
48
+ */
49
+ const isEmptyFilterValue = (value: unknown): boolean => {
50
+ if (value === undefined || value === null || value === '') return true;
51
+ if (Array.isArray(value)) return value.length === 0;
52
+ if (typeof value === 'object') {
53
+ // Plain `{}` only — descriptor shapes like `{ type: 'today' }` have own keys and survive this check.
54
+ return Object.keys(value as Record<string, unknown>).length === 0;
55
+ }
56
+ return false;
57
+ };
58
+
59
+ /**
60
+ * Build a nested object from a dotted path. `'user.createdBy.password'` + `{ $includes: '123' }` becomes `{ user: { createdBy: { password: { $includes: '123' } } } }`. Matches v1's filter payload shape so server-side filter resolution sees the same association chain whether the request came from a v1 or v2 page.
61
+ */
62
+ const nestPath = (path: string, leaf: unknown): Record<string, unknown> => {
63
+ const segments = path.split('.');
64
+ let result: unknown = leaf;
65
+ for (let i = segments.length - 1; i >= 0; i--) {
66
+ result = { [segments[i]]: result };
67
+ }
68
+ return result as Record<string, unknown>;
69
+ };
70
+
71
+ /**
72
+ * Compile a reactive filter group into the `{ $and: [{ path: { op: val } }] }` envelope accepted by NocoBase's resource `list` filter param. Returns `undefined` when the group is empty so callers can drop the param.
73
+ *
74
+ * Mirrors v1's `removeNullCondition` + filter compile path, but works on the v2 `{ logic, items }` group structure rather than v1's Formily-bracketed `$and.0.path.$eq` shape:
75
+ *
76
+ * - Rows missing `path` or `operator` are dropped (still mid-edit).
77
+ * - Rows whose `value` is empty (`undefined`, `''`, `[]`, `{}`) are dropped — matches v1, which sends `filter={}` for a row with only a field/operator picked. Sending `{lockedTs:{}}` would 500.
78
+ * - Dotted association paths (`user.createdBy.password`) are expanded into nested objects — matches v1's payload shape, which the server resolves along the association chain rather than treating the dotted string as a single key.
79
+ * - Empty groups (after pruning) propagate as `undefined` so the outermost caller can drop the whole `filter` param.
80
+ */
81
+ export function compileFilterGroup(group: FilterGroupValue | undefined): CompiledFilter {
82
+ if (!group?.items?.length) return undefined;
83
+ const compiled = group.items
84
+ .map((entry) => {
85
+ if (isGroup(entry)) return compileFilterGroup(entry);
86
+ if (!isCondition(entry) || !entry.path || !entry.operator) return undefined;
87
+ if (isEmptyFilterValue(entry.value)) return undefined;
88
+ return nestPath(entry.path, { [entry.operator]: entry.value });
89
+ })
90
+ .filter((v): v is Record<string, unknown> => !!v);
91
+ if (!compiled.length) return undefined;
92
+ return { [group.logic]: compiled };
93
+ }
94
+
95
+ const createEmptyGroup = (): FilterGroupValue => ({ logic: '$and', items: [] });
96
+
97
+ /** Which footer button triggered the apply — useful for closing a popover on Submit but keeping it open on Reset. */
98
+ export type FilterApplyAction = 'submit' | 'reset';
99
+
100
+ export interface UseFilterActionPropsArgs extends UseFilterOptionsArgs {
101
+ /** Collection whose fields populate the filter row's field picker. */
102
+ collection: Collection | undefined;
103
+ /**
104
+ * Called when the user submits or resets the filter popover. Receives the compiled filter param (`undefined` when cleared) and which footer button triggered the call. Typical implementation: `(filter, action) => { listRequest.run(filter); if (action === 'submit') closePopover(); }`.
105
+ */
106
+ onApply: (filter: CompiledFilter, action: FilterApplyAction) => void;
107
+ }
108
+
109
+ export interface UseFilterActionPropsResult {
110
+ /**
111
+ * Reactive filter group state. Pass directly to `<FilterContent value={...}>`. Stable across renders.
112
+ */
113
+ value: FilterGroupValue;
114
+ /** Field-option tree (for inspection or custom badges). */
115
+ options: FilterOption[];
116
+ /** Bound `FilterItem` component to plug into `<FilterContent FilterItem={...}>`. */
117
+ FilterItem: ReturnType<typeof createCollectionFilterItem> | undefined;
118
+ /**
119
+ * Ready-to-use `ctx` for `<FilterContent ctx={...}>`. Wires Submit / Reset buttons to `onSubmit` / `onReset` below.
120
+ */
121
+ ctx: FilterCtx;
122
+ /** Imperative trigger — submit current group as a compiled filter. */
123
+ onSubmit: () => void;
124
+ /** Imperative trigger — clear the group and emit an empty filter. */
125
+ onReset: () => void;
126
+ /**
127
+ * Count of top-level condition rows. Useful for showing a badge like `Filter (3)` on the trigger button — matches v1's `field.title = t('{{count}} filter items', { count })`.
128
+ */
129
+ conditionCount: number;
130
+ }
131
+
132
+ /**
133
+ * v2 equivalent of v1's `useFilterActionProps` for non-schema surfaces (settings pages, panels, side drawers). Bundles three things v1's hook returned implicitly through schema:
134
+ *
135
+ * - A reactive `{ logic, items }` group state that `<FilterContent>` reads.
136
+ * - A bound `FilterItem` component (driven by `createCollectionFilterItem`).
137
+ * - A `ctx` object that turns `<FilterContent>`'s `dispatchEvent('submit' | 'reset')` into a compiled filter param passed to `onApply`.
138
+ *
139
+ * Pair with antd `Popover` to recreate the legacy `Filter.Action` UX:
140
+ *
141
+ * ```tsx
142
+ * const { value, ctx, FilterItem, onSubmit, conditionCount } = useFilterActionProps({
143
+ * collection,
144
+ * onApply: (filter) => listRequest.run(filter),
145
+ * t,
146
+ * });
147
+ * return (
148
+ * <Popover content={<FilterContent value={value} ctx={ctx} FilterItem={FilterItem} />}>
149
+ * <Button>{t('Filter')}{conditionCount ? ` (${conditionCount})` : ''}</Button>
150
+ * </Popover>
151
+ * );
152
+ * ```
153
+ */
154
+ export function useFilterActionProps(args: UseFilterActionPropsArgs): UseFilterActionPropsResult {
155
+ const { collection, onApply, filterableFieldNames, nonfilterableFieldNames, noIgnore, t } = args;
156
+
157
+ // Held in a ref so the group object identity is stable for the lifetime of the host component — `<FilterContent>` mutates this object directly (push/splice on `items`, swap `logic`), and a fresh observable on every render would reset that internal state.
158
+ const valueRef = useRef<FilterGroupValue>();
159
+ if (!valueRef.current) {
160
+ valueRef.current = observable(createEmptyGroup()) as FilterGroupValue;
161
+ }
162
+ const value = valueRef.current;
163
+
164
+ const options = useFilterOptions(collection, { filterableFieldNames, nonfilterableFieldNames, noIgnore, t });
165
+
166
+ const FilterItem = useMemo(
167
+ () =>
168
+ collection
169
+ ? createCollectionFilterItem(collection, { filterableFieldNames, nonfilterableFieldNames, noIgnore, t })
170
+ : undefined,
171
+ [collection, filterableFieldNames, nonfilterableFieldNames, noIgnore, t],
172
+ );
173
+
174
+ const onSubmit = useMemoizedFn(() => {
175
+ onApply(compileFilterGroup(value), 'submit');
176
+ });
177
+
178
+ const onReset = useMemoizedFn(() => {
179
+ value.logic = '$and';
180
+ value.items = [];
181
+ onApply(undefined, 'reset');
182
+ });
183
+
184
+ const translate = useMemoizedFn((key: string) => (t ? t(key) : key));
185
+
186
+ const ctx = useMemo<FilterCtx>(
187
+ () => ({
188
+ model: {
189
+ translate,
190
+ dispatchEvent: (event: string) => {
191
+ if (event === 'submit') onSubmit();
192
+ else if (event === 'reset') onReset();
193
+ },
194
+ },
195
+ }),
196
+ [translate, onSubmit, onReset],
197
+ );
198
+
199
+ // Re-read on each render so `observer`-wrapped hosts re-render when the reactive `items` array length changes. No useMemo needed — the `value` object's identity is stable (held in a ref), but its observable `items.length` is what we actually care about, and the eslint exhaustive-deps rule rightly complains about depending on a mutable property of a stable ref.
200
+ const conditionCount = value.items.length;
201
+
202
+ return { value, options, FilterItem, ctx, onSubmit, onReset, conditionCount };
203
+ }
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  export * from './createFormRegistry';
11
+ export * from './filter';
11
12
  export * from './DialogFormLayout';
12
13
  export * from './DrawerFormLayout';
13
14
  export * from './EnvVariableInput';
@@ -0,0 +1,144 @@
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 { CollectionOptions } from '@nocobase/flow-engine';
11
+ import React, { FC, ReactNode, useEffect, useRef } from 'react';
12
+ import { useApp } from '../hooks';
13
+
14
+ export interface ExtendCollectionsProviderProps {
15
+ /** Data source key to extend. Defaults to `'main'`. */
16
+ dataSource?: string;
17
+ /** Collections to surface for the lifetime of this provider's subtree. */
18
+ collections: CollectionOptions[];
19
+ /**
20
+ * When `true`, re-sync the data source whenever the `collections` prop
21
+ * reference changes after mount: add entries newly present in the prop and
22
+ * remove entries no longer present (only those this provider registered).
23
+ * The diff runs in the same render as the prop change so children see the
24
+ * new state on their first render — at the cost of one observable mutation
25
+ * per change.
26
+ *
27
+ * Defaults to `false`. Most pages pass a stable (often module-level)
28
+ * `collections` list and don't need this; leaving it off avoids accidental
29
+ * re-registration when callers forget to memoize. Enable only when your
30
+ * collection list legitimately varies during the provider's lifetime.
31
+ */
32
+ syncOnChange?: boolean;
33
+ children?: ReactNode;
34
+ }
35
+
36
+ /**
37
+ * Mount-scoped collection injector. Adds the given `collections` to the target
38
+ * data source on first render — synchronously, so children can read
39
+ * `getCollection(name)` on their own first render — and removes them on
40
+ * unmount. Survives mid-session data-source reloads via the
41
+ * `dataSource:loaded` event by re-registering only the names this provider
42
+ * owns.
43
+ *
44
+ * Use this for client-only collections — e.g. a `schema-only` server
45
+ * collection that isn't auto-published to the v2 data source, or a pure
46
+ * UI-side mirror — so downstream components (like `<CollectionFilter>`) can
47
+ * resolve the collection by name.
48
+ *
49
+ * Default behavior is "static-at-mount": subsequent changes to the
50
+ * `collections` prop are ignored. Pass `syncOnChange` to opt into diffing on
51
+ * prop change.
52
+ */
53
+ export const ExtendCollectionsProvider: FC<ExtendCollectionsProviderProps> = ({
54
+ dataSource = 'main',
55
+ collections,
56
+ syncOnChange = false,
57
+ children,
58
+ }) => {
59
+ const app = useApp();
60
+ // Lazy-ref init guard. `ownedRef.current === null` only on the first render;
61
+ // once populated, StrictMode dev's second render and any subsequent
62
+ // re-render see a non-null ref and skip re-registering. React docs bless
63
+ // this idiom for "init exactly once on mount" — see "Avoiding recreating
64
+ // the ref contents".
65
+ const ownedRef = useRef<CollectionOptions[] | null>(null);
66
+ // Identity of the `collections` reference we last reacted to; gates the
67
+ // opt-in diff so StrictMode's double-render doesn't diff twice.
68
+ const lastCollectionsRef = useRef<CollectionOptions[] | null>(null);
69
+
70
+ if (ownedRef.current === null) {
71
+ const ds = app.dataSourceManager?.getDataSource?.(dataSource);
72
+ const owned: CollectionOptions[] = [];
73
+ if (ds) {
74
+ for (const c of collections) {
75
+ if (ds.getCollection?.(c.name)) continue;
76
+ ds.addCollection?.(c);
77
+ owned.push(c);
78
+ }
79
+ }
80
+ ownedRef.current = owned;
81
+ lastCollectionsRef.current = collections;
82
+ } else if (syncOnChange && lastCollectionsRef.current !== collections) {
83
+ const ds = app.dataSourceManager?.getDataSource?.(dataSource);
84
+ if (ds) {
85
+ const nextNames = new Set(collections.map((c) => c.name));
86
+ const prevOwned = new Map(ownedRef.current.map((c) => [c.name, c]));
87
+ for (const name of prevOwned.keys()) {
88
+ if (!nextNames.has(name)) ds.removeCollection?.(name);
89
+ }
90
+ const nextOwned: CollectionOptions[] = [];
91
+ for (const c of collections) {
92
+ const previous = prevOwned.get(c.name);
93
+ if (previous) {
94
+ // First-registered wins: keep the existing options object, mirroring
95
+ // the original behavior where re-adding a present name was a no-op.
96
+ // Callers who need to update a collection should remount the
97
+ // provider (e.g. `key={signature}`).
98
+ nextOwned.push(previous);
99
+ continue;
100
+ }
101
+ if (ds.getCollection?.(c.name)) continue;
102
+ ds.addCollection?.(c);
103
+ nextOwned.push(c);
104
+ }
105
+ ownedRef.current = nextOwned;
106
+ }
107
+ lastCollectionsRef.current = collections;
108
+ }
109
+
110
+ useEffect(() => {
111
+ const onLoaded = (event: Event) => {
112
+ const key = (event as CustomEvent<{ dataSourceKey: string }>).detail?.dataSourceKey;
113
+ if (key !== dataSource && key !== '*') return;
114
+ const ds = app.dataSourceManager?.getDataSource?.(dataSource);
115
+ if (!ds || !ownedRef.current) return;
116
+ // dataSource was just reloaded from the server — our owned client-only
117
+ // entries got wiped. Re-add only the ones we own, using the snapshot in
118
+ // the ref so we don't accidentally seize names this provider never
119
+ // registered.
120
+ for (const c of ownedRef.current) {
121
+ if (ds.getCollection?.(c.name)) continue;
122
+ ds.addCollection?.(c);
123
+ }
124
+ };
125
+ app.eventBus?.addEventListener('dataSource:loaded', onLoaded);
126
+ return () => {
127
+ app.eventBus?.removeEventListener('dataSource:loaded', onLoaded);
128
+ const ds = app.dataSourceManager?.getDataSource?.(dataSource);
129
+ const owned = ownedRef.current ?? [];
130
+ ownedRef.current = null;
131
+ lastCollectionsRef.current = null;
132
+ if (!ds) return;
133
+ for (const c of owned) ds.removeCollection?.(c.name);
134
+ };
135
+ // `collections` / `syncOnChange` intentionally excluded — they drive the
136
+ // render-phase init/diff above, not this effect. The listener reads
137
+ // `ownedRef` each time it fires (async, never during render).
138
+ // eslint-disable-next-line react-hooks/exhaustive-deps
139
+ }, [app, dataSource]);
140
+
141
+ return <>{children}</>;
142
+ };
143
+
144
+ export default ExtendCollectionsProvider;