From 87a05884a8bbf7f3237eee65dc862f675ee2d62f Mon Sep 17 00:00:00 2001 From: Marta Kowalska Date: Wed, 28 Jan 2026 14:26:58 +0000 Subject: [PATCH] v2.5.2 --- .sync | 2 +- demos/cases/GanttTaskTypes.css | 24 ++-- demos/cases/GanttTaskTypes.jsx | 20 +-- demos/cases/ProExport.css | 11 ++ demos/cases/ProExport.jsx | 199 +++++++++++++++++++++++++++ demos/cases/ProMSProject.css | 5 + demos/cases/ProMSProject.jsx | 60 ++++++++ demos/cases/ProSummariesConvert.jsx | 95 +------------ demos/cases/ProSummariesProgress.jsx | 122 ++-------------- demos/common/Index.jsx | 19 ++- demos/custom/UbloadButton.css | 14 ++ demos/custom/UploadButton.jsx | 35 +++++ demos/data.js | 64 ++++----- demos/routes.js | 2 +- package-lock.json | 141 ++++++++++--------- package.json | 18 +-- src/components/ContextMenu.jsx | 24 ++-- src/components/Editor.jsx | 12 +- src/components/Gantt.jsx | 17 ++- src/components/chart/Bars.jsx | 42 +++--- src/components/chart/Chart.css | 1 + src/components/editor/Links.jsx | 104 +++++++------- src/index.js | 4 + whatsnew.md | 37 +++-- 24 files changed, 627 insertions(+), 445 deletions(-) create mode 100644 demos/cases/ProExport.css create mode 100644 demos/cases/ProExport.jsx create mode 100644 demos/cases/ProMSProject.css create mode 100644 demos/cases/ProMSProject.jsx create mode 100644 demos/custom/UbloadButton.css create mode 100644 demos/custom/UploadButton.jsx diff --git a/.sync b/.sync index 9a9dc47..b02e0ac 100644 --- a/.sync +++ b/.sync @@ -1 +1 @@ -913b95b49f0d253d50d6b3461c48aaa9facb7ba2 +b33bea9b4d106757875dab25dada246fc8d8864a diff --git a/demos/cases/GanttTaskTypes.css b/demos/cases/GanttTaskTypes.css index c720101..3da7f6b 100644 --- a/demos/cases/GanttTaskTypes.css +++ b/demos/cases/GanttTaskTypes.css @@ -2,54 +2,54 @@ overflow: hidden; height: 100%; } -.demo.wx-I1glfWSB > .wx-gantt .wx-bar.wx-task.narrow { +.demo.wx-I1glfWSB .wx-gantt .wx-bar.wx-task.narrow { background-color: #676a81; color: transparent; height: 10px !important; margin-top: 10px; border: 1px solid #63667a; } -.demo.wx-I1glfWSB > .wx-gantt .wx-bar.wx-task.narrow .wx-progress-percent { +.demo.wx-I1glfWSB .wx-gantt .wx-bar.wx-task.narrow .wx-progress-percent { background-color: #1a2630; } -.demo.wx-I1glfWSB > .wx-gantt .wx-bar.wx-task.narrow .wx-link { +.demo.wx-I1glfWSB .wx-gantt .wx-bar.wx-task.narrow .wx-link { background-color: #384047; border-radius: 0px; } -.demo.wx-I1glfWSB > .wx-gantt .wx-bar.wx-task.narrow .wx-link .wx-inner { +.demo.wx-I1glfWSB .wx-gantt .wx-bar.wx-task.narrow .wx-link .wx-inner { border-radius: 0px; } -.demo.wx-I1glfWSB > .wx-gantt .wx-bar.wx-task.round { +.demo.wx-I1glfWSB .wx-gantt .wx-bar.wx-task.round { background-color: #ffeb3b; border: 1px solid #ffeb3b; color: #384047; border-radius: 50px; } -.demo.wx-I1glfWSB > .wx-gantt .wx-bar.wx-task.round .wx-progress-percent { +.demo.wx-I1glfWSB .wx-gantt .wx-bar.wx-task.round .wx-progress-percent { background-color: #ffc107; } -.demo.wx-I1glfWSB > .wx-gantt .wx-bar.wx-task.round .wx-progress-wrapper { +.demo.wx-I1glfWSB .wx-gantt .wx-bar.wx-task.round .wx-progress-wrapper { border-radius: 50px; } -.demo.wx-I1glfWSB > .wx-gantt .wx-bar.wx-task.progress { +.demo.wx-I1glfWSB .wx-gantt .wx-bar.wx-task.progress { background-color: transparent; color: var(--wx-color-font); border-radius: 50px; border: 1px solid #00bcd4; } -.demo.wx-I1glfWSB > .wx-gantt .wx-bar.wx-task.progress .wx-progress-percent { +.demo.wx-I1glfWSB .wx-gantt .wx-bar.wx-task.progress .wx-progress-percent { background-color: #00bcd4; } -.demo.wx-I1glfWSB > .wx-gantt .wx-bar.wx-task.progress .wx-progress-wrapper { +.demo.wx-I1glfWSB .wx-gantt .wx-bar.wx-task.progress .wx-progress-wrapper { border-radius: 50px; } -.demo.wx-I1glfWSB > .wx-gantt .wx-bar.wx-task.urgent { +.demo.wx-I1glfWSB .wx-gantt .wx-bar.wx-task.urgent { background-color: #f49a82; border: 1px solid #f45e36; } -.demo.wx-I1glfWSB > .wx-gantt .wx-bar.wx-task.urgent .wx-progress-percent { +.demo.wx-I1glfWSB .wx-gantt .wx-bar.wx-task.urgent .wx-progress-percent { background-color: #f45e36; } diff --git a/demos/cases/GanttTaskTypes.jsx b/demos/cases/GanttTaskTypes.jsx index 7f2510e..3c3dbf7 100644 --- a/demos/cases/GanttTaskTypes.jsx +++ b/demos/cases/GanttTaskTypes.jsx @@ -1,6 +1,6 @@ import { useMemo, useState } from 'react'; import { getTypedData, taskTypes } from '../data'; -import { Gantt, Editor } from '../../src/'; +import { Gantt, Editor, ContextMenu } from '../../src/'; import './GanttTaskTypes.css'; function GanttTaskTypes(props) { @@ -11,14 +11,16 @@ function GanttTaskTypes(props) { return (
- + + + {api && }
); diff --git a/demos/cases/ProExport.css b/demos/cases/ProExport.css new file mode 100644 index 0000000..65fea25 --- /dev/null +++ b/demos/cases/ProExport.css @@ -0,0 +1,11 @@ +.rselect { + width: 150px; +} +.wx-gantt .myMarker { + background-color: rgba(255, 84, 84, 0.77); +} +.gtcell { + position: relative; + height: calc(100% - 48px); + border-top: var(--wx-gantt-border); +} diff --git a/demos/cases/ProExport.jsx b/demos/cases/ProExport.jsx new file mode 100644 index 0000000..a8b0230 --- /dev/null +++ b/demos/cases/ProExport.jsx @@ -0,0 +1,199 @@ +import { useMemo, useState } from 'react'; +import { getData } from '../data'; +import { Gantt, version } from '../../src/'; +import { Toolbar, registerToolbarItem } from '@svar-ui/react-toolbar'; +import { Switch, RichSelect, Segmented } from '@svar-ui/react-core'; +import { Calendar } from '@svar-ui/gantt-store'; +import './ProExport.css'; + +registerToolbarItem('switch', Switch); +registerToolbarItem('segmented', Segmented); +registerToolbarItem('richselect', RichSelect); + +export default function ProExport({ skinSettings }) { + + const [size, setSize] = useState('auto'); + const [fit, setFit] = useState(true); + const [api, setApi] = useState(); + const [config, setConfig] = useState('basic'); + + const data = useMemo(() => { + const advanced = config === 'advanced'; + return getData(null, { + splitTasks: advanced, + baselines: advanced, + unscheduledTasks: advanced, + }); + }, [config]); + + const items = useMemo(() => { + return [ + { text: 'Page size' }, + { + id: 'size', + comp: 'richselect', + css: 'rselect', + value: size, + options: [ + { id: 'auto', label: 'Auto' }, + { id: 'a4-landscape', label: 'A4 Landscape' }, + { id: 'a4', label: 'A4 Portrait' }, + { id: 'a3-landscape', label: 'A3 Landscape' }, + { id: 'a3', label: 'A3 Portrait' }, + ], + handler: () => {}, + }, + { text: 'Fit to page' }, + { + id: 'fit', + comp: 'switch', + value: fit, + handler: () => {}, + }, + { + id: 'export-pdf', + comp: 'button', + text: 'To PDF', + handler: () => exportOthers('pdf'), + }, + { + id: 'export-png', + comp: 'button', + text: 'To PNG', + handler: () => exportOthers('png'), + }, + { comp: 'separator' }, + { + id: 'export-xlsx', + comp: 'button', + text: 'To XLSX', + handler: () => exportExcel(false), + }, + { + id: 'export-xlsx-chart', + comp: 'button', + text: 'To XLSX with Chart', + handler: () => exportExcel(true), + }, + { + id: 'export-mspx', + comp: 'button', + text: 'To MS Project (XML)', + handler: () => exportOthers('mspx'), + }, + { comp: 'spacer' }, + { + id: 'config', + comp: 'segmented', + value: config, + options: [ + { id: 'basic', label: 'Basic' }, + { id: 'advanced', label: 'Advanced' }, + ], + handler: () => {}, + }, + ]; + }, [size, fit, config, api]); + + const markers = useMemo( + () => [ + { + start: new Date(2026, 3, 8), + text: 'Approval of strategy', + css: 'myMarker', + }, + ], + [], + ); + + const calendar = useMemo(() => new Calendar(), []); + + function handleClick({ item }) { + const parts = item.id.split('-'); + if (parts[0] === 'export') { + if (parts[1] === 'xlsx') { + exportExcel(parts[2] === 'chart'); + } else { + exportOthers(parts[1]); + } + } + } + + const url = 'https://export.svar.dev/gantt/' + version; + + function exportExcel(visual) { + api.exec('export-data', { + url, + format: 'xlsx', + excel: { + columns: visual + ? [ + { + id: 'text', + header: 'Task name', + width: 200, + }, + ] + : null, + sheetNames: ['Tasks', 'Links'], + dateFormat: 'yyyy-mmm-dd', + visual, + }, + }); + } + + function exportOthers(format) { + const parts = size.split('-'); + const props = { + size: parts[0], + landscape: parts[1] === 'landscape', + fitSize: fit && size != 'auto', + styles: '.wx-gantt .myMarker{ background-color: rgba(255, 84, 84, 0.77);', + }; + api.exec('export-data', { + url, + format, + pdf: props, + png: props, + ganttConfig: { + cellWidth: 30, + }, + }); + } + + function handleChange({ item, value }) { + if (item.id === 'size') setSize(value); + else if (item.id === 'fit') setFit(value); + else if (item.id === 'config') setConfig(value); + } + + return ( + <> + +
+ {config === 'basic' ? ( + + ) : ( + + )} +
+ + ); +} diff --git a/demos/cases/ProMSProject.css b/demos/cases/ProMSProject.css new file mode 100644 index 0000000..6e3b465 --- /dev/null +++ b/demos/cases/ProMSProject.css @@ -0,0 +1,5 @@ +.gtcell { + position: relative; + height: calc(100% - 48px); + border-top: var(--wx-gantt-border); +} diff --git a/demos/cases/ProMSProject.jsx b/demos/cases/ProMSProject.jsx new file mode 100644 index 0000000..5832bc7 --- /dev/null +++ b/demos/cases/ProMSProject.jsx @@ -0,0 +1,60 @@ +import { getData } from "../data"; +import { Gantt } from "../../src"; +import { Toolbar, registerToolbarItem } from "@wx/react-toolbar"; +import UploadButton from "../custom/UploadButton"; +import { useCallback, useState } from "react"; + +import './ProMSProject.css'; + +registerToolbarItem("upload", UploadButton); +export default function ProMSProject({ skinSettings }) { + const data = getData(); + const [api, setApi] = useState(); + + const handleClick = useCallback(({ item }) => { + if (item.id === "export") { + api.exec("export-data", { format: "mspx" }); + } + }, [api]); + + const importMSProject = useCallback(() => { + const file = document.getElementById("import-file").files[0]; + const reader = new FileReader(); + reader.onload = e => { + const xml = e.target.result; + api.exec("import-data", { + data: xml, + }); + }; + reader.readAsText(file); + }, [api]); + + const items = [ + { + id: "export", + comp: "button", + text: "Download MS Project XML", + }, + { + id: "import", + comp: "upload", + text: "Upload MS Project XML", + onChange: importMSProject, + }, + ]; + + return ( + <> + +
+ +
+ + ); +} \ No newline at end of file diff --git a/demos/cases/ProSummariesConvert.jsx b/demos/cases/ProSummariesConvert.jsx index 74f2fae..83ec4e7 100644 --- a/demos/cases/ProSummariesConvert.jsx +++ b/demos/cases/ProSummariesConvert.jsx @@ -1,109 +1,28 @@ -import { useMemo, useCallback, useRef } from 'react'; -import { useStoreLater } from '@svar-ui/lib-react'; +import { useMemo, useState } from 'react'; import { getData } from '../data'; import { Gantt, ContextMenu, Editor, - getEditorItems, - defaultTaskTypes, -} from '../../src'; +} from '../../src/'; export default function SummariesConvert({ skinSettings }) { const data = useMemo(() => getData(), []); - const gApi = useRef(null); - - const toSummary = useCallback( - (id, self) => { - const api = gApi.current; - if (!api) return; - const task = api.getTask(id); - if (!self) id = task.parent; - - if (id && task.type !== 'summary') { - api.exec('update-task', { - id, - task: { type: 'summary' }, - }); - } - }, - [gApi], - ); - - const toTask = useCallback( - (id) => { - const api = gApi.current; - if (!api) return; - const obj = api.getTask(id); - if (obj && !obj.data?.length) { - api.exec('update-task', { - id, - task: { type: 'task' }, - }); - } - }, - [gApi], - ); - - const init = useCallback( - (api) => { - gApi.current = api; - - api.getState().tasks.forEach((task) => { - if (task.data?.length) { - toSummary(task.id, true); - } - }); - - api.on('add-task', ({ id, mode }) => { - if (mode === 'child') toSummary(id); - }); - - api.on('move-task', ({ id, source, mode, inProgress }) => { - if (inProgress) return; - if (mode == 'child') toSummary(id); - else toTask(source); - }); - - api.on('delete-task', ({ source }) => { - toTask(source); - }); - }, - [toSummary, toTask], - ); - - const activeTaskId = useStoreLater(gApi.current, 'activeTask'); - - const items = useMemo(() => { - const api = gApi.current; - const task = activeTaskId ? api?.getTask(activeTaskId) : null; - if (task) { - return getEditorItems().map((item) => { - item = { ...item }; - if (item.comp === 'select' && item.key === 'type') { - item.options = - task.type !== 'summary' - ? defaultTaskTypes.filter((t) => t.id !== 'summary') - : []; - } - return item; - }); - } - return undefined; - }, [activeTaskId, gApi.current]); + const [api, setApi] = useState(); return (
- + - {gApi.current && } + {api && }
); } diff --git a/demos/cases/ProSummariesProgress.jsx b/demos/cases/ProSummariesProgress.jsx index 2deac7d..d5d0e87 100644 --- a/demos/cases/ProSummariesProgress.jsx +++ b/demos/cases/ProSummariesProgress.jsx @@ -1,131 +1,29 @@ -import { useState, useMemo, useCallback, useEffect, useRef } from 'react'; +import { useState, useMemo } from 'react'; import { getData } from '../data'; -import { Gantt, Editor, getEditorItems, ContextMenu } from '../../src'; -import './ProSummariesProgress.css'; +import { Gantt, Editor, ContextMenu } from '../../src'; function SummariesProgress({ skinSettings }) { const data = useMemo(() => getData(), []); - const tasks = data.tasks; + const { tasks, links, scales } = data; const [api, setApi] = useState(); - const dayDiff = (next, prev) => { - const d = (next - prev) / 1000 / 60 / 60 / 24; - return Math.ceil(Math.abs(d)); - }; - - /** - * The formula of calculation is ∑d*p / ∑d , where "d" is task duration in days, - * "p" is the task progress and "∑" stands for a sum of all loaded tasks - */ - function getSummaryProgress(id) { - const [totalProgress, totalDuration] = collectProgressFromKids(id); - const res = totalProgress / totalDuration; - return isNaN(res) ? 0 : Math.round(res); - } - - function collectProgressFromKids(id) { - let totalProgress = 0, - totalDuration = 0; - const kids = api.getTask(id).data; - - kids?.forEach((kid) => { - let duration = 0; - if (kid.type !== 'milestone' && kid.type !== 'summary') { - duration = kid.duration || dayDiff(kid.end, kid.start); - totalDuration += duration; - totalProgress += duration * kid.progress; - } - - const [p, d] = collectProgressFromKids(kid.id); - totalProgress += p; - totalDuration += d; - }); - return [totalProgress, totalDuration]; - } - - function recalcSummaryProgress(id, self) { - const { tasks } = api.getState(); - const task = api.getTask(id); - - if (task.type != 'milestone') { - const summary = - self && task.type === 'summary' ? id : tasks.getSummaryId(id); - - if (summary) { - const progress = getSummaryProgress(summary); - api.exec('update-task', { - id: summary, - task: { progress }, - }); - } - } - } - const recalcRef = useRef(null); - recalcRef.current = recalcSummaryProgress; - - const init = useCallback((api) => { - setApi(api); - // auto progress calculations - api.on('add-task', ({ id }) => { - recalcRef.current(id); - }); - api.on('update-task', ({ id }) => { - recalcRef.current(id); - }); - - api.on('delete-task', ({ source }) => { - recalcRef.current(source, true); - }); - api.on('copy-task', ({ id }) => { - recalcRef.current(id); - }); - api.on('move-task', ({ id, source, inProgress }) => { - if (inProgress) return; - - if (api.getTask(id).parent != source) recalcRef.current(source, true); - recalcRef.current(id); - }); - - }, []); - return ( -
+ <> -
-
- {api && } -
+ {api && } + ); } -function ConfiguredEditor({ api }) { - const [editorItems, setEditorItems] = useState(getEditorItems()); - - // disabling progress slider for summary tasks - api.on('show-editor', ({ id }) => { - if (id) { - const type = api.getTask(id).type; - setEditorItems( - getEditorItems().map((ed) => ({ - ...ed, - ...(ed.key == 'progress' && { - config: { disabled: type === 'summary' }, - }), - })), - ); - } - }); - return ; -} - export default SummariesProgress; diff --git a/demos/common/Index.jsx b/demos/common/Index.jsx index 1fe91f3..ddeae57 100644 --- a/demos/common/Index.jsx +++ b/demos/common/Index.jsx @@ -17,28 +17,25 @@ function DemoExplorerContent({ }) { const navigate = useNavigate(); const [skin, setSkin] = useState(skins[0].id); - const [title, setTitle] = useState(''); - const [githubLink, setGithubLink] = useState(''); const [show, setShow] = useState(true); + const currentPath = window.location.hash.slice(1); + const parts = currentPath.split('/'); + const currentPage = `/${parts[1]}/:skin`; + const matched = links.find((a) => a[0] === currentPage); + const title = matched ? matched[1] : ''; const baseLink = 'https://github.com/svar-widgets/react-' + productTag + '/tree/main/demos/cases/'; + const name = matched ? (matched[3] || matched[1]) : ''; + const githubLink = `${baseLink}${name}.jsx`; + const handleRouteChange = useCallback((path) => { const parts = path.split('/'); - const page = parts[1]; const newSkin = parts[2]; setSkin(newSkin); - - const targetPage = `/${page}/:skin`; - const matched = links.find((a) => a[0] === targetPage); - if (matched) { - setTitle(matched[1]); - const name = matched[3] || matched[1]; - setGithubLink(`${baseLink}${name}.jsx`); - } }, []); const handleSkinChange = ({ value }) => { diff --git a/demos/custom/UbloadButton.css b/demos/custom/UbloadButton.css new file mode 100644 index 0000000..553d483 --- /dev/null +++ b/demos/custom/UbloadButton.css @@ -0,0 +1,14 @@ +.button { + font-size: 14px; + line-height: 20px; + font-weight: 600; + padding: 5px 16px; + border: 1px solid transparent; + border-radius: 3px; + background-color: #f2f2f7; + color: #2c2f3c; +} +:global(.wx-willow-dark-theme .button) { + background-color: #384047; + color: rgba(255, 255, 255, 0.9); +} diff --git a/demos/custom/UploadButton.jsx b/demos/custom/UploadButton.jsx new file mode 100644 index 0000000..d51190c --- /dev/null +++ b/demos/custom/UploadButton.jsx @@ -0,0 +1,35 @@ +import React, { useRef } from 'react'; +import './UbloadButton.css'; + +function UploadButton({ text, onChange }) { + + const fileInput = useRef(null); + + function handleClick() { + fileInput.current?.click(); + } + + function handleChange() { + if (onChange) { + onChange(); + } + } + + return ( + <> + + + + ); +} + +export default UploadButton; \ No newline at end of file diff --git a/demos/data.js b/demos/data.js index 9affee1..4d775a9 100644 --- a/demos/data.js +++ b/demos/data.js @@ -53,7 +53,7 @@ const tasks = [ base_start: new Date(2026, 3, 2), base_end: new Date(2026, 3, 16), text: 'Project planning', - progress: 30, + progress: 40, parent: 0, type: 'summary', open: true, @@ -76,7 +76,7 @@ const tasks = [ start: new Date(2026, 3, 5), duration: 2, text: 'Discussions', - progress: 100, + progress: 72, parent: 1, type: 'task', details: 'Team discussions on project strategies.', @@ -139,7 +139,7 @@ const tasks = [ base_start: new Date(2026, 3, 2), base_end: new Date(2026, 3, 12), text: 'Project management', - progress: 10, + progress: 8, parent: 0, type: 'summary', open: true, @@ -191,7 +191,7 @@ const tasks = [ base_start: new Date(2026, 3, 9), base_end: new Date(2026, 3, 30), text: 'Development', - progress: 30, + progress: 2, parent: 0, type: 'summary', open: true, @@ -202,7 +202,7 @@ const tasks = [ start: new Date(2026, 3, 9), duration: 6, text: 'Prototyping', - progress: 3, + progress: 7, parent: 3, type: 'task', assigned: 3, @@ -233,7 +233,7 @@ const tasks = [ base_start: new Date(2026, 3, 9), base_end: new Date(2026, 4, 3), text: 'Testing', - progress: 3, + progress: 4, parent: 0, type: 'summary', open: true, @@ -244,7 +244,7 @@ const tasks = [ start: new Date(2026, 3, 9), duration: 4, text: 'Testing prototype', - progress: 3, + progress: 24, parent: 4, type: 'task', assigned: 4, @@ -300,7 +300,7 @@ const calendarTasks = [ //start: new Date(2026, 3, 2), //end: new Date(2026, 3, 18), text: 'Project planning', - progress: 30, + progress: 40, parent: 0, type: 'summary', open: true, @@ -323,7 +323,7 @@ const calendarTasks = [ start: new Date(2026, 3, 7), duration: 2, text: 'Discussions', - progress: 100, + progress: 72, parent: 1, type: 'task', details: 'Team discussions on project strategies.', @@ -386,7 +386,7 @@ const calendarTasks = [ //start: new Date(2026, 3, 2), //end: new Date(2026, 3, 14), text: 'Project management', - progress: 10, + progress: 8, parent: 0, type: 'summary', open: true, @@ -437,7 +437,7 @@ const calendarTasks = [ //start: new Date(2026, 3, 9), //end: new Date(2026, 4, 8), text: 'Development', - progress: 30, + progress: 2, parent: 0, type: 'summary', open: true, @@ -448,7 +448,7 @@ const calendarTasks = [ start: new Date(2026, 3, 13), duration: 4, text: 'Prototyping', - progress: 3, + progress: 7, parent: 3, type: 'task', assigned: 3, @@ -479,7 +479,7 @@ const calendarTasks = [ //start: new Date(2026, 3, 9), //end: new Date(2026, 4, 13), text: 'Testing', - progress: 3, + progress: 4, parent: 0, type: 'summary', open: true, @@ -490,7 +490,7 @@ const calendarTasks = [ start: new Date(2026, 3, 17), duration: 4, text: 'Testing prototype', - progress: 3, + progress: 24, parent: 4, type: 'task', assigned: 4, @@ -608,11 +608,6 @@ const links = [ }, ]; -const scales = [ - { unit: 'month', step: 1, format: '%F %Y' }, - { unit: 'day', step: 1, format: '%j', css: dayStyle }, -]; - const tasksHour = [ { id: 1, @@ -773,7 +768,7 @@ const critTasks = [ start: new Date(2026, 3, 5), end: new Date(2026, 3, 7), text: 'Discussions', - progress: 100, + progress: 72, type: 'task', open: true, details: 'Team discussions on project strategies.', @@ -828,7 +823,7 @@ const critTasks = [ start: new Date(2026, 3, 2), end: new Date(2026, 3, 12), text: 'Project management 1', - progress: 10, + progress: 8, parent: 0, type: 'summary', open: true, @@ -882,7 +877,7 @@ const critTasks = [ start: new Date(2026, 3, 2), end: new Date(2026, 3, 12), text: 'Project management 2', - progress: 10, + progress: 8, parent: 0, type: 'summary', open: true, @@ -1014,6 +1009,11 @@ function addDays(d, n) { return new Date(new Date(d).setDate(d.getDate() + n)); } +const scales = [ + { unit: 'month', step: 1, format: '%F %Y' }, + { unit: 'day', step: 1, format: '%j', css: dayStyle }, +]; + const datasets = { day: { tasks, links, scales }, hour: { tasks: tasksHour, links: linksHour, scales: scalesHour }, @@ -1021,16 +1021,6 @@ const datasets = { calendar: { tasks: calendarTasks, links, scales }, }; -export const taskTypes = [ - { id: 'task', label: 'Task' }, - { id: 'summary', label: 'Summary task' }, - { id: 'milestone', label: 'Milestone' }, - { id: 'urgent', label: 'Urgent' }, - { id: 'narrow', label: 'Narrow' }, - { id: 'progress', label: 'Progress' }, - { id: 'round', label: 'Rounded' }, -]; - export function getData(name, config) { const data = datasets[name || 'day']; @@ -1070,6 +1060,16 @@ export function getData(name, config) { return data; } +export const taskTypes = [ + { id: 'task', label: 'Task' }, + { id: 'summary', label: 'Summary task' }, + { id: 'milestone', label: 'Milestone' }, + { id: 'urgent', label: 'Urgent' }, + { id: 'narrow', label: 'Narrow' }, + { id: 'progress', label: 'Progress' }, + { id: 'round', label: 'Rounded' }, +]; + export function getTypedData() { const t = tasks.map((task, i) => { const res = { ...task }; diff --git a/demos/routes.js b/demos/routes.js index b3aadc2..f94bf2a 100644 --- a/demos/routes.js +++ b/demos/routes.js @@ -186,7 +186,7 @@ export const links = [ ], [ '/backend-provider-batch/:skin', - 'Saving to backend (batch)', + 'Saving to backend: batch request', GanttBatchProvider, 'GanttBatchProvider', ], diff --git a/package-lock.json b/package-lock.json index 53ccc05..e7c2a56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,25 +1,25 @@ { "name": "@svar-ui/react-gantt", - "version": "2.4.5", + "version": "2.5.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@svar-ui/react-gantt", - "version": "2.4.5", + "version": "2.5.2", "license": "MIT", "dependencies": { - "@svar-ui/gantt-data-provider": "2.4.4", - "@svar-ui/gantt-locales": "2.4.4", - "@svar-ui/gantt-store": "2.4.4", + "@svar-ui/gantt-data-provider": "2.5.1", + "@svar-ui/gantt-locales": "2.5.1", + "@svar-ui/gantt-store": "2.5.1", "@svar-ui/lib-dom": "0.11.1", "@svar-ui/lib-react": "1.2.2", "@svar-ui/lib-state": "1.9.6", - "@svar-ui/react-core": "2.4.1", - "@svar-ui/react-editor": "2.4.1", - "@svar-ui/react-grid": "2.4.1", - "@svar-ui/react-menu": "2.4.1", - "@svar-ui/react-toolbar": "2.4.1" + "@svar-ui/react-core": "2.4.3", + "@svar-ui/react-editor": "2.4.3", + "@svar-ui/react-grid": "2.5.1", + "@svar-ui/react-menu": "2.4.3", + "@svar-ui/react-toolbar": "2.4.3" }, "devDependencies": { "@types/react": "18.2.66", @@ -1345,24 +1345,24 @@ } }, "node_modules/@svar-ui/gantt-data-provider": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@svar-ui/gantt-data-provider/-/gantt-data-provider-2.4.4.tgz", - "integrity": "sha512-Jngj0P75STm6cdNqG3mHBcIBq4OWa4eqRLQjgv09xONhx9rYXn9xOOFIyPpyxaeKY9wKr2+H6Z+dhOrJfBg3pA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@svar-ui/gantt-data-provider/-/gantt-data-provider-2.5.1.tgz", + "integrity": "sha512-aBn01tezBRlX5mrvf2lhnBTO1VuXEyPa9a2bKi/qhNBJ5F943EBx2utijC7dVbPKPQvIDU8Iq+utUqLjshOgHQ==", "license": "MIT", "dependencies": { "@svar-ui/lib-data-provider": "1.7.1" } }, "node_modules/@svar-ui/gantt-locales": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@svar-ui/gantt-locales/-/gantt-locales-2.4.4.tgz", - "integrity": "sha512-cVXxoA+4cRMGuzKUC6TKM/A9RysxSlL2BJ6F5kx3VzLNzpKAytDWNRp5j+2QPLBqjtcc/KF5Ha2lEUOW2qc2EA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@svar-ui/gantt-locales/-/gantt-locales-2.5.1.tgz", + "integrity": "sha512-6E3qO4wh/+3o5WZPaPTyMt95AeKt9j3nMsmaemEBZEYeLRV67HP0cTUJHFdqrr3aTRkwhPyc0Xym7QZw8OMdSw==", "license": "MIT" }, "node_modules/@svar-ui/gantt-store": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@svar-ui/gantt-store/-/gantt-store-2.4.4.tgz", - "integrity": "sha512-AioX4ndXz17xvK9nw4qlUJ4PWnHZ3mEylIxAE3KqQZtc1CG6HfjPdfZghMR6sGaYhztNNPaeRoEIiQOk0ageyQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@svar-ui/gantt-store/-/gantt-store-2.5.1.tgz", + "integrity": "sha512-8G66oNpFq2GIy5BtOZ1c07ppK5NTbJldUPBosS5Rgf2wbN5OhjNThGdMYoE0y9kXPBVdjHYVZVef3hd0b/pUkQ==", "license": "MIT", "dependencies": { "@svar-ui/lib-state": "1.9.6", @@ -1370,24 +1370,24 @@ } }, "node_modules/@svar-ui/grid-data-provider": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@svar-ui/grid-data-provider/-/grid-data-provider-2.4.0.tgz", - "integrity": "sha512-Rm0Zbwj3/ZZLTdpmJXX/lGKafL2sqZuikOh/8KXNty7RXjx6WqryjaAkX6t0cl8lnhPjV/awC4R4+uYRW4UbEQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@svar-ui/grid-data-provider/-/grid-data-provider-2.5.0.tgz", + "integrity": "sha512-jI7cPJ9f+ozUoT9IYwHG8coeGvICp6iGI3F4IOLcJ7Hb0/NGaRwz1l0EIt7GjNeJodEIlbpdUZhfbIPSfErx6Q==", "license": "MIT", "dependencies": { "@svar-ui/lib-data-provider": "1.7.1" } }, "node_modules/@svar-ui/grid-locales": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@svar-ui/grid-locales/-/grid-locales-2.4.0.tgz", - "integrity": "sha512-UnhsQYzDAplMES08QXupa7/1BdBXyCujnTuQaIKZcDf/PGRiSFo3I3iNCk6UdJ5Tnj4si9CqB8/wcIp2YyAHSg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@svar-ui/grid-locales/-/grid-locales-2.5.0.tgz", + "integrity": "sha512-xzNE0IRFCeu0KyMNI1CT40sHU5YrvlyWiYk0sMNvUGnC7n+5wfdWeRdAILeZkwXjM4keCl8qhUfTPiMpVYyyGw==", "license": "MIT" }, "node_modules/@svar-ui/grid-store": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@svar-ui/grid-store/-/grid-store-2.4.0.tgz", - "integrity": "sha512-GJyVC+5G+aA8EfcoKMwrHVP2/OK7bUUbp9ofyZTGUAyuXO9tkxRnfquvk+nTA1ybOfbFCY8rSsJTEHYcgXQCTw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@svar-ui/grid-store/-/grid-store-2.5.0.tgz", + "integrity": "sha512-Y5x+M5HANyohP7xExIPFzyMCtf5ABVbEwfac/Q7eY5UPUq2eUszfHgqn1YmyV5F7s+YpIXpD0Ed4kMW7MoaEIg==", "license": "MIT", "dependencies": { "@svar-ui/lib-dom": "0.11.1", @@ -1425,9 +1425,9 @@ "license": "MIT" }, "node_modules/@svar-ui/react-comments": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@svar-ui/react-comments/-/react-comments-2.4.1.tgz", - "integrity": "sha512-zliH0x74guyS3dk/e+wh00t3tFBTaL+j9wMw34PDqkKBiBAOKRUuNhTT7nNRYSnqb1xr7RnHtqMdYhTYelueAg==", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@svar-ui/react-comments/-/react-comments-2.4.3.tgz", + "integrity": "sha512-hwx3g0LPIBIBbO/YniLvNRaSwxlC/J0Uo2h2BK5N/FszwcBwruYkHBcXI0CEGpJltEaWHYKeO1hF0U5D2SzeBQ==", "license": "MIT", "dependencies": { "@svar-ui/comments-locales": "2.4.0", @@ -1435,8 +1435,8 @@ "@svar-ui/lib-data-provider": "1.7.1", "@svar-ui/lib-dom": "0.11.1", "@svar-ui/lib-state": "1.9.6", - "@svar-ui/react-core": "2.4.1", - "@svar-ui/react-menu": "2.4.1" + "@svar-ui/react-core": "2.4.3", + "@svar-ui/react-menu": "2.4.3" }, "peerDependencies": { "react": ">=18", @@ -1444,9 +1444,9 @@ } }, "node_modules/@svar-ui/react-core": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@svar-ui/react-core/-/react-core-2.4.1.tgz", - "integrity": "sha512-lEB12BuBt3xq+68vWPM4K3CUFaDbvy5ZCyIIRXLBxmeeu8IIlVkSrDvPFQ0fUn05+mB3JzXxQx1SjKd2Ic90Tg==", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@svar-ui/react-core/-/react-core-2.4.3.tgz", + "integrity": "sha512-ufyK48znpYyV+99mb0e44u4aoZN/D/aQxCy58z4xmNWJmQvPFhCu+5QVwnodh9BUbmRp8OHAQjugBAF1eL1doQ==", "license": "MIT", "dependencies": { "@svar-ui/core-locales": "^2.4.0", @@ -1459,18 +1459,18 @@ } }, "node_modules/@svar-ui/react-editor": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@svar-ui/react-editor/-/react-editor-2.4.1.tgz", - "integrity": "sha512-kcxywPxQ821z/8mmAuIJmd1M6mQr/iRdzDD5qEbBqBL1KEASC5a2KCOTfuiKdykYJVgVfqCcQDCd/TyO5NzkYQ==", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@svar-ui/react-editor/-/react-editor-2.4.3.tgz", + "integrity": "sha512-cAd7ASRoezGaMdPXQ7EuuKHYRNOr60OOqPvP2gmn+j2cF0ofRRHpo0G5kTwgQws3MmzAlYP3WOXe0gEMZ81iwg==", "license": "MIT", "dependencies": { "@svar-ui/editor-locales": "2.4.0", "@svar-ui/lib-dom": "0.11.1", "@svar-ui/lib-state": "1.9.6", - "@svar-ui/react-comments": "2.4.1", - "@svar-ui/react-core": "2.4.1", - "@svar-ui/react-tasklist": "2.4.1", - "@svar-ui/react-toolbar": "2.4.1" + "@svar-ui/react-comments": "2.4.3", + "@svar-ui/react-core": "2.4.3", + "@svar-ui/react-tasklist": "2.4.3", + "@svar-ui/react-toolbar": "2.4.3" }, "peerDependencies": { "react": ">=18", @@ -1478,9 +1478,9 @@ } }, "node_modules/@svar-ui/react-filter": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@svar-ui/react-filter/-/react-filter-2.4.1.tgz", - "integrity": "sha512-FEoGwofzkMpQtWVXArmP9DL/s9mLsRLedLVea9BQQ7yjND2fnlLKo9c8wMvlQJ2e7reZ8QQbWskh11v0dx9vtQ==", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@svar-ui/react-filter/-/react-filter-2.4.3.tgz", + "integrity": "sha512-fO9rK0nJEDUf9dQuUKdNOECHDIwuFhSEp27tDDAfSFYOOzOy+ysObs6yMSQ5I8PmCUv/gkIsxu4cYR+Pm4xnJg==", "license": "MIT", "dependencies": { "@svar-ui/filter-locales": "2.4.0", @@ -1488,9 +1488,8 @@ "@svar-ui/lib-dom": "0.11.1", "@svar-ui/lib-react": "1.2.2", "@svar-ui/lib-state": "1.9.6", - "@svar-ui/react-core": "2.4.1", - "@svar-ui/react-grid": "^2.0.0", - "@svar-ui/react-menu": "2.4.1" + "@svar-ui/react-core": "2.4.3", + "@svar-ui/react-menu": "2.4.3" }, "peerDependencies": { "react": ">=18", @@ -1498,21 +1497,21 @@ } }, "node_modules/@svar-ui/react-grid": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@svar-ui/react-grid/-/react-grid-2.4.1.tgz", - "integrity": "sha512-Er89V+wxdaXQryUAYi4lvxY66j84/m1mx5JO5Ui3iXAKaKbsSNY9dHArU4vaXphFKeA017u95M52hUJNmG6URQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@svar-ui/react-grid/-/react-grid-2.5.1.tgz", + "integrity": "sha512-b4xJGnEIAYcksdSzyfLxb2NyNsaNZiSjiuHVeAUBhX6yGtfZarh99oFFIK6zn94HySbh57S4bSy4fzzGrEqChw==", "license": "MIT", "dependencies": { - "@svar-ui/grid-data-provider": "2.4.0", - "@svar-ui/grid-locales": "2.4.0", - "@svar-ui/grid-store": "2.4.0", + "@svar-ui/grid-data-provider": "2.5.0", + "@svar-ui/grid-locales": "2.5.0", + "@svar-ui/grid-store": "2.5.0", "@svar-ui/lib-dom": "0.11.1", "@svar-ui/lib-react": "1.2.2", "@svar-ui/lib-state": "1.9.6", - "@svar-ui/react-core": "2.4.1", - "@svar-ui/react-editor": "2.4.1", - "@svar-ui/react-filter": "2.4.1", - "@svar-ui/react-menu": "2.4.1" + "@svar-ui/react-core": "2.4.3", + "@svar-ui/react-editor": "2.4.3", + "@svar-ui/react-filter": "2.4.3", + "@svar-ui/react-menu": "2.4.3" }, "peerDependencies": { "react": ">=18", @@ -1520,14 +1519,14 @@ } }, "node_modules/@svar-ui/react-menu": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@svar-ui/react-menu/-/react-menu-2.4.1.tgz", - "integrity": "sha512-psW3v0VwoKHVTTkZ5kI+R7WocymCA9C96igr/bX6dWpg/pUVO6/8xQMn0p8vuYxPq02nGYncbmDTIzAOT5SPLA==", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@svar-ui/react-menu/-/react-menu-2.4.3.tgz", + "integrity": "sha512-c3d6CxeJS3/mA6lkQZY4c8AblRk1oRi0xfADktcrxLVKiyzrwn8alEUJ6QqWlGK7TjCHX/IPsWwRK4e32FrjJA==", "license": "MIT", "dependencies": { "@svar-ui/core-locales": "2.4.0", "@svar-ui/lib-dom": "0.11.1", - "@svar-ui/react-core": "2.4.1" + "@svar-ui/react-core": "2.4.3" }, "peerDependencies": { "react": ">=18", @@ -1535,15 +1534,15 @@ } }, "node_modules/@svar-ui/react-tasklist": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@svar-ui/react-tasklist/-/react-tasklist-2.4.1.tgz", - "integrity": "sha512-tyg5Fh18PEH5UTpRDb99BuYpK6A1q1dqQ1yN4uYC4c7tufvE/WqJWqsvVy2rcmAl6y0Xmzd01NPu+/TunFGWKw==", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@svar-ui/react-tasklist/-/react-tasklist-2.4.3.tgz", + "integrity": "sha512-M7BgmfzUZq6XM+/OcRVRh78baa66pDXtV+w/xhLWSHbSOP8LcTZgJQJm3CxjucRSB4SpN9+Yw0dA/E5EZJLT8A==", "license": "MIT", "dependencies": { "@svar-ui/lib-data-provider": "1.7.1", "@svar-ui/lib-dom": "0.11.1", "@svar-ui/lib-state": "1.9.6", - "@svar-ui/react-core": "2.4.1", + "@svar-ui/react-core": "2.4.3", "@svar-ui/tasklist-locales": "2.4.0" }, "peerDependencies": { @@ -1552,14 +1551,14 @@ } }, "node_modules/@svar-ui/react-toolbar": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@svar-ui/react-toolbar/-/react-toolbar-2.4.1.tgz", - "integrity": "sha512-cKFMkmLQ0X8xToG0omni2/y850oL5cxU/6RtPOUv4uC9FFrgrbwJKiuummSxST6nVCrLBfZ+JlKazZH1Ez3Rew==", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@svar-ui/react-toolbar/-/react-toolbar-2.4.3.tgz", + "integrity": "sha512-w9qMxDhmOw2PJRZM9gGLQ7oAhY1ImVfUeGrdG2Mg5D9uo1R6sX74zV3DxaRoRbO79PIqaPgY6ROwQoG00V+/Ow==", "license": "MIT", "dependencies": { "@svar-ui/core-locales": "2.4.0", "@svar-ui/lib-dom": "0.11.1", - "@svar-ui/react-core": "2.4.1" + "@svar-ui/react-core": "2.4.3" }, "peerDependencies": { "react": ">=18", diff --git a/package.json b/package.json index 6a8ec45..eb79546 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@svar-ui/react-gantt", - "version": "2.4.5", + "version": "2.5.2", "description": "A customizable, high-performance React Gantt chart component for building project timelines.", "productTag": "gantt", "productTrial": true, @@ -59,17 +59,17 @@ "react-dom": ">=18" }, "dependencies": { - "@svar-ui/gantt-data-provider": "2.4.4", - "@svar-ui/gantt-locales": "2.4.4", - "@svar-ui/gantt-store": "2.4.4", + "@svar-ui/gantt-data-provider": "2.5.1", + "@svar-ui/gantt-locales": "2.5.1", + "@svar-ui/gantt-store": "2.5.1", "@svar-ui/lib-dom": "0.11.1", "@svar-ui/lib-state": "1.9.6", "@svar-ui/lib-react": "1.2.2", - "@svar-ui/react-core": "2.4.1", - "@svar-ui/react-menu": "2.4.1", - "@svar-ui/react-grid": "2.4.1", - "@svar-ui/react-toolbar": "2.4.1", - "@svar-ui/react-editor": "2.4.1" + "@svar-ui/react-core": "2.4.3", + "@svar-ui/react-menu": "2.4.3", + "@svar-ui/react-grid": "2.5.1", + "@svar-ui/react-toolbar": "2.4.3", + "@svar-ui/react-editor": "2.4.3" }, "homepage": "https://svar.dev/react/gantt/", "repository": { diff --git a/src/components/ContextMenu.jsx b/src/components/ContextMenu.jsx index 6c452b3..ae155ca 100644 --- a/src/components/ContextMenu.jsx +++ b/src/components/ContextMenu.jsx @@ -41,8 +41,18 @@ const ContextMenu = forwardRef(function ContextMenu( const selectedVal = useStoreLater(api, 'selected'); const selectedTasksVal = useStoreLater(api, '_selected'); const splitTasksVal = useStoreLater(api, 'splitTasks'); + const summaryVal = useStoreLater(api, 'summary'); - const fullOptions = useMemo(() => getMenuOptions({ splitTasks: true }), []); + const config = useMemo( + () => ({ + splitTasks: splitTasksVal, + taskTypes: taskTypesVal, + summary: summaryVal, + }), + [splitTasksVal, taskTypesVal, summaryVal], + ); + + const fullOptions = useMemo(() => getMenuOptions(config), [config]); useEffect(() => { if (!api) return; @@ -68,20 +78,14 @@ const ContextMenu = forwardRef(function ContextMenu( function getOptions() { const finalOptions = optionsInit.length ? optionsInit - : getMenuOptions({ splitTasks: splitTasksVal }); - const convertOption = finalOptions.find((o) => o.id === 'convert-task'); - if (convertOption) { - convertOption.data = []; - (taskTypesVal || []).forEach((t) => { - convertOption.data.push(convertOption.dataFactory(t)); - }); - } + : getMenuOptions(config); + return applyLocaleFn(finalOptions); } const cOptions = useMemo(() => { return getOptions(); - }, [api, optionsInit, taskTypesVal, splitTasksVal, _]); + }, [api, optionsInit, config, _]); const selectedTasks = useMemo( () => (selectedTasksVal && selectedTasksVal.length ? selectedTasksVal : []), diff --git a/src/components/Editor.jsx b/src/components/Editor.jsx index 4def3fc..1c6f066 100644 --- a/src/components/Editor.jsx +++ b/src/components/Editor.jsx @@ -95,6 +95,7 @@ function Editor({ const activeTask = useStore(api, '_activeTask'); const taskId = useStore(api, 'activeTask'); const unscheduledTasks = useStore(api, 'unscheduledTasks'); + const summary = useStore(api, 'summary'); const links = useStore(api, 'links'); const splitTasks = useStore(api, 'splitTasks'); const segmentIndex = useMemo( @@ -105,9 +106,10 @@ function Editor({ () => segmentIndex || segmentIndex === 0, [segmentIndex], ); + const taskTypes = useStore(api, 'taskTypes'); const baseItems = useMemo( - () => getEditorItems({ unscheduledTasks }), - [unscheduledTasks], + () => getEditorItems({ unscheduledTasks, summary, taskTypes }), + [unscheduledTasks, summary, taskTypes], ); const undo = useStore(api, 'undo'); @@ -116,8 +118,6 @@ function Editor({ const [editorValues, setEditorValues] = useState(); const [editorErrors, setEditorErrors] = useState(null); - const taskTypes = useStore(api, 'taskTypes'); - const task = useMemo(() => { if (!activeTask) return null; let data; @@ -165,7 +165,7 @@ function Editor({ item.onLinksChange = handleLinksChange; } if (item.comp === 'select' && item.key === 'type') { - const options = item.options ?? (taskTypes ? taskTypes : []); + const options = item.options ?? []; item.options = options.map((t) => ({ ...t, label: _(t.label), @@ -196,7 +196,7 @@ function Editor({ return eItems.filter( (item) => !item.isHidden || !item.isHidden(editorValues, api.getState()), ); - }, [items, baseItems, editorValues, taskTypes, _, api, autoSave]); + }, [items, baseItems, editorValues, _, api, autoSave]); const editorKeys = useMemo( () => editorItems.map((i) => i.key), diff --git a/src/components/Gantt.jsx b/src/components/Gantt.jsx index 55a6595..b39f9c1 100644 --- a/src/components/Gantt.jsx +++ b/src/components/Gantt.jsx @@ -88,6 +88,8 @@ const Gantt = forwardRef(function Gantt( calendar = null, undo = false, splitTasks = false, + summary = null, + _export = false, ...restProps }, ref, @@ -136,12 +138,16 @@ const Gantt = forwardRef(function Gantt( // parse task dates effect const parsedTasksRef = useRef(null); if (parsedTasksRef.current !== tasks) { - parseTaskDates(tasks, { durationUnit, splitTasks, calendar }); + if (!_export) { + parseTaskDates(tasks, { durationUnit, splitTasks, calendar }); + } parsedTasksRef.current = tasks; } useEffect(() => { - parseTaskDates(tasks, { durationUnit, splitTasks, calendar }); - }, [tasks, durationUnit, calendar, splitTasks]); + if (!_export) { + parseTaskDates(tasks, { durationUnit, splitTasks, calendar }); + } + }, [tasks, durationUnit, calendar, splitTasks, _export]); const firstInRoute = useMemo(() => dataStore.in, [dataStore]); @@ -176,7 +182,7 @@ const Gantt = forwardRef(function Gantt( on: firstInRoute.on.bind(firstInRoute), detach: firstInRoute.detach.bind(firstInRoute), getTask: dataStore.getTask.bind(dataStore), - serialize: dataStore.serialize.bind(dataStore), + serialize: () => dataStore.serialize(), getTable: (waitRender) => waitRender ? new Promise((res) => setTimeout(() => res(tableAPIRef.current), 1)) @@ -229,6 +235,7 @@ const Gantt = forwardRef(function Gantt( undo, _weekStart: lCalendar.weekStart, splitTasks, + summary, }); } initOnceRef.current++; @@ -258,6 +265,7 @@ const Gantt = forwardRef(function Gantt( undo, lCalendar, splitTasks, + summary, dataStore, ]); @@ -290,6 +298,7 @@ const Gantt = forwardRef(function Gantt( undo, _weekStart: lCalendar.weekStart, splitTasks, + summary, }); } diff --git a/src/components/chart/Bars.jsx b/src/components/chart/Bars.jsx index 5ed3ecf..6a6980c 100644 --- a/src/components/chart/Bars.jsx +++ b/src/components/chart/Bars.jsx @@ -34,6 +34,7 @@ function Bars(props) { const tree = useStore(api, 'tasks'); const schedule = useStore(api, 'schedule'); const splitTasks = useStore(api, 'splitTasks'); + const summary = useStore(api, 'summary'); const tasks = useMemo(() => { if (!areaValue || !Array.isArray(rTasksValue)) return []; @@ -53,7 +54,15 @@ function Bars(props) { const [taskMove, setTaskMove] = useState(null); const progressFromRef = useRef(null); - const [selectedLink, setSelectedLink] = useState(null); + const [selectedLinkId, setSelectedLinkId] = useState(null); + + const selectedLink = useMemo(() => { + return ( + selectedLinkId && { + ...rLinksValue.find((link) => link.id === selectedLinkId), + } + ); + }, [selectedLinkId, rLinksCounter]); const [touched, setTouched] = useState(undefined); const touchTimerRef = useRef(null); @@ -200,12 +209,9 @@ function Bars(props) { [down], ); - const onSelectLink = useCallback( - (id) => { - setSelectedLink(id && { ...rLinksValue.find((link) => link.id === id) }); - }, - [rLinksValue], - ); + const onSelectLink = useCallback((id) => { + setSelectedLinkId(id); + }, []); const up = useCallback(() => { if (progressFromRef.current) { @@ -285,10 +291,11 @@ function Bars(props) { const { mode, l, w, x, id, start, segment, index } = taskMove; const task = api.getTask(id); const dx = clientX - x; + const minWidth = Math.round(lengthUnitWidth) || 1; if ( (!start && Math.abs(dx) < 20) || - (mode === 'start' && w - dx < lengthUnitWidth) || - (mode === 'end' && w + dx < lengthUnitWidth) || + (mode === "start" && w - dx < minWidth) || + (mode === "end" && w + dx < minWidth) || (mode === 'move' && ((dx < 0 && l + dx < 0) || (dx > 0 && l + w + dx > totalWidth))) || @@ -315,6 +322,7 @@ function Bars(props) { width: width, left: left, inProgress: true, + start, ...(segment && { segmentIndex: index }), }); @@ -465,8 +473,8 @@ function Bars(props) { }); } } else if (css.contains('wx-delete-button-icon')) { - api.exec('delete-link', { id: selectedLink.id }); - setSelectedLink(null); + api.exec('delete-link', { id: selectedLinkId }); + setSelectedLinkId(null); } else { let segmentIndex; const segmentNode = locate(e, 'data-segment'); @@ -608,7 +616,7 @@ function Bars(props) { 'wx-link wx-left' + (linkFrom ? ' wx-visible' : '') + (!linkFrom || - (!alreadyLinked(task.id, true) && isLinkMarkerVisible(task.id)) + (!alreadyLinked(task.id, true) && isLinkMarkerVisible(task.id)) ? ' wx-target' : '') + (linkFrom && linkFrom.id === task.id && linkFrom.start @@ -619,7 +627,7 @@ function Bars(props) { 'wx-link wx-right' + (linkFrom ? ' wx-visible' : '') + (!linkFrom || - (!alreadyLinked(task.id, false) && isLinkMarkerVisible(task.id)) + (!alreadyLinked(task.id, false) && isLinkMarkerVisible(task.id)) ? ' wx-target' : '') + (linkFrom && linkFrom.id === task.id && !linkFrom.start @@ -638,7 +646,7 @@ function Bars(props) { > {!readonly ? ( task.id === selectedLink?.target && - selectedLink?.type[2] === 's' ? ( + selectedLink?.type[2] === 's' ? (