v2.3.1
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
import { useMemo } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt } from '../../src/';
|
||||
|
||||
export default function BasicInit({ skinSettings }) {
|
||||
const data = useMemo(() => getData(), []);
|
||||
|
||||
return (
|
||||
<Gantt
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={data.scales}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
.rows.wx-gnwCbZe9 {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--wx-background);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bar.wx-gnwCbZe9 {
|
||||
padding: 10px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.gtcell.wx-gnwCbZe9 {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
border-top: var(--wx-gantt-border);
|
||||
overflow: hidden;
|
||||
}
|
||||
.label.wx-gnwCbZe9 {
|
||||
padding-right: 20px;
|
||||
font-size: var(--wx-font-size);
|
||||
font-weight: var(--wx-label-font-weight);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt } from '../../src/';
|
||||
import { RadioButtonGroup } from '@svar-ui/react-core';
|
||||
import './ChartBorders.css';
|
||||
|
||||
export default function ChartBorders({ skinSettings }) {
|
||||
const data = useMemo(() => getData(), []);
|
||||
const [cellBorders, setCellBorders] = useState('full');
|
||||
|
||||
const options = useMemo(
|
||||
() => [
|
||||
{ id: 'full', label: 'Full' },
|
||||
{ id: 'column', label: 'Column' },
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="wx-gnwCbZe9 rows">
|
||||
<div className="wx-gnwCbZe9 bar">
|
||||
<div className="wx-gnwCbZe9 label">Chart cell borders</div>
|
||||
<RadioButtonGroup
|
||||
options={options}
|
||||
value={cellBorders}
|
||||
onChange={({ value }) => setCellBorders(value)}
|
||||
type="inline"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="wx-gnwCbZe9 gtcell">
|
||||
<Gantt
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={data.scales}
|
||||
cellBorders={cellBorders}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt, ContextMenu as ContextMenuComponent, Editor } from '../../src/';
|
||||
|
||||
function ContextMenu(props) {
|
||||
const { skinSettings } = props;
|
||||
const [api, setApi] = useState(null);
|
||||
const data = useMemo(() => getData(), []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextMenuComponent api={api}>
|
||||
<Gantt
|
||||
init={setApi}
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={data.scales}
|
||||
/>
|
||||
</ContextMenuComponent>
|
||||
{api && <Editor api={api} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ContextMenu;
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt, ContextMenu, Editor } from '../../src/';
|
||||
|
||||
function ContextMenuHandler(props) {
|
||||
const { skinSettings } = props;
|
||||
const [api, setApi] = useState();
|
||||
|
||||
const data = useMemo(() => getData(), []);
|
||||
|
||||
const resolver = useCallback((id) => {
|
||||
return id > 2;
|
||||
}, []);
|
||||
|
||||
const filter = useCallback((option, task) => {
|
||||
const type = task.type;
|
||||
if (option.id) {
|
||||
const ids = option.id.toString().split(':');
|
||||
if (type == 'milestone' && ids[0] == 'add-task') return ids[1] != 'child';
|
||||
}
|
||||
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextMenu api={api} resolver={resolver} filter={filter}>
|
||||
<Gantt
|
||||
init={setApi}
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={data.scales}
|
||||
/>
|
||||
</ContextMenu>
|
||||
{api && <Editor api={api} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ContextMenuHandler;
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useState, useContext, useMemo } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt, ContextMenu, Editor, defaultMenuOptions } from '../../src/';
|
||||
import { context } from '@svar-ui/react-core';
|
||||
|
||||
export default function ContextMenuOptions({ skinSettings }) {
|
||||
const helpers = useContext(context.helpers);
|
||||
|
||||
const data = useMemo(() => getData(), []);
|
||||
|
||||
const [api, setApi] = useState();
|
||||
|
||||
function actionHandler() {
|
||||
helpers.showNotice({ text: "'My action' clicked" });
|
||||
}
|
||||
|
||||
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.push({
|
||||
id: 'my-action',
|
||||
text: 'My action',
|
||||
icon: 'wxi-empty',
|
||||
handler: actionHandler,
|
||||
});
|
||||
return arr;
|
||||
});
|
||||
|
||||
function onClick({ context: ctx, action }) {
|
||||
if (!action.handler)
|
||||
helpers.showNotice({
|
||||
text: `'${action.id}' clicked for the '${ctx.id}' task`,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextMenu api={api} options={options} onClick={onClick}>
|
||||
<Gantt
|
||||
init={setApi}
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={data.scales}
|
||||
/>
|
||||
</ContextMenu>
|
||||
{api && <Editor api={api} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
.rows.wx-T6JFUSGo {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bar.wx-T6JFUSGo {
|
||||
padding: 12px;
|
||||
}
|
||||
.gtcell.wx-T6JFUSGo {
|
||||
position: relative;
|
||||
height: calc(100% - 56px);
|
||||
border-top: var(--wx-gantt-border);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useRef, useState, useCallback, useMemo } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt, ContextMenu, Editor } from '../../src/';
|
||||
import { Button } from '@svar-ui/react-core';
|
||||
import './DropDownMenu.css';
|
||||
|
||||
export default function DropDownMenu({ skinSettings }) {
|
||||
const apiRef = useRef(null);
|
||||
const [api, setApi] = useState(null);
|
||||
const menu = useRef(null);
|
||||
|
||||
const data = useMemo(() => getData(), []);
|
||||
|
||||
const resolver = useCallback(() => {
|
||||
const inst = apiRef.current;
|
||||
const selected = inst ? inst.getReactiveState().selected : null;
|
||||
const id =
|
||||
selected && selected.length ? selected[selected.length - 1] : null;
|
||||
return id ? inst.getTask(id) : null;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextMenu api={api} resolver={resolver} at="right" ref={menu} />
|
||||
|
||||
<div className="wx-T6JFUSGo rows">
|
||||
<div className="wx-T6JFUSGo bar">
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={(ev) => menu.current && menu.current.show(ev)}
|
||||
>
|
||||
Task action
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="wx-T6JFUSGo gtcell">
|
||||
<Gantt
|
||||
ref={(inst) => {
|
||||
apiRef.current = inst;
|
||||
setApi(inst);
|
||||
}}
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={data.scales}
|
||||
/>
|
||||
<Editor api={api} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Gantt, ContextMenu, Editor } from '../../src';
|
||||
|
||||
const parseDates = (data) => {
|
||||
data.forEach((item) => {
|
||||
item.start = new Date(item.start);
|
||||
if (item.end) item.end = new Date(item.end);
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
|
||||
export default function GanttBackend() {
|
||||
const server = 'https://master--svar-gantt-go--dev.webix.io';
|
||||
|
||||
const [api, setApi] = useState();
|
||||
const [tasks, setTasks] = useState([]);
|
||||
const [links, setLinks] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
fetch(server + '/tasks')
|
||||
.then((res) => res.json())
|
||||
.then((arr) => parseDates(arr)),
|
||||
fetch(server + '/links').then((res) => res.json()),
|
||||
]).then(([t, l]) => {
|
||||
setTasks(t);
|
||||
setLinks(l);
|
||||
});
|
||||
}, []);
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextMenu api={api}>
|
||||
<Gantt init={init} tasks={tasks} links={links} />
|
||||
</ContextMenu>
|
||||
{api && <Editor api={api} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { RestDataProvider } from '@svar-ui/gantt-data-provider';
|
||||
import { Gantt, ContextMenu, Editor } from '../../src';
|
||||
|
||||
export default function GanttBatchProvider() {
|
||||
const restProvider = useMemo(
|
||||
() =>
|
||||
new RestDataProvider('https://master--svar-gantt-go--dev.webix.io', {
|
||||
batchURL: 'batch',
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const [api, setApi] = useState();
|
||||
const [tasks, setTasks] = useState();
|
||||
const [links, setLinks] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
restProvider.getData().then(({ tasks: t, links: l }) => {
|
||||
setTasks(t);
|
||||
setLinks(l);
|
||||
});
|
||||
}, [restProvider]);
|
||||
|
||||
const init = useCallback(
|
||||
(api) => {
|
||||
setApi(api);
|
||||
api.setNext(restProvider);
|
||||
|
||||
api.on('request-data', (ev) => {
|
||||
restProvider.getData(ev.id).then(({ tasks, links }) => {
|
||||
api.exec('provide-data', {
|
||||
id: ev.id,
|
||||
data: {
|
||||
tasks,
|
||||
links,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
[restProvider],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextMenu api={api}>
|
||||
<Gantt init={init} tasks={tasks} links={links} />
|
||||
</ContextMenu>
|
||||
{api && <Editor api={api} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
.rows.wx-q0L4gF4x {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bar.wx-q0L4gF4x {
|
||||
padding: 10px 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.gtcell.wx-q0L4gF4x {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
border-top: var(--wx-gantt-border);
|
||||
overflow: hidden;
|
||||
}
|
||||
.label.wx-q0L4gF4x {
|
||||
padding-right: 10px;
|
||||
font-size: var(--wx-font-size);
|
||||
font-weight: var(--wx-label-font-weight);
|
||||
}
|
||||
i.wx-q0L4gF4x {
|
||||
position: relative;
|
||||
right: -5px;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt } from '../../src/';
|
||||
import { Button } from '@svar-ui/react-core';
|
||||
import './GanttCustomSort.css';
|
||||
|
||||
export default function GanttCustomSort({ skinSettings }) {
|
||||
const data = useMemo(() => getData(), []);
|
||||
|
||||
const [gApi, setGApi] = useState(null);
|
||||
const [sortConfig, setSortConfig] = useState({});
|
||||
const getIcons = useCallback(() => {
|
||||
const obj = { text: '', start: '', duration: '' };
|
||||
const { key, order } = sortConfig;
|
||||
if (key) obj[key] = `wxi-arrow-${order == 'asc' ? 'up' : 'down'}`;
|
||||
return obj;
|
||||
}, [sortConfig]);
|
||||
const [icons, setIcons] = useState(() => getIcons());
|
||||
|
||||
const sort = useCallback(
|
||||
(id) => {
|
||||
const { key, order } = sortConfig;
|
||||
let newOrder = !key ? 'desc' : 'asc';
|
||||
|
||||
if (key === id) newOrder = order === 'asc' ? 'desc' : 'asc';
|
||||
|
||||
if (gApi) gApi.exec('sort-tasks', { key: id, order: newOrder });
|
||||
},
|
||||
[gApi, sortConfig],
|
||||
);
|
||||
|
||||
const init = useCallback((api) => {
|
||||
api.on('sort-tasks', (config) => {
|
||||
setSortConfig(config);
|
||||
// Compute icons based on the incoming config to mirror Svelte's sequential state updates
|
||||
const obj = { text: '', start: '', duration: '' };
|
||||
const { key, order } = config;
|
||||
if (key) obj[key] = `wxi-arrow-${order == 'asc' ? 'up' : 'down'}`;
|
||||
setIcons(obj);
|
||||
});
|
||||
setGApi(api);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="wx-q0L4gF4x rows">
|
||||
<div className="wx-q0L4gF4x bar">
|
||||
<div className="wx-q0L4gF4x label">Sort by</div>
|
||||
<Button onClick={() => sort('text')}>
|
||||
Task Name <i className={'wx-q0L4gF4x ' + icons.text}></i>
|
||||
</Button>
|
||||
<Button onClick={() => sort('start')}>
|
||||
Start Date <i className={'wx-q0L4gF4x ' + icons.start}></i>
|
||||
</Button>
|
||||
<Button onClick={() => sort('duration')}>
|
||||
Duration <i className={'wx-q0L4gF4x ' + icons.duration}></i>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="wx-q0L4gF4x gtcell">
|
||||
<Gantt
|
||||
init={init}
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={data.scales}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
.demo.wx-6q6Giv9n {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.gtcell.wx-6q6Giv9n {
|
||||
overflow: hidden;
|
||||
border: var(--wx-gantt-border);
|
||||
height: calc(100% - 32px);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useMemo } from 'react';
|
||||
import { getData, zoomConfig } from '../data';
|
||||
import { Gantt } from '../../src/';
|
||||
import './GanttCustomZoom.css';
|
||||
|
||||
const GanttCustomZoom = ({ skinSettings }) => {
|
||||
const data = useMemo(() => getData(), []);
|
||||
|
||||
return (
|
||||
<div className="wx-6q6Giv9n demo">
|
||||
<h4>
|
||||
Point over Gantt chart, then hold Ctrl and use mouse wheel to zoom
|
||||
</h4>
|
||||
<div className="wx-6q6Giv9n gtcell">
|
||||
<Gantt
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
zoom={zoomConfig}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GanttCustomZoom;
|
||||
@@ -0,0 +1,28 @@
|
||||
.rows.wx-d4Cw5r6y {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--wx-background);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bar.wx-d4Cw5r6y {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
}
|
||||
.bar.wx-d4Cw5r6y div {
|
||||
display: flex;
|
||||
}
|
||||
.gtcell.wx-d4Cw5r6y {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
border-top: var(--wx-gantt-border);
|
||||
overflow: hidden;
|
||||
}
|
||||
.label.wx-d4Cw5r6y {
|
||||
padding-right: 20px;
|
||||
font-size: var(--wx-font-size);
|
||||
font-weight: var(--wx-label-font-weight);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt, ContextMenu, Editor, defaultEditorItems } from '../../src';
|
||||
import { RadioButtonGroup } from '@svar-ui/react-core';
|
||||
import './GanttDurationUnitChanges.css';
|
||||
|
||||
export default function GanttDurationUnitChanges({ skinSettings }) {
|
||||
const initialData = getData();
|
||||
const [tasks, setTasks] = useState(initialData.tasks);
|
||||
const [links] = useState(initialData.links);
|
||||
|
||||
const scalesMap = useMemo(
|
||||
() => ({
|
||||
hour: getData('hour').scales,
|
||||
day: getData().scales,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const options = [
|
||||
{ id: 'hour', label: 'Hour' },
|
||||
{ id: 'day', label: 'Day' },
|
||||
];
|
||||
|
||||
const [durationUnit, setDurationUnit] = useState('hour');
|
||||
const [scales, setScales] = useState(scalesMap['hour']);
|
||||
|
||||
const [api, setApi] = useState(null);
|
||||
|
||||
const items = useMemo(
|
||||
() =>
|
||||
defaultEditorItems.map((ed) => ({
|
||||
...ed,
|
||||
...(ed.comp === 'date' && {
|
||||
config: { time: durationUnit === 'hour' },
|
||||
}),
|
||||
})),
|
||||
[durationUnit],
|
||||
);
|
||||
|
||||
function handleUnitChange({ value }) {
|
||||
const sTasks = api.serialize().map((task) => {
|
||||
if (task.start && task.end) {
|
||||
const ms = 1000 * 60 * 60 * (value === 'day' ? 24 : 1);
|
||||
const duration = Math.floor((task.end - task.start) / ms);
|
||||
return { ...task, duration };
|
||||
}
|
||||
return task;
|
||||
});
|
||||
setTasks(sTasks);
|
||||
setDurationUnit(value);
|
||||
setScales(scalesMap[value]);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="wx-d4Cw5r6y rows">
|
||||
<div className="wx-d4Cw5r6y bar">
|
||||
<div className="wx-d4Cw5r6y label">Gantt duration unit</div>
|
||||
<RadioButtonGroup
|
||||
options={options}
|
||||
value={durationUnit}
|
||||
type="inline"
|
||||
onChange={handleUnitChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="wx-d4Cw5r6y gtcell">
|
||||
<ContextMenu api={api}>
|
||||
<Gantt
|
||||
init={setApi}
|
||||
{...skinSettings}
|
||||
tasks={tasks}
|
||||
links={links}
|
||||
scales={scales}
|
||||
cellWidth={40}
|
||||
durationUnit={durationUnit}
|
||||
lengthUnit={'hour'}
|
||||
/>
|
||||
</ContextMenu>
|
||||
{api && <Editor api={api} items={items} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useMemo, useState, useCallback } from 'react';
|
||||
import {
|
||||
Gantt,
|
||||
ContextMenu,
|
||||
Editor,
|
||||
defaultEditorItems,
|
||||
defaultColumns,
|
||||
} from '../../src';
|
||||
import { format } from 'date-fns';
|
||||
import { getData } from '../data';
|
||||
|
||||
export default function GanttDurationUnitHour({ skinSettings }) {
|
||||
const { tasks, links, scales } = useMemo(() => getData('hour'), []);
|
||||
|
||||
const [api, setApi] = useState(null);
|
||||
|
||||
const items = useMemo(
|
||||
() =>
|
||||
defaultEditorItems.map((ed) => ({
|
||||
...ed,
|
||||
...(ed.comp === 'date' && { config: { time: true } }),
|
||||
})),
|
||||
[],
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
defaultColumns.map((col) => ({
|
||||
...col,
|
||||
...(col.id === 'start' && {
|
||||
template: (d) => format(d, 'MMM d, HH:mm'),
|
||||
width: 120,
|
||||
}),
|
||||
})),
|
||||
[],
|
||||
);
|
||||
|
||||
const highlightTime = useCallback((date, unit) => {
|
||||
const h = date.getHours();
|
||||
if ((unit === 'hour' && h < 8) || h > 21) return 'wx-weekend';
|
||||
return '';
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextMenu api={api}>
|
||||
<Gantt
|
||||
init={setApi}
|
||||
{...(skinSettings || {})}
|
||||
tasks={tasks}
|
||||
links={links}
|
||||
columns={columns}
|
||||
scales={scales}
|
||||
cellWidth={40}
|
||||
durationUnit="hour"
|
||||
lengthUnit="minute"
|
||||
highlightTime={highlightTime}
|
||||
/>
|
||||
</ContextMenu>
|
||||
{api && <Editor api={api} items={items} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
.wx-6xDuvBVG .gantt_task_line .gantt_link_control {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.wx-6xDuvBVG .gantt_task_line .gantt_link_control .gantt_link_point {
|
||||
position: relative;
|
||||
top: auto;
|
||||
left: auto;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
margin: 0 3px;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useMemo, useState, useCallback } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt, Editor } from '../../src/';
|
||||
|
||||
function GanttEditor(props) {
|
||||
const { skinSettings } = props;
|
||||
|
||||
const data = useMemo(() => getData(), []);
|
||||
|
||||
const [api, setApi] = useState();
|
||||
|
||||
const init = useCallback((ganttApi) => {
|
||||
setApi(ganttApi);
|
||||
// show Editor on "add-task" action
|
||||
ganttApi.on('add-task', ({ id }) => {
|
||||
ganttApi.exec('show-editor', { id });
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Gantt
|
||||
init={init}
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={data.scales}
|
||||
/>
|
||||
{api && <Editor api={api} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default GanttEditor;
|
||||
@@ -0,0 +1,81 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { getData } from '../data';
|
||||
import {
|
||||
Gantt,
|
||||
Editor,
|
||||
defaultEditorItems,
|
||||
registerEditorItem,
|
||||
} from '../../src';
|
||||
import { Comments } from '@svar-ui/react-comments';
|
||||
registerEditorItem('comments', Comments);
|
||||
|
||||
function GanttEditorComments(props) {
|
||||
const { skinSettings } = props;
|
||||
|
||||
const data = useMemo(() => {
|
||||
const data = getData();
|
||||
data.tasks.forEach((d, i) => {
|
||||
d.comments = !i
|
||||
? [
|
||||
{
|
||||
id: 1,
|
||||
user: 1,
|
||||
content:
|
||||
'Greetings, fellow colleagues. I would like to share my insights on this task. I reckon we should deal with at least half of the points in the plan without further delays.',
|
||||
date: new Date(),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
user: 2,
|
||||
content:
|
||||
"Hi, Diego. I am sure that that's exactly what is thought best out there in Dunwall. Let's just do what we are supposed to do to get the result.",
|
||||
date: new Date(),
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
user: 3,
|
||||
content:
|
||||
"Absolutely, Diego. Action speaks louder than words, and in this case, it's about executing the plan efficiently. Let's prioritize tasks and tackle them head-on.",
|
||||
date: new Date(),
|
||||
},
|
||||
]
|
||||
: [];
|
||||
});
|
||||
return data;
|
||||
}, []);
|
||||
|
||||
const [api, setApi] = useState();
|
||||
const keys = useMemo(() => ['text', 'details'], []);
|
||||
const items = useMemo(() => {
|
||||
const items = defaultEditorItems.filter((op) => keys.indexOf(op.key) >= 0);
|
||||
items.push({
|
||||
key: 'comments',
|
||||
comp: 'comments',
|
||||
label: 'Comments',
|
||||
users: [
|
||||
{ id: 1, name: 'Alex' },
|
||||
{ id: 2, name: 'John' },
|
||||
{ id: 3, name: 'Bob' },
|
||||
{ id: 4, name: 'Mary' },
|
||||
{ id: 5, name: 'Kate' },
|
||||
],
|
||||
activeUser: 1,
|
||||
});
|
||||
return items;
|
||||
}, [keys]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Gantt
|
||||
init={setApi}
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={data.scales}
|
||||
/>
|
||||
{api && <Editor api={api} items={items} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default GanttEditorComments;
|
||||
@@ -0,0 +1,56 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt, Editor, defaultEditorItems } from '../../src';
|
||||
|
||||
function GanttEditorConfig({ skinSettings }) {
|
||||
const data = useMemo(() => getData(), []);
|
||||
|
||||
const [api, setApi] = useState();
|
||||
|
||||
const bottomBar = useMemo(
|
||||
() => ({
|
||||
items: [
|
||||
{ comp: 'button', type: 'secondary', text: 'Close', id: 'close' },
|
||||
{ comp: 'spacer' },
|
||||
{ comp: 'button', type: 'danger', text: 'Delete', id: 'delete' },
|
||||
{ comp: 'button', type: 'primary', text: 'Save', id: 'save' },
|
||||
],
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const keys = useMemo(
|
||||
() => ['text', 'type', 'start', 'end', 'duration', 'progress', 'details'],
|
||||
[],
|
||||
);
|
||||
|
||||
const items = useMemo(
|
||||
() =>
|
||||
keys.map((key) => ({
|
||||
...defaultEditorItems.find((op) => op.key === key),
|
||||
})),
|
||||
[keys],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Gantt
|
||||
init={setApi}
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={data.scales}
|
||||
/>
|
||||
{api && <Editor
|
||||
api={api}
|
||||
items={items}
|
||||
bottomBar={bottomBar}
|
||||
topBar={false}
|
||||
placement="modal"
|
||||
autoSave={false}
|
||||
/>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default GanttEditorConfig;
|
||||
@@ -0,0 +1,82 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Gantt,
|
||||
Editor,
|
||||
defaultEditorItems,
|
||||
registerEditorItem,
|
||||
defaultTaskTypes,
|
||||
} from '../../src';
|
||||
import { RadioButtonGroup } from '@svar-ui/react-core';
|
||||
import UsersCustomCombo from '../custom/UsersCustomCombo.jsx';
|
||||
import AvatarCell from '../custom/AvatarCell.jsx';
|
||||
import { getData, users } from '../data';
|
||||
|
||||
export default function GanttEditorCustomControls({ skinSettings }) {
|
||||
useEffect(() => {
|
||||
registerEditorItem('radio', RadioButtonGroup);
|
||||
registerEditorItem('custom-combo', UsersCustomCombo);
|
||||
}, []);
|
||||
|
||||
const items = useMemo(() => {
|
||||
const items = defaultEditorItems.map((item) => ({ ...item }));
|
||||
items.splice(
|
||||
defaultEditorItems.findIndex((d) => d.key === 'type'),
|
||||
1,
|
||||
{
|
||||
key: 'type',
|
||||
comp: 'radio',
|
||||
label: 'Type',
|
||||
options: defaultTaskTypes.map((o) => ({
|
||||
...o,
|
||||
value: o.id,
|
||||
})),
|
||||
config: {
|
||||
type: 'inline',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'assigned',
|
||||
comp: 'custom-combo',
|
||||
label: 'Assigned',
|
||||
options: users,
|
||||
},
|
||||
);
|
||||
|
||||
items.forEach((d) => {
|
||||
if (d.comp === 'date') {
|
||||
d.config = {
|
||||
time: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return items;
|
||||
}, []);
|
||||
|
||||
const data = useMemo(() => getData(), []);
|
||||
const [api, setApi] = useState();
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{ id: 'text', header: 'Task name', width: 220 },
|
||||
{ id: 'assigned', header: 'Assigned', width: 160, cell: AvatarCell },
|
||||
{ id: 'start', header: 'Start Date', width: 100 },
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Gantt
|
||||
init={setApi}
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={data.scales}
|
||||
lengthUnit="hour"
|
||||
columns={columns}
|
||||
/>
|
||||
{api && <Editor api={api} items={items} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt, Editor } from '../../src';
|
||||
|
||||
function GanttEditorReadonly({...skinSettings}) {
|
||||
const data = useMemo(() => getData(), []);
|
||||
const [api, setApi] = useState();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Gantt
|
||||
init={setApi}
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={data.scales}
|
||||
/>
|
||||
{api && <Editor api={api} readonly={true} topBar={true} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default GanttEditorReadonly;
|
||||
@@ -0,0 +1,96 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { getData } from '../data';
|
||||
import {
|
||||
Gantt,
|
||||
Editor,
|
||||
defaultEditorItems,
|
||||
registerEditorItem,
|
||||
} from '../../src';
|
||||
import { Tasklist } from '@svar-ui/react-tasklist';
|
||||
|
||||
registerEditorItem('tasks', Tasklist);
|
||||
|
||||
export default function GanttEditorTasks({ skinSettings }) {
|
||||
const data = useMemo(() => {
|
||||
const data = getData();
|
||||
data.tasks.forEach((d, i) => {
|
||||
d.tasks = !i
|
||||
? [
|
||||
{
|
||||
id: 1,
|
||||
content:
|
||||
'Research best practices for integrating third-party libraries with React',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
content:
|
||||
'Explore modern approaches to building applications using React',
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
content:
|
||||
'Explore different methods for integrating React with existing JavaScript frameworks',
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
date: new Date(),
|
||||
content: 'Learn about routing in React using React Router',
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
content:
|
||||
'Understand principles and best practices for component development in React',
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
content:
|
||||
'Explore different methods for integrating React with existing JavaScript frameworks',
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
content: 'Optimize performance in React applications',
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
content:
|
||||
'Work with API requests and data handling in React applications',
|
||||
status: 0,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
});
|
||||
return data;
|
||||
}, []);
|
||||
|
||||
const items = useMemo(() => {
|
||||
const keys = ['text', 'details'];
|
||||
const items = defaultEditorItems.filter((op) => keys.indexOf(op.key) >= 0);
|
||||
items.push({
|
||||
key: 'tasks',
|
||||
comp: 'tasks',
|
||||
label: 'Tasks',
|
||||
});
|
||||
return items;
|
||||
}, []);
|
||||
|
||||
const [api, setApi] = useState();
|
||||
return (
|
||||
<>
|
||||
<Gantt
|
||||
init={setApi}
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={data.scales}
|
||||
/>
|
||||
{api && <Editor api={api} items={items} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
.wx-22L1T8uz {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt, Editor, defaultEditorItems } from '../../src';
|
||||
|
||||
function GanttEditorValidation({ skinSettings }) {
|
||||
const data = useMemo(() => getData(), []);
|
||||
const [api, setApi] = useState();
|
||||
|
||||
const items = useMemo(
|
||||
() =>
|
||||
defaultEditorItems.map((ed) => ({
|
||||
...ed,
|
||||
...(ed.comp === 'text' && { required: true }),
|
||||
...(ed.comp === 'counter' && {
|
||||
validation: (v) => v <= 50,
|
||||
validationMessage: 'Task duration should not exceed 50 days',
|
||||
}),
|
||||
})),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Gantt
|
||||
init={setApi}
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={data.scales}
|
||||
/>
|
||||
{api && <Editor api={api} items={items} autoSave={false} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default GanttEditorValidation;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useMemo } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt } from '../../src/';
|
||||
|
||||
export default function GanttFixedColumns(props) {
|
||||
const { skinSettings } = props;
|
||||
|
||||
const data = useMemo(() => getData(), []);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{ id: 'text', header: 'Task name', width: 120 },
|
||||
{
|
||||
id: 'start',
|
||||
header: 'Start date',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
id: 'duration',
|
||||
header: 'Duration',
|
||||
width: 90,
|
||||
align: 'center',
|
||||
},
|
||||
{ id: 'add-task', header: 'Add task', width: 50, align: 'center' },
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<Gantt
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={data.scales}
|
||||
columns={columns}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useMemo } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt } from '../../src/';
|
||||
|
||||
function GanttFlexColumns({ skinSettings }) {
|
||||
const data = useMemo(() => getData(), []);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{ id: 'text', header: 'Task name', flexgrow: 2 },
|
||||
{
|
||||
id: 'start',
|
||||
header: 'Start date',
|
||||
flexgrow: 1,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
id: 'duration',
|
||||
header: 'Duration',
|
||||
align: 'center',
|
||||
flexgrow: 1,
|
||||
},
|
||||
{
|
||||
id: 'add-task',
|
||||
header: 'Add task',
|
||||
width: 50,
|
||||
align: 'center',
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<Gantt
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={data.scales}
|
||||
columns={columns}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default GanttFlexColumns;
|
||||
@@ -0,0 +1,4 @@
|
||||
.wrapper.wx-0moLF6Ul {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt, defaultTaskTypes } from '../../src/';
|
||||
import Form from '../custom/Form.jsx';
|
||||
import './GanttForm.css';
|
||||
|
||||
export default function GanttForm({ skinSettings }) {
|
||||
const data = useMemo(() => getData(), []);
|
||||
const taskTypes = defaultTaskTypes;
|
||||
|
||||
const [task, setTask] = useState();
|
||||
const [gApi, setGApi] = useState();
|
||||
|
||||
const formAction = useCallback(
|
||||
({ action, data }) => {
|
||||
switch (action) {
|
||||
case 'close-form':
|
||||
setTask(null);
|
||||
break;
|
||||
default:
|
||||
gApi.exec(action, data);
|
||||
break;
|
||||
}
|
||||
},
|
||||
[gApi],
|
||||
);
|
||||
|
||||
const init = useCallback((api) => {
|
||||
api.intercept('show-editor', ({ id }) => {
|
||||
if (id) setTask(api.getState().tasks.byId(id));
|
||||
return false;
|
||||
});
|
||||
setGApi(api);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="wx-0moLF6Ul wrapper">
|
||||
<Gantt
|
||||
init={init}
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={data.scales}
|
||||
/>
|
||||
{task ? (
|
||||
<Form task={task} taskTypes={taskTypes} onAction={formAction} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
.demo.wx-0qqHrQ85 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.gtcell.wx-0qqHrQ85 {
|
||||
overflow: hidden;
|
||||
border: var(--wx-gantt-border);
|
||||
height: calc(100% - 32px);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useMemo } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt, Fullscreen } from '../../src/';
|
||||
import './GanttFullscreen.css';
|
||||
|
||||
function GanttFullscreen({ skinSettings }) {
|
||||
const data = useMemo(() => getData(), []);
|
||||
|
||||
return (
|
||||
<div className="wx-0qqHrQ85 demo">
|
||||
<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}
|
||||
/>
|
||||
</Fullscreen>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GanttFullscreen;
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useMemo } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt } from '../../src/';
|
||||
import AvatarCell from '../custom/AvatarCell.jsx';
|
||||
import NameAndDateCell from '../custom/NameAndDateCell.jsx';
|
||||
import AddTaskCell from '../custom/AddTaskCell.jsx';
|
||||
|
||||
function GanttGrid({ skinSettings }) {
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{ id: 'text', header: 'Task', width: 220, cell: NameAndDateCell },
|
||||
{ id: 'assigned', header: 'Assigned', width: 160, cell: AvatarCell },
|
||||
{
|
||||
id: 'add-task',
|
||||
header: { cell: AddTaskCell },
|
||||
align: 'center',
|
||||
width: 80,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const data = useMemo(() => getData(), []);
|
||||
|
||||
return (
|
||||
<Gantt
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={data.scales}
|
||||
columns={columns}
|
||||
cellHeight={40}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default GanttGrid;
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useMemo } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt } from '../../src/';
|
||||
|
||||
function GanttHolidays({ skinSettings }) {
|
||||
const data = useMemo(() => getData(), []);
|
||||
|
||||
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 */ }
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
function isDayOff(date) {
|
||||
const d = date.getDay();
|
||||
return d == 0 || d == 6;
|
||||
}
|
||||
|
||||
function isHourOff(date) {
|
||||
const h = date.getHours();
|
||||
return h < 8 || h == 12 || h > 17;
|
||||
}
|
||||
|
||||
function highlightTime(d, u) {
|
||||
if (u == 'day' && isDayOff(d)) return 'wx-weekend';
|
||||
if (u == 'hour' && (isDayOff(d) || isHourOff(d))) return 'wx-weekend';
|
||||
return '';
|
||||
}
|
||||
|
||||
return (
|
||||
<Gantt
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={scales}
|
||||
highlightTime={highlightTime}
|
||||
zoom
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default GanttHolidays;
|
||||
@@ -0,0 +1,18 @@
|
||||
.demo.wx-VOqDTkHq {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.bar.wx-VOqDTkHq {
|
||||
padding: 20px;
|
||||
background-color: var(--wx-background);
|
||||
border: var(--wx-border);
|
||||
|
||||
--wx-input-width: 100px;
|
||||
}
|
||||
|
||||
.gantt.wx-VOqDTkHq {
|
||||
position: relative;
|
||||
height: calc(100% - 74px);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { getData, bigScales } from '../data';
|
||||
import { Gantt } from '../../src/';
|
||||
import { Select } from '@svar-ui/react-core';
|
||||
import './GanttLengthUnit.css';
|
||||
|
||||
function GanttLengthUnit({ skinSettings }) {
|
||||
const data = useMemo(() => getData(), []);
|
||||
|
||||
const options = [
|
||||
{ id: 'minute', label: 'Minute' },
|
||||
{ id: 'hour', label: 'Hour' },
|
||||
{ id: 'day', label: 'Day' },
|
||||
{ id: 'week', label: 'Week' },
|
||||
{ id: 'month', label: 'Month' },
|
||||
{ id: 'quarter', label: 'Quarter' },
|
||||
];
|
||||
|
||||
const [lengthUnit, setLengthUnit] = useState('day');
|
||||
|
||||
const scales = useMemo(() => {
|
||||
let scales;
|
||||
switch (lengthUnit) {
|
||||
case 'minute':
|
||||
scales = [
|
||||
{ unit: 'day', step: 1, format: 'MMM d' },
|
||||
{ unit: 'hour', step: 1, format: 'HH:mm' },
|
||||
];
|
||||
break;
|
||||
case 'hour':
|
||||
scales = [
|
||||
{ unit: 'month', step: 1, format: 'MMM' },
|
||||
{ unit: 'day', step: 1, format: 'MMM d' },
|
||||
];
|
||||
break;
|
||||
case 'day':
|
||||
scales = [
|
||||
{ unit: 'month', step: 1, format: 'MMM' },
|
||||
{ unit: 'week', step: 1, format: 'w' },
|
||||
];
|
||||
break;
|
||||
case 'week':
|
||||
scales = [
|
||||
{ unit: 'year', step: 1, format: 'yyyy' },
|
||||
{ unit: 'month', step: 1, format: 'MMM' },
|
||||
];
|
||||
break;
|
||||
case 'month':
|
||||
scales = [
|
||||
{ unit: 'year', step: 1, format: 'yyyy' },
|
||||
{ unit: 'quarter', step: 1, format: 'QQQ' },
|
||||
];
|
||||
break;
|
||||
case 'quarter':
|
||||
scales = [{ unit: 'year', step: 1, format: 'yyyy' }];
|
||||
break;
|
||||
default:
|
||||
scales = bigScales;
|
||||
}
|
||||
return scales;
|
||||
}, [lengthUnit]);
|
||||
|
||||
return (
|
||||
<div className="wx-VOqDTkHq demo">
|
||||
<div className="wx-VOqDTkHq bar">
|
||||
<Select
|
||||
value={lengthUnit}
|
||||
options={options}
|
||||
onChange={({ value }) => setLengthUnit(value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="wx-VOqDTkHq gantt">
|
||||
<Gantt
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={scales}
|
||||
lengthUnit={lengthUnit}
|
||||
cellWidth={300}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GanttLengthUnit;
|
||||
@@ -0,0 +1,18 @@
|
||||
.rows.wx-ycv5Oz8L {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bar.wx-ycv5Oz8L {
|
||||
padding: 12px;
|
||||
border-bottom: var(--wx-gantt-border);
|
||||
}
|
||||
.gtcell.wx-ycv5Oz8L {
|
||||
position: relative;
|
||||
height: calc(100% - 106px);
|
||||
border-top: var(--wx-gantt-border);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt, ContextMenu, Toolbar, Editor } from '../../src/';
|
||||
import { Segmented, Locale } from '@svar-ui/react-core';
|
||||
import { cn } from '@svar-ui/gantt-locales';
|
||||
import { cn as cnCore } from '@svar-ui/core-locales';
|
||||
import './GanttLocale.css';
|
||||
|
||||
function GanttLocale({ skinSettings }) {
|
||||
const langs = [
|
||||
{ id: 'en', label: 'EN' },
|
||||
{ id: 'cn', label: 'CN' },
|
||||
];
|
||||
const [lang, setLang] = useState('en');
|
||||
|
||||
return (
|
||||
<div className="wx-ycv5Oz8L rows">
|
||||
<div className="wx-ycv5Oz8L bar">
|
||||
<Segmented
|
||||
options={langs}
|
||||
value={lang}
|
||||
onChange={({ value }) => setLang(value)}
|
||||
/>
|
||||
</div>
|
||||
{lang === 'en' && (
|
||||
<GanttWidget
|
||||
skinSettings={skinSettings}
|
||||
/>
|
||||
)}
|
||||
{lang === 'cn' && (
|
||||
<Locale words={{ ...cn, ...cnCore }}>
|
||||
<GanttWidget skinSettings={skinSettings} />
|
||||
</Locale>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GanttWidget(props) {
|
||||
const { skinSettings } = props;
|
||||
|
||||
const [api, setApi] = useState(null);
|
||||
const data = useMemo(() => getData(), []);
|
||||
|
||||
|
||||
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}
|
||||
/>
|
||||
</ContextMenu>
|
||||
{api && <Editor api={api} />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default GanttLocale;
|
||||
@@ -0,0 +1,18 @@
|
||||
.demo.wx-megyPaP4 {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.bar.wx-megyPaP4 {
|
||||
padding: 20px;
|
||||
background-color: var(--wx-background);
|
||||
border: var(--wx-border);
|
||||
|
||||
--wx-input-width: 180px;
|
||||
}
|
||||
|
||||
.gantt.wx-megyPaP4 {
|
||||
position: relative;
|
||||
height: calc(100% - 74px);
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt, registerScaleUnit } from '../../src/';
|
||||
import { Select } from '@svar-ui/react-core';
|
||||
import {
|
||||
startOfMonth,
|
||||
endOfMonth,
|
||||
isSameMonth,
|
||||
addMonths,
|
||||
addDays,
|
||||
format,
|
||||
differenceInDays,
|
||||
} 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' },
|
||||
];
|
||||
|
||||
export default function GanttMinScaleUnit({ skinSettings }) {
|
||||
const data = useMemo(() => getData(), []);
|
||||
|
||||
const getMidDate = (d) => {
|
||||
const m = d.getMonth();
|
||||
return m === 1 ? 15 : 16;
|
||||
};
|
||||
|
||||
const sprintStart = (d) => {
|
||||
const monthStart = startOfMonth(d);
|
||||
const midDate = getMidDate(d);
|
||||
if (d.getDate() >= midDate) monthStart.setDate(midDate);
|
||||
return monthStart;
|
||||
};
|
||||
|
||||
const sprintEnd = (d) => {
|
||||
const monthEnd = endOfMonth(d);
|
||||
const midDate = getMidDate(d);
|
||||
if (d.getDate() < midDate) monthEnd.setDate(midDate - 1);
|
||||
return monthEnd;
|
||||
};
|
||||
|
||||
const sprintFormat = (d) => {
|
||||
const monthStr = format(d, 'MMMM');
|
||||
const start = d.getDate();
|
||||
const end = sprintEnd(d).getDate();
|
||||
return `${monthStr} ${start} - ${end}`;
|
||||
};
|
||||
|
||||
const allScales = useMemo(
|
||||
() => [
|
||||
{ unit: 'month', step: 1, format: 'MMMM yyy' },
|
||||
{ unit: 'sprint', step: 1, format: sprintFormat },
|
||||
{ unit: 'week', step: 1, format: 'w' },
|
||||
{ unit: 'day', step: 1, format: 'd' },
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const [scaleOption, setScaleOption] = useState(2);
|
||||
|
||||
const scales = useMemo(() => {
|
||||
if (scaleOption == 1) return [allScales[1]];
|
||||
if (scaleOption == 2) return allScales.slice(0, 2);
|
||||
if (scaleOption == 3) return allScales.slice(0, 3);
|
||||
return allScales;
|
||||
}, [scaleOption, allScales]);
|
||||
|
||||
const registeredRef = useRef(false);
|
||||
if (!registeredRef.current) {
|
||||
registerScaleUnit('sprint', {
|
||||
start: sprintStart,
|
||||
end: sprintEnd,
|
||||
isSame: (a, b) => {
|
||||
if (!a || !b) return true;
|
||||
const sameMonth = isSameMonth(a, b);
|
||||
if (!sameMonth) return false;
|
||||
const midDate = getMidDate(a);
|
||||
return a.getDate() < midDate == b.getDate() < midDate;
|
||||
},
|
||||
add: (d, amount) => {
|
||||
const date = d.getDate();
|
||||
const start = sprintStart(d);
|
||||
const diff = date - start.getDate();
|
||||
let newDate = addMonths(start, Math.floor(amount / 2));
|
||||
const midDate = getMidDate(newDate);
|
||||
if (amount % 2) {
|
||||
newDate = addDays(newDate, midDate);
|
||||
newDate = sprintStart(newDate);
|
||||
}
|
||||
return addDays(newDate, diff);
|
||||
},
|
||||
diff: (endDate, startDate) => {
|
||||
return Math.floor(differenceInDays(endDate, startDate) / 15);
|
||||
},
|
||||
smallerCount: {
|
||||
day: (d) => {
|
||||
if (!d) return 15;
|
||||
const start = sprintStart(d).getDate();
|
||||
const end = sprintEnd(d).getDate();
|
||||
return end - start + 1;
|
||||
},
|
||||
},
|
||||
biggerCount: {
|
||||
year: 24,
|
||||
quarter: 6,
|
||||
month: 2,
|
||||
},
|
||||
});
|
||||
registeredRef.current = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="wx-megyPaP4 demo">
|
||||
<div className="wx-megyPaP4 bar">
|
||||
<Select
|
||||
value={scaleOption}
|
||||
options={options}
|
||||
onChange={({ value }) => setScaleOption(value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="wx-megyPaP4 gantt">
|
||||
<Gantt
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={scales}
|
||||
zoom={true}
|
||||
start={new Date(2024, 3, 1)}
|
||||
end={new Date(2024, 5, 1)}
|
||||
cellWidth={60}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
.rows.wx-NlISAKUQ {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.row.wx-NlISAKUQ {
|
||||
padding: 13px;
|
||||
}
|
||||
|
||||
.ganttCell.wx-NlISAKUQ {
|
||||
height: 410px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.ganttHeader.wx-NlISAKUQ {
|
||||
padding: 13px;
|
||||
border-bottom: var(--wx-gantt-border);
|
||||
}
|
||||
|
||||
.ganttBox.wx-NlISAKUQ {
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt } from '../../src/';
|
||||
import { Button } from '@svar-ui/react-core';
|
||||
import './GanttMultiple.css';
|
||||
|
||||
function GanttMultiple(props) {
|
||||
const { skinSettings } = props;
|
||||
|
||||
const counterRef = useRef(0);
|
||||
const [gantts, setGantts] = useState(() => {
|
||||
const initial = [];
|
||||
initial.push({
|
||||
id: counterRef.current,
|
||||
data: getData(),
|
||||
});
|
||||
counterRef.current += 1;
|
||||
initial.push({
|
||||
id: counterRef.current,
|
||||
data: getData(),
|
||||
});
|
||||
counterRef.current += 1;
|
||||
return initial;
|
||||
});
|
||||
|
||||
function addGantt() {
|
||||
setGantts((prev) => {
|
||||
const next = [
|
||||
...prev,
|
||||
{
|
||||
id: counterRef.current,
|
||||
data: getData(),
|
||||
},
|
||||
];
|
||||
counterRef.current += 1;
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
function removeGantt(id) {
|
||||
setGantts((prev) => prev.filter((a) => a.id !== id));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="wx-NlISAKUQ rows">
|
||||
<div className="wx-NlISAKUQ row">
|
||||
<Button type="primary" onClick={addGantt}>
|
||||
Add Gantt
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{gantts.map((gantt, id) => (
|
||||
<div className="wx-NlISAKUQ ganttCell" key={id}>
|
||||
<div className="wx-NlISAKUQ ganttHeader">
|
||||
<Button type="secondary" onClick={() => removeGantt(gantt.id)}>
|
||||
Delete Gantt
|
||||
</Button>
|
||||
</div>
|
||||
<div className="wx-NlISAKUQ ganttBox">
|
||||
<Gantt
|
||||
{...skinSettings}
|
||||
tasks={gantt.data.tasks}
|
||||
links={gantt.data.links}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GanttMultiple;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useMemo } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt } from '../../src/';
|
||||
|
||||
function GanttNoGrid(props) {
|
||||
const { skinSettings } = props;
|
||||
|
||||
const data = useMemo(() => getData(), []);
|
||||
|
||||
return (
|
||||
<Gantt
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={data.scales}
|
||||
columns={false}
|
||||
cellWidth={60}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default GanttNoGrid;
|
||||
@@ -0,0 +1,26 @@
|
||||
.rows.wx-KB3Eoqwm {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.row.wx-KB3Eoqwm {
|
||||
padding: 13px;
|
||||
font-family: var(--wx-font-family);
|
||||
font-size: var(--wx-font-size);
|
||||
}
|
||||
|
||||
.gtcell.wx-KB3Eoqwm {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
border-top: var(--wx-gantt-border);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.gtcell.wx-KB3Eoqwm:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useState, useEffect, useMemo, useRef } from 'react';
|
||||
import { getGeneratedData, complexScales } from '../data';
|
||||
import { Gantt } from '../../src/';
|
||||
import { Button } from '@svar-ui/react-core';
|
||||
import './GanttPerformance.css';
|
||||
|
||||
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
|
||||
</>
|
||||
) : (
|
||||
<Button type="primary" onClick={() => setStart(new Date())}>
|
||||
Press me to render Gantt chart with 1 000 tasks
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{start ? (
|
||||
<div className="wx-KB3Eoqwm gtcell">
|
||||
<Gantt
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={complexScales}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GanttPerformance;
|
||||
@@ -0,0 +1,38 @@
|
||||
.rows.wx-RPSbwjNq {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.bar.wx-RPSbwjNq {
|
||||
display: flex;
|
||||
height: 60px;
|
||||
align-items: end;
|
||||
font-family: var(--wx-font-family);
|
||||
font-size: var(--wx-font-size);
|
||||
padding-top: 12px;
|
||||
--wx-label-width: 130px;
|
||||
}
|
||||
|
||||
.bar.wx-RPSbwjNq:first-child {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.gantt.wx-RPSbwjNq {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
border-top: var(--wx-gantt-border);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gantt.wx-RPSbwjNq.hide-progress > .wx-gantt .wx-bar .wx-progress-marker {
|
||||
display: none;
|
||||
}
|
||||
.gantt.wx-RPSbwjNq.hide-links > .wx-gantt .wx-bar .wx-link {
|
||||
display: none;
|
||||
}
|
||||
.gantt.wx-RPSbwjNq.hide-drag > .wx-gantt .wx-bar {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { useState, useEffect, useMemo, useRef, useCallback } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt, defaultColumns, Editor } 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 [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 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]);
|
||||
|
||||
useEffect(() => {
|
||||
orderRef.current = order;
|
||||
}, [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
|
||||
});
|
||||
}, []);
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
edit ? defaultColumns : defaultColumns.filter((a) => a.id != 'add-task'),
|
||||
[edit],
|
||||
);
|
||||
|
||||
// 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>
|
||||
<Field label="Creating links" position={'left'}>
|
||||
{({ id }) => (
|
||||
<Switch
|
||||
value={newLink}
|
||||
onChange={({ value }) => setNewLink(value)}
|
||||
id={id}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<Field label="Dragging tasks" position={'left'}>
|
||||
{({ id }) => (
|
||||
<Switch
|
||||
value={drag}
|
||||
onChange={({ value }) => setDrag(value)}
|
||||
id={id}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<Field label="Reordering tasks" position={'left'}>
|
||||
{({ id }) => (
|
||||
<Switch
|
||||
value={order}
|
||||
onChange={({ value }) => setOrder(value)}
|
||||
id={id}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
'wx-RPSbwjNq ' +
|
||||
('gantt' +
|
||||
(!edit ? ' hide-progress' : '') +
|
||||
(!newLink ? ' hide-links' : '') +
|
||||
(!drag ? ' hide-drag' : ''))
|
||||
}
|
||||
>
|
||||
<Gantt
|
||||
init={init}
|
||||
{...(skinSettings || {})}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={data.scales}
|
||||
columns={columns}
|
||||
/>
|
||||
{api && <Editor api={api} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GanttPreventActions;
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { RestDataProvider } from '@svar-ui/gantt-data-provider';
|
||||
import { Gantt, ContextMenu, Editor } from '../../src';
|
||||
|
||||
export default function GanttProvider() {
|
||||
const restProvider = useMemo(
|
||||
() => new RestDataProvider('https://master--svar-gantt-go--dev.webix.io'),
|
||||
[],
|
||||
);
|
||||
|
||||
const [api, setApi] = useState();
|
||||
const [tasks, setTasks] = useState();
|
||||
const [links, setLinks] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
restProvider.getData().then(({ tasks: t, links: l }) => {
|
||||
setTasks(t);
|
||||
setLinks(l);
|
||||
});
|
||||
}, [restProvider]);
|
||||
|
||||
const init = useCallback(
|
||||
(api) => {
|
||||
setApi(api);
|
||||
|
||||
api.setNext(restProvider);
|
||||
|
||||
api.on('request-data', (ev) => {
|
||||
restProvider.getData(ev.id).then(({ tasks, links }) => {
|
||||
api.exec('provide-data', {
|
||||
id: ev.id,
|
||||
data: {
|
||||
tasks,
|
||||
links,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
[restProvider],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextMenu api={api}>
|
||||
<Gantt init={init} tasks={tasks} links={links} />
|
||||
</ContextMenu>
|
||||
{api && <Editor api={api} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useMemo } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt } from '../../src/';
|
||||
|
||||
export default function GanttReadOnly({ skinSettings }) {
|
||||
const data = useMemo(() => getData(), []);
|
||||
|
||||
return (
|
||||
<Gantt
|
||||
readonly={true}
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={data.scales}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt, registerScaleUnit } from '../../src/';
|
||||
import {
|
||||
startOfMonth,
|
||||
endOfMonth,
|
||||
isSameMonth,
|
||||
addMonths,
|
||||
addDays,
|
||||
format,
|
||||
} from 'date-fns';
|
||||
|
||||
export default function GanttScaleUnit(props) {
|
||||
const { skinSettings } = props;
|
||||
|
||||
const data = useMemo(() => getData(), []);
|
||||
|
||||
const getMidDate = (d) => {
|
||||
const m = d.getMonth();
|
||||
return m === 1 ? 15 : 16;
|
||||
};
|
||||
|
||||
const sprintStart = (d) => {
|
||||
const monthStart = startOfMonth(d);
|
||||
const midDate = getMidDate(d);
|
||||
if (d.getDate() >= midDate) monthStart.setDate(midDate);
|
||||
return monthStart;
|
||||
};
|
||||
|
||||
const sprintEnd = (d) => {
|
||||
const monthEnd = endOfMonth(d);
|
||||
const midDate = getMidDate(d);
|
||||
if (d.getDate() < midDate) monthEnd.setDate(midDate - 1);
|
||||
return monthEnd;
|
||||
};
|
||||
|
||||
const sprintFormat = (d) => {
|
||||
const monthStr = format(d, 'MMMM');
|
||||
const start = d.getDate();
|
||||
const end = sprintEnd(d).getDate();
|
||||
return `${monthStr} ${start} - ${end}`;
|
||||
};
|
||||
|
||||
const registeredRef = useRef(false);
|
||||
if (!registeredRef.current) {
|
||||
registerScaleUnit('sprint', {
|
||||
start: sprintStart,
|
||||
end: sprintEnd,
|
||||
isSame: (a, b) => {
|
||||
const sameMonth = isSameMonth(a, b);
|
||||
if (!sameMonth) return false;
|
||||
const midDate = getMidDate(a);
|
||||
return a.getDate() < midDate == b.getDate() < midDate;
|
||||
},
|
||||
add: (d, amount) => {
|
||||
const date = d.getDate();
|
||||
const start = sprintStart(d);
|
||||
const diff = date - start.getDate();
|
||||
let newDate = addMonths(start, Math.floor(amount / 2));
|
||||
const midDate = getMidDate(newDate);
|
||||
if (amount % 2) {
|
||||
newDate = addDays(newDate, midDate);
|
||||
newDate = sprintStart(newDate);
|
||||
}
|
||||
return addDays(newDate, diff);
|
||||
},
|
||||
});
|
||||
registeredRef.current = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<Gantt
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={[
|
||||
{ unit: 'month', step: 1, format: 'MMMM yyy' },
|
||||
{ unit: 'sprint', step: 1, format: sprintFormat },
|
||||
{ unit: 'day', step: 1, format: 'd' },
|
||||
]}
|
||||
zoom={true}
|
||||
start={new Date(2024, 3, 1)}
|
||||
end={new Date(2024, 5, 1)}
|
||||
cellWidth={60}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useMemo } from 'react';
|
||||
import { getData, complexScales } from '../data';
|
||||
import { Gantt } from '../../src/';
|
||||
|
||||
function GanttScales({ skinSettings }) {
|
||||
const data = useMemo(() => getData(), []);
|
||||
|
||||
return (
|
||||
<Gantt
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={complexScales}
|
||||
start={new Date(2024, 3, 1)}
|
||||
end={new Date(2024, 4, 12)}
|
||||
cellWidth={60}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default GanttScales;
|
||||
@@ -0,0 +1,19 @@
|
||||
.rows.wx-MZ5YXrqJ {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--wx-background);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bar.wx-MZ5YXrqJ {
|
||||
padding: 10px;
|
||||
}
|
||||
.gtcell.wx-MZ5YXrqJ {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
border-top: var(--wx-gantt-border);
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { getData, complexScales } from '../data';
|
||||
import { Gantt } from '../../src/';
|
||||
import { Slider } from '@svar-ui/react-core';
|
||||
import './GanttSizes.css';
|
||||
|
||||
function GanttSizes({ skinSettings }) {
|
||||
const data = useMemo(() => getData(), []);
|
||||
const [cellWidth, setCellWidth] = useState(100);
|
||||
const [scaleHeight, setScaleHeight] = useState(38);
|
||||
const [cellHeight, setCellHeight] = useState(36);
|
||||
|
||||
return (
|
||||
<div className="wx-MZ5YXrqJ rows">
|
||||
<div className="wx-MZ5YXrqJ bar">
|
||||
<Slider
|
||||
label="Cell width"
|
||||
value={cellWidth}
|
||||
onChange={({ value }) => setCellWidth(value)}
|
||||
min={20}
|
||||
max={200}
|
||||
/>
|
||||
<Slider
|
||||
label="Cell height"
|
||||
value={cellHeight}
|
||||
onChange={({ value }) => setCellHeight(value)}
|
||||
min={20}
|
||||
max={60}
|
||||
/>
|
||||
<Slider
|
||||
label="Scale height"
|
||||
value={scaleHeight}
|
||||
onChange={({ value }) => setScaleHeight(value)}
|
||||
min={20}
|
||||
max={60}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="wx-MZ5YXrqJ gtcell">
|
||||
<Gantt
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={complexScales}
|
||||
cellWidth={cellWidth}
|
||||
cellHeight={cellHeight}
|
||||
scaleHeight={scaleHeight}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GanttSizes;
|
||||
@@ -0,0 +1,12 @@
|
||||
.demo.wx-fr1PMpDW {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.gtcell.wx-fr1PMpDW {
|
||||
overflow: hidden;
|
||||
border: var(--wx-gantt-border);
|
||||
height: calc(100% - 32px);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useMemo } from 'react';
|
||||
import { getData, zoomConfig } from '../data';
|
||||
import { Gantt } from '../../src/';
|
||||
import './GanttSort.css';
|
||||
|
||||
function GanttSort({ skinSettings }) {
|
||||
const data = useMemo(() => getData(), []);
|
||||
|
||||
function init(api) {
|
||||
api.intercept('sort-tasks', (config) => {
|
||||
return config.key == 'text';
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="wx-fr1PMpDW demo">
|
||||
<h4>Sorting by the "Task Name" column only</h4>
|
||||
<div className="wx-fr1PMpDW gtcell">
|
||||
<Gantt
|
||||
init={init}
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
zoom={zoomConfig}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GanttSort;
|
||||
@@ -0,0 +1,21 @@
|
||||
.demo.wx-FJQN2sNt {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.bar.wx-FJQN2sNt {
|
||||
display: flex;
|
||||
padding: 12px;
|
||||
border-bottom: var(--wx-gantt-border);
|
||||
}
|
||||
|
||||
.gantt.wx-FJQN2sNt {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.input.wx-FJQN2sNt {
|
||||
margin: 4px;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt } from '../../src/';
|
||||
import { DatePicker, Field, Locale, Switch } from '@svar-ui/react-core';
|
||||
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 [autoScale, setAutoScale] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="wx-FJQN2sNt demo">
|
||||
<Locale>
|
||||
<div className="wx-FJQN2sNt bar">
|
||||
<Field label="Start" position="left">
|
||||
{({ id }) => (
|
||||
<DatePicker
|
||||
value={start}
|
||||
id={id}
|
||||
onChange={({ value }) => setStart(value)}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<Field label="End" position="left">
|
||||
{({ id }) => (
|
||||
<DatePicker
|
||||
value={end}
|
||||
id={id}
|
||||
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>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
</Locale>
|
||||
|
||||
<div className="wx-FJQN2sNt gantt">
|
||||
<Gantt
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={data.scales}
|
||||
autoScale={autoScale}
|
||||
zoom
|
||||
start={start}
|
||||
end={end}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { useMemo, useCallback, useRef } from 'react';
|
||||
import { useStoreLater } from '@svar-ui/lib-react';
|
||||
import { getData } from '../data';
|
||||
import {
|
||||
Gantt,
|
||||
ContextMenu,
|
||||
Editor,
|
||||
defaultEditorItems,
|
||||
defaultTaskTypes,
|
||||
} from '../../src/';
|
||||
|
||||
export default function GanttSummariesConvert({ 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 defaultEditorItems.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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt, ContextMenu, Editor } from '../../src/';
|
||||
|
||||
export default function GanttSummariesNoDrag({ skinSettings }) {
|
||||
const data = useMemo(() => getData(), []);
|
||||
|
||||
const [api, setApi] = useState();
|
||||
|
||||
const init = useCallback((api) => {
|
||||
setApi(api);
|
||||
api.intercept('drag-task', ({ id, top }) => {
|
||||
return !(typeof top === 'undefined' && api.getTask(id).type == 'summary');
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="wx-h4Sohvpn gt-cell">
|
||||
<ContextMenu api={api}>
|
||||
<Gantt
|
||||
init={init}
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={data.scales}
|
||||
/>
|
||||
</ContextMenu>
|
||||
{api && <Editor api={api} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.gt-cell > .wx-gantt .wx-summary .wx-progress-marker {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt, Editor, defaultEditorItems, ContextMenu } from '../../src';
|
||||
import './GanttSummariesProgress.css';
|
||||
|
||||
function GanttSummariesProgress({ skinSettings }) {
|
||||
const data = useMemo(() => getData(), []);
|
||||
const tasks = data.tasks;
|
||||
|
||||
const [api, setApi] = useState();
|
||||
|
||||
const [items, setItems] = useState(defaultEditorItems);
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
// disabling progress slider for summary tasks
|
||||
api.on('show-editor', ({ id }) => {
|
||||
if (id) {
|
||||
const type = api.getTask(id).type;
|
||||
setItems(
|
||||
defaultEditorItems.map((ed) => ({
|
||||
...ed,
|
||||
...(ed.key == 'progress' && {
|
||||
config: { disabled: type === 'summary' },
|
||||
}),
|
||||
})),
|
||||
);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!api) return;
|
||||
api.getState().tasks.forEach((task) => {
|
||||
recalcSummaryProgress(task.id, true);
|
||||
});
|
||||
}, [api]);
|
||||
|
||||
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 && <Editor api={api} items={items} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GanttSummariesProgress;
|
||||
@@ -0,0 +1,55 @@
|
||||
.demo.wx-I1glfWSB {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
.demo.wx-I1glfWSB > .wx-gantt .wx-bar.wx-task.narrow {
|
||||
background-color: #676a81;
|
||||
color: transparent;
|
||||
height: 10px !important;
|
||||
margin-top: 10px;
|
||||
border: 1px solid #63667a;
|
||||
}
|
||||
.demo.wx-I1glfWSB > .wx-gantt .wx-bar.wx-task.narrow .wx-progress-percent {
|
||||
background-color: #1a2630;
|
||||
}
|
||||
.demo.wx-I1glfWSB > .wx-gantt .wx-bar.wx-task.narrow .wx-link {
|
||||
background-color: #384047;
|
||||
border-radius: 0px;
|
||||
}
|
||||
.demo.wx-I1glfWSB > .wx-gantt .wx-bar.wx-task.narrow .wx-link .wx-inner {
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
.demo.wx-I1glfWSB > .wx-gantt .wx-bar.wx-task.round {
|
||||
background-color: #ffeb3b;
|
||||
border: 1px solid #ffeb3b;
|
||||
color: #384047;
|
||||
border-radius: 50px;
|
||||
}
|
||||
.demo.wx-I1glfWSB > .wx-gantt .wx-bar.wx-task.round .wx-progress-percent {
|
||||
background-color: #ffc107;
|
||||
}
|
||||
.demo.wx-I1glfWSB > .wx-gantt .wx-bar.wx-task.round .wx-progress-wrapper {
|
||||
border-radius: 50px;
|
||||
}
|
||||
|
||||
.demo.wx-I1glfWSB > .wx-gantt .wx-bar.wx-task.progress {
|
||||
background-color: transparent;
|
||||
color: var(--wx-color-font);
|
||||
border-radius: 50px;
|
||||
border: 1px solid #00bcd4;
|
||||
}
|
||||
.demo.wx-I1glfWSB > .wx-gantt .wx-bar.wx-task.progress .wx-progress-percent {
|
||||
background-color: #00bcd4;
|
||||
}
|
||||
.demo.wx-I1glfWSB > .wx-gantt .wx-bar.wx-task.progress .wx-progress-wrapper {
|
||||
border-radius: 50px;
|
||||
}
|
||||
|
||||
.demo.wx-I1glfWSB > .wx-gantt .wx-bar.wx-task.urgent {
|
||||
background-color: #f49a82;
|
||||
border: 1px solid #f45e36;
|
||||
}
|
||||
.demo.wx-I1glfWSB > .wx-gantt .wx-bar.wx-task.urgent .wx-progress-percent {
|
||||
background-color: #f45e36;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { getTypedData, taskTypes } from '../data';
|
||||
import { Gantt, Editor } from '../../src/';
|
||||
import './GanttTaskTypes.css';
|
||||
|
||||
function GanttTaskTypes(props) {
|
||||
const { skinSettings } = props;
|
||||
|
||||
const data = useMemo(() => getTypedData(), []);
|
||||
const [api, setApi] = useState(null);
|
||||
|
||||
return (
|
||||
<div className="wx-I1glfWSB demo">
|
||||
<Gantt
|
||||
init={setApi}
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={data.scales}
|
||||
taskTypes={taskTypes}
|
||||
/>
|
||||
{api && <Editor api={api} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GanttTaskTypes;
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useMemo, useRef, useCallback } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt } from '../../src/';
|
||||
import MyTaskContent from '../custom/MyTaskContent.jsx';
|
||||
|
||||
export default function GanttText({ skinSettings }) {
|
||||
const data = useMemo(() => getData(), []);
|
||||
const api = useRef(null);
|
||||
|
||||
const doClick = useCallback((ev) => {
|
||||
api.current.exec('update-task', {
|
||||
id: ev.id,
|
||||
task: {
|
||||
clicked: ev.clicked,
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Gantt
|
||||
{...skinSettings}
|
||||
ref={api}
|
||||
onCustomClick={doClick}
|
||||
taskTemplate={MyTaskContent}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={data.scales}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
.gtcell.wx-2rSWAdWv {
|
||||
height: calc(100% - 50px);
|
||||
border-top: var(--wx-gantt-border);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt, Toolbar, Editor } from '../../src/';
|
||||
import './GanttToolbar.css';
|
||||
|
||||
export default function GanttToolbar(props) {
|
||||
const { skinSettings } = props;
|
||||
|
||||
const data = useMemo(() => getData(), []);
|
||||
|
||||
const [api, setApi] = useState();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toolbar api={api} />
|
||||
<div className="wx-2rSWAdWv gtcell">
|
||||
<Gantt
|
||||
{...skinSettings}
|
||||
init={setApi}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={data.scales}
|
||||
/>
|
||||
{api && <Editor api={api} />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
.gtcell.wx-vkht5Uhi {
|
||||
height: calc(100% - 50px);
|
||||
border-top: var(--wx-gantt-border);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
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 './GanttToolbarButtons.css';
|
||||
|
||||
export default function GanttToolbarButtons({ skinSettings }) {
|
||||
const helpers = useContext(context.helpers);
|
||||
const [api, setApi] = useState();
|
||||
|
||||
const data = useMemo(() => getData(), []);
|
||||
|
||||
const actionHandler = useCallback(() => {
|
||||
helpers.showNotice({ text: "'My action' clicked" });
|
||||
}, [helpers]);
|
||||
|
||||
const items = useMemo(() => {
|
||||
const items = defaultToolbarButtons.filter((b) => {
|
||||
return b.id?.indexOf('indent') === -1;
|
||||
});
|
||||
|
||||
items.push({
|
||||
id: 'my-action',
|
||||
comp: 'icon',
|
||||
icon: 'wxi-cat',
|
||||
handler: actionHandler,
|
||||
});
|
||||
|
||||
return items;
|
||||
}, [actionHandler]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toolbar api={api} items={items} />
|
||||
<div className="wx-vkht5Uhi gtcell">
|
||||
<Gantt
|
||||
{...skinSettings}
|
||||
init={setApi}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={data.scales}
|
||||
/>
|
||||
{api && <Editor api={api} />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
.gtcell.wx-7sTOb4gt {
|
||||
height: calc(100% - 50px);
|
||||
border-top: var(--wx-gantt-border);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt, Editor } from '../../src/';
|
||||
import { Toolbar } from '@svar-ui/react-toolbar';
|
||||
import { useStoreLater } from '@svar-ui/lib-react';
|
||||
import './GanttToolbarCustom.css';
|
||||
|
||||
export default function GanttToolbarCustom({ skinSettings }) {
|
||||
const data = useMemo(() => getData(), []);
|
||||
|
||||
const [api, setApi] = useState(null);
|
||||
const selectedValue = useStoreLater(api, "selected");
|
||||
|
||||
function handleAdd() {
|
||||
if (!api) return;
|
||||
api.exec('add-task', {
|
||||
task: {
|
||||
text: 'New task',
|
||||
},
|
||||
target: selectedValue[0],
|
||||
mode: 'after',
|
||||
});
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
if (!api) return;
|
||||
const order = getActionOrder(true);
|
||||
order.forEach((id) => api.exec('delete-task', { id }));
|
||||
}
|
||||
|
||||
function handleMove(mode) {
|
||||
if (!api) return;
|
||||
const changeDir = mode === 'down';
|
||||
const order = getActionOrder(changeDir);
|
||||
order.forEach((id) => api.exec('move-task', { id, mode }));
|
||||
}
|
||||
|
||||
function getActionOrder(changeDir) {
|
||||
if (!api) return [];
|
||||
const tasks = selectedValue
|
||||
.map((id) => api.getTask(id))
|
||||
.sort((a, b) => {
|
||||
return a.$level - b.$level || a.$y - b.$y;
|
||||
});
|
||||
const idOrder = tasks.map((o) => o.id);
|
||||
if (changeDir) return idOrder.reverse();
|
||||
return idOrder;
|
||||
}
|
||||
|
||||
const allItems = [
|
||||
{
|
||||
comp: 'button',
|
||||
type: 'primary',
|
||||
text: 'Add task',
|
||||
handler: handleAdd,
|
||||
},
|
||||
{
|
||||
comp: 'button',
|
||||
text: 'Delete task',
|
||||
handler: handleDelete,
|
||||
},
|
||||
{
|
||||
comp: 'button',
|
||||
type: 'primary',
|
||||
text: 'Move task down',
|
||||
handler: () => handleMove('down'),
|
||||
},
|
||||
{
|
||||
comp: 'button',
|
||||
type: 'primary',
|
||||
text: 'Move task up',
|
||||
handler: () => handleMove('up'),
|
||||
},
|
||||
];
|
||||
|
||||
const items = useMemo(() => {
|
||||
if (api && selectedValue) {
|
||||
return selectedValue.length ? allItems : [allItems[0]];
|
||||
}
|
||||
return [allItems[0]];
|
||||
}, [api, selectedValue, allItems]);
|
||||
|
||||
const GanttInitialised = useMemo(() => {
|
||||
return <Gantt
|
||||
{...(skinSettings || {})}
|
||||
init={setApi}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={data.scales}
|
||||
/>
|
||||
}, [skinSettings, data.tasks, data.links, data.scales]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toolbar items={items} />
|
||||
<div className="wx-7sTOb4gt gtcell">
|
||||
{GanttInitialised}
|
||||
{api && <Editor api={api} />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt, Tooltip } from '../../src/';
|
||||
import MyTooltipContent from '../custom/MyTooltipContent.jsx';
|
||||
|
||||
function GanttTooltips({ skinSettings }) {
|
||||
const data = useMemo(() => getData(), []);
|
||||
const [api, setApi] = useState(null);
|
||||
|
||||
return (
|
||||
<Tooltip api={api} content={MyTooltipContent}>
|
||||
<Gantt
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={data.scales}
|
||||
init={setApi}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export default GanttTooltips;
|
||||
@@ -0,0 +1,12 @@
|
||||
.demo.wx-HQBKHlAu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.gtcell.wx-HQBKHlAu {
|
||||
overflow: hidden;
|
||||
border: var(--wx-gantt-border);
|
||||
height: calc(100% - 32px);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useMemo, useCallback } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt } from '../../src/';
|
||||
import './GanttZoom.css';
|
||||
|
||||
function GanttZoom({ skinSettings }) {
|
||||
const data = useMemo(() => getData(), []);
|
||||
|
||||
const init = useCallback((api) => {
|
||||
api.on('zoom-scale', () => {
|
||||
console.log('The current zoom level is', api.getState().zoom);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="wx-HQBKHlAu demo">
|
||||
<h4>
|
||||
Point over Gantt chart, then hold Ctrl and use mouse wheel to zoom
|
||||
</h4>
|
||||
<div className="wx-HQBKHlAu gtcell">
|
||||
<Gantt
|
||||
init={init}
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
cellWidth={100}
|
||||
zoom
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GanttZoom;
|
||||
@@ -0,0 +1,27 @@
|
||||
.rows.wx-DZzpn0qn {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--wx-background);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bar.wx-DZzpn0qn {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.gtcell.wx-DZzpn0qn {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
border-top: var(--wx-gantt-border);
|
||||
overflow: hidden;
|
||||
}
|
||||
.label.wx-DZzpn0qn {
|
||||
padding-right: 20px;
|
||||
font-size: var(--wx-font-size);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt, HeaderMenu } from '../../src/';
|
||||
import { RadioButtonGroup } from '@svar-ui/react-core';
|
||||
import './GridHeaderMenu.css';
|
||||
|
||||
function GridHeaderMenu({ skinSettings }) {
|
||||
const data = useMemo(() => getData(), []);
|
||||
|
||||
const [api, setApi] = useState(null);
|
||||
|
||||
const [selected, setSelected] = useState('all');
|
||||
const options = [
|
||||
{ id: 'all', label: 'All' },
|
||||
{ id: 'some', label: 'Some' },
|
||||
];
|
||||
const hidable = { start: true, duration: true };
|
||||
|
||||
const columns = useMemo(
|
||||
() => (selected === 'some' ? hidable : null),
|
||||
[selected],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="wx-DZzpn0qn rows">
|
||||
<div className="wx-DZzpn0qn bar">
|
||||
<div>Right-click the grid header and select visible columns</div>
|
||||
<div className="wx-DZzpn0qn bar">
|
||||
<div className="wx-DZzpn0qn label">Columns that can be hidden:</div>
|
||||
<RadioButtonGroup
|
||||
options={options}
|
||||
value={selected}
|
||||
onChange={({ value }) => setSelected(value)}
|
||||
type="inline"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="wx-DZzpn0qn gtcell">
|
||||
<HeaderMenu api={api} columns={columns}>
|
||||
<Gantt
|
||||
init={setApi}
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={data.scales}
|
||||
/>
|
||||
</HeaderMenu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GridHeaderMenu;
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useMemo } from 'react';
|
||||
import { getData } from '../data';
|
||||
import { Gantt } from '../../src/';
|
||||
|
||||
function GridInlineEditors(props) {
|
||||
const { skinSettings } = props;
|
||||
|
||||
const data = useMemo(() => getData(), []);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'text',
|
||||
header: 'Task name',
|
||||
width: 170,
|
||||
sort: true,
|
||||
editor: 'text',
|
||||
},
|
||||
{
|
||||
id: 'start',
|
||||
header: 'Start date',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
sort: true,
|
||||
editor: 'datepicker',
|
||||
},
|
||||
{
|
||||
id: 'duration',
|
||||
header: 'Duration',
|
||||
width: 80,
|
||||
sort: true,
|
||||
align: 'center',
|
||||
editor: 'text',
|
||||
},
|
||||
{ id: 'add-task', header: 'Add task', width: 50, align: 'center' },
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<Gantt
|
||||
{...skinSettings}
|
||||
tasks={data.tasks}
|
||||
links={data.links}
|
||||
scales={data.scales}
|
||||
columns={columns}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default GridInlineEditors;
|
||||
Reference in New Issue
Block a user