Skip to content

Commit

Permalink
feat(web): UI扩展增加自定义导航栏链接 (#1311)
Browse files Browse the repository at this point in the history
  • Loading branch information
ddadaal and tongchong authored Jul 5, 2024
1 parent ca9bf27 commit f14bf6c
Show file tree
Hide file tree
Showing 14 changed files with 373 additions and 132 deletions.
8 changes: 8 additions & 0 deletions .changeset/young-buttons-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@scow/portal-web": patch
"@scow/mis-web": patch
"@scow/lib-web": patch
"@scow/docs": patch
---

UI 扩展增加导航栏链接自定义
51 changes: 29 additions & 22 deletions apps/mis-web/src/layouts/BaseLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import { DesktopOutlined, RobotOutlined } from "@ant-design/icons";
import { UiExtensionStore } from "@scow/lib-web/build/extensions/UiExtensionStore";
import { BaseLayout as LibBaseLayout } from "@scow/lib-web/build/layouts/base/BaseLayout";
import { JumpToAnotherLink } from "@scow/lib-web/build/layouts/base/header/components";
import { HeaderNavbarLink } from "@scow/lib-web/build/layouts/base/header";
import { PropsWithChildren, useMemo } from "react";
import { useStore } from "simstate";
import { LanguageSwitcher } from "src/components/LanguageSwitcher";
Expand Down Expand Up @@ -43,6 +43,28 @@ export const BaseLayout =

const uiExtensionStore = useStore(UiExtensionStore);

const toCallbackPage = (url: string) => userStore.user
? `${url}/api/auth/callback?token=${userStore.user.token}`
: url;

const navbarLinks: HeaderNavbarLink[] = [];

if (publicConfig.PORTAL_URL) {
navbarLinks.push({
icon: <DesktopOutlined style={{ paddingRight: 2 }} />,
href: toCallbackPage(publicConfig.PORTAL_URL),
text: t("layouts.route.navLinkTextPortal"),
});
}

if (publicConfig.AI_URL) {
navbarLinks.push({
icon: <RobotOutlined style={{ paddingRight: 2 }} />,
href: publicConfig.AI_URL,
text: t("layouts.route.navLinkTextAI"),
});
}

return (
<LibBaseLayout
logout={userStore.logout}
Expand All @@ -55,27 +77,12 @@ export const BaseLayout =
from="mis"
extensionStoreData={uiExtensionStore.data}
languageId={languageId}
headerRightContent={(
<>
<JumpToAnotherLink
user={userStore.user}
icon={<DesktopOutlined style={{ paddingRight: 2 }} />}
link={publicConfig.PORTAL_URL}
linkText={t("layouts.route.navLinkTextPortal")}
/>
<JumpToAnotherLink
user={userStore.user}
icon={<RobotOutlined style={{ paddingRight: 2 }} />}
link={publicConfig.AI_URL}
linkText={t("layouts.route.navLinkTextAI")}
/>
{
systemLanguageConfig.isUsingI18n ? (
<LanguageSwitcher initialLanguage={initialLanguage} />
) : undefined
}
</>
)}
headerNavbarLinks={navbarLinks}
headerRightContent={
systemLanguageConfig.isUsingI18n ? (
<LanguageSwitcher initialLanguage={initialLanguage} />
) : undefined
}
>
{children}
</LibBaseLayout>
Expand Down
48 changes: 28 additions & 20 deletions apps/portal-web/src/layouts/BaseLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import { RobotOutlined } from "@ant-design/icons";
import { UiExtensionStore } from "@scow/lib-web/build/extensions/UiExtensionStore";
import { BaseLayout as LibBaseLayout } from "@scow/lib-web/build/layouts/base/BaseLayout";
import { JumpToAnotherLink } from "@scow/lib-web/build/layouts/base/header/components";
import { HeaderNavbarLink } from "@scow/lib-web/build/layouts/base/header";
import { PropsWithChildren } from "react";
import { useStore } from "simstate";
import { LanguageSwitcher } from "src/components/LanguageSwitcher";
Expand Down Expand Up @@ -60,6 +60,29 @@ export const BaseLayout = ({ footerText, versionTag, initialLanguage, children }
userStore.logout();
};

const toCallbackPage = (url: string) => userStore.user
? `${url}/api/auth/callback?token=${userStore.user.token}`
: url;

const navbarLinks: HeaderNavbarLink[] = [];

if (publicConfig.MIS_URL) {
navbarLinks.push({
icon: <MisIcon style={{ paddingRight: 2 }} />,
href: toCallbackPage(publicConfig.MIS_URL),
text: t("baseLayout.linkTextMis"),
});
}

if (publicConfig.AI_URL) {
navbarLinks.push({
icon: <RobotOutlined style={{ paddingRight: 2 }} />,
href: publicConfig.AI_URL,
text: t("baseLayout.linkTextAI"),
});
}


return (
<LibBaseLayout
logout={logout}
Expand All @@ -72,26 +95,11 @@ export const BaseLayout = ({ footerText, versionTag, initialLanguage, children }
languageId={languageId}
extensionStoreData={uiExtensionStore.data}
from="portal"
headerNavbarLinks={navbarLinks}
headerRightContent={(
<>
<JumpToAnotherLink
user={userStore.user}
icon={<MisIcon style={{ paddingRight: 2, stroke:"#9B0000", fill:"#9B0000" }} />}
link={publicConfig.MIS_URL}
linkText={t("baseLayout.linkTextMis")}
/>
<JumpToAnotherLink
user={userStore.user}
icon={<RobotOutlined style={{ paddingRight: 2 }} />}
link={publicConfig.AI_URL}
linkText={t("baseLayout.linkTextAI")}
/>
{
systemLanguageConfig.isUsingI18n ? (
<LanguageSwitcher initialLanguage={initialLanguage} />
) : undefined
}
</>
systemLanguageConfig.isUsingI18n ? (
<LanguageSwitcher initialLanguage={initialLanguage} />
) : undefined
)}
>
{children}
Expand Down
65 changes: 49 additions & 16 deletions docs/docs/integration/ui-extension/develop.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,29 +41,34 @@ UI扩展的功能应实现为标准的网页。当访问SCOW的扩展路径时

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

| JSON属性路径 | 类型 | 是否必须 | 解释 |
| --------------------------- | ------ | -------- | --------------------------------------- |
| `portal` | 对象 || 关于门户系统的配置 |
| `portal.rewriteNavigations` | 布尔值 || 是否重写门户系统的导航项。默认为`false` |
| `mis` | 对象 || 关于管理系统的配置 |
| `mis.rewriteNavigations` | 布尔值 || 是否重写管理系统的导航项。默认为`false` |
| JSON属性路径 | 类型 | 是否必须 | 解释 |
| --------------------------- | ------ | -------- | --------------------------------------------------- |
| `portal` | 对象 || 关于门户系统的配置 |
| `portal.rewriteNavigations` | 布尔值 || 是否重写门户系统的导航项。默认为`false` |
| `portal.navbarLinks` | 布尔值 || 是否在门户系统中增加导航栏右侧的链接。默认为`false` |
| `mis` | 对象 || 关于管理系统的配置 |
| `mis.rewriteNavigations` | 布尔值 || 是否重写管理系统的导航项。默认为`false` |
| `mis.navbarLinks` | 布尔值 || 是否在管理系统中增加导航栏右侧的链接。默认为`false` |

例如,您可以返回如下类型的JSON,表示要重写门户系统的导航项,但是不重写管理系统的导航项
例如,您可以返回如下类型的JSON,表示在门户系统中重写导航项并增加导航栏右侧的链接,在管理系统中不重写管理系统的导航项,也不增加导航栏右侧的链接

```json
{
"portal": {
"rewriteNavigations": true
"rewriteNavigations": true,
"navbarLinks": true
},
"mis": {
"rewriteNavigations": false,
}
}
```

### 重写门户系统的导航项:POST /api/portal/rewriteNavigations
### 重写导航项:POST /api/\{portal,mis\}/rewriteNavigations

重写门户系统的导航项。若您在`GET /api/manifests`中返回的`portal.rewriteNavigations``true`,则必须实现此接口。


重写门户(portal)或者管理系统(mis)的导航项。若您在`GET /api/manifests`中返回的`{portal/mis}.rewriteNavigations``true`,则必须实现对应的接口。

SCOW将会在body中传入默认情况下SCOW将会显示的导航项。下表为传入的JSON参数的属性。除此表之外,[上下文参数](#上下文参数)同样也会被作为查询字符串传入。

Expand All @@ -76,7 +81,7 @@ SCOW将会在body中传入默认情况下SCOW将会显示的导航项。下表
| `navs[].openInNewPage` | 布尔值 || 此导航项的页面是否在新窗口中打开 |
| `navs[].children` | 对象数组,类型与`navs`数组的每一项相同 || 此导航项的子项。 |

您需要返回以下类型的JSON,表示重写后的门户系统的导航项。您可以重写系统默认导航项的属性。
您需要返回以下类型的JSON,表示重写后的导航项。您可以重写系统默认导航项的属性。

| JSON属性路径 | 类型 | 是否必须 | 解释 |
| ---------------------- | -------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
Expand All @@ -86,8 +91,8 @@ SCOW将会在body中传入默认情况下SCOW将会显示的导航项。下表
| `navs[].text` | 字符串 || 导航项的文本 |
| `navs[].icon` | 对象 || 导航项的图标信息。如果不填,将显示默认导航项中具有相同的path的导航项的图标。如果不存在具有相同path的导航项,将显示[Ant Design Icon](https://ant.design/components/icon-cn)`LinkOutlined` |
| `navs[].icon.src` | 图标URL || 导航项的图标地址。必须是完整的、可公开访问的URL |
| `navs[].icon.alt` | 布尔值 || 导航项的图标alt属性。可不填 |
| `navs[].openInNewPage` | 布尔值 | | 此导航项的页面是否在新窗口中打开 |
| `navs[].icon.alt` | 文本 || 导航项的图标alt属性。可不填 |
| `navs[].openInNewPage` | 布尔值 | | 此导航项的页面是否在新窗口中打开,默认`false` |
| `navs[].children` | 对象数组,类型与`navs`数组的每一项相同 || 此导航项的子项。 |

关于返回的路径的说明:
Expand All @@ -106,11 +111,39 @@ SCOW将会在body中传入默认情况下SCOW将会显示的导航项。下表

如果配置了多个UI扩展,那么SCOW将会按照配置中的顺序依次调用每个需要重写导航项的UI扩展的此接口,并将上一个UI扩展的输出作为下一个UI扩展的输入,并将最终结果作为SCOW的导航项。

#### 重写管理系统的导航项:POST /api/mis/rewriteNavigations
### 增加导航栏链接:POST /api/\{portal,mis\}/navbarLinks

:::note

在1.6.2版本中可用。

:::

在门户(portal)或者管理系统(mis)导航栏右侧上部显示其他链接。若您在`GET /api/manifests`中返回的`{portal/mis}.navbarLinks``true`,则必须实现对应的接口。

SCOW在调用接口时,会将[上下文参数](#上下文参数)作为查询字符串传入。

您需要返回以下类型的JSON,表示需要增加的导航栏的链接。

| JSON属性路径 | 类型 | 是否必须 | 解释 |
| ----------------------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------- |
| `navbarLinks` | 对象数组 || 导航项 |
| `navbarLinks[].href` | 字符串 || 点击此导链接跳转的href,将会被直接填入`<a>`元素的`href`属性 |
| `navbarLinks[].text` | 字符串 || 导航项的文本 |
| `navbarLinks[].icon` | 对象 || 导航项的图标信息。如果不填,将显示[Ant Design Icon](https://ant.design/components/icon-cn)`LinkOutlined` |
| `navbarLinks[].icon.src` | 图标URL || 导航项的图标地址。必须是完整的、可公开访问的URL |
| `navbarLinks[].icon.alt` | 文本 || 导航项的图标alt属性。可不填 |
| `navbarLinks[].openInNewPage` | 布尔值 || 此导航项的页面是否在新窗口中打开,默认`true` |
| `navbarLinks[].priority` | 数字 || 此链接的优先级。默认为0. |

如果配置了多个UI扩展,那么SCOW将会按照配置中的顺序依次调用每个需要增加导航栏链接的UI扩展的此接口,并将获得的所有链接按以下规则**从左到右**排列:

重写门户系统的导航项。若您在`GET /api/manifests`中返回的`mis.rewriteNavigations``true`,则必须实现此接口。
- 优先级(`priority`)属性从大到小
- 当优先级相同时,依照获取此链接的顺序,也即
- 返回链接的UI扩展在配置中的顺序从前往后
- 同一个UI扩展返回的链接在响应中的列表的顺序从前往后

此接口的参数及响应与门户系统的完全相同
注意,当右上角导航栏链接数量**大于等于5个**,或者屏幕宽度小于**768px**时,所有导航栏链接将会仅显示图标

## 注意事项

Expand Down
Binary file modified docs/docs/integration/ui-extension/extension.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 5 additions & 2 deletions docs/src/components/HomepageFeatures.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,12 @@ const FeatureList: FeatureItem[] = [
通过
<Link href="/docs/integration/scow-api-hook/api">SCOW API</Link>
调用SCOW API,<br/>
或者通过
通过
<Link href="/docs/integration/scow-api-hook/hook">SCOW Hook</Link>
监听SCOW事件
监听SCOW事件,<br/>
使用
<Link href="/docs/integration/ui-extension">UI扩展</Link>
将您的系统集成进SCOW。
</p>
),
},
Expand Down
31 changes: 31 additions & 0 deletions libs/web/src/extensions/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy
* SCOW is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/

import { z } from "zod";

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

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

export function isUrl(input: string): boolean {
try {
new URL(input);
return true;
} catch {
return false;
}
}

1 change: 1 addition & 0 deletions libs/web/src/extensions/manifests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { z } from "zod";

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

export const ExtensionManifestsSchema = z.object({
Expand Down
40 changes: 40 additions & 0 deletions libs/web/src/extensions/navbarLinks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Copyright (c) 2022 Peking University and Peking University Institute for Computing and Digital Economy
* SCOW is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/

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

export const NavbarLink = z.object({
href: z.string({ description: "链接的目标路径,将会被直接填入<a>标签的href" }),
text: z.string(),
icon: z.optional(z.object({
src: z.string(),
alt: z.string().optional(),
})),
openInNewPage: z.boolean().default(true),
priority: z.number().default(0),
});

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

export const navbarLinksRoute = (from: "portal" | "mis") => defineExtensionRoute({
path: `/${from}/navbarLinks`,
method: "POST" as const,
query: ScowExtensionRouteContext,
responses: {
200: z.object({
navbarLinks: z.optional(z.array(NavbarLink)),
}),
},
});

16 changes: 2 additions & 14 deletions libs/web/src/extensions/navigations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import { LinkOutlined } from "@ant-design/icons";
import { join } from "path";
import { isUrl, ScowExtensionRouteContext } 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 @@ -39,11 +40,7 @@ export const NavItem = BaseNavItem.extend({
export const rewriteNavigationsRoute = (from: "portal" | "mis") => defineExtensionRoute({
path: `/${from}/rewriteNavigations`,
method: "POST" as const,
query: z.object({
scowUserToken: z.string().optional(),
scowDark: z.string(),
scowLangId: z.string(),
}),
query: ScowExtensionRouteContext,
body: z.object({
navs: z.array(NavItem) as z.ZodType<NavItem[]>,
}),
Expand All @@ -65,15 +62,6 @@ export const fromNavItemProps = (props: NavItemProps[]): NavItem[] => {
}));
};

function isUrl(input: string): boolean {
try {
new URL(input);
return true;
} catch {
return false;
}
}

/**
* 将Extension API返回的导航栏转换为用于渲染的导航栏
* @param originalItems 传给Extension API的原始导航栏
Expand Down
Loading

0 comments on commit f14bf6c

Please sign in to comment.