This commit is contained in:
Marta Kowalska
2025-12-22 20:22:30 +00:00
parent d4429300aa
commit bb81e7a336
90 changed files with 3676 additions and 1828 deletions

View File

@@ -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',

View File

@@ -44,7 +44,7 @@ export default function DropDownMenu({ skinSettings }) {
links={data.links}
scales={data.scales}
/>
<Editor api={api} />
{api && <Editor api={api} />}
</div>
</div>
</>

View File

@@ -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 (
<>

View File

@@ -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' },

View File

@@ -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 } }),
})),

View File

@@ -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',

View File

@@ -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 && <Editor
api={api}
items={items}
bottomBar={bottomBar}
topBar={false}
placement="modal"
autoSave={false}
/>}
{api && (
<Editor
api={api}
items={items}
bottomBar={bottomBar}
topBar={false}
placement="modal"
autoSave={false}
/>
)}
</>
);
}

View File

@@ -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'),

View File

@@ -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();

View File

@@ -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',

View File

@@ -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' && {

View File

@@ -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 }) {
<h4>Click the "expand" icon, or click on Gantt and press Ctrl+Shift+F</h4>
<div className="wx-0qqHrQ85 gtcell">
<Fullscreen hotkey="ctrl+shift+f">
<Gantt
{...skinSettings}
tasks={data.tasks}
links={data.links}
/>
<Gantt {...skinSettings} tasks={data.tasks} links={data.links} />
</Fullscreen>
</div>
</div>

View File

@@ -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' },
],
[],
);

View File

@@ -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;

View File

@@ -22,11 +22,7 @@ function GanttLocale({ skinSettings }) {
onChange={({ value }) => setLang(value)}
/>
</div>
{lang === 'en' && (
<GanttWidget
skinSettings={skinSettings}
/>
)}
{lang === 'en' && <GanttWidget skinSettings={skinSettings} />}
{lang === 'cn' && (
<Locale words={{ ...cn, ...cnCore }}>
<GanttWidget skinSettings={skinSettings} />
@@ -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 (
<>
<Toolbar api={api} />
<div className="wx-ycv5Oz8L gtcell">
<ContextMenu api={api}>
<Gantt
{...skinSettings}
tasks={data.tasks}
links={data.links}
scales={data.scales}
init={setApi}
/>
<Gantt {...settings} init={setApi} />
</ContextMenu>
{api && <Editor api={api} />}
</div>

View File

@@ -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}
/>
</div>

View File

@@ -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 <span>{label}</span>
}
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 (
<div className="wx-KB3Eoqwm rows">
<div className="wx-KB3Eoqwm row">
{start ? (
<>
1 000 tasks ({years} years ) rendered in{' '}
<span ref={outAreaRef}></span> ms
10 000 tasks ({years} years ) rendered in{' '}
<RenderTime start={start} />
</>
) : (
<Button type="primary" onClick={() => setStart(new Date())}>
Press me to render Gantt chart with 1 000 tasks
<Button type="primary" onClick={() => setStart(new Date()) }>
Press me to render Gantt chart with 10 000 tasks
</Button>
)}
</div>

View File

@@ -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;
}

View File

@@ -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 (
<div className="wx-RPSbwjNq rows">
<div className="wx-RPSbwjNq bar">
<Field label="Adding and editing" position={'left'}>
{({ id }) => (
<Switch
value={edit}
onChange={({ value }) => setEdit(value)}
id={id}
/>
)}
<Field label="Adding tasks" position={'left'}>
<Switch value={add} onChange={({ value }) => setAdd(value)} />
</Field>
<Field label="Creating links" position={'left'}>
{({ id }) => (
<Switch
value={newLink}
onChange={({ value }) => setNewLink(value)}
id={id}
/>
)}
<Switch value={newLink} onChange={({ value }) => setNewLink(value)} />
</Field>
<Field label="Deleting links" position={'left'}>
<Switch
value={deleteLink}
onChange={({ value }) => setDeleteLink(value)}
/>
</Field>
<Field label="Dragging tasks" position={'left'}>
{({ id }) => (
<Switch
value={drag}
onChange={({ value }) => setDrag(value)}
id={id}
/>
)}
<Switch value={drag} onChange={({ value }) => setDrag(value)} />
</Field>
<Field label="Reordering tasks" position={'left'}>
{({ id }) => (
<Switch
value={order}
onChange={({ value }) => setOrder(value)}
id={id}
/>
)}
<Switch value={order} onChange={({ value }) => setOrder(value)} />
</Field>
<Field label="Editing progress" position={'left'}>
<Switch
value={progress}
onChange={({ value }) => setProgress(value)}
/>
</Field>
</div>
<div
className={
'wx-RPSbwjNq ' +
('gantt' +
(!edit ? ' hide-progress' : '') +
(!newLink ? ' hide-links' : '') +
(!drag ? ' hide-drag' : ''))
(!deleteLink ? ' hide-delete-links' : '') +
(!drag ? ' hide-drag' : '') +
(!progress ? ' hide-progress' : ''))
}
>
<Gantt
@@ -113,7 +84,6 @@ function GanttPreventActions({ skinSettings }) {
scales={data.scales}
columns={columns}
/>
{api && <Editor api={api} />}
</div>
</div>
);

View File

@@ -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}
/>
);

