@nocobase/client-v2 2.2.0-beta.9 → 3.0.0-alpha.1

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 (208) hide show
  1. package/es/APIClient.d.ts +1 -3
  2. package/es/BaseApplication.d.ts +1 -0
  3. package/es/acl/ACLProvider.d.ts +10 -0
  4. package/es/authRedirect.d.ts +5 -0
  5. package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
  6. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
  7. package/es/components/AttachmentUpload.d.ts +51 -0
  8. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  9. package/es/components/category-tabs/index.d.ts +9 -0
  10. package/es/components/form/ScanInput/useCodeScanner.d.ts +3 -2
  11. package/es/components/form/TypedVariableInput.d.ts +31 -5
  12. package/es/components/form/filter/CollectionFilter.d.ts +2 -0
  13. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  14. package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
  15. package/es/components/form/filter/index.d.ts +2 -0
  16. package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
  17. package/es/components/index.d.ts +2 -0
  18. package/es/flow/actions/index.d.ts +1 -1
  19. package/es/flow/actions/linkageRules.d.ts +3 -0
  20. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
  21. package/es/flow/admin-shell/admin-layout/NocoBaseLogo.d.ts +10 -0
  22. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  23. package/es/flow/components/FieldAssignValueInput.d.ts +9 -0
  24. package/es/flow/components/FlowRoute.d.ts +2 -2
  25. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  26. package/es/flow/components/filter/FilterGroup.d.ts +1 -0
  27. package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
  28. package/es/flow/index.d.ts +1 -0
  29. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  30. package/es/flow/models/base/ActionModelCore.d.ts +4 -2
  31. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  32. package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
  33. package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
  34. package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
  35. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  36. package/es/flow/models/blocks/table/TableColumnModel.d.ts +1 -0
  37. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  38. package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
  39. package/es/flow-compat/routeTypes.d.ts +1 -0
  40. package/es/index.d.ts +8 -1
  41. package/es/index.mjs +190 -141
  42. package/es/nocobase-buildin-plugin/currentUserAuthStatus.d.ts +11 -0
  43. package/es/nocobase-buildin-plugin/index.d.ts +9 -0
  44. package/es/settings-app/SettingsApplication.d.ts +18 -0
  45. package/es/settings-app/SettingsBrand.d.ts +16 -0
  46. package/es/settings-app/SettingsBuildInPlugin.d.ts +9 -0
  47. package/es/settings-app/SettingsDocumentRedirect.d.ts +10 -0
  48. package/es/settings-app/SettingsGroupNav.d.ts +17 -0
  49. package/es/settings-app/SettingsPluginSettingsManager.d.ts +14 -0
  50. package/es/settings-app/SettingsRouterManager.d.ts +15 -0
  51. package/es/settings-app/SettingsSearch.d.ts +17 -0
  52. package/es/settings-app/SettingsShell.d.ts +10 -0
  53. package/es/settings-app/isSettingsApp.d.ts +18 -0
  54. package/es/settings-app/runtimePaths.d.ts +18 -0
  55. package/es/settings-app/settingsDocumentPath.d.ts +14 -0
  56. package/es/settings-app/settingsTheme.d.ts +87 -0
  57. package/es/settings-app/useSettingsThemeConfig.d.ts +22 -0
  58. package/es/settings-center/AdminSettingsLayout.d.ts +2 -2
  59. package/es/settings-center/groups.d.ts +66 -0
  60. package/es/settings-center/useSettingsGroups.d.ts +36 -0
  61. package/es/settings-center/useSettingsSearch.d.ts +35 -0
  62. package/es/settings-center/utils.d.ts +24 -0
  63. package/es/theme/index.d.ts +1 -0
  64. package/es/theme/itemActive.d.ts +24 -0
  65. package/es/theme/type.d.ts +9 -0
  66. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  67. package/es/utils/index.d.ts +1 -0
  68. package/lib/index.js +195 -146
  69. package/lib/locale/languageCodes.js +3 -1
  70. package/package.json +9 -7
  71. package/src/APIClient.ts +1 -10
  72. package/src/Application.tsx +4 -0
  73. package/src/BaseApplication.tsx +11 -6
  74. package/src/__tests__/app.test.tsx +73 -0
  75. package/src/__tests__/authRedirect.test.ts +137 -0
  76. package/src/__tests__/browserChecker.test.ts +185 -6
  77. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  78. package/src/__tests__/mockSettingsApplication.ts +29 -0
  79. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +79 -10
  80. package/src/__tests__/plugin-manager.test.tsx +7 -6
  81. package/src/__tests__/settings-application.test.ts +164 -0
  82. package/src/__tests__/settings-auth-routes.test.ts +71 -0
  83. package/src/__tests__/settings-center.test.tsx +38 -37
  84. package/src/__tests__/settings-layout-root.test.tsx +260 -0
  85. package/src/__tests__/settings-runtime-paths.test.ts +99 -0
  86. package/src/__tests__/settings-shell.test.tsx +200 -0
  87. package/src/acl/ACLProvider.tsx +21 -0
  88. package/src/authRedirect.ts +72 -3
  89. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  90. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  91. package/src/collection-manager/field-validation.ts +1 -1
  92. package/src/components/AttachmentUpload.tsx +275 -0
  93. package/src/components/README.md +7 -1
  94. package/src/components/README.zh-CN.md +6 -1
  95. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  96. package/src/components/category-tabs/index.ts +10 -0
  97. package/src/components/form/JsonTextArea.tsx +4 -0
  98. package/src/components/form/ScanInput/CodeScanner.tsx +79 -35
  99. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  100. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +284 -11
  101. package/src/components/form/ScanInput/useCodeScanner.ts +332 -24
  102. package/src/components/form/TypedVariableInput.tsx +452 -101
  103. package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
  104. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  105. package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
  106. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  107. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
  108. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  109. package/src/components/form/filter/index.ts +2 -0
  110. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  111. package/src/components/index.ts +2 -0
  112. package/src/flow/FlowPage.tsx +9 -1
  113. package/src/flow/__tests__/FlowPage.test.tsx +50 -3
  114. package/src/flow/__tests__/FlowRoute.test.tsx +128 -10
  115. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  116. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  117. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  118. package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
  119. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
  120. package/src/flow/actions/index.ts +2 -0
  121. package/src/flow/actions/linkageRules.tsx +111 -16
  122. package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
  123. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -57
  124. package/src/flow/admin-shell/admin-layout/NocoBaseLogo.tsx +77 -0
  125. package/src/flow/admin-shell/admin-layout/TopbarActionsBar.tsx +37 -14
  126. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
  127. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +77 -13
  128. package/src/flow/common/Markdown/style.ts +4 -0
  129. package/src/flow/components/BlockItemCard.tsx +2 -2
  130. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  131. package/src/flow/components/FieldAssignValueInput.tsx +91 -20
  132. package/src/flow/components/FlowRoute.tsx +78 -32
  133. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  134. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
  135. package/src/flow/components/filter/FilterGroup.tsx +33 -5
  136. package/src/flow/components/filter/VariableFilterItem.tsx +158 -10
  137. package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
  138. package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
  139. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +70 -1
  140. package/src/flow/index.ts +3 -0
  141. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  142. package/src/flow/models/base/ActionModelCore.tsx +11 -2
  143. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  144. package/src/flow/models/base/CollectionBlockModel.tsx +6 -2
  145. package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
  146. package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
  147. package/src/flow/models/base/PageModel/PageTabModel.tsx +249 -18
  148. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
  149. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
  150. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
  151. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +1075 -7
  152. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  153. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  154. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  155. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +28 -3
  156. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
  157. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
  158. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +66 -4
  159. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  160. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  161. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  162. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
  163. package/src/flow/models/blocks/table/TableBlockModel.tsx +4 -2
  164. package/src/flow/models/blocks/table/TableColumnModel.tsx +30 -1
  165. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  166. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
  167. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +2 -1
  168. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  169. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  170. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  171. package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
  172. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  173. package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
  174. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
  175. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
  176. package/src/flow/models/topbar/TopbarActionModel.tsx +52 -180
  177. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  178. package/src/flow-compat/fieldValidationConstants.ts +1 -1
  179. package/src/flow-compat/routeTypes.ts +1 -0
  180. package/src/index.ts +14 -1
  181. package/src/locale/languageCodes.ts +3 -1
  182. package/src/nocobase-buildin-plugin/currentUserAuthStatus.ts +58 -0
  183. package/src/nocobase-buildin-plugin/index.tsx +110 -70
  184. package/src/settings-app/SettingsApplication.ts +31 -0
  185. package/src/settings-app/SettingsBrand.tsx +108 -0
  186. package/src/settings-app/SettingsBuildInPlugin.ts +10 -0
  187. package/src/settings-app/SettingsDocumentRedirect.tsx +26 -0
  188. package/src/settings-app/SettingsGroupNav.tsx +124 -0
  189. package/src/settings-app/SettingsPluginSettingsManager.ts +38 -0
  190. package/src/settings-app/SettingsRouterManager.ts +92 -0
  191. package/src/settings-app/SettingsSearch.tsx +238 -0
  192. package/src/settings-app/SettingsShell.tsx +167 -0
  193. package/src/settings-app/isSettingsApp.ts +21 -0
  194. package/src/settings-app/runtimePaths.ts +104 -0
  195. package/src/settings-app/settingsDocumentPath.ts +66 -0
  196. package/src/settings-app/settingsTheme.ts +301 -0
  197. package/src/settings-app/useSettingsThemeConfig.ts +100 -0
  198. package/src/settings-center/AdminSettingsLayout.tsx +122 -141
  199. package/src/settings-center/SystemSettingsPage.tsx +4 -170
  200. package/src/settings-center/groups.ts +108 -0
  201. package/src/settings-center/useSettingsGroups.ts +105 -0
  202. package/src/settings-center/useSettingsSearch.ts +173 -0
  203. package/src/settings-center/utils.tsx +50 -1
  204. package/src/theme/index.tsx +1 -0
  205. package/src/theme/itemActive.ts +31 -0
  206. package/src/theme/type.ts +10 -0
  207. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  208. package/src/utils/index.tsx +1 -0
