feat: kanban (#230)
* feat: kanban * feat(client): improve kanban * fix: unknown group
This commit is contained in:
parent
ffe3463dc2
commit
3d5817e0b3
@ -12,7 +12,8 @@ function Card({ children, index, renderCard, disableCardDrag }) {
|
||||
{...provided.dragHandleProps}
|
||||
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>
|
||||
);
|
||||
}}
|
||||
|
@ -46,16 +46,18 @@ function Column({
|
||||
{cardAdderPosition === 'top' && allowAddCard && renderCardAdder({ column: children, onConfirm: onCardNew })}
|
||||
<DroppableColumn droppableId={String(children.id)}>
|
||||
{children?.cards?.length ? (
|
||||
children.cards.map((card, index) => (
|
||||
<Card
|
||||
key={card.id}
|
||||
index={index}
|
||||
renderCard={(dragging) => renderCard(children, card, dragging)}
|
||||
disableCardDrag={disableCardDrag}
|
||||
>
|
||||
{card}
|
||||
</Card>
|
||||
))
|
||||
<div className="react-kanban-card-skeleton">
|
||||
{children.cards.map((card, index) => (
|
||||
<Card
|
||||
key={card.id}
|
||||
index={index}
|
||||
renderCard={(dragging) => renderCard(children, card, dragging)}
|
||||
disableCardDrag={disableCardDrag}
|
||||
>
|
||||
{card}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="react-kanban-card-skeleton" />
|
||||
)}
|
||||
|
@ -1,5 +1,6 @@
|
||||
.react-kanban-board {
|
||||
padding: 5px;
|
||||
// padding: 5px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.react-kanban-card {
|
||||
@ -14,8 +15,19 @@
|
||||
|
||||
.react-kanban-card-skeleton {
|
||||
box-sizing: border-box;
|
||||
max-width: 250px;
|
||||
min-width: 250px;
|
||||
max-width: 300px;
|
||||
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 {
|
||||
@ -35,15 +47,16 @@
|
||||
}
|
||||
|
||||
.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;
|
||||
border-radius: 2px;
|
||||
background-color: #eee;
|
||||
margin: 5px;
|
||||
input {
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.react-kanban-card-adder-form {
|
||||
@ -114,14 +127,7 @@
|
||||
}
|
||||
|
||||
.react-kanban-column-header {
|
||||
padding-bottom: 10px;
|
||||
font-weight: bold;
|
||||
|
||||
input {
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.react-kanban-column-header__button {
|
||||
|
@ -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) => {
|
||||
const { getCollectionFields, getInterface } = useCollectionManager();
|
||||
const fields = getCollectionFields(collectionName);
|
||||
|
@ -6,7 +6,7 @@ import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCompile, useDesignable } from '../../../schema-component';
|
||||
import { SchemaInitializer } from '../../../schema-initializer';
|
||||
import { useCardItemInitializerFields } from './hoooks';
|
||||
import { useFormItemInitializerFields } from '../../../schema-initializer/Initializers/utils';
|
||||
|
||||
const titleCss = css`
|
||||
pointer-events: none;
|
||||
@ -23,6 +23,15 @@ const titleCss = css`
|
||||
left: 2px;
|
||||
`;
|
||||
|
||||
export const removeGridFormItem = (schema, cb) => {
|
||||
cb(schema, {
|
||||
removeParentsIfNoChildren: true,
|
||||
breakRemoveOn: {
|
||||
'x-component': 'Kanban.Card',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const KanbanCardDesigner = (props: any) => {
|
||||
const { dn, designable } = useDesignable();
|
||||
const { t } = useTranslation();
|
||||
@ -34,6 +43,7 @@ export const KanbanCardDesigner = (props: any) => {
|
||||
field,
|
||||
fieldSchema,
|
||||
};
|
||||
const fields = useFormItemInitializerFields();
|
||||
if (!designable) {
|
||||
return null;
|
||||
}
|
||||
@ -42,11 +52,21 @@ export const KanbanCardDesigner = (props: any) => {
|
||||
<div className={'general-schema-designer-icons'}>
|
||||
<Space size={2} align={'center'}>
|
||||
<SchemaInitializer.Button
|
||||
wrap={(s) => {
|
||||
s['type'] = 'string';
|
||||
s['x-read-pretty'] = true;
|
||||
return s;
|
||||
}}
|
||||
items={[
|
||||
{
|
||||
type: 'itemGroup',
|
||||
title: t('Display fields'),
|
||||
children: useCardItemInitializerFields(),
|
||||
children: fields.map((field) => {
|
||||
return {
|
||||
...field,
|
||||
remove: removeGridFormItem,
|
||||
};
|
||||
}),
|
||||
},
|
||||
]}
|
||||
component={<MenuOutlined style={{ cursor: 'pointer', fontSize: 12 }} />}
|
||||
|
@ -1,27 +1,72 @@
|
||||
import { createForm } from '@formily/core';
|
||||
import { FieldContext, FormContext, observer } from '@formily/react';
|
||||
import { css } from '@emotion/css';
|
||||
import { observer, RecursionField, useFieldSchema } from '@formily/react';
|
||||
import { Card } from 'antd';
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import { BlockItem } from '../block-item';
|
||||
import { CardContext } from './context';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { ActionContext, BlockItem } from '..';
|
||||
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) => {
|
||||
const { allowRemoveCard, onCardRemove, children } = props;
|
||||
const { card, dragging } = useContext(CardContext);
|
||||
const form = useMemo(
|
||||
() =>
|
||||
createForm({
|
||||
values: { card: { ...card } },
|
||||
}),
|
||||
[card],
|
||||
);
|
||||
const { setDisableCardDrag, cardViewerSchema, card, cardField, columnIndex, cardIndex } =
|
||||
useContext(KanbanCardContext);
|
||||
const fieldSchema = useFieldSchema();
|
||||
const [visible, setVisible] = useState(false);
|
||||
return (
|
||||
<BlockItem className={'noco-card-item'}>
|
||||
<FieldContext.Provider value={undefined}>
|
||||
<FormContext.Provider value={form}>
|
||||
<Card style={{ width: 220, marginBottom: 15, cursor: 'pointer' }}>{children}</Card>
|
||||
</FormContext.Provider>
|
||||
</FieldContext.Provider>
|
||||
</BlockItem>
|
||||
<>
|
||||
<Card
|
||||
onClick={(e) => {
|
||||
setVisible(true);
|
||||
}}
|
||||
className={css`
|
||||
/* .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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -1,11 +1,54 @@
|
||||
import { ISchema, useField, useFieldSchema } from '@formily/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 { useDesignable } from '../../hooks';
|
||||
|
||||
export const KanbanDesigner = () => {
|
||||
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 (
|
||||
<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
|
||||
removeParentsIfNoChildren
|
||||
breakRemoveOn={{
|
||||
|
@ -1,13 +1,14 @@
|
||||
import { ArrayField } from '@formily/core';
|
||||
import { observer, RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { Card } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import { SchemaComponent } from '../..';
|
||||
import { Spin, Tag } from 'antd';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { SchemaComponentOptions } from '../..';
|
||||
import { AsyncDataProvider, RecordProvider, useRequest } from '../../../';
|
||||
import { Board } from '../../../board';
|
||||
import { Action, ActionContext } from '../action';
|
||||
import { CardContext, ColumnContext } from './context';
|
||||
import '../../../board/style.less';
|
||||
import { Action } from '../action';
|
||||
import { KanbanCardContext, KanbanColumnContext } from './context';
|
||||
import { KanbanCard } from './Kanban.Card';
|
||||
import { KanbanCardDesigner } from './Kanban.Card.Designer';
|
||||
import { KanbanCardViewer } from './Kanban.CardViewer';
|
||||
import { KanbanDesigner } from './Kanban.Designer';
|
||||
@ -29,14 +30,31 @@ const useDefDataSource = (options, props) => {
|
||||
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) => {
|
||||
const { useDataSource = useDefDataSource, groupField, useDragEndAction, ...restProps } = props;
|
||||
const { useDataSource = useDefDataSource, groupField, useEvents, ...restProps } = props;
|
||||
const field = useField<ArrayField>();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const [board, setBoard] = useState<any>({ columns: [] });
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [record, setRecord] = useState<any>({});
|
||||
const { run: runDragEnd } = useDragEndAction?.() ?? {};
|
||||
const { onCardDragEnd } = useEvents?.() ?? {};
|
||||
const [disableCardDrag, setDisableCardDrag] = useState(false);
|
||||
const result = useDataSource(
|
||||
{
|
||||
uid: fieldSchema['x-uid'],
|
||||
@ -55,7 +73,6 @@ export const Kanban: ComposedKanban = observer((props: any) => {
|
||||
}
|
||||
return buf;
|
||||
}, null);
|
||||
console.log('board', board);
|
||||
const cardAdderSchema: Schema = fieldSchema.reduceProperties((buf, current) => {
|
||||
if (current['x-component'] === 'Kanban.CardAdder') {
|
||||
return current;
|
||||
@ -74,58 +91,68 @@ export const Kanban: ComposedKanban = observer((props: any) => {
|
||||
field.value = updatedBoard.columns;
|
||||
};
|
||||
const cardDragEndHandler = (card, fromColumn, toColumn) => {
|
||||
onCardDragEnd?.({ columns: field.value, groupField }, fromColumn, toColumn);
|
||||
const updatedBoard = Board.moveCard({ columns: field.value }, fromColumn, toColumn);
|
||||
field.value = updatedBoard.columns;
|
||||
runDragEnd?.(card, fromColumn, toColumn);
|
||||
};
|
||||
return (
|
||||
<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
|
||||
onCardRemove={cardRemoveHandler}
|
||||
onCardDragEnd={cardDragEndHandler}
|
||||
renderCard={(card, { column, dragging }) => {
|
||||
const columnIndex = field.value?.indexOf(column);
|
||||
const cardIndex = column?.cards?.indexOf(card);
|
||||
return (
|
||||
<RecordProvider record={card}>
|
||||
<CardContext.Provider value={{ card, column, dragging }}>
|
||||
<Card style={{ width: 220, marginBottom: 15, cursor: 'pointer' }}>
|
||||
<RecursionField name={`${columnIndex}.cards.${cardIndex}`} schema={cardSchema} onlyRenderProperties />
|
||||
</Card>
|
||||
</CardContext.Provider>
|
||||
</RecordProvider>
|
||||
);
|
||||
}}
|
||||
renderCardAdder={({ column }) => {
|
||||
return (
|
||||
<ColumnContext.Provider value={{ column }}>
|
||||
<SchemaComponent memoized name={uid()} schema={cardAdderSchema as any} />
|
||||
</ColumnContext.Provider>
|
||||
);
|
||||
}}
|
||||
{...restProps}
|
||||
>
|
||||
{{
|
||||
columns: field.value?.slice() || [],
|
||||
}}
|
||||
</Board>
|
||||
</AsyncDataProvider>
|
||||
<Spin spinning={result.loading}>
|
||||
<AsyncDataProvider value={result}>
|
||||
<Board
|
||||
disableCardDrag={disableCardDrag}
|
||||
onCardRemove={cardRemoveHandler}
|
||||
onCardDragEnd={cardDragEndHandler}
|
||||
renderColumnHeader={({ title, color }) => (
|
||||
<div className={'react-kanban-column-header'}>
|
||||
<Tag color={color}>{title}</Tag>
|
||||
</div>
|
||||
)}
|
||||
renderCard={(card, { column, dragging }) => {
|
||||
const columnIndex = field.value?.indexOf(column);
|
||||
const cardIndex = column?.cards?.indexOf(card);
|
||||
return (
|
||||
cardSchema && (
|
||||
<RecordProvider record={card}>
|
||||
<KanbanCardContext.Provider
|
||||
value={{
|
||||
setDisableCardDrag,
|
||||
cardViewerSchema,
|
||||
cardField: field,
|
||||
card,
|
||||
column,
|
||||
dragging,
|
||||
columnIndex,
|
||||
cardIndex,
|
||||
}}
|
||||
>
|
||||
<RecursionField name={cardSchema.name} schema={cardSchema} />
|
||||
</KanbanCardContext.Provider>
|
||||
</RecordProvider>
|
||||
)
|
||||
);
|
||||
}}
|
||||
renderCardAdder={({ column }) => {
|
||||
return (
|
||||
<KanbanColumnContext.Provider value={{ column, groupField }}>
|
||||
<SchemaComponentOptions scope={{ useCreateKanbanCardValues }}>
|
||||
<RecursionField name={cardAdderSchema.name} schema={cardAdderSchema} />
|
||||
</SchemaComponentOptions>
|
||||
</KanbanColumnContext.Provider>
|
||||
);
|
||||
}}
|
||||
{...restProps}
|
||||
>
|
||||
{{
|
||||
columns: field.value?.slice() || [],
|
||||
}}
|
||||
</Board>
|
||||
</AsyncDataProvider>
|
||||
</Spin>
|
||||
);
|
||||
});
|
||||
|
||||
Kanban.Card = () => null;
|
||||
|
||||
Kanban.Card = KanbanCard;
|
||||
Kanban.CardAdder = Action;
|
||||
|
||||
Kanban.CardViewer = KanbanCardViewer;
|
||||
|
||||
Kanban.Card.Designer = KanbanCardDesigner;
|
||||
|
||||
Kanban.Designer = KanbanDesigner;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const CardContext = createContext(null);
|
||||
export const ColumnContext = createContext(null);
|
||||
export const KanbanCardContext = createContext(null);
|
||||
export const KanbanColumnContext = createContext(null);
|
||||
|
@ -1,6 +1,6 @@
|
||||
interface IGroupField {
|
||||
name: string;
|
||||
enum: Array<{ label: string; value: string; index: number }>;
|
||||
enum: Array<{ label: string; value: string; color?: string; }>;
|
||||
}
|
||||
|
||||
interface IBoard {
|
||||
|
@ -2,20 +2,29 @@ export const toGroupDataSource = (groupField: IGroupField, dataSource: Array<any
|
||||
if (dataSource.length === 0) {
|
||||
return { columns: [] };
|
||||
}
|
||||
const groupDataSource = [];
|
||||
groupField.enum.forEach((item, index) => {
|
||||
groupDataSource.push({
|
||||
const groupDataSource = {
|
||||
__unknown__: {
|
||||
id: '__unknown__',
|
||||
title: 'Unknown',
|
||||
color: 'default',
|
||||
cards: [],
|
||||
},
|
||||
};
|
||||
groupField.enum.forEach((item) => {
|
||||
groupDataSource[item.value] = {
|
||||
id: item.value,
|
||||
title: item.label,
|
||||
index: item.index,
|
||||
color: item.color,
|
||||
cards: [],
|
||||
});
|
||||
};
|
||||
});
|
||||
dataSource.forEach((ds) => {
|
||||
const group = groupDataSource.find((g) => g.id === ds[groupField.name]);
|
||||
if (group) {
|
||||
group.cards.push(ds);
|
||||
const value = ds[groupField.name];
|
||||
if (value && groupDataSource[value]) {
|
||||
groupDataSource[value].cards.push(ds);
|
||||
} else {
|
||||
groupDataSource.__unknown__.cards.push(ds);
|
||||
}
|
||||
});
|
||||
return { columns: groupDataSource };
|
||||
return { columns: Object.values(groupDataSource) };
|
||||
};
|
||||
|
@ -13,8 +13,8 @@ const useLabelFields = (collectionName?: any) => {
|
||||
const { getCollectionFields } = useCollectionManager();
|
||||
const targetFields = getCollectionFields(collectionName);
|
||||
return targetFields
|
||||
?.filter((field) => !field?.target && field.type !== 'boolean')
|
||||
?.map((field) => {
|
||||
?.filter?.((field) => !field?.target && field.type !== 'boolean')
|
||||
?.map?.((field) => {
|
||||
return {
|
||||
value: field.name,
|
||||
label: compile(field?.uiSchema?.title || field.name),
|
||||
@ -30,7 +30,7 @@ export const TableColumnDeigner = (props) => {
|
||||
const initialValue = {
|
||||
title: columnSchema?.title,
|
||||
};
|
||||
const options = useLabelFields(collectionField.target);
|
||||
const options = useLabelFields(collectionField?.target);
|
||||
return (
|
||||
<GeneralSchemaDesigner>
|
||||
<SchemaSettings.PopupItem
|
||||
|
@ -1,14 +1,16 @@
|
||||
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 React from 'react';
|
||||
import { useAPIClient } from '../../../';
|
||||
import { createDesignable, useDesignable } from '../../hooks';
|
||||
|
||||
const useDragEnd = () => {
|
||||
const useDragEnd = (props?: any) => {
|
||||
const { refresh } = useDesignable();
|
||||
const api = useAPIClient();
|
||||
|
||||
return ({ active, over }: DragEndEvent) => {
|
||||
return (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
const activeSchema = active?.data?.current?.schema;
|
||||
const overSchema = over?.data?.current?.schema;
|
||||
const insertAdjacent = over?.data?.current?.insertAdjacent;
|
||||
@ -16,10 +18,12 @@ const useDragEnd = () => {
|
||||
const wrapSchema = over?.data?.current?.wrapSchema;
|
||||
|
||||
if (!activeSchema || !overSchema) {
|
||||
props?.onDragEnd?.(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeSchema === overSchema) {
|
||||
props?.onDragEnd?.(event);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -32,7 +36,9 @@ const useDragEnd = () => {
|
||||
dn.loadAPIClientEvents();
|
||||
|
||||
if (activeSchema.parent === overSchema.parent) {
|
||||
return dn.insertBeforeBeginOrAfterEnd(activeSchema);
|
||||
dn.insertBeforeBeginOrAfterEnd(activeSchema);
|
||||
props?.onDragEnd?.(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (insertAdjacent) {
|
||||
@ -41,14 +47,15 @@ const useDragEnd = () => {
|
||||
breakRemoveOn,
|
||||
removeParentsIfNoChildren: true,
|
||||
});
|
||||
props?.onDragEnd?.(event);
|
||||
return;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const DndContext = observer((props) => {
|
||||
export const DndContext = observer((props: Props) => {
|
||||
return (
|
||||
<DndKitContext collisionDetection={rectIntersection} onDragEnd={useDragEnd()}>
|
||||
<DndKitContext collisionDetection={rectIntersection} {...props} onDragEnd={useDragEnd(props)}>
|
||||
<DragOverlay
|
||||
dropAnimation={{
|
||||
duration: 10,
|
||||
|
@ -24,6 +24,11 @@ export const BlockInitializers = {
|
||||
title: 'Calendar',
|
||||
component: 'CalendarBlockInitializer',
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
title: 'Kanban',
|
||||
component: 'KanbanBlockInitializer',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -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,
|
||||
};
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
@ -5,6 +5,7 @@ export * from './CollectionFieldInitializer';
|
||||
export * from './FilterActionInitializer';
|
||||
export * from './FormBlockInitializer';
|
||||
export * from './GeneralInitializer';
|
||||
export * from './KanbanBlockInitializer';
|
||||
export * from './LinkToCollectionFieldInitializer';
|
||||
export * from './MarkdownBlockInitializer';
|
||||
export * from './RecordDetailsBlockInitializer';
|
||||
|
Loading…
Reference in New Issue
Block a user