View File

@@ -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}
/>
);

View File

@@ -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 }) {
<Locale>
<div className="wx-FJQN2sNt bar">
<Field label="Start" position="left">
{({ id }) => (
<DatePicker
value={start}
id={id}
onChange={({ value }) => setStart(value)}
/>
)}
<DatePicker
value={start}
onChange={({ value }) => setStart(value)}
/>
</Field>
<Field label="End" position="left">
{({ id }) => (
<DatePicker
value={end}
id={id}
onChange={({ value }) => setEnd(value)}
/>
)}
<DatePicker value={end} onChange={({ value }) => setEnd(value)} />
</Field>
<Field label="autoScale" position="left">
{({ id }) => (
<div className="wx-FJQN2sNt input">
<Switch
value={autoScale}
id={id}
onChange={({ value }) => setAutoScale(value)}
/>
</div>
)}
<div className="wx-FJQN2sNt input">
<Switch
value={autoScale}
onChange={({ value }) => setAutoScale(value)}
/>
</div>
</Field>
</div>
</Locale>

View File

@@ -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;
});

View File

@@ -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 <Gantt
{...(skinSettings || {})}
init={setApi}
tasks={data.tasks}
links={data.links}
scales={data.scales}
/>
return (
<Gantt
{...(skinSettings || {})}
init={setApi}
tasks={data.tasks}
links={data.links}
scales={data.scales}
/>
);
}, [skinSettings, data.tasks, data.links, data.scales]);
return (

View File

@@ -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;
}

View File

@@ -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 (
<div className="demo wx-vkht5Uh1">
<div className="bar wx-vkht5Uh1">
<Field label="Project start" position="left">
<DatePicker
value={projectStart}
onChange={({ value }) => setProjectStart(value)}
/>
</Field>
</div>
<div className="gantt wx-vkht5Uh1">
{api && <Editor api={api} />}
<ContextMenu api={api}>
<Gantt
{...skinSettings}
init={setApi}
tasks={data.tasks}
links={data.links}
scales={data.scales}
schedule={{ auto: true }}
projectStart={projectStart}
projectEnd={new Date(2026, 5, 2)}
/>
</ContextMenu>
</div>
</div>
);
}
export default ProAutoSchedule;

View File

@@ -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 (
<>
<Gantt
ref={setApi}
{...skinSettings}
baselines={true}
cellHeight={45}
tasks={data.tasks}
links={data.links}
/>
{api && <Editor api={api} items={items} />}
</>
);
}
export default ProBaseline;

View File

@@ -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 (
<>
<Gantt
{...skinSettings}
init={setApi}
calendar={calendar}
tasks={tasks}
links={links}
scales={scales}
cellWidth={60}
/>
{api && <Editor api={api} />}
</>
);
}
export default ProCalendar;

View File

@@ -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);
}

View File

@@ -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 (
<div className="wx-FkPcChng rows">
<div className="wx-FkPcChng bar">
<span> Rule: every Wednesday is off</span>
<Button type="primary" onClick={addNewRule}>
Add rule
</Button>
</div>
<div className="wx-FkPcChng gtcell">
<Gantt
{...skinSettings}
init={setApi}
calendar={calendar}
tasks={tasks}
links={links}
scales={scales}
cellWidth={60}
/>
{api && <Editor api={api} />}
</div>
</div>
);
}
export default ProCalendarChanges;

View File

@@ -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);
}

View File

