feat: improve kanban
This commit is contained in:
parent
dfd4b821ff
commit
5299a2ece9
@ -145,6 +145,7 @@ export const SchemaField = createSchemaField({
|
||||
scope: {
|
||||
Table,
|
||||
Calendar,
|
||||
Kanban,
|
||||
useAsyncDataSource,
|
||||
ChinaRegion: {
|
||||
loadData: loadChinaRegionData,
|
||||
|
@ -5,7 +5,11 @@ const [CollectionsProvider, useCollectionsContext] = constate(() => {
|
||||
const result = useRequest('collections:findAll', {
|
||||
formatResult: (result) => result?.data,
|
||||
});
|
||||
return { ...result, collections: result.data || [] };
|
||||
return {
|
||||
...result, collections: result.data || [], findCollection(name) {
|
||||
return result?.data?.find((item) => item.name === name);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export { CollectionsProvider, useCollectionsContext };
|
||||
|
@ -1,9 +1,13 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useCollectionContext } from '../constate';
|
||||
import { Resource } from '../resource';
|
||||
|
||||
export const useResource = (options: any = {}) => {
|
||||
const { collection } = useCollectionContext();
|
||||
const resource = Resource.make(collection?.name);
|
||||
useEffect(() => {
|
||||
options.onSuccess && options.onSuccess({});
|
||||
}, []);
|
||||
return {
|
||||
initialValues: {},
|
||||
loading: false,
|
||||
|
@ -410,40 +410,66 @@ function generateCardItemSchema(component) {
|
||||
},
|
||||
],
|
||||
properties: {
|
||||
card1: {
|
||||
create: {
|
||||
type: 'void',
|
||||
'x-component': 'Kanban.Card',
|
||||
title: '添加卡片',
|
||||
'x-designable-bar': 'Kanban.AddCardDesignableBar',
|
||||
'x-component': 'Kanban.Card.AddNew',
|
||||
// 'x-decorator': 'AddNew.Displayed',
|
||||
'x-component-props': {
|
||||
type: 'text',
|
||||
icon: 'PlusOutlined',
|
||||
},
|
||||
properties: {
|
||||
item1: {
|
||||
modal: {
|
||||
type: 'void',
|
||||
'x-component': 'Kanban.Item',
|
||||
title: '新增数据',
|
||||
'x-decorator': 'Form',
|
||||
'x-component': 'Action.Drawer',
|
||||
'x-component-props': {
|
||||
useOkAction: '{{ Kanban.useCreateAction }}',
|
||||
},
|
||||
properties: {
|
||||
title: {
|
||||
type: 'string',
|
||||
// title: '标题',
|
||||
'x-read-pretty': true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-component-props': {
|
||||
addNewComponent: 'AddNew.FormItem',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
card1: {
|
||||
type: 'void',
|
||||
name: uid(),
|
||||
'x-decorator': 'Form',
|
||||
'x-component': 'Kanban.Card',
|
||||
'x-designable-bar': 'Kanban.Card.DesignableBar',
|
||||
'x-read-pretty': true,
|
||||
'x-decorator-props': {
|
||||
useResource: '{{ Kanban.useResource }}',
|
||||
},
|
||||
properties: {},
|
||||
},
|
||||
view1: {
|
||||
type: 'void',
|
||||
title: '修改数据',
|
||||
'x-decorator': 'Form',
|
||||
'x-component': 'Kanban.Card.View',
|
||||
'x-component-props': {
|
||||
useOkAction: '{{ Kanban.useUpdateAction }}',
|
||||
},
|
||||
'x-decorator-props': {
|
||||
useResource: '{{ Kanban.useResource }}',
|
||||
},
|
||||
properties: {
|
||||
item1: {
|
||||
[uid()]: {
|
||||
type: 'void',
|
||||
'x-component': 'Kanban.Item',
|
||||
properties: {
|
||||
title: {
|
||||
type: 'string',
|
||||
title: '标题',
|
||||
'x-read-pretty': true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
'x-component': 'Grid',
|
||||
'x-component-props': {
|
||||
addNewComponent: 'AddNew.FormItem',
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -1225,46 +1251,58 @@ AddNew.CardItem = observer((props: any) => {
|
||||
title={item.title}
|
||||
>
|
||||
<Menu.ItemGroup title={'选择分组字段'}>
|
||||
<Menu.Item
|
||||
style={{ minWidth: 150 }}
|
||||
key={`Kanban.collection.${item.name}.${view.key}`}
|
||||
onClick={async () => {
|
||||
let data = generateCardItemSchema('Kanban');
|
||||
const collectionName = item.name;
|
||||
if (schema['key']) {
|
||||
data['key'] = uid();
|
||||
}
|
||||
if (collectionName) {
|
||||
data['x-component-props'] =
|
||||
data['x-component-props'] || {};
|
||||
data['x-component-props']['resource'] =
|
||||
collectionName;
|
||||
data['x-component-props']['collectionName'] =
|
||||
collectionName;
|
||||
// data['x-component-props']['groupField'] = '';
|
||||
}
|
||||
if (isGridBlock(schema)) {
|
||||
path.pop();
|
||||
path.pop();
|
||||
data = generateGridBlock(data);
|
||||
} else if (isGrid(schema)) {
|
||||
data = generateGridBlock(data);
|
||||
}
|
||||
if (data) {
|
||||
let s;
|
||||
if (isGrid(schema)) {
|
||||
s = appendChild(data, [...path]);
|
||||
} else if (defaultAction === 'insertAfter') {
|
||||
s = insertAfter(data, [...path]);
|
||||
} else {
|
||||
s = insertBefore(data, [...path]);
|
||||
}
|
||||
await createSchema(s);
|
||||
}
|
||||
}}
|
||||
>
|
||||
分组字段1
|
||||
</Menu.Item>
|
||||
{item?.generalFields
|
||||
?.filter((item) => {
|
||||
return item?.uiSchema?.enum;
|
||||
})
|
||||
?.map((field) => {
|
||||
return (
|
||||
<Menu.Item
|
||||
style={{ minWidth: 150 }}
|
||||
key={`Kanban.collection.${field.name}.${view.key}`}
|
||||
onClick={async () => {
|
||||
let data = generateCardItemSchema('Kanban');
|
||||
const collectionName = item.name;
|
||||
if (schema['key']) {
|
||||
data['key'] = uid();
|
||||
}
|
||||
if (collectionName) {
|
||||
data['x-component-props'] =
|
||||
data['x-component-props'] || {};
|
||||
data['x-component-props']['resource'] =
|
||||
collectionName;
|
||||
data['x-component-props'][
|
||||
'collectionName'
|
||||
] = collectionName;
|
||||
data['x-component-props']['groupName'] =
|
||||
field.name;
|
||||
}
|
||||
if (isGridBlock(schema)) {
|
||||
path.pop();
|
||||
path.pop();
|
||||
data = generateGridBlock(data);
|
||||
} else if (isGrid(schema)) {
|
||||
data = generateGridBlock(data);
|
||||
}
|
||||
if (data) {
|
||||
let s;
|
||||
if (isGrid(schema)) {
|
||||
s = appendChild(data, [...path]);
|
||||
} else if (
|
||||
defaultAction === 'insertAfter'
|
||||
) {
|
||||
s = insertAfter(data, [...path]);
|
||||
} else {
|
||||
s = insertBefore(data, [...path]);
|
||||
}
|
||||
await createSchema(s);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{field?.uiSchema?.title}
|
||||
</Menu.Item>
|
||||
);
|
||||
})}
|
||||
</Menu.ItemGroup>
|
||||
</Menu.SubMenu>
|
||||
// <Menu.Item
|
||||
@ -1553,7 +1591,7 @@ AddNew.PaneItem = observer((props: any) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const blockSchema = useContext(BlockSchemaContext);
|
||||
const useResource = `{{ ${blockSchema['x-component']}.useResource }}`;
|
||||
console.log('AddNew.PaneItem.useResource', useResource)
|
||||
console.log('AddNew.PaneItem.useResource', useResource);
|
||||
return (
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
|
@ -12,7 +12,7 @@ import cls from 'classnames';
|
||||
import { Button, Dropdown, Menu, Space } from 'antd';
|
||||
import SwitchMenuItem from '../../components/SwitchMenuItem';
|
||||
import { uid } from '@formily/shared';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { SettingOutlined } from '@ant-design/icons';
|
||||
import { Droppable, SortableItem } from '../../components/Sortable';
|
||||
|
||||
export const ActionBar = observer((props: any) => {
|
||||
@ -257,7 +257,7 @@ function AddActionButton() {
|
||||
className={'designable-btn designable-btn-dash'}
|
||||
style={{ marginLeft: 8 }}
|
||||
type={'dashed'}
|
||||
icon={<PlusOutlined />}
|
||||
icon={<SettingOutlined />}
|
||||
>
|
||||
配置工具栏
|
||||
</Button>
|
||||
|
@ -183,7 +183,7 @@ export const Calendar: any = observer((props: any) => {
|
||||
console.log('field.value', field.value);
|
||||
return (
|
||||
<CollectionProvider collectionName={props.collectionName}>
|
||||
<CalendarContext.Provider value={{ resource, service, props }}>
|
||||
<CalendarContext.Provider value={{ field, resource, service, props }}>
|
||||
<div {...props} style={{ height: 700 }}>
|
||||
<Drawer
|
||||
width={'50%'}
|
||||
|
@ -28,6 +28,7 @@ import { uid } from '@formily/shared';
|
||||
import { getSchemaPath } from '../../components/schema-renderer';
|
||||
import { useCollection, useCollectionContext } from '../../constate';
|
||||
import { useTable } from '../table';
|
||||
import { BlockSchemaContext } from '../../context';
|
||||
|
||||
export const DesignableBar = observer((props) => {
|
||||
const field = useField();
|
||||
@ -35,12 +36,14 @@ export const DesignableBar = observer((props) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { dragRef } = useContext(DraggableBlockContext);
|
||||
const { props: tableProps } = useTable();
|
||||
const { collection } = useCollectionContext();
|
||||
// const collectionName = field.componentProps?.collectionName || tableProps?.collectionName;
|
||||
// const { collection } = useCollection({ collectionName });
|
||||
const blockSchema = useContext(BlockSchemaContext);
|
||||
const collectionName = blockSchema['x-component-props']?.collectionName;
|
||||
const { collection } = useCollection({ collectionName });
|
||||
return (
|
||||
<div className={cls('designable-bar', { active: visible })}>
|
||||
<div className={'designable-info'}>{collection?.title || collection?.name}</div>
|
||||
<div className={'designable-info'}>
|
||||
{collection?.title || collection?.name}
|
||||
</div>
|
||||
<span
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
115
packages/client/src/schemas/kanban/CardDesignableBar.tsx
Normal file
115
packages/client/src/schemas/kanban/CardDesignableBar.tsx
Normal file
@ -0,0 +1,115 @@
|
||||
import React, { useContext, useMemo, useRef, useState } from 'react';
|
||||
import { createForm } from '@formily/core';
|
||||
import {
|
||||
SchemaOptionsContext,
|
||||
Schema,
|
||||
useFieldSchema,
|
||||
observer,
|
||||
SchemaExpressionScopeContext,
|
||||
FormProvider,
|
||||
useField,
|
||||
useForm,
|
||||
RecursionField,
|
||||
} from '@formily/react';
|
||||
import {
|
||||
useSchemaPath,
|
||||
SchemaField,
|
||||
useDesignable,
|
||||
removeSchema,
|
||||
ISchema,
|
||||
createSchema,
|
||||
} from '../';
|
||||
import get from 'lodash/get';
|
||||
import { Button, Dropdown, Menu, Space } from 'antd';
|
||||
import { MenuOutlined, DragOutlined } from '@ant-design/icons';
|
||||
import cls from 'classnames';
|
||||
import { FormLayout } from '@formily/antd';
|
||||
import './style.less';
|
||||
import AddNew from '../add-new';
|
||||
import { DraggableBlockContext } from '../../components/drag-and-drop';
|
||||
import { isGridRowOrCol } from '../grid';
|
||||
import constate from 'constate';
|
||||
import { useEffect } from 'react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { getSchemaPath } from '../../components/schema-renderer';
|
||||
import {
|
||||
useCollection,
|
||||
useCollectionContext,
|
||||
useDisplayedMapContext,
|
||||
} from '../../constate';
|
||||
import { useTable } from '../table';
|
||||
import SwitchMenuItem from '../../components/SwitchMenuItem';
|
||||
|
||||
export const CardDesignableBar = observer((props) => {
|
||||
const field = useField();
|
||||
const { designable, schema, appendChild, deepRemove } = useDesignable();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { dragRef } = useContext(DraggableBlockContext);
|
||||
const { collection, fields } = useCollectionContext();
|
||||
const displayed = useDisplayedMapContext();
|
||||
console.log('useDisplayedMapContext', schema);
|
||||
return (
|
||||
<div className={cls('designable-bar', { active: visible })}>
|
||||
<span
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className={cls('designable-bar-actions', { active: visible })}
|
||||
>
|
||||
<Space size={2}>
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
visible={visible}
|
||||
onVisibleChange={(visible) => {
|
||||
setVisible(visible);
|
||||
}}
|
||||
overlay={
|
||||
<Menu>
|
||||
<Menu.ItemGroup className={'display-fields'} title={`字段展示`}>
|
||||
{fields?.map((field) => (
|
||||
<SwitchMenuItem
|
||||
key={field.key}
|
||||
title={field?.uiSchema?.title}
|
||||
checked={displayed.has(field.name)}
|
||||
onChange={async (checked) => {
|
||||
if (!checked) {
|
||||
const s: any = displayed.get(field.name);
|
||||
const p = getSchemaPath(s);
|
||||
const removed = deepRemove(p);
|
||||
if (!removed) {
|
||||
console.log('getSchemaPath', p, removed);
|
||||
return;
|
||||
}
|
||||
const last = removed.pop();
|
||||
displayed.remove(field.name);
|
||||
if (isGridRowOrCol(last)) {
|
||||
await removeSchema(last);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let data: ISchema = {
|
||||
key: uid(),
|
||||
type: 'void',
|
||||
'x-decorator': 'Form.Field.Item',
|
||||
'x-designable-bar': 'Form.Field.DesignableBar',
|
||||
'x-component': 'Form.Field',
|
||||
'x-component-props': {
|
||||
fieldName: field.name,
|
||||
},
|
||||
};
|
||||
const s = appendChild(data);
|
||||
await createSchema(s);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Menu.ItemGroup>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<MenuOutlined />
|
||||
</Dropdown>
|
||||
</Space>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
});
|
201
packages/client/src/schemas/kanban/Field.DesignableBar.tsx
Normal file
201
packages/client/src/schemas/kanban/Field.DesignableBar.tsx
Normal file
@ -0,0 +1,201 @@
|
||||
import React, { useContext, useMemo, useRef, useState } from 'react';
|
||||
import { createForm } from '@formily/core';
|
||||
import {
|
||||
SchemaOptionsContext,
|
||||
Schema,
|
||||
useFieldSchema,
|
||||
observer,
|
||||
SchemaExpressionScopeContext,
|
||||
FormProvider,
|
||||
ISchema,
|
||||
useField,
|
||||
useForm,
|
||||
RecursionField,
|
||||
} from '@formily/react';
|
||||
import {
|
||||
useSchemaPath,
|
||||
SchemaField,
|
||||
useDesignable,
|
||||
removeSchema,
|
||||
updateSchema,
|
||||
} from '../';
|
||||
import get from 'lodash/get';
|
||||
import { Button, Dropdown, Menu, Space, Switch } from 'antd';
|
||||
import { MenuOutlined, DragOutlined } from '@ant-design/icons';
|
||||
import cls from 'classnames';
|
||||
import { FormDialog, FormLayout } from '@formily/antd';
|
||||
import './style.less';
|
||||
import AddNew from '../add-new';
|
||||
import { DraggableBlockContext } from '../../components/drag-and-drop';
|
||||
import { isGridRowOrCol } from '../grid';
|
||||
import constate from 'constate';
|
||||
import { useEffect } from 'react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { getSchemaPath } from '../../components/schema-renderer';
|
||||
import { RandomNameContext } from '.';
|
||||
import { useCollectionContext, useDisplayedMapContext } from '../../constate';
|
||||
import SwitchMenuItem from '../../components/SwitchMenuItem';
|
||||
|
||||
export const FieldDesignableBar = observer((props) => {
|
||||
const field = useField();
|
||||
const { schema, deepRemove } = useDesignable();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { dragRef } = useContext(DraggableBlockContext);
|
||||
const randomName = useContext(RandomNameContext);
|
||||
const displayed = useDisplayedMapContext();
|
||||
const fieldName = schema['x-component-props']?.['fieldName'];
|
||||
const { getField } = useCollectionContext();
|
||||
|
||||
const collectionField = getField(fieldName);
|
||||
|
||||
console.log({ collectionField });
|
||||
|
||||
const realField = field
|
||||
.query(field.address.concat(randomName, fieldName))
|
||||
.take();
|
||||
|
||||
return (
|
||||
<div className={cls('designable-bar', { active: visible })}>
|
||||
<span
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className={cls('designable-bar-actions', { active: visible })}
|
||||
>
|
||||
<Space size={2}>
|
||||
<AddNew.FormItem defaultAction={'insertAfter'} ghost />
|
||||
{dragRef && <DragOutlined ref={dragRef} />}
|
||||
<Dropdown
|
||||
placement={'bottomRight'}
|
||||
trigger={['click']}
|
||||
visible={visible}
|
||||
onVisibleChange={(visible) => {
|
||||
setVisible(visible);
|
||||
}}
|
||||
overlay={
|
||||
<Menu>
|
||||
<Menu.Item
|
||||
key={'update'}
|
||||
onClick={async () => {
|
||||
const values = await FormDialog('修改字段名称', () => {
|
||||
return (
|
||||
<FormLayout layout={'vertical'}>
|
||||
<SchemaField
|
||||
schema={{
|
||||
type: 'object',
|
||||
properties: {
|
||||
fieldName: {
|
||||
type: 'string',
|
||||
title: '原字段名称',
|
||||
'x-read-pretty': true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
title: '自定义名称',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</FormLayout>
|
||||
);
|
||||
}).open({
|
||||
initialValues: {
|
||||
fieldName: collectionField?.uiSchema?.title,
|
||||
title: schema['title'],
|
||||
},
|
||||
});
|
||||
const title = values.title || null;
|
||||
field
|
||||
.query(field.address.concat(randomName, fieldName))
|
||||
.take((f) => {
|
||||
f.title = title || collectionField?.uiSchema?.title;
|
||||
});
|
||||
schema['title'] = title;
|
||||
await updateSchema({
|
||||
key: schema['key'],
|
||||
title,
|
||||
});
|
||||
}}
|
||||
>
|
||||
修改字段名称
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
style={{ minWidth: 150 }}
|
||||
onClick={async () => {
|
||||
const values = await FormDialog('编辑描述', () => {
|
||||
return (
|
||||
<FormLayout layout={'vertical'}>
|
||||
<SchemaField
|
||||
schema={{
|
||||
type: 'object',
|
||||
properties: {
|
||||
description: {
|
||||
type: 'string',
|
||||
'x-component': 'Input.TextArea',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</FormLayout>
|
||||
);
|
||||
}).open({
|
||||
initialValues: {
|
||||
description: schema['description'],
|
||||
},
|
||||
});
|
||||
const description = values.description || null;
|
||||
realField.description =
|
||||
description || collectionField?.uiSchema?.description;
|
||||
schema['description'] = description;
|
||||
await updateSchema({
|
||||
key: schema['key'],
|
||||
description,
|
||||
});
|
||||
}}
|
||||
>
|
||||
编辑描述
|
||||
</Menu.Item>
|
||||
<SwitchMenuItem
|
||||
title={'必填'}
|
||||
checked={schema.required as boolean}
|
||||
onChange={(checked) => {
|
||||
field
|
||||
.query(field.address.concat(randomName, fieldName))
|
||||
.take((f: any) => {
|
||||
f.required = checked;
|
||||
schema.required = checked;
|
||||
updateSchema(schema);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
key={'delete'}
|
||||
onClick={async () => {
|
||||
const removed = deepRemove();
|
||||
const fieldName =
|
||||
schema['x-component-props']?.['fieldName'];
|
||||
console.log({ schema, removed, fieldName });
|
||||
const last = removed.pop();
|
||||
displayed.remove(fieldName);
|
||||
if (isGridRowOrCol(last)) {
|
||||
await removeSchema(last);
|
||||
}
|
||||
}}
|
||||
>
|
||||
移除
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<MenuOutlined />
|
||||
</Dropdown>
|
||||
</Space>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
});
|
@ -1,5 +1,11 @@
|
||||
import React from 'react';
|
||||
import { observer, RecursionField, Schema, useField } from '@formily/react';
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
observer,
|
||||
RecursionField,
|
||||
Schema,
|
||||
useField,
|
||||
useForm,
|
||||
} from '@formily/react';
|
||||
import { DndContext, DragOverlay, closestCorners } from '@dnd-kit/core';
|
||||
import {
|
||||
SortableContext,
|
||||
@ -13,7 +19,7 @@ import './style.less';
|
||||
import { useState } from 'react';
|
||||
import { groupBy } from 'lodash';
|
||||
import cls from 'classnames';
|
||||
import { Button, Card, Drawer } from 'antd';
|
||||
import { Button, Card, Drawer, Tag } from 'antd';
|
||||
import {
|
||||
SchemaRenderer,
|
||||
useDesignable,
|
||||
@ -23,6 +29,13 @@ import { useContext } from 'react';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { uid } from '@formily/shared';
|
||||
import { DesignableBar } from './DesignableBar';
|
||||
import { CollectionProvider, useCollectionContext } from '../../constate';
|
||||
import { Resource } from '../../resource';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { Action } from '../action';
|
||||
import { Form } from '../form';
|
||||
import { CardDesignableBar } from './CardDesignableBar';
|
||||
import { VisibleContext } from '../../context';
|
||||
|
||||
export function SortableItem(props) {
|
||||
const {
|
||||
@ -59,16 +72,22 @@ export function SortableItem(props) {
|
||||
|
||||
const useColumns = () => {
|
||||
const field = useField<Formily.Core.Models.ArrayField>();
|
||||
const { getField } = useCollectionContext();
|
||||
let groupField = field.componentProps.groupField;
|
||||
const groupName = field.componentProps.groupName;
|
||||
const collectionField = getField(groupName);
|
||||
if (collectionField) {
|
||||
groupField = collectionField?.uiSchema;
|
||||
}
|
||||
const values = field.value;
|
||||
const [groups, setGroups] = useState(['A', 'B', 'C']);
|
||||
const columns = groups.map((name) => {
|
||||
const columns = groupField?.enum?.map((group) => {
|
||||
return {
|
||||
id: name,
|
||||
title: name,
|
||||
items: values?.filter((item) => item.type === name) || [],
|
||||
...group,
|
||||
items: values?.filter((item) => item[groupName] === group.value) || [],
|
||||
};
|
||||
});
|
||||
return { values, groups, columns, setGroups };
|
||||
console.log('useColumns', { values, columns });
|
||||
return { values, columns };
|
||||
};
|
||||
|
||||
interface KanbanCardContextProps {
|
||||
@ -79,10 +98,19 @@ interface KanbanCardContextProps {
|
||||
|
||||
const KanbanCardContext = createContext<KanbanCardContextProps>(null);
|
||||
|
||||
export const Kanban: any = observer(() => {
|
||||
export const KanbanContext = createContext<any>(null);
|
||||
export const KanbanColumnContext = createContext<any>(null);
|
||||
|
||||
export const useKanban = () => {
|
||||
return useContext(KanbanContext);
|
||||
};
|
||||
|
||||
const InternalKanban = observer((props: any) => {
|
||||
const field = useField<Formily.Core.Models.ArrayField>();
|
||||
const groupName = field.componentProps.groupName;
|
||||
const { values, columns } = useColumns();
|
||||
const { schema } = useDesignable();
|
||||
const { collectionName } = props;
|
||||
const cardSchema = schema.reduceProperties((prev, current) => {
|
||||
if (current['x-component'] === 'Kanban.Card') {
|
||||
return current;
|
||||
@ -95,148 +123,270 @@ export const Kanban: any = observer(() => {
|
||||
}
|
||||
return prev;
|
||||
}, null);
|
||||
const addCardSchema = schema.reduceProperties((prev, current) => {
|
||||
if (current['x-component'] === 'Kanban.Card.AddNew') {
|
||||
return current;
|
||||
}
|
||||
return prev;
|
||||
}, null);
|
||||
const resource = Resource.make(collectionName);
|
||||
const service = useRequest(
|
||||
(params) => {
|
||||
if (!collectionName) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
return resource.list({
|
||||
...params,
|
||||
perPage: -1,
|
||||
});
|
||||
},
|
||||
{
|
||||
formatResult: (data) => data?.data,
|
||||
onSuccess(data) {
|
||||
field.setValue(data);
|
||||
},
|
||||
manual: true,
|
||||
// refreshDeps: [props.fieldNames],
|
||||
},
|
||||
);
|
||||
useEffect(() => {
|
||||
service.run({
|
||||
defaultFilter: props?.defaultFilter,
|
||||
sort: 'sort',
|
||||
});
|
||||
}, [props.defaultFilter]);
|
||||
return (
|
||||
<div>
|
||||
{/* <div>
|
||||
<KanbanContext.Provider value={{ field, resource, service, props }}>
|
||||
<div>
|
||||
{/* <div>
|
||||
<Button>筛选</Button>
|
||||
</div> */}
|
||||
<div className={'kanban'}>
|
||||
<DndContext
|
||||
// collisionDetection={closestCorners}
|
||||
onDragEnd={({ active, over }) => {
|
||||
const source = values.find((item) => item.id === active?.id);
|
||||
const target = values.find((item) => item.id === over?.id);
|
||||
if (source && target) {
|
||||
const fromIndex = values.findIndex(
|
||||
(item) => item.id === active?.id,
|
||||
);
|
||||
const toIndex = values.findIndex((item) => item.id === over?.id);
|
||||
// setValues((items) => {
|
||||
// const fromIndex = items.findIndex(
|
||||
// (item) => item.id === active?.id,
|
||||
// );
|
||||
// const toIndex = items.findIndex(
|
||||
// (item) => item.id === over?.id,
|
||||
// );
|
||||
// return arrayMove(items, fromIndex, toIndex);
|
||||
// });
|
||||
field.move(fromIndex, toIndex);
|
||||
}
|
||||
}}
|
||||
onDragOver={({ active, over }) => {
|
||||
if (!over) {
|
||||
return;
|
||||
}
|
||||
if (active?.id === over?.id) {
|
||||
return;
|
||||
}
|
||||
const source = values.find((item) => item.id === active?.id);
|
||||
if (source && over?.data?.current?.type === 'column') {
|
||||
source.type = over.id;
|
||||
return;
|
||||
}
|
||||
console.log('active.id', active.id, over?.data?.current);
|
||||
const target = values.find((item) => item.id === over?.id);
|
||||
if (source && target) {
|
||||
if (source.type !== target.type) {
|
||||
source.type = target.type;
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DragOverlay style={{ pointerEvents: 'none' }}>aaa</DragOverlay>
|
||||
<div className={'kanban'}>
|
||||
<div className={'kanban-container'}>
|
||||
<DndContext
|
||||
// collisionDetection={closestCorners}
|
||||
onDragEnd={({ active, over }) => {
|
||||
const source = values.find((item) => item.id === active?.id);
|
||||
const target = values.find((item) => item.id === over?.id);
|
||||
if (source && target) {
|
||||
const fromIndex = values.findIndex(
|
||||
(item) => item.id === active?.id,
|
||||
);
|
||||
const toIndex = values.findIndex(
|
||||
(item) => item.id === over?.id,
|
||||
);
|
||||
// setValues((items) => {
|
||||
// const fromIndex = items.findIndex(
|
||||
// (item) => item.id === active?.id,
|
||||
// );
|
||||
// const toIndex = items.findIndex(
|
||||
// (item) => item.id === over?.id,
|
||||
// );
|
||||
// return arrayMove(items, fromIndex, toIndex);
|
||||
// });
|
||||
field.move(fromIndex, toIndex);
|
||||
}
|
||||
}}
|
||||
onDragOver={({ active, over }) => {
|
||||
if (!over) {
|
||||
return;
|
||||
}
|
||||
if (active?.id === over?.id) {
|
||||
return;
|
||||
}
|
||||
const source = values.find((item) => item.id === active?.id);
|
||||
if (source && over?.data?.current?.type === 'column') {
|
||||
source[groupName] = over.id;
|
||||
return;
|
||||
}
|
||||
console.log('active.id', active.id, over?.data?.current);
|
||||
const target = values.find((item) => item.id === over?.id);
|
||||
if (source && target) {
|
||||
if (source[groupName] !== target[groupName]) {
|
||||
source[groupName] = target[groupName];
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DragOverlay style={{ pointerEvents: 'none' }}>aaa</DragOverlay>
|
||||
|
||||
<SortableContext
|
||||
strategy={horizontalListSortingStrategy}
|
||||
items={columns}
|
||||
>
|
||||
{columns.map((column) => (
|
||||
<SortableItem
|
||||
key={column.id}
|
||||
type={'column'}
|
||||
className={'column'}
|
||||
id={column.id}
|
||||
<SortableContext
|
||||
strategy={horizontalListSortingStrategy}
|
||||
items={
|
||||
columns?.map((column) => ({
|
||||
...column,
|
||||
id: column.value,
|
||||
})) || []
|
||||
}
|
||||
>
|
||||
<div className={'column-title'}>{column.title}</div>
|
||||
<SortableContext
|
||||
strategy={verticalListSortingStrategy}
|
||||
items={column.items}
|
||||
>
|
||||
{column.items.map((item) => {
|
||||
const index = values.findIndex(
|
||||
(value) => value.id === item.id,
|
||||
);
|
||||
return (
|
||||
<SortableItem
|
||||
key={item.id}
|
||||
className={'item'}
|
||||
id={item.id}
|
||||
type={'item'}
|
||||
{columns?.map((column) => (
|
||||
<SortableItem
|
||||
id={column.value}
|
||||
key={column.value}
|
||||
type={'column'}
|
||||
className={'column'}
|
||||
>
|
||||
<KanbanColumnContext.Provider value={column}>
|
||||
<div className={'column-title'}>
|
||||
<Tag color={column.color}>{column.label}</Tag>
|
||||
</div>
|
||||
<SortableContext
|
||||
strategy={verticalListSortingStrategy}
|
||||
items={column.items}
|
||||
>
|
||||
<KanbanCardContext.Provider
|
||||
value={{
|
||||
index: index,
|
||||
record: item,
|
||||
viewSchema: cardViewSchema,
|
||||
}}
|
||||
>
|
||||
<RecursionField name={index} schema={cardSchema} />
|
||||
</KanbanCardContext.Provider>
|
||||
</SortableItem>
|
||||
);
|
||||
})}
|
||||
</SortableContext>
|
||||
<Button
|
||||
type={'text'}
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
field.push({
|
||||
id: uid(),
|
||||
type: column.id,
|
||||
title: uid(),
|
||||
});
|
||||
}}
|
||||
{column.items.map((item) => {
|
||||
const index = values.findIndex(
|
||||
(value) => value.id === item.id,
|
||||
);
|
||||
return (
|
||||
<SortableItem
|
||||
key={item.id}
|
||||
className={'item'}
|
||||
id={item.id}
|
||||
type={'item'}
|
||||
>
|
||||
<KanbanCardContext.Provider
|
||||
value={{
|
||||
index: index,
|
||||
record: item,
|
||||
viewSchema: cardViewSchema,
|
||||
}}
|
||||
>
|
||||
<RecursionField
|
||||
name={index}
|
||||
schema={cardSchema}
|
||||
/>
|
||||
</KanbanCardContext.Provider>
|
||||
</SortableItem>
|
||||
);
|
||||
})}
|
||||
</SortableContext>
|
||||
<RecursionField
|
||||
name={addCardSchema?.name}
|
||||
schema={addCardSchema}
|
||||
/>
|
||||
{/* <Button
|
||||
type={'text'}
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
field.push({
|
||||
id: uid(),
|
||||
title: uid(),
|
||||
[groupName]: column.value,
|
||||
});
|
||||
}}
|
||||
>
|
||||
添加卡片
|
||||
</Button> */}
|
||||
</KanbanColumnContext.Provider>
|
||||
</SortableItem>
|
||||
))}
|
||||
<div
|
||||
id={'addColumn'}
|
||||
key={'addColumn'}
|
||||
// type={'column'}
|
||||
className={'column add-column'}
|
||||
>
|
||||
添加卡片
|
||||
</Button>
|
||||
</SortableItem>
|
||||
))}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
{/* <span className={'add-column-plus'}/> */}
|
||||
<span><PlusOutlined/> 添加列表</span>
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</KanbanContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
export const Kanban: any = observer((props: any) => {
|
||||
return (
|
||||
<CollectionProvider collectionName={props.collectionName}>
|
||||
<InternalKanban {...props} />
|
||||
</CollectionProvider>
|
||||
);
|
||||
});
|
||||
|
||||
Kanban.useCreateAction = () => {
|
||||
const { service, resource, props } = useKanban();
|
||||
const column = useContext(KanbanColumnContext);
|
||||
const groupName = props.groupName;
|
||||
const form = useForm();
|
||||
return {
|
||||
async run() {
|
||||
await resource.create({
|
||||
...form.values,
|
||||
[groupName]: column.value,
|
||||
});
|
||||
await form.reset();
|
||||
return service.refresh();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
Kanban.useUpdateAction = () => {
|
||||
const { service, resource, props } = useKanban();
|
||||
const ctx = useContext(KanbanCardContext);
|
||||
const form = useForm();
|
||||
return {
|
||||
async run() {
|
||||
await resource.save(form.values, {
|
||||
resourceKey: ctx.record.id,
|
||||
});
|
||||
await service.refresh();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
Kanban.useResource = ({ onSuccess }) => {
|
||||
const { props } = useKanban();
|
||||
const { collection } = useCollectionContext();
|
||||
const ctx = useContext(KanbanCardContext);
|
||||
const resource = Resource.make({
|
||||
resourceName: collection?.name || props.collectionName,
|
||||
resourceKey: ctx?.record?.id,
|
||||
});
|
||||
console.log(
|
||||
'collection?.name || props.collectionName',
|
||||
collection?.name || props.collectionName,
|
||||
);
|
||||
const service = useRequest(
|
||||
(params?: any) => {
|
||||
return Promise.resolve(ctx.record);
|
||||
},
|
||||
{
|
||||
// formatResult: (result) => result?.data,
|
||||
onSuccess,
|
||||
refreshDeps: [ctx?.record],
|
||||
// manual,
|
||||
},
|
||||
);
|
||||
return { resource, service, initialValues: service.data, ...service };
|
||||
};
|
||||
|
||||
Kanban.Card = observer((props) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { index, viewSchema } = useContext(KanbanCardContext);
|
||||
const { DesignableBar } = useDesignable();
|
||||
const { children, ...others } = props;
|
||||
return (
|
||||
<>
|
||||
<VisibleContext.Provider value={[visible, setVisible]}>
|
||||
<Card
|
||||
onClick={(e) => {
|
||||
setVisible(true);
|
||||
}}
|
||||
bordered={false}
|
||||
{...others}
|
||||
>
|
||||
{props.children}
|
||||
{children}
|
||||
<DesignableBar />
|
||||
</Card>
|
||||
<Drawer
|
||||
width={'50%'}
|
||||
title={'查看数据'}
|
||||
visible={visible}
|
||||
onClose={() => {
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
<RecursionField name={index} schema={viewSchema} />
|
||||
</Drawer>
|
||||
</>
|
||||
<RecursionField name={index} schema={viewSchema} />
|
||||
</VisibleContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
Kanban.Card.View = observer((props) => {
|
||||
return <div>{props.children}</div>;
|
||||
});
|
||||
|
||||
Kanban.Card.View = Action.Drawer;
|
||||
Kanban.Card.AddNew = Action;
|
||||
Kanban.DesignableBar = DesignableBar;
|
||||
Kanban.Card.DesignableBar = CardDesignableBar;
|
||||
|
@ -1,9 +1,38 @@
|
||||
.column {
|
||||
position: relative;
|
||||
width: 25vw;
|
||||
width: 300px;
|
||||
background: #f9f9f9;
|
||||
margin: 0 12px;
|
||||
margin-right: 24px;
|
||||
padding: 24px;
|
||||
&.add-column {
|
||||
padding: 0;
|
||||
margin-right: 0;
|
||||
background: rgba(241, 139, 98, 0.05);
|
||||
cursor: pointer;
|
||||
color: #F18B62;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
.add-column-plus {
|
||||
position: relative;
|
||||
&::before {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 50px;
|
||||
background: #F18B62;
|
||||
}
|
||||
&::after {
|
||||
position: absolute;
|
||||
width: 50px;
|
||||
height: 1px;
|
||||
background: #F18B62;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
background: rgba(241, 139, 98, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.column-title {
|
||||
@ -14,7 +43,12 @@
|
||||
|
||||
.kanban {
|
||||
display: flex;
|
||||
margin: 0 -12px;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.kanban-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.item {
|
||||
@ -31,4 +65,42 @@
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.kanban .ant-card-body {
|
||||
position: relative;
|
||||
&:hover {
|
||||
> .designable-bar {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
> .designable-bar {
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
right: -1px;
|
||||
left: -1px;
|
||||
bottom: -1px;
|
||||
border-radius: 2px;
|
||||
background: none !important;
|
||||
border: 2px solid rgba(241, 139, 98, 0.3) !important;
|
||||
&.active {
|
||||
display: block;
|
||||
}
|
||||
.designable-bar-actions {
|
||||
pointer-events: all;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
line-height: 1rem;
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
z-index: 10;
|
||||
padding: 0 3px;
|
||||
.anticon {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user