From bb81e7a33644f2d9f8f0771c5da4436ea9410efd Mon Sep 17 00:00:00 2001 From: Marta Kowalska Date: Mon, 22 Dec 2025 20:22:30 +0000 Subject: [PATCH] v2.4.3 --- .gitignore | 3 +- .sync | 2 +- demos/cases/ContextMenuOptions.jsx | 6 +- demos/cases/DropDownMenu.jsx | 2 +- demos/cases/GanttBackend.jsx | 39 +- demos/cases/GanttDurationUnitChanges.jsx | 10 +- demos/cases/GanttDurationUnitHour.jsx | 4 +- demos/cases/GanttEditorComments.jsx | 9 +- demos/cases/GanttEditorConfig.jsx | 24 +- demos/cases/GanttEditorCustomControls.jsx | 4 +- demos/cases/GanttEditorReadonly.jsx | 2 +- demos/cases/GanttEditorTasks.jsx | 9 +- demos/cases/GanttEditorValidation.jsx | 4 +- demos/cases/GanttFullscreen.jsx | 9 +- demos/cases/GanttHolidays.jsx | 8 +- demos/cases/GanttLengthUnit.jsx | 22 +- demos/cases/GanttLocale.jsx | 23 +- demos/cases/GanttMinScaleUnit.jsx | 24 +- demos/cases/GanttPerformance.jsx | 58 +- demos/cases/GanttPreventActions.css | 12 +- demos/cases/GanttPreventActions.jsx | 84 +- demos/cases/GanttScaleUnit.jsx | 8 +- demos/cases/GanttScales.jsx | 4 +- demos/cases/GanttStartEnd.jsx | 38 +- demos/cases/GanttToolbarButtons.jsx | 4 +- demos/cases/GanttToolbarCustom.jsx | 18 +- demos/cases/ProAutoSchedule.css | 23 + demos/cases/ProAutoSchedule.jsx | 42 + demos/cases/ProBaselines.jsx | 54 ++ demos/cases/ProCalendar.jsx | 43 + demos/cases/ProCalendarChanges.css | 20 + demos/cases/ProCalendarChanges.jsx | 72 ++ demos/cases/ProCriticalPath.css | 12 + demos/cases/ProCriticalPath.jsx | 62 ++ demos/cases/ProMarkers.css | 10 + demos/cases/ProMarkers.jsx | 42 + demos/cases/ProScheduleAll.css | 23 + demos/cases/ProScheduleAll.jsx | 160 ++++ demos/cases/ProSplitTasks.css | 4 + demos/cases/ProSplitTasks.jsx | 28 + demos/cases/ProSummariesConvert.jsx | 109 +++ demos/cases/ProSummariesProgress.css | 9 + demos/cases/ProSummariesProgress.jsx | 131 +++ demos/cases/ProUndo.css | 35 + demos/cases/ProUndo.jsx | 79 ++ demos/cases/ProUndoToolbar.css | 4 + demos/cases/ProUndoToolbar.jsx | 27 + demos/cases/ProUnscheduledTasks.jsx | 25 + .../cases/ProUnscheduledTasksAndBaselines.jsx | 89 ++ demos/common/Index.css | 31 +- demos/common/Index.jsx | 57 +- demos/common/Router.jsx | 4 +- demos/custom/Form.jsx | 70 +- demos/data.js | 767 +++++++++++++++--- demos/routes.js | 42 +- eslint.config.js | 5 +- license.txt | 595 +------------- package-lock.json | 267 +++--- package.json | 31 +- readme.md | 57 +- src/components/ContextMenu.jsx | 83 +- src/components/Editor.css | 2 +- src/components/Editor.jsx | 330 +++++--- src/components/Gantt.jsx | 156 +++- src/components/Layout.css | 1 + src/components/Layout.jsx | 65 +- src/components/Resizer.jsx | 121 ++- src/components/Toolbar.jsx | 68 +- src/components/chart/BarSegments.css | 22 + src/components/chart/BarSegments.jsx | 60 ++ src/components/chart/Bars.css | 101 ++- src/components/chart/Bars.jsx | 358 +++++--- src/components/chart/CellGrid.jsx | 14 +- src/components/chart/Chart.css | 2 - src/components/chart/Chart.jsx | 66 +- src/components/chart/Links.css | 21 +- src/components/chart/Links.jsx | 61 +- src/components/chart/TimeScale.css | 30 + src/components/chart/TimeScale.jsx | 42 + src/components/editor/Links.css | 4 + src/components/editor/Links.jsx | 39 +- src/components/grid/Grid.jsx | 204 +++-- src/helpers/debug.js | 40 + src/helpers/prepareConfig.js | 73 ++ src/index.js | 5 +- src/themes/Material.css | 18 +- src/themes/Willow.css | 9 +- src/themes/WillowDark.css | 9 +- vite.config.js | 5 +- whatsnew.md | 36 + 90 files changed, 3676 insertions(+), 1828 deletions(-) create mode 100644 demos/cases/ProAutoSchedule.css create mode 100644 demos/cases/ProAutoSchedule.jsx create mode 100644 demos/cases/ProBaselines.jsx create mode 100644 demos/cases/ProCalendar.jsx create mode 100644 demos/cases/ProCalendarChanges.css create mode 100644 demos/cases/ProCalendarChanges.jsx create mode 100644 demos/cases/ProCriticalPath.css create mode 100644 demos/cases/ProCriticalPath.jsx create mode 100644 demos/cases/ProMarkers.css create mode 100644 demos/cases/ProMarkers.jsx create mode 100644 demos/cases/ProScheduleAll.css create mode 100644 demos/cases/ProScheduleAll.jsx create mode 100644 demos/cases/ProSplitTasks.css create mode 100644 demos/cases/ProSplitTasks.jsx create mode 100644 demos/cases/ProSummariesConvert.jsx create mode 100644 demos/cases/ProSummariesProgress.css create mode 100644 demos/cases/ProSummariesProgress.jsx create mode 100644 demos/cases/ProUndo.css create mode 100644 demos/cases/ProUndo.jsx create mode 100644 demos/cases/ProUndoToolbar.css create mode 100644 demos/cases/ProUndoToolbar.jsx create mode 100644 demos/cases/ProUnscheduledTasks.jsx create mode 100644 demos/cases/ProUnscheduledTasksAndBaselines.jsx create mode 100644 src/components/chart/BarSegments.css create mode 100644 src/components/chart/BarSegments.jsx create mode 100644 src/components/chart/TimeScale.css create mode 100644 src/components/chart/TimeScale.jsx create mode 100644 src/helpers/debug.js create mode 100644 src/helpers/prepareConfig.js diff --git a/.gitignore b/.gitignore index 22e5151..2fd4e34 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* -yarn.lock node_modules dist @@ -25,3 +24,5 @@ dist-ssr *.njsproj *.sln *.sw? +yarn.log +.claude diff --git a/.sync b/.sync index 2a6c28b..9a9dc47 100644 --- a/.sync +++ b/.sync @@ -1 +1 @@ -0d06fa37fa91f51a38b15b4dac095537d366a653 +913b95b49f0d253d50d6b3461c48aaa9facb7ba2 diff --git a/demos/cases/ContextMenuOptions.jsx b/demos/cases/ContextMenuOptions.jsx index bcaa3a5..acaf94d 100644 --- a/demos/cases/ContextMenuOptions.jsx +++ b/demos/cases/ContextMenuOptions.jsx @@ -1,6 +1,6 @@ import { useState, useContext, useMemo } from 'react'; import { getData } from '../data'; -import { Gantt, ContextMenu, Editor, defaultMenuOptions } from '../../src/'; +import { Gantt, ContextMenu, Editor, getMenuOptions } from '../../src/'; import { context } from '@svar-ui/react-core'; export default function ContextMenuOptions({ skinSettings }) { @@ -17,9 +17,7 @@ export default function ContextMenuOptions({ skinSettings }) { const [options] = useState(() => { const ids = ['cut-task', 'copy-task', 'paste-task', 'delete-task']; let arr = [{ id: 'add-task:after', text: ' Add below', icon: 'wxi-plus' }]; - arr = arr.concat( - defaultMenuOptions.filter((op) => ids.indexOf(op.id) >= 0), - ); + arr = arr.concat(getMenuOptions().filter((op) => ids.indexOf(op.id) >= 0)); arr.push({ id: 'my-action', text: 'My action', diff --git a/demos/cases/DropDownMenu.jsx b/demos/cases/DropDownMenu.jsx index 95a8294..3460ab6 100644 --- a/demos/cases/DropDownMenu.jsx +++ b/demos/cases/DropDownMenu.jsx @@ -44,7 +44,7 @@ export default function DropDownMenu({ skinSettings }) { links={data.links} scales={data.scales} /> - + {api && } diff --git a/demos/cases/GanttBackend.jsx b/demos/cases/GanttBackend.jsx index 66e291a..e3f251d 100644 --- a/demos/cases/GanttBackend.jsx +++ b/demos/cases/GanttBackend.jsx @@ -9,7 +9,6 @@ const parseDates = (data) => { return data; }; - export default function GanttBackend() { const server = 'https://master--svar-gantt-go--dev.webix.io'; @@ -29,30 +28,26 @@ export default function GanttBackend() { }); }, []); - const init = useCallback( - (api) => { - setApi(api); + const init = useCallback((api) => { + setApi(api); - api.on('request-data', (ev) => { - Promise.all([ - fetch(server + `/tasks/${ev.id}`) - .then((res) => res.json()) - .then((arr) => parseDates(arr)), - fetch(server + `/links/${ev.id}`).then((res) => res.json()), - ]).then(([tasks, links]) => { - api.exec('provide-data', { - id: ev.id, - data: { - tasks, - links, - }, - }); + api.on('request-data', (ev) => { + Promise.all([ + fetch(server + `/tasks/${ev.id}`) + .then((res) => res.json()) + .then((arr) => parseDates(arr)), + fetch(server + `/links/${ev.id}`).then((res) => res.json()), + ]).then(([tasks, links]) => { + api.exec('provide-data', { + id: ev.id, + data: { + tasks, + links, + }, }); }); - }, - [], - ); - + }); + }, []); return ( <> diff --git a/demos/cases/GanttDurationUnitChanges.jsx b/demos/cases/GanttDurationUnitChanges.jsx index 9483534..0836acb 100644 --- a/demos/cases/GanttDurationUnitChanges.jsx +++ b/demos/cases/GanttDurationUnitChanges.jsx @@ -1,6 +1,6 @@ import { useMemo, useState } from 'react'; import { getData } from '../data'; -import { Gantt, ContextMenu, Editor, defaultEditorItems } from '../../src'; +import { Gantt, ContextMenu, Editor, getEditorItems } from '../../src'; import { RadioButtonGroup } from '@svar-ui/react-core'; import './GanttDurationUnitChanges.css'; @@ -18,18 +18,18 @@ export default function GanttDurationUnitChanges({ skinSettings }) { ); const options = [ - { id: 'hour', label: 'Hour' }, { id: 'day', label: 'Day' }, + { id: 'hour', label: 'Hour' }, ]; - const [durationUnit, setDurationUnit] = useState('hour'); - const [scales, setScales] = useState(scalesMap['hour']); + const [durationUnit, setDurationUnit] = useState('day'); + const [scales, setScales] = useState(scalesMap['day']); const [api, setApi] = useState(null); const items = useMemo( () => - defaultEditorItems.map((ed) => ({ + getEditorItems().map((ed) => ({ ...ed, ...(ed.comp === 'date' && { config: { time: durationUnit === 'hour' }, diff --git a/demos/cases/GanttDurationUnitHour.jsx b/demos/cases/GanttDurationUnitHour.jsx index 0ad3aba..bba358c 100644 --- a/demos/cases/GanttDurationUnitHour.jsx +++ b/demos/cases/GanttDurationUnitHour.jsx @@ -3,7 +3,7 @@ import { Gantt, ContextMenu, Editor, - defaultEditorItems, + getEditorItems, defaultColumns, } from '../../src'; import { format } from 'date-fns'; @@ -16,7 +16,7 @@ export default function GanttDurationUnitHour({ skinSettings }) { const items = useMemo( () => - defaultEditorItems.map((ed) => ({ + getEditorItems().map((ed) => ({ ...ed, ...(ed.comp === 'date' && { config: { time: true } }), })), diff --git a/demos/cases/GanttEditorComments.jsx b/demos/cases/GanttEditorComments.jsx index d2fcb11..f67f30e 100644 --- a/demos/cases/GanttEditorComments.jsx +++ b/demos/cases/GanttEditorComments.jsx @@ -1,11 +1,6 @@ import { useState, useMemo } from 'react'; import { getData } from '../data'; -import { - Gantt, - Editor, - defaultEditorItems, - registerEditorItem, -} from '../../src'; +import { Gantt, Editor, getEditorItems, registerEditorItem } from '../../src'; import { Comments } from '@svar-ui/react-comments'; registerEditorItem('comments', Comments); @@ -47,7 +42,7 @@ function GanttEditorComments(props) { const [api, setApi] = useState(); const keys = useMemo(() => ['text', 'details'], []); const items = useMemo(() => { - const items = defaultEditorItems.filter((op) => keys.indexOf(op.key) >= 0); + const items = getEditorItems().filter((op) => keys.indexOf(op.key) >= 0); items.push({ key: 'comments', comp: 'comments', diff --git a/demos/cases/GanttEditorConfig.jsx b/demos/cases/GanttEditorConfig.jsx index 946d70a..948c615 100644 --- a/demos/cases/GanttEditorConfig.jsx +++ b/demos/cases/GanttEditorConfig.jsx @@ -1,6 +1,6 @@ import { useState, useMemo } from 'react'; import { getData } from '../data'; -import { Gantt, Editor, defaultEditorItems } from '../../src'; +import { Gantt, Editor, getEditorItems } from '../../src'; function GanttEditorConfig({ skinSettings }) { const data = useMemo(() => getData(), []); @@ -24,12 +24,14 @@ function GanttEditorConfig({ skinSettings }) { [], ); + const defaultEditorItems = useMemo(() => getEditorItems(), []); + const items = useMemo( () => keys.map((key) => ({ ...defaultEditorItems.find((op) => op.key === key), })), - [keys], + [keys, defaultEditorItems], ); return ( @@ -41,14 +43,16 @@ function GanttEditorConfig({ skinSettings }) { links={data.links} scales={data.scales} /> - {api && } + {api && ( + + )} ); } diff --git a/demos/cases/GanttEditorCustomControls.jsx b/demos/cases/GanttEditorCustomControls.jsx index 0cb82da..95d3379 100644 --- a/demos/cases/GanttEditorCustomControls.jsx +++ b/demos/cases/GanttEditorCustomControls.jsx @@ -2,9 +2,9 @@ import { useEffect, useMemo, useState } from 'react'; import { Gantt, Editor, - defaultEditorItems, registerEditorItem, defaultTaskTypes, + getEditorItems, } from '../../src'; import { RadioButtonGroup } from '@svar-ui/react-core'; import UsersCustomCombo from '../custom/UsersCustomCombo.jsx'; @@ -18,6 +18,8 @@ export default function GanttEditorCustomControls({ skinSettings }) { }, []); const items = useMemo(() => { + const defaultEditorItems = getEditorItems(); + const items = defaultEditorItems.map((item) => ({ ...item })); items.splice( defaultEditorItems.findIndex((d) => d.key === 'type'), diff --git a/demos/cases/GanttEditorReadonly.jsx b/demos/cases/GanttEditorReadonly.jsx index 37c9dad..b46fba6 100644 --- a/demos/cases/GanttEditorReadonly.jsx +++ b/demos/cases/GanttEditorReadonly.jsx @@ -2,7 +2,7 @@ import { useMemo, useState } from 'react'; import { getData } from '../data'; import { Gantt, Editor } from '../../src'; -function GanttEditorReadonly({...skinSettings}) { +function GanttEditorReadonly({ ...skinSettings }) { const data = useMemo(() => getData(), []); const [api, setApi] = useState(); diff --git a/demos/cases/GanttEditorTasks.jsx b/demos/cases/GanttEditorTasks.jsx index e591a48..89e95dd 100644 --- a/demos/cases/GanttEditorTasks.jsx +++ b/demos/cases/GanttEditorTasks.jsx @@ -1,11 +1,6 @@ import { useState, useMemo } from 'react'; import { getData } from '../data'; -import { - Gantt, - Editor, - defaultEditorItems, - registerEditorItem, -} from '../../src'; +import { Gantt, Editor, getEditorItems, registerEditorItem } from '../../src'; import { Tasklist } from '@svar-ui/react-tasklist'; registerEditorItem('tasks', Tasklist); @@ -71,7 +66,7 @@ export default function GanttEditorTasks({ skinSettings }) { const items = useMemo(() => { const keys = ['text', 'details']; - const items = defaultEditorItems.filter((op) => keys.indexOf(op.key) >= 0); + const items = getEditorItems().filter((op) => keys.indexOf(op.key) >= 0); items.push({ key: 'tasks', comp: 'tasks', diff --git a/demos/cases/GanttEditorValidation.jsx b/demos/cases/GanttEditorValidation.jsx index ee56b32..fa0c0ee 100644 --- a/demos/cases/GanttEditorValidation.jsx +++ b/demos/cases/GanttEditorValidation.jsx @@ -1,6 +1,6 @@ import { useMemo, useState } from 'react'; import { getData } from '../data'; -import { Gantt, Editor, defaultEditorItems } from '../../src'; +import { Gantt, Editor, getEditorItems } from '../../src'; function GanttEditorValidation({ skinSettings }) { const data = useMemo(() => getData(), []); @@ -8,7 +8,7 @@ function GanttEditorValidation({ skinSettings }) { const items = useMemo( () => - defaultEditorItems.map((ed) => ({ + getEditorItems().map((ed) => ({ ...ed, ...(ed.comp === 'text' && { required: true }), ...(ed.comp === 'counter' && { diff --git a/demos/cases/GanttFullscreen.jsx b/demos/cases/GanttFullscreen.jsx index 5cc196e..bcccaf1 100644 --- a/demos/cases/GanttFullscreen.jsx +++ b/demos/cases/GanttFullscreen.jsx @@ -1,6 +1,7 @@ import { useMemo } from 'react'; import { getData } from '../data'; -import { Gantt, Fullscreen } from '../../src/'; +import { Gantt } from '../../src/'; +import { Fullscreen } from '@svar-ui/react-core'; import './GanttFullscreen.css'; function GanttFullscreen({ skinSettings }) { @@ -11,11 +12,7 @@ function GanttFullscreen({ skinSettings }) {

Click the "expand" icon, or click on Gantt and press Ctrl+Shift+F

- +
diff --git a/demos/cases/GanttHolidays.jsx b/demos/cases/GanttHolidays.jsx index f4b5024..b3583cb 100644 --- a/demos/cases/GanttHolidays.jsx +++ b/demos/cases/GanttHolidays.jsx @@ -7,10 +7,10 @@ function GanttHolidays({ skinSettings }) { const scales = useMemo( () => [ - { unit: 'year', step: 1, format: 'yyyy' }, - { unit: 'month', step: 2, format: 'MMMM yyy' }, - { unit: 'week', step: 1, format: 'wo' }, - { unit: 'day', step: 1, format: 'd, EEEE' /* , css: dayStyle */ } + { unit: 'year', step: 1, format: '%Y' }, + { unit: 'month', step: 2, format: '%F %Y' }, + { unit: 'week', step: 1, format: 'Week %W' }, + { unit: 'day', step: 1, format: '%j, %l' }, ], [], ); diff --git a/demos/cases/GanttLengthUnit.jsx b/demos/cases/GanttLengthUnit.jsx index 380b73d..fa4b5e7 100644 --- a/demos/cases/GanttLengthUnit.jsx +++ b/demos/cases/GanttLengthUnit.jsx @@ -23,36 +23,36 @@ function GanttLengthUnit({ skinSettings }) { switch (lengthUnit) { case 'minute': scales = [ - { unit: 'day', step: 1, format: 'MMM d' }, - { unit: 'hour', step: 1, format: 'HH:mm' }, + { unit: 'day', step: 1, format: '%M %j' }, + { unit: 'hour', step: 1, format: '%H:%i' }, ]; break; case 'hour': scales = [ - { unit: 'month', step: 1, format: 'MMM' }, - { unit: 'day', step: 1, format: 'MMM d' }, + { unit: 'month', step: 1, format: '%M' }, + { unit: 'day', step: 1, format: '%M %j' }, ]; break; case 'day': scales = [ - { unit: 'month', step: 1, format: 'MMM' }, - { unit: 'week', step: 1, format: 'w' }, + { unit: 'month', step: 1, format: '%M' }, + { unit: 'week', step: 1, format: '%w' }, ]; break; case 'week': scales = [ - { unit: 'year', step: 1, format: 'yyyy' }, - { unit: 'month', step: 1, format: 'MMM' }, + { unit: 'year', step: 1, format: '%Y' }, + { unit: 'month', step: 1, format: '%M' }, ]; break; case 'month': scales = [ - { unit: 'year', step: 1, format: 'yyyy' }, - { unit: 'quarter', step: 1, format: 'QQQ' }, + { unit: 'year', step: 1, format: '%Y' }, + { unit: 'quarter', step: 1, format: '%Q' }, ]; break; case 'quarter': - scales = [{ unit: 'year', step: 1, format: 'yyyy' }]; + scales = [{ unit: 'year', step: 1, format: '%Y' }]; break; default: scales = bigScales; diff --git a/demos/cases/GanttLocale.jsx b/demos/cases/GanttLocale.jsx index bb4b2c0..4db0db9 100644 --- a/demos/cases/GanttLocale.jsx +++ b/demos/cases/GanttLocale.jsx @@ -22,11 +22,7 @@ function GanttLocale({ skinSettings }) { onChange={({ value }) => setLang(value)} /> - {lang === 'en' && ( - - )} + {lang === 'en' && } {lang === 'cn' && ( @@ -42,19 +38,20 @@ function GanttWidget(props) { const [api, setApi] = useState(null); const data = useMemo(() => getData(), []); - + const settings = { + ...skinSettings, + tasks: data.tasks, + links: data.links, + scales: data.scales, + zoom: true, + }; + return ( <>
- + {api && }
diff --git a/demos/cases/GanttMinScaleUnit.jsx b/demos/cases/GanttMinScaleUnit.jsx index 46f2ffb..fdefbc2 100644 --- a/demos/cases/GanttMinScaleUnit.jsx +++ b/demos/cases/GanttMinScaleUnit.jsx @@ -8,17 +8,17 @@ import { isSameMonth, addMonths, addDays, - format, differenceInDays, + format, } from 'date-fns'; import './GanttMinScaleUnit.css'; const options = [ - { id: 1, label: 'sprint' }, - { id: 2, label: 'month, sprint' }, - { id: 3, label: 'month, sprint, week' }, - { id: 4, label: 'month, sprint, week, day' }, - ]; + { id: 1, label: 'sprint' }, + { id: 2, label: 'month, sprint' }, + { id: 3, label: 'month, sprint, week' }, + { id: 4, label: 'month, sprint, week, day' }, +]; export default function GanttMinScaleUnit({ skinSettings }) { const data = useMemo(() => getData(), []); @@ -51,10 +51,10 @@ export default function GanttMinScaleUnit({ skinSettings }) { const allScales = useMemo( () => [ - { unit: 'month', step: 1, format: 'MMMM yyy' }, + { unit: 'month', step: 1, format: '%F %Y' }, { unit: 'sprint', step: 1, format: sprintFormat }, - { unit: 'week', step: 1, format: 'w' }, - { unit: 'day', step: 1, format: 'd' }, + { unit: 'week', step: 1, format: '%w' }, + { unit: 'day', step: 1, format: '%j' }, ], [], ); @@ -67,7 +67,7 @@ export default function GanttMinScaleUnit({ skinSettings }) { if (scaleOption == 3) return allScales.slice(0, 3); return allScales; }, [scaleOption, allScales]); - + const registeredRef = useRef(false); if (!registeredRef.current) { registerScaleUnit('sprint', { @@ -128,8 +128,8 @@ export default function GanttMinScaleUnit({ skinSettings }) { links={data.links} scales={scales} zoom={true} - start={new Date(2024, 3, 1)} - end={new Date(2024, 5, 1)} + start={new Date(2026, 3, 1)} + end={new Date(2026, 5, 1)} cellWidth={60} /> diff --git a/demos/cases/GanttPerformance.jsx b/demos/cases/GanttPerformance.jsx index 26aa938..6961ffb 100644 --- a/demos/cases/GanttPerformance.jsx +++ b/demos/cases/GanttPerformance.jsx @@ -4,33 +4,59 @@ import { Gantt } from '../../src/'; import { Button } from '@svar-ui/react-core'; import './GanttPerformance.css'; +function RenderTime({ start }){ + const [end, setEnd] = useState(null); + const [label, setLabel] = useState('') + + let active = useRef(true); + useEffect(() => { + if (start && end) { + setLabel((end - start)+" ms"); + } + + active.current = true; + setTimeout(() => { + active.current = false; + }, 1000); + }, [start, end]); + + window.__RENDER_METRICS_ENABLED__ = true; + useEffect(() => { + window.addEventListener('render-metric', (e) => { + if (!active.current) return; + if (e.detail.label === "chart") { + setEnd(new Date()); + } + }); + + return () => { + delete window.__RENDER_METRICS_ENABLED__ + }; + }, []); + + + return {label} +} + +const count = 10000; +const years = 3; +const data = getGeneratedData('', count, years); + function GanttPerformance(props) { const { skinSettings } = props; - - const count = 1000; - const years = 3; - const data = useMemo(() => getGeneratedData('', count, years), []); - const [start, setStart] = useState(null); - const outAreaRef = useRef(null); - - useEffect(() => { - if (start && outAreaRef.current) { - outAreaRef.current.innerHTML = new Date() - start; - } - }, [start]); return (
{start ? ( <> - 1 000 tasks ({years} years ) rendered in{' '} - ms + 10 000 tasks ({years} years ) rendered in{' '} + ) : ( - )}
diff --git a/demos/cases/GanttPreventActions.css b/demos/cases/GanttPreventActions.css index 71694a9..374df5f 100644 --- a/demos/cases/GanttPreventActions.css +++ b/demos/cases/GanttPreventActions.css @@ -12,7 +12,7 @@ align-items: end; font-family: var(--wx-font-family); font-size: var(--wx-font-size); - padding-top: 12px; + padding: 12px 0; --wx-label-width: 130px; } @@ -33,6 +33,16 @@ .gantt.wx-RPSbwjNq.hide-links > .wx-gantt .wx-bar .wx-link { display: none; } +.gantt.wx-RPSbwjNq.hide-delete-links > .wx-gantt .wx-delete-link { + display: none; +} +.gantt.wx-RPSbwjNq.hide-delete-links > .wx-gantt .wx-line:hover { + cursor: default; + stroke: var(--wx-gantt-link-color); +} +.gantt.wx-RPSbwjNq.hide-delete-links > .wx-gantt .wx-line.wx-line-selectable { + cursor: default; +} .gantt.wx-RPSbwjNq.hide-drag > .wx-gantt .wx-bar { cursor: pointer !important; } diff --git a/demos/cases/GanttPreventActions.jsx b/demos/cases/GanttPreventActions.jsx index 3e3d9f3..f29f4c3 100644 --- a/demos/cases/GanttPreventActions.jsx +++ b/demos/cases/GanttPreventActions.jsx @@ -1,28 +1,22 @@ -import { useState, useEffect, useMemo, useRef, useCallback } from 'react'; +import { useState, useMemo, useRef, useEffect, useCallback } from 'react'; import { getData } from '../data'; -import { Gantt, defaultColumns, Editor } from '../../src/'; +import { Gantt, defaultColumns } from '../../src/'; import { Field, Switch } from '@svar-ui/react-core'; import './GanttPreventActions.css'; function GanttPreventActions({ skinSettings }) { const data = useMemo(() => getData(), []); - const [edit, setEdit] = useState(true); // if false - cannot add and edit task + const [add, setAdd] = useState(true); // if false - cannot add and edit task const [drag, setDrag] = useState(true); // if false - cannot drag tasks on scale const [order, setOrder] = useState(true); // if false - cannot reorder tasks in grid const [newLink, setNewLink] = useState(true); // if false - cannot create new links + const [deleteLink, setDeleteLink] = useState(true); // if false - cannot delete links + const [progress, setProgress] = useState(true); // if false - cannot edit progress in chart - const ignoreRef = useRef(false); - const [api, setApi] = useState(); - - const editRef = useRef(edit); const dragRef = useRef(drag); const orderRef = useRef(order); - useEffect(() => { - editRef.current = edit; - }, [edit]); - useEffect(() => { dragRef.current = drag; }, [drag]); @@ -32,9 +26,6 @@ function GanttPreventActions({ skinSettings }) { }, [order]); const init = useCallback((gApi) => { - setApi(gApi); - - gApi.intercept('show-editor', () => editRef.current || ignoreRef.current); gApi.intercept('drag-task', (ev) => { if (typeof ev.top !== 'undefined') return orderRef.current; return dragRef.current; // ev.width && ev.left @@ -43,66 +34,46 @@ function GanttPreventActions({ skinSettings }) { const columns = useMemo( () => - edit ? defaultColumns : defaultColumns.filter((a) => a.id != 'add-task'), - [edit], + add ? defaultColumns : defaultColumns.filter((a) => a.id != 'add-task'), + [add], ); - // for demo purposes: close editor when checkbox is unchecked - useEffect(() => { - if (!edit) { - ignoreRef.current = true; - api.exec('show-editor', { id: null }); - ignoreRef.current = false; - } - }, [edit, api]); - return (
- - {({ id }) => ( - setEdit(value)} - id={id} - /> - )} + + setAdd(value)} /> - {({ id }) => ( - setNewLink(value)} - id={id} - /> - )} + setNewLink(value)} /> + + + setDeleteLink(value)} + /> - {({ id }) => ( - setDrag(value)} - id={id} - /> - )} + setDrag(value)} /> - {({ id }) => ( - setOrder(value)} - id={id} - /> - )} + setOrder(value)} /> + + + setProgress(value)} + />
- {api && }
); diff --git a/demos/cases/GanttScaleUnit.jsx b/demos/cases/GanttScaleUnit.jsx index eb133dd..f33c489 100644 --- a/demos/cases/GanttScaleUnit.jsx +++ b/demos/cases/GanttScaleUnit.jsx @@ -74,13 +74,13 @@ export default function GanttScaleUnit(props) { tasks={data.tasks} links={data.links} scales={[ - { unit: 'month', step: 1, format: 'MMMM yyy' }, + { unit: 'month', step: 1, format: '%F %Y' }, { unit: 'sprint', step: 1, format: sprintFormat }, - { unit: 'day', step: 1, format: 'd' }, + { unit: 'day', step: 1, format: '%j' }, ]} zoom={true} - start={new Date(2024, 3, 1)} - end={new Date(2024, 5, 1)} + start={new Date(2026, 3, 1)} + end={new Date(2026, 5, 1)} cellWidth={60} /> ); diff --git a/demos/cases/GanttScales.jsx b/demos/cases/GanttScales.jsx index 212edb0..9ebe944 100644 --- a/demos/cases/GanttScales.jsx +++ b/demos/cases/GanttScales.jsx @@ -11,8 +11,8 @@ function GanttScales({ skinSettings }) { tasks={data.tasks} links={data.links} scales={complexScales} - start={new Date(2024, 3, 1)} - end={new Date(2024, 4, 12)} + start={new Date(2026, 3, 1)} + end={new Date(2026, 4, 12)} cellWidth={60} /> ); diff --git a/demos/cases/GanttStartEnd.jsx b/demos/cases/GanttStartEnd.jsx index 9eeac5c..a0b51ef 100644 --- a/demos/cases/GanttStartEnd.jsx +++ b/demos/cases/GanttStartEnd.jsx @@ -6,8 +6,8 @@ import './GanttStartEnd.css'; export default function GanttStartEnd({ skinSettings }) { const data = useMemo(() => getData(), []); - const [start, setStart] = useState(new Date(2024, 3, 5)); - const [end, setEnd] = useState(new Date(2024, 4, 1)); + const [start, setStart] = useState(new Date(2026, 3, 5)); + const [end, setEnd] = useState(new Date(2026, 4, 1)); const [autoScale, setAutoScale] = useState(false); return ( @@ -15,33 +15,21 @@ export default function GanttStartEnd({ skinSettings }) {
- {({ id }) => ( - setStart(value)} - /> - )} + setStart(value)} + /> - {({ id }) => ( - setEnd(value)} - /> - )} + setEnd(value)} /> - {({ id }) => ( -
- setAutoScale(value)} - /> -
- )} +
+ setAutoScale(value)} + /> +
diff --git a/demos/cases/GanttToolbarButtons.jsx b/demos/cases/GanttToolbarButtons.jsx index e7e9551..753794c 100644 --- a/demos/cases/GanttToolbarButtons.jsx +++ b/demos/cases/GanttToolbarButtons.jsx @@ -1,7 +1,7 @@ import { useState, useMemo, useCallback, useContext } from 'react'; import { context } from '@svar-ui/react-core'; import { getData } from '../data'; -import { Gantt, Toolbar, Editor, defaultToolbarButtons } from '../../src/'; +import { Gantt, Toolbar, Editor, getToolbarButtons } from '../../src/'; import './GanttToolbarButtons.css'; export default function GanttToolbarButtons({ skinSettings }) { @@ -15,7 +15,7 @@ export default function GanttToolbarButtons({ skinSettings }) { }, [helpers]); const items = useMemo(() => { - const items = defaultToolbarButtons.filter((b) => { + const items = getToolbarButtons().filter((b) => { return b.id?.indexOf('indent') === -1; }); diff --git a/demos/cases/GanttToolbarCustom.jsx b/demos/cases/GanttToolbarCustom.jsx index 7ddf712..9bae2cb 100644 --- a/demos/cases/GanttToolbarCustom.jsx +++ b/demos/cases/GanttToolbarCustom.jsx @@ -9,7 +9,7 @@ export default function GanttToolbarCustom({ skinSettings }) { const data = useMemo(() => getData(), []); const [api, setApi] = useState(null); - const selectedValue = useStoreLater(api, "selected"); + const selectedValue = useStoreLater(api, 'selected'); function handleAdd() { if (!api) return; @@ -81,13 +81,15 @@ export default function GanttToolbarCustom({ skinSettings }) { }, [api, selectedValue, allItems]); const GanttInitialised = useMemo(() => { - return + return ( + + ); }, [skinSettings, data.tasks, data.links, data.scales]); return ( diff --git a/demos/cases/ProAutoSchedule.css b/demos/cases/ProAutoSchedule.css new file mode 100644 index 0000000..01d7c70 --- /dev/null +++ b/demos/cases/ProAutoSchedule.css @@ -0,0 +1,23 @@ +.demo.wx-vkht5Uh1 { + height: 100%; + display: flex; + flex-direction: column; +} + +.bar.wx-vkht5Uh1 { + display: flex; + align-items: center; + padding: 12px; + gap: 20px; + border-bottom: var(--wx-gantt-border); +} + +.gantt.wx-vkht5Uh1 { + position: relative; + height: 100%; + overflow: hidden; +} + +.bar.wx-left.wx-vkht5Uh1 .wx-field { + margin-bottom: 0px; +} diff --git a/demos/cases/ProAutoSchedule.jsx b/demos/cases/ProAutoSchedule.jsx new file mode 100644 index 0000000..5086c65 --- /dev/null +++ b/demos/cases/ProAutoSchedule.jsx @@ -0,0 +1,42 @@ +import { useState, useMemo } from 'react'; +import { getData } from '../data'; +import { Gantt, Editor, ContextMenu } from '../../src'; +import { DatePicker, Field } from '@svar-ui/react-core'; +import './ProAutoSchedule.css'; + +function ProAutoSchedule({ skinSettings }) { + const [api, setApi] = useState(null); + const [projectStart, setProjectStart] = useState(new Date(2026, 3, 2)); + + const data = useMemo(() => getData(), []); + + return ( +
+
+ + setProjectStart(value)} + /> + +
+
+ {api && } + + + +
+
+ ); +} + +export default ProAutoSchedule; diff --git a/demos/cases/ProBaselines.jsx b/demos/cases/ProBaselines.jsx new file mode 100644 index 0000000..6f0ae8d --- /dev/null +++ b/demos/cases/ProBaselines.jsx @@ -0,0 +1,54 @@ +import { useMemo, useState } from 'react'; +import { getData } from '../data'; +import { Gantt, Editor, getEditorItems } from '../../src/'; + +function ProBaseline({ skinSettings }) { + const data = useMemo(() => getData('day', { baselines: true }), []); + + const [api, setApi] = useState(); + + const items = useMemo( + () => + getEditorItems().flatMap((item) => + item.key === 'links' + ? [ + ...[ + { + key: 'base_start', + comp: 'date', + label: 'Baseline start', + }, + { + key: 'base_end', + comp: 'date', + label: 'Baseline end', + }, + { + key: 'base_duration', + comp: 'counter', + hidden: true, + }, + ], + item, + ] + : item, + ), + [], + ); + + return ( + <> + + {api && } + + ); +} + +export default ProBaseline; diff --git a/demos/cases/ProCalendar.jsx b/demos/cases/ProCalendar.jsx new file mode 100644 index 0000000..a26b6c5 --- /dev/null +++ b/demos/cases/ProCalendar.jsx @@ -0,0 +1,43 @@ +import { useState, useMemo } from 'react'; +import { getData } from '../data'; +import { Gantt, Editor } from '../../src'; +import { Calendar } from '@svar-ui/gantt-store'; + +function ProCalendar({ skinSettings }) { + const { tasks, links, scales } = useMemo(() => getData('calendar'), []); + + const calendar = useMemo( + () => + new Calendar({ + weekHours: { + monday: 8, + tuesday: 8, + wednesday: 8, + thursday: 8, + friday: 8, + saturday: 0, + sunday: 0, + }, + }), + [], + ); + + const [api, setApi] = useState(); + + return ( + <> + + {api && } + + ); +} + +export default ProCalendar; diff --git a/demos/cases/ProCalendarChanges.css b/demos/cases/ProCalendarChanges.css new file mode 100644 index 0000000..29920f9 --- /dev/null +++ b/demos/cases/ProCalendarChanges.css @@ -0,0 +1,20 @@ +.rows.wx-FkPcChng { + position: relative; + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + overflow: hidden; +} + +.bar.wx-FkPcChng { + padding: 12px; + display: flex; + align-items: center; + gap: 20px; +} +.gtcell.wx-FkPcChng { + position: relative; + height: calc(100% - 56px); + border-top: var(--wx-gantt-border); +} diff --git a/demos/cases/ProCalendarChanges.jsx b/demos/cases/ProCalendarChanges.jsx new file mode 100644 index 0000000..eddd171 --- /dev/null +++ b/demos/cases/ProCalendarChanges.jsx @@ -0,0 +1,72 @@ +import { useState, useMemo, useCallback } from 'react'; +import { getData } from '../data'; +import { Gantt, Editor } from '../../src'; +import { Calendar } from '@svar-ui/gantt-store'; +import { Button } from '@svar-ui/react-core'; +import './ProCalendarChanges.css'; + +function ProCalendarChanges({ skinSettings }) { + const initialData = useMemo(() => getData('calendar'), []); + const [tasks, setTasks] = useState(initialData.tasks); + const { links, scales } = initialData; + + const calendar = useMemo( + () => + new Calendar({ + weekHours: { + monday: 8, + tuesday: 8, + wednesday: 8, + thursday: 8, + friday: 8, + saturday: 0, + sunday: 0, + }, + }), + [], + ); + + const [api, setApi] = useState(); + + const addNewRule = useCallback(() => { + calendar.addRule((date) => { + const weekday = date.getDay(); + if (weekday === 3) return 0; + }); + + setTasks( + api.serialize().map((task) => { + if (!calendar.isWorkingDay(task.start)) { + task.start = calendar.getNextWorkingDay(task.start); + } + return task; + }), + ); + }, [api, calendar]); + + return ( +
+
+ Rule: every Wednesday is off + +
+ +
+ + {api && } +
+
+ ); +} + +export default ProCalendarChanges; diff --git a/demos/cases/ProCriticalPath.css b/demos/cases/ProCriticalPath.css new file mode 100644 index 0000000..6f622b5 --- /dev/null +++ b/demos/cases/ProCriticalPath.css @@ -0,0 +1,12 @@ +.demo.wx-D71fWZ6y { + height: 100%; + display: flex; + flex-direction: column; +} + +.bar.wx-D71fWZ6y { + display: flex; + justify-content: center; + padding: 12px; + border-bottom: var(--wx-gantt-border); +} diff --git a/demos/cases/ProCriticalPath.jsx b/demos/cases/ProCriticalPath.jsx new file mode 100644 index 0000000..8a77e9a --- /dev/null +++ b/demos/cases/ProCriticalPath.jsx @@ -0,0 +1,62 @@ +import { useMemo, useState } from 'react'; +import { getData } from '../data'; +import { Gantt, Editor } from '../../src'; +import { DatePicker, Field, Locale, RichSelect } from '@svar-ui/react-core'; +import './ProCriticalPath.css'; + +export default function ProCriticalPath({ skinSettings }) { + const data = useMemo(() => getData('critical'), []); + + const [api, setApi] = useState(); + const [pathMode, setPathMode] = useState('flexible'); + const [projectStart, setProjectStart] = useState(new Date(2026, 3, 2)); + const [projectEnd, setProjectEnd] = useState(new Date(2026, 3, 12)); + + function init(ganttApi) { + setApi(ganttApi); + } + + return ( + <> +
+ +
+ + setPathMode(value)} + /> + + + setProjectStart(value)} + /> + + + setProjectEnd(value)} + /> + +
+
+ +
+ {api && } + + ); +} diff --git a/demos/cases/ProMarkers.css b/demos/cases/ProMarkers.css new file mode 100644 index 0000000..a34146a --- /dev/null +++ b/demos/cases/ProMarkers.css @@ -0,0 +1,10 @@ +.gt-cell.wx-g4H1PKcW { + width: 100%; + height: 100%; +} +.gt-cell.wx-g4H1PKcW > .wx-gantt .myMiddleClass { + background-color: rgba(255, 84, 84, 0.77); +} +.gt-cell.wx-g4H1PKcW > .wx-gantt .myEndClass { + background-color: rgba(54, 206, 124, 0.77); +} diff --git a/demos/cases/ProMarkers.jsx b/demos/cases/ProMarkers.jsx new file mode 100644 index 0000000..ac5389e --- /dev/null +++ b/demos/cases/ProMarkers.jsx @@ -0,0 +1,42 @@ +import { useMemo } from 'react'; +import { getData } from '../data'; +import { Gantt } from '../../src/'; +import './ProMarkers.css'; + +const ProMarkers = ({ skinSettings }) => { + const data = useMemo(() => getData(), []); + + const markers = useMemo( + () => [ + { + start: new Date(2026, 3, 2), + text: 'Start Project', + }, + { + start: new Date(2026, 3, 8), + text: 'Today', + css: 'myMiddleClass', + }, + { + start: new Date(2026, 4, 3), + text: 'End Project', + css: 'myEndClass', + }, + ], + [], + ); + + return ( +
+ +
+ ); +}; + +export default ProMarkers; diff --git a/demos/cases/ProScheduleAll.css b/demos/cases/ProScheduleAll.css new file mode 100644 index 0000000..c718307 --- /dev/null +++ b/demos/cases/ProScheduleAll.css @@ -0,0 +1,23 @@ +.demo.wx-D71fWZ7y { + height: 100%; + display: flex; + flex-direction: column; +} + +.bar.wx-D71fWZ7y { + display: flex; + align-items: center; + padding: 12px; + gap: 20px; + border-bottom: var(--wx-gantt-border); +} + +.gantt.wx-D71fWZ7y { + position: relative; + height: 100%; + overflow: hidden; +} + +.bar.wx-D71fWZ7y .wx-field.wx-left { + margin-bottom: 0px; +} diff --git a/demos/cases/ProScheduleAll.jsx b/demos/cases/ProScheduleAll.jsx new file mode 100644 index 0000000..3ac0eae --- /dev/null +++ b/demos/cases/ProScheduleAll.jsx @@ -0,0 +1,160 @@ +import { useState, useMemo } from 'react'; +import { getData } from '../data'; +import { Gantt, Editor, ContextMenu } from '../../src'; +import { DatePicker, Field, Checkbox } from '@svar-ui/react-core'; +import { Calendar } from '@svar-ui/gantt-store'; +import './ProScheduleAll.css'; + +function ProScheduleAll({ skinSettings }) { + const data = useMemo( + () => + getData('calendar', { + splitTasks: true, + unscheduledTasks: true, + }), + [], + ); + + const [api, setApi] = useState(); + const [tasks, setTasks] = useState(data.tasks); + + const calendar = useMemo(() => new Calendar(), []); + const [projectStart, setProjectStart] = useState(new Date(2026, 3, 2)); + const [projectEnd, setProjectEnd] = useState(new Date(2026, 4, 20)); + + const [criticalPath, setCriticalPath] = useState({ type: 'flexible' }); + const [baselines, setBaselines] = useState(true); + const [unscheduledTasks, setUnscheduledTasks] = useState(true); + const [splitTasks, setSplitTasks] = useState(true); + + const [cellHeight, setCellHeight] = useState(44); + + const markers = useMemo( + () => + projectStart + ? [ + { + text: 'Start', + start: projectStart, + }, + ] + : [], + [projectStart], + ); + + function onCriticalPathChange() { + setCriticalPath( + criticalPath?.type === 'flexible' ? null : { type: 'flexible' }, + ); + } + + function onBaselinesChange(ev) { + setBaselines(ev.value); + setCellHeight(ev.value ? 44 : 38); + } + + function onSplitChange() { + setTasks( + api.serialize().map((t) => { + //recalculate duration + if (t.segments) delete t.duration; + return t; + }), + ); + } + + //calculate baselines after summary dates are set + function init(ganttApi) { + setApi(ganttApi); + setTasks( + ganttApi.serialize().map((t) => { + return { + ...t, + base_start: t.start, + base_end: t.end, + base_duration: t.segments ? 0 : t.duration, + }; + }), + ); + } + + /*data.links.push({ + source: 2, + target: 3, + type: "e2s", + id: 100, + }); + data.links.push({ + source: 30, + target: 4, + type: "e2s", + id: 101, + });*/ + + return ( +
+
+ + setProjectStart(value)} + /> + + + setProjectEnd(value)} + /> + + + + setUnscheduledTasks(value)} + /> + { + setSplitTasks(ev.value); + onSplitChange(); + }} + /> +
+
+ {api && } + + + +
+
+ ); +} + +export default ProScheduleAll; diff --git a/demos/cases/ProSplitTasks.css b/demos/cases/ProSplitTasks.css new file mode 100644 index 0000000..58e2990 --- /dev/null +++ b/demos/cases/ProSplitTasks.css @@ -0,0 +1,4 @@ +.gtcell.wx-4eOjA4yB { + height: calc(100% - 50px); + border-top: var(--wx-gantt-border); +} diff --git a/demos/cases/ProSplitTasks.jsx b/demos/cases/ProSplitTasks.jsx new file mode 100644 index 0000000..f192020 --- /dev/null +++ b/demos/cases/ProSplitTasks.jsx @@ -0,0 +1,28 @@ +import { useState, useMemo } from 'react'; +import { getData } from '../data'; +import { Gantt, ContextMenu, Editor, Toolbar } from '../../src/'; +import './ProSplitTasks.css'; + +export default function ProSplitTasks({ skinSettings }) { + const [api, setApi] = useState(); + const data = useMemo(() => getData('day', { splitTasks: true }), []); + + return ( + <> + +
+ + + + {api && } +
+ + ); +} diff --git a/demos/cases/ProSummariesConvert.jsx b/demos/cases/ProSummariesConvert.jsx new file mode 100644 index 0000000..74f2fae --- /dev/null +++ b/demos/cases/ProSummariesConvert.jsx @@ -0,0 +1,109 @@ +import { useMemo, useCallback, useRef } from 'react'; +import { useStoreLater } from '@svar-ui/lib-react'; +import { getData } from '../data'; +import { + Gantt, + ContextMenu, + Editor, + getEditorItems, + defaultTaskTypes, +} 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]); + + return ( +
+ + + + {gApi.current && } +
+ ); +} diff --git a/demos/cases/ProSummariesProgress.css b/demos/cases/ProSummariesProgress.css new file mode 100644 index 0000000..da5cfe5 --- /dev/null +++ b/demos/cases/ProSummariesProgress.css @@ -0,0 +1,9 @@ +.gt-cell > .wx-gantt .wx-summary .wx-progress-marker { + display: none; +} + +.gt-cell.wx-OeNgRLo4, +.wrapper.wx-OeNgRLo4 { + width: 100%; + height: 100%; +} diff --git a/demos/cases/ProSummariesProgress.jsx b/demos/cases/ProSummariesProgress.jsx new file mode 100644 index 0000000..2deac7d --- /dev/null +++ b/demos/cases/ProSummariesProgress.jsx @@ -0,0 +1,131 @@ +import { useState, useMemo, useCallback, useEffect, useRef } from 'react'; +import { getData } from '../data'; +import { Gantt, Editor, getEditorItems, ContextMenu } from '../../src'; +import './ProSummariesProgress.css'; + +function SummariesProgress({ skinSettings }) { + const data = useMemo(() => getData(), []); + const tasks = data.tasks; + + 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 && } +
+ ); +} + +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/cases/ProUndo.css b/demos/cases/ProUndo.css new file mode 100644 index 0000000..85dd2bf --- /dev/null +++ b/demos/cases/ProUndo.css @@ -0,0 +1,35 @@ +.rows.wx-D71fWZ9y { + position: relative; + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + overflow: hidden; +} +.buttons.wx-D71fWZ9y { + display: flex; + gap: 10px; + padding: 8px 12px; +} +.gtcell.wx-D71fWZ9y { + position: relative; + height: calc(100% - 48px); + border-top: var(--wx-gantt-border); +} +.button.wx-D71fWZ9y { + position: relative; +} +.button.wx-D71fWZ9y span { + background-color: var(--wx-color-danger); + height: 18px; + width: 18px; + border-radius: 50px; + position: absolute; + text-align: center; + font-size: 12px; + line-height: 12px; + padding: 3px 0; + color: #fff; + top: -6px; + right: -6px; +} diff --git a/demos/cases/ProUndo.jsx b/demos/cases/ProUndo.jsx new file mode 100644 index 0000000..c8a04d8 --- /dev/null +++ b/demos/cases/ProUndo.jsx @@ -0,0 +1,79 @@ +import { useState, useEffect, useMemo, useRef } from 'react'; +import { Button } from '@svar-ui/react-core'; +import { getData } from '../data'; +import { Gantt, Editor, ContextMenu } from '../../src'; +import './ProUndo.css'; + +export default function ProUndo({ skinSettings }) { + const [api, setApi] = useState(); + + const { tasks, links, scales } = useMemo(() => getData(), []); + function init(ganttApi) { + setApi(ganttApi); + } + + return ( +
+ {api && } +
+ + + + {api && } +
+
+ ); +} + +function History({ api }) { + + const [history, setHistory] = useState(null); + function handleUndo() { + api.exec('undo'); + } + + function handleRedo() { + api.exec('redo'); + } + + + useEffect(() => { + api.getReactiveState().history.subscribe((v) => (setHistory(v))); + }, []); + + return ( +
+
+ + {history && history.undo > 0 && ( + {history.undo} + )} +
+
+ + {history && history.redo > 0 && ( + {history.redo} + )} +
+
+); +} \ No newline at end of file diff --git a/demos/cases/ProUndoToolbar.css b/demos/cases/ProUndoToolbar.css new file mode 100644 index 0000000..c2563e8 --- /dev/null +++ b/demos/cases/ProUndoToolbar.css @@ -0,0 +1,4 @@ +.gtcell.wx-D71fWZAy { + height: calc(100% - 50px); + border-top: var(--wx-gantt-border); +} diff --git a/demos/cases/ProUndoToolbar.jsx b/demos/cases/ProUndoToolbar.jsx new file mode 100644 index 0000000..00a0137 --- /dev/null +++ b/demos/cases/ProUndoToolbar.jsx @@ -0,0 +1,27 @@ +import { useState, useMemo } from 'react'; +import { getData } from '../data'; +import { Gantt, Toolbar, Editor } from '../../src/'; +import './ProUndoToolbar.css'; + +export default function ProUndoToolbar({ skinSettings }) { + const [api, setApi] = useState(); + + const data = useMemo(() => getData(), []); + + return ( + <> + +
+ + {api && } +
+ + ); +} diff --git a/demos/cases/ProUnscheduledTasks.jsx b/demos/cases/ProUnscheduledTasks.jsx new file mode 100644 index 0000000..f55abd0 --- /dev/null +++ b/demos/cases/ProUnscheduledTasks.jsx @@ -0,0 +1,25 @@ +import { useState } from 'react'; +import { getData } from '../data'; +import { Gantt, Editor } from '../../src'; + +function ProUnscheduledTasks({ skinSettings }) { + const [api, setApi] = useState(null); + + const data = getData('day', { unscheduledTasks: true }); + + return ( + <> + + {api && } + + ); +} + +export default ProUnscheduledTasks; diff --git a/demos/cases/ProUnscheduledTasksAndBaselines.jsx b/demos/cases/ProUnscheduledTasksAndBaselines.jsx new file mode 100644 index 0000000..9c65f3c --- /dev/null +++ b/demos/cases/ProUnscheduledTasksAndBaselines.jsx @@ -0,0 +1,89 @@ +import { useMemo, useState } from 'react'; +import { Gantt, Editor, defaultEditorItems, defaultColumns } from '../../src'; +import { format } from 'date-fns'; +import { getBaselinesData } from '../data'; + +function ProUnscheduledTasksAndBaselines({ skinSettings }) { + const [api, setApi] = useState(null); + + const data = useMemo(() => { + const rawData = getBaselinesData(); + const cloned = { ...rawData }; + + cloned.tasks = cloned.tasks.map((task) => { + if (task.id === 10 || task.id === 12) { + task = { ...task, unscheduled: true }; + } + + if (task.id === 10) { + task = { + ...task, + start: new Date(2024, 3, 5), + end: new Date(2024, 3, 7), + }; + } + if (task.id === 11) { + task = { + ...task, + start: new Date(2024, 3, 2), + end: new Date(2024, 3, 5), + }; + } + return task; + }); + + return cloned; + }, []); + + const items = useMemo(() => { + return defaultEditorItems.flatMap((item) => + item.key === 'links' + ? [ + ...[ + { + key: 'base_start', + comp: 'date', + label: 'Baseline start', + }, + { + key: 'base_end', + comp: 'date', + label: 'Baseline end', + }, + ], + item, + ] + : item, + ); + }, []); + + const columns = useMemo(() => { + return defaultColumns.map((col) => { + if (col.id == 'start') + col.template = (value, row) => + row.unscheduled ? '-' : format(value, 'dd-MM-yyyy'); + if (col.id == 'duration') + col.template = (value, row) => (row.unscheduled ? '-' : value); + return col; + }); + }, []); + + return ( + <> + + {api && } + + ); +} + +export default ProUnscheduledTasksAndBaselines; diff --git a/demos/common/Index.css b/demos/common/Index.css index c14de09..1def0f6 100644 --- a/demos/common/Index.css +++ b/demos/common/Index.css @@ -76,13 +76,13 @@ } .wx-demos.sidebar.active { - width: 300px; + width: 320px; } .wx-demos.sidebar-content { display: flex; flex-direction: column; - width: 300px; + width: 320px; gap: 16px; height: 100%; border-right: 1px solid #ebebeb; @@ -133,7 +133,7 @@ .wx-demos.box-links { display: flex; flex-direction: column; - gap: 6px; + gap: 5px; } .wx-demos.hint { @@ -210,14 +210,14 @@ .wx-demos.demo { display: flex; align-items: center; - height: 37px; font-weight: 400; - padding-left: 16px; + padding: 8px 12px 8px 16px; border-left: 4px solid transparent; color: #595b66; list-style: none; cursor: pointer; text-decoration: none; + min-height: 38px; } .wx-demos.demo.active { @@ -226,7 +226,26 @@ .wx-demos.demo.active, .wx-demos.demo:hover { - font-weight: 500; + font-weight: 400; color: #42454d; background-color: #f1f1f1; } + +.wx-demos.demo.active { + font-weight: 500; +} + +.wx-demos.demo .pro { + color: #087a9f; + border: 1px solid #087a9f; + border-radius: 4px; + padding: 0px 8px; + font-size: 12px; + font-weight: 600; + margin-left: auto; + letter-spacing: 0.4px; +} + +.wx-theme { + height: 100%; +} diff --git a/demos/common/Index.jsx b/demos/common/Index.jsx index 6c1e63e..1fe91f3 100644 --- a/demos/common/Index.jsx +++ b/demos/common/Index.jsx @@ -1,5 +1,6 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useCallback } from 'react'; import { HashRouter, NavLink, useNavigate } from 'react-router-dom'; +import { Willow } from '@svar-ui/react-core'; import Router from './Router'; import { links } from '../routes'; @@ -18,38 +19,28 @@ function DemoExplorerContent({ const [skin, setSkin] = useState(skins[0].id); const [title, setTitle] = useState(''); const [githubLink, setGithubLink] = useState(''); - const [show, setShow] = useState(false); + const [show, setShow] = useState(true); const baseLink = 'https://github.com/svar-widgets/react-' + productTag + '/tree/main/demos/cases/'; - useEffect(() => { - document.body.className = `wx-willow-theme`; + 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 handleRouteChange = useCallback( - (path) => { - const parts = path.split('/'); - const page = parts[1]; - const newSkin = parts[2]; - - if (newSkin && newSkin !== skin) { - 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`); - } - }, - [skin], - ); - const handleSkinChange = ({ value }) => { setSkin(value); const currentPath = window.location.hash.slice(1); @@ -63,8 +54,10 @@ function DemoExplorerContent({ setShow(!show); }; + const SkinComponent = skins.find((s) => s.id === skin).Component; + return ( - @@ -169,12 +165,9 @@ function DemoExplorerContent({ } export default function DemoExplorer(props) { - const skins = props.skins; return ( <> - {skins.map((skin) => ( - - ))} + diff --git a/demos/common/Router.jsx b/demos/common/Router.jsx index 4cdddda..f278281 100644 --- a/demos/common/Router.jsx +++ b/demos/common/Router.jsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { Routes, Route, @@ -12,7 +12,9 @@ export default function Router({ onRouteChange }) { const location = useLocation(); const navigate = useNavigate(); + const lastRef = useRef(location.pathname); useEffect(() => { + if (lastRef.current === location.pathname) return; if (location.pathname === '/') { navigate('/base/willow', { replace: true }); } else { diff --git a/demos/custom/Form.jsx b/demos/custom/Form.jsx index 0cd5d22..a6d541b 100644 --- a/demos/custom/Form.jsx +++ b/demos/custom/Form.jsx @@ -65,68 +65,50 @@ export default function Form(props) {
- {({ id }) => ( - handleChange(ev, 'text')} - /> - )} + handleChange(ev, 'text')} + /> - {({ id }) => ( -