@nocobase/client-v2 2.1.0-beta.37 → 2.1.0-beta.40
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.
- package/es/Application.d.ts +7 -0
- package/es/BaseApplication.d.ts +3 -0
- package/es/RouterManager.d.ts +1 -0
- package/es/collection-field-interface/CollectionFieldInterface.d.ts +51 -15
- package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +82 -3
- package/es/collection-manager/field-configure.d.ts +80 -0
- package/es/collection-manager/field-validation.d.ts +43 -0
- package/es/collection-manager/filter-operators/index.d.ts +46 -0
- package/es/collection-manager/filter-operators/operators.d.ts +30 -0
- package/es/collection-manager/interfaces/checkbox.d.ts +1 -41
- package/es/collection-manager/interfaces/checkboxGroup.d.ts +12 -44
- package/es/collection-manager/interfaces/collection.d.ts +12 -51
- package/es/collection-manager/interfaces/color.d.ts +1 -16
- package/es/collection-manager/interfaces/createdAt.d.ts +1 -44
- package/es/collection-manager/interfaces/createdBy.d.ts +0 -4
- package/es/collection-manager/interfaces/dateOnly.d.ts +7 -44
- package/es/collection-manager/interfaces/datetime.d.ts +1 -44
- package/es/collection-manager/interfaces/datetimeNoTz.d.ts +1 -44
- package/es/collection-manager/interfaces/email.d.ts +1 -29
- package/es/collection-manager/interfaces/id.d.ts +1 -16
- package/es/collection-manager/interfaces/index.d.ts +2 -3
- package/es/collection-manager/interfaces/input.d.ts +1 -102
- package/es/collection-manager/interfaces/integer.d.ts +1 -95
- package/es/collection-manager/interfaces/json.d.ts +16 -7
- package/es/collection-manager/interfaces/m2m.d.ts +11 -19
- package/es/collection-manager/interfaces/m2o.d.ts +11 -19
- package/es/collection-manager/interfaces/markdown.d.ts +1 -63
- package/es/collection-manager/interfaces/multipleSelect.d.ts +12 -44
- package/es/collection-manager/interfaces/nanoid.d.ts +1 -34
- package/es/collection-manager/interfaces/number.d.ts +1 -87
- package/es/collection-manager/interfaces/o2m.d.ts +12 -24
- package/es/collection-manager/interfaces/obo.d.ts +207 -0
- package/es/collection-manager/interfaces/oho.d.ts +207 -0
- package/es/collection-manager/interfaces/password.d.ts +1 -56
- package/es/collection-manager/interfaces/percent.d.ts +1 -84
- package/es/collection-manager/interfaces/phone.d.ts +1 -25
- package/es/collection-manager/interfaces/properties/index.d.ts +0 -28
- package/es/collection-manager/interfaces/radioGroup.d.ts +1 -29
- package/es/collection-manager/interfaces/richText.d.ts +1 -63
- package/es/collection-manager/interfaces/select.d.ts +12 -44
- package/es/collection-manager/interfaces/snowflake-id.d.ts +1 -34
- package/es/collection-manager/interfaces/tableoid.d.ts +1 -10
- package/es/collection-manager/interfaces/textarea.d.ts +1 -51
- package/es/collection-manager/interfaces/time.d.ts +1 -16
- package/es/collection-manager/interfaces/types.d.ts +3 -12
- package/es/collection-manager/interfaces/unixTimestamp.d.ts +1 -44
- package/es/collection-manager/interfaces/updatedAt.d.ts +1 -44
- package/es/collection-manager/interfaces/updatedBy.d.ts +0 -4
- package/es/collection-manager/interfaces/url.d.ts +1 -20
- package/es/collection-manager/interfaces/uuid.d.ts +1 -34
- package/es/collection-manager/template-fields.d.ts +53 -0
- package/es/components/KeepAlive.d.ts +22 -0
- package/es/components/RouterBridge.d.ts +9 -0
- package/es/components/form/VariableInput.d.ts +53 -2
- package/es/components/form/filter/CollectionFilter.d.ts +9 -1
- package/es/components/form/filter/CollectionFilterItem.d.ts +10 -2
- package/es/data-source/ExtendCollectionsProvider.d.ts +28 -2
- package/es/flow/FlowPage.d.ts +2 -1
- package/es/flow/admin-shell/AdminLayoutRouteCoordinator.d.ts +8 -40
- package/es/flow/admin-shell/BaseLayoutModel.d.ts +89 -0
- package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +74 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +12 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutModel.d.ts +7 -92
- package/es/flow/admin-shell/admin-layout/index.d.ts +2 -0
- package/es/flow/admin-shell/useAdminLayoutRoutePage.d.ts +2 -2
- package/es/flow/admin-shell/useLayoutRoutePage.d.ts +23 -0
- package/es/flow/components/FlowRoute.d.ts +10 -1
- package/es/flow/components/filter/useFilterOptions.d.ts +13 -1
- package/es/flow/index.d.ts +4 -0
- package/es/flow/models/base/PageModel/PageModel.d.ts +3 -1
- package/es/flow/models/blocks/assign-form/assignFieldValuesFlow.d.ts +84 -0
- package/es/flow/models/blocks/assign-form/index.d.ts +1 -0
- package/es/flow/models/blocks/form/FormActionGroupModel.d.ts +1 -0
- package/es/flow/models/blocks/form/FormActionModel.d.ts +9 -2
- package/es/flow/models/blocks/table/TableBlockModel.d.ts +10 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.d.ts +1 -1
- package/es/index.d.ts +5 -0
- package/es/index.mjs +470 -460
- package/es/layout-manager/LayoutContentRoute.d.ts +14 -0
- package/es/layout-manager/LayoutManager.d.ts +22 -0
- package/es/layout-manager/LayoutRoute.d.ts +14 -0
- package/es/layout-manager/index.d.ts +13 -0
- package/es/layout-manager/types.d.ts +20 -0
- package/es/layout-manager/utils.d.ts +14 -0
- package/es/settings-center/index.d.ts +1 -1
- package/es/settings-center/plugin-manager/BulkEnableButton.d.ts +15 -0
- package/es/settings-center/plugin-manager/PluginCard.d.ts +15 -0
- package/es/settings-center/plugin-manager/PluginDetail.d.ts +16 -0
- package/es/settings-center/{PluginManagerPage.d.ts → plugin-manager/index.d.ts} +1 -7
- package/es/settings-center/plugin-manager/types.d.ts +34 -0
- package/lib/index.js +470 -460
- package/package.json +8 -7
- package/src/Application.tsx +51 -12
- package/src/BaseApplication.tsx +6 -0
- package/src/PluginSettingsManager.ts +1 -1
- package/src/RouterManager.tsx +17 -1
- package/src/__tests__/PluginSettingsManager.test.ts +41 -2
- package/src/__tests__/app.test.tsx +8 -1
- package/src/__tests__/globalDeps.test.ts +1 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +45 -2
- package/src/__tests__/plugin-manager.test.tsx +177 -0
- package/src/__tests__/settings-center.test.tsx +24 -2
- package/src/collection-field-interface/CollectionFieldInterface.ts +71 -77
- package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +201 -4
- package/src/collection-manager/field-configure.ts +548 -0
- package/src/collection-manager/field-validation.ts +195 -0
- package/src/collection-manager/filter-operators/index.ts +176 -0
- package/src/collection-manager/{interfaces/properties → filter-operators}/operators.ts +24 -13
- package/src/collection-manager/interfaces/checkbox.ts +2 -9
- package/src/collection-manager/interfaces/checkboxGroup.ts +2 -10
- package/src/collection-manager/interfaces/collection.ts +2 -15
- package/src/collection-manager/interfaces/color.ts +2 -2
- package/src/collection-manager/interfaces/createdAt.ts +2 -2
- package/src/collection-manager/interfaces/createdBy.ts +1 -12
- package/src/collection-manager/interfaces/dateOnly.ts +8 -2
- package/src/collection-manager/interfaces/datetime.ts +2 -2
- package/src/collection-manager/interfaces/datetimeNoTz.ts +2 -2
- package/src/collection-manager/interfaces/email.ts +2 -9
- package/src/collection-manager/interfaces/id.ts +1 -2
- package/src/collection-manager/interfaces/index.ts +2 -3
- package/src/collection-manager/interfaces/input.ts +2 -133
- package/src/collection-manager/interfaces/integer.ts +2 -71
- package/src/collection-manager/interfaces/json.tsx +17 -11
- package/src/collection-manager/interfaces/m2m.tsx +0 -21
- package/src/collection-manager/interfaces/m2o.tsx +0 -22
- package/src/collection-manager/interfaces/markdown.ts +2 -51
- package/src/collection-manager/interfaces/multipleSelect.ts +2 -14
- package/src/collection-manager/interfaces/nanoid.ts +2 -2
- package/src/collection-manager/interfaces/number.ts +2 -85
- package/src/collection-manager/interfaces/o2m.tsx +1 -22
- package/src/collection-manager/interfaces/obo.tsx +145 -0
- package/src/collection-manager/interfaces/oho.tsx +145 -0
- package/src/collection-manager/interfaces/password.ts +2 -44
- package/src/collection-manager/interfaces/percent.ts +2 -74
- package/src/collection-manager/interfaces/phone.ts +2 -2
- package/src/collection-manager/interfaces/properties/index.ts +0 -133
- package/src/collection-manager/interfaces/radioGroup.ts +2 -2
- package/src/collection-manager/interfaces/richText.ts +2 -51
- package/src/collection-manager/interfaces/select.ts +2 -14
- package/src/collection-manager/interfaces/snowflake-id.ts +2 -2
- package/src/collection-manager/interfaces/tableoid.ts +1 -2
- package/src/collection-manager/interfaces/textarea.ts +2 -51
- package/src/collection-manager/interfaces/time.ts +2 -2
- package/src/collection-manager/interfaces/types.ts +4 -12
- package/src/collection-manager/interfaces/unixTimestamp.tsx +2 -2
- package/src/collection-manager/interfaces/updatedAt.ts +2 -2
- package/src/collection-manager/interfaces/updatedBy.ts +1 -12
- package/src/collection-manager/interfaces/url.ts +2 -4
- package/src/collection-manager/interfaces/uuid.ts +2 -2
- package/src/collection-manager/template-fields.ts +109 -0
- package/src/components/KeepAlive.tsx +131 -0
- package/src/components/README.md +1 -0
- package/src/components/README.zh-CN.md +1 -0
- package/src/components/RouterBridge.tsx +28 -4
- package/src/components/__tests__/KeepAlive.test.tsx +63 -0
- package/src/components/__tests__/RouterBridge.test.tsx +27 -0
- package/src/components/form/VariableInput.tsx +101 -28
- package/src/components/form/__tests__/VariableInput.test.ts +85 -0
- package/src/components/form/filter/CollectionFilter.tsx +11 -1
- package/src/components/form/filter/CollectionFilterItem.tsx +12 -4
- package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +42 -0
- package/src/components/form/filter/useFilterActionProps.ts +7 -4
- package/src/data-source/ExtendCollectionsProvider.tsx +94 -20
- package/src/data-source/__tests__/ExtendCollectionsProvider.test.tsx +264 -0
- package/src/flow/FlowPage.tsx +35 -7
- package/src/flow/__tests__/FlowPage.test.tsx +79 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +529 -2
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +191 -0
- package/src/flow/actions/__tests__/openView.subModelKey.test.tsx +33 -0
- package/src/flow/actions/aclCheck.tsx +4 -0
- package/src/flow/actions/aclCheckRefresh.tsx +4 -0
- package/src/flow/actions/dateTimeFormat.tsx +12 -8
- package/src/flow/actions/linkageRules.tsx +122 -0
- package/src/flow/actions/openView.tsx +28 -4
- package/src/flow/admin-shell/AdminLayoutRouteCoordinator.ts +11 -329
- package/src/flow/admin-shell/BaseLayoutModel.tsx +455 -0
- package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +502 -0
- package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +547 -3
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +4 -4
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +160 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +0 -12
- package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +28 -201
- package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +11 -2
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +1 -26
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +149 -27
- package/src/flow/admin-shell/admin-layout/index.ts +2 -0
- package/src/flow/admin-shell/useAdminLayoutRoutePage.ts +10 -26
- package/src/flow/admin-shell/useLayoutRoutePage.ts +61 -0
- package/src/flow/components/AdminLayout.tsx +4 -154
- package/src/flow/components/FlowRoute.tsx +105 -15
- package/src/flow/components/filter/useFilterOptions.ts +27 -5
- package/src/flow/index.ts +4 -0
- package/src/flow/models/actions/UpdateRecordActionModel.tsx +14 -95
- package/src/flow/models/actions/UpdateRecordActionUtils.ts +4 -7
- package/src/flow/models/actions/__tests__/AssignFormRefill.test.ts +26 -1
- package/src/flow/models/base/ActionModel.tsx +8 -1
- package/src/flow/models/base/PageModel/PageModel.tsx +51 -18
- package/src/flow/models/base/PageModel/RootPageModel.tsx +6 -13
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +102 -1
- package/src/flow/models/base/RouteModel.tsx +1 -1
- package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +63 -2
- package/src/flow/models/blocks/assign-form/assignFieldValuesFlow.tsx +206 -0
- package/src/flow/models/blocks/assign-form/index.ts +1 -0
- package/src/flow/models/blocks/form/FormActionGroupModel.tsx +14 -0
- package/src/flow/models/blocks/form/FormActionModel.tsx +30 -3
- package/src/flow/models/blocks/form/FormItemModel.tsx +8 -1
- package/src/flow/models/blocks/form/__tests__/FormActionGroupModel.test.ts +46 -0
- package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +71 -0
- package/src/flow/models/blocks/form/submitHandler.ts +8 -1
- package/src/flow/models/blocks/form/submitValues.ts +4 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +118 -16
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +114 -0
- package/src/flow/models/fields/AssociationFieldModel/SubFormFieldModel.tsx +7 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +1 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -5
- package/src/flow/models/fields/ClickableFieldModel.tsx +9 -1
- package/src/flow/models/fields/CollectionSelectorFieldModel.tsx +8 -2
- package/src/flow/models/fields/DisplayEnumFieldModel.tsx +8 -2
- package/src/flow/models/fields/DisplayTimeFieldModel.tsx +1 -1
- package/src/flow/models/fields/TimeFieldModel.tsx +1 -1
- package/src/flow/models/fields/__tests__/TimeFieldModel.test.tsx +61 -0
- package/src/flow/models/fields/mobile-components/MobileDatePicker.tsx +19 -3
- package/src/flow/models/fields/mobile-components/__tests__/MobileDatePicker.test.tsx +94 -0
- package/src/flow/models/topbar/TopbarActionModel.tsx +1 -1
- package/src/flow/utils/__tests__/dateTimeFormat.test.ts +91 -0
- package/src/index.ts +5 -0
- package/src/layout-manager/LayoutContentRoute.tsx +90 -0
- package/src/layout-manager/LayoutManager.tsx +185 -0
- package/src/layout-manager/LayoutRoute.tsx +138 -0
- package/src/layout-manager/__tests__/LayoutManager.test.tsx +335 -0
- package/src/layout-manager/__tests__/LayoutRoute.test.tsx +473 -0
- package/src/layout-manager/index.ts +14 -0
- package/src/layout-manager/types.ts +22 -0
- package/src/layout-manager/utils.ts +37 -0
- package/src/nocobase-buildin-plugin/index.tsx +56 -48
- package/src/settings-center/index.ts +1 -1
- package/src/settings-center/plugin-manager/BulkEnableButton.tsx +111 -0
- package/src/settings-center/plugin-manager/PluginCard.tsx +270 -0
- package/src/settings-center/plugin-manager/PluginDetail.tsx +195 -0
- package/src/settings-center/plugin-manager/index.tsx +254 -0
- package/src/settings-center/plugin-manager/types.ts +35 -0
- package/src/settings-center/utils.tsx +8 -1
- package/src/theme/__tests__/globalStyles.test.ts +24 -0
- package/src/theme/globalStyles.ts +10 -0
- package/src/utils/globalDeps.ts +2 -0
- package/es/collection-manager/interfaces/linkTo.d.ts +0 -90
- package/es/collection-manager/interfaces/o2o.d.ts +0 -621
- package/es/collection-manager/interfaces/properties/operators.d.ts +0 -294
- package/es/collection-manager/interfaces/subTable.d.ts +0 -172
- package/src/collection-manager/interfaces/linkTo.ts +0 -120
- package/src/collection-manager/interfaces/o2o.tsx +0 -561
- package/src/collection-manager/interfaces/subTable.ts +0 -218
- package/src/settings-center/PluginManagerPage.tsx +0 -162
|
@@ -0,0 +1,131 @@
|
|
|
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 { RouteContext } from '@ant-design/pro-layout';
|
|
11
|
+
import _ from 'lodash';
|
|
12
|
+
import React, { createContext, FC, memo, useContext, useRef } from 'react';
|
|
13
|
+
import {
|
|
14
|
+
UNSAFE_DataRouterContext,
|
|
15
|
+
UNSAFE_DataRouterStateContext,
|
|
16
|
+
UNSAFE_LocationContext,
|
|
17
|
+
UNSAFE_RouteContext,
|
|
18
|
+
} from 'react-router-dom';
|
|
19
|
+
|
|
20
|
+
const KeepAliveContext = createContext(true);
|
|
21
|
+
const hidden = { display: 'none' };
|
|
22
|
+
|
|
23
|
+
export const KeepAliveProvider: FC<{ active: boolean; parentActive: boolean }> = memo(
|
|
24
|
+
({ children, active, parentActive }) => {
|
|
25
|
+
const currentLocationContext = useContext(UNSAFE_LocationContext);
|
|
26
|
+
const currentRouteContext = useContext(UNSAFE_RouteContext);
|
|
27
|
+
const currentDataRouterContext = useContext(UNSAFE_DataRouterContext);
|
|
28
|
+
const currentDataRouterStateContext = useContext(UNSAFE_DataRouterStateContext);
|
|
29
|
+
const routeContextValue = useContext(RouteContext);
|
|
30
|
+
|
|
31
|
+
const prevLocationContextRef = useRef(currentLocationContext);
|
|
32
|
+
const prevRouteContextRef = useRef(currentRouteContext);
|
|
33
|
+
const prevDataRouterContextRef = useRef(currentDataRouterContext);
|
|
34
|
+
const prevDataRouterStateContextRef = useRef(currentDataRouterStateContext);
|
|
35
|
+
const prevRouteContextValueRef = useRef(routeContextValue);
|
|
36
|
+
|
|
37
|
+
if (active) {
|
|
38
|
+
prevDataRouterContextRef.current = currentDataRouterContext;
|
|
39
|
+
prevDataRouterStateContextRef.current = currentDataRouterStateContext;
|
|
40
|
+
prevRouteContextValueRef.current = routeContextValue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (
|
|
44
|
+
active &&
|
|
45
|
+
!_.isEqual(_.omit(prevLocationContextRef.current.location, 'key'), _.omit(currentLocationContext.location, 'key'))
|
|
46
|
+
) {
|
|
47
|
+
prevLocationContextRef.current = currentLocationContext;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (active && !_.isEqual(prevRouteContextRef.current, currentRouteContext)) {
|
|
51
|
+
prevRouteContextRef.current = currentRouteContext;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div style={active ? { height: '100%' } : hidden}>
|
|
56
|
+
<RouteContext.Provider value={prevRouteContextValueRef.current}>
|
|
57
|
+
<UNSAFE_DataRouterContext.Provider value={prevDataRouterContextRef.current}>
|
|
58
|
+
<UNSAFE_DataRouterStateContext.Provider value={prevDataRouterStateContextRef.current}>
|
|
59
|
+
<UNSAFE_LocationContext.Provider value={prevLocationContextRef.current}>
|
|
60
|
+
<UNSAFE_RouteContext.Provider value={prevRouteContextRef.current}>
|
|
61
|
+
<KeepAliveContext.Provider value={parentActive === false ? false : active}>
|
|
62
|
+
{children}
|
|
63
|
+
</KeepAliveContext.Provider>
|
|
64
|
+
</UNSAFE_RouteContext.Provider>
|
|
65
|
+
</UNSAFE_LocationContext.Provider>
|
|
66
|
+
</UNSAFE_DataRouterStateContext.Provider>
|
|
67
|
+
</UNSAFE_DataRouterContext.Provider>
|
|
68
|
+
</RouteContext.Provider>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
export const useKeepAlive = () => {
|
|
75
|
+
const active = useContext(KeepAliveContext);
|
|
76
|
+
return { active };
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
interface KeepAliveProps {
|
|
80
|
+
uid: string;
|
|
81
|
+
children: (uid: string) => React.ReactNode;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const MINIMUM_CACHED_PAGES = 5;
|
|
85
|
+
const MAXIMUM_CACHED_PAGES = 15;
|
|
86
|
+
|
|
87
|
+
const getMaxPageCount = () => {
|
|
88
|
+
const baseCount = MINIMUM_CACHED_PAGES;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const memory = (navigator as any).deviceMemory;
|
|
92
|
+
if (memory) {
|
|
93
|
+
return Math.min(Math.max(baseCount, memory * 3), MAXIMUM_CACHED_PAGES);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const cores = navigator.hardwareConcurrency;
|
|
97
|
+
if (cores) {
|
|
98
|
+
return cores >= 8 ? MAXIMUM_CACHED_PAGES : cores >= 4 ? 7 : baseCount;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return baseCount;
|
|
102
|
+
} catch (e) {
|
|
103
|
+
return baseCount;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const MAX_RENDERED_PAGE_COUNT = getMaxPageCount();
|
|
108
|
+
|
|
109
|
+
export const KeepAlive: FC<KeepAliveProps> = React.memo(({ children, uid }) => {
|
|
110
|
+
const { active } = useKeepAlive();
|
|
111
|
+
const renderedUidListRef = useRef<string[]>([]);
|
|
112
|
+
|
|
113
|
+
if (!renderedUidListRef.current.includes(uid)) {
|
|
114
|
+
renderedUidListRef.current.push(uid);
|
|
115
|
+
if (renderedUidListRef.current.length > MAX_RENDERED_PAGE_COUNT) {
|
|
116
|
+
renderedUidListRef.current = renderedUidListRef.current.slice(-MAX_RENDERED_PAGE_COUNT);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<>
|
|
122
|
+
{renderedUidListRef.current.map((renderedUid) => (
|
|
123
|
+
<KeepAliveProvider active={renderedUid === uid} key={renderedUid} parentActive={active}>
|
|
124
|
+
{children(renderedUid)}
|
|
125
|
+
</KeepAliveProvider>
|
|
126
|
+
))}
|
|
127
|
+
</>
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
KeepAlive.displayName = 'KeepAlive';
|
package/src/components/README.md
CHANGED
|
@@ -164,6 +164,7 @@ Key props:
|
|
|
164
164
|
- `namespaces`: restrict the picker to specific top-level namespaces. Omit to expose every registered top-level property
|
|
165
165
|
- `extraNodes`: static leaves appended after the namespace-filtered nodes. Use for variables that only make sense in the current page (e.g. `$resetLink`)
|
|
166
166
|
- `converters`: override the default path ↔ string converters. `EnvVariableInput` uses this hook to lock its output to `$env`
|
|
167
|
+
- `delimiters`: token pair wrapping the stored variable reference. Defaults to `['{{', '}}']` (Handlebars HTML-escaped). Pass `['{{{', '}}}']` for fields rendered as HTML where escaping would corrupt the variable value — e.g. the in-app message body
|
|
167
168
|
- `value` / `onChange` / `placeholder` / `disabled`: standard controlled-input props
|
|
168
169
|
|
|
169
170
|
Under the hood `VariableInput` wraps `VariableHybridInput` (inline pills), `VariableTextArea` wraps `TextAreaWithContextSelector` (textarea + variable button). Both share the same MetaTree.
|
|
@@ -164,6 +164,7 @@ import { VariableInput, VariableTextArea } from '@nocobase/client-v2';
|
|
|
164
164
|
- `namespaces`:限定可选的顶层命名空间。不传就用 `flowEngine.context` 里全部已注册的
|
|
165
165
|
- `extraNodes`:在命名空间过滤后追加几条静态变量(用于 `$resetLink` 这类只在当前页面有意义的局部变量)
|
|
166
166
|
- `converters`:覆盖默认的 path ↔ string 转换器。`EnvVariableInput` 就是用这个钩子把输出锁定到 `$env`
|
|
167
|
+
- `delimiters`:变量在存储字符串里使用的开闭分隔符,默认 `['{{', '}}']`(对应 Handlebars 的 HTML 转义形式)。若字段最终以 HTML 渲染、转义会破坏变量内容(如站内信正文),传 `['{{{', '}}}']` 走 Handlebars 的原样输出形式
|
|
167
168
|
- `value` / `onChange` / `placeholder` / `disabled`:标准受控字段属性
|
|
168
169
|
|
|
169
170
|
底层共用 `VariableHybridInput`(`VariableInput`)和 `TextAreaWithContextSelector`(`VariableTextArea`),用同一套 MetaTree 数据。
|
|
@@ -7,11 +7,32 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { useLocation, useMatch, useMatches, useParams } from 'react-router-dom';
|
|
10
|
+
import { useEffect } from 'react';
|
|
11
|
+
import { useLocation, useMatches, useParams } from 'react-router-dom';
|
|
13
12
|
import { Application } from '../Application';
|
|
14
13
|
|
|
14
|
+
type LayoutMatchLike = {
|
|
15
|
+
id: string;
|
|
16
|
+
pathname: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type LayoutDefinitionLike = {
|
|
20
|
+
routeName: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function findDeepestLayoutMatch(layouts: LayoutDefinitionLike[] = [], matches: LayoutMatchLike[] = []) {
|
|
24
|
+
const layoutRouteNames = new Set(layouts.map((layout) => layout.routeName));
|
|
25
|
+
|
|
26
|
+
for (let index = matches.length - 1; index >= 0; index -= 1) {
|
|
27
|
+
const match = matches[index];
|
|
28
|
+
if (layoutRouteNames.has(match.id)) {
|
|
29
|
+
return match;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
15
36
|
export function useRouterSync(app: Application) {
|
|
16
37
|
const params = useParams();
|
|
17
38
|
const location = useLocation();
|
|
@@ -20,13 +41,16 @@ export function useRouterSync(app: Application) {
|
|
|
20
41
|
useEffect(() => {
|
|
21
42
|
const last = matches[matches.length - 1];
|
|
22
43
|
if (!last) return;
|
|
44
|
+
const layoutMatch = findDeepestLayoutMatch(app.layoutManager?.listLayouts?.(), matches);
|
|
23
45
|
engine.context['_observableCache']['route'] = {
|
|
24
46
|
name: last.id,
|
|
25
47
|
pathname: last.pathname,
|
|
26
48
|
path: last.handle?.['path'] || null,
|
|
27
49
|
params,
|
|
50
|
+
layoutRouteName: layoutMatch?.id,
|
|
51
|
+
layoutBasePathname: layoutMatch?.pathname,
|
|
28
52
|
};
|
|
29
|
-
}, [engine.context, params, matches]);
|
|
53
|
+
}, [app, engine.context, params, matches]);
|
|
30
54
|
useEffect(() => {
|
|
31
55
|
engine.context['_observableCache']['location'] = location;
|
|
32
56
|
}, [engine.context, location]);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
11
|
+
import React, { useEffect } from 'react';
|
|
12
|
+
import { createMemoryRouter, Outlet, RouterProvider, useParams } from 'react-router-dom';
|
|
13
|
+
import { describe, expect, it } from 'vitest';
|
|
14
|
+
import { KeepAlive } from '../KeepAlive';
|
|
15
|
+
|
|
16
|
+
describe('KeepAlive', () => {
|
|
17
|
+
it('keeps inactive outlet pages mounted while switching route params', async () => {
|
|
18
|
+
const events: string[] = [];
|
|
19
|
+
|
|
20
|
+
const Page = () => {
|
|
21
|
+
const { name } = useParams();
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
events.push(`mount:${name}`);
|
|
24
|
+
return () => {
|
|
25
|
+
events.push(`unmount:${name}`);
|
|
26
|
+
};
|
|
27
|
+
}, [name]);
|
|
28
|
+
|
|
29
|
+
return <div>page {name}</div>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const Layout = () => {
|
|
33
|
+
const { name } = useParams();
|
|
34
|
+
return <KeepAlive uid={name || ''}>{() => <Outlet />}</KeepAlive>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const router = createMemoryRouter(
|
|
38
|
+
[
|
|
39
|
+
{
|
|
40
|
+
path: '/:name',
|
|
41
|
+
element: <Layout />,
|
|
42
|
+
children: [{ index: true, element: <Page /> }],
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
{
|
|
46
|
+
initialEntries: ['/page-a'],
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
render(<RouterProvider router={router} />);
|
|
51
|
+
|
|
52
|
+
expect(await screen.findByText('page page-a')).toBeInTheDocument();
|
|
53
|
+
|
|
54
|
+
await router.navigate('/page-b');
|
|
55
|
+
|
|
56
|
+
expect(await screen.findByText('page page-b')).toBeInTheDocument();
|
|
57
|
+
expect(screen.getByText('page page-a')).toBeInTheDocument();
|
|
58
|
+
|
|
59
|
+
await waitFor(() => {
|
|
60
|
+
expect(events).toEqual(['mount:page-a', 'mount:page-b']);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
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 { findDeepestLayoutMatch } from '../RouterBridge';
|
|
12
|
+
|
|
13
|
+
describe('RouterBridge', () => {
|
|
14
|
+
it('uses the deepest matched layout route as layout base pathname', () => {
|
|
15
|
+
const match = findDeepestLayoutMatch(
|
|
16
|
+
[{ routeName: 'admin' }, { routeName: 'admin.settings.publicForms' }],
|
|
17
|
+
[
|
|
18
|
+
{ id: 'admin', pathname: '/admin' },
|
|
19
|
+
{ id: 'admin.settings', pathname: '/admin/settings' },
|
|
20
|
+
{ id: 'admin.settings.publicForms', pathname: '/admin/settings/public-forms' },
|
|
21
|
+
{ id: 'admin.settings.publicForms.page.view', pathname: '/admin/settings/public-forms/form-1/view/popup' },
|
|
22
|
+
],
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
expect(match?.pathname).toBe('/admin/settings/public-forms');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -25,28 +25,75 @@ import { TextAreaWithContextSelector } from '../../flow/components/TextAreaWithC
|
|
|
25
25
|
* stored values round-trip to a labelled pill instead of falling back to a
|
|
26
26
|
* raw `{{…}}` literal.
|
|
27
27
|
*/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Variable delimiters: opening + closing tokens. Default `['{{', '}}']`
|
|
31
|
+
* matches NocoBase server template convention (Handlebars HTML-escaped
|
|
32
|
+
* output). Pass `['{{{', '}}}']` to switch to Handlebars' raw/unescaped
|
|
33
|
+
* form — required for fields whose content is rendered as HTML (e.g.
|
|
34
|
+
* in-app message body) so the variable expansion bypasses HTML escaping.
|
|
35
|
+
*
|
|
36
|
+
* Restrict to literal-token pairs, since the regex builder escapes them
|
|
37
|
+
* verbatim.
|
|
38
|
+
*/
|
|
39
|
+
export type VariableDelimiters = readonly [string, string];
|
|
40
|
+
|
|
41
|
+
const DEFAULT_DELIMITERS: VariableDelimiters = ['{{', '}}'];
|
|
42
|
+
|
|
43
|
+
function escapeForRegExp(s: string): string {
|
|
44
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Factory: returns a `parseValueToPath` bound to the given delimiters.
|
|
49
|
+
* Anchored (`^…$`) — only treats the whole input as a single variable
|
|
50
|
+
* reference, matching the v1 single-line picker behaviour.
|
|
51
|
+
*/
|
|
52
|
+
export function makeParseVariablePath(delimiters: VariableDelimiters = DEFAULT_DELIMITERS) {
|
|
53
|
+
const [open, close] = delimiters;
|
|
54
|
+
const re = new RegExp(`^${escapeForRegExp(open)}\\s*(.+?)\\s*${escapeForRegExp(close)}$`);
|
|
55
|
+
return (value?: string): string[] | undefined => {
|
|
56
|
+
if (typeof value !== 'string') return undefined;
|
|
57
|
+
const match = value.trim().match(re);
|
|
58
|
+
if (!match) return undefined;
|
|
59
|
+
let pathString = match[1];
|
|
60
|
+
// Backwards-compat: accept the legacy `ctx.` prefix so values produced by
|
|
61
|
+
// pre-fix versions of the picker still resolve to a labelled pill.
|
|
62
|
+
if (pathString === 'ctx') return [];
|
|
63
|
+
if (pathString.startsWith('ctx.')) pathString = pathString.slice(4);
|
|
64
|
+
return pathString.split('.');
|
|
65
|
+
};
|
|
48
66
|
}
|
|
49
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Factory: returns a `formatPathToValue` bound to the given delimiters.
|
|
70
|
+
* No inner spaces — matches the v1 storage shape exactly so round-trips
|
|
71
|
+
* through the API stay byte-stable.
|
|
72
|
+
*/
|
|
73
|
+
export function makeFormatVariablePath(delimiters: VariableDelimiters = DEFAULT_DELIMITERS) {
|
|
74
|
+
const [open, close] = delimiters;
|
|
75
|
+
return (meta?: MetaTreeNode): string | undefined => {
|
|
76
|
+
const paths = meta?.paths || [];
|
|
77
|
+
if (paths.length === 0) return undefined;
|
|
78
|
+
return `${open}${paths.join('.')}${close}`;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Factory: returns a global regex matching every occurrence of the
|
|
84
|
+
* variable token within a longer string. Used by `VariableHybridInput`
|
|
85
|
+
* to render embedded variables as pills.
|
|
86
|
+
*/
|
|
87
|
+
export function makeVariableRegExp(delimiters: VariableDelimiters = DEFAULT_DELIMITERS): RegExp {
|
|
88
|
+
const [open, close] = delimiters;
|
|
89
|
+
return new RegExp(`${escapeForRegExp(open)}\\s*([^{}]+?)\\s*${escapeForRegExp(close)}`, 'g');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Default exports — `{{ ... }}` for the most common case. Kept named so
|
|
93
|
+
// existing callers don't need to switch to the factory.
|
|
94
|
+
export const parseVariablePath = makeParseVariablePath();
|
|
95
|
+
export const formatVariablePath = makeFormatVariablePath();
|
|
96
|
+
|
|
50
97
|
const META_TREE_CACHE_PREFIX = '@nocobase/client-v2:VariableInput:metaTree';
|
|
51
98
|
|
|
52
99
|
/**
|
|
@@ -123,8 +170,22 @@ export interface VariableInputProps {
|
|
|
123
170
|
* Override the converters used by the underlying `VariableHybridInput`.
|
|
124
171
|
* Mostly useful when the caller wants to constrain `formatPathToValue` to a
|
|
125
172
|
* specific namespace (see `EnvVariableInput` for that pattern).
|
|
173
|
+
*
|
|
174
|
+
* Takes precedence over `delimiters` when both are set on the same field
|
|
175
|
+
* (an explicit converter wins over the delimiter-derived one).
|
|
126
176
|
*/
|
|
127
177
|
converters?: VariableHybridInputConverters;
|
|
178
|
+
/**
|
|
179
|
+
* Token pair wrapping variable references in the stored string. Defaults
|
|
180
|
+
* to `['{{', '}}']` — the standard NocoBase server-template form,
|
|
181
|
+
* HTML-escaped by Handlebars. Pass `['{{{', '}}}']` for fields rendered
|
|
182
|
+
* as HTML where escaping would corrupt the variable value (e.g. the
|
|
183
|
+
* in-app message body).
|
|
184
|
+
*
|
|
185
|
+
* Ignored when `converters` is also supplied — caller-provided converters
|
|
186
|
+
* win.
|
|
187
|
+
*/
|
|
188
|
+
delimiters?: VariableDelimiters;
|
|
128
189
|
className?: string;
|
|
129
190
|
style?: React.CSSProperties;
|
|
130
191
|
}
|
|
@@ -136,16 +197,24 @@ export interface VariableInputProps {
|
|
|
136
197
|
* line of mixed literal+variable content is appropriate.
|
|
137
198
|
*/
|
|
138
199
|
export function VariableInput(props: VariableInputProps) {
|
|
139
|
-
const { namespaces, extraNodes, converters, ...rest } = props;
|
|
200
|
+
const { namespaces, extraNodes, converters, delimiters, ...rest } = props;
|
|
140
201
|
const metaTree = useFilteredMetaTree({ namespaces, extraNodes });
|
|
141
|
-
const mergedConverters = useMemo<VariableHybridInputConverters>(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
202
|
+
const mergedConverters = useMemo<VariableHybridInputConverters>(() => {
|
|
203
|
+
// Default delimiters → reuse the pre-built singletons.
|
|
204
|
+
if (!delimiters) {
|
|
205
|
+
return {
|
|
206
|
+
formatPathToValue: formatVariablePath,
|
|
207
|
+
parseValueToPath: parseVariablePath,
|
|
208
|
+
...converters,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
formatPathToValue: makeFormatVariablePath(delimiters),
|
|
213
|
+
parseValueToPath: makeParseVariablePath(delimiters),
|
|
214
|
+
variableRegExp: makeVariableRegExp(delimiters),
|
|
145
215
|
...converters,
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
);
|
|
216
|
+
};
|
|
217
|
+
}, [converters, delimiters]);
|
|
149
218
|
return <VariableHybridInput {...rest} converters={mergedConverters} metaTree={metaTree} />;
|
|
150
219
|
}
|
|
151
220
|
|
|
@@ -161,9 +230,13 @@ export interface VariableTextAreaProps extends Omit<VariableInputProps, 'convert
|
|
|
161
230
|
* is desirable (the server expands them at render time).
|
|
162
231
|
*/
|
|
163
232
|
export function VariableTextArea(props: VariableTextAreaProps) {
|
|
164
|
-
const { namespaces, extraNodes, rows, maxRows, style, ...rest } = props;
|
|
233
|
+
const { namespaces, extraNodes, rows, maxRows, style, delimiters, ...rest } = props;
|
|
165
234
|
const metaTree = useFilteredMetaTree({ namespaces, extraNodes });
|
|
166
235
|
const metaTreeGetter = useMemo(() => () => metaTree, [metaTree]);
|
|
236
|
+
const formatPathToValue = useMemo(
|
|
237
|
+
() => (delimiters ? makeFormatVariablePath(delimiters) : formatVariablePath),
|
|
238
|
+
[delimiters],
|
|
239
|
+
);
|
|
167
240
|
return (
|
|
168
241
|
<TextAreaWithContextSelector
|
|
169
242
|
{...rest}
|
|
@@ -171,7 +244,7 @@ export function VariableTextArea(props: VariableTextAreaProps) {
|
|
|
171
244
|
maxRows={maxRows}
|
|
172
245
|
style={style}
|
|
173
246
|
metaTree={metaTreeGetter}
|
|
174
|
-
formatPathToValue={(meta) =>
|
|
247
|
+
formatPathToValue={(meta) => formatPathToValue(meta) ?? ''}
|
|
175
248
|
/>
|
|
176
249
|
);
|
|
177
250
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
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 {
|
|
12
|
+
formatVariablePath,
|
|
13
|
+
makeFormatVariablePath,
|
|
14
|
+
makeParseVariablePath,
|
|
15
|
+
makeVariableRegExp,
|
|
16
|
+
parseVariablePath,
|
|
17
|
+
} from '../VariableInput';
|
|
18
|
+
|
|
19
|
+
describe('VariableInput delimiter helpers', () => {
|
|
20
|
+
describe('default delimiters {{ }}', () => {
|
|
21
|
+
it('formats meta nodes into `{{x.y}}` with no inner spaces', () => {
|
|
22
|
+
expect(formatVariablePath({ paths: ['$user', 'id'] } as any)).toBe('{{$user.id}}');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('parses `{{x.y}}` into a path array', () => {
|
|
26
|
+
expect(parseVariablePath('{{ $user.id }}')).toEqual(['$user', 'id']);
|
|
27
|
+
expect(parseVariablePath('{{$user.id}}')).toEqual(['$user', 'id']);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('strips a legacy `ctx.` prefix for backwards-compat', () => {
|
|
31
|
+
expect(parseVariablePath('{{ctx.$user.id}}')).toEqual(['$user', 'id']);
|
|
32
|
+
expect(parseVariablePath('{{ctx}}')).toEqual([]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('returns undefined for plain text', () => {
|
|
36
|
+
expect(parseVariablePath('hello world')).toBeUndefined();
|
|
37
|
+
expect(parseVariablePath('')).toBeUndefined();
|
|
38
|
+
expect(parseVariablePath(undefined)).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('triple-brace delimiters {{{ }}}', () => {
|
|
43
|
+
const format = makeFormatVariablePath(['{{{', '}}}']);
|
|
44
|
+
const parse = makeParseVariablePath(['{{{', '}}}']);
|
|
45
|
+
|
|
46
|
+
it('formats meta nodes into `{{{x.y}}}`', () => {
|
|
47
|
+
expect(format({ paths: ['$user', 'id'] } as any)).toBe('{{{$user.id}}}');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('parses `{{{x.y}}}` into a path array', () => {
|
|
51
|
+
expect(parse('{{{$user.id}}}')).toEqual(['$user', 'id']);
|
|
52
|
+
expect(parse('{{{ $user.id }}}')).toEqual(['$user', 'id']);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('does not match the double-brace form', () => {
|
|
56
|
+
expect(parse('{{$user.id}}')).toBeUndefined();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('makeVariableRegExp', () => {
|
|
61
|
+
it('matches every occurrence in a longer string under default delimiters', () => {
|
|
62
|
+
const re = makeVariableRegExp();
|
|
63
|
+
const matches = Array.from('hello {{ $user.id }} world {{ $env.X }}'.matchAll(re)).map((m) => m[1]);
|
|
64
|
+
expect(matches).toEqual(['$user.id', '$env.X']);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('matches triple-brace occurrences when configured', () => {
|
|
68
|
+
const re = makeVariableRegExp(['{{{', '}}}']);
|
|
69
|
+
const matches = Array.from('hi {{{$user.id}}} :)'.matchAll(re)).map((m) => m[1]);
|
|
70
|
+
expect(matches).toEqual(['$user.id']);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('does not match double-brace occurrences under triple-brace config', () => {
|
|
74
|
+
const re = makeVariableRegExp(['{{{', '}}}']);
|
|
75
|
+
const matches = Array.from('plain {{ x }} text'.matchAll(re));
|
|
76
|
+
expect(matches).toHaveLength(0);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('handles empty paths consistently', () => {
|
|
81
|
+
expect(formatVariablePath({ paths: [] } as any)).toBeUndefined();
|
|
82
|
+
expect(formatVariablePath(undefined)).toBeUndefined();
|
|
83
|
+
expect(makeFormatVariablePath(['{{{', '}}}'])({ paths: [] } as any)).toBeUndefined();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -25,7 +25,15 @@ export interface CollectionFilterProps {
|
|
|
25
25
|
t?: (key: string, options?: Record<string, any>) => string;
|
|
26
26
|
/** Whitelist of root-level field names to expose. */
|
|
27
27
|
filterableFieldNames?: string[];
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Blacklist of root-level field names to drop. Mirrors v1's `nonfilterable: [...]` on `Filter.Action`. When both `filterableFieldNames` and this prop are supplied, both apply (final = whitelist ∩ ¬blacklist).
|
|
30
|
+
*/
|
|
31
|
+
nonfilterableFieldNames?: string[];
|
|
32
|
+
/**
|
|
33
|
+
* Bypass the `filterableFieldNames` whitelist.
|
|
34
|
+
*
|
|
35
|
+
* Legacy escape hatch — prefer adjusting `filterableFieldNames` / `nonfilterableFieldNames` instead.
|
|
36
|
+
*/
|
|
29
37
|
noIgnore?: boolean;
|
|
30
38
|
/** Override the trigger button's label. Defaults to `t('Filter')`, or the v1-style `t('{{count}} filter items', { count })` when conditions are present. */
|
|
31
39
|
buttonText?: React.ReactNode;
|
|
@@ -50,6 +58,7 @@ export const CollectionFilter: FC<CollectionFilterProps> = (props) => {
|
|
|
50
58
|
onChange,
|
|
51
59
|
t = identity,
|
|
52
60
|
filterableFieldNames,
|
|
61
|
+
nonfilterableFieldNames,
|
|
53
62
|
noIgnore,
|
|
54
63
|
buttonText,
|
|
55
64
|
showCount = true,
|
|
@@ -63,6 +72,7 @@ export const CollectionFilter: FC<CollectionFilterProps> = (props) => {
|
|
|
63
72
|
const filterAction = useFilterActionProps({
|
|
64
73
|
collection,
|
|
65
74
|
filterableFieldNames,
|
|
75
|
+
nonfilterableFieldNames,
|
|
66
76
|
noIgnore,
|
|
67
77
|
t,
|
|
68
78
|
onApply: (filter: CompiledFilter, action: FilterApplyAction) => {
|
|
@@ -44,7 +44,15 @@ export interface CollectionFilterItemProps {
|
|
|
44
44
|
collection: Collection;
|
|
45
45
|
/** Whitelist of field names to expose; empty/undefined means all filterable fields. */
|
|
46
46
|
filterableFieldNames?: string[];
|
|
47
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* Blacklist of field names to drop. Mirrors v1's `nonfilterable: [...]` on `Filter.Action`. When both whitelist and blacklist are supplied, both apply (final = whitelist ∩ ¬blacklist).
|
|
49
|
+
*/
|
|
50
|
+
nonfilterableFieldNames?: string[];
|
|
51
|
+
/**
|
|
52
|
+
* Bypass the `filterableFieldNames` whitelist (matches the legacy FilterItem `noIgnore`).
|
|
53
|
+
*
|
|
54
|
+
* Legacy escape hatch — prefer adjusting `filterableFieldNames` / `nonfilterableFieldNames` instead.
|
|
55
|
+
*/
|
|
48
56
|
noIgnore?: boolean;
|
|
49
57
|
/** Translator; defaults to identity so callers can omit it. */
|
|
50
58
|
t?: (key: string) => string;
|
|
@@ -84,10 +92,10 @@ const toCascaderOptions = (options: FilterOption[]): CascaderOption[] =>
|
|
|
84
92
|
*/
|
|
85
93
|
export const CollectionFilterItem: FC<CollectionFilterItemProps> = observer(
|
|
86
94
|
(props) => {
|
|
87
|
-
const { collection, filterableFieldNames, noIgnore = false, t = identity } = props;
|
|
95
|
+
const { collection, filterableFieldNames, nonfilterableFieldNames, noIgnore = false, t = identity } = props;
|
|
88
96
|
const { path: leftValue, operator, value: rightValue } = props.value;
|
|
89
97
|
|
|
90
|
-
const options = useFilterOptions(collection, { filterableFieldNames, noIgnore, t });
|
|
98
|
+
const options = useFilterOptions(collection, { filterableFieldNames, nonfilterableFieldNames, noIgnore, t });
|
|
91
99
|
|
|
92
100
|
const cascaderOptions = useMemo(() => toCascaderOptions(options), [options]);
|
|
93
101
|
|
|
@@ -164,7 +172,7 @@ export const CollectionFilterItem: FC<CollectionFilterItemProps> = observer(
|
|
|
164
172
|
*/
|
|
165
173
|
export function createCollectionFilterItem(
|
|
166
174
|
collection: Collection,
|
|
167
|
-
bound?: Pick<CollectionFilterItemProps, 'filterableFieldNames' | 'noIgnore' | 't'>,
|
|
175
|
+
bound?: Pick<CollectionFilterItemProps, 'filterableFieldNames' | 'nonfilterableFieldNames' | 'noIgnore' | 't'>,
|
|
168
176
|
) {
|
|
169
177
|
const Component: FC<{ value: CollectionFilterItemValue }> = (props) => (
|
|
170
178
|
<CollectionFilterItem {...bound} value={props.value} collection={collection} />
|