1455 lines
59 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
/* eslint-disable react-hooks/rules-of-hooks */
import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
import { useField, useForm } from '@formily/react';
import {
CollectionField,
css,
getPageMenuSchema,
getTabSchema,
getVariableComponentWithScope,
NocoBaseDesktopRouteType,
useActionContext,
useAllAccessDesktopRoutes,
useAPIClient,
useBlockRequestContext,
useCollectionRecordData,
useDataBlockRequestData,
useDataBlockRequestGetter,
useInsertPageSchema,
useNocoBaseRoutes,
useRequest,
useRouterBasename,
useTableBlockContextBasicValue,
Variable,
} from '@nocobase/client';
import { uid } from '@nocobase/utils/client';
import { Checkbox, Radio, Tag, Typography } from 'antd';
import _ from 'lodash';
import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useTableBlockProps } from './useTableBlockProps';
import { getSchemaUidByRouteId } from './utils';
const VariableTextArea = getVariableComponentWithScope(Variable.TextArea);
export const createRoutesTableSchema = (collectionName: string, basename: string) => {
const isMobile = collectionName === 'mobileRoutes';
return {
type: 'void',
name: uid(),
'x-decorator': 'TableBlockProvider',
'x-decorator-props': {
collection: collectionName,
action: 'list',
dragSort: false,
params: {
sort: ['sort'],
pageSize: 20,
filter: {
'hidden.$ne': true,
},
},
treeTable: true,
},
properties: {
actions: {
type: 'void',
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 16,
},
},
properties: {
refresh: {
title: "{{t('Refresh')}}",
'x-action': 'refresh',
'x-component': 'Action',
'x-use-component-props': 'useRefreshActionProps',
'x-component-props': {
icon: 'ReloadOutlined',
},
},
delete: {
type: 'void',
title: '{{t("Delete")}}',
'x-component': 'Action',
'x-use-component-props': () => {
const tableBlockContextBasicValue = useTableBlockContextBasicValue();
const { resource, service } = useBlockRequestContext();
const { deleteRouteSchema } = useDeleteRouteSchema();
const data = useDataBlockRequestData();
const { refresh: refreshMenu } = useAllAccessDesktopRoutes();
return {
async onClick() {
const filterByTk = tableBlockContextBasicValue.field?.data?.selectedRowKeys;
if (!filterByTk?.length) {
return;
}
for (const id of filterByTk) {
const schemaUid = getSchemaUidByRouteId(id, data?.data, isMobile);
await deleteRouteSchema(schemaUid);
}
await resource.destroy({
filterByTk,
});
tableBlockContextBasicValue.field.data.clearSelectedRowKeys?.();
service?.refresh?.();
collectionName === 'desktopRoutes' && refreshMenu();
},
};
},
'x-component-props': {
confirm: {
title: "{{t('Delete routes')}}",
content: "{{t('Are you sure you want to delete it?')}}",
},
icon: 'DeleteOutlined',
},
},
hide: {
type: 'void',
title: '{{t("Hide in menu")}}',
'x-component': 'Action',
'x-use-component-props': () => {
const tableBlockContextBasicValue = useTableBlockContextBasicValue();
const { service } = useBlockRequestContext();
const { refresh: refreshMenu } = useAllAccessDesktopRoutes();
const { updateRoute } = useNocoBaseRoutes(collectionName);
return {
async onClick() {
const filterByTk = tableBlockContextBasicValue.field?.data?.selectedRowKeys;
if (!filterByTk?.length) {
return;
}
await updateRoute(filterByTk, {
hideInMenu: true,
});
tableBlockContextBasicValue.field.data.clearSelectedRowKeys?.();
service?.refresh?.();
refreshMenu();
},
};
},
'x-component-props': {
icon: 'EyeInvisibleOutlined',
confirm: {
title: "{{t('Hide in menu')}}",
content: "{{t('Are you sure you want to hide these routes in menu?')}}",
},
},
},
show: {
type: 'void',
title: '{{t("Show in menu")}}',
'x-component': 'Action',
'x-use-component-props': () => {
const tableBlockContextBasicValue = useTableBlockContextBasicValue();
const { service } = useBlockRequestContext();
const { updateRoute } = useNocoBaseRoutes(collectionName);
return {
async onClick() {
const filterByTk = tableBlockContextBasicValue.field?.data?.selectedRowKeys;
if (!filterByTk?.length) {
return;
}
await updateRoute(filterByTk, {
hideInMenu: false,
});
tableBlockContextBasicValue.field.data.clearSelectedRowKeys?.();
service?.refresh?.();
},
};
},
'x-component-props': {
icon: 'EyeOutlined',
confirm: {
title: "{{t('Show in menu')}}",
content: "{{t('Are you sure you want to show these routes in menu?')}}",
},
},
},
create: {
type: 'void',
title: '{{t("Add new")}}',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
icon: 'PlusOutlined',
},
properties: {
drawer: {
type: 'void',
'x-component': 'Action.Drawer',
'x-decorator': 'Form',
'x-decorator-props': {
useValues(options) {
return {};
},
},
title: '{{t("Add new")}}',
properties: {
formSchema: {
type: 'void',
properties: {
type: {
type: 'string',
title: '{{t("Type")}}',
'x-decorator': 'FormItem',
'x-component': (props) => {
const { t } = useTranslation();
return (
<Radio.Group {...props}>
{!isMobile && <Radio value={NocoBaseDesktopRouteType.group}>{t('Group')}</Radio>}
<Radio value={NocoBaseDesktopRouteType.page}>{t('Page')}</Radio>
<Radio value={NocoBaseDesktopRouteType.link}>{t('Link')}</Radio>
</Radio.Group>
);
},
default: NocoBaseDesktopRouteType.page,
required: true,
},
title: {
type: 'string',
title: '{{t("Title")}}',
'x-decorator': 'FormItem',
'x-component': 'Input',
required: true,
},
icon: {
type: 'string',
title: '{{t("Icon")}}',
'x-decorator': 'FormItem',
'x-component': 'IconPicker',
'x-reactions': isMobile
? {
dependencies: ['type'],
fulfill: {
state: {
required: '{{$deps[0] !== "tabs"}}',
},
},
}
: undefined,
},
// 由于历史原因,桌面端使用的是 'href' 作为 key移动端使用的是 'url' 作为 key
[isMobile ? 'url' : 'href']: {
title: '{{t("URL")}}',
type: 'string',
'x-decorator': 'FormItem',
'x-component': VariableTextArea,
description: '{{t("Do not concatenate search params in the URL")}}',
'x-reactions': {
dependencies: ['type'],
fulfill: {
state: {
hidden: '{{$deps[0] !== "link"}}',
},
},
},
},
params: {
type: 'array',
'x-component': 'ArrayItems',
'x-decorator': 'FormItem',
title: `{{t("Search parameters")}}`,
items: {
type: 'object',
properties: {
space: {
type: 'void',
'x-component': 'Space',
'x-component-props': {
style: {
flexWrap: 'nowrap',
maxWidth: '100%',
},
className: css`
& > .ant-space-item:first-child,
& > .ant-space-item:last-child {
flex-shrink: 0;
}
`,
},
properties: {
name: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: `{{t("Name")}}`,
},
},
value: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': VariableTextArea,
'x-component-props': {
placeholder: `{{t("Value")}}`,
useTypedConstant: true,
changeOnSelect: true,
},
},
remove: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.Remove',
},
},
},
},
},
'x-reactions': {
dependencies: ['type'],
fulfill: {
state: {
hidden: '{{$deps[0] !== "link"}}',
},
},
},
properties: {
add: {
type: 'void',
title: `{{t("Add parameter")}}`,
'x-component': 'ArrayItems.Addition',
},
},
},
hideInMenu: {
type: 'boolean',
title: '{{t("Show in menu")}}',
'x-decorator': 'FormItem',
'x-decorator-props': {
tooltip: '{{t(`If selected, the route will be displayed in the menu.`)}}',
},
'x-component': (props) => {
const [checked, setChecked] = useState(!props.value);
const onChange = () => {
setChecked(!checked);
props.onChange?.(checked);
};
return <Checkbox checked={checked} onChange={onChange} />;
},
default: false,
},
enableTabs: {
type: 'boolean',
title: '{{t("Enable page tabs")}}',
'x-decorator': 'FormItem',
'x-decorator-props': {
tooltip: '{{t(`If selected, the page will display Tab pages.`)}}',
},
'x-component': (props) => {
return <Checkbox checked={props.value} onChange={props.onChange} />;
},
'x-reactions': {
dependencies: ['type'],
fulfill: {
state: {
hidden: '{{$deps[0] !== "page"}}',
},
},
},
default: false,
},
},
},
footer: {
type: 'void',
'x-component': 'Action.Drawer.Footer',
properties: {
cancel: {
title: '{{t("Cancel")}}',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ cm.useCancelAction }}',
},
},
submit: {
title: '{{t("Submit")}}',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
useAction: (actionCallback?: (values: any) => void) => {
const form = useForm();
const field = useField();
const ctx = useActionContext();
const { getDataBlockRequest } = useDataBlockRequestGetter();
const { createRoute } = useNocoBaseRoutes(collectionName);
const { createRouteSchema } = useCreateRouteSchema(isMobile);
return {
async run() {
try {
await form.submit();
field.data = field.data || {};
field.data.loading = true;
const { pageSchemaUid, tabSchemaUid, menuSchemaUid, tabSchemaName } =
await createRouteSchema(form.values);
let options;
if (form.values.href || !_.isEmpty(form.values.params)) {
options = {
params: form.values.params,
// 由于历史原因,桌面端使用的是 'href' 作为 key
href: isMobile ? undefined : form.values.href,
// 由于历史原因,移动端使用的是 'url' 作为 key
url: isMobile ? form.values.url : undefined,
};
}
const res = await createRoute({
..._.omit(form.values, ['href', 'params', 'url']),
schemaUid:
NocoBaseDesktopRouteType.page === form.values.type
? pageSchemaUid
: menuSchemaUid,
menuSchemaUid,
options,
});
if (tabSchemaUid) {
await createRoute({
schemaUid: tabSchemaUid,
parentId: res?.data?.data?.id,
type: NocoBaseDesktopRouteType.tabs,
tabSchemaName,
hidden: true,
});
}
ctx.setVisible(false);
actionCallback?.(res?.data?.data);
await form.reset();
field.data.loading = false;
getDataBlockRequest()?.refresh();
} catch (error) {
if (field.data) {
field.data.loading = false;
}
throw error;
}
},
};
},
},
},
},
},
},
},
},
},
filter: {
'x-action': 'filter',
type: 'object',
'x-component': 'Filter.Action',
title: "{{t('Filter')}}",
'x-use-component-props': 'useFilterActionProps',
'x-component-props': {
icon: 'FilterOutlined',
},
'x-align': 'left',
},
},
},
table: {
type: 'array',
'x-component': 'TableV2',
'x-use-component-props': useTableBlockProps,
'x-component-props': {
rowKey: 'id',
rowSelection: {
type: 'checkbox',
},
},
properties: {
title: {
type: 'void',
'x-component': 'TableV2.Column',
title: '{{t("Title")}}',
'x-component-props': {
width: 200,
},
properties: {
title: {
type: 'string',
'x-component': function Com(props) {
const record = useCollectionRecordData();
const { t } = useTranslation();
let value = props.value;
if (record.type === NocoBaseDesktopRouteType.tabs && _.isNil(props.value)) {
value = t('Unnamed');
}
return <CollectionField {...props} value={value} />;
},
'x-read-pretty': true,
'x-component-props': {
ellipsis: true,
},
},
},
},
type: {
type: 'void',
'x-component': 'TableV2.Column',
title: '{{t("Type")}}',
'x-component-props': {
width: 100,
},
properties: {
type: {
type: 'string',
'x-component': (props) => {
return <TypeTag value={props.value} />;
},
'x-read-pretty': true,
'x-component-props': {
ellipsis: true,
},
},
},
},
hideInMenu: {
type: 'void',
'x-component': 'TableV2.Column',
title: '{{t("Show in menu")}}',
'x-component-props': {
width: 100,
},
properties: {
hideInMenu: {
type: 'boolean',
'x-component': (props) => {
return props.value ? (
<CloseOutlined style={{ color: '#ff4d4f' }} />
) : (
<CheckOutlined style={{ color: '#52c41a' }} />
);
},
'x-read-pretty': true,
'x-component-props': {
ellipsis: true,
},
},
},
},
path: {
title: '{{t("Path")}}',
type: 'void',
'x-component': 'TableV2.Column',
'x-component-props': {
width: 300,
},
properties: {
path: {
type: 'string',
'x-component': function Com() {
const data = useDataBlockRequestData();
const recordData = useCollectionRecordData();
const basenameOfCurrentRouter = useRouterBasename();
const { t } = useTranslation();
if (recordData.type === NocoBaseDesktopRouteType.group) {
return null;
}
if (recordData.type === NocoBaseDesktopRouteType.link) {
return null;
}
if (recordData.type === NocoBaseDesktopRouteType.page) {
const path = `${basenameOfCurrentRouter.slice(0, -1)}${basename}/${
isMobile ? recordData.schemaUid : recordData.menuSchemaUid
}`;
// 在点击 Access 按钮时,会用到
recordData._path = path;
return (
<Typography.Paragraph copyable style={{ marginBottom: 0 }} ellipsis>
{path}
</Typography.Paragraph>
);
}
if (recordData.type === NocoBaseDesktopRouteType.tabs && data?.data) {
const path = `${basenameOfCurrentRouter.slice(0, -1)}${basename}/${getSchemaUidByRouteId(
recordData.parentId,
data.data,
isMobile,
)}/tabs/${recordData.tabSchemaName || recordData.schemaUid}`;
recordData._path = path;
return (
<Typography.Paragraph copyable style={{ marginBottom: 0 }} ellipsis>
{path}
</Typography.Paragraph>
);
}
return <Tag>{t('Unknown')} </Tag>;
},
'x-read-pretty': true,
},
},
},
actions: {
type: 'void',
title: '{{t("Actions")}}',
'x-component': 'TableV2.Column',
properties: {
addChild: {
type: 'void',
title: '{{t("Add child route")}}',
'x-component': 'Action.Link',
'x-use-component-props': () => {
const recordData = useCollectionRecordData();
return {
disabled:
(recordData.type !== NocoBaseDesktopRouteType.group &&
recordData.type !== NocoBaseDesktopRouteType.page) ||
(!recordData.enableTabs && recordData.type === NocoBaseDesktopRouteType.page),
openMode: 'drawer',
};
},
'x-decorator': 'Space',
properties: {
drawer: {
type: 'void',
'x-component': 'Action.Drawer',
'x-decorator': 'Form',
'x-decorator-props': {
useValues(options) {
return {};
},
},
title: '{{t("Add child route")}}',
properties: {
formSchema: {
type: 'void',
properties: {
type: {
type: 'string',
title: '{{t("Type")}}',
'x-decorator': 'FormItem',
'x-component': (props) => {
const { t } = useTranslation();
const recordData = useCollectionRecordData();
const isPage = recordData.type === NocoBaseDesktopRouteType.page;
const isGroup = recordData.type === NocoBaseDesktopRouteType.group;
const defaultValue = useMemo(() => {
if (isPage) {
props.onChange(NocoBaseDesktopRouteType.tabs);
return NocoBaseDesktopRouteType.tabs;
}
return NocoBaseDesktopRouteType.page;
}, [isPage, props]);
return (
<Radio.Group {...props} defaultValue={defaultValue}>
{!isMobile && (
<Radio value={NocoBaseDesktopRouteType.group} disabled={!isGroup}>
{t('Group')}
</Radio>
)}
<Radio value={NocoBaseDesktopRouteType.page} disabled={!isGroup}>
{t('Page')}
</Radio>
<Radio value={NocoBaseDesktopRouteType.link} disabled={!isGroup}>
{t('Link')}
</Radio>
<Radio value={NocoBaseDesktopRouteType.tabs} disabled={!isPage}>
{t('Tab')}
</Radio>
</Radio.Group>
);
},
required: true,
default: NocoBaseDesktopRouteType.page,
},
title: {
type: 'string',
title: '{{t("Title")}}',
'x-decorator': 'FormItem',
'x-component': 'Input',
required: true,
},
icon: {
type: 'string',
title: '{{t("Icon")}}',
'x-decorator': 'FormItem',
'x-component': 'IconPicker',
'x-reactions': isMobile
? {
dependencies: ['type'],
fulfill: {
state: {
required: '{{$deps[0] !== "tabs"}}',
},
},
}
: undefined,
},
// 由于历史原因,桌面端使用的是 'href' 作为 key移动端使用的是 'url' 作为 key
[isMobile ? 'url' : 'href']: {
title: '{{t("URL")}}',
type: 'string',
'x-decorator': 'FormItem',
'x-component': VariableTextArea,
description: '{{t("Do not concatenate search params in the URL")}}',
'x-reactions': {
dependencies: ['type'],
fulfill: {
state: {
hidden: '{{$deps[0] !== "link"}}',
},
},
},
},
params: {
type: 'array',
'x-component': 'ArrayItems',
'x-decorator': 'FormItem',
title: `{{t("Search parameters")}}`,
items: {
type: 'object',
properties: {
space: {
type: 'void',
'x-component': 'Space',
'x-component-props': {
style: {
flexWrap: 'nowrap',
maxWidth: '100%',
},
className: css`
& > .ant-space-item:first-child,
& > .ant-space-item:last-child {
flex-shrink: 0;
}
`,
},
properties: {
name: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: `{{t("Name")}}`,
},
},
value: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': VariableTextArea,
'x-component-props': {
placeholder: `{{t("Value")}}`,
useTypedConstant: true,
changeOnSelect: true,
},
},
remove: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.Remove',
},
},
},
},
},
'x-reactions': {
dependencies: ['type'],
fulfill: {
state: {
hidden: '{{$deps[0] !== "link"}}',
},
},
},
properties: {
add: {
type: 'void',
title: `{{t("Add parameter")}}`,
'x-component': 'ArrayItems.Addition',
},
},
},
hideInMenu: {
type: 'boolean',
title: '{{t("Show in menu")}}',
'x-decorator': 'FormItem',
'x-decorator-props': {
tooltip: '{{t(`If selected, the route will be displayed in the menu.`)}}',
},
'x-component': (props) => {
const [checked, setChecked] = useState(!props.value);
const onChange = () => {
setChecked(!checked);
props.onChange?.(checked);
};
return <Checkbox checked={checked} onChange={onChange} />;
},
default: false,
},
enableTabs: {
type: 'boolean',
title: '{{t("Enable page tabs")}}',
'x-decorator': 'FormItem',
'x-decorator-props': {
tooltip: '{{t(`If selected, the page will display Tab pages.`)}}',
},
'x-component': (props) => {
return <Checkbox checked={props.value} onChange={props.onChange} />;
},
'x-reactions': {
dependencies: ['type'],
fulfill: {
state: {
hidden: '{{$deps[0] !== "page"}}',
},
},
},
default: false,
},
},
},
footer: {
type: 'void',
'x-component': 'Action.Drawer.Footer',
properties: {
cancel: {
title: '{{t("Cancel")}}',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ cm.useCancelAction }}',
},
},
submit: {
title: '{{t("Submit")}}',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
useAction: () => {
const form = useForm();
const field = useField();
const ctx = useActionContext();
const { getDataBlockRequest } = useDataBlockRequestGetter();
const { createRoute } = useNocoBaseRoutes(collectionName);
const { createRouteSchema, createTabRouteSchema } = useCreateRouteSchema(isMobile);
const recordData = useCollectionRecordData();
return {
async run() {
try {
await form.submit();
field.data = field.data || {};
field.data.loading = true;
if (form.values.type === NocoBaseDesktopRouteType.tabs) {
const { tabSchemaUid, tabSchemaName } = await createTabRouteSchema({
...form.values,
parentSchemaUid: recordData.schemaUid,
});
await createRoute({
parentId: recordData.id,
type: NocoBaseDesktopRouteType.tabs,
schemaUid: tabSchemaUid,
tabSchemaName,
...form.values,
});
} else {
let options;
const { pageSchemaUid, tabSchemaUid, menuSchemaUid, tabSchemaName } =
await createRouteSchema(form.values);
if (form.values.href || !_.isEmpty(form.values.params)) {
options = {
href: form.values.href,
params: form.values.params,
};
}
const res = await createRoute({
parentId: recordData.id,
..._.omit(form.values, ['href', 'params']),
schemaUid:
NocoBaseDesktopRouteType.page === form.values.type
? pageSchemaUid
: menuSchemaUid,
menuSchemaUid,
options,
});
if (tabSchemaUid) {
await createRoute({
parentId: res?.data?.data?.id,
type: NocoBaseDesktopRouteType.tabs,
schemaUid: tabSchemaUid,
tabSchemaName,
hidden: true,
});
}
}
ctx.setVisible(false);
await form.reset();
field.data.loading = false;
getDataBlockRequest()?.refresh();
} catch (error) {
if (field.data) {
field.data.loading = false;
}
throw error;
}
},
};
},
},
},
},
},
},
},
},
},
edit: {
type: 'void',
title: '{{t("Edit")}}',
'x-component': 'Action.Link',
'x-component-props': {
openMode: 'drawer',
},
'x-decorator': 'Space',
properties: {
drawer: {
type: 'void',
'x-component': 'Action.Drawer',
'x-decorator': 'Form',
'x-decorator-props': {
useValues(options) {
const recordData = useCollectionRecordData();
const ctx = useActionContext();
return useRequest(
() =>
Promise.resolve({
data: {
...recordData,
href: recordData.options?.href,
params: recordData.options?.params,
url: recordData.options?.url,
},
}),
{ ...options, refreshDeps: [ctx.visible] },
);
},
},
title: '{{t("Edit")}}',
properties: {
formSchema: {
type: 'void',
properties: {
type: {
type: 'string',
title: '{{t("Type")}}',
'x-decorator': 'FormItem',
'x-component': (props) => {
return <TypeTag value={props.value} />;
},
default: NocoBaseDesktopRouteType.page,
},
title: {
type: 'string',
title: '{{t("Title")}}',
'x-decorator': 'FormItem',
'x-component': 'Input',
required: true,
},
icon: {
type: 'string',
title: '{{t("Icon")}}',
'x-decorator': 'FormItem',
'x-component': 'IconPicker',
'x-reactions': isMobile
? {
dependencies: ['type'],
fulfill: {
state: {
required: '{{$deps[0] !== "tabs"}}',
},
},
}
: undefined,
},
// 由于历史原因,桌面端使用的是 'href' 作为 key移动端使用的是 'url' 作为 key
[isMobile ? 'url' : 'href']: {
title: '{{t("URL")}}',
type: 'string',
'x-decorator': 'FormItem',
'x-component': VariableTextArea,
description: '{{t("Do not concatenate search params in the URL")}}',
'x-reactions': {
dependencies: ['type'],
fulfill: {
state: {
hidden: '{{$deps[0] !== "link"}}',
},
},
},
},
params: {
type: 'array',
'x-component': 'ArrayItems',
'x-decorator': 'FormItem',
title: `{{t("Search parameters")}}`,
items: {
type: 'object',
properties: {
space: {
type: 'void',
'x-component': 'Space',
'x-component-props': {
style: {
flexWrap: 'nowrap',
maxWidth: '100%',
},
className: css`
& > .ant-space-item:first-child,
& > .ant-space-item:last-child {
flex-shrink: 0;
}
`,
},
properties: {
name: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: `{{t("Name")}}`,
},
},
value: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': VariableTextArea,
'x-component-props': {
placeholder: `{{t("Value")}}`,
useTypedConstant: true,
changeOnSelect: true,
},
},
remove: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.Remove',
},
},
},
},
},
'x-reactions': {
dependencies: ['type'],
fulfill: {
state: {
hidden: '{{$deps[0] !== "link"}}',
},
},
},
properties: {
add: {
type: 'void',
title: `{{t("Add parameter")}}`,
'x-component': 'ArrayItems.Addition',
},
},
},
hideInMenu: {
type: 'boolean',
title: '{{t("Show in menu")}}',
'x-decorator': 'FormItem',
'x-decorator-props': {
tooltip: '{{t(`If selected, the route will be displayed in the menu.`)}}',
},
'x-component': (props) => {
const [checked, setChecked] = useState(!props.value);
const onChange = () => {
setChecked(!checked);
props.onChange?.(checked);
};
return <Checkbox checked={checked} onChange={onChange} />;
},
default: false,
},
enableTabs: {
type: 'boolean',
title: '{{t("Enable page tabs")}}',
'x-decorator': 'FormItem',
'x-decorator-props': {
tooltip: '{{t(`If selected, the page will display Tab pages.`)}}',
},
'x-component': (props) => {
return <Checkbox checked={props.value} onChange={props.onChange} />;
},
'x-reactions': {
dependencies: ['type'],
fulfill: {
state: {
hidden: '{{$deps[0] !== "page"}}',
},
},
},
default: false,
},
},
},
footer: {
type: 'void',
'x-component': 'Action.Drawer.Footer',
properties: {
cancel: {
title: '{{t("Cancel")}}',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ cm.useCancelAction }}',
},
},
submit: {
title: '{{t("Submit")}}',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
useAction: (actionCallback?: (values: any) => void) => {
const form = useForm();
const field = useField();
const recordData = useCollectionRecordData();
const ctx = useActionContext();
const { getDataBlockRequest } = useDataBlockRequestGetter();
const { updateRoute } = useNocoBaseRoutes(collectionName);
return {
async run() {
try {
await form.submit();
field.data = field.data || {};
field.data.loading = true;
let options;
if (form.values.href || !_.isEmpty(form.values.params)) {
options = {
href: form.values.href,
params: form.values.params,
};
}
const res = await updateRoute(recordData.id, {
..._.omit(form.values, ['href', 'params']),
options,
});
ctx.setVisible(false);
actionCallback?.(res?.data?.data);
await form.reset();
field.data.loading = false;
getDataBlockRequest()?.refresh();
} catch (error) {
if (field.data) {
field.data.loading = false;
}
throw error;
}
},
};
},
},
},
},
},
},
},
},
},
access: {
type: 'void',
title: '{{t("View")}}',
'x-component': 'Action.Link',
'x-use-component-props': () => {
const recordData = useCollectionRecordData();
return {
onClick: () => {
window.open(recordData._path, '_blank');
},
disabled: !recordData._path,
};
},
'x-decorator': 'Space',
},
delete: {
type: 'void',
title: '{{t("Delete")}}',
'x-decorator': 'Space',
'x-component': 'Action.Link',
'x-use-component-props': () => {
const recordData = useCollectionRecordData();
const api = useAPIClient();
const resource = useMemo(() => api.resource(collectionName), [api]);
const { getDataBlockRequest } = useDataBlockRequestGetter();
const { deleteRouteSchema } = useDeleteRouteSchema();
return {
onClick: async () => {
await deleteRouteSchema(recordData.schemaUid);
resource
.destroy({
filterByTk: recordData.id,
})
.then(() => {
getDataBlockRequest().refresh();
})
.catch((error) => {
console.error(error);
});
},
};
},
'x-component-props': {
confirm: {
title: "{{t('Delete route')}}",
content: "{{t('Are you sure you want to delete it?')}}",
},
},
},
},
},
},
},
},
};
};
function useCreateRouteSchema(isMobile: boolean) {
const collectionName = 'uiSchemas';
const api = useAPIClient();
const resource = useMemo(() => api.resource(collectionName), [api, collectionName]);
const insertPageSchema = useInsertPageSchema();
const createRouteSchema = useCallback(
async ({ type }: { type: NocoBaseDesktopRouteType }) => {
const menuSchemaUid = isMobile ? undefined : uid();
const pageSchemaUid = uid();
const tabSchemaName = uid();
const tabSchemaUid = type === NocoBaseDesktopRouteType.page ? uid() : undefined;
const typeToSchema = {
[NocoBaseDesktopRouteType.page]: isMobile
? getMobilePageSchema(pageSchemaUid, tabSchemaUid).schema
: getPageMenuSchema({
pageSchemaUid,
tabSchemaUid,
tabSchemaName,
}),
};
if (!typeToSchema[type]) {
return {};
}
if (isMobile) {
await resource['insertAdjacent']({
resourceIndex: 'mobile',
position: 'beforeEnd',
values: {
schema: typeToSchema[type],
},
});
} else {
await insertPageSchema(typeToSchema[type]);
}
return { menuSchemaUid, pageSchemaUid, tabSchemaUid, tabSchemaName };
},
[isMobile, resource, insertPageSchema],
);
/**
* 创建 Tab 的接口和其它的不太一样,所以单独实现一个方法
*/
const createTabRouteSchema = useCallback(
async ({ title, icon, parentSchemaUid }: { title: string; icon: string; parentSchemaUid: string }) => {
const tabSchemaUid = uid();
const tabSchemaName = uid();
await resource[`insertAdjacent/${parentSchemaUid}`]({
position: 'beforeEnd',
values: {
schema: isMobile
? getPageContentTabSchema(tabSchemaUid)
: getTabSchema({ title, icon, schemaUid: tabSchemaUid, tabSchemaName }),
},
});
return { tabSchemaUid, tabSchemaName };
},
[isMobile, resource],
);
return { createRouteSchema, createTabRouteSchema };
}
function useDeleteRouteSchema(collectionName = 'uiSchemas') {
const api = useAPIClient();
const resource = useMemo(() => api.resource(collectionName), [api, collectionName]);
const { refresh: refreshMenu } = useAllAccessDesktopRoutes();
const deleteRouteSchema = useCallback(
async (schemaUid: string) => {
const res = await resource[`remove/${schemaUid}`]();
refreshMenu();
return res;
},
[resource, refreshMenu],
);
return { deleteRouteSchema };
}
function TypeTag(props) {
const { t } = useTranslation();
const colorMap = {
[NocoBaseDesktopRouteType.group]: 'blue',
[NocoBaseDesktopRouteType.page]: 'green',
[NocoBaseDesktopRouteType.link]: 'red',
[NocoBaseDesktopRouteType.tabs]: 'orange',
};
const valueMap = {
[NocoBaseDesktopRouteType.group]: t('Group'),
[NocoBaseDesktopRouteType.page]: t('Page'),
[NocoBaseDesktopRouteType.link]: t('Link'),
[NocoBaseDesktopRouteType.tabs]: t('Tab'),
};
return <Tag color={colorMap[props.value]}>{valueMap[props.value]}</Tag>;
}
// copy from @nocobase/plugin-mobile/client
// TODO: 需要把相关代码移动到 @nocobase/plugin-mobile
const spaceClassName = css(`
&:first-child {
.ant-space-item {
width: 30px;
height: 30px;
transform: rotate(45deg);
span {
position: relative;
bottom: -15px;
right: -8px;
transform: rotate(-45deg);
font-size: 10px;
}
}
}
`);
// copy from @nocobase/plugin-mobile/client
// TODO: 需要把相关代码移动到 @nocobase/plugin-mobile
const mobilePageHeaderSchema = {
type: 'void',
'x-component': 'MobilePageHeader',
properties: {
pageNavigationBar: {
type: 'void',
'x-component': 'MobilePageNavigationBar',
properties: {
actionBar: {
type: 'void',
'x-component': 'MobileNavigationActionBar',
'x-initializer': 'mobile:navigation-bar:actions',
'x-component-props': {
spaceProps: {
style: {
flexWrap: 'nowrap',
},
},
},
properties: {},
},
},
},
pageTabs: {
type: 'void',
'x-component': 'MobilePageTabs',
},
},
};
// copy from @nocobase/plugin-mobile/client
// TODO: 需要把相关代码移动到 @nocobase/plugin-mobile
function getMobilePageSchema(pageSchemaUid: string, firstTabUid: string) {
const pageSchema = {
type: 'void',
name: pageSchemaUid,
'x-uid': pageSchemaUid,
'x-component': 'MobilePageProvider',
'x-settings': 'mobile:page',
'x-decorator': 'BlockItem',
'x-decorator-props': {
style: {
height: '100%',
},
},
'x-toolbar-props': {
draggable: false,
spaceWrapperStyle: { right: -15, top: -15 },
spaceClassName,
toolbarStyle: {
overflowX: 'hidden',
},
},
properties: {
header: mobilePageHeaderSchema,
content: getMobilePageContentSchema(firstTabUid),
},
};
return { schema: pageSchema };
}
// copy from @nocobase/plugin-mobile/client
// TODO: 需要把相关代码移动到 @nocobase/plugin-mobile
function getMobilePageContentSchema(firstTabUid: string) {
return {
type: 'void',
'x-component': 'MobilePageContent',
properties: {
[firstTabUid]: getPageContentTabSchema(firstTabUid),
},
};
}
// copy from @nocobase/plugin-mobile/client
// TODO: 需要把相关代码移动到 @nocobase/plugin-mobile
function getPageContentTabSchema(pageSchemaUid: string) {
return {
type: 'void',
'x-uid': pageSchemaUid,
'x-async': true,
'x-component': 'Grid',
'x-component-props': {
showDivider: false,
},
'x-initializer': 'mobile:addBlock',
};
}