mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-08 23:19:26 +08:00
feat: add Gantt block
This commit is contained in:
parent
61621a9759
commit
e70e124a47
@ -0,0 +1,54 @@
|
||||
import { ArrayField } from '@formily/core';
|
||||
import { useField } from '@formily/react';
|
||||
import React, { createContext, useContext, useEffect } from 'react';
|
||||
import { BlockProvider, useBlockRequestContext } from './BlockProvider';
|
||||
|
||||
export const GanttBlockContext = createContext<any>({});
|
||||
|
||||
const InternalGanttBlockProvider = (props) => {
|
||||
const { fieldNames, timeRange } = props;
|
||||
const field = useField();
|
||||
const { resource, 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={{ ...props.params, paginate: false }}>
|
||||
<InternalGanttBlockProvider {...props} />
|
||||
</BlockProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useGanttBlockContext = () => {
|
||||
return useContext(GanttBlockContext);
|
||||
};
|
||||
|
||||
export const useGanttBlockProps = () => {
|
||||
const ctx = useGanttBlockContext();
|
||||
const field = useField<ArrayField>();
|
||||
useEffect(() => {
|
||||
if (!ctx?.service?.loading) {
|
||||
field.componentProps.dataSource = ctx?.service?.data?.data;
|
||||
}
|
||||
}, [ctx?.service?.loading]);
|
||||
return {
|
||||
fieldNames: ctx.fieldNames,
|
||||
timeRange: ctx.timeRange,
|
||||
};
|
||||
};
|
@ -8,4 +8,5 @@ export * from './TableBlockProvider';
|
||||
export * from './TableFieldProvider';
|
||||
export * from './TableSelectorProvider';
|
||||
export * from './FormFieldProvider';
|
||||
export * from './GanttBlockProvider'
|
||||
|
||||
|
@ -0,0 +1,162 @@
|
||||
import { ISchema, useField, useFieldSchema } from '@formily/react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCompile, useDesignable } from '../..';
|
||||
import { useCalendarBlockContext } 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 } = useCalendarBlockContext();
|
||||
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.SwitchItem
|
||||
title={t('Show lunar')}
|
||||
checked={field.decoratorProps.showLunar}
|
||||
onChange={(v) => {
|
||||
field.decoratorProps.showLunar = v;
|
||||
fieldSchema['x-decorator-props']['showLunar'] = v;
|
||||
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.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={'Calendar'} collectionName={name} resourceName={defaultResource} />
|
||||
<SchemaSettings.Divider />
|
||||
<SchemaSettings.Remove
|
||||
removeParentsIfNoChildren
|
||||
breakRemoveOn={{
|
||||
'x-component': 'Grid',
|
||||
}}
|
||||
/>
|
||||
</GeneralSchemaDesigner>
|
||||
);
|
||||
};
|
@ -0,0 +1,31 @@
|
||||
.calendarBottomText {
|
||||
text-anchor: middle;
|
||||
fill: #333;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.calendarTopTick {
|
||||
stroke: #e6e4e4;
|
||||
}
|
||||
|
||||
.calendarTopText {
|
||||
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;
|
||||
}
|
||||
|
||||
.calendarHeader {
|
||||
fill: #ffffff;
|
||||
stroke: #e0e0e0;
|
||||
stroke-width: 1.4;
|
||||
}
|
@ -0,0 +1,395 @@
|
||||
import React, { ReactChild } from "react";
|
||||
import { ViewMode } from "../../types/public-types";
|
||||
import { TopPartOfCalendar } from "./top-part-of-calendar";
|
||||
import {
|
||||
getCachedDateTimeFormat,
|
||||
getDaysInMonth,
|
||||
getLocalDayOfWeek,
|
||||
getLocaleMonth,
|
||||
getWeekNumberISO8601,
|
||||
} from "../../helpers/date-helper";
|
||||
import { DateSetup } from "../../types/date-setup";
|
||||
import styles from "./calendar.module.css";
|
||||
|
||||
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={styles.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={styles.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={styles.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={styles.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()}`;
|
||||
|
||||
bottomValues.push(
|
||||
<text
|
||||
key={date.getTime()}
|
||||
y={headerHeight * 0.8}
|
||||
x={columnWidth * i + columnWidth * 0.5}
|
||||
className={styles.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",
|
||||
}).format(date);
|
||||
|
||||
bottomValues.push(
|
||||
<text
|
||||
key={date.getTime()}
|
||||
y={headerHeight * 0.8}
|
||||
x={columnWidth * (i + +rtl)}
|
||||
className={styles.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",
|
||||
}).format(date);
|
||||
|
||||
bottomValues.push(
|
||||
<text
|
||||
key={date.getTime()}
|
||||
y={headerHeight * 0.8}
|
||||
x={columnWidth * (i + +rtl)}
|
||||
className={styles.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={styles.calendarHeader}
|
||||
/>
|
||||
{bottomValues} {topValues}
|
||||
</g>
|
||||
);
|
||||
};
|
@ -0,0 +1,41 @@
|
||||
import React from "react";
|
||||
import styles from "./calendar.module.css";
|
||||
|
||||
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={styles.calendarTopTick}
|
||||
key={value + "line"}
|
||||
/>
|
||||
<text
|
||||
key={value + "text"}
|
||||
y={yText}
|
||||
x={xText}
|
||||
className={styles.calendarTopText}
|
||||
>
|
||||
{value}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
};
|
@ -0,0 +1,21 @@
|
||||
.ganttVerticalContainer {
|
||||
overflow: hidden;
|
||||
font-size: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.horizontalContainer {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
outline: none;
|
||||
position: relative;
|
||||
}
|
@ -0,0 +1,603 @@
|
||||
import React, {
|
||||
useState,
|
||||
SyntheticEvent,
|
||||
useRef,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import { ViewMode, GanttProps, Task } from "../../types/public-types";
|
||||
import { GridProps } from "../grid/grid";
|
||||
import { ganttDateRange, seedDates } from "../../helpers/date-helper";
|
||||
import { CalendarProps } from "../calendar/calendar";
|
||||
import { TaskGanttContentProps } from "./task-gantt-content";
|
||||
import { TaskListHeaderDefault } from "../task-list/task-list-header";
|
||||
import { TaskListTableDefault } from "../task-list/task-list-table";
|
||||
import { StandardTooltipContent, Tooltip } from "../other/tooltip";
|
||||
import { VerticalScroll } from "../other/vertical-scroll";
|
||||
import { TaskListProps, TaskList } from "../task-list/task-list";
|
||||
import { TaskGantt } from "./task-gantt";
|
||||
import { BarTask } from "../../types/bar-task";
|
||||
import { convertToBarTasks } from "../../helpers/bar-helper";
|
||||
import { GanttEvent } from "../../types/gantt-task-actions";
|
||||
import { DateSetup } from "../../types/date-setup";
|
||||
import { HorizontalScroll } from "../other/horizontal-scroll";
|
||||
import { removeHiddenTasks, sortTasks } from "../../helpers/other-helper";
|
||||
import styles from "./gantt.module.css";
|
||||
|
||||
|
||||
export function initTasks() {
|
||||
const currentDate = new Date();
|
||||
const tasks: Task[] = [
|
||||
{
|
||||
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 1),
|
||||
end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 15),
|
||||
name: "Some Project",
|
||||
id: "ProjectSample",
|
||||
progress: 25,
|
||||
type: "project",
|
||||
hideChildren: false,
|
||||
displayOrder: 1,
|
||||
},
|
||||
{
|
||||
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 1),
|
||||
end: new Date(
|
||||
currentDate.getFullYear(),
|
||||
currentDate.getMonth(),
|
||||
2,
|
||||
12,
|
||||
28
|
||||
),
|
||||
name: "Idea",
|
||||
id: "Task 0",
|
||||
progress: 45,
|
||||
type: "task",
|
||||
project: "ProjectSample",
|
||||
displayOrder: 2,
|
||||
},
|
||||
{
|
||||
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 2),
|
||||
end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 4, 0, 0),
|
||||
name: "Research",
|
||||
id: "Task 1",
|
||||
progress: 25,
|
||||
dependencies: ["Task 0"],
|
||||
type: "task",
|
||||
project: "ProjectSample",
|
||||
displayOrder: 3,
|
||||
},
|
||||
{
|
||||
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 4),
|
||||
end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 8, 0, 0),
|
||||
name: "Discussion with team",
|
||||
id: "Task 2",
|
||||
progress: 10,
|
||||
dependencies: ["Task 1"],
|
||||
type: "task",
|
||||
project: "ProjectSample",
|
||||
displayOrder: 4,
|
||||
},
|
||||
{
|
||||
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 8),
|
||||
end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 9, 0, 0),
|
||||
name: "Developing",
|
||||
id: "Task 3",
|
||||
progress: 2,
|
||||
dependencies: ["Task 2"],
|
||||
type: "task",
|
||||
project: "ProjectSample",
|
||||
displayOrder: 5,
|
||||
},
|
||||
{
|
||||
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 8),
|
||||
end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 10),
|
||||
name: "Review",
|
||||
id: "Task 4",
|
||||
type: "task",
|
||||
progress: 70,
|
||||
dependencies: ["Task 2"],
|
||||
project: "ProjectSample",
|
||||
displayOrder: 6,
|
||||
},
|
||||
{
|
||||
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 15),
|
||||
end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 15),
|
||||
name: "Release",
|
||||
id: "Task 6",
|
||||
progress: currentDate.getMonth(),
|
||||
type: "milestone",
|
||||
dependencies: ["Task 4"],
|
||||
project: "ProjectSample",
|
||||
displayOrder: 7,
|
||||
},
|
||||
{
|
||||
start: new Date(currentDate.getFullYear(), currentDate.getMonth(), 18),
|
||||
end: new Date(currentDate.getFullYear(), currentDate.getMonth(), 19),
|
||||
name: "Party Time",
|
||||
id: "Task 9",
|
||||
progress: 0,
|
||||
isDisabled: true,
|
||||
type: "task",
|
||||
},
|
||||
];
|
||||
return tasks;
|
||||
}
|
||||
export const Gantt: any = ({
|
||||
tasks=initTasks(),
|
||||
headerHeight = 50,
|
||||
columnWidth = 60,
|
||||
listCellWidth = "155px",
|
||||
rowHeight = 50,
|
||||
ganttHeight = 0,
|
||||
viewMode = ViewMode.Day,
|
||||
preStepsCount = 1,
|
||||
locale = "en-GB",
|
||||
barFill = 60,
|
||||
barCornerRadius = 3,
|
||||
barProgressColor = "#a3a3ff",
|
||||
barProgressSelectedColor = "#8282f5",
|
||||
barBackgroundColor = "#b8c2cc",
|
||||
barBackgroundSelectedColor = "#aeb8c2",
|
||||
projectProgressColor = "#7db59a",
|
||||
projectProgressSelectedColor = "#59a985",
|
||||
projectBackgroundColor = "#fac465",
|
||||
projectBackgroundSelectedColor = "#f7bb53",
|
||||
milestoneBackgroundColor = "#f1c453",
|
||||
milestoneBackgroundSelectedColor = "#f29e4c",
|
||||
rtl = false,
|
||||
handleWidth = 8,
|
||||
timeStep = 300000,
|
||||
arrowColor = "grey",
|
||||
fontFamily = "Arial, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue",
|
||||
fontSize = "14px",
|
||||
arrowIndent = 20,
|
||||
todayColor = "rgba(252, 248, 227, 0.5)",
|
||||
viewDate,
|
||||
TooltipContent = StandardTooltipContent,
|
||||
TaskListHeader = TaskListHeaderDefault,
|
||||
TaskListTable = TaskListTableDefault,
|
||||
onDateChange,
|
||||
onProgressChange,
|
||||
onDoubleClick,
|
||||
onClick,
|
||||
onDelete,
|
||||
onSelect,
|
||||
onExpanderClick,
|
||||
}) => {
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const taskListRef = useRef<HTMLDivElement>(null);
|
||||
const [dateSetup, setDateSetup] = useState<DateSetup>(() => {
|
||||
const [startDate, endDate] = ganttDateRange(tasks, viewMode, preStepsCount);
|
||||
return { viewMode, dates: seedDates(startDate, endDate, viewMode) };
|
||||
});
|
||||
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 svgWidth = dateSetup.dates.length * columnWidth;
|
||||
const ganttFullHeight = barTasks.length * rowHeight;
|
||||
|
||||
const [scrollY, setScrollY] = useState(0);
|
||||
const [scrollX, setScrollX] = useState(-1);
|
||||
const [ignoreScrollEvent, setIgnoreScrollEvent] = useState(false);
|
||||
|
||||
// task change events
|
||||
useEffect(() => {
|
||||
let filteredTasks: Task[];
|
||||
if (onExpanderClick) {
|
||||
filteredTasks = removeHiddenTasks(tasks);
|
||||
} else {
|
||||
filteredTasks = tasks;
|
||||
}
|
||||
filteredTasks = filteredTasks.sort(sortTasks);
|
||||
console.log(4)
|
||||
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,
|
||||
onExpanderClick,
|
||||
]);
|
||||
|
||||
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 handleExpanderClick = (task: Task) => {
|
||||
if (onExpanderClick && task.hideChildren !== undefined) {
|
||||
onExpanderClick({ ...task, hideChildren: !task.hideChildren });
|
||||
}
|
||||
};
|
||||
const gridProps: GridProps = {
|
||||
columnWidth,
|
||||
svgWidth,
|
||||
tasks: tasks,
|
||||
rowHeight,
|
||||
dates: dateSetup.dates,
|
||||
todayColor,
|
||||
rtl,
|
||||
};
|
||||
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,
|
||||
onProgressChange,
|
||||
onDoubleClick,
|
||||
onClick,
|
||||
onDelete,
|
||||
};
|
||||
|
||||
const tableProps: TaskListProps = {
|
||||
rowHeight,
|
||||
rowWidth: listCellWidth,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
tasks: barTasks,
|
||||
locale,
|
||||
headerHeight,
|
||||
scrollY,
|
||||
ganttHeight,
|
||||
horizontalContainerClass: styles.horizontalContainer,
|
||||
selectedTask,
|
||||
taskListRef,
|
||||
setSelectedTask: handleSelectedTask,
|
||||
onExpanderClick: handleExpanderClick,
|
||||
TaskListHeader,
|
||||
TaskListTable,
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className={styles.wrapper}
|
||||
onKeyDown={handleKeyDown}
|
||||
tabIndex={0}
|
||||
ref={wrapperRef}
|
||||
>
|
||||
{listCellWidth && <TaskList {...tableProps} />}
|
||||
<TaskGantt
|
||||
gridProps={gridProps}
|
||||
calendarProps={calendarProps}
|
||||
barProps={barProps}
|
||||
ganttHeight={ganttHeight}
|
||||
scrollY={scrollY}
|
||||
scrollX={scrollX}
|
||||
/>
|
||||
{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>
|
||||
<HorizontalScroll
|
||||
svgWidth={svgWidth}
|
||||
taskListWidth={taskListWidth}
|
||||
scroll={scrollX}
|
||||
rtl={rtl}
|
||||
onScroll={handleScrollX}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,302 @@
|
||||
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";
|
||||
|
||||
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 = 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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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={handleBarEventStart}
|
||||
key={task.id}
|
||||
isSelected={!!selectedTask && task.id === selectedTask.id}
|
||||
rtl={rtl}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
</g>
|
||||
);
|
||||
};
|
@ -0,0 +1,76 @@
|
||||
import React, { useRef, useEffect } from "react";
|
||||
import { GridProps, Grid } from "../grid/grid";
|
||||
import { CalendarProps, Calendar } from "../calendar/calendar";
|
||||
import { TaskGanttContentProps, TaskGanttContent } from "./task-gantt-content";
|
||||
import styles from "./gantt.module.css";
|
||||
|
||||
export type TaskGanttProps = {
|
||||
gridProps: GridProps;
|
||||
calendarProps: CalendarProps;
|
||||
barProps: TaskGanttContentProps;
|
||||
ganttHeight: number;
|
||||
scrollY: number;
|
||||
scrollX: number;
|
||||
};
|
||||
export const TaskGantt: React.FC<TaskGanttProps> = ({
|
||||
gridProps,
|
||||
calendarProps,
|
||||
barProps,
|
||||
ganttHeight,
|
||||
scrollY,
|
||||
scrollX,
|
||||
}) => {
|
||||
const ganttSVGRef = useRef<SVGSVGElement>(null);
|
||||
const horizontalContainerRef = useRef<HTMLDivElement>(null);
|
||||
const verticalGanttContainerRef = useRef<HTMLDivElement>(null);
|
||||
const newBarProps = { ...barProps, svg: ganttSVGRef };
|
||||
|
||||
useEffect(() => {
|
||||
if (horizontalContainerRef.current) {
|
||||
horizontalContainerRef.current.scrollTop = scrollY;
|
||||
}
|
||||
}, [scrollY]);
|
||||
|
||||
useEffect(() => {
|
||||
if (verticalGanttContainerRef.current) {
|
||||
verticalGanttContainerRef.current.scrollLeft = scrollX;
|
||||
}
|
||||
}, [scrollX]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.ganttVerticalContainer}
|
||||
ref={verticalGanttContainerRef}
|
||||
dir="ltr"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={gridProps.svgWidth}
|
||||
height={calendarProps.headerHeight}
|
||||
fontFamily={barProps.fontFamily}
|
||||
>
|
||||
<Calendar {...calendarProps} />
|
||||
</svg>
|
||||
<div
|
||||
ref={horizontalContainerRef}
|
||||
className={styles.horizontalContainer}
|
||||
style={
|
||||
ganttHeight
|
||||
? { height: ganttHeight, width: gridProps.svgWidth }
|
||||
: { width: gridProps.svgWidth }
|
||||
}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={gridProps.svgWidth}
|
||||
height={barProps.rowHeight * barProps.tasks.length}
|
||||
fontFamily={barProps.fontFamily}
|
||||
ref={ganttSVGRef}
|
||||
>
|
||||
<Grid {...gridProps} />
|
||||
<TaskGanttContent {...newBarProps} />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,127 @@
|
||||
import React, { ReactChild } from "react";
|
||||
import { Task } from "../../types/public-types";
|
||||
import { addToDate } from "../../helpers/date-helper";
|
||||
import styles from "./grid.module.css";
|
||||
|
||||
export type GridBodyProps = {
|
||||
tasks: Task[];
|
||||
dates: Date[];
|
||||
svgWidth: number;
|
||||
rowHeight: number;
|
||||
columnWidth: number;
|
||||
todayColor: string;
|
||||
rtl: boolean;
|
||||
};
|
||||
export const GridBody: React.FC<GridBodyProps> = ({
|
||||
tasks,
|
||||
dates,
|
||||
rowHeight,
|
||||
svgWidth,
|
||||
columnWidth,
|
||||
todayColor,
|
||||
rtl,
|
||||
}) => {
|
||||
let y = 0;
|
||||
const gridRows: ReactChild[] = [];
|
||||
const rowLines: ReactChild[] = [
|
||||
<line
|
||||
key="RowLineFirst"
|
||||
x="0"
|
||||
y1={0}
|
||||
x2={svgWidth}
|
||||
y2={0}
|
||||
className={styles.gridRowLine}
|
||||
/>,
|
||||
];
|
||||
for (const task of tasks) {
|
||||
gridRows.push(
|
||||
<rect
|
||||
key={"Row" + task.id}
|
||||
x="0"
|
||||
y={y}
|
||||
width={svgWidth}
|
||||
height={rowHeight}
|
||||
className={styles.gridRow}
|
||||
/>
|
||||
);
|
||||
rowLines.push(
|
||||
<line
|
||||
key={"RowLine" + task.id}
|
||||
x="0"
|
||||
y1={y + rowHeight}
|
||||
x2={svgWidth}
|
||||
y2={y + rowHeight}
|
||||
className={styles.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={styles.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,15 @@
|
||||
.gridRow {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.gridRow:nth-child(even) {
|
||||
fill: #f5f5f5;
|
||||
}
|
||||
|
||||
.gridRowLine {
|
||||
stroke: #ebeff2;
|
||||
}
|
||||
|
||||
.gridTick {
|
||||
stroke: #e6e4e4;
|
||||
}
|
@ -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,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,33 @@
|
||||
.scrollWrapper {
|
||||
overflow: auto;
|
||||
max-width: 100%;
|
||||
/*firefox*/
|
||||
scrollbar-width: thin;
|
||||
/*iPad*/
|
||||
height: 1.2rem;
|
||||
}
|
||||
.scrollWrapper::-webkit-scrollbar {
|
||||
width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
}
|
||||
.scrollWrapper::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
.scrollWrapper::-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;
|
||||
}
|
||||
.scrollWrapper::-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;
|
||||
}
|
||||
@media only screen and (max-device-width: 1024px) and (-webkit-min-device-pixel-ratio: 2) {
|
||||
}
|
||||
.scroll {
|
||||
height: 1px;
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
import React, { SyntheticEvent, useRef, useEffect } from "react";
|
||||
import styles from "./horizontal-scroll.module.css";
|
||||
|
||||
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={styles.scrollWrapper}
|
||||
onScroll={onScroll}
|
||||
ref={scrollRef}
|
||||
>
|
||||
<div style={{ width: svgWidth }} className={styles.scroll} />
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,30 @@
|
||||
.tooltipDefaultContainer {
|
||||
background: #fff;
|
||||
padding: 12px;
|
||||
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
|
||||
}
|
||||
|
||||
.tooltipDefaultContainerParagraph {
|
||||
font-size: 12px;
|
||||
margin-bottom: 6px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.tooltipDetailsContainer {
|
||||
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;
|
||||
}
|
||||
|
||||
.tooltipDetailsContainerHidden {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
pointer-events: none;
|
||||
}
|
@ -0,0 +1,145 @@
|
||||
import React, { useRef, useEffect, useState } from "react";
|
||||
import { Task } from "../../types/public-types";
|
||||
import { BarTask } from "../../types/bar-task";
|
||||
import styles from "./tooltip.module.css";
|
||||
|
||||
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
|
||||
? styles.tooltipDetailsContainer
|
||||
: styles.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={styles.tooltipDefaultContainer} style={style}>
|
||||
<b style={{ fontSize: fontSize + 6 }}>{`${
|
||||
task.name
|
||||
}: ${task.start.getDate()}-${
|
||||
task.start.getMonth() + 1
|
||||
}-${task.start.getFullYear()} - ${task.end.getDate()}-${
|
||||
task.end.getMonth() + 1
|
||||
}-${task.end.getFullYear()}`}</b>
|
||||
{task.end.getTime() - task.start.getTime() !== 0 && (
|
||||
<p className={styles.tooltipDefaultContainerParagraph}>{`Duration: ${~~(
|
||||
(task.end.getTime() - task.start.getTime()) /
|
||||
(1000 * 60 * 60 * 24)
|
||||
)} day(s)`}</p>
|
||||
)}
|
||||
|
||||
<p className={styles.tooltipDefaultContainerParagraph}>
|
||||
{!!task.progress && `Progress: ${task.progress} %`}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,27 @@
|
||||
.scroll {
|
||||
overflow: hidden auto;
|
||||
width: 1rem;
|
||||
flex-shrink: 0;
|
||||
/*firefox*/
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.scroll::-webkit-scrollbar {
|
||||
width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
}
|
||||
.scroll::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
.scroll::-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;
|
||||
}
|
||||
.scroll::-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,41 @@
|
||||
import React, { SyntheticEvent, useRef, useEffect } from "react";
|
||||
import styles from "./vertical-scroll.module.css";
|
||||
|
||||
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={{
|
||||
height: ganttHeight,
|
||||
marginTop: headerHeight,
|
||||
marginLeft: rtl ? "" : "-1rem",
|
||||
}}
|
||||
className={styles.scroll}
|
||||
onScroll={onScroll}
|
||||
ref={scrollRef}
|
||||
>
|
||||
<div style={{ height: ganttFullHeight, width: 1 }} />
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
import styles from "./bar.module.css";
|
||||
|
||||
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={styles.barHandle}
|
||||
ry={barCornerRadius}
|
||||
rx={barCornerRadius}
|
||||
onMouseDown={onMouseDown}
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,65 @@
|
||||
import React from "react";
|
||||
import style from "./bar.module.css";
|
||||
|
||||
type BarDisplayProps = {
|
||||
x: number;
|
||||
y: number;
|
||||
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,
|
||||
width,
|
||||
height,
|
||||
isSelected,
|
||||
progressX,
|
||||
progressWidth,
|
||||
barCornerRadius,
|
||||
styles,
|
||||
onMouseDown,
|
||||
}) => {
|
||||
const getProcessColor = () => {
|
||||
return isSelected ? styles.progressSelectedColor : styles.progressColor;
|
||||
};
|
||||
|
||||
const getBarColor = () => {
|
||||
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={style.barBackground}
|
||||
/>
|
||||
<rect
|
||||
x={progressX}
|
||||
width={progressWidth}
|
||||
y={y}
|
||||
height={height}
|
||||
ry={barCornerRadius}
|
||||
rx={barCornerRadius}
|
||||
fill={getProcessColor()}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
};
|
@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
import styles from "./bar.module.css";
|
||||
|
||||
type BarProgressHandleProps = {
|
||||
progressPoint: string;
|
||||
onMouseDown: (event: React.MouseEvent<SVGPolygonElement, MouseEvent>) => void;
|
||||
};
|
||||
export const BarProgressHandle: React.FC<BarProgressHandleProps> = ({
|
||||
progressPoint,
|
||||
onMouseDown,
|
||||
}) => {
|
||||
return (
|
||||
<polygon
|
||||
className={styles.barHandle}
|
||||
points={progressPoint}
|
||||
onMouseDown={onMouseDown}
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,48 @@
|
||||
import React from "react";
|
||||
import { getProgressPoint } from "../../../helpers/bar-helper";
|
||||
import { BarDisplay } from "./bar-display";
|
||||
import { BarProgressHandle } from "./bar-progress-handle";
|
||||
import { TaskItemProps } from "../task-item";
|
||||
import styles from "./bar.module.css";
|
||||
|
||||
export const BarSmall: React.FC<TaskItemProps> = ({
|
||||
task,
|
||||
isProgressChangeable,
|
||||
isDateChangeable,
|
||||
onEventStart,
|
||||
isSelected,
|
||||
}) => {
|
||||
const progressPoint = getProgressPoint(
|
||||
task.progressWidth + task.x1,
|
||||
task.y,
|
||||
task.height
|
||||
);
|
||||
return (
|
||||
<g className={styles.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,21 @@
|
||||
.barWrapper {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.barWrapper:hover .barHandle {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.barHandle {
|
||||
fill: #ddd;
|
||||
cursor: ew-resize;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.barBackground {
|
||||
user-select: none;
|
||||
stroke-width: 0;
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
import React from "react";
|
||||
import { getProgressPoint } from "../../../helpers/bar-helper";
|
||||
import { BarDisplay } from "./bar-display";
|
||||
import { BarDateHandle } from "./bar-date-handle";
|
||||
import { BarProgressHandle } from "./bar-progress-handle";
|
||||
import { TaskItemProps } from "../task-item";
|
||||
import styles from "./bar.module.css";
|
||||
|
||||
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={styles.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">
|
||||
{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,8 @@
|
||||
.milestoneWrapper {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.milestoneBackground {
|
||||
user-select: none;
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
import React from "react";
|
||||
import { TaskItemProps } from "../task-item";
|
||||
import styles from "./milestone.module.css";
|
||||
|
||||
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={styles.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={styles.milestoneBackground}
|
||||
onMouseDown={e => {
|
||||
isDateChangeable && onEventStart("move", task, e);
|
||||
}}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
};
|
@ -0,0 +1,13 @@
|
||||
.projectWrapper {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.projectBackground {
|
||||
user-select: none;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.projectTop {
|
||||
user-select: none;
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
import React from "react";
|
||||
import { TaskItemProps } from "../task-item";
|
||||
import styles from "./project.module.css";
|
||||
|
||||
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;
|
||||
|
||||
const projectLeftTriangle = [
|
||||
task.x1,
|
||||
task.y + task.height / 2 - 1,
|
||||
task.x1,
|
||||
task.y + task.height,
|
||||
task.x1 + 15,
|
||||
task.y + task.height / 2 - 1,
|
||||
].join(",");
|
||||
const projectRightTriangle = [
|
||||
task.x2,
|
||||
task.y + task.height / 2 - 1,
|
||||
task.x2,
|
||||
task.y + task.height,
|
||||
task.x2 - 15,
|
||||
task.y + task.height / 2 - 1,
|
||||
].join(",");
|
||||
|
||||
return (
|
||||
<g tabIndex={0} className={styles.projectWrapper}>
|
||||
<rect
|
||||
fill={barColor}
|
||||
x={task.x1}
|
||||
width={projectWith}
|
||||
y={task.y}
|
||||
height={task.height}
|
||||
rx={task.barCornerRadius}
|
||||
ry={task.barCornerRadius}
|
||||
className={styles.projectBackground}
|
||||
/>
|
||||
<rect
|
||||
x={task.progressX}
|
||||
width={task.progressWidth}
|
||||
y={task.y}
|
||||
height={task.height}
|
||||
ry={task.barCornerRadius}
|
||||
rx={task.barCornerRadius}
|
||||
fill={processColor}
|
||||
/>
|
||||
<rect
|
||||
fill={barColor}
|
||||
x={task.x1}
|
||||
width={projectWith}
|
||||
y={task.y}
|
||||
height={task.height / 2}
|
||||
rx={task.barCornerRadius}
|
||||
ry={task.barCornerRadius}
|
||||
className={styles.projectTop}
|
||||
/>
|
||||
<polygon
|
||||
className={styles.projectTop}
|
||||
points={projectLeftTriangle}
|
||||
fill={barColor}
|
||||
/>
|
||||
<polygon
|
||||
className={styles.projectTop}
|
||||
points={projectRightTriangle}
|
||||
fill={barColor}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
};
|
@ -0,0 +1,125 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
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 style from "./task-list.module.css";
|
||||
|
||||
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);
|
||||
|
||||
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={getX()}
|
||||
y={task.y + taskHeight * 0.5}
|
||||
className={
|
||||
isTextInside
|
||||
? style.barLabel
|
||||
: style.barLabel && style.barLabelOutside
|
||||
}
|
||||
ref={textRef}
|
||||
>
|
||||
{task.name}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
};
|
@ -0,0 +1,23 @@
|
||||
.barLabel {
|
||||
fill: #fff;
|
||||
text-anchor: middle;
|
||||
font-weight: lighter;
|
||||
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;
|
||||
}
|
||||
|
||||
.barLabelOutside {
|
||||
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,23 @@
|
||||
.ganttTable {
|
||||
display: table;
|
||||
border-bottom: #e6e4e4 1px solid;
|
||||
border-top: #e6e4e4 1px solid;
|
||||
border-left: #e6e4e4 1px solid;
|
||||
}
|
||||
|
||||
.ganttTable_Header {
|
||||
display: table-row;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.ganttTable_HeaderSeparator {
|
||||
border-right: 1px solid rgb(196, 196, 196);
|
||||
opacity: 1;
|
||||
margin-left: -2px;
|
||||
}
|
||||
|
||||
.ganttTable_HeaderItem {
|
||||
display: table-cell;
|
||||
vertical-align: -webkit-baseline-middle;
|
||||
vertical-align: middle;
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
import React from "react";
|
||||
import styles from "./task-list-header.module.css";
|
||||
|
||||
export const TaskListHeaderDefault: React.FC<{
|
||||
headerHeight: number;
|
||||
rowWidth: string;
|
||||
fontFamily: string;
|
||||
fontSize: string;
|
||||
}> = ({ headerHeight, fontFamily, fontSize, rowWidth }) => {
|
||||
return (
|
||||
<div
|
||||
className={styles.ganttTable}
|
||||
style={{
|
||||
fontFamily: fontFamily,
|
||||
fontSize: fontSize,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={styles.ganttTable_Header}
|
||||
style={{
|
||||
height: headerHeight - 2,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={styles.ganttTable_HeaderItem}
|
||||
style={{
|
||||
minWidth: rowWidth,
|
||||
}}
|
||||
>
|
||||
Name
|
||||
</div>
|
||||
<div
|
||||
className={styles.ganttTable_HeaderSeparator}
|
||||
style={{
|
||||
height: headerHeight * 0.5,
|
||||
marginTop: headerHeight * 0.2,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={styles.ganttTable_HeaderItem}
|
||||
style={{
|
||||
minWidth: rowWidth,
|
||||
}}
|
||||
>
|
||||
From
|
||||
</div>
|
||||
<div
|
||||
className={styles.ganttTable_HeaderSeparator}
|
||||
style={{
|
||||
height: headerHeight * 0.5,
|
||||
marginTop: headerHeight * 0.25,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={styles.ganttTable_HeaderItem}
|
||||
style={{
|
||||
minWidth: rowWidth,
|
||||
}}
|
||||
>
|
||||
To
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,38 @@
|
||||
.taskListWrapper {
|
||||
display: table;
|
||||
border-bottom: #e6e4e4 1px solid;
|
||||
border-left: #e6e4e4 1px solid;
|
||||
}
|
||||
|
||||
.taskListTableRow {
|
||||
display: table-row;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.taskListTableRow:nth-of-type(even) {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.taskListCell {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.taskListNameWrapper {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.taskListExpander {
|
||||
color: rgb(86 86 86);
|
||||
font-size: 0.6rem;
|
||||
padding: 0.15rem 0.2rem 0rem 0.2rem;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.taskListEmptyExpander {
|
||||
font-size: 0.6rem;
|
||||
padding-left: 1rem;
|
||||
user-select: none;
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
import React, { useMemo } from "react";
|
||||
import styles from "./task-list-table.module.css";
|
||||
import { Task } from "../../types/public-types";
|
||||
|
||||
const localeDateStringCache = {};
|
||||
const toLocaleDateStringFactory =
|
||||
(locale: string) =>
|
||||
(date: Date, dateTimeOptions: Intl.DateTimeFormatOptions) => {
|
||||
const key = date.toString();
|
||||
let lds = localeDateStringCache[key];
|
||||
if (!lds) {
|
||||
lds = date.toLocaleDateString(locale, dateTimeOptions);
|
||||
localeDateStringCache[key] = lds;
|
||||
}
|
||||
return lds;
|
||||
};
|
||||
const dateTimeOptions: Intl.DateTimeFormatOptions = {
|
||||
weekday: "short",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
};
|
||||
|
||||
export const TaskListTableDefault: React.FC<{
|
||||
rowHeight: number;
|
||||
rowWidth: string;
|
||||
fontFamily: string;
|
||||
fontSize: string;
|
||||
locale: string;
|
||||
tasks: Task[];
|
||||
selectedTaskId: string;
|
||||
setSelectedTask: (taskId: string) => void;
|
||||
onExpanderClick: (task: Task) => void;
|
||||
}> = ({
|
||||
rowHeight,
|
||||
rowWidth,
|
||||
tasks,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
locale,
|
||||
onExpanderClick,
|
||||
}) => {
|
||||
const toLocaleDateString = useMemo(
|
||||
() => toLocaleDateStringFactory(locale),
|
||||
[locale]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.taskListWrapper}
|
||||
style={{
|
||||
fontFamily: fontFamily,
|
||||
fontSize: fontSize,
|
||||
}}
|
||||
>
|
||||
{tasks.map(t => {
|
||||
let expanderSymbol = "";
|
||||
if (t.hideChildren === false) {
|
||||
expanderSymbol = "▼";
|
||||
} else if (t.hideChildren === true) {
|
||||
expanderSymbol = "▶";
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.taskListTableRow}
|
||||
style={{ height: rowHeight }}
|
||||
key={`${t.id}row`}
|
||||
>
|
||||
<div
|
||||
className={styles.taskListCell}
|
||||
style={{
|
||||
minWidth: rowWidth,
|
||||
maxWidth: rowWidth,
|
||||
}}
|
||||
title={t.name}
|
||||
>
|
||||
<div className={styles.taskListNameWrapper}>
|
||||
<div
|
||||
className={
|
||||
expanderSymbol
|
||||
? styles.taskListExpander
|
||||
: styles.taskListEmptyExpander
|
||||
}
|
||||
onClick={() => onExpanderClick(t)}
|
||||
>
|
||||
{expanderSymbol}
|
||||
</div>
|
||||
<div>{t.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={styles.taskListCell}
|
||||
style={{
|
||||
minWidth: rowWidth,
|
||||
maxWidth: rowWidth,
|
||||
}}
|
||||
>
|
||||
{toLocaleDateString(t.start, dateTimeOptions)}
|
||||
</div>
|
||||
<div
|
||||
className={styles.taskListCell}
|
||||
style={{
|
||||
minWidth: rowWidth,
|
||||
maxWidth: rowWidth,
|
||||
}}
|
||||
>
|
||||
{toLocaleDateString(t.end, dateTimeOptions)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,95 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { BarTask } from '../../types/bar-task';
|
||||
import { Task } from '../../types/public-types';
|
||||
|
||||
export type TaskListProps = {
|
||||
headerHeight: number;
|
||||
rowWidth: string;
|
||||
fontFamily: string;
|
||||
fontSize: string;
|
||||
rowHeight: number;
|
||||
ganttHeight: number;
|
||||
scrollY: number;
|
||||
locale: string;
|
||||
tasks: Task[];
|
||||
taskListRef: React.RefObject<HTMLDivElement>;
|
||||
horizontalContainerClass?: string;
|
||||
selectedTask: BarTask | undefined;
|
||||
setSelectedTask: (task: string) => void;
|
||||
onExpanderClick: (task: Task) => void;
|
||||
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;
|
||||
setSelectedTask: (taskId: string) => void;
|
||||
onExpanderClick: (task: Task) => void;
|
||||
}>;
|
||||
};
|
||||
|
||||
export const TaskList: React.FC<TaskListProps> = ({
|
||||
headerHeight,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
rowWidth,
|
||||
rowHeight,
|
||||
scrollY,
|
||||
tasks,
|
||||
selectedTask,
|
||||
setSelectedTask,
|
||||
onExpanderClick,
|
||||
locale,
|
||||
ganttHeight,
|
||||
taskListRef,
|
||||
horizontalContainerClass,
|
||||
TaskListHeader,
|
||||
TaskListTable,
|
||||
}) => {
|
||||
const horizontalContainerRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
if (horizontalContainerRef.current) {
|
||||
horizontalContainerRef.current.scrollTop = scrollY;
|
||||
}
|
||||
}, [scrollY]);
|
||||
|
||||
const headerProps = {
|
||||
headerHeight,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
rowWidth,
|
||||
};
|
||||
const selectedTaskId = selectedTask ? selectedTask.id : '';
|
||||
const tableProps = {
|
||||
rowHeight,
|
||||
rowWidth,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
tasks,
|
||||
locale,
|
||||
selectedTaskId: selectedTaskId,
|
||||
setSelectedTask,
|
||||
onExpanderClick,
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={taskListRef}>
|
||||
<TaskListHeader {...headerProps} />
|
||||
<div
|
||||
ref={horizontalContainerRef}
|
||||
className={horizontalContainerClass}
|
||||
style={ganttHeight ? { height: ganttHeight } : {}}
|
||||
>
|
||||
<TaskListTable {...tableProps} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,589 @@
|
||||
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,
|
||||
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 = 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,201 @@
|
||||
import { Task, ViewMode } from '../types/public-types';
|
||||
|
||||
const DateTimeFormat = Intl.DateTimeFormat;
|
||||
type DateTimeFormat = typeof DateTimeFormat;
|
||||
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) => {
|
||||
console.log(tasks);
|
||||
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',
|
||||
}).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,
|
||||
}).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;
|
||||
}
|
||||
};
|
@ -0,0 +1,25 @@
|
||||
// import DeleteEvent from './DeleteEvent';
|
||||
import { ActionBar } from '../action';
|
||||
import { Gantt } from './components/gantt/gantt';
|
||||
import { GanttDesigner } from './Gantt.Designer';
|
||||
// import { Event } from './Event';
|
||||
// import { Nav } from './Nav';
|
||||
// import './style.less';
|
||||
// import { Title } from './Title';
|
||||
// import { Today } from './Today';
|
||||
// import { ViewSelect } from './ViewSelect';
|
||||
|
||||
import { ViewMode } from './types/public-types';
|
||||
|
||||
Gantt.ActionBar = ActionBar;
|
||||
// Calendar.Event = Event;
|
||||
// Calendar.DeleteEvent = DeleteEvent;
|
||||
// Calendar.Title = Title;
|
||||
// Calendar.Today = Today;
|
||||
Gantt.ViewMode = ViewMode;
|
||||
// Gantt.ViewSelect = ViewSelect;
|
||||
Gantt.Designer = GanttDesigner;
|
||||
|
||||
// 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,23 @@
|
||||
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[];
|
||||
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 = "Quarter Day",
|
||||
HalfDay = "Half Day",
|
||||
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[];
|
||||
}
|
@ -36,4 +36,5 @@ export * from './tabs';
|
||||
export * from './time-picker';
|
||||
export * from './tree-select';
|
||||
export * from './upload';
|
||||
export * from './gantt'
|
||||
import './index.less';
|
||||
|
@ -161,7 +161,6 @@ SchemaInitializer.Item = (props: SchemaInitializerItemProps) => {
|
||||
return <Menu.Divider key={`divider-${indexA}`} />;
|
||||
}
|
||||
if (item.type === 'itemGroup') {
|
||||
console.log(item.children);
|
||||
return (
|
||||
<Menu.ItemGroup
|
||||
// @ts-ignore
|
||||
|
@ -42,6 +42,12 @@ export const BlockInitializers = {
|
||||
title: '{{t("Kanban")}}',
|
||||
component: 'KanbanBlockInitializer',
|
||||
},
|
||||
{
|
||||
key: 'Gantt',
|
||||
type: 'item',
|
||||
title: '{{t("Gantt")}}',
|
||||
component: 'GanttBlockInitializer',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SchemaInitializer } from '../SchemaInitializer';
|
||||
import {
|
||||
itemsMerge,
|
||||
useAssociatedTableColumnInitializerFields,
|
||||
useTableColumnInitializerFields,
|
||||
useInheritsTableColumnInitializerFields,
|
||||
} from '../utils';
|
||||
import { useCompile } from '../../schema-component';
|
||||
|
||||
// 甘特图表格列配置
|
||||
export const GanttColumnInitializers = (props: any) => {
|
||||
const { items = [] } = props;
|
||||
const { t } = useTranslation();
|
||||
const associatedFields = useAssociatedTableColumnInitializerFields();
|
||||
const inheritFields = useInheritsTableColumnInitializerFields();
|
||||
const compile = useCompile();
|
||||
const fieldItems: any[] = [
|
||||
{
|
||||
type: 'itemGroup',
|
||||
title: t('Display fields'),
|
||||
children: useTableColumnInitializerFields(),
|
||||
},
|
||||
];
|
||||
if (inheritFields?.length > 0) {
|
||||
inheritFields.forEach((inherit) => {
|
||||
Object.values(inherit)[0].length &&
|
||||
fieldItems.push(
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
type: 'itemGroup',
|
||||
title: t(`Parent collection fields`) + '(' + compile(`${Object.keys(inherit)[0]}`) + ')',
|
||||
children: Object.values(inherit)[0],
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
if (associatedFields?.length > 0) {
|
||||
fieldItems.push(
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
type: 'itemGroup',
|
||||
title: t('Display association fields'),
|
||||
children: associatedFields,
|
||||
},
|
||||
);
|
||||
}
|
||||
fieldItems.push(
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
title: t('Action column'),
|
||||
component: 'TableActionColumnInitializer',
|
||||
},
|
||||
);
|
||||
console.log(5)
|
||||
return (
|
||||
<SchemaInitializer.Button
|
||||
insertPosition={'beforeEnd'}
|
||||
icon={'SettingOutlined'}
|
||||
wrap={(s) => {
|
||||
console.log(s)
|
||||
if (s['x-action-column']) {
|
||||
return s;
|
||||
}
|
||||
return {
|
||||
type: 'void',
|
||||
'x-decorator': 'TableV2.Column.Decorator',
|
||||
'x-designer': 'TableV2.Column.Designer',
|
||||
'x-component': 'TableV2.Column',
|
||||
properties: {
|
||||
[s.name]: {
|
||||
...s,
|
||||
},
|
||||
},
|
||||
};
|
||||
}}
|
||||
items={itemsMerge(fieldItems, items)}
|
||||
>
|
||||
{t('Configure columns')}
|
||||
</SchemaInitializer.Button>
|
||||
);
|
||||
};
|
@ -19,5 +19,6 @@ export * from './TableActionInitializers';
|
||||
export * from './TableColumnInitializers';
|
||||
export * from './TableSelectorInitializers';
|
||||
export * from './TabPaneInitializers';
|
||||
export * from './GanttColumnInitializers'
|
||||
// association filter
|
||||
export * from '../../schema-component/antd/association-filter/AssociationFilter';
|
||||
|
@ -0,0 +1,103 @@
|
||||
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 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,
|
||||
'x-component': 'Select',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
range: {
|
||||
title: t('Time range'),
|
||||
enum: [
|
||||
{ label: '{{t("Hour")}}', value: 'hour', color: 'orange' },
|
||||
{ label: '{{t("Quarter of day")}}', value: 'quarterOfDay', color: 'default' },
|
||||
{ label: '{{t("Half of day")}}', value: 'halOfDay', 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' },
|
||||
],
|
||||
'x-component': 'Select',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</FormLayout>
|
||||
</SchemaComponentOptions>
|
||||
);
|
||||
}).open({
|
||||
initialValues: {},
|
||||
});
|
||||
insert(
|
||||
createGanttBlockSchema({
|
||||
collection: item.name,
|
||||
fieldNames: {
|
||||
...values,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
@ -37,6 +37,8 @@ export * from './TableSelectorInitializer';
|
||||
export * from './UpdateActionInitializer';
|
||||
export * from './UpdateSubmitActionInitializer';
|
||||
export * from './ViewActionInitializer';
|
||||
export * from './GanttBlockInitializer'
|
||||
|
||||
// association filter
|
||||
export * from '../../schema-component/antd/association-filter/AssociationFilter';
|
||||
export * from '../../schema-component/antd/association-filter/ActionBarAssociationFilterAction';
|
||||
|
@ -1016,6 +1016,96 @@ export const createCalendarBlockSchema = (options) => {
|
||||
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: 'array',
|
||||
'x-initializer': 'GanttColumnInitializers',
|
||||
'x-component': 'Gantt',
|
||||
'x-component-props': {
|
||||
useProps: '{{ useGanttBlockProps }}',
|
||||
},
|
||||
properties: {
|
||||
toolBar: {
|
||||
type: 'void',
|
||||
'x-component': 'Gantt.ActionBar',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
},
|
||||
'x-initializer': 'GanttActionInitializers',
|
||||
properties: {},
|
||||
},
|
||||
event: {
|
||||
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-props': {
|
||||
actionInitializers: 'GanttFormActionInitializers',
|
||||
},
|
||||
'x-initializer': 'RecordBlockInitializers',
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
console.log(JSON.stringify(schema, null, 2));
|
||||
return schema;
|
||||
};
|
||||
export const createKanbanBlockSchema = (options) => {
|
||||
const { collection, resource, groupField, ...others } = options;
|
||||
const schema: ISchema = {
|
||||
|
Loading…
x
Reference in New Issue
Block a user