chore(hera-core): 迁移 hera-core 的通知、移动端入口到其他插件 (#1571)

Co-authored-by: sealday <sealday@gmail.com>
Reviewed-on: daoyoucloud/tachybase#1571
This commit is contained in:
sealday 2024-09-26 21:17:49 +08:00
parent f411efb1bc
commit 2425e43621
41 changed files with 1667 additions and 550 deletions

View File

@ -26,6 +26,8 @@ import {
} from '../..'; } from '../..';
import { Plugin } from '../../application/Plugin'; import { Plugin } from '../../application/Plugin';
import { VariablesProvider } from '../../variables'; import { VariablesProvider } from '../../variables';
import { PageTab } from '../page-style/PageTab';
import { usePageStyle } from '../page-style/usePageStyle';
import { NoticeArea } from './NoticeArea'; import { NoticeArea } from './NoticeArea';
const useStyles = createStyles(({ css, token }) => { const useStyles = createStyles(({ css, token }) => {
@ -407,6 +409,7 @@ export const InternalAdminLayout = (props: any) => {
const app = useApp(); const app = useApp();
const params = useParams<any>(); const params = useParams<any>();
const { styles } = useStyles(); const { styles } = useStyles();
const pageStyle = usePageStyle();
return ( return (
<Layout> <Layout>
<GlobalStyleForAdminLayout /> <GlobalStyleForAdminLayout />
@ -447,7 +450,7 @@ export const InternalAdminLayout = (props: any) => {
{params.name && <Layout.Sider className={styles.sider} theme={'light'} ref={sideMenuRef}></Layout.Sider>} {params.name && <Layout.Sider className={styles.sider} theme={'light'} ref={sideMenuRef}></Layout.Sider>}
<Layout.Content className={styles.main}> <Layout.Content className={styles.main}>
<header className={styles.mainHeader}></header> <header className={styles.mainHeader}></header>
<Outlet /> {params.name && pageStyle === 'tab' ? <PageTab /> : <Outlet />}
</Layout.Content> </Layout.Content>
</Layout> </Layout>
); );

View File

@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next';
import { Navigate, useNavigate } from 'react-router-dom'; import { Navigate, useNavigate } from 'react-router-dom';
import { useAPIClient } from '../api-client'; import { useAPIClient } from '../api-client';
import { Application, AttachmentPreviewManager, NoticeLevel, useApp } from '../application'; import { Application, NoticeLevel, useApp } from '../application';
import { Plugin } from '../application/Plugin'; import { Plugin } from '../application/Plugin';
import { BlockSchemaComponentPlugin } from '../block-provider'; import { BlockSchemaComponentPlugin } from '../block-provider';
import { CollectionPlugin } from '../collection-manager'; import { CollectionPlugin } from '../collection-manager';
@ -24,15 +24,14 @@ import { ACLPlugin } from './acl';
import { AdminLayoutPlugin } from './admin-layout'; import { AdminLayoutPlugin } from './admin-layout';
import { PluginAssistant } from './assistant'; import { PluginAssistant } from './assistant';
import { AttachmentPreviewPlugin } from './attachment-preview'; import { AttachmentPreviewPlugin } from './attachment-preview';
import { PluginBuiltInCollections } from './built-in-collections';
import { PluginContextMenu } from './context-menu'; import { PluginContextMenu } from './context-menu';
import { RemoteDocumentTitlePlugin } from './document-title'; import { RemoteDocumentTitlePlugin } from './document-title';
import { LocalePlugin } from './locale/LocalePlugin'; import { LocalePlugin } from './locale/LocalePlugin';
import { PluginPageStyle } from './page-style';
import { PinnedListPlugin } from './pinned-list'; import { PinnedListPlugin } from './pinned-list';
import { PMPlugin } from './pm'; import { PMPlugin } from './pm';
import { SystemSettingsPlugin } from './system-settings'; import { SystemSettingsPlugin } from './system-settings';
import { UserSettingsPlugin } from './user-settings'; import { UserSettingsPlugin } from './user-settings';
import { PluginWebNotification } from './web-notification/PluginWebNotification';
export { AdminProvider, NoticeArea } from './admin-layout'; export { AdminProvider, NoticeArea } from './admin-layout';
export * from './context-menu'; export * from './context-menu';
@ -358,5 +357,6 @@ export class BuiltInPlugin extends Plugin {
await this.app.pm.add(PluginAssistant, { name: 'drag-assistant' }); await this.app.pm.add(PluginAssistant, { name: 'drag-assistant' });
await this.app.pm.add(UserSettingsPlugin, { name: 'user-settings' }); await this.app.pm.add(UserSettingsPlugin, { name: 'user-settings' });
await this.app.pm.add(AttachmentPreviewPlugin, { name: 'attachment-preview' }); await this.app.pm.add(AttachmentPreviewPlugin, { name: 'attachment-preview' });
await this.app.pm.add(PluginPageStyle, { name: 'page-style' });
} }
} }

View File

@ -1,8 +1,8 @@
import React, { createContext, useEffect, useState } from 'react'; import React, { createContext, useEffect, useState } from 'react';
import { useCurrentUserContext, useCurrentUserSettingsMenu } from '@tachybase/client';
import { TabsProps } from 'antd'; import { TabsProps } from 'antd';
import { useCurrentUserContext, useCurrentUserSettingsMenu } from '../../user';
import { useTabSettings } from './useTabSettings'; import { useTabSettings } from './useTabSettings';
export interface PageStyleContextValue { export interface PageStyleContextValue {

View File

@ -1,9 +1,11 @@
import React, { useContext, useEffect } from 'react'; import React, { useContext, useEffect } from 'react';
import { css, RemoteSchemaComponent, useDocumentTitle } from '@tachybase/client';
import { css } from '@emotion/css';
import { Tabs } from 'antd'; import { Tabs } from 'antd';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { RemoteSchemaComponent } from '../../schema-component';
import { useDocumentTitle } from '../document-title';
import { PageStyleContext } from './PageStyle.provider'; import { PageStyleContext } from './PageStyle.provider';
export const PageTab = () => { export const PageTab = () => {

View File

@ -1,5 +1,4 @@
import { Plugin } from '@tachybase/client'; import { Plugin } from '../../application/Plugin';
import { PageStyleProvider } from './PageStyle.provider'; import { PageStyleProvider } from './PageStyle.provider';
export class PluginPageStyle extends Plugin { export class PluginPageStyle extends Plugin {

View File

@ -1,8 +1,11 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { SelectWithTitle, useAPIClient, useCurrentUserContext } from '@tachybase/client';
import { error } from '@tachybase/utils/client'; import { error } from '@tachybase/utils/client';
import { useTranslation } from '../../locale'; import { useTranslation } from 'react-i18next';
import { useAPIClient } from '../../api-client';
import { SelectWithTitle } from '../../common';
import { useCurrentUserContext } from '../../user';
export const useTabSettings = () => { export const useTabSettings = () => {
return { return {

View File

@ -38,8 +38,8 @@ const useStyles = createStyles(({ css }) => {
export const PinnedPluginList = () => { export const PinnedPluginList = () => {
const { allowAll, snippets } = useACLRoleContext(); const { allowAll, snippets } = useACLRoleContext();
const getSnippetsAllow = (aclKey) => { const getSnippetsAllow = (aclKey, isPublic = false) => {
return allowAll || snippets?.includes(aclKey); return allowAll || isPublic || snippets?.includes(aclKey);
}; };
const { styles } = useStyles(); const { styles } = useStyles();
const ctx = useContext(PinnedPluginListContext); const ctx = useContext(PinnedPluginListContext);
@ -48,7 +48,7 @@ export const PinnedPluginList = () => {
<div className={styles.container}> <div className={styles.container}>
{Object.keys(ctx.items) {Object.keys(ctx.items)
.sort((a, b) => ctx.items[a].order - ctx.items[b].order) .sort((a, b) => ctx.items[a].order - ctx.items[b].order)
.filter((key) => getSnippetsAllow(ctx.items[key].snippet)) .filter((key) => getSnippetsAllow(ctx.items[key].snippet, ctx.items[key].isPublic))
.map((key) => { .map((key) => {
const Action = get(components, ctx.items[key].component); const Action = get(components, ctx.items[key].component);
return Action ? <Action key={key} /> : null; return Action ? <Action key={key} /> : null;

View File

@ -845,5 +845,8 @@
"Full Screen":"Full Screen", "Full Screen":"Full Screen",
"Designer Mode":"Designer Mode", "Designer Mode":"Designer Mode",
"Embedded page": "Embedded page", "Embedded page": "Embedded page",
"Page style": "Page Style",
"classical": "classical",
"tabs": "tabs",
"There are no full screen blocks available at the current location":"There are no full screen blocks available at the current location" "There are no full screen blocks available at the current location":"There are no full screen blocks available at the current location"
} }

View File

@ -961,5 +961,8 @@
"User settings":"个人设置", "User settings":"个人设置",
"Import collection": "导入数据表", "Import collection": "导入数据表",
"Embedded page": "嵌入页面", "Embedded page": "嵌入页面",
"Page style": "页面样式",
"classical": "经典",
"tabs": "标签",
"Demonstration text": "演示文本" "Demonstration text": "演示文本"
} }

View File

@ -8,7 +8,5 @@ export * from './custom-components/CustomField';
export * from './fields/AssociatedField'; export * from './fields/AssociatedField';
export * from './fields/CalcResult'; export * from './fields/CalcResult';
export * from './fields/Expression'; export * from './fields/Expression';
export * from './system/MobileLink';
export * from './system/Notifications';
export * from './system/OnlineUserProvider'; export * from './system/OnlineUserProvider';
export * from './excel-table/ExcelFile'; export * from './excel-table/ExcelFile';

View File

@ -1,35 +0,0 @@
import React from 'react';
import { css } from '@tachybase/client';
import { MobileOutlined } from '@ant-design/icons';
import { Button, Tooltip } from 'antd';
import { useTranslation } from '../../locale';
export const MobileLink = () => {
const { t } = useTranslation();
return (
<div
className={css`
.ant-btn {
border: 0;
height: var(--tb-header-height);
width: 46px;
border-radius: 0;
background: none;
color: rgba(255, 255, 255, 0.65) !important;
&:hover {
background: rgba(255, 255, 255, 0.1) !important;
}
}
`}
style={{ display: 'inline-block' }}
>
<Tooltip title={t('Mobile UI')} placement="bottom">
<Button role="button" href="/mobile">
<MobileOutlined />
</Button>
</Tooltip>
</div>
);
};

View File

@ -1,104 +0,0 @@
import React from 'react';
import { css, useRequest } from '@tachybase/client';
import { dayjs } from '@tachybase/utils/client';
import { BellOutlined } from '@ant-design/icons';
import { Badge, Button, List, Popover } from 'antd';
import { useNavigate } from 'react-router-dom';
import { useInitializationLinkKey, useLinkKey } from '../../hooks/useNotifications';
export const Notifications = () => {
// 注册链接关联链接管理使用tachybase代码方式注册数据表
useInitializationLinkKey();
const linkDetail = useLinkKey();
const navigate = useNavigate();
const { data, run } = useRequest<{ data: any }>({
url: `/system_notifications:get`,
});
const getNotificationList = data?.data || [];
const { run: updateRead } = useRequest<{ data: any }>(
{
url: `/system_notifications:update`,
params: {
ids: getNotificationList.map((item) => item.id),
},
},
{
onSuccess() {
run();
},
},
);
const content = (
<List
style={{ width: '400px' }}
itemLayout="horizontal"
dataSource={getNotificationList}
footer={
<Button.Group style={{ width: '100%' }}>
<Button
block
onClick={() => {
updateRead();
}}
>
</Button>
<Button
block
onClick={() => {
navigate(linkDetail?.link);
}}
>
</Button>
</Button.Group>
}
renderItem={(item: any) => (
<List.Item>
<List.Item.Meta title={item.title} description={dayjs(item.createdAt).calendar()} />
{item.content}
</List.Item>
)}
/>
);
return (
<div
className={css`
.ant-btn {
border: 0;
height: var(--tb-header-height);
width: 46px;
border-radius: 0;
background: none;
color: rgba(255, 255, 255, 0.65) !important;
&:hover {
background: rgba(255, 255, 255, 0.1) !important;
}
}
a {
padding-top: 12px;
}
.ant-badge-count {
min-width: 10px;
height: 10px;
line-height: 10px;
font-size: 9px;
border-radius: 5px;
padding: 0 2px;
}
`}
style={{ display: 'inline-block' }}
>
<Popover content={content}>
<Badge count={getNotificationList.length} size="small" offset={[-15, 15]}>
<Button role="button">
<BellOutlined />
</Button>
</Badge>
</Popover>
</div>
);
};

View File

@ -2,7 +2,6 @@ import React, { useEffect } from 'react';
import { useCurrentUserSettingsMenu } from '@tachybase/client'; import { useCurrentUserSettingsMenu } from '@tachybase/client';
import { usePluginVersion } from '../../hooks/usePluginVersion'; import { usePluginVersion } from '../../hooks/usePluginVersion';
import { useTabSettings } from '../page-style/useTabSettings';
const useHeraVersion = () => { const useHeraVersion = () => {
const version = usePluginVersion(); const version = usePluginVersion();
@ -16,10 +15,9 @@ const useHeraVersion = () => {
export const HeraVersionProvider = ({ children }) => { export const HeraVersionProvider = ({ children }) => {
const { addMenuItem } = useCurrentUserSettingsMenu(); const { addMenuItem } = useCurrentUserSettingsMenu();
const heraVersion = useHeraVersion(); const heraVersion = useHeraVersion();
const tabItem = useTabSettings();
useEffect(() => { useEffect(() => {
addMenuItem(heraVersion, { before: 'divider_1' }); addMenuItem(heraVersion, { before: 'divider_1' });
}, [addMenuItem, tabItem, heraVersion]); }, [addMenuItem, heraVersion]);
return <>{children}</>; return <>{children}</>;
}; };

View File

@ -21,7 +21,6 @@ import { EmbedPlugin } from './features/embed';
import { PluginFieldAppends } from './features/field-appends'; import { PluginFieldAppends } from './features/field-appends';
import { PluginHeraVersion } from './features/hera-version'; import { PluginHeraVersion } from './features/hera-version';
import { PluginOutbound } from './features/outbound'; import { PluginOutbound } from './features/outbound';
import { PluginPageStyle } from './features/page-style';
import { PluginPDF } from './features/pdf'; import { PluginPDF } from './features/pdf';
import { PluginSheet } from './features/sheet'; import { PluginSheet } from './features/sheet';
import { useGetCustomAssociatedComponents } from './hooks/useGetCustomAssociatedComponents'; import { useGetCustomAssociatedComponents } from './hooks/useGetCustomAssociatedComponents';
@ -36,7 +35,7 @@ import {
} from './interfaces'; } from './interfaces';
import { TstzrangeFieldInterface } from './interfaces/TstzrangeFieldInterface'; import { TstzrangeFieldInterface } from './interfaces/TstzrangeFieldInterface';
import { Locale, tval } from './locale'; import { Locale, tval } from './locale';
import { AdminLayout, DetailsPage, PageLayout } from './pages'; import { DetailsPage, PageLayout } from './pages';
import { CreateSubmitActionInitializer, SettingBlockInitializer } from './schema-initializer'; import { CreateSubmitActionInitializer, SettingBlockInitializer } from './schema-initializer';
import { useCreateActionProps } from './schema-initializer/actions/hooks/useCreateActionProps'; import { useCreateActionProps } from './schema-initializer/actions/hooks/useCreateActionProps';
import { EditTitle, IsTablePageSize, PageModeSetting, usePaginationVisible } from './schema-settings'; import { EditTitle, IsTablePageSize, PageModeSetting, usePaginationVisible } from './schema-settings';
@ -59,7 +58,6 @@ export class PluginCoreClient extends Plugin {
await this.app.pm.add(PluginGroupBlock); await this.app.pm.add(PluginGroupBlock);
await this.app.pm.add(EmbedPlugin); await this.app.pm.add(EmbedPlugin);
await this.app.pm.add(DepartmentsPlugin); await this.app.pm.add(DepartmentsPlugin);
await this.app.pm.add(PluginPageStyle);
await this.app.pm.add(PluginHeraVersion); await this.app.pm.add(PluginHeraVersion);
await this.app.pm.add(PluginPDF); await this.app.pm.add(PluginPDF);
await this.app.pm.add(PluginOutbound); await this.app.pm.add(PluginOutbound);
@ -115,7 +113,6 @@ export class PluginCoreClient extends Plugin {
}); });
this.app.addComponents({ this.app.addComponents({
AdminLayout,
AssociatedField, AssociatedField,
CalcResult, CalcResult,
CodeMirror, CodeMirror,

View File

@ -1,384 +0,0 @@
import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
import {
AdminProvider,
css,
CurrentUser,
findByUid,
findMenuItem,
NoticeArea,
PinnedPluginList,
SchemaComponent,
useACLRoleContext,
useAdminSchemaUid,
useApp,
useDocumentTitle,
useRequest,
useSystemSettings,
useToken,
} from '@tachybase/client';
import { useSessionStorageState } from 'ahooks';
import { App, Layout } from 'antd';
import { Link, Outlet, useMatch, useNavigate, useParams } from 'react-router-dom';
import { MobileLink } from '../components/system/MobileLink';
import { Notifications } from '../components/system/Notifications';
import { OnlineUserDropdown } from '../components/system/OnlineUserProvider';
import { PageTab } from '../features/page-style/PageTab';
import { usePageStyle } from '../features/page-style/usePageStyle';
const filterByACL = (schema, options) => {
const { allowAll, allowMenuItemIds = [] } = options;
if (allowAll) {
return schema;
}
const filterSchema = (s) => {
if (!s) {
return;
}
for (const key in s.properties) {
if (Object.prototype.hasOwnProperty.call(s.properties, key)) {
const element = s.properties[key];
if (element['x-uid'] && !allowMenuItemIds.includes(element['x-uid'])) {
delete s.properties[key];
}
if (element['x-uid']) {
filterSchema(element);
}
}
}
};
filterSchema(schema);
return schema;
};
const SchemaIdContext = createContext(null);
const useMenuProps = () => {
const defaultSelectedUid = useContext(SchemaIdContext);
return {
selectedUid: defaultSelectedUid,
defaultSelectedUid,
};
};
const MenuEditor = (props) => {
const { notification } = App.useApp();
const [hasNotice, setHasNotice] = useSessionStorageState('plugin-notice', { defaultValue: false });
const { setTitle } = useDocumentTitle();
const navigate = useNavigate();
const params = useParams<any>();
const isMatchAdmin = useMatch('/admin');
const isMatchAdminName = useMatch('/admin/:name');
const defaultSelectedUid = params.name;
const { sideMenuRef } = props;
const ctx = useACLRoleContext();
const [current, setCurrent] = useState(null);
const onSelect = ({ item }) => {
const schema = item.props.schema;
setTitle(schema.title);
setCurrent(schema);
navigate(`/admin/${schema['x-uid']}`);
};
const adminSchemaUid = useAdminSchemaUid();
const { data, loading } = useRequest<{
data: any;
}>(
{
url: `/uiSchemas:getJsonSchema/${adminSchemaUid}`,
},
{
refreshDeps: [adminSchemaUid],
onSuccess(data) {
const schema = filterByACL(data?.data, ctx);
// url 为 `/admin` 的情况
if (isMatchAdmin) {
const s = findMenuItem(schema);
if (s) {
navigate(`/admin/${s['x-uid']}`);
setTitle(s.title);
} else {
navigate(`/admin/`);
}
return;
}
// url 不为 `/admin/xxx` 的情况,不做处理
if (!isMatchAdminName) return;
// url 为 `admin/xxx` 的情况
const s = findByUid(schema, defaultSelectedUid);
if (s) {
setTitle(s.title);
} else {
const s = findMenuItem(schema);
if (s) {
navigate(`/admin/${s['x-uid']}`);
setTitle(s.title);
} else {
navigate(`/admin/`);
}
}
},
},
);
useEffect(() => {
const properties = Object.values(current?.root?.properties || {}).shift()?.['properties'] || data?.data?.properties;
if (properties && sideMenuRef.current) {
const pageType = Object.values(properties).find(
(item) => item['x-uid'] === params.name && item['x-component'] === 'Menu.Item',
);
if (pageType) {
sideMenuRef.current.style.display = 'none';
} else {
sideMenuRef.current.style.display = 'block';
}
}
}, [data?.data, params.name, sideMenuRef]);
const schema = useMemo(() => {
const s = filterByACL(data?.data, ctx);
if (s?.['x-component-props']) {
s['x-component-props']['useProps'] = useMenuProps;
}
return s;
}, [data?.data]);
useRequest(
{
url: 'applicationPlugins:list',
params: {
sort: 'id',
paginate: false,
},
},
{
onSuccess: ({ data }) => {
setHasNotice(true);
const errorPlugins = data.filter((item) => !item.isCompatible);
if (errorPlugins.length) {
notification.error({
message: 'Plugin dependencies check failed',
description: (
<div>
<div>
These plugins failed dependency checks. Please go to the{' '}
<Link to="/admin/pm/list/local/">plugin management page</Link> for more details.{' '}
</div>
<ul>
{errorPlugins.map((item) => (
<li key={item.id}>
{item.displayName} - {item.packageName}
</li>
))}
</ul>
</div>
),
});
}
},
manual: true,
// ready: !hasNotice,
},
);
if (loading) {
return void 0;
}
return (
<SchemaIdContext.Provider value={defaultSelectedUid}>
<SchemaComponent memoized scope={{ useMenuProps, onSelect, sideMenuRef, defaultSelectedUid }} schema={schema} />
</SchemaIdContext.Provider>
);
};
export const InternalAdminLayout = () => {
const sideMenuRef = useRef<HTMLDivElement>();
const result = useSystemSettings();
const params = useParams<{ name?: string }>();
const app = useApp();
const { token } = useToken();
const pageStyle = usePageStyle();
return (
<Layout>
<Layout.Header
className={css`
.ant-menu.ant-menu-dark .ant-menu-item-selected,
.ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-selected,
.ant-menu-submenu-horizontal.ant-menu-submenu-selected {
background-color: ${token.colorBgHeaderMenuActive};
color: ${token.colorTextHeaderMenuActive};
}
.ant-menu-dark.ant-menu-horizontal > .ant-menu-item:hover {
background-color: ${token.colorBgHeaderMenuHover};
color: ${token.colorTextHeaderMenuHover};
}
position: fixed;
left: 0;
right: 0;
height: var(--tb-header-height);
line-height: var(--tb-header-height);
padding: 0;
z-index: 100;
background-color: ${token.colorBgHeader};
.ant-menu {
background-color: transparent;
}
.ant-menu-item {
color: ${token.colorTextHeaderMenu};
}
`}
>
<div
className={css`
position: relative;
width: 100%;
height: 100%;
display: flex;
`}
>
<div
className={css`
position: relative;
z-index: 1;
flex: 1 1 auto;
display: flex;
height: 100%;
`}
>
<div
className={css`
display: inline-flex;
flex-shrink: 0;
color: #fff;
padding: 0;
align-items: center;
padding: 0 12px 0 12px;
cursor: pointer;
`}
onClick={() => {
location.href = app.adminUrl;
}}
>
<img
className={css`
object-fit: contain;
height: 28px;
`}
src={result?.data?.data?.logo?.url}
/>
<h1
className={css`
color: #fff;
height: 32px;
margin: 0 0 0 12px;
font-weight: 600;
font-size: 18px;
line-height: 32px;
`}
>
{result?.data?.data?.title}
</h1>
</div>
<div
className={css`
flex: 1 1 auto;
width: 0;
display: flex;
`}
>
<NoticeArea
className={css`
flex: 1;
`}
/>
<MenuEditor sideMenuRef={sideMenuRef} />
</div>
</div>
<div
className={css`
display: inline-flex;
position: relative;
flex-shrink: 0;
height: 100%;
z-index: 10;
`}
>
<PinnedPluginList />
<MobileLink />
<Notifications />
<OnlineUserDropdown />
<CurrentUser />
</div>
</div>
</Layout.Header>
{params.name && (
<Layout.Sider
theme={'light'}
ref={sideMenuRef}
style={{ height: '100vh' }}
className={css`
.ant-layout-sider-children {
display: flex;
flex-direction: column;
}
`}
>
<header
className={css`
height: var(--tb-header-height);
width: 100%;
background: transparent;
pointer-events: none;
`}
></header>
</Layout.Sider>
)}
<Layout.Content
className={css`
display: flex;
flex-direction: column;
position: relative;
overflow-y: auto;
height: 100vh;
max-height: 100vh;
> div {
position: relative;
}
.ant-layout-footer {
position: absolute;
bottom: 0;
text-align: center;
width: 100%;
z-index: 0;
padding: 0px 50px;
}
`}
>
<header
className={css`
flex-shrink: 0;
height: var(--tb-header-height);
line-height: var(--tb-header-height);
background: transparent;
pointer-events: none;
`}
></header>
<>{params.name && pageStyle === 'tab' ? <PageTab /> : <Outlet />}</>
</Layout.Content>
</Layout>
);
};
export const AdminLayout = (props) => {
const AdminComponent = (
<AdminProvider>
<InternalAdminLayout {...props} />
</AdminProvider>
);
return AdminComponent;
};

View File

@ -1,4 +1,3 @@
export * from './AdminLayout';
export * from './DetailsPage'; export * from './DetailsPage';
export * from '../features/outbound/OutboundPage'; export * from '../features/outbound/OutboundPage';
export * from './PageLayout'; export * from './PageLayout';

View File

@ -3,11 +3,16 @@
"version": "0.22.4", "version": "0.22.4",
"main": "dist/server/index.js", "main": "dist/server/index.js",
"dependencies": { "dependencies": {
"antd": "5.19.4" "@ant-design/icons": "~5.3.7",
"antd": "5.19.4",
"react-i18next": "^14.1.2",
"react-router-dom": "~6.25.1"
}, },
"peerDependencies": { "peerDependencies": {
"@tachybase/client": "workspace:*", "@tachybase/client": "workspace:*",
"@tachybase/database": "workspace:*", "@tachybase/database": "workspace:*",
"@tachybase/plugin-workflow": "workspace:*",
"@tachybase/schema": "workspace:*",
"@tachybase/server": "workspace:*", "@tachybase/server": "workspace:*",
"@tachybase/test": "workspace:*" "@tachybase/test": "workspace:*"
} }

View File

@ -0,0 +1,39 @@
import React from 'react';
import {
createTableBlockSchema,
SchemaInitializerItem,
useSchemaInitializer,
useSchemaInitializerItem,
} from '@tachybase/client';
import { ISchema } from '@tachybase/schema';
import { BellOutlined } from '@ant-design/icons';
import { useTranslation } from './locale';
export const MessageBlockInitializer = () => {
const { insert } = useSchemaInitializer();
const { t } = useTranslation();
const itemConfig = useSchemaInitializerItem();
const schema = createTableBlockSchema({
collection: 'messages',
rowKey: 'id',
tableActionInitializers: 'MessageTable:configureActions',
tableColumnInitializers: 'MessageTable:configureColumns',
tableActionColumnInitializers: 'MessageTable:configureItemActions',
tableBlockProvider: 'MessageBlockProvider',
disableTemplate: true,
});
return (
<SchemaInitializerItem
icon={<BellOutlined />}
onClick={() => {
insert(schema as ISchema);
}}
title={t('Messages')}
{...itemConfig}
/>
);
};

View File

@ -0,0 +1,18 @@
import React from 'react';
import { ExtendCollectionsProvider, TableBlockProvider } from '@tachybase/client';
import { useAuditChangesCollection, useCollectionsCollection, useMessageCollection } from './collections';
export const MessageBlockProvider = ({ children, ...restProps }) => {
const auditChangesCollection = useAuditChangesCollection();
const MessageCollection = useMessageCollection();
const collectionsCollection = useCollectionsCollection();
return (
<ExtendCollectionsProvider collections={[MessageCollection, auditChangesCollection, collectionsCollection]}>
<TableBlockProvider name="message" {...restProps}>
{children}
</TableBlockProvider>
</ExtendCollectionsProvider>
);
};

View File

@ -0,0 +1,37 @@
import React from 'react';
import { PinnedPluginListProvider, SchemaComponentOptions, useToken } from '@tachybase/client';
import { BellOutlined } from '@ant-design/icons';
import { Button, Tooltip } from 'antd';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from './locale';
const NotificationLink = () => {
const navigate = useNavigate();
const { token } = useToken();
const { t } = useTranslation();
return (
<Tooltip title={t('Messages')}>
<Button
icon={<BellOutlined style={{ color: token.colorTextHeaderMenu }} />}
title={t('Messages')}
onClick={() => {
navigate('/admin/messages');
}}
/>
</Tooltip>
);
};
export const MessageNotificationProvider = (props) => {
return (
<PinnedPluginListProvider
items={{
nl: { order: 220, component: 'NotificationLink', pin: true, isPublic: true },
}}
>
<SchemaComponentOptions components={{ NotificationLink }}>{props.children}</SchemaComponentOptions>
</PinnedPluginListProvider>
);
};

View File

@ -0,0 +1,86 @@
import React from 'react';
import { Page, SchemaComponent, SchemaComponentContext, SchemaComponentOptions } from '@tachybase/client';
export const MessagePage = () => {
return (
<SchemaComponentContext.Provider value={{ designable: false }}>
<SchemaComponent
schema={{
type: 'void',
'x-component': 'Page',
properties: {
messages: {
type: 'void',
'x-decorator': 'MessageBlockProvider',
'x-acl-action': 'messages:list',
'x-decorator-props': {
collection: 'messages',
action: 'list',
params: {
pageSize: 20,
},
rowKey: 'id',
showIndex: true,
dragSort: false,
disableTemplate: true,
},
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': 'blockSettings:table',
'x-component': 'CardItem',
'x-filter-targets': [],
properties: {
actions: {
type: 'void',
'x-initializer': 'MessageTable:configureActions',
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 'var(--tb-spacing)',
},
},
},
smvxfjqmkl7: {
type: 'array',
'x-initializer': 'MessageTable:configureColumns',
'x-component': 'TableV2',
'x-use-component-props': 'useTableBlockProps',
'x-component-props': {
rowKey: 'id',
rowSelection: {
type: 'checkbox',
},
},
properties: {
actions: {
type: 'void',
title: '{{ t("Actions") }}',
'x-action-column': 'actions',
'x-decorator': 'TableV2.Column.ActionBar',
'x-component': 'TableV2.Column',
'x-component-props': {
width: 150,
fixed: 'right',
},
'x-designer': 'TableV2.ActionColumnDesigner',
'x-initializer': 'MessageTable:configureItemActions',
properties: {
he7o0zao83h: {
type: 'void',
'x-decorator': 'DndContext',
'x-component': 'Space',
'x-component-props': {
split: '|',
},
},
},
},
},
},
},
},
},
}}
></SchemaComponent>
</SchemaComponentContext.Provider>
);
};

View File

@ -0,0 +1,24 @@
import React from 'react';
import { SchemaComponentOptions } from '@tachybase/client';
import { MessageViewActionInitializer } from './components/MessageViewActionInitializer';
import { Message } from './deplicated/Message';
import { MessageTableActionColumnInitializer } from './initializers/MessageTableActionColumnInitializer';
import { MessageBlockInitializer } from './MessageBlockInitializer';
import { MessageBlockProvider } from './MessageBlockProvider';
export const MessageProvider = (props: any) => {
return (
<SchemaComponentOptions
components={{
Message,
MessageBlockProvider,
MessageBlockInitializer,
MessageViewActionInitializer,
MessageTableActionColumnInitializer,
}}
>
{props.children}
</SchemaComponentOptions>
);
};

View File

@ -0,0 +1,151 @@
import { tval } from './locale';
export const useMessageCollection = () => {
return {
name: 'messages',
title: tval('Messages'),
fields: [
{
name: 'createdAt',
type: 'date',
interface: 'createdAt',
uiSchema: {
type: 'datetime',
title: tval('Created at'),
'x-component': 'DatePicker',
'x-component-props': {
showTime: true,
ellipsis: true,
},
'x-read-pretty': true,
},
},
{
interface: 'input',
type: 'string',
name: 'title',
allowNull: false,
unique: false,
uiSchema: {
type: 'string',
title: '{{t("Title")}}',
'x-component': 'Input',
required: true,
},
},
{
interface: 'input',
type: 'string',
name: 'content',
allowNull: false,
unique: false,
uiSchema: {
type: 'string',
title: '{{t("Content")}}',
'x-component': 'Input',
required: true,
},
},
{
name: 'type',
type: 'string',
interface: 'select',
uiSchema: {
type: 'string',
title: tval('Action type'),
'x-component': 'Select',
'x-component-props': { ellipsis: true },
'x-read-pretty': true,
enum: [
{ label: tval('Create record'), value: 'create', color: 'lime' },
{ label: tval('Update record'), value: 'update', color: 'gold' },
{ label: tval('Delete record'), value: 'destroy', color: 'magenta' },
],
},
},
{
name: 'recordId',
type: 'string',
interface: 'input',
uiSchema: {
title: tval('Record ID'),
type: 'string',
'x-component': 'Input',
'x-component-props': { ellipsis: true },
},
},
{
collectionName: 'messages',
name: 'collection',
type: 'belongsTo',
interface: 'm2o',
target: 'collections',
targetKey: 'name',
sourceKey: 'id',
foreignKey: 'collectionName',
uiSchema: {
type: 'object',
title: tval('Collection'),
'x-component': 'AssociationField',
'x-component-props': { fieldNames: { value: 'name', label: 'title' }, ellipsis: true },
'x-read-pretty': true,
},
},
{
name: 'user',
collectionName: 'messages',
type: 'belongsTo',
interface: 'createdBy',
targetKey: 'id',
foreignKey: 'createdById',
target: 'users',
uiSchema: {
type: 'object',
title: tval('User'),
'x-component': 'AssociationField',
'x-component-props': { fieldNames: { value: 'id', label: 'nickname' }, ellipsis: true },
'x-read-pretty': true,
},
},
],
};
};
export const useAuditChangesCollection = () => {
return {
name: 'auditChanges',
title: tval('Audit Changes'),
fields: [],
};
};
export const useCollectionsCollection = () => {
return {
name: 'collections',
title: tval('Collections'),
fields: [
{
name: 'name',
type: 'string',
interface: 'input',
uiSchema: {
title: tval('Collection name'),
type: 'string',
'x-component': 'Input',
'x-component-props': { ellipsis: true },
},
},
{
name: 'title',
type: 'string',
interface: 'input',
uiSchema: {
title: tval('Collection display name'),
type: 'string',
'x-component': 'Input',
'x-component-props': { ellipsis: true },
},
},
],
};
};

View File

@ -0,0 +1,300 @@
import React from 'react';
import { ActionInitializer } from '@tachybase/client';
import { uid } from '@tachybase/utils/client';
import { tval } from '../locale';
export const MessageViewActionInitializer = () => {
const changes = {
type: 'string',
'x-component': 'AssociationField',
'x-decorator': 'FormItem',
'x-collection-field': 'Message.changes',
'x-read-pretty': true,
'x-component-props': {
multiple: true,
fieldNames: {
value: 'id',
label: 'id',
},
mode: 'SubTable',
},
properties: {
block: {
type: 'void',
'x-component': 'AssociationField.SubTable',
properties: {
[uid()]: {
type: 'void',
'x-decorator': 'TableV2.Column.Decorator',
'x-component': 'TableV2.Column',
properties: {},
},
[uid()]: {
type: 'void',
'x-decorator': 'TableV2.Column.Decorator',
'x-component': 'TableV2.Column',
properties: {},
},
[uid()]: {
type: 'void',
'x-decorator': 'TableV2.Column.Decorator',
'x-component': 'TableV2.Column',
properties: {},
},
},
},
},
};
const schema = {
type: 'void',
title: tval('View'),
'x-action': 'view',
'x-toolbar': 'ActionSchemaToolbar',
'x-settings': 'actionSettings:view',
'x-component': 'Action',
'x-component-props': {
openMode: 'drawer',
},
properties: {
drawer: {
type: 'void',
title: tval('View record'),
'x-component': 'Action.Container',
'x-component-props': {
className: 'nb-action-popup',
},
properties: {
tabs: {
type: 'void',
'x-component': 'Tabs',
'x-component-props': {},
properties: {
tab1: {
type: 'void',
title: tval('Details'),
'x-component': 'Tabs.TabPane',
'x-designer': 'Tabs.Designer',
'x-component-props': {},
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
properties: {
'7ypqrxaqysp': {
type: 'void',
'x-component': 'Grid.Row',
properties: {
'1fc2l8dwe7m': {
type: 'void',
'x-component': 'Grid.Col',
properties: {
'5ley6xifrsb': {
type: 'void',
'x-acl-action': 'Message:get',
'x-decorator': 'FormBlockProvider',
'x-decorator-props': {
resource: 'messages',
collection: 'messages',
readPretty: true,
action: 'get',
params: {
appends: ['collection', 'user', 'changes'],
},
useParams: '{{ useParamsFromRecord }}',
useSourceId: '{{ useSourceIdFromParentRecord }}',
},
'x-component': 'CardItem',
properties: {
bv710pbf9w6: {
type: 'void',
'x-component': 'FormV2',
'x-read-pretty': true,
'x-component-props': {
useProps: '{{ useFormBlockProps }}',
},
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
properties: {
g4c24abnbd9: {
type: 'void',
'x-component': 'Grid.Row',
properties: {
fkt9dj5lu1k: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
createdAt: {
type: 'string',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': 'Message.createdAt',
'x-component-props': {},
'x-read-pretty': true,
},
},
},
},
},
l267bkv423v: {
type: 'void',
'x-component': 'Grid.Row',
properties: {
tehaepm5xqy: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
type: {
type: 'string',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': 'Message.type',
'x-component-props': {},
'x-read-pretty': true,
},
},
},
},
},
zhmn2tkzdh2: {
type: 'void',
'x-component': 'Grid.Row',
properties: {
yuwyej17i64: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
recordId: {
type: 'string',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': 'Message.recordId',
'x-component-props': {},
},
},
},
},
},
'6q933ic06mj': {
type: 'void',
'x-component': 'Grid.Row',
properties: {
nwtbkqd99zl2: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
'collection.title': {
type: 'string',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': 'Message.collection.title',
'x-component-props': {},
},
},
},
nwtbkqd99zl1: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
'collection.name': {
type: 'string',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': 'Message.collection.name',
'x-component-props': {},
},
},
},
},
},
xd4kzulndqk: {
type: 'void',
'x-component': 'Grid.Row',
properties: {
k3sd72n83zd: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
user: {
type: 'string',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': 'Message.user',
'x-component-props': {},
'x-read-pretty': true,
properties: {
viewer: {
type: 'void',
title: tval('View record'),
'x-component': 'RecordPicker.Viewer',
'x-component-props': {
className: 'nb-action-popup',
},
properties: {
tabs: {
type: 'void',
'x-component': 'Tabs',
'x-component-props': {},
'x-initializer': 'TabPaneInitializers',
properties: {
tab1: {
type: 'void',
title: tval('Details'),
'x-component': 'Tabs.TabPane',
'x-designer': 'Tabs.Designer',
'x-component-props': {},
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'popup:common:addBlock',
},
},
},
},
},
},
},
},
},
},
},
},
},
'3lghgclbtcg': {
type: 'void',
'x-component': 'Grid.Row',
properties: {
'8l1v1auwlx0': {
type: 'void',
'x-component': 'Grid.Col',
properties: {
changes,
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
};
return <ActionInitializer schema={schema} />;
};

View File

@ -0,0 +1,513 @@
import React, { createContext, useContext } from 'react';
import {
ExtendCollectionsProvider,
FormProvider,
SchemaComponent,
TableBlockProvider,
useCollection_deprecated,
useCompile,
useRecord,
} from '@tachybase/client';
import { ArrayTable } from '@tachybase/components';
import { observer, useField } from '@tachybase/schema';
import { tval } from '../locale';
import { MessageDesigner } from './MessageDesigner';
const collection = {
name: 'messages',
title: tval('Messages'),
fields: [
{
name: 'createdAt',
type: 'date',
interface: 'createdAt',
uiSchema: {
type: 'datetime',
title: tval('Created at'),
'x-component': 'DatePicker',
'x-component-props': {
showTime: true,
},
'x-read-pretty': true,
},
},
{
name: 'type',
type: 'string',
interface: 'select',
uiSchema: {
type: 'string',
title: tval('Action type'),
'x-component': 'Select',
'x-read-pretty': true,
enum: [
{ label: tval('Create record'), value: 'create', color: 'lime' },
{ label: tval('Update record'), value: 'update', color: 'gold' },
{ label: tval('Delete record'), value: 'destroy', color: 'magenta' },
],
},
},
],
};
const Username = observer(
() => {
const field = useField<any>();
return <div>{field?.value?.nickname || field.value?.id}</div>;
},
{ displayName: 'Username' },
);
const Collection = observer(
() => {
const field = useField<any>();
const { title, name } = field.value || {};
const compile = useCompile();
return <div>{title ? compile(title) : name}</div>;
},
{ displayName: 'Collection' },
);
const Field = observer(
() => {
const field = useField<any>();
const compile = useCompile();
if (!field.value) {
return null;
}
return <div>{field.value?.uiSchema?.title ? compile(field.value?.uiSchema?.title) : field.value.name}</div>;
},
{ displayName: 'Field' },
);
const Value = observer(
() => {
const field = useField<any>();
const record = ArrayTable.useRecord();
if (record.field?.uiSchema) {
return (
<FormProvider>
<SchemaComponent
schema={{
name: record.field.name,
...record.field?.uiSchema,
default: field.value,
'x-read-pretty': true,
}}
/>
</FormProvider>
);
}
return <div>{field.value ? JSON.stringify(field.value) : null}</div>;
},
{ displayName: 'Value' },
);
const IsAssociationBlock = createContext(null);
IsAssociationBlock.displayName = 'IsAssociationBlock';
export const Message: any = () => {
const isAssoc = useContext(IsAssociationBlock);
const ext = {};
if (!isAssoc) {
ext['column31'] = {
type: 'void',
'x-component': 'TableV2.Column',
title: tval('Record ID'),
properties: {
recordId: {
'x-component': 'Input',
'x-read-pretty': true,
},
},
};
}
return (
<SchemaComponent
memoized
components={{ ArrayTable, Username, Collection, Field, Value }}
schema={{
type: 'void',
name: 'lfm4trkw8j3',
'x-component': 'div',
properties: {
actions: {
type: 'void',
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 16,
},
},
properties: {
filter: {
type: 'void',
title: tval('Filter'),
'x-action': 'filter',
'x-component': 'Filter.Action',
'x-component-props': {
icon: 'FilterOutlined',
useProps: '{{ useFilterActionProps }}',
},
'x-align': 'left',
},
},
},
y84dlntcaup: {
type: 'array',
'x-component': 'TableV2',
'x-component-props': {
rowKey: 'id',
rowSelection: {
type: 'checkbox',
},
useProps: '{{ useTableBlockProps }}',
},
properties: {
actions: {
type: 'void',
title: tval('Actions'),
'x-action-column': 'actions',
'x-decorator': 'TableV2.Column.ActionBar',
'x-component': 'TableV2.Column',
'x-designer': 'TableV2.ActionColumnDesigner',
'x-initializer': 'table:configureItemActions',
properties: {
actions: {
type: 'void',
'x-decorator': 'DndContext',
'x-component': 'Space',
'x-component-props': {
split: '|',
},
properties: {
o80rypwmeeg: {
type: 'void',
title: tval('View'),
'x-toolbar': 'ActionSchemaToolbar',
'x-settings': 'actionSettings:view',
'x-component': 'Action.Link',
'x-component-props': {
openMode: 'drawer',
},
properties: {
drawer: {
type: 'void',
title: tval('View record'),
'x-component': 'Action.Container',
'x-component-props': {
className: 'nb-action-popup',
},
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
properties: {
tdlav8o9o17: {
type: 'void',
'x-component': 'Grid.Row',
properties: {
'7bsnaf47i6g': {
type: 'void',
'x-component': 'Grid.Col',
properties: {
'6s2qbihe3tu': {
type: 'void',
'x-acl-action': 'Message:get',
'x-decorator': 'FormBlockProvider',
'x-decorator-props': {
resource: 'messages',
collection: 'messages',
readPretty: true,
action: 'get',
params: {
appends: ['collection', 'user', 'changes'],
},
useParams: '{{ useParamsFromRecord }}',
useSourceId: '{{ useSourceIdFromParentRecord }}',
},
'x-component': 'CardItem',
properties: {
mevpuonrda0: {
type: 'void',
'x-component': 'FormV2',
'x-read-pretty': true,
'x-component-props': {
useProps: '{{ useFormBlockProps }}',
},
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
properties: {
row1: {
type: 'void',
'x-component': 'Grid.Row',
properties: {
col11: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
collection: {
type: 'string',
title: tval('Collection display name'),
'x-component': 'Collection',
'x-decorator': 'FormItem',
'x-read-pretty': true,
},
},
},
},
},
row2: {
type: 'void',
'x-component': 'Grid.Row',
properties: {
col21: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
type: {
type: 'string',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-read-pretty': true,
},
},
},
},
},
row3: {
type: 'void',
'x-component': 'Grid.Row',
properties: {
col31: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
createdAt: {
type: 'string',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-read-pretty': true,
},
},
},
},
},
row4: {
type: 'void',
'x-component': 'Grid.Row',
properties: {
col41: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
user: {
type: 'string',
title: tval('User'),
'x-component': 'Username',
'x-decorator': 'FormItem',
'x-read-pretty': true,
},
},
},
},
},
row5: {
type: 'void',
'x-component': 'Grid.Row',
properties: {
col51: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
changes: {
type: 'array',
title: tval('Field value changes'),
'x-decorator': 'FormItem',
'x-component': 'ArrayTable',
'x-component-props': {
pagination: { pageSize: 10 },
scroll: { x: '100%' },
},
items: {
type: 'object',
properties: {
column3: {
type: 'void',
'x-component': 'ArrayTable.Column',
'x-component-props': {
width: 200,
title: tval('Field'),
},
properties: {
field: {
type: 'string',
//'x-decorator': 'Editable',
'x-component': 'Field',
},
},
},
column4: {
type: 'void',
'x-component': 'ArrayTable.Column',
'x-component-props': {
width: 200,
title: tval('Before change'),
},
properties: {
before: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Value',
},
},
},
column5: {
type: 'void',
'x-component': 'ArrayTable.Column',
'x-component-props': {
width: 200,
title: tval('After change'),
},
properties: {
after: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Value',
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
column1: {
type: 'void',
'x-decorator': 'TableV2.Column.Decorator',
'x-component': 'TableV2.Column',
properties: {
createdAt: {
'x-component': 'CollectionField',
'x-read-pretty': true,
},
},
},
column2: {
type: 'void',
'x-decorator': 'TableV2.Column.Decorator',
'x-component': 'TableV2.Column',
properties: {
type: {
'x-component': 'CollectionField',
'x-read-pretty': true,
},
},
},
column3: {
type: 'void',
'x-component': 'TableV2.Column',
title: tval('Collection display name'),
properties: {
collection: {
'x-component': 'Collection',
'x-read-pretty': true,
},
},
},
...ext,
column4: {
type: 'void',
'x-component': 'TableV2.Column',
title: tval('User'),
properties: {
user: {
'x-component': 'Username',
'x-read-pretty': true,
},
},
},
},
},
},
}}
/>
);
};
Message.Decorator = observer(
(props: any) => {
const parent = useCollection_deprecated();
const record = useRecord();
let filter = props?.params?.filter;
if (parent.name) {
const filterByTk = record?.[parent.filterTargetKey || 'id'];
if (filter) {
filter = {
$and: [
filter,
{
collectionName: parent.name,
recordId: `${filterByTk}`,
},
],
};
} else {
filter = {
collectionName: parent.name,
recordId: `${filterByTk}`,
};
}
}
const defaults = {
collection: 'messages',
resource: 'messages',
action: 'list',
params: {
pageSize: 20,
appends: ['collection', 'user'],
...props.params,
filter,
sort: '-createdAt',
},
rowKey: 'id',
showIndex: true,
dragSort: false,
};
return (
<IsAssociationBlock.Provider value={!!parent.name}>
<ExtendCollectionsProvider collections={[collection]}>
<TableBlockProvider {...defaults}>{props.children}</TableBlockProvider>
</ExtendCollectionsProvider>
</IsAssociationBlock.Provider>
);
},
{ displayName: 'Message.Decorator' },
);
Message.Designer = MessageDesigner;

View File

@ -0,0 +1,78 @@
import React from 'react';
import {
GeneralSchemaDesigner,
SchemaSettingsDataScope,
SchemaSettingsDivider,
SchemaSettingsRemove,
SchemaSettingsSelectItem,
useCollection_deprecated,
useDesignable,
useFormBlockContext,
useTableBlockContext,
} from '@tachybase/client';
import { useField, useFieldSchema } from '@tachybase/schema';
import { useTranslation } from '../locale';
export const MessageDesigner = () => {
const { name, title } = useCollection_deprecated();
const field = useField();
const fieldSchema = useFieldSchema();
const { form } = useFormBlockContext();
const { service } = useTableBlockContext();
const { t } = useTranslation();
const { dn } = useDesignable();
return (
<GeneralSchemaDesigner title={title || name}>
<SchemaSettingsDataScope
collectionName={name}
defaultFilter={fieldSchema?.['x-decorator-props']?.params?.filter || {}}
form={form}
onSubmit={({ filter }) => {
const params = field.decoratorProps.params || {};
params.filter = filter;
field.decoratorProps.params = params;
fieldSchema['x-decorator-props']['params'] = params;
service.run({ ...service.params?.[0], filter, page: 1 });
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-decorator-props': fieldSchema['x-decorator-props'],
},
});
}}
/>
<SchemaSettingsSelectItem
title={t('Records per page')}
value={field.decoratorProps?.params?.pageSize || 20}
options={[
{ label: '10', value: 10 },
{ label: '20', value: 20 },
{ label: '50', value: 50 },
{ label: '100', value: 100 },
{ label: '200', value: 200 },
]}
onChange={(pageSize) => {
const params = field.decoratorProps.params || {};
params.pageSize = pageSize;
field.decoratorProps.params = params;
fieldSchema['x-decorator-props']['params'] = params;
service.run({ ...service.params?.[0], pageSize, page: 1 });
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-decorator-props': fieldSchema['x-decorator-props'],
},
});
}}
/>
<SchemaSettingsDivider />
<SchemaSettingsRemove
removeParentsIfNoChildren
breakRemoveOn={{
'x-component': 'Grid',
}}
/>
</GeneralSchemaDesigner>
);
};

View File

@ -1,6 +1,14 @@
import { Plugin } from '@tachybase/client'; import { Plugin, SchemaInitializerItemType } from '@tachybase/client';
import PluginWorkflow from '@tachybase/plugin-workflow/client';
import { lang } from './locale'; import { MessageTableColumnInitializers } from './initializers/MessageColumnInitializers';
import { MessageTableActionColumnInitializers } from './initializers/MessageTableActionColumnInitializers';
import { MessageTableActionInitializers } from './initializers/MessageTableActionInitializers';
import { MessageInstruction } from './instructions/MessageInstruction';
import { lang, tval } from './locale';
import { MessageNotificationProvider } from './MessageNotificationProvider';
import { MessagePage } from './MessagePage';
import { MessageProvider } from './MessageProvider';
import { SubscriptionManager } from './SubscriptionManager'; import { SubscriptionManager } from './SubscriptionManager';
interface MessageType { interface MessageType {
@ -32,6 +40,31 @@ export class PluginMessagesClient extends Plugin {
// You can get and modify the app instance here // You can get and modify the app instance here
async load() { async load() {
this.app.addComponents({
Messages: MessagePage,
});
this.app.use(MessageProvider);
this.app.schemaInitializerManager.add(MessageTableActionInitializers);
this.app.schemaInitializerManager.add(MessageTableActionColumnInitializers);
this.app.schemaInitializerManager.add(MessageTableColumnInitializers);
const workflowPlugin = this.pm.get(PluginWorkflow);
workflowPlugin.registerInstruction('message-instruction', MessageInstruction);
const blockInitializers = this.app.schemaInitializerManager.get('page:addBlock');
const recordBlockInitializers = this.app.schemaInitializerManager.get('popup:common:addBlock');
const messages: Omit<SchemaInitializerItemType, 'name'> = {
title: tval('messages'),
Component: 'MessageBlockInitializer',
};
blockInitializers.add('otherBlocks.messages', messages);
recordBlockInitializers.add('otherBlocks.messages', messages);
this.app.use(MessageNotificationProvider);
this.app.router.add('admin.messages', {
path: '/admin/messages',
Component: MessagePage,
});
this.userSettingsManager.add('sub-manager', { this.userSettingsManager.add('sub-manager', {
title: lang('Subscription management'), title: lang('Subscription management'),
icon: 'BellOutlined', icon: 'BellOutlined',

View File

@ -0,0 +1,91 @@
import React from 'react';
import {
SchemaInitializer,
SchemaInitializerChildren,
useAssociatedTableColumnInitializerFields,
useCompile,
useInheritsTableColumnInitializerFields,
useTableColumnInitializerFields,
} from '@tachybase/client';
import { tval, useTranslation } from '../locale';
// 表格列配置
const ParentCollectionFields = () => {
const inheritFields = useInheritsTableColumnInitializerFields();
const { t } = useTranslation();
const compile = useCompile();
if (!inheritFields?.length) return null;
const res = [];
inheritFields.forEach((inherit) => {
Object.values(inherit)[0].length &&
res.push({
type: 'itemGroup',
divider: true,
title: t(`Parent collection fields`) + '(' + compile(`${Object.keys(inherit)[0]}`) + ')',
children: Object.values(inherit)[0].filter((v: any) => !v?.field?.isForeignKey),
});
});
return <SchemaInitializerChildren>{res}</SchemaInitializerChildren>;
};
const AssociatedFields = () => {
const associatedFields = useAssociatedTableColumnInitializerFields();
const { t } = useTranslation();
if (!associatedFields?.length) return null;
const schema: any = [
{
type: 'itemGroup',
divider: true,
title: t('Display association fields'),
children: associatedFields,
},
];
return <SchemaInitializerChildren>{schema}</SchemaInitializerChildren>;
};
export const MessageTableColumnInitializers = new SchemaInitializer({
name: 'MessageTable:configureColumns',
insertPosition: 'beforeEnd',
icon: 'SettingOutlined',
title: tval('Configure columns'),
wrap(s) {
if (s['x-action-column']) {
return s;
}
return {
type: 'void',
'x-toolbar': 'TableColumnSchemaToolbar',
'x-settings': 'fieldSettings:TableColumn',
'x-decorator': 'TableV2.Column.Decorator',
'x-component': 'TableV2.Column',
properties: {
[s.name]: {
...s,
},
},
};
},
items: [
{
name: 'displayFields',
type: 'itemGroup',
title: tval('Display fields'),
useChildren: useTableColumnInitializerFields,
},
{
name: 'parentCollectionFields',
Component: ParentCollectionFields,
},
{
name: 'associationFields',
Component: AssociatedFields,
},
{
name: 'actionColumn',
title: tval('Action column'),
Component: 'MessageTableActionColumnInitializer',
},
],
});

View File

@ -0,0 +1,29 @@
import React from 'react';
import { InitializerWithSwitch, useSchemaInitializerItem } from '@tachybase/client';
import { tval } from '../locale';
export const MessageTableActionColumnInitializer = () => {
const schema = {
type: 'void',
title: tval('Actions'),
'x-decorator': 'TableV2.Column.ActionBar',
'x-component': 'TableV2.Column',
'x-designer': 'TableV2.ActionColumnDesigner',
'x-initializer': 'MessageTable:configureItemActions',
'x-action-column': 'actions',
properties: {
actions: {
type: 'void',
'x-decorator': 'DndContext',
'x-component': 'Space',
'x-component-props': {
split: '|',
},
properties: {},
},
},
};
const itemConfig = useSchemaInitializerItem();
return <InitializerWithSwitch {...itemConfig} schema={schema} item={itemConfig} type={'x-action-column'} />;
};

View File

@ -0,0 +1,69 @@
import React from 'react';
import { createDesignable, Resizable, SchemaInitializer, useAPIClient, useDesignable } from '@tachybase/client';
import { useFieldSchema } from '@tachybase/schema';
import { MenuOutlined } from '@ant-design/icons';
import { tval, useTranslation } from '../locale';
export const MessageTableActionColumnInitializers = new SchemaInitializer({
name: 'MessageTable:configureItemActions',
insertPosition: 'beforeEnd',
Component: (props: any) => <MenuOutlined {...props} style={{ cursor: 'pointer' }} />,
useInsert() {
const fieldSchema = useFieldSchema();
const api = useAPIClient();
const { refresh } = useDesignable();
const { t } = useTranslation();
return (schema) => {
const spaceSchema = fieldSchema.reduceProperties((buf, schema) => {
if (schema['x-component'] === 'Space') {
return schema;
}
return buf;
}, null);
if (!spaceSchema) {
return;
}
const dn = createDesignable({
t,
api,
refresh,
current: spaceSchema,
});
dn.loadAPIClientEvents();
dn.insertBeforeEnd(schema);
};
},
items: [
{
name: 'enableActions',
type: 'itemGroup',
title: tval('Enable actions'),
children: [
{
name: 'view',
type: 'item',
title: tval('View'),
Component: 'MessageViewActionInitializer',
schema: {
'x-component': 'Action.Link',
'x-action': 'view',
'x-decorator': 'ACLActionProvider',
},
},
],
},
{
name: 'divider',
type: 'divider',
},
{
name: 'columnWidth',
type: 'item',
title: tval('Column width'),
Component: Resizable,
},
],
});

View File

@ -0,0 +1,39 @@
import { SchemaInitializer } from '@tachybase/client';
import { tval } from '../locale';
export type { ButtonProps } from 'antd';
export const MessageTableActionInitializers = new SchemaInitializer({
name: 'MessageTable:configureActions',
title: tval('Configure actions'),
icon: 'SettingOutlined',
style: {
marginLeft: 8,
},
items: [
{
type: 'itemGroup',
title: tval('Enable actions'),
name: 'enableActions',
children: [
{
name: 'filter',
title: tval('Filter'),
Component: 'FilterActionInitializer',
schema: {
'x-align': 'left',
},
},
{
name: 'refresh',
title: tval('Refresh'),
Component: 'RefreshActionInitializer',
schema: {
'x-align': 'right',
},
},
],
},
],
});

View File

@ -0,0 +1,27 @@
import { Instruction, VariableOption } from '@tachybase/plugin-workflow/client';
export class MessageInstruction extends Instruction {
title = 'Messages';
type = 'message-instruction';
group = 'extended';
fieldset = {
digit: {
type: 'number',
title: 'Digit',
name: 'digit',
'x-decorator': 'FormItem',
'x-component': 'InputNumber',
'x-component-props': {
min: 1,
max: 10,
},
default: 6,
},
};
useVariables(node, options): VariableOption {
return {
value: node.key,
label: node.title,
};
}
}

View File

@ -0,0 +1,14 @@
import { i18n, tval as nTval } from '@tachybase/client';
import { useTranslation as useT } from 'react-i18next';
export const NAMESPACE = 'messages';
export function lang(key: string) {
return i18n.t(key, { ns: NAMESPACE });
}
export function useTranslation() {
return useT(NAMESPACE);
}
export const tval = (key: string) => nTval(key, { ns: NAMESPACE });

View File

@ -0,0 +1,20 @@
import { FlowNodeModel, IJob, Instruction, InstructionResult, JOB_STATUS, Processor } from '@tachybase/plugin-workflow';
export class MessageInstruction extends Instruction {
async run(node: FlowNodeModel, input: any, processor: Processor): Promise<IJob> {
console.log('my instruction runs!');
const msgRepo = node.db.getRepository('messages');
await msgRepo.create({
values: {
title: 'testing',
content: 'testing',
},
});
return {
status: JOB_STATUS.RESOLVED,
};
}
}

View File

@ -1,11 +1,17 @@
import { PluginWorkflow } from '@tachybase/plugin-workflow';
import { Plugin } from '@tachybase/server'; import { Plugin } from '@tachybase/server';
import { MessageInstruction } from './instructions/message-instruction';
export class PluginMessagesServer extends Plugin { export class PluginMessagesServer extends Plugin {
async afterAdd() {} async afterAdd() {}
async beforeLoad() {} async beforeLoad() {}
async load() {} async load() {
const workflowPlugin = this.app.getPlugin<PluginWorkflow>(PluginWorkflow);
workflowPlugin.registerInstruction('message-instruction', MessageInstruction);
}
async install() {} async install() {}

View File

@ -0,0 +1,37 @@
import React from 'react';
import { css, PinnedPluginListProvider, SchemaComponentOptions, useToken } from '@tachybase/client';
import { MobileOutlined } from '@ant-design/icons';
import { Button, Tooltip } from 'antd';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from './locale';
const MobileLink = () => {
const navigate = useNavigate();
const { token } = useToken();
const { t } = useTranslation();
return (
<Tooltip title={t('Mobile UI')}>
<Button
icon={<MobileOutlined style={{ color: token.colorTextHeaderMenu }} />}
title={t('Mobile UI')}
onClick={() => {
navigate('/mobile');
}}
/>
</Tooltip>
);
};
export const MobileLinkProvider = (props) => {
return (
<PinnedPluginListProvider
items={{
me: { order: 210, component: 'MobileLink', pin: true, isPublic: true },
}}
>
<SchemaComponentOptions components={{ MobileLink }}>{props.children}</SchemaComponentOptions>
</PinnedPluginListProvider>
);
};

View File

@ -17,6 +17,7 @@ import {
import { NAMESPACE } from './locale'; import { NAMESPACE } from './locale';
import { MAppNotFound } from './MAppNotFound'; import { MAppNotFound } from './MAppNotFound';
import { MobileClientProvider } from './MobileClientProvider'; import { MobileClientProvider } from './MobileClientProvider';
import { MobileLinkProvider } from './MobileLinkProvider';
import MApplication from './router/Application'; import MApplication from './router/Application';
export class MobileClientPlugin extends Plugin { export class MobileClientPlugin extends Plugin {
@ -26,6 +27,7 @@ export class MobileClientPlugin extends Plugin {
this.addRoutes(); this.addRoutes();
this.addSettings(); this.addSettings();
this.app.use(MobileClientProvider); this.app.use(MobileClientProvider);
this.app.use(MobileLinkProvider);
this.app.schemaInitializerManager.add(mBlockInitializers_deprecated); this.app.schemaInitializerManager.add(mBlockInitializers_deprecated);
this.app.schemaInitializerManager.add(mBlockInitializers); this.app.schemaInitializerManager.add(mBlockInitializers);
this.app.schemaInitializerManager.add(ImageSearchConfigureFields); this.app.schemaInitializerManager.add(ImageSearchConfigureFields);

View File

@ -47,5 +47,6 @@
"AllProducts": "AllProducts", "AllProducts": "AllProducts",
"sort":"sort", "sort":"sort",
"Add new":"Add new", "Add new":"Add new",
"Mobile UI": "Mobile UI",
"please enter the date": "please enter the date" "please enter the date": "please enter the date"
} }

View File

@ -50,5 +50,6 @@
"TabSearch":"表格搜索", "TabSearch":"表格搜索",
"ImageSearch":"图片搜索", "ImageSearch":"图片搜索",
"Add new":"添加", "Add new":"添加",
"Mobile UI": "移动端页面",
"please enter the date": "请输入日期" "please enter the date": "请输入日期"
} }

View File

@ -3355,12 +3355,21 @@ importers:
packages/plugins/@tachybase/plugin-messages: packages/plugins/@tachybase/plugin-messages:
dependencies: dependencies:
'@ant-design/icons':
specifier: ~5.3.7
version: 5.3.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@tachybase/client': '@tachybase/client':
specifier: workspace:* specifier: workspace:*
version: link:../../../core/client version: link:../../../core/client
'@tachybase/database': '@tachybase/database':
specifier: workspace:* specifier: workspace:*
version: link:../../../core/database version: link:../../../core/database
'@tachybase/plugin-workflow':
specifier: workspace:*
version: link:../plugin-workflow
'@tachybase/schema':
specifier: workspace:*
version: link:../../../core/schema
'@tachybase/server': '@tachybase/server':
specifier: workspace:* specifier: workspace:*
version: link:../../../core/server version: link:../../../core/server
@ -3370,6 +3379,12 @@ importers:
antd: antd:
specifier: 5.19.4 specifier: 5.19.4
version: 5.19.4(date-fns@3.6.0)(luxon@3.5.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 5.19.4(date-fns@3.6.0)(luxon@3.5.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-i18next:
specifier: ^14.1.2
version: 14.1.2(i18next@23.13.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-router-dom:
specifier: ~6.25.1
version: 6.25.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
packages/plugins/@tachybase/plugin-mobile-client: packages/plugins/@tachybase/plugin-mobile-client:
dependencies: dependencies: