feat: kanban (#230)

* feat: kanban

* feat(client): improve kanban

* fix: unknown group
This commit is contained in:
chenos 2022-03-10 23:52:44 +08:00 committed by GitHub
parent ffe3463dc2
commit 3d5817e0b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 542 additions and 126 deletions

View File

@ -12,7 +12,8 @@ function Card({ children, index, renderCard, disableCardDrag }) {
{...provided.dragHandleProps} {...provided.dragHandleProps}
data-testid={`card-${children.id}`} data-testid={`card-${children.id}`}
> >
<div style={{ display: 'inline-block', whiteSpace: 'normal' }}>{renderCard(isDragging)}</div> {renderCard(isDragging)}
{/* <div style={{ display: 'inline-block', whiteSpace: 'normal' }}></div> */}
</div> </div>
); );
}} }}

View File

@ -46,7 +46,8 @@ function Column({
{cardAdderPosition === 'top' && allowAddCard && renderCardAdder({ column: children, onConfirm: onCardNew })} {cardAdderPosition === 'top' && allowAddCard && renderCardAdder({ column: children, onConfirm: onCardNew })}
<DroppableColumn droppableId={String(children.id)}> <DroppableColumn droppableId={String(children.id)}>
{children?.cards?.length ? ( {children?.cards?.length ? (
children.cards.map((card, index) => ( <div className="react-kanban-card-skeleton">
{children.cards.map((card, index) => (
<Card <Card
key={card.id} key={card.id}
index={index} index={index}
@ -55,7 +56,8 @@ function Column({
> >
{card} {card}
</Card> </Card>
)) ))}
</div>
) : ( ) : (
<div className="react-kanban-card-skeleton" /> <div className="react-kanban-card-skeleton" />
)} )}

View File

@ -1,5 +1,6 @@
.react-kanban-board { .react-kanban-board {
padding: 5px; // padding: 5px;
margin-bottom: 24px;
} }
.react-kanban-card { .react-kanban-card {
@ -14,8 +15,19 @@
.react-kanban-card-skeleton { .react-kanban-card-skeleton {
box-sizing: border-box; box-sizing: border-box;
max-width: 250px; max-width: 300px;
min-width: 250px; min-width: 300px;
height: 70vh;
overflow-x: hidden;
overflow-y: auto;
padding: 0 12px;
margin-bottom: 12px;
> div {
margin-bottom: 12px;
}
+ div {
display: none !important;
}
} }
.react-kanban-card--dragging { .react-kanban-card--dragging {
@ -35,15 +47,16 @@
} }
.react-kanban-column { .react-kanban-column {
// padding: 15px 0;
// border-radius: 2px;
background-color: #f9f9f9;
margin-right: 15px;
padding-bottom: 15px;
width: 300px;
}
.react-kanban-column-header {
padding: 15px; padding: 15px;
border-radius: 2px;
background-color: #eee;
margin: 5px;
input {
&:focus {
outline: none;
}
}
} }
.react-kanban-card-adder-form { .react-kanban-card-adder-form {
@ -114,14 +127,7 @@
} }
.react-kanban-column-header { .react-kanban-column-header {
padding-bottom: 10px;
font-weight: bold;
input {
&:focus {
outline: none;
}
}
} }
.react-kanban-column-header__button { .react-kanban-column-header__button {

View File

@ -28,6 +28,30 @@ export const useCancelFilterAction = () => {
}; };
}; };
export const useKanbanEvents = () => {
const { resource } = useCollection();
return {
async onCardDragEnd({ columns, groupField }, { fromColumnId, fromPosition }, { toColumnId, toPosition }) {
const sourceColumn = columns.find((column) => column.id === fromColumnId);
const destinationColumn = columns.find((column) => column.id === toColumnId);
const sourceCard = sourceColumn?.cards?.[fromPosition];
const targetCard = destinationColumn?.cards?.[toPosition];
const values = {
sourceId: sourceCard.id,
sortField: `${groupField.name}_sort`,
};
if (targetCard) {
values['targetId'] = targetCard.id;
} else {
values['targetScope'] = {
[groupField.name]: toColumnId,
};
}
await resource.move(values);
},
};
};
export const useSortFields = (collectionName: string) => { export const useSortFields = (collectionName: string) => {
const { getCollectionFields, getInterface } = useCollectionManager(); const { getCollectionFields, getInterface } = useCollectionManager();
const fields = getCollectionFields(collectionName); const fields = getCollectionFields(collectionName);

View File

@ -6,7 +6,7 @@ import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useCompile, useDesignable } from '../../../schema-component'; import { useCompile, useDesignable } from '../../../schema-component';
import { SchemaInitializer } from '../../../schema-initializer'; import { SchemaInitializer } from '../../../schema-initializer';
import { useCardItemInitializerFields } from './hoooks'; import { useFormItemInitializerFields } from '../../../schema-initializer/Initializers/utils';
const titleCss = css` const titleCss = css`
pointer-events: none; pointer-events: none;
@ -23,6 +23,15 @@ const titleCss = css`
left: 2px; left: 2px;
`; `;
export const removeGridFormItem = (schema, cb) => {
cb(schema, {
removeParentsIfNoChildren: true,
breakRemoveOn: {
'x-component': 'Kanban.Card',
},
});
};
export const KanbanCardDesigner = (props: any) => { export const KanbanCardDesigner = (props: any) => {
const { dn, designable } = useDesignable(); const { dn, designable } = useDesignable();
const { t } = useTranslation(); const { t } = useTranslation();
@ -34,6 +43,7 @@ export const KanbanCardDesigner = (props: any) => {
field, field,
fieldSchema, fieldSchema,
}; };
const fields = useFormItemInitializerFields();
if (!designable) { if (!designable) {
return null; return null;
} }
@ -42,11 +52,21 @@ export const KanbanCardDesigner = (props: any) => {
<div className={'general-schema-designer-icons'}> <div className={'general-schema-designer-icons'}>
<Space size={2} align={'center'}> <Space size={2} align={'center'}>
<SchemaInitializer.Button <SchemaInitializer.Button
wrap={(s) => {
s['type'] = 'string';
s['x-read-pretty'] = true;
return s;
}}
items={[ items={[
{ {
type: 'itemGroup', type: 'itemGroup',
title: t('Display fields'), title: t('Display fields'),
children: useCardItemInitializerFields(), children: fields.map((field) => {
return {
...field,
remove: removeGridFormItem,
};
}),
}, },
]} ]}
component={<MenuOutlined style={{ cursor: 'pointer', fontSize: 12 }} />} component={<MenuOutlined style={{ cursor: 'pointer', fontSize: 12 }} />}

View File

@ -1,27 +1,72 @@
import { createForm } from '@formily/core'; import { css } from '@emotion/css';
import { FieldContext, FormContext, observer } from '@formily/react'; import { observer, RecursionField, useFieldSchema } from '@formily/react';
import { Card } from 'antd'; import { Card } from 'antd';
import React, { useContext, useMemo } from 'react'; import React, { useContext, useState } from 'react';
import { BlockItem } from '../block-item'; import { ActionContext, BlockItem } from '..';
import { CardContext } from './context'; import { DndContext } from '../..';
import { RecordProvider } from '../../../record-provider';
import { SchemaComponentOptions } from '../../core/SchemaComponentOptions';
import { KanbanCardContext } from './context';
const FormItem = observer((props) => {
return <BlockItem {...props} />;
});
export const KanbanCard: any = observer((props: any) => { export const KanbanCard: any = observer((props: any) => {
const { allowRemoveCard, onCardRemove, children } = props; const { setDisableCardDrag, cardViewerSchema, card, cardField, columnIndex, cardIndex } =
const { card, dragging } = useContext(CardContext); useContext(KanbanCardContext);
const form = useMemo( const fieldSchema = useFieldSchema();
() => const [visible, setVisible] = useState(false);
createForm({
values: { card: { ...card } },
}),
[card],
);
return ( return (
<BlockItem className={'noco-card-item'}> <>
<FieldContext.Provider value={undefined}> <Card
<FormContext.Provider value={form}> onClick={(e) => {
<Card style={{ width: 220, marginBottom: 15, cursor: 'pointer' }}>{children}</Card> setVisible(true);
</FormContext.Provider> }}
</FieldContext.Provider> className={css`
</BlockItem> /* .ant-description-input {
line-height: 1.15;
} */
.ant-formily-item-label {
display: none;
}
.ant-formily-item-feedback-layout-loose {
margin-bottom: 12px;
}
.nb-block-item:last-child {
.ant-formily-item {
margin-bottom: 0;
}
}
`}
bordered={false}
hoverable
style={{ cursor: 'pointer', overflow: 'hidden' }}
>
<SchemaComponentOptions components={{}}>
<DndContext
onDragStart={() => {
setDisableCardDrag(true);
}}
onDragEnd={() => {
setDisableCardDrag(false);
}}
>
<RecursionField
basePath={cardField.address.concat(`${columnIndex}.cards.${cardIndex}`)}
schema={fieldSchema}
onlyRenderProperties
/>
</DndContext>
</SchemaComponentOptions>
</Card>
{cardViewerSchema && (
<ActionContext.Provider value={{ openMode: 'drawer', visible, setVisible }}>
<RecordProvider record={card}>
<RecursionField name={cardViewerSchema.name} schema={cardViewerSchema} onlyRenderProperties />
</RecordProvider>
</ActionContext.Provider>
)}
</>
); );
}); });

View File

@ -1,11 +1,54 @@
import { ISchema, useField, useFieldSchema } from '@formily/react';
import React from 'react'; import React from 'react';
import { useCollection } from '../../../collection-manager'; import { useTranslation } from 'react-i18next';
import { useCollection, useResourceActionContext } from '../../../collection-manager';
import { useCollectionFilterOptions } from '../../../collection-manager/action-hooks';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useDesignable } from '../../hooks';
export const KanbanDesigner = () => { export const KanbanDesigner = () => {
const { name, title } = useCollection(); const { name, title } = useCollection();
const field = useField();
const fieldSchema = useFieldSchema();
const dataSource = useCollectionFilterOptions(name);
const ctx = useResourceActionContext();
const { t } = useTranslation();
const { dn } = useDesignable();
const defaultFilter = fieldSchema?.['x-decorator-props']?.request?.params?.filter || {};
return ( return (
<GeneralSchemaDesigner title={title || name}> <GeneralSchemaDesigner title={title || name}>
<SchemaSettings.ModalItem
title={'设置数据范围'}
schema={
{
type: 'object',
title: '设置数据范围',
properties: {
filter: {
default: defaultFilter,
title: '数据范围',
enum: dataSource,
'x-component': 'Filter',
'x-component-props': {},
},
},
} as ISchema
}
onSubmit={({ filter }) => {
const params = field.decoratorProps.request.params || {};
params.filter = filter;
field.decoratorProps.request.params = params;
fieldSchema['x-decorator-props']['request']['params'] = params;
ctx.run({ ...ctx.params?.[0], filter });
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-decorator-props': fieldSchema['x-decorator-props'],
},
});
}}
/>
<SchemaSettings.Divider />
<SchemaSettings.Remove <SchemaSettings.Remove
removeParentsIfNoChildren removeParentsIfNoChildren
breakRemoveOn={{ breakRemoveOn={{

View File

@ -1,13 +1,14 @@
import { ArrayField } from '@formily/core'; import { ArrayField } from '@formily/core';
import { observer, RecursionField, Schema, useField, useFieldSchema } from '@formily/react'; import { observer, RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
import { uid } from '@formily/shared'; import { Spin, Tag } from 'antd';
import { Card } from 'antd'; import React, { useContext, useState } from 'react';
import React, { useState } from 'react'; import { SchemaComponentOptions } from '../..';
import { SchemaComponent } from '../..';
import { AsyncDataProvider, RecordProvider, useRequest } from '../../../'; import { AsyncDataProvider, RecordProvider, useRequest } from '../../../';
import { Board } from '../../../board'; import { Board } from '../../../board';
import { Action, ActionContext } from '../action'; import '../../../board/style.less';
import { CardContext, ColumnContext } from './context'; import { Action } from '../action';
import { KanbanCardContext, KanbanColumnContext } from './context';
import { KanbanCard } from './Kanban.Card';
import { KanbanCardDesigner } from './Kanban.Card.Designer'; import { KanbanCardDesigner } from './Kanban.Card.Designer';
import { KanbanCardViewer } from './Kanban.CardViewer'; import { KanbanCardViewer } from './Kanban.CardViewer';
import { KanbanDesigner } from './Kanban.Designer'; import { KanbanDesigner } from './Kanban.Designer';
@ -29,14 +30,31 @@ const useDefDataSource = (options, props) => {
return useRequest(useRequestProps(props), options); return useRequest(useRequestProps(props), options);
}; };
const useCreateKanbanCardValues = (options) => {
const { column, groupField } = useContext(KanbanColumnContext);
return useRequest(
() =>
Promise.resolve({
data: {
[groupField.name]: column.id === '__unknown__' ? undefined : column.id,
},
}),
{
...options,
refreshDeps: [column.id],
},
);
};
export const Kanban: ComposedKanban = observer((props: any) => { export const Kanban: ComposedKanban = observer((props: any) => {
const { useDataSource = useDefDataSource, groupField, useDragEndAction, ...restProps } = props; const { useDataSource = useDefDataSource, groupField, useEvents, ...restProps } = props;
const field = useField<ArrayField>(); const field = useField<ArrayField>();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const [board, setBoard] = useState<any>({ columns: [] }); const [board, setBoard] = useState<any>({ columns: [] });
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [record, setRecord] = useState<any>({}); const [record, setRecord] = useState<any>({});
const { run: runDragEnd } = useDragEndAction?.() ?? {}; const { onCardDragEnd } = useEvents?.() ?? {};
const [disableCardDrag, setDisableCardDrag] = useState(false);
const result = useDataSource( const result = useDataSource(
{ {
uid: fieldSchema['x-uid'], uid: fieldSchema['x-uid'],
@ -55,7 +73,6 @@ export const Kanban: ComposedKanban = observer((props: any) => {
} }
return buf; return buf;
}, null); }, null);
console.log('board', board);
const cardAdderSchema: Schema = fieldSchema.reduceProperties((buf, current) => { const cardAdderSchema: Schema = fieldSchema.reduceProperties((buf, current) => {
if (current['x-component'] === 'Kanban.CardAdder') { if (current['x-component'] === 'Kanban.CardAdder') {
return current; return current;
@ -74,40 +91,53 @@ export const Kanban: ComposedKanban = observer((props: any) => {
field.value = updatedBoard.columns; field.value = updatedBoard.columns;
}; };
const cardDragEndHandler = (card, fromColumn, toColumn) => { const cardDragEndHandler = (card, fromColumn, toColumn) => {
onCardDragEnd?.({ columns: field.value, groupField }, fromColumn, toColumn);
const updatedBoard = Board.moveCard({ columns: field.value }, fromColumn, toColumn); const updatedBoard = Board.moveCard({ columns: field.value }, fromColumn, toColumn);
field.value = updatedBoard.columns; field.value = updatedBoard.columns;
runDragEnd?.(card, fromColumn, toColumn);
}; };
return ( return (
<Spin spinning={result.loading}>
<AsyncDataProvider value={result}> <AsyncDataProvider value={result}>
{cardViewerSchema && (
<ActionContext.Provider value={{ visible, setVisible }}>
<RecordProvider record={record}>
<SchemaComponent name={record.id} schema={cardViewerSchema as any} onlyRenderProperties />
</RecordProvider>
</ActionContext.Provider>
)}
<Board <Board
disableCardDrag={disableCardDrag}
onCardRemove={cardRemoveHandler} onCardRemove={cardRemoveHandler}
onCardDragEnd={cardDragEndHandler} onCardDragEnd={cardDragEndHandler}
renderColumnHeader={({ title, color }) => (
<div className={'react-kanban-column-header'}>
<Tag color={color}>{title}</Tag>
</div>
)}
renderCard={(card, { column, dragging }) => { renderCard={(card, { column, dragging }) => {
const columnIndex = field.value?.indexOf(column); const columnIndex = field.value?.indexOf(column);
const cardIndex = column?.cards?.indexOf(card); const cardIndex = column?.cards?.indexOf(card);
return ( return (
cardSchema && (
<RecordProvider record={card}> <RecordProvider record={card}>
<CardContext.Provider value={{ card, column, dragging }}> <KanbanCardContext.Provider
<Card style={{ width: 220, marginBottom: 15, cursor: 'pointer' }}> value={{
<RecursionField name={`${columnIndex}.cards.${cardIndex}`} schema={cardSchema} onlyRenderProperties /> setDisableCardDrag,
</Card> cardViewerSchema,
</CardContext.Provider> cardField: field,
card,
column,
dragging,
columnIndex,
cardIndex,
}}
>
<RecursionField name={cardSchema.name} schema={cardSchema} />
</KanbanCardContext.Provider>
</RecordProvider> </RecordProvider>
)
); );
}} }}
renderCardAdder={({ column }) => { renderCardAdder={({ column }) => {
return ( return (
<ColumnContext.Provider value={{ column }}> <KanbanColumnContext.Provider value={{ column, groupField }}>
<SchemaComponent memoized name={uid()} schema={cardAdderSchema as any} /> <SchemaComponentOptions scope={{ useCreateKanbanCardValues }}>
</ColumnContext.Provider> <RecursionField name={cardAdderSchema.name} schema={cardAdderSchema} />
</SchemaComponentOptions>
</KanbanColumnContext.Provider>
); );
}} }}
{...restProps} {...restProps}
@ -117,15 +147,12 @@ export const Kanban: ComposedKanban = observer((props: any) => {
}} }}
</Board> </Board>
</AsyncDataProvider> </AsyncDataProvider>
</Spin>
); );
}); });
Kanban.Card = () => null; Kanban.Card = KanbanCard;
Kanban.CardAdder = Action; Kanban.CardAdder = Action;
Kanban.CardViewer = KanbanCardViewer; Kanban.CardViewer = KanbanCardViewer;
Kanban.Card.Designer = KanbanCardDesigner; Kanban.Card.Designer = KanbanCardDesigner;
Kanban.Designer = KanbanDesigner; Kanban.Designer = KanbanDesigner;

View File

@ -1,4 +1,4 @@
import { createContext } from 'react'; import { createContext } from 'react';
export const CardContext = createContext(null); export const KanbanCardContext = createContext(null);
export const ColumnContext = createContext(null); export const KanbanColumnContext = createContext(null);

View File

@ -1,6 +1,6 @@
interface IGroupField { interface IGroupField {
name: string; name: string;
enum: Array<{ label: string; value: string; index: number }>; enum: Array<{ label: string; value: string; color?: string; }>;
} }
interface IBoard { interface IBoard {

View File

@ -2,20 +2,29 @@ export const toGroupDataSource = (groupField: IGroupField, dataSource: Array<any
if (dataSource.length === 0) { if (dataSource.length === 0) {
return { columns: [] }; return { columns: [] };
} }
const groupDataSource = []; const groupDataSource = {
groupField.enum.forEach((item, index) => { __unknown__: {
groupDataSource.push({ id: '__unknown__',
title: 'Unknown',
color: 'default',
cards: [],
},
};
groupField.enum.forEach((item) => {
groupDataSource[item.value] = {
id: item.value, id: item.value,
title: item.label, title: item.label,
index: item.index, color: item.color,
cards: [], cards: [],
}); };
}); });
dataSource.forEach((ds) => { dataSource.forEach((ds) => {
const group = groupDataSource.find((g) => g.id === ds[groupField.name]); const value = ds[groupField.name];
if (group) { if (value && groupDataSource[value]) {
group.cards.push(ds); groupDataSource[value].cards.push(ds);
} else {
groupDataSource.__unknown__.cards.push(ds);
} }
}); });
return { columns: groupDataSource }; return { columns: Object.values(groupDataSource) };
}; };

View File

@ -13,8 +13,8 @@ const useLabelFields = (collectionName?: any) => {
const { getCollectionFields } = useCollectionManager(); const { getCollectionFields } = useCollectionManager();
const targetFields = getCollectionFields(collectionName); const targetFields = getCollectionFields(collectionName);
return targetFields return targetFields
?.filter((field) => !field?.target && field.type !== 'boolean') ?.filter?.((field) => !field?.target && field.type !== 'boolean')
?.map((field) => { ?.map?.((field) => {
return { return {
value: field.name, value: field.name,
label: compile(field?.uiSchema?.title || field.name), label: compile(field?.uiSchema?.title || field.name),
@ -30,7 +30,7 @@ export const TableColumnDeigner = (props) => {
const initialValue = { const initialValue = {
title: columnSchema?.title, title: columnSchema?.title,
}; };
const options = useLabelFields(collectionField.target); const options = useLabelFields(collectionField?.target);
return ( return (
<GeneralSchemaDesigner> <GeneralSchemaDesigner>
<SchemaSettings.PopupItem <SchemaSettings.PopupItem

View File

@ -1,14 +1,16 @@
import { DndContext as DndKitContext, DragEndEvent, DragOverlay, rectIntersection } from '@dnd-kit/core'; import { DndContext as DndKitContext, DragEndEvent, DragOverlay, rectIntersection } from '@dnd-kit/core';
import { Props } from '@dnd-kit/core/dist/components/DndContext/DndContext';
import { observer } from '@formily/react'; import { observer } from '@formily/react';
import React from 'react'; import React from 'react';
import { useAPIClient } from '../../../'; import { useAPIClient } from '../../../';
import { createDesignable, useDesignable } from '../../hooks'; import { createDesignable, useDesignable } from '../../hooks';
const useDragEnd = () => { const useDragEnd = (props?: any) => {
const { refresh } = useDesignable(); const { refresh } = useDesignable();
const api = useAPIClient(); const api = useAPIClient();
return ({ active, over }: DragEndEvent) => { return (event: DragEndEvent) => {
const { active, over } = event;
const activeSchema = active?.data?.current?.schema; const activeSchema = active?.data?.current?.schema;
const overSchema = over?.data?.current?.schema; const overSchema = over?.data?.current?.schema;
const insertAdjacent = over?.data?.current?.insertAdjacent; const insertAdjacent = over?.data?.current?.insertAdjacent;
@ -16,10 +18,12 @@ const useDragEnd = () => {
const wrapSchema = over?.data?.current?.wrapSchema; const wrapSchema = over?.data?.current?.wrapSchema;
if (!activeSchema || !overSchema) { if (!activeSchema || !overSchema) {
props?.onDragEnd?.(event);
return; return;
} }
if (activeSchema === overSchema) { if (activeSchema === overSchema) {
props?.onDragEnd?.(event);
return; return;
} }
@ -32,7 +36,9 @@ const useDragEnd = () => {
dn.loadAPIClientEvents(); dn.loadAPIClientEvents();
if (activeSchema.parent === overSchema.parent) { if (activeSchema.parent === overSchema.parent) {
return dn.insertBeforeBeginOrAfterEnd(activeSchema); dn.insertBeforeBeginOrAfterEnd(activeSchema);
props?.onDragEnd?.(event);
return;
} }
if (insertAdjacent) { if (insertAdjacent) {
@ -41,14 +47,15 @@ const useDragEnd = () => {
breakRemoveOn, breakRemoveOn,
removeParentsIfNoChildren: true, removeParentsIfNoChildren: true,
}); });
props?.onDragEnd?.(event);
return; return;
} }
}; };
}; };
export const DndContext = observer((props) => { export const DndContext = observer((props: Props) => {
return ( return (
<DndKitContext collisionDetection={rectIntersection} onDragEnd={useDragEnd()}> <DndKitContext collisionDetection={rectIntersection} {...props} onDragEnd={useDragEnd(props)}>
<DragOverlay <DragOverlay
dropAnimation={{ dropAnimation={{
duration: 10, duration: 10,

View File

@ -24,6 +24,11 @@ export const BlockInitializers = {
title: 'Calendar', title: 'Calendar',
component: 'CalendarBlockInitializer', component: 'CalendarBlockInitializer',
}, },
{
type: 'item',
title: 'Kanban',
component: 'KanbanBlockInitializer',
},
], ],
}, },
{ {

View File

@ -0,0 +1,226 @@
import { FormOutlined } from '@ant-design/icons';
import { FormDialog, FormLayout } from '@formily/antd';
import { ISchema, SchemaOptionsContext } from '@formily/react';
import React, { useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { useAPIClient } from '../../../api-client';
import { useCollectionManager } from '../../../collection-manager';
import { SchemaComponent, SchemaComponentOptions } from '../../../schema-component';
import { SchemaInitializer } from '../../SchemaInitializer';
const createSchema = (collectionName, { groupField, sortName }) => {
const schema: ISchema = {
type: 'void',
'x-collection': 'collections',
'x-decorator': 'ResourceActionProvider',
'x-decorator-props': {
collection: collectionName,
request: {
resource: collectionName,
action: 'list',
params: {
paginate: false,
sort: [sortName],
},
},
},
'x-designer': 'Kanban.Designer',
'x-component': 'BlockItem',
properties: {
kanban: {
type: 'array',
name: 'kanban',
'x-component': 'Kanban',
'x-component-props': {
useDataSource: '{{ cm.useDataSourceFromRAC }}',
groupField: groupField?.uiSchema,
cardAdderPosition: 'bottom',
allowAddCard: { on: 'bottom' },
disableColumnDrag: true,
useEvents: '{{ cm.useKanbanEvents }}',
},
properties: {
card: {
type: 'void',
name: 'card',
'x-component': 'Kanban.Card',
'x-decorator': 'BlockItem',
'x-designer': 'Kanban.Card.Designer',
'x-read-pretty': true,
properties: {},
},
cardAdder: {
type: 'void',
'x-component': 'Kanban.CardAdder',
'x-designer': 'Action.Designer',
'x-component-props': {
type: 'text',
openMode: 'drawer',
},
title: '添加卡片',
properties: {
drawer: {
type: 'void',
title: '{{ t("Add new record") }}',
'x-component': 'Action.Container',
'x-component-props': {},
'x-decorator': 'Form',
'x-decorator-props': {
useValues: '{{useCreateKanbanCardValues}}',
},
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'GridFormItemInitializers',
properties: {},
},
footer: {
type: 'void',
'x-component': 'Action.Container.Footer',
properties: {
actions: {
type: 'void',
'x-initializer': 'PopupFormActionInitializers',
'x-decorator': 'DndContext',
'x-component': 'ActionBar',
'x-component-props': {
layout: 'one-column',
},
properties: {},
},
},
},
},
},
},
},
cardViewer: {
type: 'void',
'x-component': 'Kanban.CardViewer',
properties: {
drawer: {
type: 'void',
'x-component': 'Action.Container',
'x-component-props': {
className: 'nb-action-popup',
},
title: '{{ t("View record") }}',
properties: {
tabs: {
type: 'void',
'x-component': 'Tabs',
'x-component-props': {},
'x-initializer': 'TabPaneInitializers',
properties: {
tab1: {
type: 'void',
title: '详情',
'x-component': 'Tabs.TabPane',
'x-designer': 'Tabs.Designer',
'x-component-props': {},
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'RecordBlockInitializers',
properties: {},
},
},
},
},
},
},
},
},
},
},
},
},
};
return schema;
};
export const KanbanBlockInitializer = (props) => {
const { insert } = props;
const { collections, getCollection } = useCollectionManager();
const { t } = useTranslation();
const options = useContext(SchemaOptionsContext);
const api = useAPIClient();
return (
<SchemaInitializer.Item
{...props}
icon={<FormOutlined />}
onClick={async ({ item }) => {
const collection = getCollection(item.name);
const fields = collection?.fields
?.filter((field) => ['select', 'radioGroup'].includes(field.interface))
?.map((field) => {
return {
label: field?.uiSchema?.title,
value: field.name,
uiSchema: {
...field.uiSchema,
name: field.name,
},
};
});
const values = await FormDialog('创建看板区块', () => {
return (
<SchemaComponentOptions scope={options.scope} components={{ ...options.components }}>
<FormLayout layout={'vertical'}>
<SchemaComponent
schema={{
properties: {
groupField: {
title: '分组字段',
enum: fields,
required: true,
'x-component': 'Select',
'x-component-props': {
objectValue: true,
fieldNames: { label: 'label', value: 'value' },
},
'x-decorator': 'FormItem',
},
},
}}
/>
</FormLayout>
</SchemaComponentOptions>
);
}).open({
initialValues: {},
});
const sortName = `${values.groupField.value}_sort`;
const exists = collection?.fields?.find((field) => field.name === sortName);
if (!exists) {
await api.resource('collections.fields', item.name).create({
values: {
type: 'sort',
name: sortName,
hidden: true,
scopeKey: values.groupField.value,
},
});
}
insert(createSchema(item.name, { ...values, sortName }));
}}
items={[
{
type: 'itemGroup',
title: t('Select data source'),
children: collections
?.filter((item) => !item.inherit)
?.map((item) => {
return {
type: 'item',
name: item.name,
title: item.title,
};
}),
},
]}
/>
);
};

View File

@ -5,6 +5,7 @@ export * from './CollectionFieldInitializer';
export * from './FilterActionInitializer'; export * from './FilterActionInitializer';
export * from './FormBlockInitializer'; export * from './FormBlockInitializer';
export * from './GeneralInitializer'; export * from './GeneralInitializer';
export * from './KanbanBlockInitializer';
export * from './LinkToCollectionFieldInitializer'; export * from './LinkToCollectionFieldInitializer';
export * from './MarkdownBlockInitializer'; export * from './MarkdownBlockInitializer';
export * from './RecordDetailsBlockInitializer'; export * from './RecordDetailsBlockInitializer';