Skip to content

Commit

Permalink
feat(): evaluate menu title expressions
Browse files Browse the repository at this point in the history
  • Loading branch information
weareoutman committed Dec 27, 2024
1 parent 5780327 commit 596a06e
Show file tree
Hide file tree
Showing 6 changed files with 426 additions and 17 deletions.
1 change: 1 addition & 0 deletions bricks/nav/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ import "./directory-tree/directory-tree-internal-node/index.js";
import "./nav-logo/index.js";
import "./poll-announce/index.js";
import "./data-providers/get-menu-config-tree.js";
import "./data-providers/get-menu-config-options.js";
167 changes: 167 additions & 0 deletions bricks/nav/src/data-providers/get-menu-config-options.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { describe, test, expect } from "@jest/globals";
import { initializeI18n } from "@next-core/i18n";
import type { MicroApp } from "@next-core/types";
import { getMenuConfigOptions } from "./get-menu-config-options.js";
import type { MenuRawData } from "./get-menu-config-tree.js";

initializeI18n();

const mockError = jest.spyOn(console, "error");

describe("getMenuConfigOptions", () => {
test("should work", async () => {
const menuList: MenuRawData[] = [
{
menuId: "menu-a",
title: "<% I18N('MENU_A') %>",
type: "main",
app: [{ appId: "app-a" }],
instanceId: "i-a",
i18n: {
en: {
MENU_A: "Menu A",
},
},
},
{
menuId: "menu-b",
title: "<% I18N('MENU_B', 'Default Menu B') %>",
type: "main",
app: [{ appId: "app-a" }],
instanceId: "i-a",
},
{
menuId: "menu-c",
title: "<% I18N('MENU_C') %>",
type: "main",
app: [{ appId: "app-a" }],
instanceId: "i-a",
},
{
menuId: "menu-d",
title: undefined!,
type: "main",
app: [{ appId: "app-a" }],
instanceId: "i-a",
},
{
menuId: "menu-e",
title: "<% APP.localeName %>",
type: "main",
app: [{ appId: "app-a" }],
instanceId: "i-a",
overrideApp: {
name: "App A",
} as MicroApp,
},
{
menuId: "menu-f",
title: "<% `${CTX.name} - ${I18n('')}` %>",
type: "main",
app: [{ appId: "app-a" }],
instanceId: "i-a",
},
];
expect(await getMenuConfigOptions(menuList)).toEqual([
{
label: "Menu A (menu-a)",
value: "menu-a",
},
{
label: "Default Menu B (menu-b)",
value: "menu-b",
},
{
label: "MENU_C (menu-c)",
value: "menu-c",
},
{
label: " (menu-d)",
value: "menu-d",
},
{
label: "App A (menu-e)",
value: "menu-e",
},
{
label: "<% `${CTX.name} - ${I18n('')}` %> (menu-f)",
value: "menu-f",
},
]);
});

test("syntax error in expression", async () => {
mockError.mockReturnValueOnce();
expect(
await getMenuConfigOptions([
{
menuId: "menu-b",
title: "<% I18N('MENU_B' %>",
type: "main",
app: [{ appId: "app-a" }],
instanceId: "i-a",
},
])
).toEqual([
{
label: "<% I18N('MENU_B' %> (menu-b)",
value: "menu-b",
},
]);
expect(mockError).toBeCalledTimes(1);
expect(mockError).toBeCalledWith(
"Parse menu title expression \"<% I18N('MENU_B' %>\" failed:",
expect.any(SyntaxError)
);
});

test("expression evaluates error", async () => {
mockError.mockReturnValueOnce();
expect(
await getMenuConfigOptions([
{
menuId: "menu-b",
title: "<% I18N[0][0] %>",
type: "main",
app: [{ appId: "app-a" }],
instanceId: "i-a",
},
])
).toEqual([
{
label: "<% I18N[0][0] %> (menu-b)",
value: "menu-b",
},
]);
expect(mockError).toBeCalledTimes(1);
expect(mockError).toBeCalledWith(
'Evaluate menu title expression "<% I18N[0][0] %>" failed:',
expect.any(TypeError)
);
});

test("expression to non-string", async () => {
mockError.mockReturnValueOnce();
expect(
await getMenuConfigOptions([
{
menuId: "menu-b",
title: "<% {} %>",
type: "main",
app: [{ appId: "app-a" }],
instanceId: "i-a",
},
])
).toEqual([
{
label: "<% {} %> (menu-b)",
value: "menu-b",
},
]);
expect(mockError).toBeCalledTimes(1);
expect(mockError).toBeCalledWith(
'The result of menu title expression "<% {} %>" is not a string:',
{}
);
});
});
43 changes: 43 additions & 0 deletions bricks/nav/src/data-providers/get-menu-config-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { createProviderClass } from "@next-core/utils/general";
import { i18n } from "@next-core/i18n";
import type { MenuRawData } from "./get-menu-config-tree";
import { smartDisplayForMenuTitle } from "./shared/smartDisplayForMenuTitle";

