diff --git a/packages/client/src/components/schema-renderer/index.tsx b/packages/client/src/components/schema-renderer/index.tsx index eb2d4dd04..e2df53cfa 100644 --- a/packages/client/src/components/schema-renderer/index.tsx +++ b/packages/client/src/components/schema-renderer/index.tsx @@ -145,6 +145,7 @@ export const SchemaField = createSchemaField({ scope: { Table, Calendar, + Kanban, useAsyncDataSource, ChinaRegion: { loadData: loadChinaRegionData, diff --git a/packages/client/src/constate/Collections.ts b/packages/client/src/constate/Collections.ts index 31b5b9b6b..bb952e049 100644 --- a/packages/client/src/constate/Collections.ts +++ b/packages/client/src/constate/Collections.ts @@ -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 }; diff --git a/packages/client/src/hooks/useResource.ts b/packages/client/src/hooks/useResource.ts index baec3af24..9e26cf55f 100644 --- a/packages/client/src/hooks/useResource.ts +++ b/packages/client/src/hooks/useResource.ts @@ -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, diff --git a/packages/client/src/schemas/add-new/index.tsx b/packages/client/src/schemas/add-new/index.tsx index be0158f42..aa9f2156f 100644 --- a/packages/client/src/schemas/add-new/index.tsx +++ b/packages/client/src/schemas/add-new/index.tsx @@ -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} > - { - 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 - + {item?.generalFields + ?.filter((item) => { + return item?.uiSchema?.enum; + }) + ?.map((field) => { + return ( + { + 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} + + ); + })} // { 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 ( { @@ -257,7 +257,7 @@ function AddActionButton() { className={'designable-btn designable-btn-dash'} style={{ marginLeft: 8 }} type={'dashed'} - icon={} + icon={} > 配置工具栏 diff --git a/packages/client/src/schemas/calendar/index.tsx b/packages/client/src/schemas/calendar/index.tsx index eece338de..9be900b91 100644 --- a/packages/client/src/schemas/calendar/index.tsx +++ b/packages/client/src/schemas/calendar/index.tsx @@ -183,7 +183,7 @@ export const Calendar: any = observer((props: any) => { console.log('field.value', field.value); return ( - +
{ 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 (
-
{collection?.title || collection?.name}
+
+ {collection?.title || collection?.name} +
{ e.stopPropagation(); diff --git a/packages/client/src/schemas/kanban/CardDesignableBar.tsx b/packages/client/src/schemas/kanban/CardDesignableBar.tsx new file mode 100644 index 000000000..1b6a75db6 --- /dev/null +++ b/packages/client/src/schemas/kanban/CardDesignableBar.tsx @@ -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 ( +
+ { + e.stopPropagation(); + }} + className={cls('designable-bar-actions', { active: visible })} + > + + { + setVisible(visible); + }} + overlay={ + + + {fields?.map((field) => ( + { + 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); + }} + /> + ))} + + + } + > + + + + +
+ ); +}); diff --git a/packages/client/src/schemas/kanban/Field.DesignableBar.tsx b/packages/client/src/schemas/kanban/Field.DesignableBar.tsx new file mode 100644 index 000000000..69a8f0e84 --- /dev/null +++ b/packages/client/src/schemas/kanban/Field.DesignableBar.tsx @@ -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 ( +
+ { + e.stopPropagation(); + }} + className={cls('designable-bar-actions', { active: visible })} + > + + + {dragRef && } + { + setVisible(visible); + }} + overlay={ + + { + const values = await FormDialog('修改字段名称', () => { + return ( + + + + ); + }).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, + }); + }} + > + 修改字段名称 + + { + const values = await FormDialog('编辑描述', () => { + return ( + + + + ); + }).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, + }); + }} + > + 编辑描述 + + { + field + .query(field.address.concat(randomName, fieldName)) + .take((f: any) => { + f.required = checked; + schema.required = checked; + updateSchema(schema); + }); + }} + /> + + { + 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); + } + }} + > + 移除 + + + } + > + + + + +
+ ); +}); diff --git a/packages/client/src/schemas/kanban/index.tsx b/packages/client/src/schemas/kanban/index.tsx index 1de48be03..158432749 100644 --- a/packages/client/src/schemas/kanban/index.tsx +++ b/packages/client/src/schemas/kanban/index.tsx @@ -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(); + 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(null); -export const Kanban: any = observer(() => { +export const KanbanContext = createContext(null); +export const KanbanColumnContext = createContext(null); + +export const useKanban = () => { + return useContext(KanbanContext); +}; + +const InternalKanban = observer((props: any) => { const field = useField(); + 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 ( -
- {/*
+ +
+ {/*
*/} -
- { - 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; - } - } - }} - > - aaa +
+
+ { + 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]; + } + } + }} + > + aaa - - {columns.map((column) => ( - ({ + ...column, + id: column.value, + })) || [] + } > -
{column.title}
- - {column.items.map((item) => { - const index = values.findIndex( - (value) => value.id === item.id, - ); - return ( - ( + + +
+ {column.label} +
+ - - - -
- ); - })} -
- */} + +
+ ))} +
- 添加卡片 - - - ))} - - + {/* */} + 添加列表 +
+
+
+
+
-
+
); }); +export const Kanban: any = observer((props: any) => { + return ( + + + + ); +}); + +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 ( - <> + { setVisible(true); }} bordered={false} + {...others} > - {props.children} + {children} + - { - setVisible(false); - }} - > - - - + + ); }); -Kanban.Card.View = observer((props) => { - return
{props.children}
; -}); - +Kanban.Card.View = Action.Drawer; +Kanban.Card.AddNew = Action; Kanban.DesignableBar = DesignableBar; +Kanban.Card.DesignableBar = CardDesignableBar; diff --git a/packages/client/src/schemas/kanban/style.less b/packages/client/src/schemas/kanban/style.less index e17a966c6..5e1905c05 100644 --- a/packages/client/src/schemas/kanban/style.less +++ b/packages/client/src/schemas/kanban/style.less @@ -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; -} \ No newline at end of file +} + +.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; + } + } + } +} +