@@ -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 (
<>
<div className="demo wx-D71fWZ6y">
<Locale>
<div className="bar wx-D71fWZ6y">
<Field label="Mode" position="left">
<RichSelect
options={[
{ id: 'flexible', label: 'Flexible' },
{ id: 'strict', label: 'Strict' },
]}
value={pathMode}
onChange={({ value }) => setPathMode(value)}
/>
</Field>
<Field label="Project start" position="left">
<DatePicker
value={projectStart}
onChange={({ value }) => setProjectStart(value)}
/>
</Field>
<Field label="Project end" position="left">
<DatePicker
value={projectEnd}
onChange={({ value }) => setProjectEnd(value)}
/>
</Field>
</div>
</Locale>
<Gantt
{...skinSettings}
init={init}
tasks={data.tasks}
links={data.links}
scales={data.scales}
criticalPath={{ type: pathMode }}
projectStart={projectStart}
projectEnd={projectEnd}
/>
</div>
{api && <Editor api={api} />}
</>
);
}

View File

@@ -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);
}

View File

@@ -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 (
<div className="wx-g4H1PKcW gt-cell">
<Gantt
{...skinSettings}
markers={markers}
tasks={data.tasks}
links={data.links}
scales={data.scales}
/>
</div>
);
};
export default ProMarkers;

View File

@@ -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;
}

View File

@@ -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 (
<div className="demo wx-D71fWZ7y">
<div className="bar wx-D71fWZ7y">
<Field label="Project start" position="left" width="250px">
<DatePicker
value={projectStart}
onChange={({ value }) => setProjectStart(value)}
/>
</Field>
<Field label="Project end" position="left" width="250px">
<DatePicker
value={projectEnd}
onChange={({ value }) => setProjectEnd(value)}
/>
</Field>
<Checkbox
value={!!criticalPath}
label="Critical path"
onChange={onCriticalPathChange}
/>
<Checkbox
value={baselines}
label="Baselines"
onChange={onBaselinesChange}
/>
<Checkbox
value={unscheduledTasks}
label="Unscheduled tasks"
onChange={({ value }) => setUnscheduledTasks(value)}
/>
<Checkbox
value={splitTasks}
label="Split tasks"
onChange={(ev) => {
setSplitTasks(ev.value);
onSplitChange();
}}
/>
</div>
<div className="gantt wx-D71fWZ7y">
{api && <Editor api={api} />}
<ContextMenu api={api}>
<Gantt
init={init}
{...skinSettings}
cellWidth={50}
cellHeight={cellHeight}
tasks={tasks}
links={data.links}
scales={data.scales}
calendar={calendar}
schedule={{ auto: true }}
criticalPath={criticalPath}
projectStart={projectStart}
projectEnd={projectEnd}
markers={markers}
baselines={baselines}
unscheduledTasks={unscheduledTasks}
splitTasks={splitTasks}
/>
</ContextMenu>
</div>
</div>
);
}
export default ProScheduleAll;

View File

@@ -0,0 +1,4 @@
.gtcell.wx-4eOjA4yB {
height: calc(100% - 50px);
border-top: var(--wx-gantt-border);
}

View File

@@ -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 (
<>
<Toolbar api={api} />
<div className="gtcell">
<ContextMenu api={api}>
<Gantt
init={setApi}
{...skinSettings}
tasks={data.tasks}
links={data.links}
scales={data.scales}
splitTasks={true}
/>
</ContextMenu>
{api && <Editor api={api} />}
</div>
</>
);
}

View File

@@ -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 (
<div className="wx-TEIogFEZ gt-cell">
<ContextMenu api={gApi.current}>
<Gantt
init={init}
{...skinSettings}
tasks={data.tasks}
links={data.links}
scales={data.scales}
/>
</ContextMenu>
{gApi.current && <Editor api={gApi.current} items={items} />}
</div>
);
}

View File

@@ -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%;
}

View File

@@ -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 (
<div className="wx-OeNgRLo4 wrapper">
<ContextMenu api={api}>
<div className="wx-OeNgRLo4 gt-cell">
<Gantt
{...skinSettings}
init={init}
tasks={tasks}
links={data.links}
scales={data.scales}
cellWidth={30}
/>
</div>
</ContextMenu>
{api && <ConfiguredEditor api={api} />}
</div>
);
}
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 <Editor api={api} items={editorItems} />;
}
export default SummariesProgress;

35
demos/cases/ProUndo.css Normal file
View File

@@ -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;
}

79
demos/cases/ProUndo.jsx Normal file
View File

