feat: gantt block (#1393)
* feat: add Gantt block * feat: gantt block congiure action * feat: gantt block congiure fields * feat: gantt block congiure fields * feat: gantt block congiure fields * feat: gantt block code improve * feat: gantt block code improve * feat: gantt block code improve * feat: gantt block time range * feat: gantt block date change * feat: gantt block progress change * feat: gantt block columnWidth fix * feat: gantt block action configure * feat: gant block init fix * feat: gant block table configure * feat: gant block table configure * feat: gant block table configure * feat: gant expend when table expend * feat: modulecss ->less * feat: style improve * feat: style module -> emotion css * feat: gantt process handle * feat: gantt block empty state placeholder * feat: table actions suport config * feat: gantt block suprt time range change * feat: gantt block suprt process update * feat: gantt block suprt process update * feat: gantt block suprt process update * feat: gantt block suprt process update * feat: gantt block configure actions * feat: gantt block style improve * feat: gantt table actions column is configured by default * feat: gantt block surport bulk update * feat: gantt block surport bulk update&bulk edit * feat: gantt block action configure fix * feat: gant block code improve * feat: do not display gantt bar when the start date is empty * feat: gant configure actions * feat: gant task change * feat: gant calendar style improve * style: gant local * style: gant grid style improve * style: gant calendar style improve * style: gant style improve * refactor: gant bar suport click * feat: adjacency list * fix: error * feat: collection extender * Revert "feat: collection extender" This reverts commit a942eee7699724fe831a98bae3f67a42db91a28b. * fix: registerBuiltInListeners * chore: getAllNodeIds # Conflicts: # packages/plugins/acl/src/server.ts * fix: get list data tree ids # Conflicts: # packages/plugins/acl/src/__tests__/list-action.test.ts * feat: suport add child * feat: demo3 * feat: suport add child * feat: support add child * feat: support add child * fix: tree true * feat: suport expend and collapse * feat: support expend and collapse * feat: support expend and collapse * feat: table block of the selector supports tree table * feat: expand and collapse are only displayed in the tree table * fix: when the tree table is closed and opened, it needs to be refreshed to take effect * fix: test * refactor: add child is hidden when the tree table is closed * refactor: tree table filter the children field * refactor: tree table filter the children field * refactor: tree table filter the children field * refactor: expand and collapse button does not support modify name and icon * refactor: parent cannot be modified when adding child * refactor: expand and collapse button hide when treetable is closed * refactor: expand and collapse button hide when treetable is closed * refactor: expand and collapse button hide when treetable is closed * refactor: expand and collapse button hide when treetable is closed * refactor: expand and collapse button hide when treetable is closed * refactor: expand and collapse button hide when treetable is closed * refactor: expand and collapse button hide when treetable is closed * refactor: expand and collapse button hide when treetable is closed * refactor: filter out the node itself when selecting parent * refactor: filter out the node itself when selecting parent * refactor: tree collection locale * refactor: tree collection locale * feat: gantt block support tree collection * fix: parameter exception when creating data selector * feat: gantt block support tree collection * feat: gantt block support tree collection * fix: tableActionColumnInitializers * style: style improve * fix: gantt expansion exception * style: style improve * style: style improve * style: calendar style improve * style: style improve * style: code improve * refactor: gantt chart also highlights when table rows are selected * fix: time scale default value * fix: end field is nor requried to set * fix: detailed blocks not appear when child record is clicked * fix: detailed blocks not appear when child record is clicked * refactor: gantt default expand all * feat: gantt adapt to compact themes * feat: gantt adapt to compact themes * refactor: style improve * refactor: style improve * refactor: style improve * refactor: style improve * refactor: style improve * refactor: style improve * refactor: style improve * refactor: sort by start date * refactor: style improve * refactor: style improve * fix: styling * fix: font-family * fix: font-weight: 400 * feat: custom color * fix: styling * fix: style * refactor: style improve * refactor: style improve --------- Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
59865248f0
commit
1a933315e4
@ -5,17 +5,19 @@ import { CalendarBlockProvider, useCalendarBlockProps } from './CalendarBlockPro
|
|||||||
import { DetailsBlockProvider, useDetailsBlockProps } from './DetailsBlockProvider';
|
import { DetailsBlockProvider, useDetailsBlockProps } from './DetailsBlockProvider';
|
||||||
import { FilterFormBlockProvider } from './FilterFormBlockProvider';
|
import { FilterFormBlockProvider } from './FilterFormBlockProvider';
|
||||||
import { FormBlockProvider, useFormBlockProps } from './FormBlockProvider';
|
import { FormBlockProvider, useFormBlockProps } from './FormBlockProvider';
|
||||||
import { FormFieldProvider, useFormFieldProps } from './FormFieldProvider';
|
|
||||||
import * as bp from './hooks';
|
import * as bp from './hooks';
|
||||||
import { KanbanBlockProvider, useKanbanBlockProps } from './KanbanBlockProvider';
|
import { KanbanBlockProvider, useKanbanBlockProps } from './KanbanBlockProvider';
|
||||||
import { TableBlockProvider, useTableBlockProps } from './TableBlockProvider';
|
import { TableBlockProvider, useTableBlockProps } from './TableBlockProvider';
|
||||||
import { TableFieldProvider, useTableFieldProps } from './TableFieldProvider';
|
import { TableFieldProvider, useTableFieldProps } from './TableFieldProvider';
|
||||||
import { TableSelectorProvider, useTableSelectorProps } from './TableSelectorProvider';
|
import { TableSelectorProvider, useTableSelectorProps } from './TableSelectorProvider';
|
||||||
|
import { FormFieldProvider, useFormFieldProps } from './FormFieldProvider';
|
||||||
|
import { GanttBlockProvider, useGanttBlockProps } from './GanttBlockProvider';
|
||||||
|
|
||||||
export const BlockSchemaComponentProvider: React.FC = (props) => {
|
export const BlockSchemaComponentProvider: React.FC = (props) => {
|
||||||
return (
|
return (
|
||||||
<SchemaComponentOptions
|
<SchemaComponentOptions
|
||||||
components={{
|
components={{
|
||||||
|
GanttBlockProvider,
|
||||||
CalendarBlockProvider,
|
CalendarBlockProvider,
|
||||||
TableFieldProvider,
|
TableFieldProvider,
|
||||||
TableBlockProvider,
|
TableBlockProvider,
|
||||||
@ -40,6 +42,7 @@ export const BlockSchemaComponentProvider: React.FC = (props) => {
|
|||||||
useTableBlockProps,
|
useTableBlockProps,
|
||||||
useTableSelectorProps,
|
useTableSelectorProps,
|
||||||
useKanbanBlockProps,
|
useKanbanBlockProps,
|
||||||
|
useGanttBlockProps,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
108
packages/core/client/src/block-provider/GanttBlockProvider.tsx
Normal file
108
packages/core/client/src/block-provider/GanttBlockProvider.tsx
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import { useField } from '@formily/react';
|
||||||
|
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||||
|
import { BlockProvider, useBlockRequestContext } from './BlockProvider';
|
||||||
|
import { TableBlockProvider } from './TableBlockProvider';
|
||||||
|
|
||||||
|
export const GanttBlockContext = createContext<any>({});
|
||||||
|
|
||||||
|
const formatData = (
|
||||||
|
data = [],
|
||||||
|
fieldNames,
|
||||||
|
tasks: any[] = [],
|
||||||
|
projectId: any = undefined,
|
||||||
|
hideChildren: boolean = false,
|
||||||
|
) => {
|
||||||
|
data.forEach((item: any) => {
|
||||||
|
if (item.children && item.children.length) {
|
||||||
|
tasks.push({
|
||||||
|
start: new Date(item[fieldNames.start]),
|
||||||
|
end: new Date(item[fieldNames.end]),
|
||||||
|
name: item[fieldNames.title],
|
||||||
|
id: item.id + '',
|
||||||
|
type: 'project',
|
||||||
|
progress: item[fieldNames.progress] * 100 || 0,
|
||||||
|
hideChildren: hideChildren,
|
||||||
|
project: projectId,
|
||||||
|
color: item.color,
|
||||||
|
});
|
||||||
|
formatData(item.children, fieldNames, tasks, item.id + '', hideChildren);
|
||||||
|
} else {
|
||||||
|
tasks.push({
|
||||||
|
start: item[fieldNames.start] ? new Date(item[fieldNames.start]) : undefined,
|
||||||
|
end: new Date(item[fieldNames.end] || item[fieldNames.start]),
|
||||||
|
name: item[fieldNames.title],
|
||||||
|
id: item.id + '',
|
||||||
|
type: fieldNames.end ? 'task' : 'milestone',
|
||||||
|
progress: item[fieldNames.progress] * 100 || 0,
|
||||||
|
project: projectId,
|
||||||
|
color: item.color,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return tasks;
|
||||||
|
};
|
||||||
|
const InternalGanttBlockProvider = (props) => {
|
||||||
|
const { fieldNames, timeRange, resource } = props;
|
||||||
|
const field = useField();
|
||||||
|
const { service } = useBlockRequestContext();
|
||||||
|
// if (service.loading) {
|
||||||
|
// return <Spin />;
|
||||||
|
// }
|
||||||
|
return (
|
||||||
|
<GanttBlockContext.Provider
|
||||||
|
value={{
|
||||||
|
field,
|
||||||
|
service,
|
||||||
|
resource,
|
||||||
|
fieldNames,
|
||||||
|
timeRange,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</GanttBlockContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GanttBlockProvider = (props) => {
|
||||||
|
return (
|
||||||
|
<BlockProvider {...props} params={{ tree:true, paginate: false, sort: props.fieldNames.start }}>
|
||||||
|
<TableBlockProvider {...props} params={{ tree:true, paginate: false, sort: props.fieldNames.start }}>
|
||||||
|
<InternalGanttBlockProvider {...props} />
|
||||||
|
</TableBlockProvider>
|
||||||
|
</BlockProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGanttBlockContext = () => {
|
||||||
|
return useContext(GanttBlockContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGanttBlockProps = () => {
|
||||||
|
const ctx = useGanttBlockContext();
|
||||||
|
const [tasks, setTasks] = useState<any>([]);
|
||||||
|
const onExpanderClick = (task: any) => {
|
||||||
|
const data = ctx.field.data;
|
||||||
|
const tasksData = data.map((t: any) => (t.id === task.id ? task : t));
|
||||||
|
setTasks(tasksData);
|
||||||
|
ctx.field.data = tasksData;
|
||||||
|
};
|
||||||
|
const expandAndCollapseAll = (flag) => {
|
||||||
|
const data = formatData(ctx.service.data?.data, ctx.fieldNames, [], undefined, flag);
|
||||||
|
setTasks(data);
|
||||||
|
ctx.field.data = data;
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ctx?.service?.loading) {
|
||||||
|
const data = formatData(ctx.service.data?.data, ctx.fieldNames);
|
||||||
|
setTasks(data);
|
||||||
|
ctx.field.data = data;
|
||||||
|
}
|
||||||
|
}, [ctx?.service?.loading]);
|
||||||
|
return {
|
||||||
|
fieldNames: ctx.fieldNames,
|
||||||
|
timeRange: ctx.timeRange,
|
||||||
|
onExpanderClick,
|
||||||
|
expandAndCollapseAll,
|
||||||
|
tasks,
|
||||||
|
};
|
||||||
|
};
|
@ -21,7 +21,7 @@ interface Props {
|
|||||||
|
|
||||||
const InternalTableBlockProvider = (props: Props) => {
|
const InternalTableBlockProvider = (props: Props) => {
|
||||||
const { params, showIndex, dragSort, rowKey, childrenColumnName } = props;
|
const { params, showIndex, dragSort, rowKey, childrenColumnName } = props;
|
||||||
const field = useField();
|
const field: any = useField();
|
||||||
const { resource, service } = useBlockRequestContext();
|
const { resource, service } = useBlockRequestContext();
|
||||||
const [expandFlag, setExpandFlag] = useState(false);
|
const [expandFlag, setExpandFlag] = useState(false);
|
||||||
return (
|
return (
|
||||||
@ -66,6 +66,9 @@ export const useAssociationNames = (collection) => {
|
|||||||
if (schema['x-component'] === 'TableV2') {
|
if (schema['x-component'] === 'TableV2') {
|
||||||
return schema;
|
return schema;
|
||||||
}
|
}
|
||||||
|
if (schema['x-component'] === 'Gantt') {
|
||||||
|
return schema.properties?.table;
|
||||||
|
}
|
||||||
return buf;
|
return buf;
|
||||||
}, new Schema({}));
|
}, new Schema({}));
|
||||||
return uniq(
|
return uniq(
|
||||||
@ -174,6 +177,7 @@ export const useTableBlockProps = () => {
|
|||||||
console.log(selectedRowKeys);
|
console.log(selectedRowKeys);
|
||||||
ctx.field.data = ctx?.field?.data || {};
|
ctx.field.data = ctx?.field?.data || {};
|
||||||
ctx.field.data.selectedRowKeys = selectedRowKeys;
|
ctx.field.data.selectedRowKeys = selectedRowKeys;
|
||||||
|
ctx?.field?.onRowSelect?.(selectedRowKeys);
|
||||||
},
|
},
|
||||||
async onRowDragEnd({ from, to }) {
|
async onRowDragEnd({ from, to }) {
|
||||||
await ctx.resource.move({
|
await ctx.resource.move({
|
||||||
@ -244,5 +248,8 @@ export const useTableBlockProps = () => {
|
|||||||
// 更新表格的选中状态
|
// 更新表格的选中状态
|
||||||
setSelectedRow((prev) => (prev?.includes(record[ctx.rowKey]) ? [] : [...value]));
|
setSelectedRow((prev) => (prev?.includes(record[ctx.rowKey]) ? [] : [...value]));
|
||||||
},
|
},
|
||||||
|
onExpand(expanded, record) {
|
||||||
|
ctx?.field.onExpandClick?.(expanded, record);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -775,6 +775,8 @@ export const useRefreshActionProps = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const useDetailsPaginationProps = () => {
|
export const useDetailsPaginationProps = () => {
|
||||||
const ctx = useDetailsBlockContext();
|
const ctx = useDetailsBlockContext();
|
||||||
const count = ctx.service?.data?.meta?.count || 0;
|
const count = ctx.service?.data?.meta?.count || 0;
|
||||||
|
@ -3,10 +3,11 @@ export * from './BlockSchemaComponentProvider';
|
|||||||
export * from './CalendarBlockProvider';
|
export * from './CalendarBlockProvider';
|
||||||
export * from './FilterFormBlockProvider';
|
export * from './FilterFormBlockProvider';
|
||||||
export * from './FormBlockProvider';
|
export * from './FormBlockProvider';
|
||||||
export * from './FormFieldProvider';
|
|
||||||
export * from './KanbanBlockProvider';
|
export * from './KanbanBlockProvider';
|
||||||
export * from './SharedFilterProvider';
|
export * from './SharedFilterProvider';
|
||||||
export * from './TableBlockProvider';
|
export * from './TableBlockProvider';
|
||||||
export * from './TableFieldProvider';
|
export * from './TableFieldProvider';
|
||||||
export * from './TableSelectorProvider';
|
export * from './TableSelectorProvider';
|
||||||
|
export * from './FormFieldProvider';
|
||||||
|
export * from './GanttBlockProvider'
|
||||||
|
export * from './SharedFilterProvider';
|
||||||
|
@ -112,6 +112,15 @@ export default {
|
|||||||
"Delete this event?": "Delete this event?",
|
"Delete this event?": "Delete this event?",
|
||||||
"Delete Event": "Delete Event",
|
"Delete Event": "Delete Event",
|
||||||
"Kanban": "Kanban",
|
"Kanban": "Kanban",
|
||||||
|
"Gantt":"Gantt",
|
||||||
|
"Create gantt block":"Create gantt block",
|
||||||
|
"Progress field":"Progress field",
|
||||||
|
"Time scale":"Time scale",
|
||||||
|
"Hour":"Hour",
|
||||||
|
"Quarter of day":"Quarter of day",
|
||||||
|
"Half of day":"Half of day",
|
||||||
|
"Year":"Year",
|
||||||
|
"QuarterYear":"QuarterYear",
|
||||||
"Select grouping field": "Select grouping field",
|
"Select grouping field": "Select grouping field",
|
||||||
"Media": "Media",
|
"Media": "Media",
|
||||||
"Markdown": "Markdown",
|
"Markdown": "Markdown",
|
||||||
|
@ -109,6 +109,15 @@ export default {
|
|||||||
"Select data source": "データソースを選択",
|
"Select data source": "データソースを選択",
|
||||||
"Calendar": "カレンダー",
|
"Calendar": "カレンダー",
|
||||||
"Kanban": "かんばん",
|
"Kanban": "かんばん",
|
||||||
|
"Gantt":"ガント図",
|
||||||
|
"Create gantt block":"ガントチャートブロックの作成",
|
||||||
|
"Progress field":"進捗フィールド",
|
||||||
|
"Time scale":"時間スケールレベル",
|
||||||
|
"Hour":"時間",
|
||||||
|
"Quarter of day":"四分の一日",
|
||||||
|
"Half of day":"半日",
|
||||||
|
"Year":"年",
|
||||||
|
"QuarterYear":"四半期",
|
||||||
"Select grouping field": "グループフィールドを選択してください",
|
"Select grouping field": "グループフィールドを選択してください",
|
||||||
"Media": "メディア",
|
"Media": "メディア",
|
||||||
"Markdown": "マークダウン",
|
"Markdown": "マークダウン",
|
||||||
|
@ -125,6 +125,15 @@ export default {
|
|||||||
'Delete this event?': '是否删除这个日程?',
|
'Delete this event?': '是否删除这个日程?',
|
||||||
'Delete Event': '删除日程',
|
'Delete Event': '删除日程',
|
||||||
"Kanban": "看板",
|
"Kanban": "看板",
|
||||||
|
"Gantt":"甘特图",
|
||||||
|
"Create gantt block":"创建甘特图区块",
|
||||||
|
"Progress field":"进度字段",
|
||||||
|
"Time scale":"时间缩放等级",
|
||||||
|
"Hour":"小时",
|
||||||
|
"Quarter of day":"四分之一天",
|
||||||
|
"Half of day":"半天",
|
||||||
|
"Year":"年",
|
||||||
|
"QuarterYear":"季度",
|
||||||
"Select grouping field": "选择分组字段",
|
"Select grouping field": "选择分组字段",
|
||||||
"Media": "多媒体",
|
"Media": "多媒体",
|
||||||
"Markdown": "Markdown",
|
"Markdown": "Markdown",
|
||||||
|
@ -0,0 +1,196 @@
|
|||||||
|
import { ISchema, useField, useFieldSchema } from '@formily/react';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useCompile, useDesignable } from '../..';
|
||||||
|
import { useGanttBlockContext } from '../../../block-provider';
|
||||||
|
import { useCollection } from '../../../collection-manager';
|
||||||
|
import { useCollectionFilterOptions } from '../../../collection-manager/action-hooks';
|
||||||
|
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||||
|
import { useSchemaTemplate } from '../../../schema-templates';
|
||||||
|
|
||||||
|
const useOptions = (type = 'string') => {
|
||||||
|
const compile = useCompile();
|
||||||
|
const { fields } = useCollection();
|
||||||
|
const options = fields
|
||||||
|
?.filter((field) => field.type === type)
|
||||||
|
?.map((field) => {
|
||||||
|
return {
|
||||||
|
value: field.name,
|
||||||
|
label: compile(field?.uiSchema?.title),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return options;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GanttDesigner = () => {
|
||||||
|
const field = useField();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const { name, title, fields } = useCollection();
|
||||||
|
const dataSource = useCollectionFilterOptions(name);
|
||||||
|
const { service } = useGanttBlockContext();
|
||||||
|
const { dn } = useDesignable();
|
||||||
|
const compile = useCompile();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const template = useSchemaTemplate();
|
||||||
|
const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {};
|
||||||
|
const fieldNames = fieldSchema?.['x-decorator-props']?.['fieldNames'] || {};
|
||||||
|
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
|
||||||
|
return (
|
||||||
|
<GeneralSchemaDesigner template={template} title={title || name}>
|
||||||
|
<SchemaSettings.BlockTitleItem />
|
||||||
|
<SchemaSettings.SelectItem
|
||||||
|
title={t('Title field')}
|
||||||
|
value={fieldNames.title}
|
||||||
|
options={useOptions('string')}
|
||||||
|
onChange={(title) => {
|
||||||
|
const fieldNames = field.decoratorProps.fieldNames || {};
|
||||||
|
fieldNames['title'] = title;
|
||||||
|
field.decoratorProps.params = fieldNames;
|
||||||
|
fieldSchema['x-decorator-props']['params'] = fieldNames;
|
||||||
|
// Select切换option后value未按照预期切换,固增加以下代码
|
||||||
|
fieldSchema['x-decorator-props']['fieldNames'] = fieldNames;
|
||||||
|
service.refresh();
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
'x-decorator-props': field.decoratorProps,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dn.refresh();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SchemaSettings.SelectItem
|
||||||
|
title={t('Time scale')}
|
||||||
|
value={fieldNames.range||'day'}
|
||||||
|
options={[
|
||||||
|
{ label: compile('{{t("Hour")}}'), value: 'hour', color: 'orange' },
|
||||||
|
{ label: compile('{{t("Quarter of day")}}'), value: 'quarterDay', color: 'default' },
|
||||||
|
{ label: compile('{{t("Half of day")}}'), value: 'halfDay', color: 'blue' },
|
||||||
|
{ label: compile('{{t("Day")}}'), value: 'day', color: 'yellow' },
|
||||||
|
{ label: compile('{{t("Week")}}'), value: 'week', color: 'pule' },
|
||||||
|
{ label: compile('{{t("Month")}}'), value: 'month', color: 'green' },
|
||||||
|
{ label: compile('{{t("QuarterYear")}}'), value: 'quarterYear', color: 'red' },
|
||||||
|
{ label: compile('{{t("Year")}}'), value: 'year', color: 'green' },
|
||||||
|
]}
|
||||||
|
onChange={(range) => {
|
||||||
|
const fieldNames = field.decoratorProps.fieldNames || {};
|
||||||
|
fieldNames['range'] = range;
|
||||||
|
field.decoratorProps.params = fieldNames;
|
||||||
|
fieldSchema['x-decorator-props']['params'] = fieldNames;
|
||||||
|
// Select切换option后value未按照预期切换,固增加以下代码
|
||||||
|
fieldSchema['x-decorator-props']['fieldNames'] = fieldNames;
|
||||||
|
service.refresh();
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
'x-decorator-props': field.decoratorProps,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dn.refresh();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SchemaSettings.SelectItem
|
||||||
|
title={t('Start date field')}
|
||||||
|
value={fieldNames.start}
|
||||||
|
options={useOptions('date')}
|
||||||
|
onChange={(start) => {
|
||||||
|
const fieldNames = field.decoratorProps.fieldNames || {};
|
||||||
|
fieldNames['start'] = start;
|
||||||
|
field.decoratorProps.fieldNames = fieldNames;
|
||||||
|
fieldSchema['x-decorator-props']['fieldNames'] = fieldNames;
|
||||||
|
service.refresh();
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
'x-decorator-props': field.decoratorProps,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dn.refresh();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SchemaSettings.SelectItem
|
||||||
|
title={t('End date field')}
|
||||||
|
value={fieldNames.end}
|
||||||
|
options={useOptions('date')}
|
||||||
|
onChange={(end) => {
|
||||||
|
const fieldNames = field.decoratorProps.fieldNames || {};
|
||||||
|
fieldNames['end'] = end;
|
||||||
|
field.decoratorProps.fieldNames = fieldNames;
|
||||||
|
fieldSchema['x-decorator-props']['fieldNames'] = fieldNames;
|
||||||
|
service.refresh();
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
'x-decorator-props': field.decoratorProps,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dn.refresh();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SchemaSettings.SelectItem
|
||||||
|
title={t('Progress field')}
|
||||||
|
value={fieldNames.progress}
|
||||||
|
options={useOptions('float')}
|
||||||
|
onChange={(progress) => {
|
||||||
|
const fieldNames = field.decoratorProps.fieldNames || {};
|
||||||
|
fieldNames['progress'] = progress;
|
||||||
|
field.decoratorProps.fieldNames = fieldNames;
|
||||||
|
fieldSchema['x-decorator-props']['fieldNames'] = fieldNames;
|
||||||
|
service.refresh();
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
'x-decorator-props': field.decoratorProps,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dn.refresh();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SchemaSettings.ModalItem
|
||||||
|
title={t('Set the data scope')}
|
||||||
|
schema={
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
title: t('Set the data scope'),
|
||||||
|
properties: {
|
||||||
|
filter: {
|
||||||
|
default: defaultFilter,
|
||||||
|
enum: dataSource,
|
||||||
|
'x-component': 'Filter',
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ISchema
|
||||||
|
}
|
||||||
|
initialValues={
|
||||||
|
{
|
||||||
|
// title: field.title,
|
||||||
|
// icon: field.componentProps.icon,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onSubmit={({ filter }) => {
|
||||||
|
const params = field.decoratorProps.params || {};
|
||||||
|
params.filter = filter;
|
||||||
|
field.decoratorProps.params = params;
|
||||||
|
fieldSchema['x-decorator-props']['params'] = params;
|
||||||
|
service.run({ ...service?.params?.[0], filter });
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
'x-decorator-props': field.decoratorProps,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SchemaSettings.Divider />
|
||||||
|
<SchemaSettings.Template componentName={'Gantt'} collectionName={name} resourceName={defaultResource} />
|
||||||
|
<SchemaSettings.Divider />
|
||||||
|
<SchemaSettings.Remove
|
||||||
|
removeParentsIfNoChildren
|
||||||
|
breakRemoveOn={{
|
||||||
|
'x-component': 'Grid',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</GeneralSchemaDesigner>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,380 @@
|
|||||||
|
import React, { ReactChild } from 'react';
|
||||||
|
import { cx } from '@emotion/css';
|
||||||
|
import { DateSetup } from '../../types/date-setup';
|
||||||
|
import { ViewMode } from '../../types/public-types';
|
||||||
|
import { TopPartOfCalendar } from './top-part-of-calendar';
|
||||||
|
import {
|
||||||
|
getCachedDateTimeFormat,
|
||||||
|
getDaysInMonth,
|
||||||
|
getLocalDayOfWeek,
|
||||||
|
getLocaleMonth,
|
||||||
|
getWeekNumberISO8601,
|
||||||
|
} from '../../helpers/date-helper';
|
||||||
|
import { calendarBottomText, calendarHeader } from './style';
|
||||||
|
|
||||||
|
export type CalendarProps = {
|
||||||
|
dateSetup: DateSetup;
|
||||||
|
locale: string;
|
||||||
|
viewMode: ViewMode;
|
||||||
|
rtl: boolean;
|
||||||
|
headerHeight: number;
|
||||||
|
columnWidth: number;
|
||||||
|
fontFamily: string;
|
||||||
|
fontSize: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Calendar: React.FC<CalendarProps> = ({
|
||||||
|
dateSetup,
|
||||||
|
locale,
|
||||||
|
viewMode,
|
||||||
|
rtl,
|
||||||
|
headerHeight,
|
||||||
|
columnWidth,
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
}) => {
|
||||||
|
const getCalendarValuesForYear = () => {
|
||||||
|
const topValues: ReactChild[] = [];
|
||||||
|
const bottomValues: ReactChild[] = [];
|
||||||
|
const topDefaultHeight = headerHeight * 0.5;
|
||||||
|
for (let i = 0; i < dateSetup.dates.length; i++) {
|
||||||
|
const date = dateSetup.dates[i];
|
||||||
|
const bottomValue = date.getFullYear();
|
||||||
|
bottomValues.push(
|
||||||
|
<text
|
||||||
|
key={date.getTime()}
|
||||||
|
y={headerHeight * 0.8}
|
||||||
|
x={columnWidth * i + columnWidth * 0.5}
|
||||||
|
className={cx(calendarBottomText)}
|
||||||
|
>
|
||||||
|
{bottomValue}
|
||||||
|
</text>,
|
||||||
|
);
|
||||||
|
if (i === 0 || date.getFullYear() !== dateSetup.dates[i - 1].getFullYear()) {
|
||||||
|
const topValue = date.getFullYear().toString();
|
||||||
|
let xText: number;
|
||||||
|
if (rtl) {
|
||||||
|
xText = (6 + i + date.getFullYear() + 1) * columnWidth;
|
||||||
|
} else {
|
||||||
|
xText = (6 + i - date.getFullYear()) * columnWidth;
|
||||||
|
}
|
||||||
|
topValues.push(
|
||||||
|
<TopPartOfCalendar
|
||||||
|
key={topValue}
|
||||||
|
value={topValue}
|
||||||
|
x1Line={columnWidth * i}
|
||||||
|
y1Line={0}
|
||||||
|
y2Line={headerHeight}
|
||||||
|
xText={xText}
|
||||||
|
yText={topDefaultHeight * 0.9}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [topValues, bottomValues];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCalendarValuesForQuarterYear = () => {
|
||||||
|
const topValues: ReactChild[] = [];
|
||||||
|
const bottomValues: ReactChild[] = [];
|
||||||
|
const topDefaultHeight = headerHeight * 0.5;
|
||||||
|
for (let i = 0; i < dateSetup.dates.length; i++) {
|
||||||
|
const date = dateSetup.dates[i];
|
||||||
|
// const bottomValue = getLocaleMonth(date, locale);
|
||||||
|
const quarter = 'Q' + Math.floor((date.getMonth() + 3) / 3);
|
||||||
|
bottomValues.push(
|
||||||
|
<text
|
||||||
|
key={date.getTime()}
|
||||||
|
y={headerHeight * 0.8}
|
||||||
|
x={columnWidth * i + columnWidth * 0.5}
|
||||||
|
className={cx(calendarBottomText)}
|
||||||
|
>
|
||||||
|
{quarter}
|
||||||
|
</text>,
|
||||||
|
);
|
||||||
|
if (i === 0 || date.getFullYear() !== dateSetup.dates[i - 1].getFullYear()) {
|
||||||
|
const topValue = date.getFullYear().toString();
|
||||||
|
let xText: number;
|
||||||
|
if (rtl) {
|
||||||
|
xText = (6 + i + date.getMonth() + 1) * columnWidth;
|
||||||
|
} else {
|
||||||
|
xText = (6 + i - date.getMonth()) * columnWidth;
|
||||||
|
}
|
||||||
|
topValues.push(
|
||||||
|
<TopPartOfCalendar
|
||||||
|
key={topValue}
|
||||||
|
value={topValue}
|
||||||
|
x1Line={columnWidth * i}
|
||||||
|
y1Line={0}
|
||||||
|
y2Line={topDefaultHeight}
|
||||||
|
xText={Math.abs(xText)}
|
||||||
|
yText={topDefaultHeight * 0.9}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [topValues, bottomValues];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCalendarValuesForMonth = () => {
|
||||||
|
const topValues: ReactChild[] = [];
|
||||||
|
const bottomValues: ReactChild[] = [];
|
||||||
|
const topDefaultHeight = headerHeight * 0.5;
|
||||||
|
for (let i = 0; i < dateSetup.dates.length; i++) {
|
||||||
|
const date = dateSetup.dates[i];
|
||||||
|
const bottomValue = getLocaleMonth(date, locale);
|
||||||
|
bottomValues.push(
|
||||||
|
<text
|
||||||
|
key={bottomValue + date.getFullYear()}
|
||||||
|
y={headerHeight * 0.8}
|
||||||
|
x={columnWidth * i + columnWidth * 0.5}
|
||||||
|
className={cx(calendarBottomText)}
|
||||||
|
>
|
||||||
|
{bottomValue}
|
||||||
|
</text>,
|
||||||
|
);
|
||||||
|
if (i === 0 || date.getFullYear() !== dateSetup.dates[i - 1].getFullYear()) {
|
||||||
|
const topValue = date.getFullYear().toString();
|
||||||
|
let xText: number;
|
||||||
|
if (rtl) {
|
||||||
|
xText = (6 + i + date.getMonth() + 1) * columnWidth;
|
||||||
|
} else {
|
||||||
|
xText = (6 + i - date.getMonth()) * columnWidth;
|
||||||
|
}
|
||||||
|
topValues.push(
|
||||||
|
<TopPartOfCalendar
|
||||||
|
key={topValue}
|
||||||
|
value={topValue}
|
||||||
|
x1Line={columnWidth * i}
|
||||||
|
y1Line={0}
|
||||||
|
y2Line={topDefaultHeight}
|
||||||
|
xText={xText}
|
||||||
|
yText={topDefaultHeight * 0.9}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [topValues, bottomValues];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCalendarValuesForWeek = () => {
|
||||||
|
const topValues: ReactChild[] = [];
|
||||||
|
const bottomValues: ReactChild[] = [];
|
||||||
|
let weeksCount: number = 1;
|
||||||
|
const topDefaultHeight = headerHeight * 0.5;
|
||||||
|
const dates = dateSetup.dates;
|
||||||
|
for (let i = dates.length - 1; i >= 0; i--) {
|
||||||
|
const date = dates[i];
|
||||||
|
let topValue = '';
|
||||||
|
if (i === 0 || date.getMonth() !== dates[i - 1].getMonth()) {
|
||||||
|
// top
|
||||||
|
topValue = `${getLocaleMonth(date, locale)}, ${date.getFullYear()}`;
|
||||||
|
}
|
||||||
|
// bottom
|
||||||
|
const bottomValue = `W${getWeekNumberISO8601(date)}`;
|
||||||
|
|
||||||
|
bottomValues.push(
|
||||||
|
<text
|
||||||
|
key={date.getTime()}
|
||||||
|
y={headerHeight * 0.8}
|
||||||
|
x={columnWidth * (i + +rtl)}
|
||||||
|
className={cx(calendarBottomText)}
|
||||||
|
>
|
||||||
|
{bottomValue}
|
||||||
|
</text>,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (topValue) {
|
||||||
|
// if last day is new month
|
||||||
|
if (i !== dates.length - 1) {
|
||||||
|
topValues.push(
|
||||||
|
<TopPartOfCalendar
|
||||||
|
key={topValue}
|
||||||
|
value={topValue}
|
||||||
|
x1Line={columnWidth * i + weeksCount * columnWidth}
|
||||||
|
y1Line={0}
|
||||||
|
y2Line={topDefaultHeight}
|
||||||
|
xText={columnWidth * i + columnWidth * weeksCount * 0.5}
|
||||||
|
yText={topDefaultHeight * 0.9}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
weeksCount = 0;
|
||||||
|
}
|
||||||
|
weeksCount++;
|
||||||
|
}
|
||||||
|
return [topValues, bottomValues];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCalendarValuesForDay = () => {
|
||||||
|
const topValues: ReactChild[] = [];
|
||||||
|
const bottomValues: ReactChild[] = [];
|
||||||
|
const topDefaultHeight = headerHeight * 0.5;
|
||||||
|
const dates = dateSetup.dates;
|
||||||
|
for (let i = 0; i < dates.length; i++) {
|
||||||
|
const date = dates[i];
|
||||||
|
// const bottomValue = `${getLocalDayOfWeek(date, locale, 'short')}, ${date.getDate().toString()}`;
|
||||||
|
const bottomValue = `${date.getDate().toString()}`;
|
||||||
|
|
||||||
|
bottomValues.push(
|
||||||
|
<text
|
||||||
|
key={date.getTime()}
|
||||||
|
y={headerHeight * 0.8}
|
||||||
|
x={columnWidth * i + columnWidth * 0.5}
|
||||||
|
className={cx(calendarBottomText)}
|
||||||
|
>
|
||||||
|
{bottomValue}
|
||||||
|
</text>,
|
||||||
|
);
|
||||||
|
if (i + 1 !== dates.length && date.getMonth() !== dates[i + 1].getMonth()) {
|
||||||
|
const topValue = getLocaleMonth(date, locale);
|
||||||
|
|
||||||
|
topValues.push(
|
||||||
|
<TopPartOfCalendar
|
||||||
|
key={topValue + date.getFullYear()}
|
||||||
|
value={topValue}
|
||||||
|
x1Line={columnWidth * (i + 1)}
|
||||||
|
y1Line={0}
|
||||||
|
y2Line={topDefaultHeight}
|
||||||
|
xText={columnWidth * (i + 1) - getDaysInMonth(date.getMonth(), date.getFullYear()) * columnWidth * 0.5}
|
||||||
|
yText={topDefaultHeight * 0.9}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [topValues, bottomValues];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCalendarValuesForPartOfDay = () => {
|
||||||
|
const topValues: ReactChild[] = [];
|
||||||
|
const bottomValues: ReactChild[] = [];
|
||||||
|
const ticks = viewMode === ViewMode.HalfDay ? 2 : 4;
|
||||||
|
const topDefaultHeight = headerHeight * 0.5;
|
||||||
|
const dates = dateSetup.dates;
|
||||||
|
for (let i = 0; i < dates.length; i++) {
|
||||||
|
const date = dates[i];
|
||||||
|
const bottomValue = getCachedDateTimeFormat(locale, {
|
||||||
|
hour: 'numeric',
|
||||||
|
})
|
||||||
|
//@ts-ignore
|
||||||
|
.format(date)
|
||||||
|
.replace('时', '');
|
||||||
|
bottomValues.push(
|
||||||
|
<text
|
||||||
|
key={date.getTime()}
|
||||||
|
y={headerHeight * 0.8}
|
||||||
|
x={columnWidth * (i + +rtl)}
|
||||||
|
className={cx(calendarBottomText)}
|
||||||
|
fontFamily={fontFamily}
|
||||||
|
>
|
||||||
|
{bottomValue}
|
||||||
|
</text>,
|
||||||
|
);
|
||||||
|
if (i === 0 || date.getDate() !== dates[i - 1].getDate()) {
|
||||||
|
const topValue = `${getLocalDayOfWeek(date, locale, 'short')}, ${date.getDate()} ${getLocaleMonth(
|
||||||
|
date,
|
||||||
|
locale,
|
||||||
|
)}`;
|
||||||
|
topValues.push(
|
||||||
|
<TopPartOfCalendar
|
||||||
|
key={topValue + date.getFullYear()}
|
||||||
|
value={topValue}
|
||||||
|
x1Line={columnWidth * i + ticks * columnWidth}
|
||||||
|
y1Line={0}
|
||||||
|
y2Line={topDefaultHeight}
|
||||||
|
xText={columnWidth * i + ticks * columnWidth * 0.5}
|
||||||
|
yText={topDefaultHeight * 0.9}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [topValues, bottomValues];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCalendarValuesForHour = () => {
|
||||||
|
const topValues: ReactChild[] = [];
|
||||||
|
const bottomValues: ReactChild[] = [];
|
||||||
|
const topDefaultHeight = headerHeight * 0.5;
|
||||||
|
const dates = dateSetup.dates;
|
||||||
|
for (let i = 0; i < dates.length; i++) {
|
||||||
|
const date = dates[i];
|
||||||
|
const bottomValue = getCachedDateTimeFormat(locale, {
|
||||||
|
hour: 'numeric',
|
||||||
|
})
|
||||||
|
//@ts-ignore
|
||||||
|
.format(date)
|
||||||
|
?.replace('时', '');
|
||||||
|
bottomValues.push(
|
||||||
|
<text
|
||||||
|
key={date.getTime()}
|
||||||
|
y={headerHeight * 0.8}
|
||||||
|
x={columnWidth * (i + +rtl)}
|
||||||
|
className={cx(calendarBottomText)}
|
||||||
|
fontFamily={fontFamily}
|
||||||
|
>
|
||||||
|
{bottomValue}
|
||||||
|
</text>,
|
||||||
|
);
|
||||||
|
if (i !== 0 && date.getDate() !== dates[i - 1].getDate()) {
|
||||||
|
const displayDate = dates[i - 1];
|
||||||
|
const topValue = `${getLocalDayOfWeek(displayDate, locale, 'long')}, ${displayDate.getDate()} ${getLocaleMonth(
|
||||||
|
displayDate,
|
||||||
|
locale,
|
||||||
|
)}`;
|
||||||
|
const topPosition = (date.getHours() - 24) / 2;
|
||||||
|
topValues.push(
|
||||||
|
<TopPartOfCalendar
|
||||||
|
key={topValue + displayDate.getFullYear()}
|
||||||
|
value={topValue}
|
||||||
|
x1Line={columnWidth * i}
|
||||||
|
y1Line={0}
|
||||||
|
y2Line={topDefaultHeight}
|
||||||
|
xText={columnWidth * (i + topPosition)}
|
||||||
|
yText={topDefaultHeight * 0.9}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [topValues, bottomValues];
|
||||||
|
};
|
||||||
|
|
||||||
|
let topValues: ReactChild[] = [];
|
||||||
|
let bottomValues: ReactChild[] = [];
|
||||||
|
switch (dateSetup.viewMode) {
|
||||||
|
case ViewMode.Year:
|
||||||
|
[topValues, bottomValues] = getCalendarValuesForYear();
|
||||||
|
break;
|
||||||
|
case ViewMode.QuarterYear:
|
||||||
|
[topValues, bottomValues] = getCalendarValuesForQuarterYear();
|
||||||
|
break;
|
||||||
|
case ViewMode.Month:
|
||||||
|
[topValues, bottomValues] = getCalendarValuesForMonth();
|
||||||
|
break;
|
||||||
|
case ViewMode.Week:
|
||||||
|
[topValues, bottomValues] = getCalendarValuesForWeek();
|
||||||
|
break;
|
||||||
|
case ViewMode.Day:
|
||||||
|
[topValues, bottomValues] = getCalendarValuesForDay();
|
||||||
|
break;
|
||||||
|
case ViewMode.QuarterDay:
|
||||||
|
case ViewMode.HalfDay:
|
||||||
|
[topValues, bottomValues] = getCalendarValuesForPartOfDay();
|
||||||
|
break;
|
||||||
|
case ViewMode.Hour:
|
||||||
|
[topValues, bottomValues] = getCalendarValuesForHour();
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<g className="calendar" fontSize={fontSize} fontFamily={fontFamily}>
|
||||||
|
<rect
|
||||||
|
x={0}
|
||||||
|
y={0}
|
||||||
|
width={columnWidth * dateSetup.dates.length}
|
||||||
|
height={headerHeight}
|
||||||
|
className={cx(calendarHeader)}
|
||||||
|
/>
|
||||||
|
{bottomValues} {topValues}
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,38 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
|
export const calendarBottomText = css`
|
||||||
|
text-anchor: middle;
|
||||||
|
fill: rgba(0, 0, 0, 0.85);
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
font-weight: 500;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const calendarTopTick = css`
|
||||||
|
stroke: #f0f0f0;
|
||||||
|
stroke-width: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const calendarTopText = css`
|
||||||
|
text-anchor: middle;
|
||||||
|
/* fill: #555; */
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
font-weight: 500;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const calendarHeader = css`
|
||||||
|
fill: #fafafa;
|
||||||
|
// stroke: #e0e0e0;
|
||||||
|
stroke-width: 1.4;
|
||||||
|
background: #fafafa;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
`;
|
@ -0,0 +1,43 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { cx } from '@emotion/css';
|
||||||
|
import { calendarTopTick, calendarTopText } from './style';
|
||||||
|
|
||||||
|
|
||||||
|
type TopPartOfCalendarProps = {
|
||||||
|
value: string;
|
||||||
|
x1Line: number;
|
||||||
|
y1Line: number;
|
||||||
|
y2Line: number;
|
||||||
|
xText: number;
|
||||||
|
yText: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TopPartOfCalendar: React.FC<TopPartOfCalendarProps> = ({
|
||||||
|
value,
|
||||||
|
x1Line,
|
||||||
|
y1Line,
|
||||||
|
y2Line,
|
||||||
|
xText,
|
||||||
|
yText,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<g className="calendarTop">
|
||||||
|
<line
|
||||||
|
x1={x1Line}
|
||||||
|
y1={y1Line}
|
||||||
|
x2={x1Line}
|
||||||
|
y2={y2Line}
|
||||||
|
className={cx(calendarTopTick)}
|
||||||
|
key={value + "line"}
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
key={value + "text"}
|
||||||
|
y={yText}
|
||||||
|
x={xText}
|
||||||
|
className={cx(calendarTopText)}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,6 @@
|
|||||||
|
import { observer } from '@formily/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const Event = observer((props) => {
|
||||||
|
return <>{props.children}</>;
|
||||||
|
});
|
@ -0,0 +1,546 @@
|
|||||||
|
import { css, cx } from '@emotion/css';
|
||||||
|
import { createForm } from '@formily/core';
|
||||||
|
import { RecursionField, Schema, useFieldSchema } from '@formily/react';
|
||||||
|
import { message } from 'antd';
|
||||||
|
import React, { SyntheticEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useCurrentAppInfo } from '../../../../../appInfo';
|
||||||
|
import { useBlockRequestContext, useGanttBlockContext, useTableBlockContext } from '../../../../../block-provider';
|
||||||
|
import { RecordProvider } from '../../../../../record-provider';
|
||||||
|
import { useDesignable } from '../../../../../schema-component';
|
||||||
|
import { ActionContext } from '../../../action';
|
||||||
|
import { convertToBarTasks } from '../../helpers/bar-helper';
|
||||||
|
import { ganttDateRange, seedDates } from '../../helpers/date-helper';
|
||||||
|
import { removeHiddenTasks, sortTasks } from '../../helpers/other-helper';
|
||||||
|
import { BarTask } from '../../types/bar-task';
|
||||||
|
import { DateSetup } from '../../types/date-setup';
|
||||||
|
import { GanttEvent } from '../../types/gantt-task-actions';
|
||||||
|
import { Task } from '../../types/public-types';
|
||||||
|
import { CalendarProps } from '../calendar/calendar';
|
||||||
|
import { GridProps } from '../grid/grid';
|
||||||
|
import { HorizontalScroll } from '../other/horizontal-scroll';
|
||||||
|
import { StandardTooltipContent, Tooltip } from '../other/tooltip';
|
||||||
|
import { VerticalScroll } from '../other/vertical-scroll';
|
||||||
|
import { wrapper } from './style';
|
||||||
|
import { TaskGantt } from './task-gantt';
|
||||||
|
import { TaskGanttContentProps } from './task-gantt-content';
|
||||||
|
|
||||||
|
const getColumnWidth = (dataSetLength: any, clientWidth: any) => {
|
||||||
|
const columnWidth = clientWidth / dataSetLength > 50 ? Math.floor(clientWidth / dataSetLength) + 20 : 50;
|
||||||
|
return columnWidth;
|
||||||
|
};
|
||||||
|
export const DeleteEventContext = React.createContext({
|
||||||
|
close: () => {},
|
||||||
|
});
|
||||||
|
const GanttRecordViewer = (props) => {
|
||||||
|
const { visible, setVisible, record } = props;
|
||||||
|
const form = useMemo(() => createForm(), [record]);
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const eventSchema: Schema = fieldSchema.properties.detail;
|
||||||
|
const close = useCallback(() => {
|
||||||
|
setVisible(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
eventSchema && (
|
||||||
|
<DeleteEventContext.Provider value={{ close }}>
|
||||||
|
<ActionContext.Provider value={{ visible, setVisible }}>
|
||||||
|
<RecordProvider record={record}>
|
||||||
|
<RecursionField schema={eventSchema} name={eventSchema.name} />
|
||||||
|
</RecordProvider>
|
||||||
|
</ActionContext.Provider>
|
||||||
|
</DeleteEventContext.Provider>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export const Gantt: any = (props: any) => {
|
||||||
|
const { designable } = useDesignable();
|
||||||
|
const currentTheme = localStorage.getItem('NOCOBASE_THEME');
|
||||||
|
const {
|
||||||
|
headerHeight = currentTheme === 'compact' ? (designable ? 53 : 45) : designable ? 65 : 55,
|
||||||
|
listCellWidth = '155px',
|
||||||
|
rowHeight = currentTheme === 'compact' ? 45 : 55.56,
|
||||||
|
ganttHeight = 0,
|
||||||
|
preStepsCount = 1,
|
||||||
|
barFill = 60,
|
||||||
|
barCornerRadius = 2,
|
||||||
|
barProgressColor = '#1890ff',
|
||||||
|
barProgressSelectedColor = '#1890ff',
|
||||||
|
barBackgroundColor = '#1890ff',
|
||||||
|
barBackgroundSelectedColor = '#1890ff',
|
||||||
|
projectProgressColor = '#1890ff',
|
||||||
|
projectProgressSelectedColor = '#1890ff',
|
||||||
|
projectBackgroundColor = '#1890ff',
|
||||||
|
projectBackgroundSelectedColor = '#1890ff',
|
||||||
|
milestoneBackgroundColor = '#f1c453',
|
||||||
|
milestoneBackgroundSelectedColor = '#f29e4c',
|
||||||
|
rtl = false,
|
||||||
|
handleWidth = 8,
|
||||||
|
timeStep = 300000,
|
||||||
|
arrowColor = 'grey',
|
||||||
|
fontFamily = `-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'`,
|
||||||
|
fontSize = currentTheme === 'compact' ? '12px' : '14px',
|
||||||
|
arrowIndent = 20,
|
||||||
|
todayColor = 'rgba(252, 248, 227, 0.5)',
|
||||||
|
viewDate,
|
||||||
|
TooltipContent = StandardTooltipContent,
|
||||||
|
onDoubleClick,
|
||||||
|
onClick,
|
||||||
|
onDelete,
|
||||||
|
onSelect,
|
||||||
|
useProps,
|
||||||
|
} = props;
|
||||||
|
const { onExpanderClick, tasks, expandAndCollapseAll } = useProps();
|
||||||
|
const ctx = useGanttBlockContext();
|
||||||
|
const appInfo = useCurrentAppInfo();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const locale = appInfo.data?.lang;
|
||||||
|
const tableCtx = useTableBlockContext();
|
||||||
|
const { resource, service } = useBlockRequestContext();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const { fieldNames } = useProps(props);
|
||||||
|
const viewMode = fieldNames.range || 'day';
|
||||||
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
|
const taskListRef = useRef<HTMLDivElement>(null);
|
||||||
|
const verticalGanttContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [dateSetup, setDateSetup] = useState<DateSetup>(() => {
|
||||||
|
const [startDate, endDate] = ganttDateRange(tasks, viewMode, preStepsCount);
|
||||||
|
return { viewMode, dates: seedDates(startDate, endDate, viewMode) };
|
||||||
|
});
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const [record, setRecord] = useState<any>({});
|
||||||
|
const [currentViewDate, setCurrentViewDate] = useState<Date | undefined>(undefined);
|
||||||
|
const [taskListWidth, setTaskListWidth] = useState(0);
|
||||||
|
const [svgContainerWidth, setSvgContainerWidth] = useState(0);
|
||||||
|
const [svgContainerHeight, setSvgContainerHeight] = useState(ganttHeight);
|
||||||
|
const [barTasks, setBarTasks] = useState<BarTask[]>([]);
|
||||||
|
const [ganttEvent, setGanttEvent] = useState<GanttEvent>({
|
||||||
|
action: '',
|
||||||
|
});
|
||||||
|
const taskHeight = useMemo(() => (rowHeight * barFill) / 100, [rowHeight, barFill]);
|
||||||
|
const [selectedTask, setSelectedTask] = useState<BarTask>();
|
||||||
|
const [failedTask, setFailedTask] = useState<BarTask | null>(null);
|
||||||
|
const [scrollY, setScrollY] = useState(0);
|
||||||
|
const [scrollX, setScrollX] = useState(-1);
|
||||||
|
const [ignoreScrollEvent, setIgnoreScrollEvent] = useState(false);
|
||||||
|
const columnWidth: number = getColumnWidth(dateSetup.dates.length, verticalGanttContainerRef.current?.clientWidth);
|
||||||
|
const svgWidth = dateSetup.dates.length * columnWidth;
|
||||||
|
const ganttFullHeight = barTasks.length * rowHeight;
|
||||||
|
const { expandFlag } = tableCtx;
|
||||||
|
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
tableCtx.field.onExpandClick = handleTableExpanderClick;
|
||||||
|
tableCtx.field.onRowSelect = handleRowSelect;
|
||||||
|
tableCtx.setExpandFlag(true);
|
||||||
|
}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
expandAndCollapseAll?.(!expandFlag);
|
||||||
|
}, [expandFlag]);
|
||||||
|
// task change events
|
||||||
|
useEffect(() => {
|
||||||
|
let filteredTasks: Task[];
|
||||||
|
if (onExpanderClick) {
|
||||||
|
filteredTasks = removeHiddenTasks(tasks);
|
||||||
|
} else {
|
||||||
|
filteredTasks = tasks;
|
||||||
|
}
|
||||||
|
filteredTasks = filteredTasks.sort(sortTasks);
|
||||||
|
const [startDate, endDate] = ganttDateRange(filteredTasks, viewMode, preStepsCount);
|
||||||
|
let newDates = seedDates(startDate, endDate, viewMode);
|
||||||
|
if (rtl) {
|
||||||
|
newDates = newDates.reverse();
|
||||||
|
if (scrollX === -1) {
|
||||||
|
setScrollX(newDates.length * columnWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setDateSetup({ dates: newDates, viewMode });
|
||||||
|
setBarTasks(
|
||||||
|
convertToBarTasks(
|
||||||
|
filteredTasks,
|
||||||
|
newDates,
|
||||||
|
columnWidth,
|
||||||
|
rowHeight,
|
||||||
|
taskHeight,
|
||||||
|
barCornerRadius,
|
||||||
|
handleWidth,
|
||||||
|
rtl,
|
||||||
|
barProgressColor,
|
||||||
|
barProgressSelectedColor,
|
||||||
|
barBackgroundColor,
|
||||||
|
barBackgroundSelectedColor,
|
||||||
|
projectProgressColor,
|
||||||
|
projectProgressSelectedColor,
|
||||||
|
projectBackgroundColor,
|
||||||
|
projectBackgroundSelectedColor,
|
||||||
|
milestoneBackgroundColor,
|
||||||
|
milestoneBackgroundSelectedColor,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
tasks,
|
||||||
|
viewMode,
|
||||||
|
preStepsCount,
|
||||||
|
rowHeight,
|
||||||
|
barCornerRadius,
|
||||||
|
columnWidth,
|
||||||
|
taskHeight,
|
||||||
|
handleWidth,
|
||||||
|
barProgressColor,
|
||||||
|
barProgressSelectedColor,
|
||||||
|
barBackgroundColor,
|
||||||
|
barBackgroundSelectedColor,
|
||||||
|
projectProgressColor,
|
||||||
|
projectProgressSelectedColor,
|
||||||
|
projectBackgroundColor,
|
||||||
|
projectBackgroundSelectedColor,
|
||||||
|
milestoneBackgroundColor,
|
||||||
|
milestoneBackgroundSelectedColor,
|
||||||
|
rtl,
|
||||||
|
scrollX,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
viewMode === dateSetup.viewMode &&
|
||||||
|
((viewDate && !currentViewDate) || (viewDate && currentViewDate?.valueOf() !== viewDate.valueOf()))
|
||||||
|
) {
|
||||||
|
const dates = dateSetup.dates;
|
||||||
|
const index = dates.findIndex(
|
||||||
|
(d, i) =>
|
||||||
|
viewDate.valueOf() >= d.valueOf() && i + 1 !== dates.length && viewDate.valueOf() < dates[i + 1].valueOf(),
|
||||||
|
);
|
||||||
|
if (index === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setCurrentViewDate(viewDate);
|
||||||
|
setScrollX(columnWidth * index);
|
||||||
|
}
|
||||||
|
}, [viewDate, columnWidth, dateSetup.dates, dateSetup.viewMode, viewMode, currentViewDate, setCurrentViewDate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { changedTask, action } = ganttEvent;
|
||||||
|
if (changedTask) {
|
||||||
|
if (action === 'delete') {
|
||||||
|
setGanttEvent({ action: '' });
|
||||||
|
setBarTasks(barTasks.filter((t) => t.id !== changedTask.id));
|
||||||
|
} else if (action === 'move' || action === 'end' || action === 'start' || action === 'progress') {
|
||||||
|
const prevStateTask = barTasks.find((t) => t.id === changedTask.id);
|
||||||
|
if (
|
||||||
|
prevStateTask &&
|
||||||
|
(prevStateTask.start.getTime() !== changedTask.start.getTime() ||
|
||||||
|
prevStateTask.end.getTime() !== changedTask.end.getTime() ||
|
||||||
|
prevStateTask.progress !== changedTask.progress)
|
||||||
|
) {
|
||||||
|
// actions for change
|
||||||
|
const newTaskList = barTasks.map((t) => (t.id === changedTask.id ? changedTask : t));
|
||||||
|
setBarTasks(newTaskList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [ganttEvent, barTasks]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (failedTask) {
|
||||||
|
setBarTasks(barTasks.map((t) => (t.id !== failedTask.id ? t : failedTask)));
|
||||||
|
setFailedTask(null);
|
||||||
|
}
|
||||||
|
}, [failedTask, barTasks]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!listCellWidth) {
|
||||||
|
setTaskListWidth(0);
|
||||||
|
}
|
||||||
|
if (taskListRef.current) {
|
||||||
|
setTaskListWidth(taskListRef.current.offsetWidth);
|
||||||
|
}
|
||||||
|
}, [taskListRef, listCellWidth]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (wrapperRef.current) {
|
||||||
|
setSvgContainerWidth(wrapperRef.current.offsetWidth - taskListWidth);
|
||||||
|
}
|
||||||
|
}, [wrapperRef, taskListWidth]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ganttHeight) {
|
||||||
|
setSvgContainerHeight(ganttHeight + headerHeight);
|
||||||
|
} else {
|
||||||
|
setSvgContainerHeight(tasks.length * rowHeight + headerHeight);
|
||||||
|
}
|
||||||
|
}, [ganttHeight, tasks, headerHeight, rowHeight]);
|
||||||
|
|
||||||
|
// scroll events
|
||||||
|
useEffect(() => {
|
||||||
|
const handleWheel = (event: WheelEvent) => {
|
||||||
|
if (event.shiftKey || event.deltaX) {
|
||||||
|
const scrollMove = event.deltaX ? event.deltaX : event.deltaY;
|
||||||
|
let newScrollX = scrollX + scrollMove;
|
||||||
|
if (newScrollX < 0) {
|
||||||
|
newScrollX = 0;
|
||||||
|
} else if (newScrollX > svgWidth) {
|
||||||
|
newScrollX = svgWidth;
|
||||||
|
}
|
||||||
|
setScrollX(newScrollX);
|
||||||
|
event.preventDefault();
|
||||||
|
} else if (ganttHeight) {
|
||||||
|
let newScrollY = scrollY + event.deltaY;
|
||||||
|
if (newScrollY < 0) {
|
||||||
|
newScrollY = 0;
|
||||||
|
} else if (newScrollY > ganttFullHeight - ganttHeight) {
|
||||||
|
newScrollY = ganttFullHeight - ganttHeight;
|
||||||
|
}
|
||||||
|
if (newScrollY !== scrollY) {
|
||||||
|
setScrollY(newScrollY);
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setIgnoreScrollEvent(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// subscribe if scroll is necessary
|
||||||
|
wrapperRef.current?.addEventListener('wheel', handleWheel, {
|
||||||
|
passive: false,
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
wrapperRef.current?.removeEventListener('wheel', handleWheel);
|
||||||
|
};
|
||||||
|
}, [wrapperRef, scrollY, scrollX, ganttHeight, svgWidth, rtl, ganttFullHeight]);
|
||||||
|
|
||||||
|
const handleScrollY = (event: SyntheticEvent<HTMLDivElement>) => {
|
||||||
|
if (scrollY !== event.currentTarget.scrollTop && !ignoreScrollEvent) {
|
||||||
|
setScrollY(event.currentTarget.scrollTop);
|
||||||
|
setIgnoreScrollEvent(true);
|
||||||
|
} else {
|
||||||
|
setIgnoreScrollEvent(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScrollX = (event: SyntheticEvent<HTMLDivElement>) => {
|
||||||
|
if (scrollX !== event.currentTarget.scrollLeft && !ignoreScrollEvent) {
|
||||||
|
setScrollX(event.currentTarget.scrollLeft);
|
||||||
|
setIgnoreScrollEvent(true);
|
||||||
|
} else {
|
||||||
|
setIgnoreScrollEvent(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles arrow keys events and transform it to new scroll
|
||||||
|
*/
|
||||||
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
let newScrollY = scrollY;
|
||||||
|
let newScrollX = scrollX;
|
||||||
|
let isX = true;
|
||||||
|
switch (event.key) {
|
||||||
|
case 'Down': // IE/Edge specific value
|
||||||
|
case 'ArrowDown':
|
||||||
|
newScrollY += rowHeight;
|
||||||
|
isX = false;
|
||||||
|
break;
|
||||||
|
case 'Up': // IE/Edge specific value
|
||||||
|
case 'ArrowUp':
|
||||||
|
newScrollY -= rowHeight;
|
||||||
|
isX = false;
|
||||||
|
break;
|
||||||
|
case 'Left':
|
||||||
|
case 'ArrowLeft':
|
||||||
|
newScrollX -= columnWidth;
|
||||||
|
break;
|
||||||
|
case 'Right': // IE/Edge specific value
|
||||||
|
case 'ArrowRight':
|
||||||
|
newScrollX += columnWidth;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (isX) {
|
||||||
|
if (newScrollX < 0) {
|
||||||
|
newScrollX = 0;
|
||||||
|
} else if (newScrollX > svgWidth) {
|
||||||
|
newScrollX = svgWidth;
|
||||||
|
}
|
||||||
|
setScrollX(newScrollX);
|
||||||
|
} else {
|
||||||
|
if (newScrollY < 0) {
|
||||||
|
newScrollY = 0;
|
||||||
|
} else if (newScrollY > ganttFullHeight - ganttHeight) {
|
||||||
|
newScrollY = ganttFullHeight - ganttHeight;
|
||||||
|
}
|
||||||
|
setScrollY(newScrollY);
|
||||||
|
}
|
||||||
|
setIgnoreScrollEvent(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Task select event
|
||||||
|
*/
|
||||||
|
const handleSelectedTask = (taskId: string) => {
|
||||||
|
const newSelectedTask = barTasks.find((t) => t.id === taskId);
|
||||||
|
const oldSelectedTask = barTasks.find((t) => !!selectedTask && t.id === selectedTask.id);
|
||||||
|
if (onSelect) {
|
||||||
|
if (oldSelectedTask) {
|
||||||
|
onSelect(oldSelectedTask, false);
|
||||||
|
}
|
||||||
|
if (newSelectedTask) {
|
||||||
|
onSelect(newSelectedTask, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setSelectedTask(newSelectedTask);
|
||||||
|
};
|
||||||
|
const handleTableExpanderClick = (expanded: boolean, record: any) => {
|
||||||
|
const task = ctx?.field?.data.find((v: any) => v.id === record.id + '');
|
||||||
|
if (onExpanderClick && record.children.length) {
|
||||||
|
onExpanderClick({ ...task, hideChildren: !expanded });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRowSelect = (keys) => {
|
||||||
|
setSelectedRowKeys(keys);
|
||||||
|
};
|
||||||
|
const handleProgressChange = async (task: Task) => {
|
||||||
|
await resource.update({
|
||||||
|
filterByTk: task.id,
|
||||||
|
values: {
|
||||||
|
...task,
|
||||||
|
[fieldNames.progress]: task.progress / 100,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
message.success(t('Saved successfully'));
|
||||||
|
await service?.refresh();
|
||||||
|
};
|
||||||
|
const handleTaskChange = async (task: Task) => {
|
||||||
|
await resource.update({
|
||||||
|
filterByTk: task.id,
|
||||||
|
values: {
|
||||||
|
...task,
|
||||||
|
[fieldNames.start]: task.start,
|
||||||
|
[fieldNames.end]: task.end,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
message.success(t('Saved successfully'));
|
||||||
|
await service?.refresh();
|
||||||
|
};
|
||||||
|
const handleBarClick = (data) => {
|
||||||
|
const flattenTree = (treeData) => {
|
||||||
|
return treeData.reduce((acc, node) => {
|
||||||
|
if (node.children) {
|
||||||
|
return acc.concat([node, ...flattenTree(node.children)]);
|
||||||
|
} else {
|
||||||
|
return acc.concat(node);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
|
const flattenedData = flattenTree(service?.data?.data);
|
||||||
|
const recordData = flattenedData?.find((item) => item.id === +data.id);
|
||||||
|
if (!recordData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setRecord(recordData);
|
||||||
|
setVisible(true);
|
||||||
|
};
|
||||||
|
const gridProps: GridProps = {
|
||||||
|
columnWidth,
|
||||||
|
svgWidth,
|
||||||
|
tasks: tasks,
|
||||||
|
rowHeight,
|
||||||
|
dates: dateSetup.dates,
|
||||||
|
todayColor,
|
||||||
|
rtl,
|
||||||
|
selectedRowKeys,
|
||||||
|
};
|
||||||
|
const calendarProps: CalendarProps = {
|
||||||
|
dateSetup,
|
||||||
|
locale,
|
||||||
|
viewMode,
|
||||||
|
headerHeight,
|
||||||
|
columnWidth,
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
rtl,
|
||||||
|
};
|
||||||
|
const barProps: TaskGanttContentProps = {
|
||||||
|
tasks: barTasks,
|
||||||
|
dates: dateSetup.dates,
|
||||||
|
ganttEvent,
|
||||||
|
selectedTask,
|
||||||
|
rowHeight,
|
||||||
|
taskHeight,
|
||||||
|
columnWidth,
|
||||||
|
arrowColor,
|
||||||
|
timeStep,
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
arrowIndent,
|
||||||
|
svgWidth,
|
||||||
|
rtl,
|
||||||
|
setGanttEvent,
|
||||||
|
setFailedTask,
|
||||||
|
setSelectedTask: handleSelectedTask,
|
||||||
|
onDateChange: handleTaskChange,
|
||||||
|
onProgressChange: fieldNames.progress && handleProgressChange,
|
||||||
|
onDoubleClick,
|
||||||
|
onClick: handleBarClick,
|
||||||
|
onDelete,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className={css`
|
||||||
|
.ant-table-container::after {
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<GanttRecordViewer visible={visible} setVisible={setVisible} record={record} />
|
||||||
|
<RecursionField name={'anctionBar'} schema={fieldSchema.properties.toolBar} />
|
||||||
|
<RecursionField name={'table'} schema={fieldSchema.properties.table} />
|
||||||
|
<div className={cx(wrapper)} onKeyDown={handleKeyDown} tabIndex={0} ref={wrapperRef}>
|
||||||
|
<TaskGantt
|
||||||
|
gridProps={gridProps}
|
||||||
|
calendarProps={calendarProps}
|
||||||
|
barProps={barProps}
|
||||||
|
ganttHeight={ganttHeight}
|
||||||
|
scrollY={scrollY}
|
||||||
|
scrollX={scrollX}
|
||||||
|
ref={verticalGanttContainerRef}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{ganttEvent.changedTask && (
|
||||||
|
<Tooltip
|
||||||
|
arrowIndent={arrowIndent}
|
||||||
|
rowHeight={rowHeight}
|
||||||
|
svgContainerHeight={svgContainerHeight}
|
||||||
|
svgContainerWidth={svgContainerWidth}
|
||||||
|
fontFamily={fontFamily}
|
||||||
|
fontSize={fontSize}
|
||||||
|
scrollX={scrollX}
|
||||||
|
scrollY={scrollY}
|
||||||
|
task={ganttEvent.changedTask}
|
||||||
|
headerHeight={headerHeight}
|
||||||
|
taskListWidth={taskListWidth}
|
||||||
|
TooltipContent={TooltipContent}
|
||||||
|
rtl={rtl}
|
||||||
|
svgWidth={svgWidth}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<VerticalScroll
|
||||||
|
ganttFullHeight={ganttFullHeight}
|
||||||
|
ganttHeight={ganttHeight}
|
||||||
|
headerHeight={headerHeight}
|
||||||
|
scroll={scrollY}
|
||||||
|
onScroll={handleScrollY}
|
||||||
|
rtl={rtl}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<HorizontalScroll
|
||||||
|
svgWidth={svgWidth}
|
||||||
|
taskListWidth={taskListWidth}
|
||||||
|
scroll={scrollX}
|
||||||
|
rtl={rtl}
|
||||||
|
onScroll={handleScrollX}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
|
export const ganttVerticalContainer=css `
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width:100%;
|
||||||
|
border-left:2px solid #f4f2f2
|
||||||
|
`
|
||||||
|
|
||||||
|
export const horizontalContainer=css `
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const wrapper =css`
|
||||||
|
display: flex;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
outline: none;
|
||||||
|
position: relative;
|
||||||
|
`
|
@ -0,0 +1,299 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { EventOption } from '../../types/public-types';
|
||||||
|
import { BarTask } from '../../types/bar-task';
|
||||||
|
import { Arrow } from '../other/arrow';
|
||||||
|
import { handleTaskBySVGMouseEvent } from '../../helpers/bar-helper';
|
||||||
|
import { isKeyboardEvent } from '../../helpers/other-helper';
|
||||||
|
import { TaskItem } from '../task-item/task-item';
|
||||||
|
import { BarMoveAction, GanttContentMoveAction, GanttEvent } from '../../types/gantt-task-actions';
|
||||||
|
|
||||||
|
let lastAction = null;
|
||||||
|
let lastStart = null;
|
||||||
|
export type TaskGanttContentProps = {
|
||||||
|
tasks: BarTask[];
|
||||||
|
dates: Date[];
|
||||||
|
ganttEvent: GanttEvent;
|
||||||
|
selectedTask: BarTask | undefined;
|
||||||
|
rowHeight: number;
|
||||||
|
columnWidth: number;
|
||||||
|
timeStep: number;
|
||||||
|
svg?: React.RefObject<SVGSVGElement>;
|
||||||
|
svgWidth: number;
|
||||||
|
taskHeight: number;
|
||||||
|
arrowColor: string;
|
||||||
|
arrowIndent: number;
|
||||||
|
fontSize: string;
|
||||||
|
fontFamily: string;
|
||||||
|
rtl: boolean;
|
||||||
|
setGanttEvent: (value: GanttEvent) => void;
|
||||||
|
setFailedTask: (value: BarTask | null) => void;
|
||||||
|
setSelectedTask: (taskId: string) => void;
|
||||||
|
} & EventOption;
|
||||||
|
|
||||||
|
export const TaskGanttContent: React.FC<TaskGanttContentProps> = ({
|
||||||
|
tasks,
|
||||||
|
dates,
|
||||||
|
ganttEvent,
|
||||||
|
selectedTask,
|
||||||
|
rowHeight,
|
||||||
|
columnWidth,
|
||||||
|
timeStep,
|
||||||
|
svg,
|
||||||
|
taskHeight,
|
||||||
|
arrowColor,
|
||||||
|
arrowIndent,
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
rtl,
|
||||||
|
setGanttEvent,
|
||||||
|
setFailedTask,
|
||||||
|
setSelectedTask,
|
||||||
|
onDateChange,
|
||||||
|
onProgressChange,
|
||||||
|
onDoubleClick,
|
||||||
|
onClick,
|
||||||
|
onDelete,
|
||||||
|
}) => {
|
||||||
|
const point = svg?.current?.createSVGPoint();
|
||||||
|
const [xStep, setXStep] = useState(0);
|
||||||
|
const [initEventX1Delta, setInitEventX1Delta] = useState(0);
|
||||||
|
const [isMoving, setIsMoving] = useState(false);
|
||||||
|
|
||||||
|
// create xStep
|
||||||
|
useEffect(() => {
|
||||||
|
const dateDelta =
|
||||||
|
dates[1]?.getTime() -
|
||||||
|
dates[0]?.getTime() -
|
||||||
|
dates[1]?.getTimezoneOffset() * 60 * 1000 +
|
||||||
|
dates[0]?.getTimezoneOffset() * 60 * 1000;
|
||||||
|
const newXStep = (timeStep * columnWidth) / dateDelta;
|
||||||
|
setXStep(newXStep);
|
||||||
|
}, [columnWidth, dates, timeStep]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleMouseMove = async (event: MouseEvent) => {
|
||||||
|
if (!ganttEvent.changedTask || !point || !svg?.current) return;
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
point.x = event.clientX;
|
||||||
|
const cursor = point.matrixTransform(svg?.current.getScreenCTM()?.inverse());
|
||||||
|
|
||||||
|
const { isChanged, changedTask } = handleTaskBySVGMouseEvent(
|
||||||
|
cursor.x,
|
||||||
|
ganttEvent.action as BarMoveAction,
|
||||||
|
ganttEvent.changedTask,
|
||||||
|
xStep,
|
||||||
|
timeStep,
|
||||||
|
initEventX1Delta,
|
||||||
|
rtl,
|
||||||
|
);
|
||||||
|
if (isChanged) {
|
||||||
|
setGanttEvent({ action: ganttEvent.action, changedTask });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = async (event: MouseEvent) => {
|
||||||
|
const { action, originalSelectedTask, changedTask } = ganttEvent;
|
||||||
|
if (!changedTask || !point || !svg?.current || !originalSelectedTask) return;
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
point.x = event.clientX;
|
||||||
|
const cursor = point.matrixTransform(svg?.current.getScreenCTM()?.inverse());
|
||||||
|
const { changedTask: newChangedTask } = handleTaskBySVGMouseEvent(
|
||||||
|
cursor.x,
|
||||||
|
action as BarMoveAction,
|
||||||
|
changedTask,
|
||||||
|
xStep,
|
||||||
|
timeStep,
|
||||||
|
initEventX1Delta,
|
||||||
|
rtl,
|
||||||
|
);
|
||||||
|
|
||||||
|
const isNotLikeOriginal =
|
||||||
|
originalSelectedTask.start !== newChangedTask.start ||
|
||||||
|
originalSelectedTask.end !== newChangedTask.end ||
|
||||||
|
originalSelectedTask.progress !== newChangedTask.progress;
|
||||||
|
|
||||||
|
// remove listeners
|
||||||
|
svg.current.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
svg.current.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
setGanttEvent({ action: '' });
|
||||||
|
setIsMoving(false);
|
||||||
|
|
||||||
|
// custom operation start
|
||||||
|
let operationSuccess: any = true;
|
||||||
|
if ((action === 'move' || action === 'end' || action === 'start') && onDateChange && isNotLikeOriginal) {
|
||||||
|
try {
|
||||||
|
const result = await onDateChange(newChangedTask, newChangedTask.barChildren);
|
||||||
|
if (result !== undefined) {
|
||||||
|
operationSuccess = result;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
operationSuccess = false;
|
||||||
|
}
|
||||||
|
} else if (onProgressChange && isNotLikeOriginal) {
|
||||||
|
try {
|
||||||
|
const result = await onProgressChange(newChangedTask, newChangedTask.barChildren);
|
||||||
|
if (result !== undefined) {
|
||||||
|
operationSuccess = result;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
operationSuccess = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If operation is failed - return old state
|
||||||
|
if (!operationSuccess) {
|
||||||
|
setFailedTask(originalSelectedTask);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isMoving &&
|
||||||
|
(ganttEvent.action === 'move' ||
|
||||||
|
ganttEvent.action === 'end' ||
|
||||||
|
ganttEvent.action === 'start' ||
|
||||||
|
ganttEvent.action === 'progress') &&
|
||||||
|
svg?.current
|
||||||
|
) {
|
||||||
|
svg.current.addEventListener('mousemove', handleMouseMove);
|
||||||
|
svg.current.addEventListener('mouseup', handleMouseUp);
|
||||||
|
setIsMoving(true);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
ganttEvent,
|
||||||
|
xStep,
|
||||||
|
initEventX1Delta,
|
||||||
|
onProgressChange,
|
||||||
|
timeStep,
|
||||||
|
onDateChange,
|
||||||
|
svg,
|
||||||
|
isMoving,
|
||||||
|
point,
|
||||||
|
rtl,
|
||||||
|
setFailedTask,
|
||||||
|
setGanttEvent,
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method is Start point of task change
|
||||||
|
*/
|
||||||
|
const handleBarEventStart = async (
|
||||||
|
action: GanttContentMoveAction,
|
||||||
|
task: BarTask,
|
||||||
|
event?: React.MouseEvent | React.KeyboardEvent,
|
||||||
|
) => {
|
||||||
|
if (!event) {
|
||||||
|
if (action === 'select') {
|
||||||
|
setSelectedTask(task.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Keyboard events
|
||||||
|
else if (isKeyboardEvent(event)) {
|
||||||
|
if (action === 'delete') {
|
||||||
|
if (onDelete) {
|
||||||
|
try {
|
||||||
|
const result = await onDelete(task);
|
||||||
|
if (result !== undefined && result) {
|
||||||
|
setGanttEvent({ action, changedTask: task });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error on Delete. ' + error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Mouse Events
|
||||||
|
else if (action === 'mouseenter') {
|
||||||
|
if (!ganttEvent.action) {
|
||||||
|
setGanttEvent({
|
||||||
|
action,
|
||||||
|
changedTask: task,
|
||||||
|
originalSelectedTask: task,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (action === 'mouseleave') {
|
||||||
|
if (ganttEvent.action === 'mouseenter') {
|
||||||
|
setGanttEvent({ action: '' });
|
||||||
|
}
|
||||||
|
} else if (action === 'dblclick') {
|
||||||
|
!!onDoubleClick && onDoubleClick(task);
|
||||||
|
} else if (action === 'click') {
|
||||||
|
!!onClick && onClick(task);
|
||||||
|
}
|
||||||
|
// Change task event start
|
||||||
|
else if (action === 'move') {
|
||||||
|
if (!svg?.current || !point) return;
|
||||||
|
point.x = event.clientX;
|
||||||
|
const cursor = point.matrixTransform(svg.current.getScreenCTM()?.inverse());
|
||||||
|
setInitEventX1Delta(cursor.x - task.x1);
|
||||||
|
setGanttEvent({
|
||||||
|
action,
|
||||||
|
changedTask: task,
|
||||||
|
originalSelectedTask: task,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setGanttEvent({
|
||||||
|
action,
|
||||||
|
changedTask: task,
|
||||||
|
originalSelectedTask: task,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBarEvent = (action, task, event) => {
|
||||||
|
if (['click'].includes(action)) {
|
||||||
|
if (!['start', 'end', 'progress'].includes(lastAction) && (!lastStart || lastStart === task.start)) {
|
||||||
|
handleBarEventStart(action, task, event);
|
||||||
|
}
|
||||||
|
lastAction = null;
|
||||||
|
lastStart = null;
|
||||||
|
} else if (['move', 'select'].includes(action)) {
|
||||||
|
lastStart = task.start;
|
||||||
|
handleBarEventStart(action, task, event);
|
||||||
|
} else {
|
||||||
|
lastStart = task.start;
|
||||||
|
lastAction = action;
|
||||||
|
handleBarEventStart(action, task, event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<g className="content">
|
||||||
|
<g className="arrows" fill={arrowColor} stroke={arrowColor}>
|
||||||
|
{tasks.map((task) => {
|
||||||
|
return task.barChildren.map((child) => {
|
||||||
|
return (
|
||||||
|
<Arrow
|
||||||
|
key={`Arrow from ${task.id} to ${tasks[child.index].id}`}
|
||||||
|
taskFrom={task}
|
||||||
|
taskTo={tasks[child.index]}
|
||||||
|
rowHeight={rowHeight}
|
||||||
|
taskHeight={taskHeight}
|
||||||
|
arrowIndent={arrowIndent}
|
||||||
|
rtl={rtl}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
</g>
|
||||||
|
<g className="bar" fontFamily={fontFamily} fontSize={fontSize}>
|
||||||
|
{tasks.map((task) => {
|
||||||
|
return (
|
||||||
|
<TaskItem
|
||||||
|
task={task}
|
||||||
|
arrowIndent={arrowIndent}
|
||||||
|
taskHeight={taskHeight}
|
||||||
|
isProgressChangeable={!!onProgressChange && !task.isDisabled}
|
||||||
|
isDateChangeable={!!onDateChange && !task.isDisabled}
|
||||||
|
isDelete={!task.isDisabled}
|
||||||
|
onEventStart={handleBarEvent}
|
||||||
|
key={task.id}
|
||||||
|
isSelected={!!selectedTask && task.id === selectedTask.id}
|
||||||
|
rtl={rtl}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,66 @@
|
|||||||
|
import React, { useRef, useEffect, forwardRef } from 'react';
|
||||||
|
import { cx } from '@emotion/css';
|
||||||
|
import { GridProps, Grid } from '../grid/grid';
|
||||||
|
import { CalendarProps, Calendar } from '../calendar/calendar';
|
||||||
|
import { TaskGanttContentProps, TaskGanttContent } from './task-gantt-content';
|
||||||
|
import { ganttVerticalContainer, horizontalContainer } from './style';
|
||||||
|
|
||||||
|
export type TaskGanttProps = {
|
||||||
|
gridProps: GridProps;
|
||||||
|
calendarProps: CalendarProps;
|
||||||
|
barProps: TaskGanttContentProps;
|
||||||
|
ganttHeight: number;
|
||||||
|
scrollY: number;
|
||||||
|
scrollX: number;
|
||||||
|
ref: any;
|
||||||
|
};
|
||||||
|
export const TaskGantt: React.FC<TaskGanttProps> = forwardRef(
|
||||||
|
({ gridProps, calendarProps, barProps, ganttHeight, scrollY, scrollX }, ref: any) => {
|
||||||
|
const ganttSVGRef = useRef<SVGSVGElement>(null);
|
||||||
|
const horizontalContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const newBarProps = { ...barProps, svg: ganttSVGRef };
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (horizontalContainerRef.current) {
|
||||||
|
horizontalContainerRef.current.scrollTop = scrollY;
|
||||||
|
}
|
||||||
|
}, [scrollY]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ref.current) {
|
||||||
|
ref.current.scrollLeft = scrollX;
|
||||||
|
}
|
||||||
|
}, [scrollX]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cx(ganttVerticalContainer)} ref={ref} dir="ltr">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={gridProps.svgWidth}
|
||||||
|
height={calendarProps.headerHeight}
|
||||||
|
fontFamily={barProps.fontFamily}
|
||||||
|
style={{ borderBottom: '1px solid #f0f0f0',fontWeight:700 }}
|
||||||
|
>
|
||||||
|
<Calendar {...calendarProps} />
|
||||||
|
</svg>
|
||||||
|
<div
|
||||||
|
ref={horizontalContainerRef}
|
||||||
|
className={cx(horizontalContainer)}
|
||||||
|
style={ganttHeight ? { maxHeight: ganttHeight, width: gridProps.svgWidth } : { width: gridProps.svgWidth }}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={gridProps.svgWidth}
|
||||||
|
height={barProps.rowHeight * (barProps.tasks.length || 3)}
|
||||||
|
fontFamily={barProps.fontFamily}
|
||||||
|
ref={ganttSVGRef}
|
||||||
|
style={{ borderBottom: '1px solid #f0f0f0' }}
|
||||||
|
>
|
||||||
|
<Grid {...gridProps} />
|
||||||
|
<TaskGanttContent {...newBarProps} />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
@ -0,0 +1,90 @@
|
|||||||
|
import React, { ReactChild } from 'react';
|
||||||
|
import { cx } from '@emotion/css';
|
||||||
|
import { Task } from '../../types/public-types';
|
||||||
|
import { addToDate } from '../../helpers/date-helper';
|
||||||
|
import { gridRowLine, gridRow, gridTick, gridHeightRow } from './style';
|
||||||
|
import { uid } from '@nocobase/utils/client';
|
||||||
|
|
||||||
|
export type GridBodyProps = {
|
||||||
|
tasks: Task[];
|
||||||
|
dates: Date[];
|
||||||
|
svgWidth: number;
|
||||||
|
rowHeight: number;
|
||||||
|
columnWidth: number;
|
||||||
|
todayColor: string;
|
||||||
|
rtl: boolean;
|
||||||
|
selectedRowKeys: any[];
|
||||||
|
};
|
||||||
|
const empty = [{ id: uid() }, { id: uid() }, { id: uid() }];
|
||||||
|
export const GridBody: React.FC<GridBodyProps> = ({
|
||||||
|
tasks,
|
||||||
|
dates,
|
||||||
|
rowHeight,
|
||||||
|
svgWidth,
|
||||||
|
columnWidth,
|
||||||
|
todayColor,
|
||||||
|
rtl,
|
||||||
|
selectedRowKeys,
|
||||||
|
}) => {
|
||||||
|
const data = tasks.length ? tasks : empty;
|
||||||
|
let y = 0;
|
||||||
|
const gridRows: ReactChild[] = [];
|
||||||
|
const rowLines: ReactChild[] = [
|
||||||
|
<line key="RowLineFirst" x="0" y1={0} x2={svgWidth} y2={0} className={cx(gridRowLine)} />,
|
||||||
|
];
|
||||||
|
for (const task of data) {
|
||||||
|
gridRows.push(
|
||||||
|
<rect
|
||||||
|
key={'Row' + task.id}
|
||||||
|
x="0"
|
||||||
|
y={y}
|
||||||
|
width={svgWidth}
|
||||||
|
height={rowHeight}
|
||||||
|
className={selectedRowKeys?.includes(+task.id) ? cx(gridHeightRow) : cx(gridRow)}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
rowLines.push(
|
||||||
|
<line
|
||||||
|
key={'RowLine' + task.id}
|
||||||
|
x="0"
|
||||||
|
y1={y + rowHeight}
|
||||||
|
x2={svgWidth}
|
||||||
|
y2={y + rowHeight}
|
||||||
|
className={cx(gridRowLine)}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
y += rowHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
let tickX = 0;
|
||||||
|
const ticks: ReactChild[] = [];
|
||||||
|
let today: ReactChild = <rect />;
|
||||||
|
for (let i = 0; i < dates.length; i++) {
|
||||||
|
const date = dates[i];
|
||||||
|
ticks.push(<line key={date.getTime()} x1={tickX} y1={0} x2={tickX} y2={y} className={cx(gridTick)} />);
|
||||||
|
if (
|
||||||
|
(i + 1 !== dates.length && date.getTime() < now.getTime() && dates[i + 1].getTime() >= now.getTime()) ||
|
||||||
|
// if current date is last
|
||||||
|
(i !== 0 &&
|
||||||
|
i + 1 === dates.length &&
|
||||||
|
date.getTime() < now.getTime() &&
|
||||||
|
addToDate(date, date.getTime() - dates[i - 1].getTime(), 'millisecond').getTime() >= now.getTime())
|
||||||
|
) {
|
||||||
|
today = <rect x={tickX} y={0} width={columnWidth} height={y} fill={todayColor} />;
|
||||||
|
}
|
||||||
|
// rtl for today
|
||||||
|
if (rtl && i + 1 !== dates.length && date.getTime() >= now.getTime() && dates[i + 1].getTime() < now.getTime()) {
|
||||||
|
today = <rect x={tickX + columnWidth} y={0} width={columnWidth} height={y} fill={todayColor} />;
|
||||||
|
}
|
||||||
|
tickX += columnWidth;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<g className="gridBody">
|
||||||
|
<g className="rows">{gridRows}</g>
|
||||||
|
<g className="rowLines">{rowLines}</g>
|
||||||
|
<g className="ticks">{ticks}</g>
|
||||||
|
<g className="today">{today}</g>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,11 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { GridBody, GridBodyProps } from "./grid-body";
|
||||||
|
|
||||||
|
export type GridProps = GridBodyProps;
|
||||||
|
export const Grid: React.FC<GridProps> = props => {
|
||||||
|
return (
|
||||||
|
<g className="grid">
|
||||||
|
<GridBody {...props} />
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,19 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
export const gridRow = css`
|
||||||
|
fill: #fff;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const gridHeightRow = css`
|
||||||
|
fill: #e6f7ff;
|
||||||
|
border-color: rgba(0, 0, 0, 0.03);
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const gridRowLine = css`
|
||||||
|
stroke: #f0f0f0;
|
||||||
|
stroke-width:0;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const gridTick = css`
|
||||||
|
stroke: #f0f0f0;
|
||||||
|
`;
|
@ -0,0 +1,106 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { BarTask } from "../../types/bar-task";
|
||||||
|
|
||||||
|
type ArrowProps = {
|
||||||
|
taskFrom: BarTask;
|
||||||
|
taskTo: BarTask;
|
||||||
|
rowHeight: number;
|
||||||
|
taskHeight: number;
|
||||||
|
arrowIndent: number;
|
||||||
|
rtl: boolean;
|
||||||
|
};
|
||||||
|
export const Arrow: React.FC<ArrowProps> = ({
|
||||||
|
taskFrom,
|
||||||
|
taskTo,
|
||||||
|
rowHeight,
|
||||||
|
taskHeight,
|
||||||
|
arrowIndent,
|
||||||
|
rtl,
|
||||||
|
}) => {
|
||||||
|
let path: string;
|
||||||
|
let trianglePoints: string;
|
||||||
|
if (rtl) {
|
||||||
|
[path, trianglePoints] = drownPathAndTriangleRTL(
|
||||||
|
taskFrom,
|
||||||
|
taskTo,
|
||||||
|
rowHeight,
|
||||||
|
taskHeight,
|
||||||
|
arrowIndent
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
[path, trianglePoints] = drownPathAndTriangle(
|
||||||
|
taskFrom,
|
||||||
|
taskTo,
|
||||||
|
rowHeight,
|
||||||
|
taskHeight,
|
||||||
|
arrowIndent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g className="arrow">
|
||||||
|
<path strokeWidth="1.5" d={path} fill="none" />
|
||||||
|
<polygon points={trianglePoints} />
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const drownPathAndTriangle = (
|
||||||
|
taskFrom: BarTask,
|
||||||
|
taskTo: BarTask,
|
||||||
|
rowHeight: number,
|
||||||
|
taskHeight: number,
|
||||||
|
arrowIndent: number
|
||||||
|
) => {
|
||||||
|
const indexCompare = taskFrom.index > taskTo.index ? -1 : 1;
|
||||||
|
const taskToEndPosition = taskTo.y + taskHeight / 2;
|
||||||
|
const taskFromEndPosition = taskFrom.x2 + arrowIndent * 2;
|
||||||
|
const taskFromHorizontalOffsetValue =
|
||||||
|
taskFromEndPosition < taskTo.x1 ? "" : `H ${taskTo.x1 - arrowIndent}`;
|
||||||
|
const taskToHorizontalOffsetValue =
|
||||||
|
taskFromEndPosition > taskTo.x1
|
||||||
|
? arrowIndent
|
||||||
|
: taskTo.x1 - taskFrom.x2 - arrowIndent;
|
||||||
|
|
||||||
|
const path = `M ${taskFrom.x2} ${taskFrom.y + taskHeight / 2}
|
||||||
|
h ${arrowIndent}
|
||||||
|
v ${(indexCompare * rowHeight) / 2}
|
||||||
|
${taskFromHorizontalOffsetValue}
|
||||||
|
V ${taskToEndPosition}
|
||||||
|
h ${taskToHorizontalOffsetValue}`;
|
||||||
|
|
||||||
|
const trianglePoints = `${taskTo.x1},${taskToEndPosition}
|
||||||
|
${taskTo.x1 - 5},${taskToEndPosition - 5}
|
||||||
|
${taskTo.x1 - 5},${taskToEndPosition + 5}`;
|
||||||
|
return [path, trianglePoints];
|
||||||
|
};
|
||||||
|
|
||||||
|
const drownPathAndTriangleRTL = (
|
||||||
|
taskFrom: BarTask,
|
||||||
|
taskTo: BarTask,
|
||||||
|
rowHeight: number,
|
||||||
|
taskHeight: number,
|
||||||
|
arrowIndent: number
|
||||||
|
) => {
|
||||||
|
const indexCompare = taskFrom.index > taskTo.index ? -1 : 1;
|
||||||
|
const taskToEndPosition = taskTo.y + taskHeight / 2;
|
||||||
|
const taskFromEndPosition = taskFrom.x1 - arrowIndent * 2;
|
||||||
|
const taskFromHorizontalOffsetValue =
|
||||||
|
taskFromEndPosition > taskTo.x2 ? "" : `H ${taskTo.x2 + arrowIndent}`;
|
||||||
|
const taskToHorizontalOffsetValue =
|
||||||
|
taskFromEndPosition < taskTo.x2
|
||||||
|
? -arrowIndent
|
||||||
|
: taskTo.x2 - taskFrom.x1 + arrowIndent;
|
||||||
|
|
||||||
|
const path = `M ${taskFrom.x1} ${taskFrom.y + taskHeight / 2}
|
||||||
|
h ${-arrowIndent}
|
||||||
|
v ${(indexCompare * rowHeight) / 2}
|
||||||
|
${taskFromHorizontalOffsetValue}
|
||||||
|
V ${taskToEndPosition}
|
||||||
|
h ${taskToHorizontalOffsetValue}`;
|
||||||
|
|
||||||
|
const trianglePoints = `${taskTo.x2},${taskToEndPosition}
|
||||||
|
${taskTo.x2 + 5},${taskToEndPosition + 5}
|
||||||
|
${taskTo.x2 + 5},${taskToEndPosition - 5}`;
|
||||||
|
return [path, trianglePoints];
|
||||||
|
};
|
@ -0,0 +1,35 @@
|
|||||||
|
import React, { SyntheticEvent, useRef, useEffect } from "react";
|
||||||
|
import { cx } from '@emotion/css';
|
||||||
|
import {scrollWrapper,horizontalScroll} from './style'
|
||||||
|
|
||||||
|
export const HorizontalScroll: React.FC<{
|
||||||
|
scroll: number;
|
||||||
|
svgWidth: number;
|
||||||
|
taskListWidth: number;
|
||||||
|
rtl: boolean;
|
||||||
|
onScroll: (event: SyntheticEvent<HTMLDivElement>) => void;
|
||||||
|
}> = ({ scroll, svgWidth, taskListWidth, rtl, onScroll }) => {
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (scrollRef.current) {
|
||||||
|
scrollRef.current.scrollLeft = scroll;
|
||||||
|
}
|
||||||
|
}, [scroll]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
dir="ltr"
|
||||||
|
style={{
|
||||||
|
margin: rtl
|
||||||
|
? `0px ${taskListWidth}px 0px 0px`
|
||||||
|
: `0px 0px 0px ${taskListWidth}px`,
|
||||||
|
}}
|
||||||
|
className={cx(scrollWrapper)}
|
||||||
|
onScroll={onScroll}
|
||||||
|
ref={scrollRef}
|
||||||
|
>
|
||||||
|
<div style={{ width: svgWidth }} className={cx(horizontalScroll)} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,101 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
|
export const scrollWrapper = css`
|
||||||
|
overflow: auto;
|
||||||
|
position: relative;
|
||||||
|
top: -14px;
|
||||||
|
max-width: 100%;
|
||||||
|
/*firefox*/
|
||||||
|
scrollbar-width: thin;
|
||||||
|
/*iPad*/
|
||||||
|
height: 1.2rem;
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 1.1rem;
|
||||||
|
height: 1.1rem;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-corner {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
border: 6px solid transparent;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
background: var(--palette-black-alpha-20, rgba(0, 0, 0, 0.2));
|
||||||
|
border-radius: 10px;
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb:hover {
|
||||||
|
border: 4px solid transparent;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
background: var(--palette-black-alpha-30, rgba(0, 0, 0, 0.3));
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const horizontalScroll = css`
|
||||||
|
height: 1px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const tooltipDefaultContainer = css`
|
||||||
|
padding: 12px;
|
||||||
|
background-color: #fff;
|
||||||
|
background-clip: padding-box;
|
||||||
|
border-radius: 2px;
|
||||||
|
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
b {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const tooltipDefaultContainerParagraph = css`
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
color: #666;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const tooltipDetailsContainer = css`
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const tooltipDetailsContainerHidden = css`
|
||||||
|
visibility: hidden;
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
pointer-events: none;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const verticalScroll = css`
|
||||||
|
overflow: hidden auto;
|
||||||
|
width: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
/*firefox*/
|
||||||
|
scrollbar-width: thin;
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 1.1rem;
|
||||||
|
height: 1.1rem;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-corner {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
border: 6px solid transparent;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
background: var(--palette-black-alpha-20, rgba(0, 0, 0, 0.2));
|
||||||
|
border-radius: 10px;
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb:hover {
|
||||||
|
border: 4px solid transparent;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
background: var(--palette-black-alpha-30, rgba(0, 0, 0, 0.3));
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
`;
|
@ -0,0 +1,137 @@
|
|||||||
|
import { cx } from '@emotion/css';
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import { getYmd } from '../../helpers/other-helper';
|
||||||
|
import { BarTask } from '../../types/bar-task';
|
||||||
|
import { Task } from '../../types/public-types';
|
||||||
|
import {
|
||||||
|
tooltipDefaultContainer,
|
||||||
|
tooltipDefaultContainerParagraph,
|
||||||
|
tooltipDetailsContainer,
|
||||||
|
tooltipDetailsContainerHidden,
|
||||||
|
} from './style';
|
||||||
|
|
||||||
|
export type TooltipProps = {
|
||||||
|
task: BarTask;
|
||||||
|
arrowIndent: number;
|
||||||
|
rtl: boolean;
|
||||||
|
svgContainerHeight: number;
|
||||||
|
svgContainerWidth: number;
|
||||||
|
svgWidth: number;
|
||||||
|
headerHeight: number;
|
||||||
|
taskListWidth: number;
|
||||||
|
scrollX: number;
|
||||||
|
scrollY: number;
|
||||||
|
rowHeight: number;
|
||||||
|
fontSize: string;
|
||||||
|
fontFamily: string;
|
||||||
|
TooltipContent: React.FC<{
|
||||||
|
task: Task;
|
||||||
|
fontSize: string;
|
||||||
|
fontFamily: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
export const Tooltip: React.FC<TooltipProps> = ({
|
||||||
|
task,
|
||||||
|
rowHeight,
|
||||||
|
rtl,
|
||||||
|
svgContainerHeight,
|
||||||
|
svgContainerWidth,
|
||||||
|
scrollX,
|
||||||
|
scrollY,
|
||||||
|
arrowIndent,
|
||||||
|
fontSize,
|
||||||
|
fontFamily,
|
||||||
|
headerHeight,
|
||||||
|
taskListWidth,
|
||||||
|
TooltipContent,
|
||||||
|
}) => {
|
||||||
|
const tooltipRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [relatedY, setRelatedY] = useState(0);
|
||||||
|
const [relatedX, setRelatedX] = useState(0);
|
||||||
|
useEffect(() => {
|
||||||
|
if (tooltipRef.current) {
|
||||||
|
const tooltipHeight = tooltipRef.current.offsetHeight * 1.1;
|
||||||
|
const tooltipWidth = tooltipRef.current.offsetWidth * 1.1;
|
||||||
|
|
||||||
|
let newRelatedY = task.index * rowHeight - scrollY + headerHeight;
|
||||||
|
let newRelatedX: number;
|
||||||
|
if (rtl) {
|
||||||
|
newRelatedX = task.x1 - arrowIndent * 1.5 - tooltipWidth - scrollX;
|
||||||
|
if (newRelatedX < 0) {
|
||||||
|
newRelatedX = task.x2 + arrowIndent * 1.5 - scrollX;
|
||||||
|
}
|
||||||
|
const tooltipLeftmostPoint = tooltipWidth + newRelatedX;
|
||||||
|
if (tooltipLeftmostPoint > svgContainerWidth) {
|
||||||
|
newRelatedX = svgContainerWidth - tooltipWidth;
|
||||||
|
newRelatedY += rowHeight;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newRelatedX = task.x2 + arrowIndent * 1.5 + taskListWidth - scrollX;
|
||||||
|
const tooltipLeftmostPoint = tooltipWidth + newRelatedX;
|
||||||
|
const fullChartWidth = taskListWidth + svgContainerWidth;
|
||||||
|
if (tooltipLeftmostPoint > fullChartWidth) {
|
||||||
|
newRelatedX = task.x1 + taskListWidth - arrowIndent * 1.5 - scrollX - tooltipWidth;
|
||||||
|
}
|
||||||
|
if (newRelatedX < taskListWidth) {
|
||||||
|
newRelatedX = svgContainerWidth + taskListWidth - tooltipWidth;
|
||||||
|
newRelatedY += rowHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tooltipLowerPoint = tooltipHeight + newRelatedY - scrollY;
|
||||||
|
if (tooltipLowerPoint > svgContainerHeight - scrollY) {
|
||||||
|
newRelatedY = svgContainerHeight - tooltipHeight;
|
||||||
|
}
|
||||||
|
setRelatedY(newRelatedY);
|
||||||
|
setRelatedX(newRelatedX);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
tooltipRef,
|
||||||
|
task,
|
||||||
|
arrowIndent,
|
||||||
|
scrollX,
|
||||||
|
scrollY,
|
||||||
|
headerHeight,
|
||||||
|
taskListWidth,
|
||||||
|
rowHeight,
|
||||||
|
svgContainerHeight,
|
||||||
|
svgContainerWidth,
|
||||||
|
rtl,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={tooltipRef}
|
||||||
|
className={relatedX ? cx(tooltipDetailsContainer) : cx(tooltipDetailsContainerHidden)}
|
||||||
|
style={{ left: relatedX, top: relatedY }}
|
||||||
|
>
|
||||||
|
<TooltipContent task={task} fontSize={fontSize} fontFamily={fontFamily} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StandardTooltipContent: React.FC<{
|
||||||
|
task: Task;
|
||||||
|
fontSize: string;
|
||||||
|
fontFamily: string;
|
||||||
|
}> = ({ task, fontSize, fontFamily }) => {
|
||||||
|
const style = {
|
||||||
|
fontSize,
|
||||||
|
fontFamily,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className={cx(tooltipDefaultContainer)} style={style}>
|
||||||
|
<b style={{ fontSize: fontSize + 6 }}>
|
||||||
|
{task.name}: {getYmd(task.start)} ~ {getYmd(task.end)}
|
||||||
|
</b>
|
||||||
|
{task.end.getTime() - task.start.getTime() !== 0 && (
|
||||||
|
<p className={cx(tooltipDefaultContainerParagraph)}>{`Duration: ${~~(
|
||||||
|
(task.end.getTime() - task.start.getTime()) /
|
||||||
|
(1000 * 60 * 60 * 24)
|
||||||
|
)} day(s)`}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p className={cx(tooltipDefaultContainerParagraph)}>{!!task.progress && `Progress: ${task.progress}%`}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,35 @@
|
|||||||
|
import React, { SyntheticEvent, useRef, useEffect } from 'react';
|
||||||
|
import { cx } from '@emotion/css';
|
||||||
|
import { verticalScroll } from './style';
|
||||||
|
|
||||||
|
export const VerticalScroll: React.FC<{
|
||||||
|
scroll: number;
|
||||||
|
ganttHeight: number;
|
||||||
|
ganttFullHeight: number;
|
||||||
|
headerHeight: number;
|
||||||
|
rtl: boolean;
|
||||||
|
onScroll: (event: SyntheticEvent<HTMLDivElement>) => void;
|
||||||
|
}> = ({ scroll, ganttHeight, ganttFullHeight, headerHeight, rtl, onScroll }) => {
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (scrollRef.current) {
|
||||||
|
scrollRef.current.scrollTop = scroll;
|
||||||
|
}
|
||||||
|
}, [scroll]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
maxHeight: ganttHeight,
|
||||||
|
marginTop: headerHeight,
|
||||||
|
marginLeft: rtl ? '' : '-1rem',
|
||||||
|
}}
|
||||||
|
className={cx(verticalScroll)}
|
||||||
|
onScroll={onScroll}
|
||||||
|
ref={scrollRef}
|
||||||
|
>
|
||||||
|
<div style={{ height: ganttFullHeight, width: 1 }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
type BarDateHandleProps = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
barCornerRadius: number;
|
||||||
|
onMouseDown: (event: React.MouseEvent<SVGRectElement, MouseEvent>) => void;
|
||||||
|
};
|
||||||
|
export const BarDateHandle: React.FC<BarDateHandleProps> = ({ x, y, width, height, barCornerRadius, onMouseDown }) => {
|
||||||
|
return (
|
||||||
|
<rect
|
||||||
|
x={x}
|
||||||
|
y={y}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
className={'barHandle'}
|
||||||
|
ry={barCornerRadius}
|
||||||
|
rx={barCornerRadius}
|
||||||
|
onMouseDown={onMouseDown}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,74 @@
|
|||||||
|
import { cx } from '@emotion/css';
|
||||||
|
import React from "react";
|
||||||
|
import { barBackground } from './style';
|
||||||
|
|
||||||
|
type BarDisplayProps = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
color?: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
isSelected: boolean;
|
||||||
|
/* progress start point */
|
||||||
|
progressX: number;
|
||||||
|
progressWidth: number;
|
||||||
|
barCornerRadius: number;
|
||||||
|
styles: {
|
||||||
|
backgroundColor: string;
|
||||||
|
backgroundSelectedColor: string;
|
||||||
|
progressColor: string;
|
||||||
|
progressSelectedColor: string;
|
||||||
|
};
|
||||||
|
onMouseDown: (event: React.MouseEvent<SVGPolygonElement, MouseEvent>) => void;
|
||||||
|
};
|
||||||
|
export const BarDisplay: React.FC<BarDisplayProps> = ({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
color,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
isSelected,
|
||||||
|
progressX,
|
||||||
|
progressWidth,
|
||||||
|
barCornerRadius,
|
||||||
|
styles,
|
||||||
|
onMouseDown,
|
||||||
|
}) => {
|
||||||
|
const getProcessColor = () => {
|
||||||
|
if (color) {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
return isSelected ? styles.progressSelectedColor : styles.progressColor;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBarColor = () => {
|
||||||
|
if (color) {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
return isSelected ? styles.backgroundSelectedColor : styles.backgroundColor;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g onMouseDown={onMouseDown}>
|
||||||
|
<rect
|
||||||
|
x={x}
|
||||||
|
width={width}
|
||||||
|
y={y}
|
||||||
|
height={height}
|
||||||
|
ry={barCornerRadius}
|
||||||
|
rx={barCornerRadius}
|
||||||
|
fill={getBarColor()}
|
||||||
|
className={cx(barBackground)}
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x={progressX}
|
||||||
|
width={progressWidth}
|
||||||
|
y={y}
|
||||||
|
height={height}
|
||||||
|
ry={barCornerRadius}
|
||||||
|
rx={barCornerRadius}
|
||||||
|
fill={getProcessColor()}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
type BarProgressHandleProps = {
|
||||||
|
progressPoint: string;
|
||||||
|
onMouseDown: (event: React.MouseEvent<SVGPolygonElement, MouseEvent>) => void;
|
||||||
|
};
|
||||||
|
export const BarProgressHandle: React.FC<BarProgressHandleProps> = ({ progressPoint, onMouseDown }) => {
|
||||||
|
return <polygon className={'barHandle'} points={progressPoint} onMouseDown={onMouseDown} />;
|
||||||
|
};
|
@ -0,0 +1,50 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { cx } from '@emotion/css';
|
||||||
|
import { getProgressPoint } from "../../../helpers/bar-helper";
|
||||||
|
import { BarDisplay } from "./bar-display";
|
||||||
|
import { BarProgressHandle } from "./bar-progress-handle";
|
||||||
|
import { TaskItemProps } from "../task-item";
|
||||||
|
import { barWrapper } from './style';
|
||||||
|
|
||||||
|
|
||||||
|
export const BarSmall: React.FC<TaskItemProps> = ({
|
||||||
|
task,
|
||||||
|
isProgressChangeable,
|
||||||
|
isDateChangeable,
|
||||||
|
onEventStart,
|
||||||
|
isSelected,
|
||||||
|
}) => {
|
||||||
|
const progressPoint = getProgressPoint(
|
||||||
|
task.progressWidth + task.x1+10,
|
||||||
|
task.y,
|
||||||
|
task.height
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<g className={cx(barWrapper)} tabIndex={0}>
|
||||||
|
<BarDisplay
|
||||||
|
x={task.x1}
|
||||||
|
y={task.y}
|
||||||
|
width={task.x2 - task.x1}
|
||||||
|
height={task.height}
|
||||||
|
progressX={task.progressX}
|
||||||
|
progressWidth={task.progressWidth}
|
||||||
|
barCornerRadius={task.barCornerRadius}
|
||||||
|
styles={task.styles}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onMouseDown={e => {
|
||||||
|
isDateChangeable && onEventStart("move", task, e);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<g className="handleGroup">
|
||||||
|
{isProgressChangeable && (
|
||||||
|
<BarProgressHandle
|
||||||
|
progressPoint={progressPoint}
|
||||||
|
onMouseDown={e => {
|
||||||
|
onEventStart("progress", task, e);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,80 @@
|
|||||||
|
import { cx } from '@emotion/css';
|
||||||
|
import React from "react";
|
||||||
|
import { getProgressPoint } from "../../../helpers/bar-helper";
|
||||||
|
import { TaskItemProps } from "../task-item";
|
||||||
|
import { BarDateHandle } from "./bar-date-handle";
|
||||||
|
import { BarDisplay } from "./bar-display";
|
||||||
|
import { BarProgressHandle } from "./bar-progress-handle";
|
||||||
|
import { barWrapper } from './style';
|
||||||
|
|
||||||
|
|
||||||
|
export const Bar: React.FC<TaskItemProps> = ({
|
||||||
|
task,
|
||||||
|
isProgressChangeable,
|
||||||
|
isDateChangeable,
|
||||||
|
rtl,
|
||||||
|
onEventStart,
|
||||||
|
isSelected,
|
||||||
|
}) => {
|
||||||
|
const progressPoint = getProgressPoint(
|
||||||
|
+!rtl * task.progressWidth + task.progressX,
|
||||||
|
task.y,
|
||||||
|
task.height
|
||||||
|
);
|
||||||
|
const handleHeight = task.height - 2;
|
||||||
|
return (
|
||||||
|
<g className={cx(barWrapper)} tabIndex={0}>
|
||||||
|
<BarDisplay
|
||||||
|
x={task.x1}
|
||||||
|
y={task.y}
|
||||||
|
color={task.color}
|
||||||
|
width={task.x2 - task.x1}
|
||||||
|
height={task.height}
|
||||||
|
progressX={task.progressX}
|
||||||
|
progressWidth={task.progressWidth}
|
||||||
|
barCornerRadius={task.barCornerRadius}
|
||||||
|
styles={task.styles}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onMouseDown={e => {
|
||||||
|
isDateChangeable && onEventStart("move", task, e);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<g className="handleGroup">
|
||||||
|
{isDateChangeable && (
|
||||||
|
<g>
|
||||||
|
{/* left */}
|
||||||
|
<BarDateHandle
|
||||||
|
x={task.x1 + 1}
|
||||||
|
y={task.y + 1}
|
||||||
|
width={task.handleWidth}
|
||||||
|
height={handleHeight}
|
||||||
|
barCornerRadius={task.barCornerRadius}
|
||||||
|
onMouseDown={e => {
|
||||||
|
onEventStart("start", task, e);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* right */}
|
||||||
|
<BarDateHandle
|
||||||
|
x={task.x2 - task.handleWidth - 1}
|
||||||
|
y={task.y + 1}
|
||||||
|
width={task.handleWidth}
|
||||||
|
height={handleHeight}
|
||||||
|
barCornerRadius={task.barCornerRadius}
|
||||||
|
onMouseDown={e => {
|
||||||
|
onEventStart("end", task, e);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
)}
|
||||||
|
{isProgressChangeable && (
|
||||||
|
<BarProgressHandle
|
||||||
|
progressPoint={progressPoint}
|
||||||
|
onMouseDown={e => {
|
||||||
|
onEventStart("progress", task, e);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,23 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
|
export const barWrapper = css`
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
.barHandle{
|
||||||
|
fill: #ddd;
|
||||||
|
cursor: ew-resize;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
&:hover .barHandle {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
|
||||||
|
export const barBackground = css`
|
||||||
|
user-select: none;
|
||||||
|
stroke-width: 0;
|
||||||
|
opacity: .6;
|
||||||
|
`;
|
@ -0,0 +1,38 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { cx } from '@emotion/css';
|
||||||
|
import { TaskItemProps } from "../task-item";
|
||||||
|
import { milestoneWrapper,milestoneBackground } from './style';
|
||||||
|
|
||||||
|
export const Milestone: React.FC<TaskItemProps> = ({
|
||||||
|
task,
|
||||||
|
isDateChangeable,
|
||||||
|
onEventStart,
|
||||||
|
isSelected,
|
||||||
|
}) => {
|
||||||
|
const transform = `rotate(45 ${task.x1 + task.height * 0.356}
|
||||||
|
${task.y + task.height * 0.85})`;
|
||||||
|
const getBarColor = () => {
|
||||||
|
return isSelected
|
||||||
|
? task.styles.backgroundSelectedColor
|
||||||
|
: task.styles.backgroundColor;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g tabIndex={0} className={cx(milestoneWrapper)}>
|
||||||
|
<rect
|
||||||
|
fill={getBarColor()}
|
||||||
|
x={task.x1}
|
||||||
|
width={task.height}
|
||||||
|
y={task.y}
|
||||||
|
height={task.height}
|
||||||
|
rx={task.barCornerRadius}
|
||||||
|
ry={task.barCornerRadius}
|
||||||
|
transform={transform}
|
||||||
|
className={cx(milestoneBackground)}
|
||||||
|
onMouseDown={e => {
|
||||||
|
isDateChangeable && onEventStart("move", task, e);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
|
export const milestoneWrapper =css`
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const milestoneBackground =css`
|
||||||
|
user-select: none;
|
||||||
|
`
|
@ -0,0 +1,34 @@
|
|||||||
|
import { cx } from '@emotion/css';
|
||||||
|
import React from 'react';
|
||||||
|
import { TaskItemProps } from '../task-item';
|
||||||
|
import { projectBackground, projectWrapper } from './style';
|
||||||
|
|
||||||
|
export const Project: React.FC<TaskItemProps> = ({ task, isSelected }) => {
|
||||||
|
const barColor = isSelected ? task.styles.backgroundSelectedColor : task.styles.backgroundColor;
|
||||||
|
const processColor = isSelected ? task.styles.progressSelectedColor : task.styles.progressColor;
|
||||||
|
const projectWith = task.x2 - task.x1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g tabIndex={0} className={cx(projectWrapper)}>
|
||||||
|
<rect
|
||||||
|
fill={task.color || barColor}
|
||||||
|
x={task.x1}
|
||||||
|
width={projectWith}
|
||||||
|
y={task.y}
|
||||||
|
height={task.height}
|
||||||
|
rx={task.barCornerRadius}
|
||||||
|
ry={task.barCornerRadius}
|
||||||
|
className={cx(projectBackground)}
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x={task.progressX}
|
||||||
|
width={task.progressWidth}
|
||||||
|
y={task.y}
|
||||||
|
height={task.height}
|
||||||
|
ry={task.barCornerRadius}
|
||||||
|
rx={task.barCornerRadius}
|
||||||
|
fill={task.color || processColor}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
|
export const projectWrapper =css `
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const projectBackground =css`
|
||||||
|
user-select: none;
|
||||||
|
opacity: 0.6;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const projectTop =css`
|
||||||
|
user-select: none;
|
||||||
|
`
|
@ -0,0 +1,37 @@
|
|||||||
|
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
|
export const barLabel =css`
|
||||||
|
fill: #fff;
|
||||||
|
text-anchor: middle;
|
||||||
|
font-weight: 400;
|
||||||
|
dominant-baseline: central;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
`
|
||||||
|
export const projectLabel =css`
|
||||||
|
fill: #130d0d;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.9em;
|
||||||
|
dominant-baseline: central;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
`
|
||||||
|
export const barLabelOutside =css`
|
||||||
|
fill: #555;
|
||||||
|
text-anchor: start;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
`
|
@ -0,0 +1,109 @@
|
|||||||
|
import { cx } from '@emotion/css';
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import { getYmd } from '../../helpers/other-helper';
|
||||||
|
import { BarTask } from '../../types/bar-task';
|
||||||
|
import { GanttContentMoveAction } from '../../types/gantt-task-actions';
|
||||||
|
import { Bar } from './bar/bar';
|
||||||
|
import { BarSmall } from './bar/bar-small';
|
||||||
|
import { Milestone } from './milestone/milestone';
|
||||||
|
import { Project } from './project/project';
|
||||||
|
import { barLabel, barLabelOutside, projectLabel } from './style';
|
||||||
|
|
||||||
|
export type TaskItemProps = {
|
||||||
|
task: BarTask;
|
||||||
|
arrowIndent: number;
|
||||||
|
taskHeight: number;
|
||||||
|
isProgressChangeable: boolean;
|
||||||
|
isDateChangeable: boolean;
|
||||||
|
isDelete: boolean;
|
||||||
|
isSelected: boolean;
|
||||||
|
rtl: boolean;
|
||||||
|
onEventStart: (
|
||||||
|
action: GanttContentMoveAction,
|
||||||
|
selectedTask: BarTask,
|
||||||
|
event?: React.MouseEvent | React.KeyboardEvent,
|
||||||
|
) => any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TaskItem: React.FC<TaskItemProps> = (props) => {
|
||||||
|
const { task, arrowIndent, isDelete, taskHeight, isSelected, rtl, onEventStart } = {
|
||||||
|
...props,
|
||||||
|
};
|
||||||
|
const textRef = useRef<SVGTextElement>(null);
|
||||||
|
const [taskItem, setTaskItem] = useState<JSX.Element>(<div />);
|
||||||
|
const [isTextInside, setIsTextInside] = useState(true);
|
||||||
|
const isProjectBar = task.typeInternal === 'project';
|
||||||
|
useEffect(() => {
|
||||||
|
switch (task.typeInternal) {
|
||||||
|
case 'milestone':
|
||||||
|
setTaskItem(<Milestone {...props} />);
|
||||||
|
break;
|
||||||
|
case 'project':
|
||||||
|
setTaskItem(<Project {...props} />);
|
||||||
|
break;
|
||||||
|
case 'smalltask':
|
||||||
|
setTaskItem(<BarSmall {...props} />);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
setTaskItem(<Bar {...props} />);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}, [task, isSelected]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (textRef.current) {
|
||||||
|
setIsTextInside(textRef.current.getBBox().width < task.x2 - task.x1);
|
||||||
|
}
|
||||||
|
}, [textRef, task]);
|
||||||
|
|
||||||
|
const getX = () => {
|
||||||
|
const width = task.x2 - task.x1;
|
||||||
|
const hasChild = task.barChildren.length > 0;
|
||||||
|
if (isTextInside) {
|
||||||
|
return task.x1 + width * 0.5;
|
||||||
|
}
|
||||||
|
if (rtl && textRef.current) {
|
||||||
|
return task.x1 - textRef.current.getBBox().width - arrowIndent * +hasChild - arrowIndent * 0.2;
|
||||||
|
} else {
|
||||||
|
return task.x1 + width + arrowIndent * +hasChild + arrowIndent * 0.2;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<g
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
switch (e.key) {
|
||||||
|
case 'Delete': {
|
||||||
|
if (isDelete) onEventStart('delete', task, e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
onEventStart('mouseenter', task, e);
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
onEventStart('mouseleave', task, e);
|
||||||
|
}}
|
||||||
|
onDoubleClick={(e) => {
|
||||||
|
onEventStart('dblclick', task, e);
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
onEventStart('click', task, e);
|
||||||
|
}}
|
||||||
|
onFocus={() => {
|
||||||
|
onEventStart('select', task);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{taskItem}
|
||||||
|
<text
|
||||||
|
x={isProjectBar ? task.x1 : getX()}
|
||||||
|
y={isProjectBar ? task.y - 8 : task.y + taskHeight * 0.5}
|
||||||
|
className={isProjectBar ? cx(projectLabel) : isTextInside ? cx(barLabel) : cx(barLabel) && cx(barLabelOutside)}
|
||||||
|
ref={textRef}
|
||||||
|
>
|
||||||
|
{isProjectBar ? `${task.name}: ${getYmd(task.start)} ~ ${getYmd(task.end)}` : task.name}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,6 @@
|
|||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
// export const GanttToolbarContext = createContext<any>(null);
|
||||||
|
export const CalendarContext = createContext(null);
|
||||||
|
export const DeleteEventContext = createContext(null);
|
||||||
|
|
@ -0,0 +1,490 @@
|
|||||||
|
import { Task } from '../types/public-types';
|
||||||
|
import { BarTask, TaskTypeInternal } from '../types/bar-task';
|
||||||
|
import { BarMoveAction } from '../types/gantt-task-actions';
|
||||||
|
|
||||||
|
export const convertToBarTasks = (
|
||||||
|
tasks: Task[],
|
||||||
|
dates: Date[],
|
||||||
|
columnWidth: number,
|
||||||
|
rowHeight: number,
|
||||||
|
taskHeight: number,
|
||||||
|
barCornerRadius: number,
|
||||||
|
handleWidth: number,
|
||||||
|
rtl: boolean,
|
||||||
|
barProgressColor: string,
|
||||||
|
barProgressSelectedColor: string,
|
||||||
|
barBackgroundColor: string,
|
||||||
|
barBackgroundSelectedColor: string,
|
||||||
|
projectProgressColor: string,
|
||||||
|
projectProgressSelectedColor: string,
|
||||||
|
projectBackgroundColor: string,
|
||||||
|
projectBackgroundSelectedColor: string,
|
||||||
|
milestoneBackgroundColor: string,
|
||||||
|
milestoneBackgroundSelectedColor: string,
|
||||||
|
) => {
|
||||||
|
let barTasks = tasks.map((t, i) => {
|
||||||
|
return convertToBarTask(
|
||||||
|
t,
|
||||||
|
i,
|
||||||
|
dates,
|
||||||
|
columnWidth,
|
||||||
|
rowHeight,
|
||||||
|
taskHeight,
|
||||||
|
barCornerRadius,
|
||||||
|
handleWidth,
|
||||||
|
rtl,
|
||||||
|
barProgressColor,
|
||||||
|
barProgressSelectedColor,
|
||||||
|
barBackgroundColor,
|
||||||
|
barBackgroundSelectedColor,
|
||||||
|
projectProgressColor,
|
||||||
|
projectProgressSelectedColor,
|
||||||
|
projectBackgroundColor,
|
||||||
|
projectBackgroundSelectedColor,
|
||||||
|
milestoneBackgroundColor,
|
||||||
|
milestoneBackgroundSelectedColor,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// set dependencies
|
||||||
|
barTasks = barTasks.map((task) => {
|
||||||
|
const dependencies = task.dependencies || [];
|
||||||
|
for (let j = 0; j < dependencies.length; j++) {
|
||||||
|
const dependence = barTasks.findIndex((value) => value.id === dependencies[j]);
|
||||||
|
if (dependence !== -1) barTasks[dependence].barChildren.push(task);
|
||||||
|
}
|
||||||
|
return task;
|
||||||
|
});
|
||||||
|
|
||||||
|
return barTasks;
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertToBarTask = (
|
||||||
|
task: Task,
|
||||||
|
index: number,
|
||||||
|
dates: Date[],
|
||||||
|
columnWidth: number,
|
||||||
|
rowHeight: number,
|
||||||
|
taskHeight: number,
|
||||||
|
barCornerRadius: number,
|
||||||
|
handleWidth: number,
|
||||||
|
rtl: boolean,
|
||||||
|
barProgressColor: string,
|
||||||
|
barProgressSelectedColor: string,
|
||||||
|
barBackgroundColor: string,
|
||||||
|
barBackgroundSelectedColor: string,
|
||||||
|
projectProgressColor: string,
|
||||||
|
projectProgressSelectedColor: string,
|
||||||
|
projectBackgroundColor: string,
|
||||||
|
projectBackgroundSelectedColor: string,
|
||||||
|
milestoneBackgroundColor: string,
|
||||||
|
milestoneBackgroundSelectedColor: string,
|
||||||
|
): BarTask => {
|
||||||
|
let barTask: BarTask;
|
||||||
|
switch (task.type) {
|
||||||
|
case 'milestone':
|
||||||
|
barTask = convertToMilestone(
|
||||||
|
task,
|
||||||
|
index,
|
||||||
|
dates,
|
||||||
|
columnWidth,
|
||||||
|
rowHeight,
|
||||||
|
taskHeight,
|
||||||
|
barCornerRadius,
|
||||||
|
handleWidth,
|
||||||
|
milestoneBackgroundColor,
|
||||||
|
milestoneBackgroundSelectedColor,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'project':
|
||||||
|
barTask = convertToBar(
|
||||||
|
task,
|
||||||
|
index,
|
||||||
|
dates,
|
||||||
|
columnWidth,
|
||||||
|
rowHeight,
|
||||||
|
taskHeight * 0.5,
|
||||||
|
barCornerRadius,
|
||||||
|
handleWidth,
|
||||||
|
rtl,
|
||||||
|
projectProgressColor,
|
||||||
|
projectProgressSelectedColor,
|
||||||
|
projectBackgroundColor,
|
||||||
|
projectBackgroundSelectedColor,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
barTask = convertToBar(
|
||||||
|
task,
|
||||||
|
index,
|
||||||
|
dates,
|
||||||
|
columnWidth,
|
||||||
|
rowHeight,
|
||||||
|
taskHeight,
|
||||||
|
barCornerRadius,
|
||||||
|
handleWidth,
|
||||||
|
rtl,
|
||||||
|
barProgressColor,
|
||||||
|
barProgressSelectedColor,
|
||||||
|
barBackgroundColor,
|
||||||
|
barBackgroundSelectedColor,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return barTask;
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertToBar = (
|
||||||
|
task: Task,
|
||||||
|
index: number,
|
||||||
|
dates: Date[],
|
||||||
|
columnWidth: number,
|
||||||
|
rowHeight: number,
|
||||||
|
taskHeight: number,
|
||||||
|
barCornerRadius: number,
|
||||||
|
handleWidth: number,
|
||||||
|
rtl: boolean,
|
||||||
|
barProgressColor: string,
|
||||||
|
barProgressSelectedColor: string,
|
||||||
|
barBackgroundColor: string,
|
||||||
|
barBackgroundSelectedColor: string,
|
||||||
|
): BarTask => {
|
||||||
|
let x1: number;
|
||||||
|
let x2: number;
|
||||||
|
if (rtl) {
|
||||||
|
x2 = taskXCoordinateRTL(task.start, dates, columnWidth);
|
||||||
|
x1 = taskXCoordinateRTL(task.end, dates, columnWidth);
|
||||||
|
} else {
|
||||||
|
x1 = taskXCoordinate(task.start, dates, columnWidth);
|
||||||
|
x2 = taskXCoordinate(task.end, dates, columnWidth);
|
||||||
|
}
|
||||||
|
let typeInternal: TaskTypeInternal = task.type;
|
||||||
|
if (typeInternal === 'task' && x2 - x1 < handleWidth * 2) {
|
||||||
|
typeInternal = 'smalltask';
|
||||||
|
x2 = x1 + handleWidth * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [progressWidth, progressX] = progressWithByParams(x1, x2, task.progress, rtl);
|
||||||
|
const y =
|
||||||
|
task.type === 'project'
|
||||||
|
? taskYCoordinate(index, rowHeight, taskHeight) +8
|
||||||
|
: taskYCoordinate(index, rowHeight, taskHeight);
|
||||||
|
const hideChildren = task.type === 'project' ? task.hideChildren : undefined;
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
backgroundColor: barBackgroundColor,
|
||||||
|
backgroundSelectedColor: barBackgroundSelectedColor,
|
||||||
|
progressColor: barProgressColor,
|
||||||
|
progressSelectedColor: barProgressSelectedColor,
|
||||||
|
...task.styles,
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
...task,
|
||||||
|
typeInternal,
|
||||||
|
x1,
|
||||||
|
x2,
|
||||||
|
y,
|
||||||
|
index,
|
||||||
|
progressX,
|
||||||
|
progressWidth,
|
||||||
|
barCornerRadius,
|
||||||
|
handleWidth,
|
||||||
|
hideChildren,
|
||||||
|
height: taskHeight,
|
||||||
|
barChildren: [],
|
||||||
|
styles,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertToMilestone = (
|
||||||
|
task: Task,
|
||||||
|
index: number,
|
||||||
|
dates: Date[],
|
||||||
|
columnWidth: number,
|
||||||
|
rowHeight: number,
|
||||||
|
taskHeight: number,
|
||||||
|
barCornerRadius: number,
|
||||||
|
handleWidth: number,
|
||||||
|
milestoneBackgroundColor: string,
|
||||||
|
milestoneBackgroundSelectedColor: string,
|
||||||
|
): BarTask => {
|
||||||
|
const x = taskXCoordinate(task.start, dates, columnWidth);
|
||||||
|
const y = taskYCoordinate(index, rowHeight, taskHeight);
|
||||||
|
|
||||||
|
const x1 = x - taskHeight * 0.5;
|
||||||
|
const x2 = x + taskHeight * 0.5;
|
||||||
|
|
||||||
|
const rotatedHeight = taskHeight / 1.414;
|
||||||
|
const styles = {
|
||||||
|
backgroundColor: milestoneBackgroundColor,
|
||||||
|
backgroundSelectedColor: milestoneBackgroundSelectedColor,
|
||||||
|
progressColor: '',
|
||||||
|
progressSelectedColor: '',
|
||||||
|
...task.styles,
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
...task,
|
||||||
|
end: task.start,
|
||||||
|
x1,
|
||||||
|
x2,
|
||||||
|
y,
|
||||||
|
index,
|
||||||
|
progressX: 0,
|
||||||
|
progressWidth: 0,
|
||||||
|
barCornerRadius,
|
||||||
|
handleWidth,
|
||||||
|
typeInternal: task.type,
|
||||||
|
progress: 0,
|
||||||
|
height: rotatedHeight,
|
||||||
|
hideChildren: undefined,
|
||||||
|
barChildren: [],
|
||||||
|
styles,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const taskXCoordinate = (xDate: Date, dates: Date[], columnWidth: number) => {
|
||||||
|
const index = dates.findIndex((d) => d?.getTime() >= xDate?.getTime()) - 1;
|
||||||
|
|
||||||
|
const remainderMillis = xDate?.getTime() - dates[index]?.getTime();
|
||||||
|
const percentOfInterval = remainderMillis / (dates[index + 1]?.getTime() - dates[index]?.getTime());
|
||||||
|
const x = index * columnWidth + percentOfInterval * columnWidth;
|
||||||
|
return x;
|
||||||
|
};
|
||||||
|
const taskXCoordinateRTL = (xDate: Date, dates: Date[], columnWidth: number) => {
|
||||||
|
let x = taskXCoordinate(xDate, dates, columnWidth);
|
||||||
|
x += columnWidth;
|
||||||
|
return x;
|
||||||
|
};
|
||||||
|
const taskYCoordinate = (index: number, rowHeight: number, taskHeight: number) => {
|
||||||
|
const y = index * rowHeight + (rowHeight - taskHeight) / 2;
|
||||||
|
return y;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const progressWithByParams = (taskX1: number, taskX2: number, progress: number, rtl: boolean) => {
|
||||||
|
const progressWidth = (taskX2 - taskX1) * progress * 0.01;
|
||||||
|
let progressX: number;
|
||||||
|
if (rtl) {
|
||||||
|
progressX = taskX2 - progressWidth;
|
||||||
|
} else {
|
||||||
|
progressX = taskX1;
|
||||||
|
}
|
||||||
|
return [progressWidth, progressX];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const progressByProgressWidth = (progressWidth: number, barTask: BarTask) => {
|
||||||
|
const barWidth = barTask.x2 - barTask.x1;
|
||||||
|
const progressPercent = Math.round((progressWidth * 100) / barWidth);
|
||||||
|
if (progressPercent >= 100) return 100;
|
||||||
|
else if (progressPercent <= 0) return 0;
|
||||||
|
else return progressPercent;
|
||||||
|
};
|
||||||
|
|
||||||
|
const progressByX = (x: number, task: BarTask) => {
|
||||||
|
if (x >= task.x2) return 100;
|
||||||
|
else if (x <= task.x1) return 0;
|
||||||
|
else {
|
||||||
|
const barWidth = task.x2 - task.x1;
|
||||||
|
const progressPercent = Math.round(((x - task.x1) * 100) / barWidth);
|
||||||
|
return progressPercent;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const progressByXRTL = (x: number, task: BarTask) => {
|
||||||
|
if (x >= task.x2) return 0;
|
||||||
|
else if (x <= task.x1) return 100;
|
||||||
|
else {
|
||||||
|
const barWidth = task.x2 - task.x1;
|
||||||
|
const progressPercent = Math.round(((task.x2 - x) * 100) / barWidth);
|
||||||
|
return progressPercent;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getProgressPoint = (progressX: number, taskY: number, taskHeight: number) => {
|
||||||
|
const point = [
|
||||||
|
progressX - 5,
|
||||||
|
taskY + taskHeight,
|
||||||
|
progressX + 5,
|
||||||
|
taskY + taskHeight,
|
||||||
|
progressX,
|
||||||
|
taskY + taskHeight - 8.66,
|
||||||
|
];
|
||||||
|
return point.join(',');
|
||||||
|
};
|
||||||
|
|
||||||
|
const startByX = (x: number, xStep: number, task: BarTask) => {
|
||||||
|
if (x >= task.x2 - task.handleWidth * 2) {
|
||||||
|
x = task.x2 - task.handleWidth * 2;
|
||||||
|
}
|
||||||
|
const steps = Math.round((x - task.x1) / xStep);
|
||||||
|
const additionalXValue = steps * xStep;
|
||||||
|
const newX = task.x1 + additionalXValue;
|
||||||
|
return newX;
|
||||||
|
};
|
||||||
|
|
||||||
|
const endByX = (x: number, xStep: number, task: BarTask) => {
|
||||||
|
if (x <= task.x1 + task.handleWidth * 2) {
|
||||||
|
x = task.x1 + task.handleWidth * 2;
|
||||||
|
}
|
||||||
|
const steps = Math.round((x - task.x2) / xStep);
|
||||||
|
const additionalXValue = steps * xStep;
|
||||||
|
const newX = task.x2 + additionalXValue;
|
||||||
|
return newX;
|
||||||
|
};
|
||||||
|
|
||||||
|
const moveByX = (x: number, xStep: number, task: BarTask) => {
|
||||||
|
const steps = Math.round((x - task.x1) / xStep);
|
||||||
|
const additionalXValue = steps * xStep;
|
||||||
|
const newX1 = task.x1 + additionalXValue;
|
||||||
|
const newX2 = newX1 + task.x2 - task.x1;
|
||||||
|
return [newX1, newX2];
|
||||||
|
};
|
||||||
|
|
||||||
|
const dateByX = (x: number, taskX: number, taskDate: Date, xStep: number, timeStep: number) => {
|
||||||
|
let newDate = new Date(((x - taskX) / xStep) * timeStep + taskDate.getTime());
|
||||||
|
newDate = new Date(newDate.getTime() + (newDate.getTimezoneOffset() - taskDate.getTimezoneOffset()) * 60000);
|
||||||
|
return newDate;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method handles event in real time(mousemove) and on finish(mouseup)
|
||||||
|
*/
|
||||||
|
export const handleTaskBySVGMouseEvent = (
|
||||||
|
svgX: number,
|
||||||
|
action: BarMoveAction,
|
||||||
|
selectedTask: BarTask,
|
||||||
|
xStep: number,
|
||||||
|
timeStep: number,
|
||||||
|
initEventX1Delta: number,
|
||||||
|
rtl: boolean,
|
||||||
|
): { isChanged: boolean; changedTask: BarTask } => {
|
||||||
|
let result: { isChanged: boolean; changedTask: BarTask };
|
||||||
|
switch (selectedTask.type) {
|
||||||
|
case 'milestone':
|
||||||
|
result = handleTaskBySVGMouseEventForMilestone(svgX, action, selectedTask, xStep, timeStep, initEventX1Delta);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
result = handleTaskBySVGMouseEventForBar(svgX, action, selectedTask, xStep, timeStep, initEventX1Delta, rtl);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTaskBySVGMouseEventForBar = (
|
||||||
|
svgX: number,
|
||||||
|
action: BarMoveAction,
|
||||||
|
selectedTask: BarTask,
|
||||||
|
xStep: number,
|
||||||
|
timeStep: number,
|
||||||
|
initEventX1Delta: number,
|
||||||
|
rtl: boolean,
|
||||||
|
): { isChanged: boolean; changedTask: BarTask } => {
|
||||||
|
const changedTask: BarTask = { ...selectedTask };
|
||||||
|
let isChanged = false;
|
||||||
|
switch (action) {
|
||||||
|
case 'progress':
|
||||||
|
if (rtl) {
|
||||||
|
changedTask.progress = progressByXRTL(svgX, selectedTask);
|
||||||
|
} else {
|
||||||
|
changedTask.progress = progressByX(svgX, selectedTask);
|
||||||
|
}
|
||||||
|
isChanged = changedTask.progress !== selectedTask.progress;
|
||||||
|
if (isChanged) {
|
||||||
|
const [progressWidth, progressX] = progressWithByParams(
|
||||||
|
changedTask.x1,
|
||||||
|
changedTask.x2,
|
||||||
|
changedTask.progress,
|
||||||
|
rtl,
|
||||||
|
);
|
||||||
|
changedTask.progressWidth = progressWidth;
|
||||||
|
changedTask.progressX = progressX;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'start': {
|
||||||
|
const newX1 = startByX(svgX, xStep, selectedTask);
|
||||||
|
changedTask.x1 = newX1;
|
||||||
|
isChanged = changedTask.x1 !== selectedTask.x1;
|
||||||
|
if (isChanged) {
|
||||||
|
if (rtl) {
|
||||||
|
changedTask.end = dateByX(newX1, selectedTask.x1, selectedTask.end, xStep, timeStep);
|
||||||
|
} else {
|
||||||
|
changedTask.start = dateByX(newX1, selectedTask.x1, selectedTask.start, xStep, timeStep);
|
||||||
|
}
|
||||||
|
const [progressWidth, progressX] = progressWithByParams(
|
||||||
|
changedTask.x1,
|
||||||
|
changedTask.x2,
|
||||||
|
changedTask.progress,
|
||||||
|
rtl,
|
||||||
|
);
|
||||||
|
changedTask.progressWidth = progressWidth;
|
||||||
|
changedTask.progressX = progressX;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'end': {
|
||||||
|
const newX2 = endByX(svgX, xStep, selectedTask);
|
||||||
|
changedTask.x2 = newX2;
|
||||||
|
isChanged = changedTask.x2 !== selectedTask.x2;
|
||||||
|
if (isChanged) {
|
||||||
|
if (rtl) {
|
||||||
|
changedTask.start = dateByX(newX2, selectedTask.x2, selectedTask.start, xStep, timeStep);
|
||||||
|
} else {
|
||||||
|
changedTask.end = dateByX(newX2, selectedTask.x2, selectedTask.end, xStep, timeStep);
|
||||||
|
}
|
||||||
|
const [progressWidth, progressX] = progressWithByParams(
|
||||||
|
changedTask.x1,
|
||||||
|
changedTask.x2,
|
||||||
|
changedTask.progress,
|
||||||
|
rtl,
|
||||||
|
);
|
||||||
|
changedTask.progressWidth = progressWidth;
|
||||||
|
changedTask.progressX = progressX;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'move': {
|
||||||
|
const [newMoveX1, newMoveX2] = moveByX(svgX - initEventX1Delta, xStep, selectedTask);
|
||||||
|
isChanged = newMoveX1 !== selectedTask.x1;
|
||||||
|
if (isChanged) {
|
||||||
|
changedTask.start = dateByX(newMoveX1, selectedTask.x1, selectedTask.start, xStep, timeStep);
|
||||||
|
changedTask.end = dateByX(newMoveX2, selectedTask.x2, selectedTask.end, xStep, timeStep);
|
||||||
|
changedTask.x1 = newMoveX1;
|
||||||
|
changedTask.x2 = newMoveX2;
|
||||||
|
const [progressWidth, progressX] = progressWithByParams(
|
||||||
|
changedTask.x1,
|
||||||
|
changedTask.x2,
|
||||||
|
changedTask.progress,
|
||||||
|
rtl,
|
||||||
|
);
|
||||||
|
changedTask.progressWidth = progressWidth;
|
||||||
|
changedTask.progressX = progressX;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { isChanged, changedTask };
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTaskBySVGMouseEventForMilestone = (
|
||||||
|
svgX: number,
|
||||||
|
action: BarMoveAction,
|
||||||
|
selectedTask: BarTask,
|
||||||
|
xStep: number,
|
||||||
|
timeStep: number,
|
||||||
|
initEventX1Delta: number,
|
||||||
|
): { isChanged: boolean; changedTask: BarTask } => {
|
||||||
|
const changedTask: BarTask = { ...selectedTask };
|
||||||
|
let isChanged = false;
|
||||||
|
switch (action) {
|
||||||
|
case 'move': {
|
||||||
|
const [newMoveX1, newMoveX2] = moveByX(svgX - initEventX1Delta, xStep, selectedTask);
|
||||||
|
isChanged = newMoveX1 !== selectedTask.x1;
|
||||||
|
if (isChanged) {
|
||||||
|
changedTask.start = dateByX(newMoveX1, selectedTask.x1, selectedTask.start, xStep, timeStep);
|
||||||
|
changedTask.end = changedTask.start;
|
||||||
|
changedTask.x1 = newMoveX1;
|
||||||
|
changedTask.x2 = newMoveX2;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { isChanged, changedTask };
|
||||||
|
};
|
@ -0,0 +1,203 @@
|
|||||||
|
import { Task, ViewMode } from '../types/public-types';
|
||||||
|
|
||||||
|
const DateTimeFormat = Intl.DateTimeFormat;
|
||||||
|
type DateTimeFormat = typeof DateTimeFormat;
|
||||||
|
//@ts-ignore
|
||||||
|
const DateTimeFormatOptions = Intl.DateTimeFormatOptions;
|
||||||
|
type DateTimeFormatOptions = typeof DateTimeFormatOptions;
|
||||||
|
type DateHelperScales = 'year' | 'month' | 'day' | 'hour' | 'minute' | 'second' | 'millisecond';
|
||||||
|
|
||||||
|
const intlDTCache = {};
|
||||||
|
export const getCachedDateTimeFormat = (
|
||||||
|
locString: string | string[],
|
||||||
|
opts: DateTimeFormatOptions = {},
|
||||||
|
): DateTimeFormat => {
|
||||||
|
const key = JSON.stringify([locString, opts]);
|
||||||
|
let dtf = intlDTCache[key];
|
||||||
|
if (!dtf) {
|
||||||
|
dtf = new Intl.DateTimeFormat(locString, opts);
|
||||||
|
intlDTCache[key] = dtf;
|
||||||
|
}
|
||||||
|
return dtf;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addToDate = (date: Date, quantity: number, scale: DateHelperScales) => {
|
||||||
|
const newDate = new Date(
|
||||||
|
date.getFullYear() + (scale === 'year' ? quantity : 0),
|
||||||
|
date.getMonth() + (scale === 'month' ? quantity : 0),
|
||||||
|
date.getDate() + (scale === 'day' ? quantity : 0),
|
||||||
|
date.getHours() + (scale === 'hour' ? quantity : 0),
|
||||||
|
date.getMinutes() + (scale === 'minute' ? quantity : 0),
|
||||||
|
date.getSeconds() + (scale === 'second' ? quantity : 0),
|
||||||
|
date.getMilliseconds() + (scale === 'millisecond' ? quantity : 0),
|
||||||
|
);
|
||||||
|
return newDate;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const startOfDate = (date: Date, scale: DateHelperScales) => {
|
||||||
|
const scores = ['millisecond', 'second', 'minute', 'hour', 'day', 'month', 'year'];
|
||||||
|
|
||||||
|
const shouldReset = (_scale: DateHelperScales) => {
|
||||||
|
const maxScore = scores.indexOf(scale);
|
||||||
|
return scores.indexOf(_scale) <= maxScore;
|
||||||
|
};
|
||||||
|
const newDate = new Date(
|
||||||
|
date?.getFullYear(),
|
||||||
|
shouldReset('year') ? 0 : date.getMonth(),
|
||||||
|
shouldReset('month') ? 1 : date.getDate(),
|
||||||
|
shouldReset('day') ? 0 : date.getHours(),
|
||||||
|
shouldReset('hour') ? 0 : date.getMinutes(),
|
||||||
|
shouldReset('minute') ? 0 : date.getSeconds(),
|
||||||
|
shouldReset('second') ? 0 : date.getMilliseconds(),
|
||||||
|
);
|
||||||
|
return newDate;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ganttDateRange = (tasks: Task[], viewMode: ViewMode, preStepsCount: number) => {
|
||||||
|
let newStartDate: Date = tasks[0]?.start || new Date();
|
||||||
|
let newEndDate: Date = tasks[0]?.start || new Date() ;
|
||||||
|
for (const task of tasks) {
|
||||||
|
if (task.start < newStartDate) {
|
||||||
|
newStartDate = task.start;
|
||||||
|
}
|
||||||
|
if (task.end > newEndDate) {
|
||||||
|
newEndDate = task.end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (viewMode) {
|
||||||
|
case ViewMode.Year:
|
||||||
|
newStartDate = addToDate(newStartDate, -1, 'year');
|
||||||
|
newStartDate = startOfDate(newStartDate, 'year');
|
||||||
|
newEndDate = addToDate(newEndDate, 1, 'year');
|
||||||
|
newEndDate = startOfDate(newEndDate, 'year');
|
||||||
|
break;
|
||||||
|
case ViewMode.QuarterYear:
|
||||||
|
newStartDate = addToDate(newStartDate, -3, 'month');
|
||||||
|
newStartDate = startOfDate(newStartDate, 'month');
|
||||||
|
newEndDate = addToDate(newEndDate, 3, 'year');
|
||||||
|
newEndDate = startOfDate(newEndDate, 'year');
|
||||||
|
break;
|
||||||
|
case ViewMode.Month:
|
||||||
|
newStartDate = addToDate(newStartDate, -1 * preStepsCount, 'month');
|
||||||
|
newStartDate = startOfDate(newStartDate, 'month');
|
||||||
|
newEndDate = addToDate(newEndDate, 1, 'year');
|
||||||
|
newEndDate = startOfDate(newEndDate, 'year');
|
||||||
|
break;
|
||||||
|
case ViewMode.Week:
|
||||||
|
newStartDate = startOfDate(newStartDate, 'day');
|
||||||
|
newStartDate = addToDate(getMonday(newStartDate), -7 * preStepsCount, 'day');
|
||||||
|
newEndDate = startOfDate(newEndDate, 'day');
|
||||||
|
newEndDate = addToDate(newEndDate, 1.5, 'month');
|
||||||
|
break;
|
||||||
|
case ViewMode.Day:
|
||||||
|
newStartDate = startOfDate(newStartDate, 'day');
|
||||||
|
newStartDate = addToDate(newStartDate, -1 * preStepsCount, 'day');
|
||||||
|
newEndDate = startOfDate(newEndDate, 'day');
|
||||||
|
newEndDate = addToDate(newEndDate, 19, 'day');
|
||||||
|
break;
|
||||||
|
case ViewMode.QuarterDay:
|
||||||
|
newStartDate = startOfDate(newStartDate, 'day');
|
||||||
|
newStartDate = addToDate(newStartDate, -1 * preStepsCount, 'day');
|
||||||
|
newEndDate = startOfDate(newEndDate, 'day');
|
||||||
|
newEndDate = addToDate(newEndDate, 66, 'hour'); // 24(1 day)*3 - 6
|
||||||
|
break;
|
||||||
|
case ViewMode.HalfDay:
|
||||||
|
newStartDate = startOfDate(newStartDate, 'day');
|
||||||
|
newStartDate = addToDate(newStartDate, -1 * preStepsCount, 'day');
|
||||||
|
newEndDate = startOfDate(newEndDate, 'day');
|
||||||
|
newEndDate = addToDate(newEndDate, 108, 'hour'); // 24(1 day)*5 - 12
|
||||||
|
break;
|
||||||
|
case ViewMode.Hour:
|
||||||
|
newStartDate = startOfDate(newStartDate, 'hour');
|
||||||
|
newStartDate = addToDate(newStartDate, -1 * preStepsCount, 'hour');
|
||||||
|
newEndDate = startOfDate(newEndDate, 'day');
|
||||||
|
newEndDate = addToDate(newEndDate, 1, 'day');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return [newStartDate, newEndDate];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const seedDates = (startDate: Date, endDate: Date, viewMode: ViewMode) => {
|
||||||
|
let currentDate: Date = new Date(startDate);
|
||||||
|
const dates: Date[] = [currentDate];
|
||||||
|
while (currentDate < endDate) {
|
||||||
|
switch (viewMode) {
|
||||||
|
case ViewMode.Year:
|
||||||
|
currentDate = addToDate(currentDate, 1, 'year');
|
||||||
|
break;
|
||||||
|
case ViewMode.QuarterYear:
|
||||||
|
currentDate = addToDate(currentDate, 3, 'month');
|
||||||
|
break;
|
||||||
|
case ViewMode.Month:
|
||||||
|
currentDate = addToDate(currentDate, 1, 'month');
|
||||||
|
break;
|
||||||
|
case ViewMode.Week:
|
||||||
|
currentDate = addToDate(currentDate, 7, 'day');
|
||||||
|
break;
|
||||||
|
case ViewMode.Day:
|
||||||
|
currentDate = addToDate(currentDate, 1, 'day');
|
||||||
|
break;
|
||||||
|
case ViewMode.HalfDay:
|
||||||
|
currentDate = addToDate(currentDate, 12, 'hour');
|
||||||
|
break;
|
||||||
|
case ViewMode.QuarterDay:
|
||||||
|
currentDate = addToDate(currentDate, 6, 'hour');
|
||||||
|
break;
|
||||||
|
case ViewMode.Hour:
|
||||||
|
currentDate = addToDate(currentDate, 1, 'hour');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
dates.push(currentDate);
|
||||||
|
}
|
||||||
|
return dates;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLocaleMonth = (date: Date, locale: string) => {
|
||||||
|
let bottomValue = getCachedDateTimeFormat(locale, {
|
||||||
|
month: 'long',
|
||||||
|
//@ts-ignore
|
||||||
|
}).format(date);
|
||||||
|
bottomValue = bottomValue.replace(bottomValue[0], bottomValue[0].toLocaleUpperCase());
|
||||||
|
return bottomValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLocalDayOfWeek = (date: Date, locale: string, format?: 'long' | 'short' | 'narrow' | undefined) => {
|
||||||
|
let bottomValue = getCachedDateTimeFormat(locale, {
|
||||||
|
weekday: format,
|
||||||
|
//@ts-ignore
|
||||||
|
}).format(date);
|
||||||
|
bottomValue = bottomValue.replace(bottomValue[0], bottomValue[0].toLocaleUpperCase());
|
||||||
|
return bottomValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns monday of current week
|
||||||
|
* @param date date for modify
|
||||||
|
*/
|
||||||
|
const getMonday = (date: Date) => {
|
||||||
|
const day = date.getDay();
|
||||||
|
const diff = date.getDate() - day + (day === 0 ? -6 : 1); // adjust when day is sunday
|
||||||
|
return new Date(date.setDate(diff));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getWeekNumberISO8601 = (date: Date) => {
|
||||||
|
const tmpDate = new Date(date.valueOf());
|
||||||
|
const dayNumber = (tmpDate.getDay() + 6) % 7;
|
||||||
|
tmpDate.setDate(tmpDate.getDate() - dayNumber + 3);
|
||||||
|
const firstThursday = tmpDate.valueOf();
|
||||||
|
tmpDate.setMonth(0, 1);
|
||||||
|
if (tmpDate.getDay() !== 4) {
|
||||||
|
tmpDate.setMonth(0, 1 + ((4 - tmpDate.getDay() + 7) % 7));
|
||||||
|
}
|
||||||
|
const weekNumber = (1 + Math.ceil((firstThursday - tmpDate.valueOf()) / 604800000)).toString();
|
||||||
|
|
||||||
|
if (weekNumber.length === 1) {
|
||||||
|
return `0${weekNumber}`;
|
||||||
|
} else {
|
||||||
|
return weekNumber;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDaysInMonth = (month: number, year: number) => {
|
||||||
|
return new Date(year, month + 1, 0).getDate();
|
||||||
|
};
|
@ -0,0 +1,61 @@
|
|||||||
|
import { BarTask } from '../types/bar-task';
|
||||||
|
import { Task } from '../types/public-types';
|
||||||
|
|
||||||
|
export function isKeyboardEvent(
|
||||||
|
event: React.MouseEvent | React.KeyboardEvent | React.FocusEvent,
|
||||||
|
): event is React.KeyboardEvent {
|
||||||
|
return (event as React.KeyboardEvent).key !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMouseEvent(
|
||||||
|
event: React.MouseEvent | React.KeyboardEvent | React.FocusEvent,
|
||||||
|
): event is React.MouseEvent {
|
||||||
|
return (event as React.MouseEvent).clientX !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isBarTask(task: Task | BarTask): task is BarTask {
|
||||||
|
return (task as BarTask).x1 !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeHiddenTasks(tasks: Task[]) {
|
||||||
|
const groupedTasks = tasks.filter((t) => t.hideChildren && t.type === 'project');
|
||||||
|
if (groupedTasks.length > 0) {
|
||||||
|
for (let i = 0; groupedTasks.length > i; i++) {
|
||||||
|
const groupedTask = groupedTasks[i];
|
||||||
|
const children = getChildren(tasks, groupedTask);
|
||||||
|
tasks = tasks.filter((t) => children.indexOf(t) === -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChildren(taskList: Task[], task: Task) {
|
||||||
|
let tasks: Task[] = [];
|
||||||
|
if (task.type !== 'project') {
|
||||||
|
tasks = taskList.filter((t) => t.dependencies && t.dependencies.indexOf(task.id) !== -1);
|
||||||
|
} else {
|
||||||
|
tasks = taskList.filter((t) => t.project && t.project === task.id);
|
||||||
|
}
|
||||||
|
var taskChildren: Task[] = [];
|
||||||
|
tasks.forEach((t) => {
|
||||||
|
taskChildren.push(...getChildren(taskList, t));
|
||||||
|
});
|
||||||
|
tasks = tasks.concat(tasks, taskChildren);
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sortTasks = (taskA: Task, taskB: Task) => {
|
||||||
|
const orderA = taskA.displayOrder || Number.MAX_VALUE;
|
||||||
|
const orderB = taskB.displayOrder || Number.MAX_VALUE;
|
||||||
|
if (orderA > orderB) {
|
||||||
|
return 1;
|
||||||
|
} else if (orderA < orderB) {
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getYmd = (date: Date) => {
|
||||||
|
return date.getFullYear() + '/' + `${date.getMonth() + 1}` + '/' + date.getDate();
|
||||||
|
};
|
@ -0,0 +1,15 @@
|
|||||||
|
import { ActionBar } from '../action';
|
||||||
|
import { Gantt } from './components/gantt/gantt';
|
||||||
|
import { GanttDesigner } from './Gantt.Designer';
|
||||||
|
import { ViewMode } from './types/public-types';
|
||||||
|
import {Event} from './components/gantt/Event';
|
||||||
|
|
||||||
|
Gantt.ActionBar = ActionBar;
|
||||||
|
|
||||||
|
Gantt.ViewMode = ViewMode;
|
||||||
|
Gantt.Designer = GanttDesigner;
|
||||||
|
Gantt.Event = Event;
|
||||||
|
|
||||||
|
// const GanttV2 = Gantt;
|
||||||
|
|
||||||
|
export { Gantt };
|
@ -0,0 +1,73 @@
|
|||||||
|
import {
|
||||||
|
seedDates,
|
||||||
|
addToDate,
|
||||||
|
getWeekNumberISO8601,
|
||||||
|
} from "../helpers/date-helper";
|
||||||
|
import { ViewMode } from "../types/public-types";
|
||||||
|
|
||||||
|
describe("seed date", () => {
|
||||||
|
test("daily", () => {
|
||||||
|
expect(
|
||||||
|
seedDates(new Date(2020, 5, 28), new Date(2020, 6, 2), ViewMode.Day)
|
||||||
|
).toEqual([
|
||||||
|
new Date(2020, 5, 28),
|
||||||
|
new Date(2020, 5, 29),
|
||||||
|
new Date(2020, 5, 30),
|
||||||
|
new Date(2020, 6, 1),
|
||||||
|
new Date(2020, 6, 2),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("weekly", () => {
|
||||||
|
expect(
|
||||||
|
seedDates(new Date(2020, 5, 28), new Date(2020, 6, 19), ViewMode.Week)
|
||||||
|
).toEqual([
|
||||||
|
new Date(2020, 5, 28),
|
||||||
|
new Date(2020, 6, 5),
|
||||||
|
new Date(2020, 6, 12),
|
||||||
|
new Date(2020, 6, 19),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("monthly", () => {
|
||||||
|
expect(
|
||||||
|
seedDates(new Date(2020, 5, 28), new Date(2020, 6, 19), ViewMode.Month)
|
||||||
|
).toEqual([new Date(2020, 5, 28), new Date(2020, 6, 28)]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("quarterly", () => {
|
||||||
|
expect(
|
||||||
|
seedDates(
|
||||||
|
new Date(2020, 5, 28),
|
||||||
|
new Date(2020, 5, 29),
|
||||||
|
ViewMode.QuarterDay
|
||||||
|
)
|
||||||
|
).toEqual([
|
||||||
|
new Date(2020, 5, 28, 0, 0),
|
||||||
|
new Date(2020, 5, 28, 6, 0),
|
||||||
|
new Date(2020, 5, 28, 12, 0),
|
||||||
|
new Date(2020, 5, 28, 18, 0),
|
||||||
|
new Date(2020, 5, 29, 0, 0),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("add to date", () => {
|
||||||
|
test("add month", () => {
|
||||||
|
expect(addToDate(new Date(2020, 0, 1), 40, "month")).toEqual(
|
||||||
|
new Date(2023, 4, 1)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("add day", () => {
|
||||||
|
expect(addToDate(new Date(2020, 0, 1), 40, "day")).toEqual(
|
||||||
|
new Date(2020, 1, 10)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("get week number", () => {
|
||||||
|
expect(getWeekNumberISO8601(new Date(2019, 11, 31))).toEqual("01");
|
||||||
|
expect(getWeekNumberISO8601(new Date(2021, 0, 1))).toEqual("53");
|
||||||
|
expect(getWeekNumberISO8601(new Date(2020, 6, 20))).toEqual("30");
|
||||||
|
});
|
@ -0,0 +1,24 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import { Gantt } from "../index";
|
||||||
|
|
||||||
|
describe("gantt", () => {
|
||||||
|
it("renders without crashing", () => {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
const root = createRoot(div);
|
||||||
|
root.render(
|
||||||
|
<Gantt
|
||||||
|
tasks={[
|
||||||
|
{
|
||||||
|
start: new Date(2020, 0, 1),
|
||||||
|
end: new Date(2020, 2, 2),
|
||||||
|
name: "Redesign website",
|
||||||
|
id: "Task 0",
|
||||||
|
progress: 45,
|
||||||
|
type: "task",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,24 @@
|
|||||||
|
import { Task, TaskType } from "./public-types";
|
||||||
|
|
||||||
|
export interface BarTask extends Task {
|
||||||
|
index: number;
|
||||||
|
typeInternal: TaskTypeInternal;
|
||||||
|
x1: number;
|
||||||
|
x2: number;
|
||||||
|
y: number;
|
||||||
|
height: number;
|
||||||
|
progressX: number;
|
||||||
|
progressWidth: number;
|
||||||
|
barCornerRadius: number;
|
||||||
|
handleWidth: number;
|
||||||
|
barChildren: BarTask[];
|
||||||
|
color?: string;
|
||||||
|
styles: {
|
||||||
|
backgroundColor: string;
|
||||||
|
backgroundSelectedColor: string;
|
||||||
|
progressColor: string;
|
||||||
|
progressSelectedColor: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TaskTypeInternal = TaskType | "smalltask";
|
@ -0,0 +1,6 @@
|
|||||||
|
import { ViewMode } from "./public-types";
|
||||||
|
|
||||||
|
export interface DateSetup {
|
||||||
|
dates: Date[];
|
||||||
|
viewMode: ViewMode;
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
import { BarTask } from "./bar-task";
|
||||||
|
|
||||||
|
export type BarMoveAction = "progress" | "end" | "start" | "move";
|
||||||
|
export type GanttContentMoveAction =
|
||||||
|
| "mouseenter"
|
||||||
|
| "mouseleave"
|
||||||
|
| "delete"
|
||||||
|
| "dblclick"
|
||||||
|
| "click"
|
||||||
|
| "select"
|
||||||
|
| ""
|
||||||
|
| BarMoveAction;
|
||||||
|
|
||||||
|
export type GanttEvent = {
|
||||||
|
changedTask?: BarTask;
|
||||||
|
originalSelectedTask?: BarTask;
|
||||||
|
action: GanttContentMoveAction;
|
||||||
|
};
|
@ -0,0 +1,145 @@
|
|||||||
|
export enum ViewMode {
|
||||||
|
Hour = "hour",
|
||||||
|
QuarterDay = "quarterDay",
|
||||||
|
HalfDay = "halfDay",
|
||||||
|
Day = "day",
|
||||||
|
/** ISO-8601 week */
|
||||||
|
Week = "week",
|
||||||
|
Month = "month",
|
||||||
|
QuarterYear = "quarterYear",
|
||||||
|
Year = "year",
|
||||||
|
}
|
||||||
|
export type TaskType = "task" | "milestone" | "project";
|
||||||
|
export interface Task {
|
||||||
|
id: string;
|
||||||
|
type: TaskType;
|
||||||
|
name: string;
|
||||||
|
start: Date;
|
||||||
|
end: Date;
|
||||||
|
/**
|
||||||
|
* From 0 to 100
|
||||||
|
*/
|
||||||
|
progress: number;
|
||||||
|
styles?: {
|
||||||
|
backgroundColor?: string;
|
||||||
|
backgroundSelectedColor?: string;
|
||||||
|
progressColor?: string;
|
||||||
|
progressSelectedColor?: string;
|
||||||
|
};
|
||||||
|
isDisabled?: boolean;
|
||||||
|
project?: string;
|
||||||
|
dependencies?: string[];
|
||||||
|
hideChildren?: boolean;
|
||||||
|
displayOrder?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EventOption {
|
||||||
|
/**
|
||||||
|
* Time step value for date changes.
|
||||||
|
*/
|
||||||
|
timeStep?: number;
|
||||||
|
/**
|
||||||
|
* Invokes on bar select on unselect.
|
||||||
|
*/
|
||||||
|
onSelect?: (task: Task, isSelected: boolean) => void;
|
||||||
|
/**
|
||||||
|
* Invokes on bar double click.
|
||||||
|
*/
|
||||||
|
onDoubleClick?: (task: Task) => void;
|
||||||
|
/**
|
||||||
|
* Invokes on bar click.
|
||||||
|
*/
|
||||||
|
onClick?: (task: Task) => void;
|
||||||
|
/**
|
||||||
|
* Invokes on end and start time change. Chart undoes operation if method return false or error.
|
||||||
|
*/
|
||||||
|
onDateChange?: (
|
||||||
|
task: Task,
|
||||||
|
children: Task[]
|
||||||
|
) => void | boolean | Promise<void> | Promise<boolean>;
|
||||||
|
/**
|
||||||
|
* Invokes on progress change. Chart undoes operation if method return false or error.
|
||||||
|
*/
|
||||||
|
onProgressChange?: (
|
||||||
|
task: Task,
|
||||||
|
children: Task[]
|
||||||
|
) => void | boolean | Promise<void> | Promise<boolean>;
|
||||||
|
/**
|
||||||
|
* Invokes on delete selected task. Chart undoes operation if method return false or error.
|
||||||
|
*/
|
||||||
|
onDelete?: (task: Task) => void | boolean | Promise<void> | Promise<boolean>;
|
||||||
|
/**
|
||||||
|
* Invokes on expander on task list
|
||||||
|
*/
|
||||||
|
onExpanderClick?: (task: Task) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DisplayOption {
|
||||||
|
viewMode?: ViewMode;
|
||||||
|
viewDate?: Date;
|
||||||
|
preStepsCount?: number;
|
||||||
|
/**
|
||||||
|
* Specifies the month name language. Able formats: ISO 639-2, Java Locale
|
||||||
|
*/
|
||||||
|
locale?: string;
|
||||||
|
rtl?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StylingOption {
|
||||||
|
headerHeight?: number;
|
||||||
|
columnWidth?: number;
|
||||||
|
listCellWidth?: string;
|
||||||
|
rowHeight?: number;
|
||||||
|
ganttHeight?: number;
|
||||||
|
barCornerRadius?: number;
|
||||||
|
handleWidth?: number;
|
||||||
|
fontFamily?: string;
|
||||||
|
fontSize?: string;
|
||||||
|
/**
|
||||||
|
* How many of row width can be taken by task.
|
||||||
|
* From 0 to 100
|
||||||
|
*/
|
||||||
|
barFill?: number;
|
||||||
|
barProgressColor?: string;
|
||||||
|
barProgressSelectedColor?: string;
|
||||||
|
barBackgroundColor?: string;
|
||||||
|
barBackgroundSelectedColor?: string;
|
||||||
|
projectProgressColor?: string;
|
||||||
|
projectProgressSelectedColor?: string;
|
||||||
|
projectBackgroundColor?: string;
|
||||||
|
projectBackgroundSelectedColor?: string;
|
||||||
|
milestoneBackgroundColor?: string;
|
||||||
|
milestoneBackgroundSelectedColor?: string;
|
||||||
|
arrowColor?: string;
|
||||||
|
arrowIndent?: number;
|
||||||
|
todayColor?: string;
|
||||||
|
TooltipContent?: React.FC<{
|
||||||
|
task: Task;
|
||||||
|
fontSize: string;
|
||||||
|
fontFamily: string;
|
||||||
|
}>;
|
||||||
|
TaskListHeader?: React.FC<{
|
||||||
|
headerHeight: number;
|
||||||
|
rowWidth: string;
|
||||||
|
fontFamily: string;
|
||||||
|
fontSize: string;
|
||||||
|
}>;
|
||||||
|
TaskListTable?: React.FC<{
|
||||||
|
rowHeight: number;
|
||||||
|
rowWidth: string;
|
||||||
|
fontFamily: string;
|
||||||
|
fontSize: string;
|
||||||
|
locale: string;
|
||||||
|
tasks: Task[];
|
||||||
|
selectedTaskId: string;
|
||||||
|
/**
|
||||||
|
* Sets selected task by id
|
||||||
|
*/
|
||||||
|
setSelectedTask: (taskId: string) => void;
|
||||||
|
onExpanderClick: (task: Task) => void;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GanttProps extends EventOption, DisplayOption, StylingOption {
|
||||||
|
tasks: Task[];
|
||||||
|
}
|
@ -40,4 +40,5 @@ export * from './time-picker';
|
|||||||
export * from './tree-select';
|
export * from './tree-select';
|
||||||
export * from './upload';
|
export * from './upload';
|
||||||
export * from './variable';
|
export * from './variable';
|
||||||
|
export * from './gantt'
|
||||||
import './index.less';
|
import './index.less';
|
||||||
|
@ -167,6 +167,7 @@ export const Table: any = observer((props: any) => {
|
|||||||
rowSelection,
|
rowSelection,
|
||||||
rowKey,
|
rowKey,
|
||||||
required,
|
required,
|
||||||
|
onExpand,
|
||||||
...others
|
...others
|
||||||
} = { ...others1, ...others2 } as any;
|
} = { ...others1, ...others2 } as any;
|
||||||
const schema = useFieldSchema();
|
const schema = useFieldSchema();
|
||||||
@ -209,11 +210,6 @@ export const Table: any = observer((props: any) => {
|
|||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
}, [requiredValidator]);
|
}, [requiredValidator]);
|
||||||
// useEffect(() => {
|
|
||||||
// const data = field.value;
|
|
||||||
// field.value = null;
|
|
||||||
// field.value = data;
|
|
||||||
// }, [treeTable]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (treeTable !== false) {
|
if (treeTable !== false) {
|
||||||
@ -227,7 +223,7 @@ export const Table: any = observer((props: any) => {
|
|||||||
} else {
|
} else {
|
||||||
setExpandesKeys([]);
|
setExpandesKeys([]);
|
||||||
}
|
}
|
||||||
}, [expandFlag]);
|
}, [expandFlag, allIncludesChildren]);
|
||||||
|
|
||||||
const components = useMemo(() => {
|
const components = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
@ -332,6 +328,8 @@ export const Table: any = observer((props: any) => {
|
|||||||
const pageSize = props?.pagination?.pageSize || 20;
|
const pageSize = props?.pagination?.pageSize || 20;
|
||||||
if (current) {
|
if (current) {
|
||||||
index = index + (current - 1) * pageSize + 1;
|
index = index + (current - 1) * pageSize + 1;
|
||||||
|
} else {
|
||||||
|
index = index + 1;
|
||||||
}
|
}
|
||||||
if (record.__index) {
|
if (record.__index) {
|
||||||
index = extractIndex(record.__index);
|
index = extractIndex(record.__index);
|
||||||
@ -490,6 +488,7 @@ export const Table: any = observer((props: any) => {
|
|||||||
onExpand: (flag, record) => {
|
onExpand: (flag, record) => {
|
||||||
const newKeys = flag ? [...expandedKeys, record.id] : expandedKeys.filter((i) => record.id !== i);
|
const newKeys = flag ? [...expandedKeys, record.id] : expandedKeys.filter((i) => record.id !== i);
|
||||||
setExpandesKeys(newKeys);
|
setExpandesKeys(newKeys);
|
||||||
|
onExpand?.(flag, record);
|
||||||
},
|
},
|
||||||
expandedRowKeys: expandedKeys,
|
expandedRowKeys: expandedKeys,
|
||||||
}}
|
}}
|
||||||
|
@ -42,6 +42,12 @@ export const BlockInitializers = {
|
|||||||
title: '{{t("Kanban")}}',
|
title: '{{t("Kanban")}}',
|
||||||
component: 'KanbanBlockInitializer',
|
component: 'KanbanBlockInitializer',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'Gantt',
|
||||||
|
type: 'item',
|
||||||
|
title: '{{t("Gantt")}}',
|
||||||
|
component: 'GanttBlockInitializer',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
import { TableActionInitializers } from './TableActionInitializers';
|
||||||
|
// 甘特图区块action配置
|
||||||
|
export const GanttActionInitializers = {
|
||||||
|
...TableActionInitializers,
|
||||||
|
// items: TableActionInitializers.items.filter((v) => {
|
||||||
|
// return v.component !== 'ActionBarAssociationFilterAction';
|
||||||
|
// }),
|
||||||
|
};
|
@ -96,14 +96,16 @@ export const TableActionInitializers = {
|
|||||||
},
|
},
|
||||||
visible: () => {
|
visible: () => {
|
||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
return (collection as any).template !== 'view';
|
const schema = useFieldSchema();
|
||||||
|
return (collection as any).template !== 'view' && schema['x-initializer'] !== 'GanttActionInitializers';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'divider',
|
type: 'divider',
|
||||||
visible: () => {
|
visible: () => {
|
||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
return (collection as any).template !== 'view';
|
const schema = useFieldSchema();
|
||||||
|
return (collection as any).template !== 'view' && schema['x-initializer'] !== 'GanttActionInitializers';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -20,5 +20,6 @@ export * from './TableActionInitializers';
|
|||||||
export * from './TableColumnInitializers';
|
export * from './TableColumnInitializers';
|
||||||
export * from './TableSelectorInitializers';
|
export * from './TableSelectorInitializers';
|
||||||
export * from './TabPaneInitializers';
|
export * from './TabPaneInitializers';
|
||||||
|
export * from './GanttActionInitializers';
|
||||||
// association filter
|
// association filter
|
||||||
export * from '../../schema-component/antd/association-filter/AssociationFilter';
|
export * from '../../schema-component/antd/association-filter/AssociationFilter';
|
||||||
|
@ -0,0 +1,119 @@
|
|||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { FormDialog, FormLayout } from '@formily/antd';
|
||||||
|
import { FormOutlined } from '@ant-design/icons';
|
||||||
|
import { SchemaOptionsContext } from '@formily/react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { useCollectionManager } from '../../collection-manager';
|
||||||
|
import { SchemaComponent, SchemaComponentOptions } from '../../schema-component';
|
||||||
|
import { createGanttBlockSchema } from '../utils';
|
||||||
|
import { DataBlockInitializer } from './DataBlockInitializer';
|
||||||
|
|
||||||
|
export const GanttBlockInitializer = (props) => {
|
||||||
|
const { insert } = props;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { getCollectionFields } = useCollectionManager();
|
||||||
|
const options = useContext(SchemaOptionsContext);
|
||||||
|
return (
|
||||||
|
<DataBlockInitializer
|
||||||
|
{...props}
|
||||||
|
componentType={'Gantt'}
|
||||||
|
icon={<FormOutlined />}
|
||||||
|
onCreateBlockSchema={async ({ item }) => {
|
||||||
|
const collectionFields = getCollectionFields(item.name);
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}).open({
|
||||||
|
initialValues: {},
|
||||||
|
});
|
||||||
|
insert(
|
||||||
|
createGanttBlockSchema({
|
||||||
|
collection: item.name,
|
||||||
|
fieldNames: {
|
||||||
|
...values,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -28,6 +28,7 @@ export * from './FilterCollapseBlockInitializer';
|
|||||||
export * from './FilterFormBlockInitializer';
|
export * from './FilterFormBlockInitializer';
|
||||||
export * from './FormBlockInitializer';
|
export * from './FormBlockInitializer';
|
||||||
export * from './G2PlotInitializer';
|
export * from './G2PlotInitializer';
|
||||||
|
export * from './GanttBlockInitializer';
|
||||||
export * from './InitializerWithSwitch';
|
export * from './InitializerWithSwitch';
|
||||||
export * from './KanbanBlockInitializer';
|
export * from './KanbanBlockInitializer';
|
||||||
export * from './MarkdownBlockInitializer';
|
export * from './MarkdownBlockInitializer';
|
||||||
|
@ -1372,6 +1372,135 @@ export const createCalendarBlockSchema = (options) => {
|
|||||||
return schema;
|
return schema;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createGanttBlockSchema = (options) => {
|
||||||
|
const { collection, resource, fieldNames, ...others } = options;
|
||||||
|
const schema: ISchema = {
|
||||||
|
type: 'void',
|
||||||
|
'x-acl-action': `${resource || collection}:list`,
|
||||||
|
'x-decorator': 'GanttBlockProvider',
|
||||||
|
'x-decorator-props': {
|
||||||
|
collection: collection,
|
||||||
|
resource: resource || collection,
|
||||||
|
action: 'list',
|
||||||
|
fieldNames: {
|
||||||
|
id: 'id',
|
||||||
|
...fieldNames,
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
paginate: false,
|
||||||
|
},
|
||||||
|
...others,
|
||||||
|
},
|
||||||
|
'x-designer': 'Gantt.Designer',
|
||||||
|
'x-component': 'CardItem',
|
||||||
|
properties: {
|
||||||
|
[uid()]: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Gantt',
|
||||||
|
'x-component-props': {
|
||||||
|
useProps: '{{ useGanttBlockProps }}',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
toolBar: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'ActionBar',
|
||||||
|
'x-component-props': {
|
||||||
|
style: {
|
||||||
|
marginBottom: 24,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-initializer': 'GanttActionInitializers',
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
type: 'array',
|
||||||
|
'x-decorator': 'div',
|
||||||
|
'x-decorator-props': {
|
||||||
|
style: {
|
||||||
|
float: 'left',
|
||||||
|
maxWidth: '35%',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'x-initializer': 'TableColumnInitializers',
|
||||||
|
'x-component': 'TableV2',
|
||||||
|
'x-component-props': {
|
||||||
|
rowKey: 'id',
|
||||||
|
rowSelection: {
|
||||||
|
type: 'checkbox',
|
||||||
|
},
|
||||||
|
useProps: '{{ useTableBlockProps }}',
|
||||||
|
pagination: false,
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
actions: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("Actions") }}',
|
||||||
|
'x-action-column': 'actions',
|
||||||
|
'x-decorator': 'TableV2.Column.ActionBar',
|
||||||
|
'x-component': 'TableV2.Column',
|
||||||
|
'x-designer': 'TableV2.ActionColumnDesigner',
|
||||||
|
'x-initializer': 'TableActionColumnInitializers',
|
||||||
|
properties: {
|
||||||
|
actions: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'DndContext',
|
||||||
|
'x-component': 'Space',
|
||||||
|
'x-component-props': {
|
||||||
|
split: '|',
|
||||||
|
},
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
detail: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Gantt.Event',
|
||||||
|
properties: {
|
||||||
|
drawer: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action.Drawer',
|
||||||
|
'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: '{{t("Details")}}',
|
||||||
|
'x-component': 'Tabs.TabPane',
|
||||||
|
'x-designer': 'Tabs.Designer',
|
||||||
|
'x-component-props': {},
|
||||||
|
properties: {
|
||||||
|
grid: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid',
|
||||||
|
'x-initializer': 'RecordBlockInitializers',
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
console.log(JSON.stringify(schema, null, 2));
|
||||||
|
return schema;
|
||||||
|
};
|
||||||
export const createKanbanBlockSchema = (options) => {
|
export const createKanbanBlockSchema = (options) => {
|
||||||
const { collection, resource, groupField, ...others } = options;
|
const { collection, resource, groupField, ...others } = options;
|
||||||
const schema: ISchema = {
|
const schema: ISchema = {
|
||||||
|
Loading…
Reference in New Issue
Block a user