feat: collection categories (#1327)

* feat: collection categories

* feat: collection category

* feat: collection category

* feat: collection category belongsToMany

* feat: collection category

* feat: collection category

* feat: collection category

* feat: collection category local

* feat: collection category

* feat: collection category collection

* feat: collection category code improve

* feat: collection category code improve

* feat: collection category column

* feat: collection category color

* feat: collection category improve

* feat: collection category improve

* feat: collection category improve

* feat: collection category improve

* feat: collection category improve

* feat: collection category refresh

* feat: collection category improve

* feat: collection category improve

* feat: collection category improve

* feat: collection category improve

* feat: collection category improve

* feat: graph collection show category color

* feat: graph collection show category color

* feat: graph collection category style improve

* fix: change the variable names to camel case

* fix: category

* fix: collectionCategory

* fix: field options

---------

Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
katherinehhh 2023-02-10 09:15:37 +08:00 committed by GitHub
parent 2b7f138491
commit f8a11cbbf0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 882 additions and 165 deletions

View File

@ -1,13 +1,13 @@
import { Spin } from 'antd'; import { Spin } from 'antd';
import React, { useContext, useState } from 'react';
import { keyBy } from 'lodash'; import { keyBy } from 'lodash';
import React, { useContext, useState } from 'react';
import { useAPIClient, useRequest } from '../api-client'; import { useAPIClient, useRequest } from '../api-client';
import { CollectionManagerSchemaComponentProvider } from './CollectionManagerSchemaComponentProvider';
import { CollectionManagerContext } from './context';
import { CollectionManagerOptions } from './types';
import { templateOptions } from '../collection-manager/Configuration/templates'; import { templateOptions } from '../collection-manager/Configuration/templates';
import * as defaultInterfaces from './interfaces';
import { useCollectionHistory } from './CollectionHistoryProvider'; import { useCollectionHistory } from './CollectionHistoryProvider';
import { CollectionManagerSchemaComponentProvider } from './CollectionManagerSchemaComponentProvider';
import { CollectionCategroriesContext, CollectionManagerContext } from './context';
import * as defaultInterfaces from './interfaces';
import { CollectionManagerOptions } from './types';
export const CollectionManagerProvider: React.FC<CollectionManagerOptions> = (props) => { export const CollectionManagerProvider: React.FC<CollectionManagerOptions> = (props) => {
const { service, interfaces, collections = [], refreshCM, templates } = props; const { service, interfaces, collections = [], refreshCM, templates } = props;
@ -38,7 +38,7 @@ export const RemoteCollectionManagerProvider = (props: any) => {
action: 'list', action: 'list',
params: { params: {
paginate: false, paginate: false,
appends: ['fields', 'fields.uiSchema'], appends: ['fields', 'fields.uiSchema','category'],
filter: { filter: {
// inherit: false, // inherit: false,
}, },
@ -72,3 +72,33 @@ export const RemoteCollectionManagerProvider = (props: any) => {
/> />
); );
}; };
export const CollectionCategroriesProvider = (props) => {
const api = useAPIClient();
const options={
url: 'collectionCategories:list',
params: {
paginate: false,
sort:['sort']
},
}
const result = useRequest(options);
if (result.loading) {
return <Spin />;
}
return (
<CollectionCategroriesContext.Provider
value={{
...result,
data: result?.data?.data,
refresh:async ()=>{
const { data } = await api.request(options);
result.mutate(data);
return data?.data || [];
}
}}
>
{props.children}
</CollectionCategroriesContext.Provider>
);
};

View File

@ -19,10 +19,17 @@ import {
ViewFieldAction, ViewFieldAction,
AddCollection, AddCollection,
AddCollectionAction, AddCollectionAction,
AddCategoryAction,
AddCategory,
EditCollection, EditCollection,
EditCollectionAction, EditCollectionAction,
ConfigurationTabs,
EditCategory,
EditCategoryAction,
} from './Configuration'; } from './Configuration';
import { CollectionCategroriesProvider } from './CollectionManagerProvider';
const schema: ISchema = { const schema: ISchema = {
type: 'object', type: 'object',
properties: { properties: {
@ -43,6 +50,7 @@ const schema2: ISchema = {
type: 'object', type: 'object',
properties: { properties: {
[uid()]: { [uid()]: {
'x-decorator': 'CollectionCategroriesProvider',
'x-component': 'ConfigurationTable', 'x-component': 'ConfigurationTable',
}, },
}, },
@ -50,15 +58,19 @@ const schema2: ISchema = {
export const CollectionManagerPane = () => { export const CollectionManagerPane = () => {
return ( return (
<Card bordered={false}> // <Card bordered={false}>
<SchemaComponent <SchemaComponent
schema={schema2} schema={schema2}
components={{ components={{
CollectionCategroriesProvider,
ConfigurationTable, ConfigurationTable,
ConfigurationTabs,
AddFieldAction, AddFieldAction,
AddCollectionField, AddCollectionField,
AddCollection, AddCollection,
AddCollectionAction, AddCollectionAction,
AddCategoryAction,
AddCategory,
EditCollection, EditCollection,
EditCollectionAction, EditCollectionAction,
EditFieldAction, EditFieldAction,
@ -67,9 +79,11 @@ export const CollectionManagerPane = () => {
OverridingFieldAction, OverridingFieldAction,
ViewCollectionField, ViewCollectionField,
ViewFieldAction, ViewFieldAction,
EditCategory,
EditCategoryAction,
}} }}
/> />
</Card> // </Card>
); );
}; };
@ -103,12 +117,15 @@ export const CollectionManagerShortcut2 = () => {
schema={schema} schema={schema}
components={{ components={{
ConfigurationTable, ConfigurationTable,
ConfigurationTabs,
AddFieldAction, AddFieldAction,
EditFieldAction, EditFieldAction,
OverridingFieldAction, OverridingFieldAction,
ViewFieldAction, ViewFieldAction,
AddCollectionAction, AddCollectionAction,
EditCollectionAction, EditCollectionAction,
AddCategoryAction,
EditCategoryAction,
}} }}
/> />
</ActionContext.Provider> </ActionContext.Provider>

View File

@ -0,0 +1,60 @@
import { PlusOutlined } from '@ant-design/icons';
import { useForm } from '@formily/react';
import { cloneDeep } from 'lodash';
import React, { useContext, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useAPIClient } from '../../api-client';
import { ActionContext, SchemaComponent, useActionContext } from '../../schema-component';
import { useCancelAction } from '../action-hooks';
import { CollectionCategroriesContext } from '../context';
import * as components from './components';
import { collectionCategorySchema } from './schemas/collections';
const useCreateCategry = () => {
const form = useForm();
const ctx = useActionContext();
const { refresh } = useContext(CollectionCategroriesContext);
const api = useAPIClient();
return {
async run() {
await form.submit();
const values = cloneDeep(form.values);
await api.resource('collectionCategories').create({
values: {
...values,
},
});
ctx.setVisible(false);
await form.reset();
await refresh();
},
};
};
export const AddCategory = (props) => {
return <AddCategoryAction {...props} />;
};
export const AddCategoryAction = (props) => {
const { scope, getContainer, children } = props;
const [visible, setVisible] = useState(false);
const { t } = useTranslation();
return (
<ActionContext.Provider value={{ visible, setVisible }}>
<div onClick={() => setVisible(true)} title={t('Add category')}>
{children || <PlusOutlined />}
</div>
<SchemaComponent
schema={collectionCategorySchema}
components={{ ...components }}
scope={{
getContainer,
useCancelAction,
createOnly: true,
useCreateCategry,
...scope,
}}
/>
</ActionContext.Provider>
);
};

View File

@ -15,7 +15,7 @@ import { useResourceActionContext, useResourceContext } from '../ResourceActionP
import * as components from './components'; import * as components from './components';
import { templateOptions } from './templates'; import { templateOptions } from './templates';
const getSchema = (schema, record: any, compile): ISchema => { const getSchema = (schema, category, compile): ISchema => {
if (!schema) { if (!schema) {
return; return;
} }
@ -30,6 +30,7 @@ const getSchema = (schema, record: any, compile): ISchema => {
const initialValue: any = { const initialValue: any = {
name: `t_${uid()}`, name: `t_${uid()}`,
template: schema.name, template: schema.name,
category,
...cloneDeep(schema.default), ...cloneDeep(schema.default),
}; };
if (initialValue.reverseField) { if (initialValue.reverseField) {
@ -234,6 +235,9 @@ export const AddCollectionAction = (props) => {
const items = templateOptions().map((option) => { const items = templateOptions().map((option) => {
return { label: compile(option.title), key: option.name }; return { label: compile(option.title), key: option.name };
}); });
const {
state: { category },
} = useResourceActionContext();
return ( return (
<RecordProvider record={record}> <RecordProvider record={record}>
<ActionContext.Provider value={{ visible, setVisible }}> <ActionContext.Provider value={{ visible, setVisible }}>
@ -248,7 +252,7 @@ export const AddCollectionAction = (props) => {
overflow: 'auto', overflow: 'auto',
}} }}
onClick={(info) => { onClick={(info) => {
const schema = getSchema(getTemplate(info.key), record, compile); const schema = getSchema(getTemplate(info.key), category, compile);
setSchema(schema); setSchema(schema);
setVisible(true); setVisible(true);
}} }}

View File

@ -7,6 +7,7 @@ import { useCurrentAppInfo } from '../../appInfo';
import { useRecord } from '../../record-provider'; import { useRecord } from '../../record-provider';
import { SchemaComponent, SchemaComponentContext, useCompile } from '../../schema-component'; import { SchemaComponent, SchemaComponentContext, useCompile } from '../../schema-component';
import { useCancelAction } from '../action-hooks'; import { useCancelAction } from '../action-hooks';
import { CollectionCategroriesContext } from '../context';
import { useCollectionManager } from '../hooks/useCollectionManager'; import { useCollectionManager } from '../hooks/useCollectionManager';
import { DataSourceContext } from '../sub-table'; import { DataSourceContext } from '../sub-table';
import { AddSubFieldAction } from './AddSubFieldAction'; import { AddSubFieldAction } from './AddSubFieldAction';
@ -76,6 +77,7 @@ export const ConfigurationTable = () => {
const { const {
data: { database }, data: { database },
} = useCurrentAppInfo(); } = useCurrentAppInfo();
const data = useContext(CollectionCategroriesContext);
const collectonsRef: any = useRef(); const collectonsRef: any = useRef();
collectonsRef.current = collections; collectonsRef.current = collections;
const compile = useCompile(); const compile = useCompile();
@ -93,9 +95,14 @@ export const ConfigurationTable = () => {
value: item.name, value: item.name,
})); }));
}; };
const loadCategories = async () => {
return data.data.map((item: any) => ({
label: compile(item.name),
value: item.id,
}));
};
const ctx = useContext(SchemaComponentContext); const ctx = useContext(SchemaComponentContext);
return ( return (
<div>
<SchemaComponentContext.Provider value={{ ...ctx, designable: false }}> <SchemaComponentContext.Provider value={{ ...ctx, designable: false }}>
<SchemaComponent <SchemaComponent
schema={collectionSchema} schema={collectionSchema}
@ -111,6 +118,7 @@ export const ConfigurationTable = () => {
useSelectedRowKeys, useSelectedRowKeys,
useAsyncDataSource, useAsyncDataSource,
loadCollections, loadCollections,
loadCategories,
useCurrentFields, useCurrentFields,
useNewId, useNewId,
useCancelAction, useCancelAction,
@ -119,6 +127,5 @@ export const ConfigurationTable = () => {
}} }}
/> />
</SchemaComponentContext.Provider> </SchemaComponentContext.Provider>
</div>
); );
}; };

View File

@ -0,0 +1,255 @@
import { MenuOutlined } from '@ant-design/icons';
import {
DndContext,
DragEndEvent,
DragOverlay,
MouseSensor,
useDraggable,
useDroppable,
useSensor,
useSensors
} from '@dnd-kit/core';
import { observer, RecursionField } from '@formily/react';
import { uid } from '@formily/shared';
import { Badge, Card, Dropdown, Menu, Modal, Tabs } from 'antd';
import React, { useContext, useState } from 'react';
import { useAPIClient } from '../../api-client';
import { SchemaComponent, SchemaComponentOptions, useCompile } from '../../schema-component';
import { CollectionCategroriesContext } from '../context';
import { useResourceActionContext } from '../ResourceActionProvider';
import { collectionTableSchema } from './schemas/collections';
function Draggable(props) {
const { attributes, listeners, setNodeRef } = useDraggable({
id: props.id,
data: props.data,
});
return (
<div ref={setNodeRef} {...listeners} {...attributes}>
<div>{props.children}</div>
</div>
);
}
function Droppable(props) {
const { isOver, setNodeRef } = useDroppable({
id: props.id,
data: props.data,
});
const style = isOver
? {
color: 'green',
}
: undefined;
return (
<div ref={setNodeRef} style={style}>
{props.children}
</div>
);
}
const TabTitle = observer(({ item }: { item: any }) => {
return (
<Droppable id={item.id.toString()} data={item}>
<div>
<Draggable id={item.id.toString()} data={item}>
<TabBar item={item} />
</Draggable>
</div>
</Droppable>
);
});
const TabBar = ({ item }) => {
const compile = useCompile();
return (
<span>
<Badge color={item.color} />
{compile(item.name)}
</span>
);
};
const DndProvider = observer((props) => {
const [activeTab, setActiveId] = useState(null);
const { refresh } = useContext(CollectionCategroriesContext);
const { refresh: refreshCM } = useResourceActionContext();
const api = useAPIClient();
const onDragEnd = async (props: DragEndEvent) => {
const { active, over } = props;
setTimeout(() => {
setActiveId(null);
});
if (over && over.id !== active.id) {
await api.resource('collectionCategories').move({
sourceId: active.id,
targetId: over.id,
});
await refresh();
await refreshCM();
}
};
function onDragStart(event) {
setActiveId(event.active?.data.current);
}
const mouseSensor = useSensor(MouseSensor, {
activationConstraint: {
distance: 10,
},
});
const sensors = useSensors(mouseSensor);
return (
<DndContext sensors={sensors} onDragEnd={onDragEnd} onDragStart={onDragStart}>
{props.children}
<DragOverlay>
{activeTab ? <span style={{ whiteSpace: 'nowrap' }}>{<TabBar item={activeTab} />}</span> : null}
</DragOverlay>
</DndContext>
);
});
export const ConfigurationTabs = () => {
const { data, refresh } = useContext(CollectionCategroriesContext);
const { refresh: refreshCM, run, defaultRequest, setState } = useResourceActionContext();
const [key, setKey] = useState('all');
const tabsItems = data
.sort((a, b) => b.sort - a.sort)
.concat()
.map((v) => {
return {
...v,
schema: collectionTableSchema,
};
});
!tabsItems.find((v) => v.id === 'all') &&
tabsItems.unshift({
name: '{{t("All collections")}}',
id: 'all',
sort: 0,
closable: false,
schema: collectionTableSchema,
});
const compile = useCompile();
const [activeKey, setActiveKey] = useState('all');
const api = useAPIClient();
const onChange = (key: string) => {
setActiveKey(key);
setKey(uid());
if (key !== 'all') {
const prevFilter = defaultRequest?.params?.filter;
const filter = { $and: [prevFilter, { 'category.id': key }] };
run({ filter });
setState?.({ category: [+key], params: [{ filter }] });
} else {
run();
setState?.({ category: [], params: [] });
}
};
const remove = (key: any) => {
Modal.confirm({
title: compile("{{t('Delete category')}}"),
content: compile("{{t('Are you sure you want to delete it?')}}"),
onOk: async () => {
await api.resource('collectionCategories').destroy({
filter: {
id: key,
},
});
key === +activeKey && setActiveKey('all');
await refresh();
await refreshCM();
},
});
};
const loadCategories = async () => {
return data.map((item: any) => ({
label: compile(item.name),
value: item.id,
}));
};
const menu = (item) => (
<Menu>
<Menu.Item key={'edit'}>
<SchemaComponent
schema={{
type: 'void',
properties: {
[uid()]: {
'x-component': 'EditCategory',
'x-component-props': {
item: item,
},
},
},
}}
/>
</Menu.Item>
<Menu.Item key="delete" onClick={() => remove(item.id)}>
{compile("{{t('Delete category')}}")}
</Menu.Item>
</Menu>
);
return (
<DndProvider>
<Tabs
addIcon={
<SchemaComponent
schema={{
type: 'void',
properties: {
addCategories: {
type: 'void',
title: '{{ t("Add category") }}',
'x-component': 'AddCategory',
'x-component-props': {
type: 'primary',
},
},
},
}}
/>
}
onChange={onChange}
defaultActiveKey="all"
type="editable-card"
destroyInactiveTabPane={true}
tabBarStyle={{ marginBottom: '0px' }}
>
{tabsItems.map((item) => {
return (
<Tabs.TabPane
tab={
item.id !== 'all' ? (
<div data-no-dnd="true">
<TabTitle item={item} />
</div>
) : (
compile(item.name)
)
}
key={item.id}
closable={item.closable}
closeIcon={
<Dropdown overlay={menu(item)}>
<MenuOutlined />
</Dropdown>
}
>
<Card bordered={false}>
<SchemaComponentOptions
inherit
scope={{ loadCategories, categoryVisible: item.id === 'all', categoryId: item.id }}
>
<RecursionField name={key} schema={item.schema} onlyRenderProperties />
</SchemaComponentOptions>
</Card>
</Tabs.TabPane>
);
})}
</Tabs>
</DndProvider>
);
};

View File

@ -0,0 +1,81 @@
import { useForm } from '@formily/react';
import { cloneDeep } from 'lodash';
import React, { useContext, useEffect, useState } from 'react';
import { useAPIClient, useRequest } from '../../api-client';
import { RecordProvider, useRecord } from '../../record-provider';
import { ActionContext, SchemaComponent, useActionContext, useCompile } from '../../schema-component';
import { useCancelAction } from '../action-hooks';
import { CollectionCategroriesContext } from '../context';
import { useResourceActionContext } from '../ResourceActionProvider';
import * as components from './components';
import { collectionCategoryEditSchema } from './schemas/collections';
const useEditCategry = () => {
const form = useForm();
const ctx = useActionContext();
const { refresh } = useContext(CollectionCategroriesContext);
const { refresh: refreshCM } = useResourceActionContext();
const api = useAPIClient();
const { id } = useRecord();
return {
async run() {
await form.submit();
const values = cloneDeep(form.values);
await api.resource('collectionCategories').update({
filter: { id: id },
values: {
...values,
},
});
ctx.setVisible(false);
await form.reset();
await refresh();
await refreshCM();
},
};
};
const useValuesFromRecord = (options) => {
const record = useRecord();
const result = useRequest(() => Promise.resolve({ data: { ...record } }), {
...options,
manual: true,
});
const ctx = useActionContext();
useEffect(() => {
if (ctx.visible) {
result.run();
}
}, [ctx.visible]);
return result;
};
export const EditCategory = (props) => {
return <EditCategoryAction {...props} />;
};
export const EditCategoryAction = (props) => {
const { scope, getContainer, item, children } = props;
const [visible, setVisible] = useState(false);
const compile = useCompile();
return (
<RecordProvider record={item}>
<ActionContext.Provider value={{ visible, setVisible }}>
<>{children || <span onClick={() => setVisible(true)}>{compile('{{ t("Edit category") }}')}</span>}</>
<SchemaComponent
schema={collectionCategoryEditSchema}
components={{ ...components }}
scope={{
getContainer,
useCancelAction,
createOnly: true,
useEditCategry,
useValuesFromRecord,
...scope,
}}
/>
</ActionContext.Provider>
</RecordProvider>
);
};

View File

@ -78,7 +78,7 @@ const getSchema = (schema: IField, record: any, compile, getContainer): ISchema
export const useValuesFromRecord = (options) => { export const useValuesFromRecord = (options) => {
const record = useRecord(); const record = useRecord();
const result = useRequest(() => Promise.resolve({ data: { autoGenId: true, ...record } }), { const result = useRequest(() => Promise.resolve({ data: { autoGenId: true, ...record,category:record?.category.map((v)=>v.id) } }), {
...options, ...options,
manual: true, manual: true,
}); });

View File

@ -0,0 +1,16 @@
import { observer } from '@formily/react';
import { Tag } from 'antd';
import React from 'react';
import { useCompile } from '../../../schema-component';
export const CollectionCategory = observer((props: any) => {
const { value } = props;
const compile = useCompile();
return (
<>
{value.map((item) => {
return <Tag color={item.color}>{compile(item?.name)}</Tag>;
})}
</>
);
});

View File

@ -11,6 +11,9 @@ export * from './OverridingCollectionField';
export * from './ViewInheritedField'; export * from './ViewInheritedField';
export * from './AddCollectionAction'; export * from './AddCollectionAction';
export * from './EditCollectionAction'; export * from './EditCollectionAction';
export * from './ConfigurationTabs';
export * from './AddCategoryAction';
export * from './EditCategoryAction'
registerValidateFormats({ registerValidateFormats({
uid: /^[A-Za-z0-9][A-Za-z0-9_-]*$/, uid: /^[A-Za-z0-9][A-Za-z0-9_-]*$/,

View File

@ -1,10 +1,12 @@
import { ISchema, Schema } from '@formily/react'; import { ISchema, Schema } from '@formily/react';
import { message } from 'antd'; import { message } from 'antd';
import { uid } from '@formily/shared';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAPIClient } from '../../../api-client'; import { useAPIClient } from '../../../api-client';
import { i18n } from '../../../i18n'; import { i18n } from '../../../i18n';
import { CollectionOptions } from '../../types'; import { CollectionOptions } from '../../types';
import { CollectionTemplate } from '../components/CollectionTemplate'; import { CollectionTemplate } from '../components/CollectionTemplate';
import { CollectionCategory } from '../components/CollectionCategory';
import { collectionFieldSchema } from './collectionFields'; import { collectionFieldSchema } from './collectionFields';
const compile = (source) => { const compile = (source) => {
@ -94,175 +96,197 @@ export const collectionSchema: ISchema = {
filter: { filter: {
'hidden.$isFalsy': true, 'hidden.$isFalsy': true,
}, },
appends: [], appends: ['category'],
}, },
}, },
}, },
properties: { properties: {
actions: { tabs: {
type: 'void', type: 'void',
'x-component': 'ActionBar', 'x-component': 'ConfigurationTabs',
'x-component-props': { },
style: { },
marginBottom: 16, },
}, },
};
export const collectionTableSchema: ISchema = {
type: 'object',
properties: {
[uid()]: {
type: 'void',
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 16,
},
},
properties: {
filter: {
type: 'void',
title: '{{ t("Filter") }}',
default: {
$and: [{ title: { $includes: '' } }, { name: { $includes: '' } }],
}, },
properties: { 'x-action': 'filter',
filter: { 'x-component': 'Filter.Action',
type: 'void', 'x-component-props': {
title: '{{ t("Filter") }}', icon: 'FilterOutlined',
default: { useProps: '{{ cm.useFilterActionProps }}',
$and: [{ title: { $includes: '' } }, { name: { $includes: '' } }], },
}, 'x-align': 'left',
'x-action': 'filter', },
'x-component': 'Filter.Action', delete: {
'x-component-props': { type: 'void',
icon: 'FilterOutlined', title: '{{ t("Delete") }}',
useProps: '{{ cm.useFilterActionProps }}', 'x-component': 'Action',
}, 'x-component-props': {
'x-align': 'left', icon: 'DeleteOutlined',
}, useAction: '{{ cm.useBulkDestroyActionAndRefreshCM }}',
delete: { confirm: {
type: 'void', title: "{{t('Delete record')}}",
title: '{{ t("Delete") }}', content: "{{t('Are you sure you want to delete it?')}}",
'x-component': 'Action',
'x-component-props': {
icon: 'DeleteOutlined',
useAction: '{{ cm.useBulkDestroyActionAndRefreshCM }}',
confirm: {
title: "{{t('Delete record')}}",
content: "{{t('Are you sure you want to delete it?')}}",
},
},
},
create: {
type: 'void',
title: '{{ t("Create collection") }}',
'x-component': 'AddCollection',
'x-component-props': {
type: 'primary',
},
}, },
}, },
}, },
table: { create: {
type: 'void', type: 'void',
'x-uid': 'input', title: '{{ t("Create collection") }}',
'x-component': 'Table.Void', 'x-component': 'AddCollection',
'x-component-props': { 'x-component-props': {
rowKey: 'name', type: 'primary',
rowSelection: { },
type: 'checkbox', },
},
},
[uid()]: {
type: 'void',
'x-uid': 'input',
'x-component': 'Table.Void',
'x-component-props': {
rowKey: 'name',
rowSelection: {
type: 'checkbox',
},
useDataSource: '{{ cm.useDataSourceFromRAC }}',
useAction() {
const api = useAPIClient();
const { t } = useTranslation();
return {
async move(from, to) {
await api.resource('collections').move({
sourceId: from.key,
targetId: to.key,
});
message.success(t('Saved successfully'), 0.2);
}, },
useDataSource: '{{ cm.useDataSourceFromRAC }}', };
useAction() { },
const api = useAPIClient(); },
const { t } = useTranslation(); properties: {
return { column1: {
async move(from, to) { type: 'void',
console.log(from, to); 'x-decorator': 'Table.Column.Decorator',
await api.resource('collections').move({ 'x-component': 'Table.Column',
sourceId: from.key, properties: {
targetId: to.key, title: {
}); 'x-component': 'CollectionField',
message.success(t('Saved successfully'), 0.2); 'x-read-pretty': true,
},
};
}, },
}, },
},
column2: {
type: 'void',
'x-decorator': 'Table.Column.Decorator',
'x-component': 'Table.Column',
properties: { properties: {
column1: { name: {
type: 'void', type: 'string',
'x-decorator': 'Table.Column.Decorator', 'x-component': 'CollectionField',
'x-component': 'Table.Column', 'x-read-pretty': true,
properties: {
title: {
'x-component': 'CollectionField',
'x-read-pretty': true,
},
},
}, },
column2: { },
type: 'void', },
'x-decorator': 'Table.Column.Decorator', column3: {
'x-component': 'Table.Column', type: 'void',
properties: { 'x-decorator': 'Table.Column.Decorator',
name: { 'x-component': 'Table.Column',
type: 'string', title: '{{t("Collection template")}}',
'x-component': 'CollectionField', properties: {
'x-read-pretty': true, template: {
}, 'x-component': CollectionTemplate,
}, 'x-read-pretty': true,
}, },
column3: { },
type: 'void', },
'x-decorator': 'Table.Column.Decorator', column4: {
'x-component': 'Table.Column', type: 'void',
title: '{{t("Collection template")}}', 'x-decorator': 'Table.Column.Decorator',
properties: { 'x-component': 'Table.Column',
template: { 'x-visible': 'categoryVisible',
'x-component': CollectionTemplate, title: '{{t("Collection category")}}',
'x-read-pretty': true, properties: {
}, category: {
}, 'x-component': CollectionCategory,
'x-read-pretty': true,
}, },
column4: { },
},
column5: {
type: 'void',
title: '{{ t("Actions") }}',
'x-component': 'Table.Column',
properties: {
actions: {
type: 'void', type: 'void',
title: '{{ t("Actions") }}', 'x-component': 'Space',
'x-component': 'Table.Column', 'x-component-props': {
split: '|',
},
properties: { properties: {
actions: { view: {
type: 'void', type: 'void',
'x-component': 'Space', title: '{{ t("Configure fields") }}',
'x-component-props': { 'x-component': 'Action.Link',
split: '|', 'x-component-props': {},
},
properties: { properties: {
view: { drawer: {
type: 'void', type: 'void',
title: '{{ t("Configure fields") }}', 'x-component': 'Action.Drawer',
'x-component': 'Action.Link', 'x-component-props': {
'x-component-props': {}, destroyOnClose: true,
},
'x-reactions': (field) => {
const i = field.path.segments[1];
const table = field.form.getValuesIn(`table.${i}`);
if (table) {
field.title = `${compile(table.title)} - ${compile('{{ t("Configure fields") }}')}`;
}
},
properties: { properties: {
drawer: { collectionFieldSchema,
type: 'void',
'x-component': 'Action.Drawer',
'x-component-props': {
destroyOnClose: true,
},
'x-reactions': (field) => {
const i = field.path.segments[1];
const table = field.form.getValuesIn(`table.${i}`);
if (table) {
field.title = `${compile(table.title)} - ${compile('{{ t("Configure fields") }}')}`;
}
},
properties: {
collectionFieldSchema,
},
},
}, },
}, },
update: { },
type: 'void', },
title: '{{ t("Edit") }}', update: {
'x-component': 'EditCollection', type: 'void',
'x-component-props': { title: '{{ t("Edit") }}',
type: 'primary', 'x-component': 'EditCollection',
}, 'x-component-props': {
}, type: 'primary',
delete: { },
type: 'void', },
title: '{{ t("Delete") }}', delete: {
'x-component': 'Action.Link', type: 'void',
'x-component-props': { title: '{{ t("Delete") }}',
confirm: { 'x-component': 'Action.Link',
title: "{{t('Delete record')}}", 'x-component-props': {
content: "{{t('Are you sure you want to delete it?')}}", confirm: {
}, title: "{{t('Delete record')}}",
useAction: '{{ cm.useDestroyActionAndRefreshCM }}', content: "{{t('Are you sure you want to delete it?')}}",
},
}, },
useAction: '{{ cm.useDestroyActionAndRefreshCM }}',
}, },
}, },
}, },
@ -273,3 +297,114 @@ export const collectionSchema: ISchema = {
}, },
}, },
}; };
export const collectionCategorySchema: ISchema = {
type: 'object',
properties: {
form: {
type: 'void',
'x-decorator': 'Form',
'x-component': 'Action.Modal',
title: '{{ t("Add category") }}',
'x-component-props': {
width: 520,
getContainer: '{{ getContainer }}',
},
properties: {
name: {
type: 'string',
title: '{{t("Category name")}}',
required: true,
'x-disabled': '{{ !createOnly }}',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
color: {
type: 'string',
title: '{{t("Color")}}',
required: false,
'x-decorator': 'FormItem',
'x-component': 'ColorSelect',
},
footer: {
type: 'void',
'x-component': 'Action.Modal.Footer',
properties: {
action1: {
title: '{{ t("Cancel") }}',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ useCancelAction }}',
},
},
action2: {
title: '{{ t("Submit") }}',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
useAction: '{{ useCreateCategry }}',
},
},
},
},
},
},
},
};
export const collectionCategoryEditSchema: ISchema = {
type: 'object',
properties: {
form: {
type: 'void',
'x-decorator': 'Form',
'x-decorator-props': {
useValues: '{{ useValuesFromRecord }}',
},
'x-component': 'Action.Modal',
title: '{{ t("Edit category") }}',
'x-component-props': {
width: 520,
getContainer: '{{ getContainer }}',
},
properties: {
name: {
type: 'string',
title: '{{t("Category name")}}',
required: true,
'x-disabled': '{{ !createOnly }}',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
color: {
type: 'string',
title: '{{t("Color")}}',
required: false,
'x-decorator': 'FormItem',
'x-component': 'ColorSelect',
},
footer: {
type: 'void',
'x-component': 'Action.Modal.Footer',
properties: {
action1: {
title: '{{ t("Cancel") }}',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ useCancelAction }}',
},
},
action2: {
title: '{{ t("Submit") }}',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
useAction: '{{ useEditCategry }}',
},
},
},
},
},
},
},
};

View File

@ -329,5 +329,9 @@ export const useFilterActionProps = () => {
const { collection } = useResourceContext(); const { collection } = useResourceContext();
const options = useFilterFieldOptions(collection.fields); const options = useFilterFieldOptions(collection.fields);
const service = useResourceActionContext(); const service = useResourceActionContext();
return useFilterFieldProps({ options, params: service.params, service }); return useFilterFieldProps({
options,
params: service.state?.params?.[0] || service.params,
service,
});
}; };

View File

@ -9,3 +9,5 @@ export const CollectionManagerContext = createContext<CollectionManagerOptions>(
export const CollectionContext = createContext<CollectionOptions>({}); export const CollectionContext = createContext<CollectionOptions>({});
export const CollectionFieldContext = createContext<CollectionFieldOptions>({}); export const CollectionFieldContext = createContext<CollectionFieldOptions>({});
export const CollectionCategroriesContext = createContext({ data: [], refresh: () => {} });

View File

@ -38,5 +38,5 @@ export const calendar: ICollectionTemplate = {
availableFieldInterfaces: { availableFieldInterfaces: {
include: [], include: [],
}, },
configurableProperties: getConfigurableProperties('title', 'name', 'inherits'), configurableProperties: getConfigurableProperties('title', 'name', 'inherits','category'),
}; };

View File

@ -9,5 +9,5 @@ export const general: ICollectionTemplate = {
default: { default: {
fields: [], fields: [],
}, },
configurableProperties: getConfigurableProperties('title', 'name', 'inherits', 'moreOptions'), configurableProperties: getConfigurableProperties('title', 'name', 'inherits', 'category', 'moreOptions'),
}; };