@@ -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 (
<div className="rows wx-D71fWZ9y">
{api && <History api={api} />}
<div className="gtcell wx-D71fWZ9y">
<ContextMenu api={api}>
<Gantt
init={init}
{...skinSettings}
tasks={tasks}
links={links}
scales={scales}
undo
/>
</ContextMenu>
{api && <Editor api={api} />}
</div>
</div>
);
}
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 (
<div className="buttons wx-D71fWZ9y">
<div className="button wx-D71fWZ9y">
<Button
type="primary"
onClick={handleUndo}
disabled={history && !history.undo}
>
Undo
</Button>
{history && history.undo > 0 && (
<span>{history.undo}</span>
)}
</div>
<div className="button wx-D71fWZ9y">
<Button
type="primary"
onClick={handleRedo}
disabled={history && !history.redo}
>
Redo
</Button>
{history && history.redo > 0 && (
<span>{history.redo}</span>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,4 @@
.gtcell.wx-D71fWZAy {
height: calc(100% - 50px);
border-top: var(--wx-gantt-border);
}

View File

@@ -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 (
<>
<Toolbar api={api} />
<div className="gtcell wx-D71fWZAy">
<Gantt
{...skinSettings}
init={setApi}
tasks={data.tasks}
links={data.links}
scales={data.scales}
undo
/>
{api && <Editor api={api} />}
</div>
</>
);
}

View File

@@ -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 (
<>
<Gantt
init={setApi}
{...skinSettings}
tasks={data.tasks}
links={data.links}
scales={data.scales}
unscheduledTasks={true}
/>
{api && <Editor api={api} />}
</>
);
}
export default ProUnscheduledTasks;

View File

@@ -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 (
<>
<Gantt
init={setApi}
{...skinSettings}
columns={columns}
tasks={data.tasks}
links={data.links}
scales={data.scales}
unscheduledTasks={true}
baselines={true}
cellHeight={45}
/>
{api && <Editor api={api} items={items} />}
</>
);
}
export default ProUnscheduledTasksAndBaselines;

View File

@@ -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%;
}

View File

