@nocobase/client-v2 2.3.0-alpha.1 → 2.3.0-beta.11
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/APIClient.d.ts +12 -0
- package/es/components/form/ScanInput/useCodeScanner.d.ts +12 -2
- package/es/components/form/ScanInput/zxingWasmDecoder.d.ts +9 -0
- package/es/flow/actions/linkageRules.d.ts +25 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +6 -30
- package/es/flow/components/FlowRoute.d.ts +2 -2
- package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
- package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +32 -0
- package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
- package/es/flow/components/field-value-variable/index.d.ts +11 -0
- package/es/flow/components/filter/VariableFilterItem.d.ts +7 -0
- package/es/flow/components/placeholders/BlockPlaceholder.d.ts +1 -0
- package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
- package/es/flow/internal/utils/operatorSchemaHelper.d.ts +2 -2
- package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
- package/es/flow/models/base/ActionModelCore.d.ts +4 -2
- package/es/flow/models/base/BlockGridModel.d.ts +3 -0
- package/es/flow/models/base/CollectionBlockModel.d.ts +3 -0
- package/es/flow/models/blocks/details/DetailsBlockModel.d.ts +3 -0
- package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
- package/es/flow/models/blocks/filter-form/FilterFormSubmitActionModel.d.ts +3 -1
- package/es/flow/models/blocks/form/FormBlockModel.d.ts +7 -0
- package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
- package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
- package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
- package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
- package/es/flow/models/blocks/table/TableColumnModel.d.ts +12 -0
- package/es/flow/models/blocks/table/utils.d.ts +1 -0
- package/es/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.d.ts +2 -1
- package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +0 -17
- package/es/flow/models/fields/AssociationFieldModel/recordSelectShared.d.ts +41 -0
- package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
- package/es/flow-compat/passwordUtils.d.ts +1 -1
- package/es/index.d.ts +1 -0
- package/es/index.mjs +148 -145
- package/es/ui-operation/index.d.ts +15 -0
- package/es/ui-operation/ui-operation-codec.d.ts +10 -0
- package/lib/index.js +170 -167
- package/lib/locale/languageCodes.js +1 -0
- package/package.json +10 -8
- package/src/APIClient.ts +92 -0
- package/src/Application.tsx +3 -1
- package/src/__tests__/APIClient.test.tsx +58 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +7 -2
- package/src/__tests__/settings-center.test.tsx +270 -6
- package/src/acl/ACLProvider.tsx +2 -0
- package/src/acl/__tests__/ACLProvider.test.tsx +92 -0
- package/src/collection-manager/__tests__/field-configure.test.ts +52 -0
- package/src/collection-manager/field-configure.ts +2 -2
- package/src/components/form/ScanInput/CodeScanner.tsx +64 -35
- package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +107 -0
- package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +294 -24
- package/src/components/form/ScanInput/__tests__/zxingWasmDecoder.test.ts +78 -0
- package/src/components/form/ScanInput/useCodeScanner.ts +281 -23
- package/src/components/form/ScanInput/zxingWasmDecoder.ts +64 -0
- package/src/components/form/filter/CollectionFilterItem.tsx +4 -2
- package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +17 -0
- package/src/components/form/table/Table.tsx +81 -6
- package/src/components/form/table/__tests__/Table.columnWidth.test.tsx +433 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +126 -8
- package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
- package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
- package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
- package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +72 -0
- package/src/flow/actions/__tests__/validation.test.ts +48 -0
- package/src/flow/actions/linkageRules.tsx +59 -16
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +78 -59
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +228 -0
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +42 -23
- package/src/flow/common/Markdown/style.ts +4 -0
- package/src/flow/components/BlockItemCard.tsx +2 -2
- package/src/flow/components/FieldAssignValueInput.tsx +42 -621
- package/src/flow/components/FlowRoute.tsx +71 -29
- package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
- package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +335 -0
- package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +389 -0
- package/src/flow/components/field-value-variable/dateValue.ts +223 -0
- package/src/flow/components/field-value-variable/index.ts +12 -0
- package/src/flow/components/filter/VariableFilterItem.tsx +23 -6
- package/src/flow/components/filter/__tests__/VariableFilterItem.rightMetaTree.test.tsx +158 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +125 -0
- package/src/flow/components/placeholders/BlockPlaceholder.tsx +70 -23
- package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +57 -7
- package/src/flow/index.ts +2 -0
- package/src/flow/internal/components/Markdown/DisplayMarkdown.tsx +14 -9
- package/src/flow/internal/components/Markdown/__tests__/DisplayMarkdown.test.tsx +46 -0
- package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
- package/src/flow/internal/utils/operatorSchemaHelper.ts +3 -2
- package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
- package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
- package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
- package/src/flow/models/base/ActionModelCore.tsx +11 -2
- package/src/flow/models/base/BlockGridModel.tsx +26 -0
- package/src/flow/models/base/CollectionBlockModel.tsx +38 -3
- package/src/flow/models/base/GridModel.tsx +0 -1
- package/src/flow/models/base/PageModel/PageTabModel.tsx +67 -17
- package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +509 -12
- package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
- package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
- package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
- package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +125 -9
- package/src/flow/models/blocks/assign-form/AssignFormGridModel.tsx +22 -1
- package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +36 -50
- package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.editor.test.tsx +140 -0
- package/src/flow/models/blocks/details/DetailsBlockModel.tsx +8 -3
- package/src/flow/models/blocks/details/__tests__/DetailsBlockModel.initialPaginationChangeRefresh.test.ts +32 -1
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +27 -3
- package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +36 -5
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +129 -14
- package/src/flow/models/blocks/filter-form/FilterFormSubmitActionModel.tsx +57 -4
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.onModelCreated.test.ts +174 -1
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +29 -0
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +359 -1
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +72 -0
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormSubmitActionModel.lifecycle.test.ts +90 -0
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +32 -4
- package/src/flow/models/blocks/filter-form/fields/FieldComponentProps.tsx +1 -1
- package/src/flow/models/blocks/filter-form/fields/__tests__/FieldComponentProps.options.test.tsx +61 -0
- package/src/flow/models/blocks/filter-manager/FilterManager.ts +5 -0
- package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +40 -0
- package/src/flow/models/blocks/form/FormBlockModel.tsx +73 -5
- package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +153 -2
- package/src/flow/models/blocks/form/__tests__/popupLinkage.test.tsx +175 -0
- package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
- package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
- package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
- package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +28 -2
- package/src/flow/models/blocks/table/TableColumnModel.tsx +48 -9
- package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.filterReset.test.ts +78 -0
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
- package/src/flow/models/blocks/table/utils.ts +7 -0
- package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +33 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +12 -3
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
- package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +29 -3
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +9 -136
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +5 -1
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +191 -3
- package/src/flow/models/fields/AssociationFieldModel/recordSelectShared.tsx +156 -0
- package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
- package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
- package/src/flow/models/fields/DisplayAssociationField/DisplaySubTableFieldModel.tsx +42 -7
- package/src/flow/models/fields/DisplayAssociationField/__tests__/DisplaySubTableFieldModel.test.tsx +351 -0
- package/src/flow/models/fields/DisplayHtmlFieldModel.tsx +2 -1
- package/src/flow/models/fields/DisplayNumberFieldModel.tsx +1 -1
- package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
- package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
- package/src/flow/models/fields/RichTextFieldModel/__tests__/RichTextFieldModel.test.tsx +74 -0
- package/src/flow/models/fields/RichTextFieldModel/index.tsx +27 -6
- package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
- package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
- package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +6 -0
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +27 -2
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +139 -13
- package/src/flow/models/topbar/TopbarActionModel.tsx +5 -5
- package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
- package/src/index.ts +1 -0
- package/src/layout-manager/LayoutContentRoute.tsx +2 -1
- package/src/layout-manager/LayoutRoute.tsx +2 -1
- package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
- package/src/locale/languageCodes.ts +1 -0
- package/src/settings-center/AdminSettingsLayout.tsx +23 -2
- package/src/ui-operation/index.ts +33 -0
- package/src/ui-operation/ui-operation-codec.ts +54 -0
|
@@ -68,6 +68,12 @@ interface LinkageRule {
|
|
|
68
68
|
}[];
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
export function updateLinkageRules<T>(rules: T[], updater: (nextRules: T[]) => void): T[] {
|
|
72
|
+
const nextRules = _.cloneDeep(rules);
|
|
73
|
+
updater(nextRules);
|
|
74
|
+
return nextRules;
|
|
75
|
+
}
|
|
76
|
+
|
|
71
77
|
const previewValueForLog = (value: any) => {
|
|
72
78
|
if (value == null) return value;
|
|
73
79
|
const t = typeof value;
|
|
@@ -114,11 +120,16 @@ const isActionFlowModel = (model: any): boolean => {
|
|
|
114
120
|
return false;
|
|
115
121
|
};
|
|
116
122
|
|
|
123
|
+
const isFormFieldModel = (model: unknown): model is FlowModel => {
|
|
124
|
+
if (!model || typeof model !== 'object') return false;
|
|
125
|
+
return !!(model as FlowModel).subModels?.field;
|
|
126
|
+
};
|
|
127
|
+
|
|
117
128
|
// 获取表单中所有字段的 model 实例的通用函数
|
|
118
129
|
const getFormFields = (ctx: any) => {
|
|
119
130
|
try {
|
|
120
131
|
const fieldModels = ctx.model?.subModels?.grid?.subModels?.items || [];
|
|
121
|
-
return fieldModels.map((model
|
|
132
|
+
return fieldModels.filter(isFormFieldModel).map((model) => ({
|
|
122
133
|
label: model.props.label || model.props.name,
|
|
123
134
|
value: model.uid,
|
|
124
135
|
model,
|
|
@@ -340,8 +351,8 @@ const FieldStateEditor = ({
|
|
|
340
351
|
const getFormFieldsByForkModel = (ctx: any) => {
|
|
341
352
|
try {
|
|
342
353
|
const fieldModels = ctx.model?.subModels?.grid?.subModels?.items || [];
|
|
343
|
-
return fieldModels.map((model
|
|
344
|
-
const forkModel = Array.from(model.forks)[0] as
|
|
354
|
+
return fieldModels.filter(isFormFieldModel).map((model) => {
|
|
355
|
+
const forkModel = Array.from(model.forks)[0] as FlowModel | undefined;
|
|
345
356
|
|
|
346
357
|
if (forkModel) {
|
|
347
358
|
return {
|
|
@@ -654,6 +665,30 @@ export const linkageSetBlockProps = defineAction({
|
|
|
654
665
|
},
|
|
655
666
|
});
|
|
656
667
|
|
|
668
|
+
const ACTION_LINKAGE_STATE_OPTIONS = [
|
|
669
|
+
{ label: 'Visible', value: 'visible' },
|
|
670
|
+
{ label: 'Hidden', value: 'hidden' },
|
|
671
|
+
{ label: 'Hidden text', value: 'hiddenText' },
|
|
672
|
+
{ label: 'Enabled', value: 'enabled' },
|
|
673
|
+
{ label: 'Disabled', value: 'disabled' },
|
|
674
|
+
] as const;
|
|
675
|
+
|
|
676
|
+
type ActionLinkageState = (typeof ACTION_LINKAGE_STATE_OPTIONS)[number]['value'];
|
|
677
|
+
|
|
678
|
+
export function getActionLinkageStateOptions(
|
|
679
|
+
model: { supportedActionLinkageStates?: readonly ActionLinkageState[] },
|
|
680
|
+
t: (key: string) => string,
|
|
681
|
+
) {
|
|
682
|
+
const supportedStates = model.supportedActionLinkageStates ? new Set(model.supportedActionLinkageStates) : undefined;
|
|
683
|
+
|
|
684
|
+
return ACTION_LINKAGE_STATE_OPTIONS.filter((option) => !supportedStates || supportedStates.has(option.value)).map(
|
|
685
|
+
(option) => ({
|
|
686
|
+
label: t(option.label),
|
|
687
|
+
value: option.value,
|
|
688
|
+
}),
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
|
|
657
692
|
export const linkageSetActionProps = defineAction({
|
|
658
693
|
name: 'linkageSetActionProps',
|
|
659
694
|
title: tExpr('Set button state'),
|
|
@@ -674,13 +709,7 @@ export const linkageSetActionProps = defineAction({
|
|
|
674
709
|
onChange={onChange}
|
|
675
710
|
placeholder={t('Please select state')}
|
|
676
711
|
style={{ width: '100%' }}
|
|
677
|
-
options={
|
|
678
|
-
{ label: t('Visible'), value: 'visible' },
|
|
679
|
-
{ label: t('Hidden'), value: 'hidden' },
|
|
680
|
-
{ label: t('Hidden text'), value: 'hiddenText' },
|
|
681
|
-
{ label: t('Enabled'), value: 'enabled' },
|
|
682
|
-
{ label: t('Disabled'), value: 'disabled' },
|
|
683
|
-
]}
|
|
712
|
+
options={getActionLinkageStateOptions(ctx.model, t)}
|
|
684
713
|
allowClear
|
|
685
714
|
/>
|
|
686
715
|
);
|
|
@@ -1500,13 +1529,26 @@ async function resolveLinkageRulesParamsPreservingRunJsScripts(ctx: FlowContext,
|
|
|
1500
1529
|
}
|
|
1501
1530
|
|
|
1502
1531
|
const LinkageRulesUI = observer(
|
|
1503
|
-
(props: {
|
|
1504
|
-
|
|
1532
|
+
(props: {
|
|
1533
|
+
readonly value: LinkageRule[];
|
|
1534
|
+
onChange?: (value: LinkageRule[]) => void;
|
|
1535
|
+
supportedActions: string[];
|
|
1536
|
+
title?: string;
|
|
1537
|
+
}) => {
|
|
1538
|
+
const { value: rules = [], onChange, supportedActions } = props;
|
|
1505
1539
|
const ctx = useFlowContext();
|
|
1506
1540
|
const flowEngine = useFlowEngine();
|
|
1507
1541
|
const t = ctx.model.translate.bind(ctx.model);
|
|
1508
1542
|
const assignPriorityTip = t('Assignment takes precedence over form field assignment');
|
|
1509
1543
|
|
|
1544
|
+
const replaceRules = (updater: (nextRules: LinkageRule[]) => void) => {
|
|
1545
|
+
if (onChange) {
|
|
1546
|
+
onChange(updateLinkageRules(rules, updater));
|
|
1547
|
+
} else {
|
|
1548
|
+
updater(rules);
|
|
1549
|
+
}
|
|
1550
|
+
};
|
|
1551
|
+
|
|
1510
1552
|
// 创建新规则的默认值
|
|
1511
1553
|
const createNewRule = (): LinkageRule => ({
|
|
1512
1554
|
key: uid(),
|
|
@@ -1523,7 +1565,7 @@ const LinkageRulesUI = observer(
|
|
|
1523
1565
|
|
|
1524
1566
|
// 删除规则
|
|
1525
1567
|
const handleDeleteRule = (index: number) => {
|
|
1526
|
-
|
|
1568
|
+
replaceRules((nextRules) => nextRules.splice(index, 1));
|
|
1527
1569
|
};
|
|
1528
1570
|
|
|
1529
1571
|
// 上移规则
|
|
@@ -1562,7 +1604,9 @@ const LinkageRulesUI = observer(
|
|
|
1562
1604
|
|
|
1563
1605
|
// 切换规则启用状态
|
|
1564
1606
|
const handleToggleEnable = (index: number, enable: boolean) => {
|
|
1565
|
-
|
|
1607
|
+
replaceRules((nextRules) => {
|
|
1608
|
+
nextRules[index].enable = enable;
|
|
1609
|
+
});
|
|
1566
1610
|
};
|
|
1567
1611
|
|
|
1568
1612
|
// 获取可用的动作类型
|
|
@@ -2222,7 +2266,7 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
|
|
|
2222
2266
|
}
|
|
2223
2267
|
|
|
2224
2268
|
rememberOriginalProp(key, model.props?.[key]);
|
|
2225
|
-
if (key === 'hiddenText') {
|
|
2269
|
+
if (key === 'hiddenText' && normalizedProps[key]) {
|
|
2226
2270
|
rememberOriginalProp('title', model.props?.title);
|
|
2227
2271
|
}
|
|
2228
2272
|
if (key === 'required') {
|
|
@@ -2286,7 +2330,6 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
|
|
|
2286
2330
|
const newProps = { ...model.__originalProps, ...patchProps };
|
|
2287
2331
|
const prevHidden = !!model.hidden;
|
|
2288
2332
|
const nextHidden = !!newProps.hiddenModel;
|
|
2289
|
-
|
|
2290
2333
|
model.setProps(_.omit(newProps, ['hiddenModel', 'value', 'hiddenText', LINKAGE_ASSIGN_MODE_PROP]));
|
|
2291
2334
|
syncFieldOptionsToForks(model, patchProps);
|
|
2292
2335
|
if (typeof model.setHidden === 'function') {
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
FlowSettingsButton,
|
|
18
18
|
observer,
|
|
19
19
|
} from '@nocobase/flow-engine';
|
|
20
|
-
import { Badge
|
|
20
|
+
import { Badge } from 'antd';
|
|
21
21
|
import qs from 'qs';
|
|
22
22
|
import React, { FC, useCallback, useContext, useEffect } from 'react';
|
|
23
23
|
import { Link, useLocation, type NavigateFunction } from 'react-router-dom';
|
|
@@ -153,6 +153,24 @@ const getLayoutRoutePathFromModel = (model: FlowModel): AdminLayoutRoutePathLike
|
|
|
153
153
|
};
|
|
154
154
|
|
|
155
155
|
const menuItemStyle = { display: 'flex', alignItems: 'center', justifyContent: 'space-between' };
|
|
156
|
+
const siderMenuItemStyle: React.CSSProperties = { ...menuItemStyle, width: '100%', minWidth: 0 };
|
|
157
|
+
const siderMenuItemContentStyle: React.CSSProperties = { flex: 1, minWidth: 0, overflow: 'hidden' };
|
|
158
|
+
const siderMenuItemLinkStyle: React.CSSProperties = {
|
|
159
|
+
display: 'block',
|
|
160
|
+
flex: 1,
|
|
161
|
+
width: '100%',
|
|
162
|
+
minWidth: 0,
|
|
163
|
+
overflow: 'hidden',
|
|
164
|
+
};
|
|
165
|
+
const getMenuItemText = (name: React.ReactNode) => {
|
|
166
|
+
if (typeof name === 'string' || typeof name === 'number') {
|
|
167
|
+
return String(name);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (React.isValidElement<{ title?: unknown }>(name) && typeof name.props.title === 'string') {
|
|
171
|
+
return name.props.title;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
156
174
|
const groupLandingEntryStyle = {
|
|
157
175
|
display: 'inline-flex',
|
|
158
176
|
alignItems: 'center',
|
|
@@ -553,16 +571,16 @@ export function resolveAdminLayoutMenuDragMoveOptionsFromEvent(
|
|
|
553
571
|
|
|
554
572
|
const GroupItem: FC<{ item: AdminLayoutMenuNode; options?: AdminLayoutMenuRenderOptions }> = (props) => {
|
|
555
573
|
const { item } = props;
|
|
574
|
+
const { inHeader } = useContext(HeaderContext);
|
|
556
575
|
const badgeCount = useEvaluatedExpression(item._route.options?.badge?.count, item._model?.context);
|
|
557
576
|
const navigate = useNavigateNoUpdate();
|
|
558
577
|
const routerBasename = useRouterBasename();
|
|
559
578
|
const { closeMobileMenu } = useContext(MobileMenuControlContext);
|
|
560
579
|
const ariaLabel =
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
: undefined;
|
|
580
|
+
getMenuItemText(item.name) ??
|
|
581
|
+
(typeof item._route?.title === 'string' || typeof item._route?.title === 'number'
|
|
582
|
+
? String(item._route.title)
|
|
583
|
+
: undefined);
|
|
566
584
|
const runtimePath = item._runtimePath;
|
|
567
585
|
const spaRuntimePath = runtimePath ? toRouterNavigationPath(runtimePath, routerBasename) : runtimePath;
|
|
568
586
|
|
|
@@ -633,7 +651,12 @@ const GroupItem: FC<{ item: AdminLayoutMenuNode; options?: AdminLayoutMenuRender
|
|
|
633
651
|
return (
|
|
634
652
|
<ParentRouteContext.Provider value={item._parentRoute}>
|
|
635
653
|
<NocoBaseRouteContext.Provider value={item._route}>
|
|
636
|
-
<div
|
|
654
|
+
<div
|
|
655
|
+
aria-label={ariaLabel}
|
|
656
|
+
title={inHeader || item._route?.tooltip ? undefined : ariaLabel}
|
|
657
|
+
role="none"
|
|
658
|
+
style={inHeader ? menuItemStyle : siderMenuItemStyle}
|
|
659
|
+
>
|
|
637
660
|
{content}
|
|
638
661
|
{landingEntry}
|
|
639
662
|
{badgeCount != null && (
|
|
@@ -650,24 +673,9 @@ const GroupItem: FC<{ item: AdminLayoutMenuNode; options?: AdminLayoutMenuRender
|
|
|
650
673
|
);
|
|
651
674
|
};
|
|
652
675
|
|
|
653
|
-
const WithTooltip: FC<{ title: React.ReactNode; hidden: boolean; badgeProps: any }> = (props) => {
|
|
654
|
-
const { inHeader } = useContext(HeaderContext);
|
|
655
|
-
|
|
656
|
-
if (props.hidden || inHeader) {
|
|
657
|
-
return props.children;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
return (
|
|
661
|
-
<Tooltip title={props.title} placement="right">
|
|
662
|
-
<Badge {...props.badgeProps} style={{ transform: 'none', maxWidth: '10em' }} dot={false}>
|
|
663
|
-
{props.children}
|
|
664
|
-
</Badge>
|
|
665
|
-
</Tooltip>
|
|
666
|
-
);
|
|
667
|
-
};
|
|
668
|
-
|
|
669
676
|
const MenuItem: FC<{ item: AdminLayoutMenuNode; options?: AdminLayoutMenuRenderOptions }> = (props) => {
|
|
670
677
|
const { item } = props;
|
|
678
|
+
const { inHeader } = useContext(HeaderContext);
|
|
671
679
|
const location = useLocation();
|
|
672
680
|
const badgeCount = useEvaluatedExpression(item._route.options?.badge?.count, item._model?.context);
|
|
673
681
|
const navigate = useNavigateNoUpdate();
|
|
@@ -677,7 +685,10 @@ const MenuItem: FC<{ item: AdminLayoutMenuNode; options?: AdminLayoutMenuRenderO
|
|
|
677
685
|
const runtimePath = item._runtimePath || path;
|
|
678
686
|
const spaRuntimePath = runtimePath ? toRouterNavigationPath(runtimePath, basenameOfCurrentRouter) : runtimePath;
|
|
679
687
|
const isDocumentNavigation = item._navigationMode === 'document';
|
|
680
|
-
const
|
|
688
|
+
const { textColor: badgeTextColor, ...antdBadgeOptions } = item._route.options?.badge || {};
|
|
689
|
+
const badgeProps = { ...antdBadgeOptions, count: badgeCount };
|
|
690
|
+
const menuItemText = getMenuItemText(item.name);
|
|
691
|
+
const nativeTitle = inHeader || item._route?.tooltip ? undefined : menuItemText;
|
|
681
692
|
|
|
682
693
|
const canUseNativeAnchor = !!runtimePath && isDocumentNavigation;
|
|
683
694
|
|
|
@@ -767,17 +778,25 @@ const MenuItem: FC<{ item: AdminLayoutMenuNode; options?: AdminLayoutMenuRenderO
|
|
|
767
778
|
return (
|
|
768
779
|
<ParentRouteContext.Provider value={item._parentRoute}>
|
|
769
780
|
<NocoBaseRouteContext.Provider value={item._route}>
|
|
770
|
-
<div role="none" style={menuItemStyle}>
|
|
771
|
-
<div onClick={handleClickLink}>
|
|
772
|
-
<Link
|
|
781
|
+
<div role="none" style={inHeader ? menuItemStyle : siderMenuItemStyle}>
|
|
782
|
+
<div onClick={handleClickLink} style={inHeader ? undefined : siderMenuItemContentStyle}>
|
|
783
|
+
<Link
|
|
784
|
+
to={location.pathname}
|
|
785
|
+
aria-label={menuItemText}
|
|
786
|
+
title={nativeTitle}
|
|
787
|
+
style={inHeader ? undefined : siderMenuItemLinkStyle}
|
|
788
|
+
>
|
|
773
789
|
{props.children}
|
|
774
790
|
</Link>
|
|
775
791
|
</div>
|
|
776
792
|
{badgeCount != null && (
|
|
777
793
|
<Badge
|
|
778
|
-
{...
|
|
779
|
-
|
|
780
|
-
|
|
794
|
+
{...badgeProps}
|
|
795
|
+
style={{
|
|
796
|
+
marginLeft: 4,
|
|
797
|
+
...(badgeTextColor == null ? {} : { color: badgeTextColor }),
|
|
798
|
+
maxWidth: '10em',
|
|
799
|
+
}}
|
|
781
800
|
dot={false}
|
|
782
801
|
></Badge>
|
|
783
802
|
)}
|
|
@@ -787,45 +806,45 @@ const MenuItem: FC<{ item: AdminLayoutMenuNode; options?: AdminLayoutMenuRenderO
|
|
|
787
806
|
);
|
|
788
807
|
}
|
|
789
808
|
|
|
790
|
-
const itemContent = (
|
|
791
|
-
<
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
}
|
|
796
|
-
|
|
809
|
+
const itemContent = canUseNativeAnchor ? (
|
|
810
|
+
<a
|
|
811
|
+
href={runtimePath}
|
|
812
|
+
aria-label={menuItemText}
|
|
813
|
+
title={nativeTitle}
|
|
814
|
+
onClick={handleClickMenuItem}
|
|
815
|
+
style={inHeader ? undefined : siderMenuItemLinkStyle}
|
|
797
816
|
>
|
|
798
|
-
{
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
) : (
|
|
815
|
-
<span aria-label={typeof item.name === 'string' ? item.name : undefined}>{props.children}</span>
|
|
816
|
-
)}
|
|
817
|
-
</WithTooltip>
|
|
817
|
+
{props.children}
|
|
818
|
+
</a>
|
|
819
|
+
) : runtimePath ? (
|
|
820
|
+
<Link
|
|
821
|
+
to={spaRuntimePath}
|
|
822
|
+
aria-label={menuItemText}
|
|
823
|
+
title={nativeTitle}
|
|
824
|
+
onClick={handleClickMenuItem}
|
|
825
|
+
style={inHeader ? undefined : siderMenuItemLinkStyle}
|
|
826
|
+
>
|
|
827
|
+
{props.children}
|
|
828
|
+
</Link>
|
|
829
|
+
) : (
|
|
830
|
+
<span aria-label={menuItemText} title={nativeTitle} style={inHeader ? undefined : siderMenuItemLinkStyle}>
|
|
831
|
+
{props.children}
|
|
832
|
+
</span>
|
|
818
833
|
);
|
|
819
834
|
|
|
820
835
|
return (
|
|
821
836
|
<ParentRouteContext.Provider value={item._parentRoute}>
|
|
822
837
|
<NocoBaseRouteContext.Provider value={item._route}>
|
|
823
|
-
<div role="none" style={menuItemStyle}>
|
|
838
|
+
<div role="none" style={inHeader ? menuItemStyle : siderMenuItemStyle}>
|
|
824
839
|
{itemContent}
|
|
825
840
|
{badgeCount != null && (
|
|
826
841
|
<Badge
|
|
827
842
|
{...badgeProps}
|
|
828
|
-
style={{
|
|
843
|
+
style={{
|
|
844
|
+
marginLeft: 4,
|
|
845
|
+
...(badgeTextColor == null ? {} : { color: badgeTextColor }),
|
|
846
|
+
maxWidth: '10em',
|
|
847
|
+
}}
|
|
829
848
|
dot={false}
|
|
830
849
|
></Badge>
|
|
831
850
|
)}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
AdminLayoutMenuItemModel,
|
|
20
20
|
AdminLayoutModel,
|
|
21
21
|
ADMIN_LAYOUT_MODEL_UID,
|
|
22
|
+
buildMenuTitleWithIcon,
|
|
22
23
|
getAdminLayoutMenuMovePositionOptions,
|
|
23
24
|
normalizeAdminLayoutMenuLegacyVariables,
|
|
24
25
|
openAdminLayoutMenuLink,
|
|
@@ -119,6 +120,233 @@ describe('AdminLayoutModel menu items', () => {
|
|
|
119
120
|
...options,
|
|
120
121
|
});
|
|
121
122
|
|
|
123
|
+
type MenuTitleOptions = {
|
|
124
|
+
title: string;
|
|
125
|
+
collapsed: boolean;
|
|
126
|
+
name?: React.ReactNode;
|
|
127
|
+
routeTitle?: string;
|
|
128
|
+
renderType?: 'item' | 'group';
|
|
129
|
+
tooltip?: string;
|
|
130
|
+
badgeCount?: number;
|
|
131
|
+
badgeIndicatorColor?: string;
|
|
132
|
+
badgeTextColor?: string;
|
|
133
|
+
depth?: number;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const createMenuTitle = (options: MenuTitleOptions) => {
|
|
137
|
+
const {
|
|
138
|
+
title,
|
|
139
|
+
collapsed,
|
|
140
|
+
name = title,
|
|
141
|
+
routeTitle = title,
|
|
142
|
+
renderType = 'item',
|
|
143
|
+
tooltip,
|
|
144
|
+
badgeCount,
|
|
145
|
+
badgeIndicatorColor,
|
|
146
|
+
badgeTextColor,
|
|
147
|
+
depth = 1,
|
|
148
|
+
} = options;
|
|
149
|
+
|
|
150
|
+
return React.createElement(
|
|
151
|
+
FlowEngineProvider,
|
|
152
|
+
{ engine },
|
|
153
|
+
React.createElement(
|
|
154
|
+
MemoryRouter,
|
|
155
|
+
{ initialEntries: ['/admin/current-page'] },
|
|
156
|
+
React.createElement(AdminLayoutMenuItemRenderer, {
|
|
157
|
+
renderType,
|
|
158
|
+
item: {
|
|
159
|
+
name,
|
|
160
|
+
path: '/admin/menu-title',
|
|
161
|
+
_runtimePath: '/apps/demo/v2/admin/menu-title',
|
|
162
|
+
_navigationMode: 'spa',
|
|
163
|
+
_isLegacy: false,
|
|
164
|
+
_depth: depth,
|
|
165
|
+
_route: {
|
|
166
|
+
type: NocoBaseDesktopRouteType.flowPage,
|
|
167
|
+
title: routeTitle,
|
|
168
|
+
tooltip,
|
|
169
|
+
schemaUid: 'menu-title',
|
|
170
|
+
options:
|
|
171
|
+
badgeCount == null
|
|
172
|
+
? {}
|
|
173
|
+
: {
|
|
174
|
+
badge: {
|
|
175
|
+
count: badgeCount,
|
|
176
|
+
styles: badgeIndicatorColor ? { indicator: { color: badgeIndicatorColor } } : undefined,
|
|
177
|
+
textColor: badgeTextColor,
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
dom: React.createElement(
|
|
183
|
+
'span',
|
|
184
|
+
{ className: 'ant-pro-base-menu-inline-item-text' },
|
|
185
|
+
collapsed ? title.slice(0, 1) : title,
|
|
186
|
+
),
|
|
187
|
+
options: { isMobile: false, collapsed },
|
|
188
|
+
}),
|
|
189
|
+
),
|
|
190
|
+
);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const renderMenuTitle = (options: MenuTitleOptions) => render(createMenuTitle(options));
|
|
194
|
+
|
|
195
|
+
it.each([false, true])(
|
|
196
|
+
'should use a native title without an extra tooltip trigger when collapsed is %s',
|
|
197
|
+
async (collapsed) => {
|
|
198
|
+
vi.useFakeTimers();
|
|
199
|
+
const title = 'A complete menu title that may be truncated';
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const { container } = renderMenuTitle({ title, collapsed });
|
|
203
|
+
const link = screen.getByRole('link', { name: title });
|
|
204
|
+
const menuItem = link.closest<HTMLElement>('[role="none"]');
|
|
205
|
+
|
|
206
|
+
expect(link).toHaveAttribute('title', title);
|
|
207
|
+
expect(link.parentElement).toBe(menuItem);
|
|
208
|
+
expect(menuItem).toHaveStyle({ width: '100%', minWidth: 0 });
|
|
209
|
+
expect(link).toHaveStyle({ display: 'block', width: '100%', minWidth: 0, overflow: 'hidden' });
|
|
210
|
+
expect(container.querySelector('.ant-tooltip')).not.toBeInTheDocument();
|
|
211
|
+
|
|
212
|
+
await act(async () => {
|
|
213
|
+
fireEvent.mouseEnter(link);
|
|
214
|
+
await vi.advanceTimersByTimeAsync(500);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
|
|
218
|
+
} finally {
|
|
219
|
+
vi.clearAllTimers();
|
|
220
|
+
vi.useRealTimers();
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
it('should use the translated native title for an icon-wrapped nested menu name', () => {
|
|
226
|
+
const title = 'A translated nested menu title';
|
|
227
|
+
const { name } = buildMenuTitleWithIcon(
|
|
228
|
+
{ title: 'menu.nested.title', icon: 'AppstoreOutlined' },
|
|
229
|
+
() => title,
|
|
230
|
+
true,
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
renderMenuTitle({ title, name, collapsed: true, depth: 2 });
|
|
234
|
+
|
|
235
|
+
expect(screen.getByRole('link', { name: title })).toHaveAttribute('title', title);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should use the translated native title for an icon-wrapped nested group name', () => {
|
|
239
|
+
const title = 'A translated nested group title';
|
|
240
|
+
const { name } = buildMenuTitleWithIcon(
|
|
241
|
+
{ title: 'menu.nested.group', icon: 'AppstoreOutlined' },
|
|
242
|
+
() => title,
|
|
243
|
+
true,
|
|
244
|
+
);
|
|
245
|
+
const { container } = renderMenuTitle({
|
|
246
|
+
title,
|
|
247
|
+
name,
|
|
248
|
+
routeTitle: 'menu.nested.group',
|
|
249
|
+
renderType: 'group',
|
|
250
|
+
collapsed: true,
|
|
251
|
+
depth: 2,
|
|
252
|
+
});
|
|
253
|
+
const group = container.querySelector('[role="none"]');
|
|
254
|
+
|
|
255
|
+
expect(group).toHaveAttribute('title', title);
|
|
256
|
+
expect(group).toHaveAttribute('aria-label', title);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('should preserve a numeric group name as the native title', () => {
|
|
260
|
+
const { container } = renderMenuTitle({
|
|
261
|
+
title: '0',
|
|
262
|
+
name: 0,
|
|
263
|
+
routeTitle: 'menu.numeric.group',
|
|
264
|
+
renderType: 'group',
|
|
265
|
+
collapsed: true,
|
|
266
|
+
});
|
|
267
|
+
const group = container.querySelector('[role="none"]');
|
|
268
|
+
|
|
269
|
+
expect(group).toHaveAttribute('title', '0');
|
|
270
|
+
expect(group).toHaveAttribute('aria-label', '0');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it.each(['item', 'group'] as const)(
|
|
274
|
+
'should let an explicit route tooltip own hover behavior for a sider %s',
|
|
275
|
+
(renderType) => {
|
|
276
|
+
const title = 'A menu title with an explicit description';
|
|
277
|
+
const { container } = renderMenuTitle({
|
|
278
|
+
title,
|
|
279
|
+
renderType,
|
|
280
|
+
collapsed: false,
|
|
281
|
+
tooltip: 'An explicitly configured menu description',
|
|
282
|
+
});
|
|
283
|
+
const titleTarget =
|
|
284
|
+
renderType === 'item' ? screen.getByRole('link', { name: title }) : container.querySelector('[role="none"]');
|
|
285
|
+
|
|
286
|
+
expect(titleTarget).not.toHaveAttribute('title');
|
|
287
|
+
expect(screen.getByRole('img', { name: 'question-circle' })).toBeInTheDocument();
|
|
288
|
+
},
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
it('should keep the same menu link when the sider expands', async () => {
|
|
292
|
+
vi.useFakeTimers();
|
|
293
|
+
const title = 'A title that fits after expanding';
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
const { rerender } = renderMenuTitle({ title, collapsed: true });
|
|
297
|
+
const link = screen.getByRole('link', { name: title });
|
|
298
|
+
|
|
299
|
+
await act(async () => {
|
|
300
|
+
rerender(createMenuTitle({ title, collapsed: false }));
|
|
301
|
+
await vi.runOnlyPendingTimersAsync();
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
expect(screen.getByRole('link', { name: title })).toBe(link);
|
|
305
|
+
expect(link).toHaveAttribute('title', title);
|
|
306
|
+
} finally {
|
|
307
|
+
vi.clearAllTimers();
|
|
308
|
+
vi.useRealTimers();
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it.each([true, false])(
|
|
313
|
+
'should render one badge indicator for a nested menu title when collapsed is %s',
|
|
314
|
+
(collapsed) => {
|
|
315
|
+
const title = 'A nested menu title with a badge';
|
|
316
|
+
const { container } = renderMenuTitle({ title, collapsed, badgeCount: 7 });
|
|
317
|
+
|
|
318
|
+
expect(container.querySelectorAll('.ant-badge-count')).toHaveLength(1);
|
|
319
|
+
},
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
it('should render one badge indicator for a top-level collapsed menu title', () => {
|
|
323
|
+
const title = 'A top-level collapsed menu title with a badge';
|
|
324
|
+
const { container } = renderMenuTitle({
|
|
325
|
+
title,
|
|
326
|
+
collapsed: true,
|
|
327
|
+
badgeCount: 7,
|
|
328
|
+
badgeTextColor: 'rgb(1, 2, 3)',
|
|
329
|
+
depth: 0,
|
|
330
|
+
});
|
|
331
|
+
const indicator = container.querySelector<HTMLElement>('.ant-badge-count');
|
|
332
|
+
|
|
333
|
+
expect(container.querySelectorAll('.ant-badge-count')).toHaveLength(1);
|
|
334
|
+
expect(indicator).toHaveStyle({ color: 'rgb(1, 2, 3)' });
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('should preserve the Ant Design indicator color when collapsed badge textColor is unset', () => {
|
|
338
|
+
const title = 'A top-level collapsed menu title with an indicator color';
|
|
339
|
+
const { container } = renderMenuTitle({
|
|
340
|
+
title,
|
|
341
|
+
collapsed: true,
|
|
342
|
+
badgeCount: 7,
|
|
343
|
+
badgeIndicatorColor: 'rgb(4, 5, 6)',
|
|
344
|
+
depth: 0,
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
expect(container.querySelector('.ant-badge-count')).toHaveStyle({ color: 'rgb(4, 5, 6)' });
|
|
348
|
+
});
|
|
349
|
+
|
|
122
350
|
it('should normalize legacy variables only inside template expressions', () => {
|
|
123
351
|
expect(normalizeAdminLayoutMenuLegacyVariables('https://a.com?id={{ $user.id }}')).toBe(
|
|
124
352
|
'https://a.com?id={{ ctx.user.id }}',
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import React from 'react';
|
|
11
11
|
import { fireEvent, render, screen } from '@testing-library/react';
|
|
12
|
-
import { MemoryRouter } from 'react-router-dom';
|
|
12
|
+
import { MemoryRouter, useLocation } from 'react-router-dom';
|
|
13
13
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
14
14
|
|
|
15
15
|
const { allowMock, appMock, flowModelRendererSpy } = vi.hoisted(() => {
|
|
@@ -270,28 +270,47 @@ describe('TopbarActionsBar helpers', () => {
|
|
|
270
270
|
expect(link).not.toHaveAttribute('target', '_blank');
|
|
271
271
|
});
|
|
272
272
|
|
|
273
|
-
it('
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
273
|
+
it.each(['/nocobase/v', '/v', '/nocobase/v/apps/jhb20'])(
|
|
274
|
+
'should navigate sub-app settings without document navigation with basename %s',
|
|
275
|
+
(basename) => {
|
|
276
|
+
appMock.current.router.getBasename = () => basename;
|
|
277
|
+
const items = getTopbarPluginSettingsItems({
|
|
278
|
+
canManagePlugins: false,
|
|
279
|
+
t: (key) => key,
|
|
280
|
+
settings: [
|
|
281
|
+
{
|
|
282
|
+
key: 'ai',
|
|
283
|
+
name: 'ai',
|
|
284
|
+
title: 'AI employees',
|
|
285
|
+
path: '/admin/settings/ai',
|
|
286
|
+
icon: null,
|
|
287
|
+
componentLoader: async () => null,
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
});
|
|
291
|
+
const item = items[0];
|
|
292
|
+
if (!item || !('label' in item)) {
|
|
293
|
+
throw new Error('Expected settings menu item');
|
|
294
|
+
}
|
|
295
|
+
const appBase = basename.endsWith('/apps/jhb20') ? basename : `${basename}/apps/jhb20`;
|
|
296
|
+
const targetHref = `${appBase}/admin/settings/ai`;
|
|
297
|
+
const LocationDisplay = () => <output aria-label="Current route">{useLocation().pathname}</output>;
|
|
298
|
+
render(
|
|
299
|
+
<MemoryRouter basename={basename} initialEntries={[`${appBase}/admin/a3pq1t1773a`]}>
|
|
300
|
+
{item.label}
|
|
301
|
+
<LocationDisplay />
|
|
302
|
+
</MemoryRouter>,
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
const link = screen.getByRole('link', { name: 'AI employees' });
|
|
306
|
+
expect(link).toHaveAttribute('href', targetHref);
|
|
307
|
+
expect(link).not.toHaveAttribute('target', '_blank');
|
|
308
|
+
const click = new MouseEvent('click', { bubbles: true, cancelable: true, button: 0 });
|
|
309
|
+
fireEvent(link, click);
|
|
310
|
+
expect(click.defaultPrevented).toBe(true);
|
|
311
|
+
expect(screen.getByLabelText('Current route')).toHaveTextContent(targetHref.slice(basename.length));
|
|
312
|
+
},
|
|
313
|
+
);
|
|
295
314
|
|
|
296
315
|
it('should not treat admin-like paths as admin runtime', () => {
|
|
297
316
|
const items = getTopbarPluginSettingsItems({
|