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