@@ -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 (
<div className={`wx-demos layout ${show ? 'active' : ''}`}>
<div className={`wx-demos wx-willow-theme layout ${show ? 'active' : ''}`}>
<div
className={`wx-demos sidebar ${show ? 'active' : ''}`}
role="tabpanel"
@@ -81,7 +74,7 @@ function DemoExplorerContent({
</a>
<div className="wx-demos separator"></div>
<a
href={`https://svar.dev/react/${productTag}/`}
href={`https://svar.dev/react/gantt/`}
target="_blank"
rel="noopener noreferrer"
>
@@ -107,6 +100,7 @@ function DemoExplorerContent({
}
>
{data[1]}
{data[4] && data[4].pro && <span className="pro">PRO</span>}
</NavLink>
))}
</div>
@@ -159,7 +153,9 @@ function DemoExplorerContent({
data-wx-portal-root="true"
>
<Globals>
<Router skin={skin} onRouteChange={handleRouteChange} />
<SkinComponent>
<Router skin={skin} onRouteChange={handleRouteChange} />
</SkinComponent>
</Globals>
</div>
</div>
@@ -169,12 +165,9 @@ function DemoExplorerContent({
}
export default function DemoExplorer(props) {
const skins = props.skins;
return (
<>
{skins.map((skin) => (
<skin.Component key={skin.id} />
))}
<Willow />
<HashRouter>
<DemoExplorerContent {...props} />
</HashRouter>

View File

@@ -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 {

View File

@@ -65,68 +65,50 @@ export default function Form(props) {
</div>
<div className="wx-QkE5vh0y body">
<Field label="Name">
{({ id }) => (
<Text
id={id}
focus={true}
value={task.text}
onChange={(ev) => handleChange(ev, 'text')}
/>
)}
<Text
focus={true}
value={task.text}
onChange={(ev) => handleChange(ev, 'text')}
/>
</Field>
<Field label="Description">
{({ id }) => (
<TextArea
id={id}
value={task.details}
onChange={(ev) => handleChange(ev, 'details')}
/>
)}
<TextArea
value={task.details}
onChange={(ev) => handleChange(ev, 'details')}
/>
</Field>
{taskTypes.length > 1 ? (
<Field label="Type">
{({ id }) => (
<Select
id={id}
value={task.type}
options={taskTypes}
onChange={(ev) => handleChange(ev, 'type')}
/>
)}
<Select
value={task.type}
options={taskTypes}
onChange={(ev) => handleChange(ev, 'type')}
/>
</Field>
) : null}
<Field label="Start date">
{({ id }) => (
<DatePicker
id={id}
value={task.start}
onChange={(ev) => handleChange(ev, 'start')}
/>
)}
<DatePicker
value={task.start}
onChange={(ev) => handleChange(ev, 'start')}
/>
</Field>
{task.type !== 'milestone' ? (
<>
<Field label="End date">
{({ id }) => (
<DatePicker
id={id}
value={task.end}
onChange={(ev) => handleChange(ev, 'end')}
/>
)}
<DatePicker
value={task.end}
onChange={(ev) => handleChange(ev, 'end')}
/>
</Field>
<Field label={`Progress: ${task.progress}%`}>
{({ id }) => (
<Slider
id={id}
value={task.progress}
onChange={(ev) => handleChange(ev, 'progress')}
/>
)}
<Slider
value={task.progress}
onChange={(ev) => handleChange(ev, 'progress')}
/>
</Field>
</>
) : null}

File diff suppressed because it is too large Load Diff

View File

@@ -30,14 +30,13 @@ import GanttTaskTypes from './cases/GanttTaskTypes.jsx';
import ChartCellBorders from './cases/ChartBorders.jsx';
import ContextMenu from './cases/ContextMenu.jsx';
import ContextMenuHandler from './cases/ContextMenuHandler.jsx';
// import DropDownMenu from "./cases/DropDownMenu.jsx";
//import DropDownMenu from "./cases/DropDownMenu.jsx";
import ContextMenuOptions from './cases/ContextMenuOptions.jsx';
import GanttHolidays from './cases/GanttHolidays.jsx';
import GanttSort from './cases/GanttSort.jsx';
import GanttCustomSort from './cases/GanttCustomSort.jsx';
import GanttSummariesProgress from './cases/GanttSummariesProgress.jsx';
import GanttSummariesNoDrag from './cases/GanttSummariesNoDrag.jsx';
import GanttSummariesConvert from './cases/GanttSummariesConvert.jsx';
import SummariesProgress from './cases/ProSummariesProgress.jsx';
import SummariesConvert from './cases/ProSummariesConvert.jsx';
import GanttEditor from './cases/GanttEditor.jsx';
import GanttEditorConfig from './cases/GanttEditorConfig.jsx';
import GanttEditorCustomControls from './cases/GanttEditorCustomControls.jsx';
@@ -79,28 +78,11 @@ export const links = [
['/holidays/:skin', 'Holidays', GanttHolidays, 'GanttHolidays'],
['/templates/:skin', 'Custom text', GanttText, 'GanttText'],
['/tooltips/:skin', 'Tooltips', GanttTooltips, 'GanttTooltips'],
['/task-types/:skin', 'Task types', GanttTaskTypes, 'GanttTaskTypes'],
[
'/summary-progress/:skin',
'Summary tasks with auto progress',
GanttSummariesProgress,
'GanttSummariesProgress',
],
[
'/summary-no-drag/:skin',
'No drag for summary tasks',
GanttSummariesNoDrag,
'GanttSummariesNoDrag',
],
[
'/summary-convert/:skin',
'Auto convert to summary tasks',
GanttSummariesConvert,
'GanttSummariesConvert',
],
['/zoom/:skin', 'Zoom', GanttZoom, 'GanttZoom'],
['/custom-zoom/:skin', 'Custom Zoom', GanttCustomZoom, 'GanttCustomZoom'],
@@ -176,17 +158,9 @@ export const links = [
HeaderMenu,
'GridHeaderMenu',
],
['/custom-edit-form/:skin', 'Custom editor', GanttForm, 'GanttForm'],
['/locale/:skin', 'Locales', GanttLocale, 'GanttLocale'],
['/fullscreen/:skin', 'Fullscreen', GanttFullscreen, 'GanttFullscreen'],
['/readonly/:skin', 'Readonly mode', GanttReadOnly, 'GanttReadOnly'],
[
'/prevent-actions/:skin',
'Preventing actions',
GanttPreventActions,
'GanttPreventActions',
],
[
'/gantt-multiple/:skin',
'Many Gantts per page',
@@ -194,7 +168,12 @@ export const links = [
'GanttMultiple',
],
['/performance/:skin', 'Performance', GanttPerformance, 'GanttPerformance'],
[
'/prevent-actions/:skin',
'Preventing UI actions',
GanttPreventActions,
'GanttPreventActions',
],
['/sorting/:skin', 'Custom sorting', GanttSort, 'GanttSort'],
['/sorting-api/:skin', 'Sort by API', GanttCustomSort, 'GanttCustomSort'],
@@ -248,4 +227,5 @@ export const links = [
GanttEditorValidation,
'GanttEditorValidation',
],
['/custom-edit-form/:skin', 'Custom edit form', GanttForm, 'GanttForm'],
];