Skip to content

Commit

Permalink
feat(web): UI扩展增加导航栏链接自动刷新 (#1391)
Browse files Browse the repository at this point in the history
允许UI扩展指定应定时自动刷新自己的导航栏链接。详情见文档。
  • Loading branch information
ddadaal authored Aug 13, 2024
1 parent 15a7bdd commit eec12d8
Show file tree
Hide file tree
Showing 11 changed files with 185 additions and 92 deletions.
6 changes: 6 additions & 0 deletions .changeset/bright-cameras-matter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@scow/lib-web": patch
"@scow/docs": patch
---

UI扩展增加导航栏链接自动刷新功能
29 changes: 20 additions & 9 deletions docs/docs/integration/ui-extension/develop.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,19 @@ UI扩展的功能应实现为标准的网页。当访问SCOW的扩展路径时

对于此接口,您需要返回如下类型的JSON内容:

| JSON属性路径 | 类型 | 是否必须 | 解释 |
| --------------------------- | ------ | -------- | --------------------------------------------------- |
| `portal` | 对象 || 关于门户系统的配置 |
| `portal.rewriteNavigations` | 布尔值 || 是否重写门户系统的导航项。默认为`false` |
| `portal.navbarLinks` | 布尔值 || 是否在门户系统中增加导航栏右侧的链接。默认为`false` |
| `mis` | 对象 || 关于管理系统的配置 |
| `mis.rewriteNavigations` | 布尔值 || 是否重写管理系统的导航项。默认为`false` |
| `mis.navbarLinks` | 布尔值 || 是否在管理系统中增加导航栏右侧的链接。默认为`false` |
| JSON属性路径 | 类型 | 是否必须 | 解释 |
| ------------------------------------------- | -------------- | -------- | ------------------------------------------------------ |
| `portal` | 对象 || 关于门户系统的配置 |
| `portal.rewriteNavigations` | 布尔值 || 是否重写门户系统的导航项。默认为`false` |
| `portal.navbarLinks` | 布尔值或者对象 || 是否在门户系统中增加导航栏右侧的链接。默认为`false` |
| `portal.navbarLinks.enabled` | 布尔值 || 是否在管理系统中增加导航栏右侧的链接。默认为`false` |
| `portal.navbarLinks.autoRefresh` | 对象 || 是否定时自动刷新导航栏右侧的链接。不设置为不定时刷新。 |
| `portal.navbarLinks.autoRefresh.enabled` | 布尔值 || 是否定时自动刷新导航栏右侧的链接。默认为`false` |
| `portal.navbarLinks.autoRefresh.intervalMs` | 数字 || 定时刷新导航栏右侧的链接的间隔,单位`ms` |

例如,您可以返回如下类型的JSON,表示在门户系统中重写导航项并增加导航栏右侧的链接,在管理系统中不重写管理系统的导航项,也不增加导航栏右侧的链接。
将路径中的`portal`改为`mis`即设置管理系统的配置,门户系统和管理系统的配置完全一致。

例如,您可以返回如下类型的JSON,表示在门户系统中重写导航项并增加导航栏右侧的链接,在管理系统中不重写管理系统的导航项,增加导航栏右侧的链接,并且开启自动刷新,每5s自动刷新一次。

```json
{
Expand All @@ -60,6 +63,13 @@ UI扩展的功能应实现为标准的网页。当访问SCOW的扩展路径时
},
"mis": {
"rewriteNavigations": false,
"navbarLinks": {
"enabled": true,
"autoRefresh": {
"enabled": true,
"intervalMs": 5000,
}
}
}
}
```
Expand Down Expand Up @@ -163,6 +173,7 @@ SCOW在调用接口时,会将[上下文参数](#上下文参数)作为查询
#### 其他注意事项

- 当右上角导航栏链接数量**大于等于5个**,或者屏幕宽度小于**768px**时,所有导航栏链接将会仅显示图标。
- 如果您开启了导航栏链接自动刷新功能,并希望每次都重新加载图标,请确保每次返回的图标的链接要有变化,否则浏览器缓存将不会重新刷新图标。

## 扩展消息

Expand Down
16 changes: 6 additions & 10 deletions libs/web/src/extensions/ExtensionPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { joinWithUrl } from "@scow/utils";
import { useRouter } from "next/router";
import React, { useEffect, useRef } from "react";
import { Head } from "src/components/head";
import { getExtensionRouteQuery } from "src/extensions/common";
import { extensionEvents } from "src/extensions/events";
import { ExtensionManifestWithUrl, UiExtensionStoreData } from "src/extensions/UiExtensionStore";
import { UserInfo } from "src/layouts/base/types";
Expand Down Expand Up @@ -79,17 +80,12 @@ export const ExtensionPage: React.FC<Props> = ({

const darkMode = useDarkMode();

const query = new URLSearchParams(
Object.fromEntries(Object.entries(rest).filter(([_, val]) => typeof val === "string")) as Record<string, string>,
);

if (user) {
query.set("scowUserToken", user.token);
}

query.set("scowDark", darkMode.dark ? "true" : "false");
const extensionQuery = getExtensionRouteQuery(darkMode.dark, currentLanguageId, user?.token);

query.set("scowLangId", currentLanguageId);
const query = new URLSearchParams({
...Object.fromEntries(Object.entries(rest).filter(([_, val]) => typeof val === "string")),
...extensionQuery,
});

const url = joinWithUrl(config.url, "extensions", ...pathParts)
+ "?" + query.toString();
Expand Down
5 changes: 3 additions & 2 deletions libs/web/src/extensions/UiExtensionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,16 @@
import { UiExtensionConfigSchema } from "@scow/config/build/uiExtensions";
import { useCallback } from "react";
import { useAsync } from "react-async";
import { fetchExtensionManifests } from "src/extensions/manifests";
import { ExtensionManifestsSchema, fetchExtensionManifests } from "src/extensions/manifests";

const fetchManifestsWithErrorHandling = (url: string, name?: string): Promise<ExtensionManifestWithUrl | undefined> =>
fetchExtensionManifests(url)
.then((x) => ({ url, manifests: x, name }))
.catch((e) => { console.error(`Error fetching extension manifests. ${e}`); return undefined; });

export interface ExtensionManifestWithUrl {
url: string; manifests: Awaited<ReturnType<typeof fetchExtensionManifests>>
url: string;
manifests: ExtensionManifestsSchema;
name?: string;
}

Expand Down
9 changes: 7 additions & 2 deletions libs/web/src/extensions/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@

import { z } from "zod";

export const ScowExtensionRouteContext = z.object({
export const ExtensionRouteQuery = z.object({
scowUserToken: z.string().optional(),
scowDark: z.enum(["true", "false"]),
scowLangId: z.string(),
});

export type ScowExtensionRouteContext = z.infer<typeof ScowExtensionRouteContext>;
export type ExtensionRouteQuery = z.infer<typeof ExtensionRouteQuery>;

export function isUrl(input: string): boolean {
try {
Expand All @@ -29,3 +29,8 @@ export function isUrl(input: string): boolean {
}
}

export const getExtensionRouteQuery = (dark: boolean, languageId: string, userToken?: string) => ({
scowDark: dark ? "true" : "false",
scowLangId: languageId,
...userToken ? { scowUserToken: userToken } : {},
}) as ExtensionRouteQuery;
13 changes: 12 additions & 1 deletion libs/web/src/extensions/manifests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,18 @@ import { z } from "zod";

export const CommonExtensionManifestsSchema = z.object({
rewriteNavigations: z.boolean().default(false),
navbarLinks: z.boolean().default(false),

navbarLinks: z.union([
z.boolean(),
z.object({
enabled: z.boolean().default(false),
autoRefresh: z.optional(z.object({
enabled: z.boolean().default(false),
intervalMs: z.number(),
})),
}),
]).default(false),

});

export const ExtensionManifestsSchema = z.object({
Expand Down
7 changes: 5 additions & 2 deletions libs/web/src/extensions/navbarLinks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* See the Mulan PSL v2 for more details.
*/

import { ScowExtensionRouteContext } from "src/extensions/common";
import { ExtensionRouteQuery } from "src/extensions/common";
import { defineExtensionRoute } from "src/extensions/routes";
import { z } from "zod";

Expand All @@ -23,14 +23,17 @@ export const NavbarLink = z.object({
})),
openInNewPage: z.boolean().default(true),
priority: z.number().default(0),
autoRefresh: z.optional(z.object({
intervalMs: z.number(),
})),
});

export type NavbarLink = z.infer<typeof NavbarLink>;

export const navbarLinksRoute = (from: "portal" | "mis") => defineExtensionRoute({
path: `/${from}/navbarLinks`,
method: "POST" as const,
query: ScowExtensionRouteContext,
query: ExtensionRouteQuery,
responses: {
200: z.object({
navbarLinks: z.optional(z.array(NavbarLink)),
Expand Down
4 changes: 2 additions & 2 deletions libs/web/src/extensions/navigations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

import { LinkOutlined } from "@ant-design/icons";
import { join } from "path";
import { isUrl, ScowExtensionRouteContext } from "src/extensions/common";
import { ExtensionRouteQuery,isUrl } from "src/extensions/common";
import { defineExtensionRoute } from "src/extensions/routes";
import { NavItemProps } from "src/layouts/base/types";
import { NavIcon } from "src/layouts/icon";
Expand Down Expand Up @@ -41,7 +41,7 @@ export const NavItem = BaseNavItem.extend({
export const rewriteNavigationsRoute = (from: "portal" | "mis") => defineExtensionRoute({
path: `/${from}/rewriteNavigations`,
method: "POST" as const,
query: ScowExtensionRouteContext,
query: ExtensionRouteQuery,
body: z.object({
navs: z.array(NavItem) as z.ZodType<NavItem[]>,
}),
Expand Down
69 changes: 10 additions & 59 deletions libs/web/src/layouts/base/BaseLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,12 @@

"use client";

import { LinkOutlined } from "@ant-design/icons";
import { arrayContainsElement } from "@scow/utils";
import { Grid, Layout } from "antd";
import { useRouter } from "next/router";
import { join } from "path";
import React, { PropsWithChildren, useCallback, useMemo, useState } from "react";
import { useAsync } from "react-async";
import { isUrl, ScowExtensionRouteContext } from "src/extensions/common";
import { NavbarLink, navbarLinksRoute } from "src/extensions/navbarLinks";
import { getExtensionRouteQuery } from "src/extensions/common";
import { fromNavItemProps, rewriteNavigationsRoute, toNavItemProps } from "src/extensions/navigations";
import { callExtensionRoute } from "src/extensions/routes";
import { UiExtensionStoreData } from "src/extensions/UiExtensionStore";
Expand All @@ -30,7 +27,6 @@ import { Header, HeaderNavbarLink } from "src/layouts/base/header";
import { SideNav } from "src/layouts/base/SideNav";
import { NavItemProps, UserInfo, UserLink } from "src/layouts/base/types";
import { useDarkMode } from "src/layouts/darkMode";
import { NavIcon } from "src/layouts/icon";
import { styled } from "styled-components";
// import logo from "src/assets/logo-no-text.svg";
const { useBreakpoint } = Grid;
Expand Down Expand Up @@ -93,11 +89,11 @@ export const BaseLayout: React.FC<PropsWithChildren<Props>> = ({
? extensionStoreData
: extensionStoreData ? [extensionStoreData] : [], [extensionStoreData]);

const routeQuery = useMemo(() => ({
scowDark: dark.dark ? "true" : "false",
scowLangId: languageId,
scowUserToken: user?.token,
}) as ScowExtensionRouteContext, [dark.dark, languageId, user?.token]);
const routeQuery = useMemo(() => getExtensionRouteQuery(
dark.dark,
languageId,
user?.token,
), [dark.dark, languageId, user?.token]);

const { data: finalRoutesData } = useAsync({
promiseFn: useCallback(async () => {
Expand Down Expand Up @@ -138,57 +134,11 @@ export const BaseLayout: React.FC<PropsWithChildren<Props>> = ({

const hasSidebar = arrayContainsElement(sidebarRoutes);


// navbar links
const { data: extensionNavbarLinks } = useAsync({
promiseFn: useCallback(async () => {
if (extensions.length === 0) { return undefined; }

const result = await Promise.all(extensions.map(async (extension) => {
const resp = await callExtensionRoute(navbarLinksRoute(from), routeQuery, {}, extension.url)
.catch((e) => {
console.warn(`Failed to call navbarLinks of extension ${extension.name ?? extension.url}. Error: `, e);
return { 200: { navbarLinks: [] as NavbarLink[] } };
});

if (resp[200]) {
return resp[200].navbarLinks?.map((x) => {

if (!isUrl(x.path)) {
const parts = ["/extensions"];

if (extension.name) {
parts.push(extension.name);
}

parts.push(x.path);
x.path = join(...parts);
}

return x;
});
}
}));

const filtered = result.flat().filter((x) => x) as NavbarLink[];

// order by priority and index. sort is stable, index is preserved
filtered.sort((a, b) => {
return b.priority - a.priority;
});

return filtered.map((x) => ({
href: x.path,
text: x.text,
icon: x.icon ? <NavIcon src={x.icon.src} alt={x.icon.alt ?? ""} /> : <LinkOutlined />,
}satisfies HeaderNavbarLink));

}, [from, routeQuery, extensions]),
});

return (
<Root>
<Header
extensions={extensions}
routeQuery={routeQuery}
setSidebarCollapsed={setSidebarCollapsed}
pathname={router.asPath}
sidebarCollapsed={sidebarCollapsed}
Expand All @@ -200,7 +150,8 @@ export const BaseLayout: React.FC<PropsWithChildren<Props>> = ({
userLinks={userLinks}
languageId={languageId}
right={headerRightContent}
navbarLinks={[...extensionNavbarLinks ?? [], ...headerNavbarLinks ?? []]}
staticNavbarLinks={headerNavbarLinks}
from={from}
activeKeys={activeKeys}
/>
<StyledLayout>
Expand Down
2 changes: 1 addition & 1 deletion libs/web/src/layouts/base/header/BigScreenMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ interface Props {
}

export const BigScreenMenu: React.FC<Props> = ({
routes, className, pathname, activeKeys,
routes, className, activeKeys,
}) => {

const router = useRouter();
Expand Down
Loading

0 comments on commit eec12d8

Please sign in to comment.