feat: add Gantt and Kanban blocks in pop ups/drawers (#4277)

* feat: add Gantt  and Kanban blocks in pop ups/drawers

* feat: add Gantt  and Kanban blocks in pop ups/drawers

* fix: bug

* fix: bug

* fix: bug

* fix: bug
This commit is contained in:
katherinehhh 2024-05-08 10:16:39 +08:00 committed by GitHub
parent 1283312eb9
commit d787edfb47
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 536 additions and 218 deletions

View File

@ -1052,7 +1052,7 @@ export const useBulkDestroyActionProps = () => {
field.data.selectedRowKeys = [];
const currentPage = service.params[0]?.page;
const totalPage = service.data?.meta?.totalPage;
if (currentPage === totalPage) {
if (currentPage === totalPage && service.params[0]) {
service.params[0].page = currentPage - 1;
}
if (callBack) {

View File

@ -22,23 +22,54 @@ import {
SchemaComponent,
DataBlockInitializer,
SchemaComponentOptions,
Collection,
CollectionFieldOptions,
} from '@nocobase/client';
import { createGanttBlockUISchema } from './createGanttBlockUISchema';
export const GanttBlockInitializer = () => {
export const GanttBlockInitializer = ({
filterCollections,
onlyCurrentDataSource,
hideSearch,
createBlockSchema,
showAssociationFields,
}: {
filterCollections: (options: { collection?: Collection; associationField?: CollectionFieldOptions }) => boolean;
onlyCurrentDataSource: boolean;
hideSearch?: boolean;
createBlockSchema?: (options: any) => any;
showAssociationFields?: boolean;
}) => {
const itemConfig = useSchemaInitializerItem();
const { createGanttBlock } = useCreateGanttBlock();
return (
<DataBlockInitializer
{...itemConfig}
componentType={'Calendar'}
icon={<FormOutlined />}
onCreateBlockSchema={async (options) => {
if (createBlockSchema) {
return createBlockSchema(options);
}
createGanttBlock(options);
}}
onlyCurrentDataSource={onlyCurrentDataSource}
hideSearch={hideSearch}
filter={filterCollections}
showAssociationFields={showAssociationFields}
/>
);
};
export const useCreateGanttBlock = () => {
const { insert } = useSchemaInitializer();
const { t } = useTranslation();
const { getCollectionFields } = useCollectionManager_deprecated();
const options = useContext(SchemaOptionsContext);
const { theme } = useGlobalTheme();
const itemConfig = useSchemaInitializerItem();
return (
<DataBlockInitializer
{...itemConfig}
componentType={'Gantt'}
icon={<FormOutlined />}
onCreateBlockSchema={async ({ item }) => {
const createGanttBlock = async ({ item }) => {
const collectionFields = getCollectionFields(item.name, item.dataSource);
const stringFields = collectionFields
?.filter((field) => field.type === 'string')
@ -137,7 +168,120 @@ export const GanttBlockInitializer = () => {
},
}),
);
};
return { createGanttBlock };
};
export function useCreateAssociationGanttBlock() {
const { insert } = useSchemaInitializer();
const { t } = useTranslation();
const options = useContext(SchemaOptionsContext);
const { theme } = useGlobalTheme();
const { getCollectionFields } = useCollectionManager_deprecated();
const createAssociationGanttBlock = async ({ item }) => {
const field = item.associationField;
const collectionFields = getCollectionFields(item.name, item.dataSource);
const stringFields = collectionFields
?.filter((field) => field.type === 'string')
?.map((field) => {
return {
label: field?.uiSchema?.title,
value: field.name,
};
});
const dateFields = collectionFields
?.filter((field) => field.type === 'date')
?.map((field) => {
return {
label: field?.uiSchema?.title,
value: field.name,
};
});
const numberFields = collectionFields
?.filter((field) => field.type === 'float')
?.map((field) => {
return {
label: field?.uiSchema?.title,
value: field.name,
};
});
const values = await FormDialog(
t('Create gantt block'),
() => {
return (
<SchemaComponentOptions scope={options.scope} components={{ ...options.components }}>
<FormLayout layout={'vertical'}>
<SchemaComponent
schema={{
properties: {
title: {
title: t('Title field'),
enum: stringFields,
required: true,
'x-component': 'Select',
'x-decorator': 'FormItem',
},
start: {
title: t('Start date field'),
enum: dateFields,
required: true,
default: 'createdAt',
'x-component': 'Select',
'x-decorator': 'FormItem',
},
end: {
title: t('End date field'),
enum: dateFields,
required: true,
'x-component': 'Select',
'x-decorator': 'FormItem',
},
progress: {
title: t('Progress field'),
enum: numberFields,
'x-component': 'Select',
'x-decorator': 'FormItem',
},
range: {
title: t('Time scale'),
enum: [
{ label: '{{t("Hour")}}', value: 'hour', color: 'orange' },
{ label: '{{t("Quarter of day")}}', value: 'quarterDay', color: 'default' },
{ label: '{{t("Half of day")}}', value: 'halfDay', color: 'blue' },
{ label: '{{t("Day")}}', value: 'day', color: 'yellow' },
{ label: '{{t("Week")}}', value: 'week', color: 'pule' },
{ label: '{{t("Month")}}', value: 'month', color: 'green' },
{ label: '{{t("Year")}}', value: 'year', color: 'green' },
{ label: '{{t("QuarterYear")}}', value: 'quarterYear', color: 'red' },
],
default: 'day',
'x-component': 'Select',
'x-decorator': 'FormItem',
},
},
}}
/>
</FormLayout>
</SchemaComponentOptions>
);
},
theme,
).open({
initialValues: {},
});
insert(
createGanttBlockUISchema({
association: `${field.collectionName}.${field.name}`,
dataSource: item.dataSource,
fieldNames: {
...values,
},
}),
);
};
return { createAssociationGanttBlock };
}

View File

@ -12,7 +12,6 @@ import React, { createContext, useContext, useEffect, useState } from 'react';
import {
useACLRoleContext,
useCollection_deprecated,
BlockProvider,
useBlockRequestContext,
TableBlockProvider,
useTableBlockContext,
@ -95,11 +94,9 @@ export const GanttBlockProvider = (props) => {
return (
<div aria-label="block-item-gantt" role="button">
<BlockProvider name="gantt" {...props} params={params}>
<TableBlockProvider {...props} params={params}>
<InternalGanttBlockProvider {...props} />
</TableBlockProvider>
</BlockProvider>
</div>
);
};
@ -114,7 +111,7 @@ export const useGanttBlockProps = () => {
const { getPrimaryKey, name, template, writableView } = useCollection_deprecated();
const { parseAction } = useACLRoleContext();
const ctxBlock = useTableBlockContext();
const [loading, setLoading] = useState(false);
const primaryKey = getPrimaryKey();
const checkPermission = (record) => {
const actionPath = `${name}:update`;
@ -136,6 +133,7 @@ export const useGanttBlockProps = () => {
ctx.field.data = data;
};
useEffect(() => {
setLoading(true);
if (!ctx?.service?.loading) {
const data = formatData(
ctx.service.data?.data,
@ -147,6 +145,7 @@ export const useGanttBlockProps = () => {
primaryKey,
);
setTasks(data);
setLoading(false);
ctx.field.data = data;
if (tasks.length > 0) {
ctxBlock.setExpandFlag(true);
@ -159,5 +158,6 @@ export const useGanttBlockProps = () => {
onExpanderClick,
expandAndCollapseAll,
tasks,
loading,
};
};

View File

@ -24,7 +24,7 @@ import {
withDynamicSchemaProps,
useDesignable,
} from '@nocobase/client';
import { message } from 'antd';
import { message, Spin } from 'antd';
import { debounce } from 'lodash';
import React, { SyntheticEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
@ -143,10 +143,11 @@ export const Gantt: any = withDynamicSchemaProps((props: any) => {
tasks,
expandAndCollapseAll,
fieldNames,
loading,
} = useProps(props); // 新版 UISchema1.0 之后)中已经废弃了 useProps这里之所以继续保留是为了兼容旧版的 UISchema
const { designable } = useDesignable();
const headerHeight = currentTheme.includes('compact') ? 45 : designable ? 65 : 55;
const rowHeight = currentTheme.includes('compact') ? 45 : 65;
const headerHeight = currentTheme?.includes('compact') ? 45 : designable ? 65 : 55;
const rowHeight = currentTheme?.includes('compact') ? 45 : 65;
const ctx = useGanttBlockContext();
const appInfo = useCurrentAppInfo();
const { t } = useTranslation();
@ -158,6 +159,7 @@ export const Gantt: any = withDynamicSchemaProps((props: any) => {
const wrapperRef = useRef<HTMLDivElement>(null);
const taskListRef = useRef<HTMLDivElement>(null);
const verticalGanttContainerRef = useRef<HTMLDivElement>(null);
const ganttRef = useRef<HTMLDivElement>(null);
const [dateSetup, setDateSetup] = useState<DateSetup>(() => {
const [startDate, endDate] = ganttDateRange(tasks, viewMode, preStepsCount);
return { viewMode, dates: seedDates(startDate, endDate, viewMode) };
@ -521,6 +523,7 @@ export const Gantt: any = withDynamicSchemaProps((props: any) => {
onDoubleClick,
onClick: handleBarClick,
onDelete,
loading,
};
return (
@ -536,6 +539,7 @@ export const Gantt: any = withDynamicSchemaProps((props: any) => {
height: ${headerHeight}px;
}
`)}
ref={ganttRef}
>
<GanttRecordViewer visible={visible} setVisible={setVisible} record={record} />
<RecursionField name={'anctionBar'} schema={fieldSchema.properties.toolBar} />
@ -576,6 +580,7 @@ export const Gantt: any = withDynamicSchemaProps((props: any) => {
onScroll={handleScrollY}
rtl={rtl}
/>
<Spin spinning={loading} style={{ visibility: 'hidden' }}>
<HorizontalScroll
svgWidth={svgWidth}
taskListWidth={taskListWidth}
@ -583,6 +588,7 @@ export const Gantt: any = withDynamicSchemaProps((props: any) => {
rtl={rtl}
onScroll={handleScrollX}
/>
</Spin>
</div>
</div>
);

View File

@ -37,6 +37,7 @@ export type TaskGanttContentProps = {
setGanttEvent: (value: GanttEvent) => void;
setFailedTask: (value: BarTask | null) => void;
setSelectedTask: (taskId: string) => void;
loading?: boolean;
} & EventOption;
export const TaskGanttContent: React.FC<TaskGanttContentProps> = ({

View File

@ -8,6 +8,7 @@
*/
import React, { forwardRef, useEffect, useRef } from 'react';
import { Spin } from 'antd';
import { Calendar, CalendarProps } from '../calendar/calendar';
import { Grid, GridProps } from '../grid/grid';
import { TaskGanttContent, TaskGanttContentProps } from './task-gantt-content';
@ -28,7 +29,6 @@ export const TaskGantt: React.FC<TaskGanttProps> = forwardRef(
const horizontalContainerRef = useRef<HTMLDivElement>(null);
const newBarProps = { ...barProps, svg: ganttSVGRef };
const { styles } = useStyles();
useEffect(() => {
if (horizontalContainerRef.current) {
horizontalContainerRef.current.scrollTop = scrollY;
@ -40,7 +40,6 @@ export const TaskGantt: React.FC<TaskGanttProps> = forwardRef(
ref.current.scrollLeft = scrollX;
}
}, [scrollX]);
return (
<div className={styles.ganttverticalcontainer} ref={ref} dir="ltr">
<svg
@ -52,6 +51,7 @@ export const TaskGantt: React.FC<TaskGanttProps> = forwardRef(
>
<Calendar {...calendarProps} />
</svg>
<Spin spinning={barProps?.loading}>
<div
ref={horizontalContainerRef}
className={styles.horizontalcontainer}
@ -60,7 +60,7 @@ export const TaskGantt: React.FC<TaskGanttProps> = forwardRef(
<svg
xmlns="http://www.w3.org/2000/svg"
width={gridProps.svgWidth}
height={barProps.rowHeight * (barProps.tasks.length || 3)}
height={barProps.rowHeight * barProps.tasks.length || 166}
fontFamily={barProps.fontFamily}
ref={ganttSVGRef}
className="ganttBody"
@ -69,6 +69,7 @@ export const TaskGantt: React.FC<TaskGanttProps> = forwardRef(
<TaskGanttContent {...newBarProps} />
</svg>
</div>
</Spin>
</div>
);
},

View File

@ -11,15 +11,16 @@ import { ISchema } from '@formily/react';
import { uid } from '@formily/shared';
export const createGanttBlockUISchema = (options: {
collectionName: string;
fieldNames: object;
dataSource: string;
association?: string;
collectionName?: string;
}): ISchema => {
const { collectionName, fieldNames, dataSource } = options;
const { collectionName, fieldNames, dataSource, association } = options;
return {
const schema = {
type: 'void',
'x-acl-action': `${collectionName}:list`,
'x-acl-action': `${association || collectionName}:list`,
'x-decorator': 'GanttBlockProvider',
'x-decorator-props': {
collection: collectionName,
@ -135,4 +136,9 @@ export const createGanttBlockUISchema = (options: {
},
},
};
if (association) {
schema['x-decorator-props']['association'] = association;
}
return schema;
};

View File

@ -17,6 +17,7 @@ import { GanttBlockProvider, useGanttBlockProps } from './GanttBlockProvider';
import { Event } from './components/gantt/Event';
import { Gantt } from './components/gantt/gantt';
import { ViewMode } from './types/public-types';
import { useCreateAssociationGanttBlock, useCreateGanttBlock } from './GanttBlockInitializer';
Gantt.ActionBar = ActionBar;
Gantt.ViewMode = ViewMode;
@ -48,7 +49,32 @@ export class PluginGanttClient extends Plugin {
title: "{{t('Gantt')}}",
Component: 'GanttBlockInitializer',
});
this.app.schemaInitializerManager.addItem('popup:common:addBlock', 'dataBlocks.gantt', {
title: "{{t('Gantt')}}",
Component: 'GanttBlockInitializer',
useComponentProps() {
const { createAssociationGanttBlock } = useCreateAssociationGanttBlock();
const { createGanttBlock } = useCreateGanttBlock();
return {
onlyCurrentDataSource: true,
filterCollections({ associationField }) {
if (associationField) {
return ['hasMany', 'belongsToMany'].includes(associationField.type);
}
return false;
},
createBlockSchema: ({ item, fromOthersInPopup }) => {
if (fromOthersInPopup) {
return createGanttBlock({ item });
}
createAssociationGanttBlock({ item });
},
showAssociationFields: true,
hideSearch: true,
};
},
});
this.app.addScopes({
useGanttBlockProps,
});

View File

@ -24,6 +24,8 @@ import {
useSchemaInitializer,
useSchemaInitializerItem,
useAPIClient,
Collection,
CollectionFieldOptions,
} from '@nocobase/client';
import { createKanbanBlockUISchema } from './createKanbanBlockUISchema';
import { CreateAndSelectSort } from './CreateAndSelectSort';
@ -85,7 +87,7 @@ const CreateKanbanForm = ({ item, sortFields, collectionFields, fields, options,
field.groupField = field.form.values?.groupField;
field.setComponentProps({
dataSource: item.dataSource,
collectionName: item.name,
collectionName: item.collectionName || item.name,
collectionFields,
sortFields: sortFields,
});
@ -109,23 +111,119 @@ const CreateKanbanForm = ({ item, sortFields, collectionFields, fields, options,
);
};
export const KanbanBlockInitializer = () => {
const { insert } = useSchemaInitializer();
const { t } = useTranslation();
const { getCollectionFields } = useCollectionManager_deprecated();
const { theme } = useGlobalTheme();
export const KanbanBlockInitializer = ({
filterCollections,
onlyCurrentDataSource,
hideSearch,
createBlockSchema,
showAssociationFields,
}: {
filterCollections: (options: { collection?: Collection; associationField?: CollectionFieldOptions }) => boolean;
onlyCurrentDataSource: boolean;
hideSearch?: boolean;
createBlockSchema?: (options: any) => any;
showAssociationFields?: boolean;
}) => {
const itemConfig = useSchemaInitializerItem();
const options = useContext(SchemaOptionsContext);
const api = useAPIClient();
const { createKanbanBlock } = useCreateKanbanBlock();
return (
<DataBlockInitializer
{...itemConfig}
componentType={'Kanban'}
componentType={'Calendar'}
icon={<FormOutlined />}
onCreateBlockSchema={async ({ item }) => {
const { data } = await api.resource('collections.fields', item.name).list({ paginate: false });
const targetFields = getCollectionFields(item.name, item.dataSource);
const collectionFields = item.dataSource === 'main' ? data.data : targetFields;
onCreateBlockSchema={async (options) => {
if (createBlockSchema) {
return createBlockSchema(options);
}
createKanbanBlock(options);
}}
onlyCurrentDataSource={onlyCurrentDataSource}
hideSearch={hideSearch}
filter={filterCollections}
showAssociationFields={showAssociationFields}
/>
);
};
export const useCreateKanbanBlock = () => {
const { insert } = useSchemaInitializer();
const { t } = useTranslation();
const { getCollectionFields } = useCollectionManager_deprecated();
const options = useContext(SchemaOptionsContext);
const { theme } = useGlobalTheme();
const api = useAPIClient();
const createKanbanBlock = async ({ item }) => {
console.log(item);
const collectionFields = getCollectionFields(item.name, item.dataSource);
const fields = collectionFields
?.filter((field) => ['select', 'radioGroup'].includes(field.interface))
?.map((field) => {
return {
label: field?.uiSchema?.title,
value: field.name,
uiSchema: {
...field.uiSchema,
name: field.name,
},
};
});
const sortFields = collectionFields
?.filter((field) => ['sort'].includes(field.interface))
?.map((field) => {
return {
label: field?.uiSchema?.title,
value: field.name,
scopeKey: field.scopeKey,
uiSchema: {
...field.uiSchema,
name: field.name,
},
};
});
const values = await FormDialog(
t('Create kanban block'),
<CreateKanbanForm
item={item}
sortFields={sortFields}
collectionFields={collectionFields}
fields={fields}
options={options}
api={api}
/>,
theme,
).open({
initialValues: {},
});
insert(
createKanbanBlockUISchema({
sortField: values.dragSortBy,
groupField: values.groupField.value,
collectionName: item.name,
dataSource: item.dataSource,
params: {
sort: [values.dragSortBy],
},
}),
);
};
return { createKanbanBlock };
};
export function useCreateAssociationKanbanBlock() {
const { insert } = useSchemaInitializer();
const { t } = useTranslation();
const options = useContext(SchemaOptionsContext);
const { theme } = useGlobalTheme();
const { getCollectionFields } = useCollectionManager_deprecated();
const api = useAPIClient();
const createAssociationKanbanBlock = async ({ item }) => {
console.log(item);
const field = item.associationField;
const collectionFields = getCollectionFields(item.name, item.dataSource);
const fields = collectionFields
?.filter((field) => ['select', 'radioGroup'].includes(field.interface))
?.map((field) => {
@ -169,14 +267,14 @@ export const KanbanBlockInitializer = () => {
createKanbanBlockUISchema({
sortField: values.dragSortBy,
groupField: values.groupField.value,
collectionName: item.name,
association: `${field.collectionName}.${field.name}`,
dataSource: item.dataSource,
params: {
sort: [values.dragSortBy],
},
}),
);
}}
/>
);
};
return { createAssociationKanbanBlock };
}

View File

@ -120,7 +120,8 @@ const useAssociationNames = (collection) => {
export const KanbanBlockProvider = (props) => {
const params = { ...props.params };
const appends = useAssociationNames(props.collection);
console.log(props);
const appends = useAssociationNames(props.association || props.collection);
if (!Object.keys(params).includes('appends')) {
params['appends'] = appends;
}

View File

@ -11,20 +11,22 @@ import { ISchema } from '@formily/react';
import { uid } from '@formily/shared';
export const createKanbanBlockUISchema = (options: {
collectionName: string;
groupField: string;
sortField: string;
dataSource: string;
params?: Record<string, any>;
collectionName?: string;
association?: string;
}): ISchema => {
const { collectionName, groupField, sortField, dataSource, params } = options;
const { collectionName, groupField, sortField, dataSource, params, association } = options;
return {
const schema = {
type: 'void',
'x-acl-action': `${collectionName}:list`,
'x-acl-action': `${association || collectionName}:list`,
'x-decorator': 'KanbanBlockProvider',
'x-decorator-props': {
collection: collectionName,
dataSource,
action: 'list',
groupField,
sortField,
@ -32,7 +34,6 @@ export const createKanbanBlockUISchema = (options: {
paginate: false,
...params,
},
dataSource,
},
// 'x-designer': 'Kanban.Designer',
'x-toolbar': 'BlockSchemaToolbar',
@ -122,4 +123,8 @@ export const createKanbanBlockUISchema = (options: {
},
},
};
if (association) {
schema['x-decorator-props']['association'] = association;
}
return schema;
};

View File

@ -16,7 +16,11 @@ import { KanbanCardViewer } from './Kanban.CardViewer';
import { KanbanDesigner } from './Kanban.Designer';
import { kanbanSettings } from './Kanban.Settings';
import { kanbanActionInitializers, kanbanActionInitializers_deprecated } from './KanbanActionInitializers';
import { KanbanBlockInitializer } from './KanbanBlockInitializer';
import {
KanbanBlockInitializer,
useCreateAssociationKanbanBlock,
useCreateKanbanBlock,
} from './KanbanBlockInitializer';
import { KanbanBlockProvider, useKanbanBlockProps } from './KanbanBlockProvider';
Kanban.Card = KanbanCard;
@ -53,6 +57,32 @@ class PluginKanbanClient extends Plugin {
title: '{{t("Kanban")}}',
Component: 'KanbanBlockInitializer',
});
this.app.schemaInitializerManager.addItem('popup:common:addBlock', 'dataBlocks.kanban', {
title: '{{t("Kanban")}}',
Component: 'KanbanBlockInitializer',
useComponentProps() {
const { createAssociationKanbanBlock } = useCreateAssociationKanbanBlock();
const { createKanbanBlock } = useCreateKanbanBlock();
return {
onlyCurrentDataSource: true,
filterCollections({ associationField }) {
if (associationField) {
return ['hasMany', 'belongsToMany'].includes(associationField.type);
}
return false;
},
createBlockSchema: ({ item, fromOthersInPopup }) => {
if (fromOthersInPopup) {
return createKanbanBlock({ item });
}
createAssociationKanbanBlock({ item });
},
showAssociationFields: true,
hideSearch: true,
};
},
});
}
}