@@ -0,0 +1,82 @@
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 { FlowEngine, FlowModel } from '@nocobase/flow-engine';
11
+ import { describe, expect, it } from 'vitest';
12
+ import { JsonFieldModel } from '../JsonFieldModel';
13
+
14
+ interface ValidationRule {
15
+ required?: boolean;
16
+ type?: string;
17
+ validator?: (_rule: unknown, value: unknown) => Promise<void>;
18
+ }
19
+
20
+ function createModels(rules: ValidationRule[]) {
21
+ const engine = new FlowEngine();
22
+ engine.registerModels({ JsonFieldModel });
23
+ const parent = engine.createModel<FlowModel<{ subModels: { field: JsonFieldModel } }>>({
24
+ use: FlowModel,
25
+ uid: 'json-form-item',
26
+ props: { rules },
27
+ subModels: {
28
+ field: {
29
+ use: JsonFieldModel,
30
+ uid: 'json-field',
31
+ },
32
+ },
33
+ });
34
+
35
+ return { field: parent.subModels.field, parent };
36
+ }
37
+
38
+ function initializeJsonValidation(field: JsonFieldModel) {
39
+ const step = field.getFlow('jsonInitSetting')?.steps.initValidation;
40
+ if (!step) {
41
+ throw new Error('JSON validation step is not registered');
42
+ }
43
+ step.handler(field.context, {});
44
+ }
45
+
46
+ describe('JsonFieldModel validation', () => {
47
+ it('replaces legacy empty rules with one serializable JSON validation rule', async () => {
48
+ const { field, parent } = createModels([{}, { required: true }]);
49
+
50
+ initializeJsonValidation(field);
51
+
52
+ const rules = parent.props.rules as ValidationRule[];
53
+ expect(rules).toHaveLength(2);
54
+ expect(rules[0]).toEqual({ required: true });
55
+ expect(rules[1]).toMatchObject({ type: 'any' });
56
+ expect(rules[1].validator).toBeTypeOf('function');
57
+ if (!rules[1].validator) {
58
+ throw new Error('JSON validator is missing');
59
+ }
60
+ await expect(rules[1].validator({}, { name: 'NocoBase' })).resolves.toBeUndefined();
61
+ await expect(rules[1].validator({}, '{"name":')).rejects.toContain('Invalid JSON format');
62
+
63
+ const persistedRules = JSON.parse(JSON.stringify(parent.serialize())).props.rules as ValidationRule[];
64
+ expect(persistedRules).toHaveLength(2);
65
+ expect(persistedRules[1]).toMatchObject({ type: 'any' });
66
+ expect(persistedRules[1].validator).toBeUndefined();
67
+ });
68
+
69
+ it('re-registers the runtime validator without duplicating its persisted placeholder', () => {
70
+ const { field, parent } = createModels([{}]);
71
+ initializeJsonValidation(field);
72
+ const persistedRules = JSON.parse(JSON.stringify(parent.serialize())).props.rules as ValidationRule[];
73
+ parent.setProps({ rules: persistedRules });
74
+
75
+ initializeJsonValidation(field);
76
+
77
+ const rules = parent.props.rules as ValidationRule[];
78
+ expect(rules).toHaveLength(1);
79
+ expect(rules[0]).toMatchObject({ type: 'any' });
80
+ expect(rules[0].validator).toBeTypeOf('function');
81
+ });
82
+ });
@@ -0,0 +1,51 @@
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 { FlowEngine, FlowEngineProvider } from '@nocobase/flow-engine';
11
+ import { render, screen, waitFor } from '@testing-library/react';
12
+ import React from 'react';
13
+ import { describe, expect, it } from 'vitest';
14
+ import { InputFieldModel } from '../InputFieldModel';
15
+ import { VariableFieldFormModel } from '../VariableFieldFormModel';
16
+
17
+ describe('VariableFieldFormModel', () => {
18
+ it('uses DOM-safe name and id for temporary controls created from reserved form property names', async () => {
19
+ const engine = new FlowEngine();
20
+ engine.registerModels({ InputFieldModel, VariableFieldFormModel });
21
+ const model = engine.createModel<VariableFieldFormModel>({
22
+ use: 'VariableFieldFormModel',
23
+ subModels: {
24
+ fields: [
25
+ {
26
+ use: 'InputFieldModel',
27
+ stepParams: {
28
+ fieldSettings: {
29
+ init: {
30
+ fieldPath: 'nodeName',
31
+ },
32
+ },
33
+ },
34
+ },
35
+ ],
36
+ },
37
+ });
38
+ const field = model.subModels.fields[0];
39
+
40
+ render(<FlowEngineProvider engine={engine}>{model.render()}</FlowEngineProvider>);
41
+
42
+ const input = await screen.findByRole('textbox');
43
+ await waitFor(() => {
44
+ expect(field.props.name).toEqual(['__nb_variable_field_nodeName']);
45
+ });
46
+ expect(input).toHaveAttribute('name', '__nb_variable_field_nodeName');
47
+ expect(input).toHaveAttribute('id', '__nb_variable_field_nodeName');
48
+ expect(field.props.id).toEqual(['__nb_variable_field_nodeName']);
49
+ expect(field.getStepParams('fieldSettings', 'init')).toEqual({ fieldPath: 'nodeName' });
50
+ });
51
+ });
@@ -23,6 +23,20 @@ import {
23
23
  } from '../AssociationFieldModel/recordSelectShared';
