mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-02 11:12:20 +08:00
feat: data modeling
This commit is contained in:
parent
af37305134
commit
1573ef3b82
@ -1,106 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Form, GeneralField, isVoidField } from '@formily/core';
|
|
||||||
import { RenderPropsChildren, SchemaComponentsContext } from '@formily/react';
|
|
||||||
import { toJS } from '@formily/reactive';
|
|
||||||
import { observer } from '@formily/reactive-react';
|
|
||||||
import { FormPath, isFn } from '@formily/shared';
|
|
||||||
import React, { Fragment, useContext } from 'react';
|
|
||||||
interface IReactiveFieldProps {
|
|
||||||
field: GeneralField;
|
|
||||||
children?: RenderPropsChildren<GeneralField>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mergeChildren = (children: RenderPropsChildren<GeneralField>, content: React.ReactNode) => {
|
|
||||||
if (!children && !content) return;
|
|
||||||
if (isFn(children)) return;
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
{children}
|
|
||||||
{content}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isValidComponent = (target: any) => target && (typeof target === 'object' || typeof target === 'function');
|
|
||||||
|
|
||||||
const renderChildren = (children: RenderPropsChildren<GeneralField>, field?: GeneralField, form?: Form) =>
|
|
||||||
isFn(children) ? children(field, form) : children;
|
|
||||||
|
|
||||||
const ReactiveInternal: React.FC<IReactiveFieldProps> = (props) => {
|
|
||||||
const components = useContext(SchemaComponentsContext);
|
|
||||||
if (!props.field) {
|
|
||||||
return <Fragment>{renderChildren(props.children)}</Fragment>;
|
|
||||||
}
|
|
||||||
const field = props.field;
|
|
||||||
const content = mergeChildren(
|
|
||||||
renderChildren(props.children, field, field.form),
|
|
||||||
field.content ?? field.componentProps.children,
|
|
||||||
);
|
|
||||||
if (field.display !== 'visible') return null;
|
|
||||||
|
|
||||||
const getComponent = (target: any) => {
|
|
||||||
return isValidComponent(target) ? target : FormPath.getIn(components, target) ?? target;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderDecorator = (children: React.ReactNode) => {
|
|
||||||
if (!field.decoratorType) {
|
|
||||||
return <Fragment>{children}</Fragment>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return React.createElement(getComponent(field.decoratorType), toJS(field.decoratorProps), children);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderComponent = () => {
|
|
||||||
if (!field.componentType) return content;
|
|
||||||
const value = !isVoidField(field) ? field.value : undefined;
|
|
||||||
const onChange = !isVoidField(field)
|
|
||||||
? (...args: any[]) => {
|
|
||||||
field.onInput(...args);
|
|
||||||
field.componentProps?.onChange?.(...args);
|
|
||||||
}
|
|
||||||
: field.componentProps?.onChange;
|
|
||||||
const onFocus = !isVoidField(field)
|
|
||||||
? (...args: any[]) => {
|
|
||||||
field.onFocus(...args);
|
|
||||||
field.componentProps?.onFocus?.(...args);
|
|
||||||
}
|
|
||||||
: field.componentProps?.onFocus;
|
|
||||||
const onBlur = !isVoidField(field)
|
|
||||||
? (...args: any[]) => {
|
|
||||||
field.onBlur(...args);
|
|
||||||
field.componentProps?.onBlur?.(...args);
|
|
||||||
}
|
|
||||||
: field.componentProps?.onBlur;
|
|
||||||
const disabled = !isVoidField(field) ? field.pattern === 'disabled' || field.pattern === 'readPretty' : undefined;
|
|
||||||
const readOnly = !isVoidField(field) ? field.pattern === 'readOnly' : undefined;
|
|
||||||
return React.createElement(
|
|
||||||
getComponent(field.componentType),
|
|
||||||
{
|
|
||||||
disabled,
|
|
||||||
readOnly,
|
|
||||||
...toJS(field.componentProps),
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
onFocus,
|
|
||||||
onBlur,
|
|
||||||
},
|
|
||||||
content,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return renderDecorator(renderComponent());
|
|
||||||
};
|
|
||||||
|
|
||||||
ReactiveInternal.displayName = 'ReactiveField';
|
|
||||||
|
|
||||||
export const ReactiveField = observer(ReactiveInternal, {
|
|
||||||
forwardRef: true,
|
|
||||||
});
|
|
@ -1,8 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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.
|
|
||||||
*/
|
|
@ -27,6 +27,8 @@
|
|||||||
"echarts-for-react": "3.0.2",
|
"echarts-for-react": "3.0.2",
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
"rehype-sanitize": "^6.0.0",
|
"rehype-sanitize": "^6.0.0",
|
||||||
"remark-gfm": "^4.0.1"
|
"remark-gfm": "^4.0.1",
|
||||||
|
"@xyflow/react": "^12.7.0",
|
||||||
|
"elkjs": "^0.10.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,6 @@ export function replaceTagBlockByIndex(
|
|||||||
indexToReplace: number,
|
indexToReplace: number,
|
||||||
newInnerStr: string,
|
newInnerStr: string,
|
||||||
): string {
|
): string {
|
||||||
console.log(input, tagName, indexToReplace, newInnerStr);
|
|
||||||
const regex = new RegExp(`<${tagName}>([\\s\\S]*?)<\\/${tagName}>`, 'gi');
|
const regex = new RegExp(`<${tagName}>([\\s\\S]*?)<\\/${tagName}>`, 'gi');
|
||||||
let currentIndex = 0;
|
let currentIndex = 0;
|
||||||
return input.replace(regex, (match, inner) => {
|
return input.replace(regex, (match, inner) => {
|
||||||
@ -100,9 +99,7 @@ const EditModal: React.FC<{
|
|||||||
form.reset();
|
form.reset();
|
||||||
}}
|
}}
|
||||||
onOk={async () => {
|
onOk={async () => {
|
||||||
console.log(option);
|
|
||||||
const content = replaceTagBlockByIndex(message.content, 'echarts', index, JSON.stringify(option));
|
const content = replaceTagBlockByIndex(message.content, 'echarts', index, JSON.stringify(option));
|
||||||
console.log(content);
|
|
||||||
await updateMessage({
|
await updateMessage({
|
||||||
sessionId: currentConversation,
|
sessionId: currentConversation,
|
||||||
messageId: message.messageId,
|
messageId: message.messageId,
|
||||||
@ -128,7 +125,7 @@ const EditModal: React.FC<{
|
|||||||
marginTop: '16px',
|
marginTop: '16px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Col span={12}>
|
<Col span={8}>
|
||||||
<SchemaComponent
|
<SchemaComponent
|
||||||
schema={{
|
schema={{
|
||||||
type: 'void',
|
type: 'void',
|
||||||
@ -158,7 +155,7 @@ const EditModal: React.FC<{
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={16}>
|
||||||
{option ? (
|
{option ? (
|
||||||
<Card size="small">
|
<Card size="small">
|
||||||
<ReactECharts
|
<ReactECharts
|
||||||
|
@ -17,6 +17,7 @@ import { Message } from '../../types';
|
|||||||
import { Code } from './Code';
|
import { Code } from './Code';
|
||||||
import { Echarts } from './ECharts';
|
import { Echarts } from './ECharts';
|
||||||
import { Form } from './Form';
|
import { Form } from './Form';
|
||||||
|
import { Collections } from '../../data-modeling/Collections';
|
||||||
|
|
||||||
export const Markdown: React.FC<{
|
export const Markdown: React.FC<{
|
||||||
message: Message;
|
message: Message;
|
||||||
@ -43,6 +44,9 @@ export const Markdown: React.FC<{
|
|||||||
echarts: (props) => {
|
echarts: (props) => {
|
||||||
return <Echarts {...props} index={getIndex('echarts')} message={message} />;
|
return <Echarts {...props} index={getIndex('echarts')} message={message} />;
|
||||||
},
|
},
|
||||||
|
collections: (props) => {
|
||||||
|
return <Collections {...props} message={message} />;
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
rehypePlugins={[
|
rehypePlugins={[
|
||||||
rehypeRaw,
|
rehypeRaw,
|
||||||
@ -50,7 +54,7 @@ export const Markdown: React.FC<{
|
|||||||
rehypeSanitize,
|
rehypeSanitize,
|
||||||
{
|
{
|
||||||
...defaultSchema,
|
...defaultSchema,
|
||||||
tagNames: [...defaultSchema.tagNames, 'echarts', 'form'],
|
tagNames: [...defaultSchema.tagNames, 'echarts', 'form', 'collections'],
|
||||||
attributes: {
|
attributes: {
|
||||||
...defaultSchema.attributes,
|
...defaultSchema.attributes,
|
||||||
form: ['uid', 'datasource', 'collection'],
|
form: ['uid', 'datasource', 'collection'],
|
||||||
|
@ -0,0 +1,145 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Handle, Position } from 'reactflow';
|
||||||
|
import { List, Card, Flex } from 'antd';
|
||||||
|
import { useApp, useToken } from '@nocobase/client';
|
||||||
|
import { useT } from '../../locale';
|
||||||
|
import { Schema } from '@formily/react';
|
||||||
|
import { KeyOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
export const CollectionNode = ({ data }) => {
|
||||||
|
const { token } = useToken();
|
||||||
|
const app = useApp();
|
||||||
|
const t = useT();
|
||||||
|
const fim = app.dataSourceManager.collectionFieldInterfaceManager;
|
||||||
|
const handleStyle = {
|
||||||
|
top: '50%',
|
||||||
|
transform: 'translateY(-50%)',
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
opacity: 0,
|
||||||
|
pointerEvents: 'none' as const,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
size="small"
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
{data.title}{' '}
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: token.fontSizeSM,
|
||||||
|
color: token.colorTextDescription,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{data.name}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
styles={{
|
||||||
|
body: {
|
||||||
|
padding: 0,
|
||||||
|
minWidth: '200px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<List
|
||||||
|
dataSource={data.fields}
|
||||||
|
size="small"
|
||||||
|
renderItem={(item: any) => {
|
||||||
|
const fieldInterface = fim.getFieldInterface(item.interface);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List.Item
|
||||||
|
style={{
|
||||||
|
position: 'relative', // 为了定位 Handle
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
justify="space-between"
|
||||||
|
gap="large"
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
id={`${data.name}-${item.name}-source-right`}
|
||||||
|
style={handleStyle}
|
||||||
|
/>
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Left}
|
||||||
|
id={`${data.name}-${item.name}-source-left`}
|
||||||
|
style={handleStyle}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
position={Position.Left}
|
||||||
|
id={`${data.name}-${item.name}-target-left`}
|
||||||
|
style={handleStyle}
|
||||||
|
/>
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
position={Position.Right}
|
||||||
|
id={`${data.name}-${item.name}-target-right`}
|
||||||
|
style={handleStyle}
|
||||||
|
/>
|
||||||
|
<Flex vertical={true}>
|
||||||
|
<div>
|
||||||
|
{item.title}
|
||||||
|
{item.primaryKey ? (
|
||||||
|
<KeyOutlined
|
||||||
|
style={{
|
||||||
|
marginLeft: '4px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: token.fontSizeSM,
|
||||||
|
color: token.colorTextDescription,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
|
<Flex vertical={true} align="flex-end">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: token.fontSizeSM,
|
||||||
|
color: token.colorTextSecondary,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{fieldInterface?.title ? Schema.compile(fieldInterface.title, { t }) : ''}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: token.fontSizeSM,
|
||||||
|
color: token.colorTextDescription,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.type}
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</List.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,174 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import { Generating } from '../chatbox/markdown/Generating';
|
||||||
|
import { useChatMessages } from '../chatbox/ChatMessagesProvider';
|
||||||
|
import mermaid from 'mermaid';
|
||||||
|
import { Card, Modal, Tabs } from 'antd';
|
||||||
|
import { DatabaseOutlined, FileTextOutlined, NodeIndexOutlined } from '@ant-design/icons';
|
||||||
|
import { useT } from '../../locale';
|
||||||
|
import { CodeInternal } from '../chatbox/markdown/Code';
|
||||||
|
import { Diagram } from './Diagram';
|
||||||
|
import { Table } from './Table';
|
||||||
|
import { useAPIClient, useApp } from '@nocobase/client';
|
||||||
|
|
||||||
|
mermaid.initialize({
|
||||||
|
startOnLoad: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const Mermaid: React.FC<{ diagram: string }> = ({ diagram }) => {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (ref.current) {
|
||||||
|
try {
|
||||||
|
const id = 'mermaid-' + Math.random().toString(36).slice(2);
|
||||||
|
mermaid.render(id, diagram, (svgCode) => {
|
||||||
|
ref.current!.innerHTML = svgCode;
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
console.log(diagram);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [diagram]);
|
||||||
|
|
||||||
|
return <div ref={ref} className="mermaid" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TabPane: React.FC = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: '70vh',
|
||||||
|
overflowY: 'auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DataModelingModal: React.FC<{
|
||||||
|
open: boolean;
|
||||||
|
setOpen: (open: boolean) => void;
|
||||||
|
collections: any[];
|
||||||
|
}> = ({ open, setOpen, collections }) => {
|
||||||
|
const t = useT();
|
||||||
|
const api = useAPIClient();
|
||||||
|
const app = useApp();
|
||||||
|
const fim = app.dataSourceManager.collectionFieldInterfaceManager;
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
key: 'collections',
|
||||||
|
label: t('Collections'),
|
||||||
|
icon: <DatabaseOutlined />,
|
||||||
|
children: <Table collections={collections} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'graph',
|
||||||
|
icon: <NodeIndexOutlined />,
|
||||||
|
label: t('Diagram'),
|
||||||
|
children: (
|
||||||
|
<TabPane>
|
||||||
|
<Diagram collections={collections} />
|
||||||
|
</TabPane>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'definition',
|
||||||
|
icon: <FileTextOutlined />,
|
||||||
|
label: 'Definition',
|
||||||
|
children: (
|
||||||
|
<TabPane>
|
||||||
|
<CodeInternal language="json" value={JSON.stringify(collections, null, 2)} />
|
||||||
|
</TabPane>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
width="90%"
|
||||||
|
onCancel={() => {
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
okText={t('Finish review and apply')}
|
||||||
|
onOk={async () => {
|
||||||
|
const result = [];
|
||||||
|
for (const collection of collections) {
|
||||||
|
const fields = collection.fields.map((field: any) => {
|
||||||
|
const fieldInterface = fim.getFieldInterface(field.interface);
|
||||||
|
if (fieldInterface) {
|
||||||
|
field.type = fieldInterface.default?.type || field.type;
|
||||||
|
field.uiSchema = fieldInterface.default?.uiSchema || field.uiSchema;
|
||||||
|
}
|
||||||
|
field.uiSchema = {
|
||||||
|
...field.uiSchema,
|
||||||
|
title: field.title,
|
||||||
|
};
|
||||||
|
if (field.enum) {
|
||||||
|
field.uiSchema = {
|
||||||
|
...field.uiSchema,
|
||||||
|
enum: field.enum,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return field;
|
||||||
|
});
|
||||||
|
result.push({
|
||||||
|
...collection,
|
||||||
|
fields,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await api.resource('ai').defineCollections({
|
||||||
|
values: {
|
||||||
|
collections,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tabs defaultActiveKey="1" items={items} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Collections = (props: any) => {
|
||||||
|
const t = useT();
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
const { children, className, message, index, ...rest } = props;
|
||||||
|
const { responseLoading } = useChatMessages();
|
||||||
|
|
||||||
|
if (responseLoading && !message.messageId) {
|
||||||
|
return <Generating />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectionsStr = String(children).replace(/\n$/, '');
|
||||||
|
const collections = JSON.parse(collectionsStr);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Card
|
||||||
|
style={{
|
||||||
|
marginBottom: '16px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
>
|
||||||
|
<Card.Meta
|
||||||
|
avatar={<DatabaseOutlined />}
|
||||||
|
title={t('Data modeling')}
|
||||||
|
description={t('Please review and finish the process')}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
<DataModelingModal open={open} setOpen={setOpen} collections={collections} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,263 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import ReactFlow, {
|
||||||
|
Background,
|
||||||
|
Controls,
|
||||||
|
MiniMap,
|
||||||
|
useNodesState,
|
||||||
|
useEdgesState,
|
||||||
|
Handle,
|
||||||
|
Position,
|
||||||
|
Edge,
|
||||||
|
Node,
|
||||||
|
} from 'reactflow';
|
||||||
|
import ELK from 'elkjs/lib/elk.bundled.js';
|
||||||
|
import '@xyflow/react/dist/style.css';
|
||||||
|
import { CollectionNode } from './CollectionNode';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { useGlobalTheme } from '@nocobase/client';
|
||||||
|
|
||||||
|
const elk = new ELK();
|
||||||
|
|
||||||
|
const nodeTypes = {
|
||||||
|
collection: CollectionNode,
|
||||||
|
};
|
||||||
|
|
||||||
|
const layoutElements = async (nodes: Node[], edges: Edge[]) => {
|
||||||
|
const isHorizontal = !edges.length;
|
||||||
|
const graph = {
|
||||||
|
id: 'root',
|
||||||
|
layoutOptions: {
|
||||||
|
'elk.algorithm': edges.length ? 'org.eclipse.elk.layered' : 'org.eclipse.elk.box',
|
||||||
|
// 'elk.spacing.nodeNode': '100',
|
||||||
|
// 'elk.direction': isHorizontal ? 'RIGHT' : 'DOWN',
|
||||||
|
// 'elk.expandNodes': true,
|
||||||
|
},
|
||||||
|
children: nodes.map((node) => ({
|
||||||
|
...node,
|
||||||
|
// Adjust the target and source handle positions based on the layout
|
||||||
|
// direction.
|
||||||
|
targetPosition: isHorizontal ? 'left' : 'top',
|
||||||
|
sourcePosition: isHorizontal ? 'right' : 'bottom',
|
||||||
|
|
||||||
|
// Hardcode a width and height for elk to use when layouting.
|
||||||
|
width: 320,
|
||||||
|
height: 50 * (node.data.fields.length + 1),
|
||||||
|
})),
|
||||||
|
edges: edges,
|
||||||
|
};
|
||||||
|
|
||||||
|
return elk
|
||||||
|
.layout(graph)
|
||||||
|
.then((layoutedGraph) => ({
|
||||||
|
nodes: layoutedGraph.children.map((node) => ({
|
||||||
|
...node,
|
||||||
|
// React Flow expects a position property on the node instead of `x`
|
||||||
|
// and `y` fields.
|
||||||
|
position: { x: node.x, y: node.y },
|
||||||
|
})),
|
||||||
|
|
||||||
|
edges: layoutedGraph.edges,
|
||||||
|
}))
|
||||||
|
.catch(console.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDiagramData = (collections: any[]) => {
|
||||||
|
const collectionMap = new Map(collections.map((col) => [col.name, col]));
|
||||||
|
|
||||||
|
for (const collection of collections) {
|
||||||
|
if (collection.autoGenId) {
|
||||||
|
collection.fields.unshift({
|
||||||
|
name: 'id',
|
||||||
|
interface: 'id',
|
||||||
|
type: 'bigInt',
|
||||||
|
title: 'ID',
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodes = collections.map((collection) => ({
|
||||||
|
id: collection.name,
|
||||||
|
type: 'collection',
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
data: {
|
||||||
|
title: collection.title,
|
||||||
|
name: collection.name,
|
||||||
|
fields: [...collection.fields],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const edges: any[] = [];
|
||||||
|
|
||||||
|
// for (const collection of collections) {
|
||||||
|
// for (const field of collection.fields) {
|
||||||
|
// const { type, target, name: fieldName, foreignKey, targetKey, through } = field;
|
||||||
|
//
|
||||||
|
// if (!target || !collectionMap.has(target)) continue;
|
||||||
|
// const targetCollection = collectionMap.get(target);
|
||||||
|
// const targetFields = targetCollection.fields || [];
|
||||||
|
// const targetFieldExists = (key: string) => targetFields.some((f: any) => f.name === key);
|
||||||
|
//
|
||||||
|
// const tk = targetKey || 'id';
|
||||||
|
// const fk = foreignKey || 'id';
|
||||||
|
//
|
||||||
|
// if (type === 'belongsTo' && target === collection.name) {
|
||||||
|
// if (!targetFieldExists(tk)) continue;
|
||||||
|
//
|
||||||
|
// edges.push({
|
||||||
|
// id: `${collection.name}-${fieldName}-to-self`,
|
||||||
|
// source: collection.name,
|
||||||
|
// sourceHandle: `${collection.name}-${fieldName}-source-right`,
|
||||||
|
// target: collection.name,
|
||||||
|
// targetHandle: `${collection.name}-${tk}-target-right`,
|
||||||
|
// type: 'smoothstep',
|
||||||
|
// animated: true,
|
||||||
|
// });
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (type === 'belongsTo') {
|
||||||
|
// if (!targetFieldExists(tk)) continue;
|
||||||
|
//
|
||||||
|
// edges.push({
|
||||||
|
// id: `${collection.name}-${fieldName}-to-${tk}`,
|
||||||
|
// source: collection.name,
|
||||||
|
// sourceHandle: `${collection.name}-${fieldName}-source-right`,
|
||||||
|
// target,
|
||||||
|
// targetHandle: `${target}-${tk}-target-left`,
|
||||||
|
// // type: 'smoothstep',
|
||||||
|
// animated: true,
|
||||||
|
// });
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (type === 'hasMany' && target === collection.name) {
|
||||||
|
// if (!targetFieldExists(tk)) continue;
|
||||||
|
//
|
||||||
|
// edges.push({
|
||||||
|
// id: `${collection.name}-${tk}-to-${fieldName}`,
|
||||||
|
// source: target,
|
||||||
|
// sourceHandle: `${collection.name}-${tk}-source-left`,
|
||||||
|
// target: collection.name,
|
||||||
|
// targetHandle: `${collection.name}-${fieldName}-target-left`,
|
||||||
|
// type: 'smoothstep',
|
||||||
|
// animated: true,
|
||||||
|
// });
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (type === 'hasMany') {
|
||||||
|
// if (!targetFieldExists(tk)) continue;
|
||||||
|
//
|
||||||
|
// edges.push({
|
||||||
|
// id: `${collection.name}-${tk}-to-${fieldName}`,
|
||||||
|
// source: target,
|
||||||
|
// sourceHandle: `${target}-${tk}-source-right`,
|
||||||
|
// target: collection.name,
|
||||||
|
// targetHandle: `${collection.name}-${fieldName}-target-left`,
|
||||||
|
// // type: 'smoothstep',
|
||||||
|
// animated: true,
|
||||||
|
// });
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (type === 'hasOne') {
|
||||||
|
// if (!targetFieldExists(fk)) continue;
|
||||||
|
//
|
||||||
|
// edges.push({
|
||||||
|
// id: `${collection.name}-${fieldName}-to-${fk}`,
|
||||||
|
// source: collection.name,
|
||||||
|
// sourceHandle: `${collection.name}-${fieldName}-source-right`,
|
||||||
|
// target,
|
||||||
|
// targetHandle: `${target}-${fk}-target-left`,
|
||||||
|
// // type: 'smoothstep',
|
||||||
|
// animated: true,
|
||||||
|
// });
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // if (type === 'belongsToMany') {
|
||||||
|
// // let label = '';
|
||||||
|
// // if (through && collectionMap.has(through)) {
|
||||||
|
// // const throughCollection = collectionMap.get(through);
|
||||||
|
// // const throughFields = throughCollection.fields || [];
|
||||||
|
// // const hasCurrent = throughFields.some((f: any) => f.target === collection.name);
|
||||||
|
// // const hasTarget = throughFields.some((f: any) => f.target === target);
|
||||||
|
// //
|
||||||
|
// // if (!hasCurrent || !hasTarget) continue;
|
||||||
|
// // label = `via ${through}`;
|
||||||
|
// // } else {
|
||||||
|
// // label = through ? `via ${through}` : 'via ?';
|
||||||
|
// // }
|
||||||
|
// //
|
||||||
|
// // edges.push({
|
||||||
|
// // id: `${collection.name}-${fieldName}-to-${target}`,
|
||||||
|
// // source: collection.name,
|
||||||
|
// // sourceHandle: `${collection.name}-${fieldName}-source`,
|
||||||
|
// // target,
|
||||||
|
// // targetHandle: `${target}-${fieldName}-target`,
|
||||||
|
// // type: 'smoothstep',
|
||||||
|
// // animated: true,
|
||||||
|
// // label,
|
||||||
|
// // });
|
||||||
|
// // }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
return layoutElements(nodes, edges);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Diagram: React.FC<{
|
||||||
|
collections: any[];
|
||||||
|
}> = ({ collections }) => {
|
||||||
|
const { isDarkTheme } = useGlobalTheme();
|
||||||
|
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getDiagramData(collections)
|
||||||
|
.then(({ nodes, edges }) => {
|
||||||
|
setNodes(nodes);
|
||||||
|
setEdges(edges);
|
||||||
|
})
|
||||||
|
.catch((error) => console.error('Error laying out elements:', error));
|
||||||
|
}, [collections]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={css`
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
svg:not(:root) {
|
||||||
|
overflow: unset;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
fitView
|
||||||
|
panOnDrag={true}
|
||||||
|
zoomOnScroll={true}
|
||||||
|
proOptions={{ hideAttribution: true }}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
// @ts-ignore
|
||||||
|
colorMode={isDarkTheme ? 'dark' : 'light'}
|
||||||
|
>
|
||||||
|
<Background />
|
||||||
|
<Controls position="top-right" />
|
||||||
|
</ReactFlow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,161 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Table as AntdTable, Checkbox, Tag } from 'antd';
|
||||||
|
import type { TableColumnsType } from 'antd';
|
||||||
|
import { useT } from '../../locale';
|
||||||
|
import { useApp } from '@nocobase/client';
|
||||||
|
import { Schema } from '@formily/react';
|
||||||
|
import lodash from 'lodash';
|
||||||
|
|
||||||
|
const useColumns = () => {
|
||||||
|
const t = useT();
|
||||||
|
const columns: TableColumnsType = [
|
||||||
|
{ title: t('Collection display name'), dataIndex: 'title', key: 'title', width: 200 },
|
||||||
|
{ title: t('Collection name'), dataIndex: 'name', key: 'name', width: 150 },
|
||||||
|
{
|
||||||
|
title: t('Collection template'),
|
||||||
|
dataIndex: 'template',
|
||||||
|
key: 'template',
|
||||||
|
width: 150,
|
||||||
|
render: (value) => {
|
||||||
|
const template = value.charAt(0).toUpperCase() + value.slice(1);
|
||||||
|
return <Tag>{t(`${template} collection`)}</Tag>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('Description'),
|
||||||
|
dataIndex: 'description',
|
||||||
|
key: 'description',
|
||||||
|
width: 350,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('Preset fields'),
|
||||||
|
key: 'preset',
|
||||||
|
width: 300,
|
||||||
|
render: (_, record) => {
|
||||||
|
const value = [];
|
||||||
|
if (record.autoGenId !== false) {
|
||||||
|
value.push('id');
|
||||||
|
}
|
||||||
|
if (record.createdAt !== false) {
|
||||||
|
value.push('createdAt');
|
||||||
|
}
|
||||||
|
if (record.updatedAt !== false) {
|
||||||
|
value.push('updatedAt');
|
||||||
|
}
|
||||||
|
if (record.createdBy) {
|
||||||
|
value.push('createdBy');
|
||||||
|
}
|
||||||
|
if (record.updatedBy) {
|
||||||
|
value.push('updatedBy');
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Checkbox.Group
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: 'ID',
|
||||||
|
value: 'id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('Created at'),
|
||||||
|
value: 'createdAt',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('Last Updated at'),
|
||||||
|
value: 'updatedAt',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('Created by'),
|
||||||
|
value: 'createdBy',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('Last updated by'),
|
||||||
|
value: 'updatedBy',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
defaultValue={value}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return columns;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useExpandColumns = () => {
|
||||||
|
const t = useT();
|
||||||
|
const app = useApp();
|
||||||
|
const fim = app.dataSourceManager.collectionFieldInterfaceManager;
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t('Field display name'),
|
||||||
|
dataIndex: 'title',
|
||||||
|
key: 'title',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('Field name'),
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('Field interface'),
|
||||||
|
dataIndex: 'interface',
|
||||||
|
key: 'interface',
|
||||||
|
render: (value) => {
|
||||||
|
const fieldInterface = fim.getFieldInterface(value);
|
||||||
|
return <Tag>{fieldInterface ? Schema.compile(fieldInterface.title, { t }) : value}</Tag>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('Description'),
|
||||||
|
dataIndex: 'description',
|
||||||
|
key: 'description',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('Options'),
|
||||||
|
key: 'options',
|
||||||
|
render: (_, record) => {
|
||||||
|
return JSON.stringify(lodash.omit(record, ['title', 'name', 'interface', 'description']), null, 2);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return columns;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ExpandedRowRender = (record) => {
|
||||||
|
const expandColumns = useExpandColumns();
|
||||||
|
return <AntdTable rowKey="name" columns={expandColumns} dataSource={record.fields} pagination={false} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Table: React.FC<{
|
||||||
|
collections: any[];
|
||||||
|
}> = ({ collections }) => {
|
||||||
|
const columns = useColumns();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AntdTable
|
||||||
|
scroll={{
|
||||||
|
y: '60vh',
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
height: '70vh',
|
||||||
|
}}
|
||||||
|
rowKey="name"
|
||||||
|
columns={columns}
|
||||||
|
dataSource={collections}
|
||||||
|
expandable={{
|
||||||
|
expandedRowRender: ExpandedRowRender,
|
||||||
|
rowExpandable: (record) => record.fields && record.fields.length > 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,251 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface CollectionOptions extends Omit<SequelizeModelOptions, 'name' | 'hooks'> {
|
||||||
|
/** The unique identifier of the collection, must be unique across the database */
|
||||||
|
name: string;
|
||||||
|
/** The display title of the collection, used for UI presentation */
|
||||||
|
title?: string;
|
||||||
|
/** The description of the collection */
|
||||||
|
description?: string;
|
||||||
|
/** Whether this collection is a through table for many-to-many relationships */
|
||||||
|
isThrough?: boolean;
|
||||||
|
/** The target key(s) used for filtering operations, can be a single key or array of keys */
|
||||||
|
filterTargetKey?: string | string[];
|
||||||
|
/** Array of field definitions for the collection */
|
||||||
|
fields?: FieldOptions[];
|
||||||
|
/**
|
||||||
|
* Whether to automatically generate an 'id' field
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
autoGenId?: boolean;
|
||||||
|
/**
|
||||||
|
* Whether to automatically generate a 'createdAt' timestamp field
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
createdAt?: boolean;
|
||||||
|
/** Whether to automatically generate an 'updatedAt' timestamp field
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
updatedAt?: boolean;
|
||||||
|
/**
|
||||||
|
* Whether to automatically generate a 'createdById' field for record ownership
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
createdBy?: boolean;
|
||||||
|
/**
|
||||||
|
* Whether to automatically generate an 'updatedById' field for tracking updates
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
updatedBy?: boolean;
|
||||||
|
/** The template identifier used to create this collection */
|
||||||
|
template: 'general' | 'tree' | 'file' | 'calendar' | 'expression';
|
||||||
|
/** The field name used for tree structure functionality */
|
||||||
|
tree?: 'adjacencyList';
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FieldOptions =
|
||||||
|
| BaseFieldOptions
|
||||||
|
| StringFieldOptions
|
||||||
|
| IntegerFieldOptions
|
||||||
|
| FloatFieldOptions
|
||||||
|
| DecimalFieldOptions
|
||||||
|
| DoubleFieldOptions
|
||||||
|
| JsonFieldOptions
|
||||||
|
| JsonbFieldOptions
|
||||||
|
| BooleanFieldOptions
|
||||||
|
| RadioFieldOptions
|
||||||
|
| TextFieldOptions
|
||||||
|
| TimeFieldOptions
|
||||||
|
| DateFieldOptions
|
||||||
|
| DatetimeTzFieldOptions
|
||||||
|
| DatetimeNoTzFieldOptions
|
||||||
|
| DateOnlyFieldOptions
|
||||||
|
| UnixTimestampFieldOptions
|
||||||
|
| UidFieldOptions
|
||||||
|
| UUIDFieldOptions
|
||||||
|
| NanoidFieldOptions
|
||||||
|
| PasswordFieldOptions
|
||||||
|
| BelongsToFieldOptions
|
||||||
|
| HasOneFieldOptions
|
||||||
|
| HasManyFieldOptions
|
||||||
|
| BelongsToManyFieldOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base options for all field types
|
||||||
|
* Provides common properties that are available to all field configurations
|
||||||
|
*/
|
||||||
|
export interface BaseFieldOptions {
|
||||||
|
/** The name of the field, used as the column name in the database */
|
||||||
|
name: string;
|
||||||
|
/** The title of the field, used for display in the UI */
|
||||||
|
title: string;
|
||||||
|
/** The description of the field */
|
||||||
|
description?: string;
|
||||||
|
/** Whether the field should be hidden from API responses and UI */
|
||||||
|
hidden?: boolean;
|
||||||
|
/** Required. The user interface component type for this field */
|
||||||
|
interface:
|
||||||
|
| 'id'
|
||||||
|
| 'input'
|
||||||
|
| 'integer'
|
||||||
|
| 'checkbox'
|
||||||
|
| 'checkboxGroup'
|
||||||
|
| 'color'
|
||||||
|
| 'createdAt'
|
||||||
|
| 'updatedAt'
|
||||||
|
| 'createdBy'
|
||||||
|
| 'updatedBy'
|
||||||
|
| 'date'
|
||||||
|
| 'datetime'
|
||||||
|
| 'datetimeNoTz'
|
||||||
|
| 'email'
|
||||||
|
| 'icon'
|
||||||
|
| 'json'
|
||||||
|
| 'markdown'
|
||||||
|
| 'multipleSelect'
|
||||||
|
| 'nanoid'
|
||||||
|
| 'number'
|
||||||
|
| 'password'
|
||||||
|
| 'percent'
|
||||||
|
| 'phone'
|
||||||
|
| 'radioGroup'
|
||||||
|
| 'richText'
|
||||||
|
| 'select'
|
||||||
|
| 'textarea'
|
||||||
|
| 'time'
|
||||||
|
| 'unixTimestamp'
|
||||||
|
| 'url'
|
||||||
|
| 'uuid'
|
||||||
|
| 'm2m'
|
||||||
|
| 'm2o'
|
||||||
|
| 'o2m'
|
||||||
|
| 'o2o';
|
||||||
|
/** enumeration options for the field, used for select/radio/checkbox interfaces */
|
||||||
|
enum?: {
|
||||||
|
label: string;
|
||||||
|
value: string | number | boolean;
|
||||||
|
}[];
|
||||||
|
/** Additional properties for extensibility */
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Base options for column-based field types
|
||||||
|
* Extends BaseFieldOptions and includes Sequelize column-specific options
|
||||||
|
* Excludes the 'type' property as it's handled by the specific field implementations
|
||||||
|
*/
|
||||||
|
export interface BaseColumnFieldOptions extends BaseFieldOptions, Omit<ModelAttributeColumnOptions, 'type'> {
|
||||||
|
/** The Sequelize data type for the column */
|
||||||
|
dataType?: DataType;
|
||||||
|
|
||||||
|
/** Index configuration for the column, can be boolean or detailed index options */
|
||||||
|
index?: boolean | ModelIndexesOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StringFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'string';
|
||||||
|
length?: number;
|
||||||
|
trim?: boolean;
|
||||||
|
}
|
||||||
|
export interface IntegerFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'integer';
|
||||||
|
}
|
||||||
|
export interface FloatFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'float';
|
||||||
|
}
|
||||||
|
export interface DecimalFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'decimal';
|
||||||
|
precision: number;
|
||||||
|
scale: number;
|
||||||
|
}
|
||||||
|
export interface DoubleFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'double';
|
||||||
|
}
|
||||||
|
export interface JsonFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'json';
|
||||||
|
}
|
||||||
|
export interface JsonbFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'jsonb';
|
||||||
|
}
|
||||||
|
export interface BooleanFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'boolean';
|
||||||
|
}
|
||||||
|
export interface RadioFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'radio';
|
||||||
|
}
|
||||||
|
export interface TextFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'text';
|
||||||
|
length?: 'tiny' | 'medium' | 'long';
|
||||||
|
trim?: boolean;
|
||||||
|
}
|
||||||
|
export interface TimeFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'time';
|
||||||
|
}
|
||||||
|
export interface DateFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'date';
|
||||||
|
}
|
||||||
|
export interface DatetimeTzFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'datetimeTz';
|
||||||
|
}
|
||||||
|
export interface DatetimeNoTzFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'datetimeNoTz';
|
||||||
|
}
|
||||||
|
export interface DateOnlyFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'dateOnly';
|
||||||
|
}
|
||||||
|
export interface UnixTimestampFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'unixTimestamp';
|
||||||
|
}
|
||||||
|
export interface UidFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'uid';
|
||||||
|
prefix?: string;
|
||||||
|
pattern?: string;
|
||||||
|
}
|
||||||
|
export interface UUIDFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'uuid';
|
||||||
|
autoFill?: boolean;
|
||||||
|
}
|
||||||
|
export interface NanoidFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'nanoid';
|
||||||
|
size?: number;
|
||||||
|
customAlphabet?: string;
|
||||||
|
autoFill?: boolean;
|
||||||
|
}
|
||||||
|
export interface PasswordFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'password';
|
||||||
|
/**
|
||||||
|
* @default 64
|
||||||
|
*/
|
||||||
|
length?: number;
|
||||||
|
/**
|
||||||
|
* @default 8
|
||||||
|
*/
|
||||||
|
randomBytesSize?: number;
|
||||||
|
}
|
||||||
|
export interface BelongsToFieldOptions extends BaseRelationFieldOptions, SequelizeBelongsToOptions {
|
||||||
|
type: 'belongsTo';
|
||||||
|
target?: string;
|
||||||
|
}
|
||||||
|
export interface HasOneFieldOptions extends BaseRelationFieldOptions, SequelizeHasOneOptions {
|
||||||
|
type: 'hasOne';
|
||||||
|
}
|
||||||
|
export interface HasManyFieldOptions extends MultipleRelationFieldOptions, SequelizeHasManyOptions {
|
||||||
|
type: 'hasMany';
|
||||||
|
target?: string;
|
||||||
|
}
|
||||||
|
export interface BelongsToManyFieldOptions
|
||||||
|
extends MultipleRelationFieldOptions,
|
||||||
|
Omit<SequelizeBelongsToManyOptions, 'through'> {
|
||||||
|
type: 'belongsToMany';
|
||||||
|
target?: string;
|
||||||
|
through?: string;
|
||||||
|
throughScope?: AssociationScope;
|
||||||
|
throughUnique?: boolean;
|
||||||
|
throughParanoid?: boolean;
|
||||||
|
}
|
@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
import { ResourceOptions } from '@nocobase/resourcer';
|
import { ResourceOptions } from '@nocobase/resourcer';
|
||||||
import { PluginAIServer } from '../plugin';
|
import { PluginAIServer } from '../plugin';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
const aiResource: ResourceOptions = {
|
const aiResource: ResourceOptions = {
|
||||||
name: 'ai',
|
name: 'ai',
|
||||||
@ -47,6 +48,84 @@ const aiResource: ResourceOptions = {
|
|||||||
ctx.body = res.models || [];
|
ctx.body = res.models || [];
|
||||||
return next();
|
return next();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
defineCollections: async (ctx, next) => {
|
||||||
|
const { collections } = ctx.action.params.values || {};
|
||||||
|
const id = {
|
||||||
|
name: 'id',
|
||||||
|
type: 'bigInt',
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false,
|
||||||
|
uiSchema: {
|
||||||
|
type: 'number',
|
||||||
|
title: '{{t("ID")}}',
|
||||||
|
'x-component': 'InputNumber',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
interface: 'integer',
|
||||||
|
};
|
||||||
|
const createdAt = {
|
||||||
|
name: 'createdAt',
|
||||||
|
interface: 'createdAt',
|
||||||
|
type: 'date',
|
||||||
|
field: 'createdAt',
|
||||||
|
uiSchema: {
|
||||||
|
type: 'datetime',
|
||||||
|
title: '{{t("Created at")}}',
|
||||||
|
'x-component': 'DatePicker',
|
||||||
|
'x-component-props': {},
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const updatedAt = {
|
||||||
|
type: 'date',
|
||||||
|
field: 'updatedAt',
|
||||||
|
name: 'updatedAt',
|
||||||
|
interface: 'updatedAt',
|
||||||
|
uiSchema: {
|
||||||
|
type: 'datetime',
|
||||||
|
title: '{{t("Last updated at")}}',
|
||||||
|
'x-component': 'DatePicker',
|
||||||
|
'x-component-props': {},
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
for (const options of collections) {
|
||||||
|
if (options.name === 'users') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (options.autoGenId !== false) {
|
||||||
|
options.autoGenId = false;
|
||||||
|
options.fields.unshift(id);
|
||||||
|
}
|
||||||
|
if (options.createdAt !== false) {
|
||||||
|
options.fields.push(createdAt);
|
||||||
|
}
|
||||||
|
if (options.updatedAt !== false) {
|
||||||
|
options.fields.push(updatedAt);
|
||||||
|
}
|
||||||
|
const primaryKey = options.fields.find((field: any) => field.primaryKey);
|
||||||
|
if (!options.filterTargetKey) {
|
||||||
|
options.filterTargetKey = primaryKey?.name || 'id';
|
||||||
|
}
|
||||||
|
// omit defaultValue
|
||||||
|
options.fields = options.fields.map((field: any) => {
|
||||||
|
return _.omit(field, ['defaultValue']);
|
||||||
|
});
|
||||||
|
ctx.db.collection(options);
|
||||||
|
}
|
||||||
|
for (const options of collections) {
|
||||||
|
const collection = ctx.db.getCollection(options.name);
|
||||||
|
await collection.sync();
|
||||||
|
}
|
||||||
|
const repo = ctx.db.getRepository('collections');
|
||||||
|
for (const options of collections) {
|
||||||
|
await repo.db2cm(options.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -152,6 +152,36 @@ export const collectionTableSchema: ISchema = {
|
|||||||
},
|
},
|
||||||
'x-align': 'left',
|
'x-align': 'left',
|
||||||
},
|
},
|
||||||
|
aiEmployee: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'AIEmployeeButton',
|
||||||
|
'x-toolbar': 'ActionSchemaToolbar',
|
||||||
|
'x-settings': 'aiEmployees:button',
|
||||||
|
'x-component-props': {
|
||||||
|
username: 'orin',
|
||||||
|
tasks: [
|
||||||
|
{
|
||||||
|
message: {
|
||||||
|
workContext: [],
|
||||||
|
attachments: [],
|
||||||
|
user: 'Create a todo list application with support for multiple lists, due dates, priorities, and task categories. Include tables for users, lists, tasks, and categories.',
|
||||||
|
},
|
||||||
|
title: 'Create a todo list application',
|
||||||
|
autoSend: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: {
|
||||||
|
workContext: [],
|
||||||
|
attachments: [],
|
||||||
|
user: 'Create a CRM application that includes the following modules: Leads, Customers, Orders, Products, Tickets',
|
||||||
|
},
|
||||||
|
title: 'Create a CRM application',
|
||||||
|
autoSend: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
delete: {
|
delete: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
title: '{{ t("Delete") }}',
|
title: '{{ t("Delete") }}',
|
||||||
|
59
yarn.lock
59
yarn.lock
@ -7939,7 +7939,7 @@
|
|||||||
resolved "https://registry.npmmirror.com/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz#096efdf55eb97480e3f5621ff9a8da552f0961e7"
|
resolved "https://registry.npmmirror.com/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz#096efdf55eb97480e3f5621ff9a8da552f0961e7"
|
||||||
integrity sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==
|
integrity sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==
|
||||||
|
|
||||||
"@types/d3-drag@*", "@types/d3-drag@^3.0.1":
|
"@types/d3-drag@*", "@types/d3-drag@^3.0.1", "@types/d3-drag@^3.0.7":
|
||||||
version "3.0.7"
|
version "3.0.7"
|
||||||
resolved "https://registry.npmmirror.com/@types/d3-drag/-/d3-drag-3.0.7.tgz#b13aba8b2442b4068c9a9e6d1d82f8bcea77fc02"
|
resolved "https://registry.npmmirror.com/@types/d3-drag/-/d3-drag-3.0.7.tgz#b13aba8b2442b4068c9a9e6d1d82f8bcea77fc02"
|
||||||
integrity sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==
|
integrity sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==
|
||||||
@ -7985,7 +7985,7 @@
|
|||||||
resolved "https://registry.npmmirror.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.6.tgz#8d3638df273ec90da34b3ac89d8784c59708cb0d"
|
resolved "https://registry.npmmirror.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.6.tgz#8d3638df273ec90da34b3ac89d8784c59708cb0d"
|
||||||
integrity sha512-qlmD/8aMk5xGorUvTUWHCiumvgaUXYldYjNVOWtYoTYY/L+WwIEAmJxUmTgr9LoGNG0PPAOmqMDJVDPc7DOpPw==
|
integrity sha512-qlmD/8aMk5xGorUvTUWHCiumvgaUXYldYjNVOWtYoTYY/L+WwIEAmJxUmTgr9LoGNG0PPAOmqMDJVDPc7DOpPw==
|
||||||
|
|
||||||
"@types/d3-interpolate@*":
|
"@types/d3-interpolate@*", "@types/d3-interpolate@^3.0.4":
|
||||||
version "3.0.4"
|
version "3.0.4"
|
||||||
resolved "https://registry.npmmirror.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c"
|
resolved "https://registry.npmmirror.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c"
|
||||||
integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==
|
integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==
|
||||||
@ -8029,6 +8029,11 @@
|
|||||||
resolved "https://registry.npmmirror.com/@types/d3-selection/-/d3-selection-3.0.10.tgz#98cdcf986d0986de6912b5892e7c015a95ca27fe"
|
resolved "https://registry.npmmirror.com/@types/d3-selection/-/d3-selection-3.0.10.tgz#98cdcf986d0986de6912b5892e7c015a95ca27fe"
|
||||||
integrity sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==
|
integrity sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==
|
||||||
|
|
||||||
|
"@types/d3-selection@^3.0.10":
|
||||||
|
version "3.0.11"
|
||||||
|
resolved "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz#bd7a45fc0a8c3167a631675e61bc2ca2b058d4a3"
|
||||||
|
integrity sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==
|
||||||
|
|
||||||
"@types/d3-shape@*":
|
"@types/d3-shape@*":
|
||||||
version "3.1.6"
|
version "3.1.6"
|
||||||
resolved "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.6.tgz#65d40d5a548f0a023821773e39012805e6e31a72"
|
resolved "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.6.tgz#65d40d5a548f0a023821773e39012805e6e31a72"
|
||||||
@ -8063,7 +8068,14 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/d3-selection" "*"
|
"@types/d3-selection" "*"
|
||||||
|
|
||||||
"@types/d3-zoom@*", "@types/d3-zoom@^3.0.1":
|
"@types/d3-transition@^3.0.8":
|
||||||
|
version "3.0.9"
|
||||||
|
resolved "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz#1136bc57e9ddb3c390dccc9b5ff3b7d2b8d94706"
|
||||||
|
integrity sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==
|
||||||
|
dependencies:
|
||||||
|
"@types/d3-selection" "*"
|
||||||
|
|
||||||
|
"@types/d3-zoom@*", "@types/d3-zoom@^3.0.1", "@types/d3-zoom@^3.0.8":
|
||||||
version "3.0.8"
|
version "3.0.8"
|
||||||
resolved "https://registry.npmmirror.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz#dccb32d1c56b1e1c6e0f1180d994896f038bc40b"
|
resolved "https://registry.npmmirror.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz#dccb32d1c56b1e1c6e0f1180d994896f038bc40b"
|
||||||
integrity sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==
|
integrity sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==
|
||||||
@ -9548,6 +9560,30 @@
|
|||||||
loupe "^2.3.7"
|
loupe "^2.3.7"
|
||||||
pretty-format "^29.7.0"
|
pretty-format "^29.7.0"
|
||||||
|
|
||||||
|
"@xyflow/react@^12.7.0":
|
||||||
|
version "12.7.0"
|
||||||
|
resolved "https://registry.npmjs.org/@xyflow/react/-/react-12.7.0.tgz#a963692cf38466ba88d54b8ad9099d5e58034460"
|
||||||
|
integrity sha512-U6VMEbYjiCg1byHrR7S+b5ZdHTjgCFX4KpBc634G/WtEBUvBLoMQdlCD6uJHqodnOAxpt3+G2wiDeTmXAFJzgQ==
|
||||||
|
dependencies:
|
||||||
|
"@xyflow/system" "0.0.62"
|
||||||
|
classcat "^5.0.3"
|
||||||
|
zustand "^4.4.0"
|
||||||
|
|
||||||
|
"@xyflow/system@0.0.62":
|
||||||
|
version "0.0.62"
|
||||||
|
resolved "https://registry.npmjs.org/@xyflow/system/-/system-0.0.62.tgz#3ddb0f549f4ffa81f3924875dce4ddd9ccc9a31d"
|
||||||
|
integrity sha512-Z2ufbnvuYxIOCGyzE/8eX8TAEM8Lpzc/JafjD1Tzy6ZJs/E7KGVU17Q1F5WDHVW+dbztJAdyXMG0ejR9bwSUAA==
|
||||||
|
dependencies:
|
||||||
|
"@types/d3-drag" "^3.0.7"
|
||||||
|
"@types/d3-interpolate" "^3.0.4"
|
||||||
|
"@types/d3-selection" "^3.0.10"
|
||||||
|
"@types/d3-transition" "^3.0.8"
|
||||||
|
"@types/d3-zoom" "^3.0.8"
|
||||||
|
d3-drag "^3.0.0"
|
||||||
|
d3-interpolate "^3.0.1"
|
||||||
|
d3-selection "^3.0.0"
|
||||||
|
d3-zoom "^3.0.0"
|
||||||
|
|
||||||
"@yarnpkg/lockfile@^1.1.0":
|
"@yarnpkg/lockfile@^1.1.0":
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.npmmirror.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31"
|
resolved "https://registry.npmmirror.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31"
|
||||||
@ -14670,6 +14706,11 @@ electron-to-chromium@^1.5.73:
|
|||||||
resolved "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.76.tgz#db20295c5061b68f07c8ea4dfcbd701485d94a3d"
|
resolved "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.76.tgz#db20295c5061b68f07c8ea4dfcbd701485d94a3d"
|
||||||
integrity sha512-CjVQyG7n7Sr+eBXE86HIulnL5N8xZY1sgmOPGuq/F0Rr0FJq63lg0kEtOIDfZBk44FnDLf6FUJ+dsJcuiUDdDQ==
|
integrity sha512-CjVQyG7n7Sr+eBXE86HIulnL5N8xZY1sgmOPGuq/F0Rr0FJq63lg0kEtOIDfZBk44FnDLf6FUJ+dsJcuiUDdDQ==
|
||||||
|
|
||||||
|
elkjs@^0.10.0:
|
||||||
|
version "0.10.0"
|
||||||
|
resolved "https://registry.npmjs.org/elkjs/-/elkjs-0.10.0.tgz#abe2aa6cb25e7439b708fab873b2448d26ed33a1"
|
||||||
|
integrity sha512-v/3r+3Bl2NMrWmVoRTMBtHtWvRISTix/s9EfnsfEWApNrsmNjqgqJOispCGg46BPwIFdkag3N/HYSxJczvCm6w==
|
||||||
|
|
||||||
elkjs@^0.8.2:
|
elkjs@^0.8.2:
|
||||||
version "0.8.2"
|
version "0.8.2"
|
||||||
resolved "https://registry.npmmirror.com/elkjs/-/elkjs-0.8.2.tgz#c37763c5a3e24e042e318455e0147c912a7c248e"
|
resolved "https://registry.npmmirror.com/elkjs/-/elkjs-0.8.2.tgz#c37763c5a3e24e042e318455e0147c912a7c248e"
|
||||||
@ -30827,6 +30868,11 @@ use-sync-external-store@1.2.0, use-sync-external-store@^1.2.0:
|
|||||||
resolved "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
resolved "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
||||||
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||||
|
|
||||||
|
use-sync-external-store@^1.2.2:
|
||||||
|
version "1.5.0"
|
||||||
|
resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz#55122e2a3edd2a6c106174c27485e0fd59bcfca0"
|
||||||
|
integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==
|
||||||
|
|
||||||
use@^3.1.0:
|
use@^3.1.0:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.npmmirror.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
|
resolved "https://registry.npmmirror.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
|
||||||
@ -31967,6 +32013,13 @@ zrender@5.6.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib "2.3.0"
|
tslib "2.3.0"
|
||||||
|
|
||||||
|
zustand@^4.4.0:
|
||||||
|
version "4.5.7"
|
||||||
|
resolved "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz#7d6bb2026a142415dd8be8891d7870e6dbe65f55"
|
||||||
|
integrity sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==
|
||||||
|
dependencies:
|
||||||
|
use-sync-external-store "^1.2.2"
|
||||||
|
|
||||||
zustand@^4.4.1:
|
zustand@^4.4.1:
|
||||||
version "4.4.7"
|
version "4.4.7"
|
||||||
resolved "https://registry.npmmirror.com/zustand/-/zustand-4.4.7.tgz#355406be6b11ab335f59a66d2cf9815e8f24038c"
|
resolved "https://registry.npmmirror.com/zustand/-/zustand-4.4.7.tgz#355406be6b11ab335f59a66d2cf9815e8f24038c"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user