tachybase_todo/packages/plugins/graph-collection-manager/src/client/components/Entity.tsx
jack zhang 2cb1203aa4
refactor(client)!: application, router and plugin (#2068)
BREAKING CHANGE:

* refactor: update umi version 3.x to version 4.x

* refactor: update react-router-dom version to 6.x

* refactor(react-router-dom): change Layout Component `props.children` to `<Outlet />`

* refactor(react-router-dom): change <Route /> props and <RouteSwitch /> correct

* refactor(react-router-dom): replace `<Redirect />` to `<Navigate replace />`

* refactor(react-router-dom): replace `useHistory` to `useNavigate`

* refactor(react-router-dom): replace `useRouteMatch` to `useParams`

* refactor(react-router-dom & dumi): fix <RouteSwitch /> & umi document bug

* refactor(react-router-dom): `useRoutes` Optimize `<RouteSwitch />` code

* refactor(react-router-dom): update `Route` types and docs

* refactor(react-router-dom): optimize RouteSwitch code

* refactor(react-router-dom): `useLocation` no generics type

* refactor(react-router-dom): add `less v3.9.0` to `resolutions` to solve the error of `gulp-less`

* refactor(react-router-dom): fix `<RouteSwitch />`  `props.routes` as an array is not handled

* chore: upgrade `dumi` and refactor docs

* fix: completed code review, add `targets` to solve browser compatibility & removed `chainWebpack`

* refactor(dumi): upgraded dumi under `packages/core/client`

* refactor(dumi): delete `packages/core/dumi-theme-nocobase`

* refactor(dumi): degrade `react`  & replace `dumi-theme-antd` to `dumi-theme-nocobase`

* refactor(dumi): solve conflicts between multiple dumi applications

* fix: login page error in react 17

* refactor(dumi): remove less resolutions

* refactor(dumi): umi add `msfu: true` config

* fix: merge bug

* fix: self code review

* fix: code reivew and test bug

* refactor: upgrade react to 18

* refactor: degrade react types to 17

* chore: fix ci error

* fix: support routerBase & fix workflow page params

* fix(doc): menu externel link

* fix: build error

* fix: delete

* fix: vitest error

* fix: react-router new code replace

* fix: vitest markdown error

* fix: title is none when refresh

* fix: merge error

* fix: sidebar width is wrong

* fix: useProps error

* fix: side-menu-width

* fix: menu selectId is wrong & useProps is string

* fix: menu selected first default & side menu hide when change

* fix: test error & v0.10 change log

* fix: new compnent doc modify

* fix: set umi `fastRefresh=false`

* refactor: application v2

* fix: improve code

* fix: bug

* fix: page = 0 error

* fix: workflow navigate error

* feat: plugin manager

* fix: afterAdd

* feat: complete basic functional refactor

* fix: performance Application

* feat: support client and server build

* refactor: nocobase build-in plugin and providers

* fix: server can't start

* refactor: all plugins package `Prodiver` change to `Plugin`

* feat: nested router and change mobile client

* feat: delete application-v1 and router-switch

* feat: improve routes

* fix: change mobile not nested

* feat: delete RouteSwitchContext and change buildin Provider to Plugin

* feat: delete RouteSwitchContext plugins

* fix: refactor SchemaComponentOptions

* feat: improve SchemaComponentOptions

* fix: add useAdminSchemaUid

* fix: merge master error

* fix: vitest error

* fix: bug

* feat: bugs

* fix: improve code

* fix: restore code

* feat: vitest

* fix: bugs

* fix: bugs

* docs: update doc

* feat: improve code

* feat: add docs and imporve code

* fix: bugs

* feat: add tests

* fix: remove deps

* fix: muti app router error

* fix: router error

* fix: workflow error

* fix: cli error

* feat: change NoCobase -> Nocobase

* fix: code review

* fix: type error

* fix: cli error and plugin demo

* feat: update doc theme

* fix: build error

* fix: mobile router

* fix: code rewview

* fix: bug

* fix: test bug

* fix: bug

* refactor: add the "client" directory to all plugins

* refactor: modify samples client and plugin template

* fix: merge error

* fix: add files in package.json

* refactor: add README to files in package.json

* fix: adjust plugins depencies

* refactor: completing plugins' devDependencies and dependencies

* fix: bug

* refactor: remove @emotion/css

* refactor: jsonwebtoken deps

* refactor: remove sequelize

* refactor: dayjs and moment deps

* fix: bugs

* fix: bug

* fix: cycle detect

* fix: merge bug

* feat: new plugin bug

* fix: lang bug

* fix: dynamic import bug

* refactor: plugins and example add father config

* feat: improve code

* fix: add AppSpin and AppError components

* Revert "refactor: plugins and example add father config"

This reverts commit 483315bca5524e4b8cbbb20cbad77986f081089d.

# Conflicts:
#	packages/plugins/auth/package.json
#	packages/plugins/multi-app-manager/package.json
#	packages/samples/command/package.json
#	packages/samples/custom-collection-template/package.json
#	packages/samples/ratelimit/package.json
#	packages/samples/shop-actions/package.json
#	packages/samples/shop-events/package.json
#	packages/samples/shop-modeling/package.json

* feat: update doc

---------

Co-authored-by: chenos <chenlinxh@gmail.com>
2023-07-07 14:35:22 +08:00

511 lines
17 KiB
TypeScript

import { DeleteOutlined, DownOutlined, EditOutlined, UpOutlined } from '@ant-design/icons';
import '@antv/x6-react-shape';
import { uid } from '@formily/shared';
import {
Action,
Checkbox,
collection,
CollectionCategroriesContext,
CollectionField,
CollectionProvider,
css,
cx,
Form,
FormItem,
Formula,
Grid,
Input,
InputNumber,
Radio,
ResourceActionProvider,
SchemaComponent,
SchemaComponentProvider,
Select,
useCollectionManager,
useCompile,
useCurrentAppInfo,
useRecord,
} from '@nocobase/client';
import { lodash } from '@nocobase/utils/client';
import { Badge, Dropdown, Popover, Tag } from 'antd';
import React, { useContext, useRef, useState } from 'react';
import {
useAsyncDataSource,
useCancelAction,
useDestroyActionAndRefreshCM,
useDestroyFieldActionAndRefreshCM,
useUpdateCollectionActionAndRefreshCM,
useValuesFromRecord,
} from '../action-hooks';
import { collectiionPopoverClass, entityContainer, headClass, tableBtnClass, tableNameClass } from '../style';
import { getPopupContainer, useGCMTranslation } from '../utils';
import { AddFieldAction } from './AddFieldAction';
import { CollectionNodeProvder } from './CollectionNodeProvder';
import { EditCollectionAction } from './EditCollectionAction';
import { EditFieldAction } from './EditFieldAction';
import { FieldSummary } from './FieldSummary';
import { OverrideFieldAction } from './OverrideFieldAction';
import { ViewFieldAction } from './ViewFieldAction';
const Entity: React.FC<{
node?: Node | any;
setTargetNode: Function | any;
targetGraph: any;
}> = (props) => {
const { node, setTargetNode, targetGraph } = props;
const {
store: {
data: { title, name, item, attrs, select },
},
id,
} = node;
const database = useCurrentAppInfo();
const collectionData = useRef();
const categoryData = useContext(CollectionCategroriesContext);
collectionData.current = { ...item, title, inherits: item.inherits && new Proxy(item.inherits, {}) };
const { category } = item;
const compile = useCompile();
const loadCollections = async (field: any) => {
return targetGraph.collections?.map((collection: any) => ({
label: compile(collection.title),
value: collection.name,
}));
};
const loadCategories = async () => {
return categoryData?.data.map((item: any) => ({
label: compile(item.name),
value: item.id,
}));
};
const portsProps = {
targetGraph,
collectionData,
setTargetNode,
node,
loadCollections,
};
return (
<div
className={cx(entityContainer)}
style={{ boxShadow: attrs?.boxShadow, border: select ? '2px dashed #f5a20a' : 0 }}
>
{category.map((v, index) => {
return (
<Badge.Ribbon
color={v.color}
style={{ width: '103%', height: '3px', marginTop: index * 5 - 8, borderRadius: 0 }}
placement="start"
/>
);
})}
<div
className={headClass}
style={{ background: attrs?.hightLight ? '#1890ff' : null, paddingTop: category.length * 3 }}
>
<span className={tableNameClass}>{compile(title)}</span>
<div className={tableBtnClass}>
<SchemaComponentProvider>
<CollectionNodeProvder setTargetNode={setTargetNode} node={node}>
<CollectionProvider collection={collection}>
<SchemaComponent
scope={{
useUpdateCollectionActionAndRefreshCM,
useCancelAction,
loadCollections,
loadCategories,
useAsyncDataSource,
Action,
DeleteOutlined,
enableInherits: database?.dialect === 'postgres',
}}
components={{
Action,
EditOutlined,
FormItem,
CollectionField,
Input,
Form,
Select,
EditCollectionAction,
Checkbox,
}}
schema={{
type: 'object',
properties: {
update: {
type: 'void',
title: '{{ t("Edit") }}',
'x-component': 'EditCollectionAction',
'x-component-props': {
type: 'primary',
item: collectionData.current,
},
},
delete: {
type: 'void',
'x-action': 'destroy',
'x-component': 'Action',
'x-component-props': {
component: DeleteOutlined,
icon: 'DeleteOutlined',
className: css`
background-color: rgb(255 236 232);
border-color: transparent;
color: #e31c1c;
height: 20px;
padding: 5px;
&:hover {
background-color: rgb(253 205 197);
}
`,
confirm: {
title: "{{t('Delete record')}}",
getContainer: getPopupContainer,
collectionConten: "{{t('Are you sure you want to delete it?')}}",
},
useAction: () => useDestroyActionAndRefreshCM({ name, id }),
},
},
},
}}
/>
</CollectionProvider>
</CollectionNodeProvder>
</SchemaComponentProvider>
</div>
</div>
<PortsCom {...portsProps} />
</div>
);
};
const PortsCom = React.memo<any>(({ targetGraph, collectionData, setTargetNode, node, loadCollections }) => {
const {
store: {
data: { title, name, item, ports, data, sourcePort, associated, targetPort },
},
} = node;
const [collapse, setCollapse] = useState(false);
const { t } = useGCMTranslation();
const compile = useCompile();
const database = useCurrentAppInfo();
const portsData = lodash.groupBy(ports.items, (v) => {
if (
v.isForeignKey ||
v.primaryKey ||
['obo', 'oho', 'o2o', 'o2m', 'm2o', 'm2m', 'linkTo', 'id'].includes(v.interface)
) {
return 'initPorts';
} else {
return 'morePorts';
}
});
const useNewId = (prefix) => {
return `${prefix || ''}${uid()}`;
};
const CollectionConten = (data) => {
const { type, name, primaryKey, allowNull, autoIncrement } = data;
return (
<div className={cx(collectiionPopoverClass)}>
<div className="field-content">
<div>
<span>name</span>: <span className="field-type">{name}</span>
</div>
<div>
<span>type</span>: <span className="field-type">{type}</span>
</div>
</div>
<p>
{primaryKey && <Tag color="green">PRIMARY</Tag>}
{allowNull && <Tag color="geekblue">ALLOWNULL</Tag>}
{autoIncrement && <Tag color="purple">AUTOINCREMENT</Tag>}
</p>
</div>
);
};
const typeColor = (v) => {
if (v.isForeignKey || v.primaryKey || v.interface === 'id') {
return 'red';
} else if (['obo', 'oho', 'o2o', 'o2m', 'm2o', 'm2m', 'linkTo'].includes(v.interface)) {
return 'orange';
}
};
const OperationButton = ({ property }) => {
const isInheritField = !(property.collectionName !== name);
return (
<div className="field-operator">
<SchemaComponentProvider
components={{
FormItem,
CollectionField,
Input,
Form,
ResourceActionProvider,
Select: (props) => (
<Select dropdownMatchSelectWidth={false} {...props} getPopupContainer={getPopupContainer} />
),
Checkbox,
Radio,
InputNumber,
Grid,
FieldSummary,
Action,
EditOutlined,
DeleteOutlined,
AddFieldAction,
OverrideFieldAction,
ViewFieldAction,
Dropdown,
Formula,
}}
scope={{
useAsyncDataSource,
loadCollections,
useCancelAction,
useNewId,
useCurrentFields,
useValuesFromRecord,
useUpdateCollectionActionAndRefreshCM,
isInheritField,
}}
>
<CollectionNodeProvder
record={collectionData.current}
setTargetNode={setTargetNode}
node={node}
handelOpenPorts={() => handelOpenPorts(true)}
>
<SchemaComponent
scope={useCancelAction}
schema={{
type: 'object',
properties: {
create: {
type: 'void',
'x-action': 'create',
'x-component': 'AddFieldAction',
'x-visible': '{{isInheritField}}',
'x-component-props': {
item: {
...property,
title,
},
database,
},
},
update: {
type: 'void',
'x-action': 'update',
'x-component': EditFieldAction,
'x-visible': '{{isInheritField}}',
'x-component-props': {
item: {
...property,
title,
__parent: collectionData.current,
},
},
},
delete: {
type: 'void',
'x-action': 'destroy',
'x-component': 'Action',
'x-visible': '{{isInheritField}}',
'x-component-props': {
component: DeleteOutlined,
icon: 'DeleteOutlined',
className: css`
background-color: rgb(255 236 232);
border-color: transparent;
color: #e31c1c;
height: 20px;
width: 20px;
padding: 5px;
&:hover {
background-color: rgb(253 205 197);
}
`,
confirm: {
title: "{{t('Delete record')}}",
getContainer: getPopupContainer,
collectionConten: "{{t('Are you sure you want to delete it?')}}",
},
useAction: () =>
useDestroyFieldActionAndRefreshCM({
collectionName: property.collectionName,
name: property.name,
}),
},
},
override: {
type: 'void',
'x-action': 'create',
'x-visible': '{{!isInheritField}}',
'x-component': 'OverrideFieldAction',
'x-component-props': {
icon: 'ReconciliationOutlined',
item: {
...property,
title,
__parent: collectionData.current,
targetCollection: name,
},
},
},
view: {
type: 'void',
'x-action': 'view',
'x-visible': '{{!isInheritField}}',
'x-component': 'ViewFieldAction',
'x-component-props': {
icon: 'ReconciliationOutlined',
item: {
...property,
title,
__parent: collectionData.current,
},
},
},
},
}}
/>
</CollectionNodeProvder>
</SchemaComponentProvider>
</div>
);
};
const { getInterface } = useCollectionManager();
// 获取当前字段列表
const useCurrentFields = () => {
const record = useRecord();
const { getCollectionFields } = useCollectionManager();
const fields = getCollectionFields(record.collectionName || record.name) as any[];
return fields;
};
const handelOpenPorts = (isCollapse?) => {
targetGraph.getCellById(item.name)?.toFront();
setCollapse(isCollapse);
const collapseNodes = targetGraph.collapseNodes || [];
collapseNodes.push({
[item.name]: isCollapse,
});
targetGraph.collapseNodes = collapseNodes;
targetGraph.getCellById(item.name).setData({ collapse: true });
};
const isCollapse = collapse && data?.collapse;
return (
<div className="body">
{portsData['initPorts']?.map((property) => {
return (
property.uiSchema && (
<Popover
content={CollectionConten(property)}
getPopupContainer={getPopupContainer}
mouseLeaveDelay={0}
zIndex={100}
title={
<div>
{compile(property.uiSchema?.title)}
<span style={{ color: '#ffa940', float: 'right' }}>
{compile(getInterface(property.interface)?.title)}
</span>
</div>
}
key={property.id}
placement="right"
>
<div
className="body-item"
key={property.id}
id={property.id}
style={{
background:
targetPort || sourcePort === property.id || associated?.includes(property.name) ? '#e6f7ff' : null,
}}
>
<div className="name">
<Badge color={typeColor(property)} />
{compile(property.uiSchema?.title)}
</div>
<div className={`type field_type`}>{compile(getInterface(property.interface)?.title)}</div>
<OperationButton property={property} />
</div>
</Popover>
)
);
})}
<div className="morePorts">
{isCollapse &&
portsData['morePorts']?.map((property) => {
return (
property.uiSchema && (
<Popover
content={CollectionConten(property)}
getPopupContainer={getPopupContainer}
mouseLeaveDelay={0}
zIndex={100}
title={
<div>
{compile(property.uiSchema?.title)}
<span style={{ color: '#ffa940', float: 'right' }}>
{compile(getInterface(property.interface)?.title)}
</span>
</div>
}
key={property.id}
placement="right"
>
<div
className="body-item"
key={property.id}
id={property.id}
style={{
background:
targetPort || sourcePort === property.id || associated?.includes(property.name)
? '#e6f7ff'
: null,
}}
>
<div className="name">
<Badge color="green" />
{compile(property.uiSchema?.title)}
</div>
<div className={`type field_type`}>{compile(getInterface(property.interface)?.title)}</div>
<OperationButton property={property} />
</div>
</Popover>
)
);
})}
</div>
<a
className={css`
display: block;
color: #958f8f;
padding: 10px 5px;
&:hover {
color: rgb(99 90 88);
}
`}
onClick={() => handelOpenPorts(!isCollapse)}
>
{isCollapse
? [
<UpOutlined style={{ margin: '0px 8px 0px 5px' }} key="icon" />,
<span key="associate">{t('Association Fields')}</span>,
]
: [
<DownOutlined style={{ margin: '0px 8px 0px 5px' }} key="icon" />,
<span key="all">{t('All Fields')}</span>,
]}
</a>
</div>
);
});
export default Entity;