@nocobase/client-v2 2.2.0-beta.9 → 2.2.0

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 (224) hide show
  1. package/es/APIClient.d.ts +13 -3
  2. package/es/BaseApplication.d.ts +1 -0
  3. package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
  4. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
  5. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  6. package/es/components/category-tabs/index.d.ts +9 -0
  7. package/es/components/form/ScanInput/useCodeScanner.d.ts +3 -2
  8. package/es/components/form/TypedVariableInput.d.ts +31 -5
  9. package/es/components/form/filter/CollectionFilter.d.ts +2 -0
  10. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  11. package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
  12. package/es/components/form/filter/index.d.ts +2 -0
  13. package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
  14. package/es/components/index.d.ts +1 -0
  15. package/es/flow/actions/index.d.ts +1 -1
  16. package/es/flow/actions/linkageRules.d.ts +27 -0
  17. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
  18. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  19. package/es/flow/components/FieldAssignValueInput.d.ts +7 -24
  20. package/es/flow/components/FlowRoute.d.ts +2 -2
  21. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  22. package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
  23. package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +31 -0
  24. package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
  25. package/es/flow/components/field-value-variable/index.d.ts +11 -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/components/placeholders/BlockPlaceholder.d.ts +1 -0
  29. package/es/flow/index.d.ts +1 -0
  30. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  31. package/es/flow/internal/utils/operatorSchemaHelper.d.ts +2 -2
  32. package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
  33. package/es/flow/models/base/ActionModelCore.d.ts +4 -2
  34. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  35. package/es/flow/models/base/CollectionBlockModel.d.ts +3 -0
  36. package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
  37. package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
  38. package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
  39. package/es/flow/models/blocks/details/DetailsBlockModel.d.ts +3 -0
  40. package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
  41. package/es/flow/models/blocks/filter-form/FilterFormSubmitActionModel.d.ts +3 -1
  42. package/es/flow/models/blocks/form/FormBlockModel.d.ts +7 -0
  43. package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
  44. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  45. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
  46. package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
  47. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  48. package/es/flow/models/blocks/table/TableColumnModel.d.ts +12 -0
  49. package/es/flow/models/blocks/table/utils.d.ts +1 -0
  50. package/es/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.d.ts +2 -1
  51. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +0 -17
  52. package/es/flow/models/fields/AssociationFieldModel/recordSelectShared.d.ts +41 -0
  53. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  54. package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
  55. package/es/flow-compat/passwordUtils.d.ts +1 -1
  56. package/es/flow-compat/routeTypes.d.ts +1 -0
  57. package/es/index.d.ts +5 -0
  58. package/es/index.mjs +199 -161
  59. package/es/ui-operation/index.d.ts +15 -0
  60. package/es/ui-operation/ui-operation-codec.d.ts +10 -0
  61. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  62. package/es/utils/index.d.ts +1 -0
  63. package/lib/index.js +215 -177
  64. package/lib/locale/languageCodes.js +3 -1
  65. package/package.json +9 -7
  66. package/src/APIClient.ts +90 -7
  67. package/src/Application.tsx +7 -1
  68. package/src/BaseApplication.tsx +11 -6
  69. package/src/__tests__/APIClient.test.tsx +58 -0
  70. package/src/__tests__/app.test.tsx +73 -0
  71. package/src/__tests__/browserChecker.test.ts +61 -0
  72. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  73. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +35 -2
  74. package/src/__tests__/settings-center.test.tsx +270 -6
  75. package/src/acl/ACLProvider.tsx +2 -0
  76. package/src/acl/__tests__/ACLProvider.test.tsx +92 -0
  77. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  78. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  79. package/src/collection-manager/field-validation.ts +1 -1
  80. package/src/components/README.md +7 -1
  81. package/src/components/README.zh-CN.md +6 -1
  82. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  83. package/src/components/category-tabs/index.ts +10 -0
  84. package/src/components/form/JsonTextArea.tsx +4 -0
  85. package/src/components/form/ScanInput/CodeScanner.tsx +79 -35
  86. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  87. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +284 -11
  88. package/src/components/form/ScanInput/useCodeScanner.ts +332 -24
  89. package/src/components/form/TypedVariableInput.tsx +452 -101
  90. package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
  91. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  92. package/src/components/form/filter/CollectionFilterItem.tsx +36 -9
  93. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  94. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +55 -0
  95. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  96. package/src/components/form/filter/index.ts +2 -0
  97. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  98. package/src/components/form/table/Table.tsx +81 -6
  99. package/src/components/form/table/__tests__/Table.columnWidth.test.tsx +433 -0
  100. package/src/components/index.ts +1 -0
  101. package/src/flow/FlowPage.tsx +9 -1
  102. package/src/flow/__tests__/FlowPage.test.tsx +50 -3
  103. package/src/flow/__tests__/FlowRoute.test.tsx +128 -10
  104. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  105. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  106. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  107. package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
  108. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +72 -0
  109. package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
  110. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
  111. package/src/flow/actions/index.ts +2 -0
  112. package/src/flow/actions/linkageRules.tsx +144 -26
  113. package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
  114. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +0 -1
  115. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +78 -59
  116. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
  117. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +228 -0
  118. package/src/flow/common/Markdown/style.ts +4 -0
  119. package/src/flow/components/BlockItemCard.tsx +2 -2
  120. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  121. package/src/flow/components/FieldAssignValueInput.tsx +72 -584
  122. package/src/flow/components/FlowRoute.tsx +78 -32
  123. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  124. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
  125. package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
  126. package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +326 -0
  127. package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +380 -0
  128. package/src/flow/components/field-value-variable/dateValue.ts +223 -0
  129. package/src/flow/components/field-value-variable/index.ts +12 -0
  130. package/src/flow/components/filter/FilterGroup.tsx +33 -5
  131. package/src/flow/components/filter/VariableFilterItem.tsx +170 -13
  132. package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
  133. package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
  134. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +195 -1
  135. package/src/flow/components/placeholders/BlockPlaceholder.tsx +70 -23
  136. package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +57 -7
  137. package/src/flow/index.ts +3 -0
  138. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  139. package/src/flow/internal/utils/operatorSchemaHelper.ts +3 -2
  140. package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
  141. package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
  142. package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
  143. package/src/flow/models/base/ActionModelCore.tsx +11 -2
  144. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  145. package/src/flow/models/base/CollectionBlockModel.tsx +38 -3
  146. package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
  147. package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
  148. package/src/flow/models/base/PageModel/PageTabModel.tsx +249 -18
  149. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
  150. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
  151. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
  152. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +1075 -7
  153. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  154. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  155. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  156. package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +125 -9
  157. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +28 -53
  158. package/src/flow/models/blocks/details/DetailsBlockModel.tsx +8 -3
  159. package/src/flow/models/blocks/details/__tests__/DetailsBlockModel.initialPaginationChangeRefresh.test.ts +32 -1
  160. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +28 -3
  161. package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +36 -5
  162. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +155 -25
  163. package/src/flow/models/blocks/filter-form/FilterFormSubmitActionModel.tsx +57 -4
  164. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.onModelCreated.test.ts +174 -1
  165. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +395 -1
  166. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +72 -0
  167. package/src/flow/models/blocks/filter-form/__tests__/FilterFormSubmitActionModel.lifecycle.test.ts +90 -0
  168. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +66 -4
  169. package/src/flow/models/blocks/filter-manager/FilterManager.ts +5 -0
  170. package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +40 -0
  171. package/src/flow/models/blocks/form/FormBlockModel.tsx +47 -0
  172. package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
  173. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  174. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +8 -2
  175. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  176. package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
  177. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
  178. package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
  179. package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
  180. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  181. package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
  182. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
  183. package/src/flow/models/blocks/table/TableBlockModel.tsx +29 -3
  184. package/src/flow/models/blocks/table/TableColumnModel.tsx +48 -9
  185. package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
  186. package/src/flow/models/blocks/table/__tests__/TableBlockModel.filterReset.test.ts +78 -0
  187. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  188. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
  189. package/src/flow/models/blocks/table/utils.ts +7 -0
  190. package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +33 -1
  191. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +10 -1
  192. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
  193. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
  194. package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +24 -1
  195. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +9 -136
  196. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +167 -3
  197. package/src/flow/models/fields/AssociationFieldModel/recordSelectShared.tsx +156 -0
  198. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  199. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  200. package/src/flow/models/fields/DisplayNumberFieldModel.tsx +1 -1
  201. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  202. package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
  203. package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
  204. package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
  205. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  206. package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
  207. package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
  208. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +26 -10
  209. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +51 -14
  210. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +139 -13
  211. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  212. package/src/flow-compat/fieldValidationConstants.ts +1 -1
  213. package/src/flow-compat/routeTypes.ts +1 -0
  214. package/src/index.ts +5 -0
  215. package/src/layout-manager/LayoutContentRoute.tsx +2 -1
  216. package/src/layout-manager/LayoutRoute.tsx +2 -1
  217. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
  218. package/src/locale/languageCodes.ts +3 -1
  219. package/src/nocobase-buildin-plugin/index.tsx +12 -0
  220. package/src/settings-center/AdminSettingsLayout.tsx +23 -2
  221. package/src/ui-operation/index.ts +33 -0
  222. package/src/ui-operation/ui-operation-codec.ts +54 -0
  223. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  224. package/src/utils/index.tsx +1 -0