export interface MenuOption {
label: string;
value: string;
}

/**
* 构造用于菜单自定义的下拉选项数据。
*
* 将对菜单标题进行表达式解析,支持 I8N 和 APP。
*/
export async function getMenuConfigOptions(
menuList: MenuRawData[]
): Promise<MenuOption[]> {
const options: MenuOption[] = [];

for (const menu of menuList) {
if (menu.type === "main") {
const menuI18nNamespace = `customize-menu/${menu.menuId}~${menu.app[0].appId}+${
menu.instanceId
}`;
// Support any language in `menu.i18n`.
Object.entries(menu.i18n ?? {}).forEach(([lang, resources]) => {
i18n.addResourceBundle(lang, menuI18nNamespace, resources);
});
options.push({
label: `${smartDisplayForMenuTitle(menu.title, menuI18nNamespace, menu.overrideApp) ?? ""} (${menu.menuId})`,
value: menu.menuId,
});
}
}

return options;
}

customElements.define(
"nav.get-menu-config-options",
createProviderClass(getMenuConfigOptions)
);
35 changes: 32 additions & 3 deletions bricks/nav/src/data-providers/get-menu-config-tree.spec.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import { describe, test, expect } from "@jest/globals";
import { initializeI18n } from "@next-core/i18n";
import { getMenuConfigTree, type MenuRawData } from "./get-menu-config-tree.js";

initializeI18n();

describe("getMenuConfigTree", () => {
test("should work", async () => {
const menuList: MenuRawData[] = [
{
menuId: "menu-a",
title: "<% 'Menu A' %>",
type: "main",
app: [{ appId: "app-a" }],
instanceId: "i-a",
items: [
{
text: "Menu A - Item 1",
sort: 10,
icon: {
imgSrc: '<% IMG.get("...") %>',
},
},
{
text: "Menu A - Item 2",
Expand Down Expand Up @@ -42,6 +50,8 @@ describe("getMenuConfigTree", () => {
menuId: "menu-a",
title: "Inject Menu A",
type: "inject",
app: [{ appId: "app-a" }],
instanceId: "i-a",
items: [
{
text: "<% `Menu A - ${I18N('ITEM_0')}` %>",
Expand All @@ -55,12 +65,19 @@ describe("getMenuConfigTree", () => {
sort: 25,
},
],
i18n: {
en: {
ITEM_0: "Item 0",
},
},
},
{
menuId: "menu-a",
title: "Inject Menu A",
injectMenuGroupId: "group-x",
type: "inject",
app: [{ appId: "app-a" }],
instanceId: "i-a",
items: [
{
text: "Group X - ii",
Expand All @@ -74,6 +91,8 @@ describe("getMenuConfigTree", () => {
title: "Inject Menu A",
injectMenuGroupId: "group-x",
type: "inject",
app: [{ appId: "app-a" }],
instanceId: "i-a",
items: [
{
text: "Group X - iii",
Expand All @@ -85,16 +104,23 @@ describe("getMenuConfigTree", () => {
menuId: "menu-a",
title: "Inject Menu A",
type: "inject",
app: [{ appId: "app-a" }],
instanceId: "i-a",
dynamicItems: true,
itemsResolve: {},
},
{
menuId: "menu-a",
title: "Inject Menu A",
type: "inject",
app: [{ appId: "app-a" }],
instanceId: "i-a",
},
];
expect(await getMenuConfigTree(menuList)).toEqual([
// Use JSON to remove the symbol properties.
expect(
JSON.parse(JSON.stringify(await getMenuConfigTree(menuList)))
).toEqual([
{
__keys: [
"0",
Expand Down Expand Up @@ -123,14 +149,17 @@ describe("getMenuConfigTree", () => {
lib: "fa",
},
key: "0-0",
title: "ITEM_0",
title: "Menu A - Item 0",
},
{
children: undefined,
data: {
children: [],
sort: 10,
text: "Menu A - Item 1",
icon: {
imgSrc: '<% IMG.get("...") %>',
},
},
faded: undefined,
icon: {
Expand Down Expand Up @@ -305,7 +334,7 @@ describe("getMenuConfigTree", () => {
lib: "fa",
},
key: "0",
title: "<% 'Menu A' %>",
title: "Menu A",
},
]);
});
Expand Down
Loading

0 comments on commit 596a06e

Please sign in to comment.