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