@@ -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;
@@ -192,6 +206,11 @@ export function MobileLazySelect(props: Readonly<LazySelectProps>) {
192
206
  return;
193
207
  }
194
208
  const selectedId = vals[0];
209
+ if (selectedId === undefined) {
210
+ onChange(undefined as unknown as AssociationOption);
211
+ handleClose();
212
+ return;
213
+ }
195
214
  const record = optionMap.get(selectedId);
196
215
  if (record) {
197
216
  if (valueMode === 'value') {
@@ -277,16 +296,11 @@ export function MobileLazySelect(props: Readonly<LazySelectProps>) {
277
296
  value={searchText}
278
297
  onChange={handleSearchChange}
279
298
  onCancel={handleClose}
299
+ cancelText={t('Cancel')}
280
300
  showCancelButton
281
301
  />
282
302
  </div>
283
- <div
284
- style={{
285
- maxHeight: '60vh',
286
- overflowY: 'auto',
287
- }}
288
- onScroll={handleScroll}
289
- >
303
+ <div style={getMobileSelectListStyle(isMultiple)} onScroll={handleScroll}>
290
304
  <CheckList multiple={isMultiple} value={selectedValueIds} onChange={handleListChange}>
291
305
  {realOptions.map((item) => {
292
306
  const optionValue = item?.[valueKey];
@@ -311,9 +325,11 @@ export function MobileLazySelect(props: Readonly<LazySelectProps>) {
311
325
  )}
312
326
  </div>
313
327
  {isMultiple && (
314
- <Button block color="primary" onClick={handleConfirm} style={{ marginTop: '16px' }}>
315
- {t('Confirm')}
316
- </Button>
328
+ <div style={mobileSelectConfirmFooterStyle}>
329
+ <Button block color="primary" onClick={handleConfirm} style={{ marginTop: '16px' }}>
330
+ {t('Confirm')}
331
+ </Button>
332
+ </div>
317
333
  )}
318
334
  </Popup>
319
335
  </>
@@ -9,13 +9,45 @@
9
9
 
10
10
  import { useFlowModelContext } from '@nocobase/flow-engine';
11
11
  import { Select } from 'antd';
12
- import { Button, CheckList, Popup, SearchBar } from 'antd-mobile';
12
+ import { Button, CheckList, ConfigProvider, Popup, SearchBar, useConfig } 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
+
29
+ function normalizeMobileLocale(locale?: string) {
30
+ return locale === 'zh-CH' ? 'zh-CN' : locale;
31
+ }
32
+
15
33
  export function MobileSelect(props) {
16
34
  const { value, displayValue, onChange, onChangeComplete, disabled, options = [], mode } = props;
35
+ const isMultiple = ['multiple', 'tags'].includes(mode);
17
36
  const ctx = useFlowModelContext();
18
37
  const t = ctx.t;
38
+ const { locale: mobileLocale } = useConfig();
39
+ const searchBarLocale = useMemo(() => {
40
+ const currentLocale = normalizeMobileLocale(ctx.locale);
41
+ const inheritedLocale = normalizeMobileLocale(mobileLocale.locale);
42
+
43
+ return {
44
+ ...mobileLocale,
45
+ SearchBar: {
46
+ ...mobileLocale.SearchBar,
47
+ name: currentLocale === inheritedLocale ? mobileLocale.SearchBar.name : t('Search'),
48
+ },
49
+ };
50
+ }, [ctx.locale, mobileLocale, t]);
19
51
  const [visible, setVisible] = useState(false);
20
52
  const [selected, setSelected] = useState(value || []);
21
53
  const [searchText, setSearchText] = useState(null);
@@ -62,19 +94,22 @@ export function MobileSelect(props) {
62
94
  destroyOnClose
63
95
  >
64
96
  <div style={{ margin: '10px' }}>
65
- <SearchBar placeholder={t('search')} value={searchText} onChange={(v) => setSearchText(v)} showCancelButton />
97
+ <ConfigProvider locale={searchBarLocale}>
98
+ <SearchBar
99
+ placeholder={t('search')}
100
+ value={searchText}
101
+ onChange={(v) => setSearchText(v)}
102
+ cancelText={t('Cancel')}
103
+ showCancelButton
104
+ />
105
+ </ConfigProvider>
66
106
  </div>
67
- <div
68
- style={{
69
- maxHeight: '60vh',
70
- overflowY: 'auto',
71
- }}
72
- >
107
+ <div style={getMobileSelectListStyle(isMultiple)}>
73
108
  <CheckList
74
- multiple={['multiple', 'tags'].includes(mode)}
109
+ multiple={isMultiple}
75
110
  value={Array.isArray(selected) ? selected : [selected]}
76
111
  onChange={(val) => {
77
- if (['multiple', 'tags'].includes(mode)) {
112
+ if (isMultiple) {
78
113
  setSelected(val);
79
114
  } else {
80
115
  setSelected(val[0]);
@@ -91,10 +126,12 @@ export function MobileSelect(props) {
91
126
  ))}
92
127
  </CheckList>
93
128
  </div>
94
- {['multiple', 'tags'].includes(mode) && (
95
- <Button block color="primary" onClick={handleConfirm} style={{ marginTop: '16px' }}>
96
- {t('Confirm')}
97
- </Button>
129
+ {isMultiple && (
130
+ <div style={mobileSelectConfirmFooterStyle}>
131
+ <Button block color="primary" onClick={handleConfirm} style={{ marginTop: '16px' }}>
132
+ {t('Confirm')}
133
+ </Button>
134
+ </div>
98
135
  )}
99
136
  </Popup>
100
137
  </>
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import React from 'react';
10
+ import React, { type ComponentProps, type ReactNode } from 'react';
11
11
  import { beforeEach, describe, it, expect, vi } from 'vitest';
12
12
  import { act, fireEvent, render, screen } from '@nocobase/test/client';
13
13
  import { MobileLazySelect } from '../MobileLazySelect';
@@ -23,11 +23,29 @@ const RELATION_OPTIONS = [
23
23
  { uuid: 'c7d99828-a1de-9e70-4c2d-b0139abdf02e' },
24
24
  ];
25
25
 
26
+ type SelectProps = ComponentProps<(typeof import('antd'))['Select']>;
27
+ type PopupProps = ComponentProps<(typeof import('antd-mobile'))['Popup']>;
28
+ type CheckListProps = ComponentProps<(typeof import('antd-mobile'))['CheckList']>;
29
+ type ButtonProps = ComponentProps<(typeof import('antd-mobile'))['Button']>;
30
+ type SearchBarProps = ComponentProps<(typeof import('antd-mobile'))['SearchBar']>;
31
+ type ConfigProviderProps = ComponentProps<(typeof import('antd-mobile'))['ConfigProvider']>;
32
+ type MobileLocale = {
33
+ locale: string;
34
+ common: { cancel: string };
35
+ SearchBar: { name: string };
36
+ };
37
+
26
38
  const mockState = vi.hoisted(() => ({
27
- selectProps: undefined as any,
28
- popupProps: undefined as any,
29
- checklistProps: undefined as any,
30
- confirmButtonProps: undefined as any,
39
+ selectProps: undefined as SelectProps | undefined,
40
+ popupProps: undefined as PopupProps | undefined,
41
+ checklistProps: undefined as CheckListProps | undefined,
42
+ confirmButtonProps: undefined as ButtonProps | undefined,
43
+ mobileLocale: {
44
+ locale: 'zh-CH',
45
+ common: { cancel: '取消' },
46
+ SearchBar: { name: '搜索框' },
47
+ },
48
+ flowLocale: 'en-US',
31
49
  }));
32
50
 
33
51
  function resetMockState() {
@@ -35,6 +53,12 @@ function resetMockState() {
35
53
  mockState.popupProps = undefined;
36
54
  mockState.checklistProps = undefined;
37
55
  mockState.confirmButtonProps = undefined;
56
+ mockState.mobileLocale = {
57
+ locale: 'zh-CH',
58
+ common: { cancel: '取消' },
59
+ SearchBar: { name: '搜索框' },
60
+ };
61
+ mockState.flowLocale = 'en-US';
38
62
  }
39
63
 
40
64
  function clickTrigger() {
@@ -115,7 +139,20 @@ vi.mock('@nocobase/flow-engine', async () => {
115
139
  return {
116
140
  ...actual,
117
141
  useFlowModelContext: () => ({
118
- t: (value: string) => value,
142
+ locale: mockState.flowLocale,
143
+ t: (value: string) =>
144
+ ({
145
+ 'zh-CN': {
146
+ Cancel: '取消',
147
+ Search: '搜索',
148
+ search: '搜索',
149
+ },
150
+ 'zh-TW': {
151
+ Cancel: '取消',
152
+ Search: '搜尋',
153
+ search: '搜尋',
154
+ },
155
+ })[mockState.flowLocale]?.[value] || value,
119
156
  }),
120
157
  useFlowModel: () => ({
121
158
  context: {
@@ -130,7 +167,7 @@ vi.mock('antd', async () => {
130
167
  const actual = await vi.importActual<any>('antd');
131
168
  return {
132
169
  ...actual,
133
- Select: (props: any) => {
170
+ Select: (props: SelectProps) => {
134
171
  mockState.selectProps = props;
135
172
  return <div data-testid="antd-select" />;
136
173
  },
@@ -138,15 +175,22 @@ vi.mock('antd', async () => {
138
175
  });
139
176
 
140
177
  vi.mock('antd-mobile', () => {
141
- const MockCheckList: any = (props: any) => {
178
+ const MockCheckList = (props: CheckListProps) => {
142
179
  mockState.checklistProps = props;
143
180
  return <div data-testid="checklist">{props.children}</div>;
144
181
  };
145
182
 
146
- MockCheckList.Item = ({ value, children }: any) => <div data-testid={`item-${value}`}>{children}</div>;
183
+ MockCheckList.Item = ({ value, children }: { value: string | number; children?: ReactNode }) => (
184
+ <div data-testid={`item-${value}`}>{children}</div>
185
+ );
147
186
 
148
187
  return {
149
- Button: (props: any) => {
188
+ ConfigProvider: ({ children, locale }: ConfigProviderProps) => {
189
+ mockState.mobileLocale = locale as MobileLocale;
190
+ return <>{children}</>;
191
+ },
192
+ useConfig: () => ({ locale: mockState.mobileLocale }),
193
+ Button: (props: ButtonProps) => {
150
194
  mockState.confirmButtonProps = props;
151
195
  return (
152
196
  <button type="button" data-testid="confirm" onClick={props.onClick}>
@@ -154,12 +198,26 @@ vi.mock('antd-mobile', () => {
154
198
  </button>
155
199
  );
156
200
  },
157
- Popup: (props: any) => {
201
+ Popup: (props: PopupProps) => {
158
202
  mockState.popupProps = props;
159
203
  return props.visible ? <div data-testid="popup">{props.children}</div> : null;
160
204
  },
161
- SearchBar: ({ value, onChange }: any) => (
162
- <input data-testid="search" value={value ?? ''} onChange={(e) => onChange?.(e.target.value)} />
205
+ SearchBar: ({ value, onChange, onCancel, cancelText, placeholder, showCancelButton }: SearchBarProps) => (
206
+ <div>
207
+ <input
208
+ aria-label={mockState.mobileLocale.SearchBar.name}
209
+ data-testid="search"
210
+ placeholder={placeholder}
211
+ type="search"
212
+ value={value ?? ''}
213
+ onChange={(e) => onChange?.(e.target.value)}
214
+ />
215
+ {showCancelButton && value ? (
216
+ <button type="button" onClick={onCancel}>
217
+ {cancelText ?? mockState.mobileLocale.common.cancel}
218
+ </button>
219
+ ) : null}
220
+ </div>
163
221
  ),
164
222
  CheckList: MockCheckList,
165
223
  };
@@ -199,6 +257,47 @@ describe('MobileSelect', () => {
199
257
  expect(onChangeComplete).toHaveBeenCalledTimes(1);
200
258
  });
201
259
 
260
+ it('localizes the search input accessible name', () => {
261
+ renderMobileSelect();
262
+
263
+ openPopup();
264
+
265
+ expect(screen.getByRole('searchbox', { name: 'Search' })).toBeInTheDocument();
266
+ });
267
+
268
+ it('renders a translated cancel action after searching', () => {
269
+ renderMobileSelect();
270
+
271
+ openPopup();
272
+ act(() => {
273
+ fireEvent.change(screen.getByTestId('search'), { target: { value: 'Option' } });
274
+ });
275
+
276
+ expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
277
+ });
278
+
279
+ it('preserves the Chinese search input accessible name', () => {
280
+ mockState.flowLocale = 'zh-CN';
281
+ renderMobileSelect();
282
+
283
+ openPopup();
284
+ act(() => {
285
+ fireEvent.change(screen.getByTestId('search'), { target: { value: 'Option' } });
286
+ });
287
+
288
+ expect(screen.getByRole('searchbox', { name: '搜索框' })).toBeInTheDocument();
289
+ expect(screen.getByRole('button', { name: '取消' })).toBeInTheDocument();
290
+ });
291
+
292
+ it('does not reuse the simplified Chinese accessible name for traditional Chinese', () => {
293
+ mockState.flowLocale = 'zh-TW';
294
+ renderMobileSelect();
295
+
296
+ openPopup();
297
+
298
+ expect(screen.getByRole('searchbox', { name: '搜尋' })).toBeInTheDocument();
299
+ });
300
+
202
301
  it('defers commit until confirm in multiple mode', () => {
203
302
  const { onChange, onChangeComplete } = renderMobileSelect({ value: [], mode: 'multiple' });
204
303
  openPopup();
@@ -289,6 +388,33 @@ describe('MobileLazySelect', () => {
289
388
  resetMockState();
290
389
  });
291
390
 
391
+ it('renders a translated cancel action after searching', () => {
392
+ renderMobileLazySelect();
393
+
394
+ openLazyPopup();
395
+ act(() => {
396
+ fireEvent.change(screen.getByTestId('search'), { target: { value: '11' } });
397
+ });
398
+
399
+ expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
400
+ });
401
+
402
+ it('clears the selected relation record when it is tapped again in single mode', () => {
403
+ const { onChange } = renderMobileLazySelect({
404
+ value: RELATION_OPTIONS[0],
405
+ multiple: false,
406
+ });
407
+
408
+ openLazyPopup();
409
+ expect(mockState.checklistProps?.value).toEqual([RELATION_OPTIONS[0].uuid]);
410
+
411
+ selectValues([]);
412
+
413
+ expect(onChange).toHaveBeenCalledTimes(1);
414
+ expect(onChange).toHaveBeenCalledWith(undefined);
415
+ expect(screen.queryByTestId('popup')).not.toBeInTheDocument();
416
+ });
417
+
292
418
  it('keeps pending relation records selected until confirm', () => {
293
419
  const { onChange, rerender } = renderMobileLazySelect();
294
420
 
@@ -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
@@ -30,6 +30,8 @@ export * from './layout-manager';
30
30
  export * from './hooks';
31
31
  export { default as languageCodes } from './locale/languageCodes';
32
32
  export * from './nocobase-buildin-plugin';
33
+ export { getRouteRuntimeVersion } from './utils/getRouteRuntimeVersion';
34
+ export type { RouteRuntimeVersion } from './utils/getRouteRuntimeVersion';
33
35
  export * from './collection-field-interface/CollectionFieldInterface';
34
36
  export * from './collection-field-interface/CollectionFieldInterfaceManager';
35
37
  export * from './collection-manager/field-configure';
@@ -40,6 +42,8 @@ export * from './collection-manager/template-fields';
40
42
  export * from './data-source';
41
43
  export * from './entry-actions';
42
44
  export * from './flow';
45
+ export { CodeEditorExtension } from './flow/components/code-editor/extension';
46
+ export type { CodeEditorExtra, CodeEditorExtraRegistry, EditorRef } from './flow/components/code-editor/types';
43
47
  export {
44
48
  DEFAULT_DATA_SOURCE_KEY,
45
49
  IconPicker,
@@ -49,4 +53,5 @@ export {
49
53
  } from './flow-compat';
50
54
  export type { NocoBaseDesktopRoute } from './flow-compat';
51
55
  export * from './utils/markdownSanitize';
56
+ export * from './ui-operation';
52
57
  export { default as AntdAppProvider } from './theme/AntdAppProvider';
@@ -38,11 +38,12 @@ export const LayoutContentRoute = (props: LayoutContentRouteProps) => {
38
38
  id: lastMatch?.id,
39
39
  name: lastMatch?.id,
40
40
  pathname: location.pathname,
41
+ state: location.state,
41
42
  params: (lastMatch?.params || {}) as Record<string, string | undefined>,
42
43
  layoutRouteName: layout.routeName,
43
44
  layoutBasePathname: layoutMatch?.pathname,
44
45
  };
45
- }, [layout.routeName, location.pathname, matches]);
46
+ }, [layout.routeName, location.pathname, location.state, matches]);
46
47
  if (!model) {
47
48
  throw new Error(`[NocoBase] Layout '${layout.routeName}' model '${layout.uid}' is not mounted.`);
48
49
  }
@@ -71,11 +71,12 @@ export const LayoutRoute = (props: LayoutRouteProps) => {
71
71
  id: lastMatch?.id,
72
72
  name: lastMatch?.id,
73
73
  pathname: location.pathname,
74
+ state: location.state,
74
75
  params: lastMatchParams,
75
76
  layoutRouteName: layout.routeName,
76
77
  layoutBasePathname: layoutMatch?.pathname,
77
78
  };
78
- }, [lastMatch?.id, lastMatchParams, layout.routeName, layoutMatch?.pathname, location.pathname]);
79
+ }, [lastMatch?.id, lastMatchParams, layout.routeName, layoutMatch?.pathname, location.pathname, location.state]);
79
80
  const { loading, data, error } = useRequest(
80
81
  async () => {
81
82
  const existingModel = flowEngine.getModel<BaseLayoutModel>(layout.uid);
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { FlowEngine, FlowEngineProvider, observer } from '@nocobase/flow-engine';
11
- import { render, screen, waitFor } from '@testing-library/react';
11
+ import { act, 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
14
  import { describe, expect, it, vi } from 'vitest';
@@ -106,6 +106,64 @@ describe('LayoutRoute', () => {
106
106
  });
107
107
  });
108
108
 
109
+ it('syncs updated router state when navigating to the same layout pathname', async () => {
110
+ const syncLayoutRoute = vi.fn();
111
+
112
+ class StateTrackingLayoutModel extends TestLayoutModel {
113
+ syncLayoutRoute(routeLike: Parameters<BaseLayoutModel['syncLayoutRoute']>[0]) {
114
+ syncLayoutRoute(routeLike);
115
+ return super.syncLayoutRoute(routeLike);
116
+ }
117
+ }
118
+
119
+ const stateTrackingLayout: LayoutDefinition = {
120
+ ...layout,
121
+ layoutModelClass: 'StateTrackingLayoutModel',
122
+ };
123
+ const engine = new FlowEngine();
124
+ engine.registerModels({ StateTrackingLayoutModel });
125
+ engine.context.defineProperty('app', {
126
+ value: {
127
+ layoutManager: {
128
+ getLayout: () => stateTrackingLayout,
129
+ },
130
+ },
131
+ });
132
+
133
+ const router = createMemoryRouter(
134
+ [
135
+ {
136
+ id: layout.routeName,
137
+ path: layout.routePath,
138
+ element: <LayoutRoute layoutRouteName="test" />,
139
+ },
140
+ ],
141
+ {
142
+ initialEntries: ['/test'],
143
+ },
144
+ );
145
+
146
+ render(
147
+ <FlowEngineProvider engine={engine}>
148
+ <RouterProvider router={router} />
149
+ </FlowEngineProvider>,
150
+ );
151
+
152
+ expect(await screen.findByTestId('layout-route')).toHaveTextContent('test');
153
+ syncLayoutRoute.mockClear();
154
+
155
+ const routeState = {
156
+ __nocobaseOpenViewInputArgs: { popup: { formData: { status: 'todo' } } },
157
+ };
158
+ await act(async () => {
159
+ await router.navigate('/test', { state: routeState });
160
+ });
161
+
162
+ await waitFor(() => {
163
+ expect(syncLayoutRoute).toHaveBeenCalledWith(expect.objectContaining({ state: routeState }));
164
+ });
165
+ });
166
+
109
167
  it('does not activate desktop route loading for generic layouts', async () => {
110
168
  const activateLayout = vi.fn(() => vi.fn());
111
169
  const engine = new FlowEngine();
@@ -413,6 +471,34 @@ describe('LayoutContentRoute', () => {
413
471
  });
414
472
  });
415
473
 
474
+ it('syncs updated router state when navigating to the same content pathname', async () => {
475
+ const syncLayoutRoute = vi.fn();
476
+
477
+ class StateTrackingLayoutModel extends TestLayoutModel {
478
+ syncLayoutRoute(routeLike: Parameters<BaseLayoutModel['syncLayoutRoute']>[0]) {
479
+ syncLayoutRoute(routeLike);
480
+ return super.syncLayoutRoute(routeLike);
481
+ }
482
+ }
483
+
484
+ const { router } = setup('/test/page-1/view/popup', layout, StateTrackingLayoutModel);
485
+ await waitFor(() => {
486
+ expect(syncLayoutRoute).toHaveBeenCalled();
487
+ });
488
+ syncLayoutRoute.mockClear();
489
+
490
+ const routeState = {
491
+ __nocobaseOpenViewInputArgs: { popup: { formData: { status: 'todo' } } },
492
+ };
493
+ await act(async () => {
494
+ await router.navigate('/test/page-1/view/popup', { state: routeState });
495
+ });
496
+
497
+ await waitFor(() => {
498
+ expect(syncLayoutRoute).toHaveBeenCalledWith(expect.objectContaining({ state: routeState }));
499
+ });
500
+ });
501
+
416
502
  it('parses nested layout route by matched layout pathname', async () => {
417
503
  const nestedLayout: LayoutDefinition = {
418
504
  ...layout,
@@ -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: '繁體中文(香港)' },
@@ -240,6 +240,16 @@ const CurrentUserProvider: FC = ({ children }) => {
240
240
  return;
241
241
  }
242
242
 
243
+ try {
244
+ await app.apiClient.auth.syncCookies();
245
+ } catch {
246
+ // Cookie bootstrap is best-effort; auth:check remains the source of truth for the current page load.
247
+ }
248
+
249
+ if (!mounted) {
250
+ return;
251
+ }
252
+
243
253
  const userMeta = createCollectionContextMeta(
244
254
  () => app.flowEngine.context.dataSourceManager?.getDataSource('main')?.getCollection('users') || null,
245
255
  app.flowEngine.translate('Current user'),
@@ -298,6 +308,8 @@ const CurrentUserProvider: FC = ({ children }) => {
298
308
  return <CurrentUserContext.Provider value={contextValue}>{children}</CurrentUserContext.Provider>;
299
309
  };
300
310
 
311
+ CurrentUserProvider.displayName = 'CurrentUserProvider';
312
+
301
313
  const RootRedirect: FC = () => {
302
314
  const app = useApp<Application>();
303
315
  const hasToken = !!app?.apiClient?.auth?.token;
@@ -14,7 +14,7 @@ import { FlowModelRenderer, useFlowEngine } from '@nocobase/flow-engine';
14
14
  import { Layout, Menu, Result, theme } from 'antd';
15
15
  import React, { useEffect, useMemo, useRef } from 'react';
16
16
  import { useTranslation } from 'react-i18next';
17
- import { Navigate, Outlet, useLocation, useNavigate, useParams } from 'react-router-dom';
17
+ import { generatePath, Navigate, Outlet, useLocation, useNavigate, useParams } from 'react-router-dom';
18
18
  import { useACLRoleContext } from '../acl';
19
19
  import { ADMIN_SETTINGS_PATH, type PluginSettingsPageType } from '../PluginSettingsManager';
20
20
  import { useApp } from '../hooks/useApp';
@@ -68,6 +68,9 @@ export const InternalAdminSettingsLayout = () => {
68
68
  const navigate = useNavigate();
69
69
  const location = useLocation();
70
70
  const params = useParams();
71
+ const routeParams = Object.fromEntries(
72
+ Object.entries(params).filter((entry): entry is [string, string] => typeof entry[1] === 'string'),
73
+ );
71
74
  const { token } = theme.useToken();
72
75
  const { t } = useTranslation();
73
76
  const { snippets = [] } = useACLRoleContext();
@@ -174,7 +177,25 @@ export const InternalAdminSettingsLayout = () => {
174
177
  return <SettingsEmpty type="route" />;
175
178
  }
176
179
 
177
- if (!currentVisibleSetting && currentSetting.isAllow === false) {
180
+ if (currentSetting.isAllow === false) {
181
+ const firstVisibleTabPath = currentVisibleTabs
182
+ .map((tab) => getDefaultSettingsPath([tab]))
183
+ .filter((path): path is string => typeof path === 'string')
184
+ .map((path) => {
185
+ try {
186
+ return generatePath(path, routeParams);
187
+ } catch {
188
+ return null;
189
+ }
190
+ })
191
+ .find((path): path is string => typeof path === 'string');
192
+ if (firstVisibleTabPath) {
193
+ return <Navigate replace to={firstVisibleTabPath} />;
194
+ }
195
+ if (!defaultSettingsPath) {
196
+ return <Navigate replace to="/admin" />;
197
+ }
198
+
178
199
  return <SettingsEmpty type="route" />;
179
200
  }
180
201