24
24
  import _ from 'lodash';
25
25
 
26
+ const mobileSelectSafeAreaPaddingBottom = 'calc(12px + env(safe-area-inset-bottom, 0px))';
27
+
28
+ const mobileSelectConfirmFooterStyle: CSSProperties = {
29
+ paddingBottom: mobileSelectSafeAreaPaddingBottom,
30
+ };
31
+
32
+ function getMobileSelectListStyle(hasConfirmFooter: boolean): CSSProperties {
33
+ return {
34
+ maxHeight: '60vh',
35
+ overflowY: 'auto',
36
+ paddingBottom: hasConfirmFooter ? undefined : mobileSelectSafeAreaPaddingBottom,
37
+ };
38
+ }
39
+
26
40
  const labelClassName = css`
27
41
  div {
28
42
  white-space: nowrap !important;
@@ -280,13 +294,7 @@ export function MobileLazySelect(props: Readonly<LazySelectProps>) {
280
294
  showCancelButton
281
295
  />
282
296
  </div>
283
- <div
284
- style={{
285
- maxHeight: '60vh',
286
- overflowY: 'auto',
287
- }}
288
- onScroll={handleScroll}
289
- >
297
+ <div style={getMobileSelectListStyle(isMultiple)} onScroll={handleScroll}>
290
298
  <CheckList multiple={isMultiple} value={selectedValueIds} onChange={handleListChange}>
291
299
  {realOptions.map((item) => {
292
300
  const optionValue = item?.[valueKey];
@@ -311,9 +319,11 @@ export function MobileLazySelect(props: Readonly<LazySelectProps>) {
311
319
  )}
312
320
  </div>
313
321
  {isMultiple && (
314
- <Button block color="primary" onClick={handleConfirm} style={{ marginTop: '16px' }}>
315
- {t('Confirm')}
316
- </Button>
322
+ <div style={mobileSelectConfirmFooterStyle}>
323
+ <Button block color="primary" onClick={handleConfirm} style={{ marginTop: '16px' }}>
324
+ {t('Confirm')}
325
+ </Button>
326
+ </div>
317
327
  )}
318
328
  </Popup>
319
329
  </>
@@ -12,8 +12,23 @@ import { Select } from 'antd';
12
12
  import { Button, CheckList, Popup, SearchBar } from 'antd-mobile';
13
13
  import React, { useEffect, useMemo, useState } from 'react';
14
14
 
15
+ const mobileSelectSafeAreaPaddingBottom = 'calc(12px + env(safe-area-inset-bottom, 0px))';
16
+
17
+ const mobileSelectConfirmFooterStyle: React.CSSProperties = {
18
+ paddingBottom: mobileSelectSafeAreaPaddingBottom,
19
+ };
20
+
21
+ function getMobileSelectListStyle(hasConfirmFooter: boolean): React.CSSProperties {
22
+ return {
23
+ maxHeight: '60vh',
24
+ overflowY: 'auto',
25
+ paddingBottom: hasConfirmFooter ? undefined : mobileSelectSafeAreaPaddingBottom,
26
+ };
27
+ }
28
+
15
29
  export function MobileSelect(props) {
16
30
  const { value, displayValue, onChange, onChangeComplete, disabled, options = [], mode } = props;
31
+ const isMultiple = ['multiple', 'tags'].includes(mode);
17
32
  const ctx = useFlowModelContext();
18
33
  const t = ctx.t;
19
34
  const [visible, setVisible] = useState(false);
@@ -64,17 +79,12 @@ export function MobileSelect(props) {
64
79
  <div style={{ margin: '10px' }}>
65
80
  <SearchBar placeholder={t('search')} value={searchText} onChange={(v) => setSearchText(v)} showCancelButton />
66
81
  </div>
67
- <div
68
- style={{
69
- maxHeight: '60vh',
70
- overflowY: 'auto',
71
- }}
72
- >
82
+ <div style={getMobileSelectListStyle(isMultiple)}>
73
83
  <CheckList
74
- multiple={['multiple', 'tags'].includes(mode)}
84
+ multiple={isMultiple}
75
85
  value={Array.isArray(selected) ? selected : [selected]}
76
86
  onChange={(val) => {
77
- if (['multiple', 'tags'].includes(mode)) {
87
+ if (isMultiple) {
78
88
  setSelected(val);
79
89
  } else {
80
90
  setSelected(val[0]);
@@ -91,10 +101,12 @@ export function MobileSelect(props) {
91
101
  ))}
92
102
  </CheckList>
93
103
  </div>
94
- {['multiple', 'tags'].includes(mode) && (
95
- <Button block color="primary" onClick={handleConfirm} style={{ marginTop: '16px' }}>
96
- {t('Confirm')}
97
- </Button>
104
+ {isMultiple && (
105
+ <div style={mobileSelectConfirmFooterStyle}>
106
+ <Button block color="primary" onClick={handleConfirm} style={{ marginTop: '16px' }}>
107
+ {t('Confirm')}
108
+ </Button>
109
+ </div>
98
110
  )}
99
111
  </Popup>
100
112
  </>
@@ -14,12 +14,11 @@ import { Button, Dropdown, theme, Tooltip, type ButtonProps, type MenuProps } fr
14
14
  import React, { useEffect, useMemo, useState } from 'react';
15
15
  import { useHotkeys } from 'react-hotkeys-hook';
16
16
  import { useTranslation } from 'react-i18next';
17
- import { Link, useLocation } from 'react-router-dom';
17
+ import { useLocation } from 'react-router-dom';
18
18
  import { useACLRoleContext } from '../../../acl';
19
- import type { BaseApplication } from '../../../BaseApplication';
20
19
  import type { PluginSettingsPageType } from '../../../PluginSettingsManager';
21
- import { shouldOpenAdminRouteInNewWindow } from '../../../RouterManager';
22
20
  import { useApp } from '../../../hooks/useApp';
21
+ import { resolveStandaloneSettingsPath } from '../../../settings-app/runtimePaths';
23
22
  import {
24
23
  filterRenderableSettings,
25
24
  filterVisibleSettings,
@@ -66,109 +65,6 @@ const topbarActionTriggerClassName = css`
66
65
  height: 100%;
