/**
* 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 (
{!isMobile && {t('Group')}}
{t('Page')}
{t('Link')}
);
},
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 ;
},
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 ;
},
'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 ;
},
'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 ;
},
'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 ? (
) : (
);
},
'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 (
{path}
);
}
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 (
{path}
);
}
return {t('Unknown')} ;
},
'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 (
{!isMobile && (
{t('Group')}
)}
{t('Page')}
{t('Link')}
{t('Tab')}
);
},
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 ;
},
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 ;
},
'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 ;
},
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 ;
},
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 ;
},
'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 {valueMap[props.value]};
}
// 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',
};
}