View File

@ -102,6 +102,17 @@ export const defaultConfigurableProperties = {
'x-visible': '{{ enableInherits}}', 'x-visible': '{{ enableInherits}}',
'x-reactions': ['{{useAsyncDataSource(loadCollections)}}'], 'x-reactions': ['{{useAsyncDataSource(loadCollections)}}'],
}, },
category: {
title: '{{t("Categories")}}',
type: 'hasMany',
name: 'category',
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {
mode: 'multiple',
},
'x-reactions': ['{{useAsyncDataSource(loadCategories)}}'],
},
...moreOptions, ...moreOptions,
moreOptions: { moreOptions: {
title: '{{t("More options")}}', title: '{{t("More options")}}',
@ -124,6 +135,7 @@ export type DefaultConfigurableKeys =
| 'name' | 'name'
| 'title' | 'title'
| 'inherits' | 'inherits'
| 'category'
| 'autoGenId' | 'autoGenId'
| 'createdBy' | 'createdBy'
| 'updatedBy' | 'updatedBy'

View File

@ -34,6 +34,13 @@ export default {
"UI editor": "UI editor", "UI editor": "UI editor",
"Collection": "Collection", "Collection": "Collection",
"Collections & Fields": "Collections & Fields", "Collections & Fields": "Collections & Fields",
"All collections":"All collections",
"Add category":"Add category",
"Delete category":"Delete category",
"Edit category":"Edit category",
"Collection category":"Collection category",
"Sort":"Sort",
"Category name":"Category name",
"Roles & Permissions": "Roles & Permissions", "Roles & Permissions": "Roles & Permissions",
"Edit profile": "Edit profile", "Edit profile": "Edit profile",
"Change password": "Change password", "Change password": "Change password",

View File

@ -34,6 +34,13 @@ export default {
"UI editor": "UI エディタ", "UI editor": "UI エディタ",
"Collection": "コレクション", "Collection": "コレクション",
"Collections & Fields": "コレクションとフィールド", "Collections & Fields": "コレクションとフィールド",
"All collections":"すべてのデータテーブル",
"Add category":"分類の追加",
"Edit category":"分類の編集",
"Sort":"ソート#ソート#",
"Category name":"分類名",
"Delete category":"分類の削除",
"Collection category":"Collection category",
"Roles & Permissions": "役割と権限", "Roles & Permissions": "役割と権限",
"Edit profile": "プロフィール", "Edit profile": "プロフィール",
"Change password": "パスワード変更", "Change password": "パスワード変更",

View File

@ -34,6 +34,13 @@ export default {
"UI editor": "界面配置", "UI editor": "界面配置",
"Collection": "数据表", "Collection": "数据表",
"Collections & Fields": "数据表配置", "Collections & Fields": "数据表配置",
"All collections":"全部数据表",
"Add category":"添加分类",
"Delete category":"删除分类",
"Edit category":"编辑分类",
"Collection category":"数据表类别",
"Sort":"排序",
"Category name":"分类名称",
"Roles & Permissions": "角色和权限", "Roles & Permissions": "角色和权限",
"Edit profile": "个人资料", "Edit profile": "个人资料",
"Change password": "修改密码", "Change password": "修改密码",

View File

@ -1,10 +1,17 @@
import { MenuOutlined } from '@ant-design/icons'; import { MenuOutlined } from '@ant-design/icons';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { ArrayField, Field } from '@formily/core'; import { ArrayField, Field } from '@formily/core';
import { observer, RecursionField, Schema, useField, useFieldSchema } from '@formily/react'; import {
observer,
RecursionField,
Schema,
useField,
useFieldSchema,
SchemaExpressionScopeContext,
} from '@formily/react';
import { Table, TableColumnProps } from 'antd'; import { Table, TableColumnProps } from 'antd';
import { default as classNames, default as cls } from 'classnames'; import { default as classNames, default as cls } from 'classnames';
import React, { useState } from 'react'; import React, { useState, useContext } from 'react';
import ReactDragListView from 'react-drag-listview'; import ReactDragListView from 'react-drag-listview';
import { DndContext } from '../..'; import { DndContext } from '../..';
import { RecordIndexProvider, RecordProvider, useRequest, useSchemaInitializer } from '../../../'; import { RecordIndexProvider, RecordProvider, useRequest, useSchemaInitializer } from '../../../';
@ -13,6 +20,11 @@ const isColumnComponent = (schema: Schema) => {
return schema['x-component']?.endsWith('.Column') > -1; return schema['x-component']?.endsWith('.Column') > -1;
}; };
const useScope = (key: any) => {
const scope = useContext(SchemaExpressionScopeContext);
return scope[key] !== false;
};
const useTableColumns = () => { const useTableColumns = () => {
const start = Date.now(); const start = Date.now();
const field = useField<ArrayField>(); const field = useField<ArrayField>();
@ -20,9 +32,10 @@ const useTableColumns = () => {
const { exists, render } = useSchemaInitializer(schema['x-initializer']); const { exists, render } = useSchemaInitializer(schema['x-initializer']);
const columns = schema const columns = schema
.reduceProperties((buf, s) => { .reduceProperties((buf, s) => {
if (isColumnComponent(s)) { if (isColumnComponent(s) && useScope(s['x-visible'])) {
return buf.concat([s]); return buf.concat([s]);
} }
return buf
}, []) }, [])
.map((s: Schema) => { .map((s: Schema) => {
return { return {

View File

@ -0,0 +1,32 @@
import { CollectionOptions } from '@nocobase/database';
export default {
name: 'collectionCategories',
autoGenId: true,
sortable: true,
fields: [
{
type: 'string',
name: 'name',
},
// {
// type: 'integer',
// name: 'sort',
// defaultValue: 0,
// },
{
type: 'string',
name: 'color',
defaultValue: 'default',
},
{
type: 'belongsToMany',
name: 'collections',
target: 'collections',
foreignKey: 'categoryId',
otherKey: 'collectionName',
targetKey: 'name',
through: 'collectionCategory',
},
],
} as CollectionOptions;

View File

@ -50,5 +50,15 @@ export default {
foreignKey: 'collectionName', foreignKey: 'collectionName',
sortBy: 'sort', sortBy: 'sort',
}, },
{
type: 'belongsToMany',
name: 'category',
target: 'collectionCategories',
sourceKey: 'name',
foreignKey: 'collectionName',
otherKey: 'categoryId',
targetKey: 'id',
through: 'collectionCategory',
},
], ],
} as CollectionOptions; } as CollectionOptions;

View File

@ -22,7 +22,7 @@ import {
useCollectionManager, useCollectionManager,
useCompile, useCompile,
useCurrentAppInfo, useCurrentAppInfo,
useRecord useRecord,
} from '@nocobase/client'; } from '@nocobase/client';
import { Badge, Dropdown, Popover, Tag } from 'antd'; import { Badge, Dropdown, Popover, Tag } from 'antd';
import { groupBy } from 'lodash'; import { groupBy } from 'lodash';
@ -33,7 +33,7 @@ import {
useDestroyActionAndRefreshCM, useDestroyActionAndRefreshCM,
useDestroyFieldActionAndRefreshCM, useDestroyFieldActionAndRefreshCM,
useUpdateCollectionActionAndRefreshCM, useUpdateCollectionActionAndRefreshCM,
useValuesFromRecord useValuesFromRecord,
} from '../action-hooks'; } from '../action-hooks';
import { collectiionPopoverClass, entityContainer, headClass, tableBtnClass, tableNameClass } from '../style'; import { collectiionPopoverClass, entityContainer, headClass, tableBtnClass, tableNameClass } from '../style';
import { useGCMTranslation } from '../utils'; import { useGCMTranslation } from '../utils';
@ -60,6 +60,7 @@ const Entity: React.FC<{
const database = useCurrentAppInfo(); const database = useCurrentAppInfo();
const collectionData = useRef(); const collectionData = useRef();
collectionData.current = { ...item, title, inherits: item.inherits && new Proxy(item.inherits, {}) }; collectionData.current = { ...item, title, inherits: item.inherits && new Proxy(item.inherits, {}) };
const { category } = item;
const compile = useCompile(); const compile = useCompile();
const loadCollections = async (field: any) => { const loadCollections = async (field: any) => {
return targetGraph.collections?.map((collection: any) => ({ return targetGraph.collections?.map((collection: any) => ({
@ -79,8 +80,21 @@ const Entity: React.FC<{
className={cx(entityContainer)} className={cx(entityContainer)}
style={{ boxShadow: attrs?.boxShadow, border: select ? '2px dashed #f5a20a' : 0 }} style={{ boxShadow: attrs?.boxShadow, border: select ? '2px dashed #f5a20a' : 0 }}
> >
<div className={headClass} style={{ background: attrs?.hightLight ? '#1890ff' : null }}> {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> <span className={tableNameClass}>{compile(title)}</span>
<div className={tableBtnClass}> <div className={tableBtnClass}>
<SchemaComponentProvider> <SchemaComponentProvider>
<CollectionNodeProvder setTargetNode={setTargetNode} node={node}> <CollectionNodeProvder setTargetNode={setTargetNode} node={node}>
@ -154,6 +168,7 @@ const Entity: React.FC<{
</SchemaComponentProvider> </SchemaComponentProvider>
</div> </div>
</div> </div>
<PortsCom {...portsProps} /> <PortsCom {...portsProps} />
</div> </div>
); );

View File

@ -128,7 +128,7 @@ export const entityContainer = css`
`; `;
export const headClass = css` export const headClass = css`
height: 40px; height: 50px;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
display: flex; display: flex;