67
66
  `;
68
67
 
69
- type TopbarSettingsAppLike = Pick<
70
- BaseApplication<any>,
71
- 'name' | 'router' | 'getPublicPath' | 'getHref' | 'layoutManager'
72
- >;
73
-
74
- const normalizeTopbarPath = (pathname?: string) => {
75
- const trimmed = pathname?.trim();
76
- if (!trimmed || trimmed === '/') {
77
- return '/';
78
- }
79
- return `/${trimmed.replace(/^\/+/, '')}`;
80
- };
81
-
82
- const normalizeTopbarBasePath = (pathname?: string) => {
83
- const normalized = normalizeTopbarPath(pathname).replace(/\/+$/, '');
84
- return normalized === '' || normalized === '/' ? '' : normalized;
85
- };
86
-
87
- const getTopbarRouterBasePath = (app: TopbarSettingsAppLike | undefined) => {
88
- return app?.router?.getBasename?.() || app?.router?.basename || app?.getPublicPath?.() || '';
89
- };
90
-
91
- const stripTopbarRouterBasePath = (pathname: string, basename?: string) => {
92
- const normalizedPath = normalizeTopbarPath(pathname);
93
- const normalizedBase = normalizeTopbarBasePath(basename);
94
-
95
- if (!normalizedBase) {
96
- return normalizedPath;
97
- }
98
-
99
- if (normalizedPath === normalizedBase) {
100
- return '/';
101
- }
102
-
103
- if (normalizedPath.startsWith(`${normalizedBase}/`)) {
104
- return normalizeTopbarPath(normalizedPath.slice(normalizedBase.length));
105
- }
106
-
107
- return normalizedPath;
108
- };
109
-
110
- const getTopbarAppPath = (pathname: string) => {
111
- const normalizedPath = normalizeTopbarPath(pathname);
112
- const match = /^(.*?\/(?:apps|_app)\/[^/]+)(?=\/|$)/.exec(normalizedPath);
113
- const appPath = match?.[1] || '';
114
-
115
- return {
116
- appPath,
117
- routePath: appPath ? normalizeTopbarPath(normalizedPath.slice(appPath.length)) : normalizedPath,
118
- };
119
- };
120
-
121
- const getTopbarContextAppPath = (app: TopbarSettingsAppLike | undefined, basename?: string) => {
122
- const basenameAppPath = getTopbarAppPath(basename || '').appPath;
123
- if (basenameAppPath) {
124
- return basenameAppPath;
125
- }
126
-
127
- const publicPathAppPath = getTopbarAppPath(app?.getPublicPath?.() || '').appPath;
128
- if (publicPathAppPath) {
129
- return publicPathAppPath;
130
- }
131
-
132
- if (app?.name && app.name !== 'main' && app.getHref) {
133
- return normalizeTopbarBasePath(app.getHref('/'));
134
- }
135
-
136
- return '';
137
- };
138
-
139
- const prependTopbarAppPath = (pathname: string, appPath: string) => {
140
- const normalizedPath = normalizeTopbarPath(pathname);
141
-
142
- if (!appPath || normalizedPath === appPath || normalizedPath.startsWith(`${appPath}/`)) {
143
- return normalizedPath;
144
- }
145
-
146
- return `${appPath}${normalizedPath}`;
147
- };
148
-
149
- const isAdminRuntimePath = (pathname: string) => {
150
- return pathname === '/admin' || pathname.startsWith('/admin/');
151
- };
152
-
153
- const getTopbarAdminRoutePath = (app: TopbarSettingsAppLike | undefined) => {
154
- try {
155
- return app?.layoutManager?.getLayout?.('admin')?.routePath;
156
- } catch {
157
- return undefined;
158
- }
159
- };
160
-
161
- const buildTopbarDocumentHref = (targetPath: string, basename?: string) => {
162
- const normalizedTarget = normalizeTopbarPath(targetPath);
163
- const normalizedBase = normalizeTopbarBasePath(basename);
164
-
165
- if (!normalizedBase || normalizedTarget === normalizedBase || normalizedTarget.startsWith(`${normalizedBase}/`)) {
166
- return normalizedTarget;
167
- }
168
-
169
- return `${normalizedBase}${normalizedTarget}`;
170
- };
171
-
172
68
  function TopbarExternalSettingsLabel(props: { title: React.ReactNode; link: string }) {
173
69
  return (
174
70
  <div
@@ -185,39 +81,13 @@ function TopbarInternalSettingsLabel(props: { title: React.ReactNode; path?: str
185
81
  const app = useApp();
186
82
  const location = useLocation();
187
83
  const targetPath = props.path || '/admin/settings';
188
- const basename = getTopbarRouterBasePath(app);
189
- const currentPath = stripTopbarRouterBasePath(location.pathname, basename);
190
- const currentLocationAppPath = getTopbarAppPath(currentPath);
191
- const currentAppPath = currentLocationAppPath.appPath || getTopbarContextAppPath(app, basename);
192
- const currentRoutePath = currentLocationAppPath.appPath ? currentLocationAppPath.routePath : currentPath;
193
- const targetPathInCurrentApp = prependTopbarAppPath(stripTopbarRouterBasePath(targetPath, basename), currentAppPath);
194
-
195
- if (currentAppPath) {
196
- const href = buildTopbarDocumentHref(targetPathInCurrentApp, basename);
197
- const shouldOpenInNewWindow = shouldOpenAdminRouteInNewWindow({
198
- currentPathname: currentPath,
199
- targetPathname: targetPathInCurrentApp,
200
- basePath: basename,
201
- adminRoutePath: getTopbarAdminRoutePath(app),
202
- });
203
-
204
- return (
205
- <a
206
- href={href}
207
- target={shouldOpenInNewWindow ? '_blank' : undefined}
208
- rel={shouldOpenInNewWindow ? 'noopener noreferrer' : undefined}
209
- >
210
- {props.title}
211
- </a>
212
- );
213
- }
214
-
215
- if (isAdminRuntimePath(currentRoutePath)) {
216
- return <Link to={targetPathInCurrentApp}>{props.title}</Link>;
217
- }
218
84
 
219
85
  return (
220
- <a href={buildTopbarDocumentHref(targetPath, basename)} target="_blank" rel="noopener noreferrer">
86
+ <a
87
+ href={resolveStandaloneSettingsPath(app, targetPath, location.pathname)}
88
+ target="_blank"
89
+ rel="noopener noreferrer"
90
+ >
221
91
  {props.title}
222
92
  </a>
223
93
  );
@@ -238,66 +108,68 @@ export function getTopbarPluginSettingsItems(options: {
238
108
  }): NonNullable<MenuProps['items']> {
239
109
  const { settings, canManagePlugins, t } = options;
240
110
  const topLevelSettings = filterVisibleSettings(filterRenderableSettings(settings));
241
- const pluginManagerSetting = topLevelSettings.find((item) => item.name === PLUGIN_MANAGER_SETTING_NAME);
111
+ const primarySettings = sortTopLevelSettings(
112
+ topLevelSettings
113
+ .filter(
114
+ (item) =>
115
+ ((item.sort || 0) < 0 || item.name === PLUGIN_MANAGER_SETTING_NAME) &&
116
+ (item.name !== PLUGIN_MANAGER_SETTING_NAME || canManagePlugins),
117
+ )
118
+ .map((item) => ({
119
+ ...item,
120
+ children: undefined,
121
+ })),
122
+ );
242
123
  const settingsByKey = new Map<string, PluginSettingsPageType>();
243
124
  const normalSettings = sortTopLevelSettings(
244
125
  topLevelSettings
245
- .filter((item) => item.name !== PLUGIN_MANAGER_SETTING_NAME)
126
+ .filter((item) => (item.sort || 0) >= 0 && item.name !== PLUGIN_MANAGER_SETTING_NAME)
246
127
  .map((item) => ({
247
128
  ...item,
248
129
  children: undefined,
249
130
  })),
250
131
  );
251
132
 
252
- normalSettings.forEach((item) => {
133
+ [...primarySettings, ...normalSettings].forEach((item) => {
253
134
  settingsByKey.set(item.key, item);
254
135
  });
255
136
 
256
- const orderedSettings = (getMenuItems(normalSettings) || []).map((item: any) => {
257
- if (item?.type === 'divider') {
258
- return item;
259
- }
260
-
261
- const matchedSetting = settingsByKey.get(String(item.key));
262
- const targetPath = matchedSetting?.path;
263
- const targetLink = matchedSetting?.link;
264
- const targetTitle = matchedSetting?.title || item.title;
265
-
266
- return {
267
- key: item.key,
268
- name: matchedSetting?.name,
269
- path: matchedSetting?.path,
270
- link: targetLink,
271
- title: targetTitle,
272
- icon: item.icon,
273
- label: targetLink ? (
274
- <TopbarExternalSettingsLabel title={targetTitle} link={targetLink} />
275
- ) : (
276
- <TopbarInternalSettingsLabel title={targetTitle} path={targetPath} />
277
- ),
278
- };
279
- });
280
-
281
- const items: NonNullable<MenuProps['items']> = [];
282
-
283
- if (canManagePlugins && pluginManagerSetting) {
284
- items.push({
285
- key: pluginManagerSetting.key,
286
- icon: pluginManagerSetting.icon || <ApiOutlined />,
287
- label: (
288
- <TopbarInternalSettingsLabel
289
- title={pluginManagerSetting.title || t('Plugin manager')}
290
- path={pluginManagerSetting.path}
291
- />
292
- ),
137
+ const buildMenuItems = (targetSettings: PluginSettingsPageType[]) =>
138
+ (getMenuItems(targetSettings) || []).map((item: any) => {
139
+ if (item?.type === 'divider') {
140
+ return item;
141
+ }
142
+
143
+ const matchedSetting = settingsByKey.get(String(item.key));
144
+ const targetPath = matchedSetting?.path;
145
+ const targetLink = matchedSetting?.link;
146
+ const isPluginManager = matchedSetting?.name === PLUGIN_MANAGER_SETTING_NAME;
147
+ const targetTitle = matchedSetting?.title || (isPluginManager ? t('Plugin manager') : item.title);
148
+
149
+ return {
150
+ key: item.key,
151
+ name: matchedSetting?.name,
152
+ path: matchedSetting?.path,
153
+ link: targetLink,
154
+ title: targetTitle,
155
+ icon: isPluginManager ? matchedSetting?.icon || <ApiOutlined /> : item.icon,
156
+ label: targetLink ? (
157
+ <TopbarExternalSettingsLabel title={targetTitle} link={targetLink} />
158
+ ) : (
159
+ <TopbarInternalSettingsLabel title={targetTitle} path={targetPath} />
160
+ ),
161
+ };
293
162
  });
294
- }
295
163
 
296
- if (canManagePlugins && orderedSettings.length) {
164
+ const primaryItems = buildMenuItems(primarySettings);
165
+ const normalItems = buildMenuItems(normalSettings);
166
+ const items: NonNullable<MenuProps['items']> = [...primaryItems];
167
+
168
+ if (primaryItems.length && normalItems.length) {
297
169
  items.push({ type: 'divider' });
298
170
  }
299
171
 
300
- items.push(...orderedSettings);
172
+ items.push(...normalItems);
301
173
 
302
174
  return items;
303
175
  }
@@ -105,9 +105,6 @@ function resolveItemDependencyPath(ctx: FlowContext, depPath: NamePath): DataSco
105
105
  ctx,
106
106
  parseFieldIndexEntries((ctx.model as any)?.context?.fieldIndex ?? (ctx as any)?.fieldIndex),
107
107
  );
108
- if (!entries.length) {
109
- return wildcardDeps();
110
- }
111
108
 
112
109
  let parentDepth = 0;
113
110
  let cursor = [...depPath];
@@ -121,6 +118,10 @@ function resolveItemDependencyPath(ctx: FlowContext, depPath: NamePath): DataSco
121
118
  return wildcardDeps();
122
119
  }
123
120
 
121
+ if (!entries.length) {
122
+ return parentDepth === 0 ? resolveRootItemDependencyPath(cursor) : emptyDeps();
123
+ }
124
+
124
125
  if (parentDepth === entries.length) {
125
126
  return resolveRootItemDependencyPath(cursor);
126
127
  }
@@ -231,7 +231,7 @@ export const FIELDS_VALIDATION_OPTIONS = {
231
231
  },
232
232
  {
233
233
  key: 'multiple',
234
- label: 'Multiple',
234
+ label: 'Multiple of',
235
235
  hasValue: true,
236
236
  params: [{ key: 'base', label: 'Base', componentType: 'inputNumber', required: true }],
237
237
  },
@@ -18,6 +18,7 @@ export enum NocoBaseDesktopRouteType {
18
18
 
19
19
  export interface NocoBaseDesktopRouteOptions {
20
20
  hasPersistedMenuInstanceFlow?: boolean;
21
+ hasPersistedPageTabFlowModel?: boolean;
21
22
  [key: string]: any;
22
23
  }
23
24
 
package/src/index.ts CHANGED
@@ -29,7 +29,16 @@ export * from './PluginSettingsManager';
29
29
  export * from './layout-manager';
30
30
  export * from './hooks';
31
31
  export { default as languageCodes } from './locale/languageCodes';
32
- export * from './nocobase-buildin-plugin';
32
+ export {
33
+ CurrentUserContext,
34
+ NocoBaseBuildInPlugin,
35
+ NocoBaseBuildInPluginV2,
36
+ useCurrentRoles,
37
+ useCurrentUserContext,
38
+ } from './nocobase-buildin-plugin';
39
+ export type { CurrentRoleOption, CurrentUserState } from './nocobase-buildin-plugin';
40
+ export { getRouteRuntimeVersion } from './utils/getRouteRuntimeVersion';
41
+ export type { RouteRuntimeVersion } from './utils/getRouteRuntimeVersion';
33
42
  export * from './collection-field-interface/CollectionFieldInterface';
34
43
  export * from './collection-field-interface/CollectionFieldInterfaceManager';
35
44
  export * from './collection-manager/field-configure';
@@ -40,6 +49,8 @@ export * from './collection-manager/template-fields';
40
49
  export * from './data-source';
41
50
  export * from './entry-actions';
42
51
  export * from './flow';
52
+ export { CodeEditorExtension } from './flow/components/code-editor/extension';
53
+ export type { CodeEditorExtra, CodeEditorExtraRegistry, EditorRef } from './flow/components/code-editor/types';
43
54
  export {
44
55
  DEFAULT_DATA_SOURCE_KEY,
45
56
  IconPicker,
@@ -50,3 +61,5 @@ export {
50
61
  export type { NocoBaseDesktopRoute } from './flow-compat';
51
62
  export * from './utils/markdownSanitize';
52
63
  export { default as AntdAppProvider } from './theme/AntdAppProvider';
64
+ export { isSettingsApp } from './settings-app/isSettingsApp';
65
+ export { MINIMAL_THEME_UID, useSettingsThemeConfig } from './settings-app/useSettingsThemeConfig';
@@ -54,6 +54,7 @@ export const languageCodes: Record<string, LocaleOptions> = {
54
54
  'ml-IN': { label: 'മലയാളം' },
55
55
  'mn-MN': { label: 'Монгол хэл' },
56
56
  'ms-MY': { label: 'بهاس ملايو' },
57
+ 'my-MM': { label: 'မြန်မာဘာသာ' },
57
58
  'nb-NO': { label: 'Norsk bokmål' },
58
59
  'ne-NP': { label: 'नेपाली' },
59
60
  'nl-BE': { label: 'Vlaams' },
@@ -73,7 +74,8 @@ export const languageCodes: Record<string, LocaleOptions> = {
73
74
  'tk-TK': { label: 'Turkmen' },
74
75
  'tr-TR': { label: 'Türkçe' },
75
76
  'uk-UA': { label: 'Українська' },
76
- 'ur-PK': { label: 'Oʻzbekcha' },
77
+ 'ur-PK': { label: 'اردو' },
78
+ 'uz-UZ': { label: 'Oʻzbekcha' },
77
79
  'vi-VN': { label: 'Tiếng Việt' },
78
80
  'zh-CN': { label: '简体中文' },
79
81
  'zh-HK': { label: '繁體中文(香港)' },
@@ -0,0 +1,58 @@
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 { useSyncExternalStore } from 'react';
11
+
12
+ export type CurrentUserAuthStatus = 'unknown' | 'authenticated' | 'unauthenticated' | 'redirecting';
13
+
14
+ type AuthStatusStore = {
15
+ getSnapshot: () => CurrentUserAuthStatus;
16
+ set: (status: CurrentUserAuthStatus) => void;
17
+ subscribe: (listener: () => void) => () => void;
18
+ };
19
+
20
+ const stores = new WeakMap<object, AuthStatusStore>();
21
+
22
+ function createAuthStatusStore(): AuthStatusStore {
23
+ let status: CurrentUserAuthStatus = 'unknown';
24
+ const listeners = new Set<() => void>();
25
+
26
+ return {
27
+ getSnapshot: () => status,
28
+ set: (nextStatus) => {
29
+ if (status === nextStatus) {
30
+ return;
31
+ }
32
+ status = nextStatus;
33
+ listeners.forEach((listener) => listener());
34
+ },
35
+ subscribe: (listener) => {
36
+ listeners.add(listener);
37
+ return () => listeners.delete(listener);
38
+ },
39
+ };
40
+ }
41
+
42
+ function getAuthStatusStore(app: object) {
43
+ let store = stores.get(app);
44
+ if (!store) {
45
+ store = createAuthStatusStore();
46
+ stores.set(app, store);
47
+ }
48
+ return store;
49
+ }
50
+
51
+ export function setCurrentUserAuthStatus(app: object, status: CurrentUserAuthStatus) {
52
+ getAuthStatusStore(app).set(status);
53
+ }
54
+
55
+ export function useCurrentUserAuthStatus(app: object) {
56
+ const store = getAuthStatusStore(app);
57
+ return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
58
+ }