diff --git a/packages/app/package.json b/packages/app/package.json index 8f65f9697..86f29e3f7 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -23,6 +23,7 @@ "dependencies": { "@ant-design/pro-layout": "^5.0.12", "@formily/antd-components": "^1.3.6", + "@lourenci/react-kanban": "^2.1.0", "@types/react-big-calendar": "^0.24.8", "@umijs/preset-react": "1.x", "@umijs/test": "^3.2.23", diff --git a/packages/app/src/components/pages/AdminLoader/Actions/index.tsx b/packages/app/src/components/pages/AdminLoader/Actions/index.tsx index 2f709d251..12ee80265 100644 --- a/packages/app/src/components/pages/AdminLoader/Actions/index.tsx +++ b/packages/app/src/components/pages/AdminLoader/Actions/index.tsx @@ -8,14 +8,14 @@ import set from 'lodash/set'; export function Create(props) { const { size, onFinish, schema = {}, associatedKey, ...restProps } = props; - const { title, viewName, transform } = schema; + const { title, pageTitle, viewName, transform, componentProps = {} } = schema; return ( <> ) @@ -157,9 +158,9 @@ export function Add(props) { export function Destroy(props) { const { size, schema = {}, onFinish } = props; - const { title } = schema; + const { title, componentProps = {} } = schema; return ( - { + { onFinish && await onFinish(); }}> ) diff --git a/packages/app/src/components/pages/AdminLoader/View/Form.tsx b/packages/app/src/components/pages/AdminLoader/View/Form.tsx index 19d7cd7fe..ac6956a55 100644 --- a/packages/app/src/components/pages/AdminLoader/View/Form.tsx +++ b/packages/app/src/components/pages/AdminLoader/View/Form.tsx @@ -90,7 +90,7 @@ export function fields2properties(fields = [], options: any = {}) { const actions = createFormActions(); export function Form(props: any) { - const { onValueChange, onReset, __parent, noRequest = false, onFinish, onDraft, resolve, data: record = {}, associatedKey, schema = {} } = props; + const { initialValues = {}, onValueChange, onReset, __parent, noRequest = false, onFinish, onDraft, resolve, data: record = {}, associatedKey, schema = {} } = props; console.log({ noRequest, record, associatedKey, __parent }); const { statusable, resourceName, rowKey = 'id', fields = [], appends = [], associationField = {} } = schema; @@ -116,6 +116,7 @@ export function Form(props: any) { layout={'vertical'} initialValues={{ ...data, + ...initialValues, associatedKey, resourceKey, }} diff --git a/packages/app/src/components/pages/AdminLoader/View/Kanban/index.tsx b/packages/app/src/components/pages/AdminLoader/View/Kanban/index.tsx new file mode 100644 index 000000000..5f239a7f2 --- /dev/null +++ b/packages/app/src/components/pages/AdminLoader/View/Kanban/index.tsx @@ -0,0 +1,301 @@ +import React, { useState } from "react"; +import { useRequest, useHistory } from 'umi'; +import api from '@/api-client'; +import { Descriptions, Popconfirm, Card, Button } from 'antd'; +import Board, { moveCard } from "@lourenci/react-kanban"; +import "@lourenci/react-kanban/dist/styles.css"; +import { PlusOutlined, CloseOutlined, RightOutlined } from '@ant-design/icons'; +import Drawer from '@/components/pages/AdminLoader/Drawer'; +import { Actions, Create, Destroy } from '../../Actions'; +import get from 'lodash/get'; +import filter from 'lodash/filter'; +import Field from '@/components/views/Field'; +import { Details, DetailsPage } from '../Table'; +import merge from 'lodash/merge'; +import { Spin } from '@nocobase/client'; +import './style.less'; + +export function Kanban(props: any) { + const { defaultFilter, associatedKey, schema = {} } = props; + const { + filter: schemaFilter, + sort, + appends, + groupField = {}, + resourceName, + createViewName = 'form', + disableCardDrag = false, + // allowAddCard = false, + // allowRemoveCard = false, + labelField, + actions = [], + fields = [], + detailsOpenMode, + details = [], + rowKey = 'id', + } = schema; + const { dataSource = [] } = groupField; + console.log({groupField, dataSource}) + + const paginated = false; + + const history = useHistory(); + + const { data, loading, mutate, refresh, run, params } = useRequest((params = {}, ...args) => { + const { current, pageSize, sorter, filter, ...restParams } = params; + console.log('paramsparamsparamsparamsparams', params, args); + return api.resource(resourceName).list({ + associatedKey, + page: paginated ? current : 1, + perPage: paginated ? pageSize : -1, + sorter, + sort: [`${groupField.name}_sort`], + 'fields[appends]': appends, + // filter, + // ...actionDefaultParams, + filter: { + and: [ + defaultFilter, + schemaFilter, + filter, + // __parent ? { + // collection_name: __parent, + // } : null, + ].filter(obj => obj && Object.keys(obj).length) + } + // ...args2, + }) + .then(({data = [], meta = {}}) => { + return { + data: { + list: data, + total: meta.count||data.length, + }, + }; + }); + }, { + paginated, + }); + + if (loading) { + return ; + } + + const columns = dataSource.map(group => { + return { + id: group.value, + title: group.label, + cards: filter(data?.list, (item) => { + return get(item, groupField.name) === group.value; + }), + }; + }); + console.log({columns}); + + const bodyStyle: any = {}; + + if (fields.length === 0) { + bodyStyle.padding = 0; + } + + const createAction = actions.find(action => { + return action.type === 'create'; + }); + + let allowAddCard = !!props.allowAddCard; + + if (createAction) { + allowAddCard = true; + } + + let allowRemoveCard = !!props.allowRemoveCard; + const destroyAction = actions.find(action => { + return action.type === 'destroy'; + }); + if (destroyAction) { + allowRemoveCard = true; + } + + return ( +
+ !['create', 'destroy'].includes(action.type))} + onTrigger={{ + async filter(values) { + const items = values.filter.and || values.filter.or; + // @ts-ignore + run({...params[0], filter: values.filter}); + // refresh(); + }, + }} + /> + { + const openDetails = (e) => { + if (!detailsOpenMode || !details.length) { + return; + } + if (detailsOpenMode === 'window') { + const paths = history.location.pathname.split('/'); + history.push(`/admin/${paths[2]}/${data[rowKey]}/0`); + } else { + Drawer.open({ + headerStyle: details.length > 1 ? { + paddingBottom: 0, + borderBottom: 0, + // paddingTop: 16, + // marginBottom: -4, + } : {}, + title: details.length > 1 ? undefined : data[labelField], + content: ({resolve, closeWithConfirm}) => ( +
+
{ + await refresh(); + resolve(); + }} + onValueChange={() => { + closeWithConfirm && closeWithConfirm(true); + }} + onDraft={async () => { + await refresh(); + resolve(); + }} + onReset={resolve} + onDataChange={async () => { + await refresh(); + }} + data={data} + resolve={resolve} + items={details} + /> +
+ ), + }); + } + } + return ( + + {data[labelField]} +
+ )} + bodyStyle={bodyStyle} + extra={ + allowRemoveCard && ( + { + await api.resource(resourceName).destroy({ + associatedKey, + filter: { + [`${rowKey}.in`]: [data[rowKey]], + }, + }); + await refresh(); + }} + /> + ) + } + > +
+ {fields.length > 0 && ( + + {fields.map((field: any) => { + return ( + + + + ) + })} + + )} +
+ + ); + }} + onCardDragEnd={async ({ columns }, card, source, destination) => { + await api.resource(resourceName).update({ + associatedKey, + resourceKey: card[rowKey], + values: { + [groupField.name]: destination.toColumnId, + }, + }); + const destColumn = columns.find( column => column.id === destination.toColumnId); + const targetIndex = get(destColumn, ['cards', destination.toPosition+1, rowKey]); + console.log({targetIndex, card, destination}) + await api.resource(resourceName).sort({ + associatedKey, + resourceKey: card[rowKey], + values: { + field: `${groupField.name}_sort`, + sticky: destination.toPosition === 0, + target: { + [rowKey]: targetIndex, + }, + }, + }); + }} + initialBoard={{columns}} + // allowAddCard={{ on: "bottom" }} + allowAddCard={false} + // onNewCardConfirm={draftCard => ({ + // id: new Date().getTime(), + // ...draftCard + // })} + renderColumnHeader={({ title, id }, { addCard }) => { + return ( + <> +
{ title }
+ {allowAddCard && { + refresh(); + }} + initialValues={{ + [groupField.name]: id, + }} + schema={merge(createAction, { + title: null, + pageTitle: '新增', + componentProps: { + type: 'text', + block: true, + className: 'noco-card-adder-button', + style: { + marginBottom: 10, + border: 0, + background: '#fff', + }, + }, + })}/>} + + ); + }} + onCardNew={console.log} + /> + + ); +} \ No newline at end of file diff --git a/packages/app/src/components/pages/AdminLoader/View/Kanban/style.less b/packages/app/src/components/pages/AdminLoader/View/Kanban/style.less new file mode 100644 index 000000000..0cfd6f372 --- /dev/null +++ b/packages/app/src/components/pages/AdminLoader/View/Kanban/style.less @@ -0,0 +1,42 @@ +.react-kanban-board { + padding: 0; + margin: -24px -36px; + background-color: #eff2f5; + .react-kanban-column { + background-color: #eff2f5; + margin: 0; + padding: 12px; + } +} + +.react-kanban-column-header { + font-weight: 500; +} + +.noco-card-adder-button:hover { + background: rgba(255, 255, 255, .4) !important; +} + +.noco-kanban-view { + .action-buttons { + margin: -24px -24px 0 !important; + background-color: #eff2f5; + padding-bottom: 40px; + .ant-btn { + border: 0; + } + .filter-action-button { + margin-left: 0; + left: 0; + } + } + .action-buttons + .react-kanban-board { + margin-top: 0; + } + .ant-descriptions-item-label { + width: auto; + } + .ant-descriptions-item { + // padding-bottom: 4px; + } +} \ No newline at end of file diff --git a/packages/app/src/components/pages/AdminLoader/View/Table.tsx b/packages/app/src/components/pages/AdminLoader/View/Table.tsx index 2314b4c42..5de40c86e 100644 --- a/packages/app/src/components/pages/AdminLoader/View/Table.tsx +++ b/packages/app/src/components/pages/AdminLoader/View/Table.tsx @@ -21,7 +21,7 @@ import pathToRegexp from 'path-to-regexp' export const icon = ; export function Details(props) { - const { __parent, associatedKey, resourceName, onFinish, onReset, onDataChange, data, items = [], resolve, onValueChange } = props; + const { __parent, associatedKey, resourceName, onFinish, onDraft, onReset, onDataChange, data, items = [], resolve, onValueChange } = props; if (!items || items.length === 0) { return null; } @@ -48,6 +48,7 @@ export function Details(props) { } return ( diff --git a/packages/app/src/components/pages/AdminLoader/View/index.tsx b/packages/app/src/components/pages/AdminLoader/View/index.tsx index c0073ff18..8e5b886b0 100644 --- a/packages/app/src/components/pages/AdminLoader/View/index.tsx +++ b/packages/app/src/components/pages/AdminLoader/View/index.tsx @@ -21,6 +21,7 @@ import { FilterForm } from './FilterForm'; import { SubTable } from './SubTable'; import { Wysiwyg } from './Wysiwyg'; import { Calendar } from './Calendar'; +import { Kanban } from './Kanban'; const VIEWS = new Map(); @@ -66,5 +67,6 @@ registerView('descriptions', Descriptions); registerView('association', Association); registerView('wysiwyg', Wysiwyg); registerView('calendar', Calendar); +registerView('kanban', Kanban); export default View; diff --git a/packages/plugin-pages/src/actions/views_v2:getInfo.ts b/packages/plugin-pages/src/actions/views_v2:getInfo.ts index d3f7560f5..dc72f0179 100644 --- a/packages/plugin-pages/src/actions/views_v2:getInfo.ts +++ b/packages/plugin-pages/src/actions/views_v2:getInfo.ts @@ -373,6 +373,16 @@ export const getInfo = async (ctx: actions.Context, next) => { if (_.isEmpty(data.sort)) { data.sort = []; } + + if (data.type === 'kanban' && data.groupField) { + const groupField = await Field.findOne({ + where: { + name: data.groupField, + collection_name: data.collection_name, + } + }); + data.groupField = groupField; + } ctx.body = data; await next(); diff --git a/packages/plugin-pages/src/collections/views_actions_v2.ts b/packages/plugin-pages/src/collections/views_actions_v2.ts index 0f4370888..a53d6b4fe 100644 --- a/packages/plugin-pages/src/collections/views_actions_v2.ts +++ b/packages/plugin-pages/src/collections/views_actions_v2.ts @@ -24,7 +24,7 @@ export default { name: 'type', title: '操作类型', dataSource: [ - { label: '过滤', value: 'filter' }, + { label: '筛选', value: 'filter' }, { label: '新增', value: 'create' }, { label: '编辑', value: 'update' }, { label: '删除', value: 'destroy' }, diff --git a/packages/plugin-pages/src/server.ts b/packages/plugin-pages/src/server.ts index e96a0e0a8..e4dd026e8 100644 --- a/packages/plugin-pages/src/server.ts +++ b/packages/plugin-pages/src/server.ts @@ -86,73 +86,84 @@ export default async function (options = {}) { const createDetailsViews = async (model, options) => { const data = model.get(); const View = database.getModel('views_v2'); - const tableDetials = _.get(data, 'x-table-props.details') || []; - if (tableDetials.length) { - const details = []; - for (const item of tableDetials) { - if (item.view) { - if (!item.view.id) { - const view = await View.create(item.view); - await view.updateAssociations(item.view); - item.view.id = view.id; - } else { - const view = await View.findByPk(item.view.id); - await view.update(item.view); - await view.updateAssociations(item.view); - } - const view = await View.findOne(View.parseApiJson({ - filter: { - id: item.view.id, - }, - fields: { - appends: ['collection', 'targetField', 'targetView'], - }, - })); - if (view) { - console.log({view}); - item.view = view.toJSON(); + const types = ['table', 'calendar', 'kanban']; + for (const type of types) { + const items = _.get(data, `x-${type}-props.details`) || []; + if (items.length) { + const details = []; + for (const item of items) { + if (item.view) { + if (!item.view.id) { + const view = await View.create(item.view); + await view.updateAssociations(item.view); + item.view.id = view.id; + } else { + const view = await View.findByPk(item.view.id); + if (view) { + await view.update(item.view); + await view.updateAssociations(item.view); + } + } + const view = await View.findOne(View.parseApiJson({ + filter: { + id: item.view.id, + }, + fields: { + appends: ['collection', 'targetField', 'targetView'], + }, + })); + if (view) { + console.log({view}); + item.view = view.toJSON(); + } } + details.push(item); } - details.push(item); + model.set(`options.x-${type}-props.details`, details); } - model.set('options.x-table-props.details', details); - } - const calendarDetials = _.get(data, 'x-calendar-props.details') || []; - if (calendarDetials.length) { - const details = []; - for (const item of calendarDetials) { - if (item.view) { - if (!item.view.id) { - const view = await View.create(item.view); - await view.updateAssociations(item.view); - item.view.id = view.id; - } else { - const view = await View.findByPk(item.view.id); - await view.update(item.view); - await view.updateAssociations(item.view); - } - const view = await View.findOne(View.parseApiJson({ - filter: { - id: item.view.id, - }, - fields: { - appends: ['collection', 'targetField', 'targetView'], - }, - })); - if (view) { - console.log({view}); - item.view = view.toJSON(); - } - } - details.push(item); - } - model.set('options.x-calendar-props.details', details); } }; database.getModel('views_v2').addHook('beforeCreate', createDetailsViews); database.getModel('views_v2').addHook('beforeUpdate', createDetailsViews); + database.getModel('views_v2').addHook('beforeSave', async (model, options) => { + const data = model.get(); + if (data.type !== 'kanban') { + return; + } + let groupField = _.get(data, `x-kanban-props.groupField`); + if (!groupField) { + return; + } + if (typeof groupField === 'object' && groupField.name) { + groupField = groupField.name; + } + const Field = database.getModel('fields'); + let field = await Field.findOne({ + where: { + name: `${groupField}_sort`, + collection_name: data.collection_name, + }, + }); + if (field) { + return; + } + await Field.create({ + interface: 'sort', + type: 'sort', + name: `${groupField}_sort`, + // TODO: 不支持相关数据 + collection_name: data.collection_name, + scope: [groupField], + title: '看板分组排序', + developerMode: true, + component: { + type: 'sort', + }, + }); + }); + database.getModel('menus').addHook('beforeSave', async (model, options) => { const { transaction } = options; // console.log('beforeSave', model.get('views')); diff --git a/packages/plugin-pages/src/views/types.ts b/packages/plugin-pages/src/views/types.ts index d0a845d65..22b32e42f 100644 --- a/packages/plugin-pages/src/views/types.ts +++ b/packages/plugin-pages/src/views/types.ts @@ -304,6 +304,15 @@ export const calendar = { }, }, info3: { + interface: 'description', + type: 'virtual', + title: '操作按钮配置', + component: { + type: 'description', + }, + }, + actions, + info4: { interface: 'description', type: 'virtual', title: '单条数据页面配置', @@ -319,6 +328,95 @@ export const calendar = { }, }; +export const kanban = { + title: '看板', + options: { + }, + properties: { + info1: { + interface: 'description', + type: 'virtual', + title: '看板配置', + component: { + type: 'description', + }, + }, + labelField: { + interface: 'select', + type: 'virtual', + title: '作为单条数据标题的字段', + name: 'labelField', + required: true, + component: { + type: 'remoteSelect', + resourceName: 'collections.fields', + labelField: 'title', + valueField: 'name', + filter: { + type: 'string', + }, + }, + }, + groupField: { + interface: 'select', + type: 'virtual', + title: '看板分组字段', + name: 'groupField', + required: true, + component: { + type: 'remoteSelect', + resourceName: 'collections.fields', + labelField: 'title', + valueField: 'name', + filter: { + type: 'string', + }, + }, + }, + fields: { + ...fields, + title: '显示在看板里的字段' + }, + info2: { + interface: 'description', + type: 'virtual', + title: '数据配置', + component: { + type: 'description', + }, + }, + filter: { + interface: 'json', + type: 'virtual', + title: '只显示符合以下条件的数据', + mode: 'replace', + defaultValue: {}, + component: { + type: 'filter', + }, + }, + info3: { + interface: 'description', + type: 'virtual', + title: '操作按钮配置', + component: { + type: 'description', + }, + }, + actions, + info4: { + interface: 'description', + type: 'virtual', + title: '单条数据页面配置', + component: { + type: 'description', + }, + }, + detailsOpenMode, + details, + }, +}; + export const wysiwyg = { title: '富文本', options: {