This commit is contained in:
Marta Kowalska
2025-10-22 08:13:59 +00:00
commit 80ca059fb8
162 changed files with 16816 additions and 0 deletions

27
.gitignore vendored Normal file
View File

@@ -0,0 +1,27 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
yarn.lock
node_modules
dist
dist-demos
dist-full
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
.husky/pre-commit Normal file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
npx lint-staged

7
.prettierrc.cjs Normal file
View File

@@ -0,0 +1,7 @@
module.exports = {
singleQuote: true,
semi: true,
useTabs: false,
tabWidth: 2,
trailingComma: 'all',
};

1
.sync Normal file
View File

@@ -0,0 +1 @@
0d06fa37fa91f51a38b15b4dac095537d366a653

View File

@@ -0,0 +1,3 @@
<svg width="18" height="17" viewBox="0 0 18 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.99935 0.666748C7.905 0.666748 6.82137 0.882296 5.81032 1.30109C4.79927 1.71987 3.88061 2.3337 3.10679 3.10752C1.54399 4.67033 0.666016 6.78994 0.666016 9.00008C0.666016 12.6834 3.05768 15.8084 6.36602 16.9167C6.78268 16.9834 6.91602 16.7251 6.91602 16.5001C6.91602 16.3084 6.91602 15.7834 6.91602 15.0917C4.60768 15.5917 4.11602 13.9751 4.11602 13.9751C3.73268 13.0084 3.19102 12.7501 3.19102 12.7501C2.43268 12.2334 3.24935 12.2501 3.24935 12.2501C4.08268 12.3084 4.52435 13.1084 4.52435 13.1084C5.24935 14.3751 6.47435 14.0001 6.94935 13.8001C7.02435 13.2584 7.24102 12.8917 7.47435 12.6834C5.62435 12.4751 3.68268 11.7584 3.68268 8.58342C3.68268 7.65841 3.99935 6.91675 4.54102 6.32508C4.45768 6.11675 4.16602 5.25008 4.62435 4.12508C4.62435 4.12508 5.32435 3.90008 6.91602 4.97508C7.57435 4.79175 8.29102 4.70008 8.99935 4.70008C9.70768 4.70008 10.4243 4.79175 11.0827 4.97508C12.6743 3.90008 13.3743 4.12508 13.3743 4.12508C13.8327 5.25008 13.541 6.11675 13.4577 6.32508C13.9993 6.91675 14.316 7.65841 14.316 8.58342C14.316 11.7667 12.366 12.4667 10.5077 12.6751C10.8077 12.9334 11.0827 13.4417 11.0827 14.2167C11.0827 15.3334 11.0827 16.2334 11.0827 16.5001C11.0827 16.7251 11.216 16.9917 11.641 16.9167C14.9493 15.8001 17.3327 12.6834 17.3327 9.00008C17.3327 7.90573 17.1171 6.8221 16.6983 5.81105C16.2796 4.80001 15.6657 3.88135 14.8919 3.10752C14.1181 2.3337 13.1994 1.71987 12.1884 1.30109C11.1773 0.882296 10.0937 0.666748 8.99935 0.666748Z" fill="#42454D"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,9 @@
<svg width="26" height="18" viewBox="0 0 26 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.06631 1.11863e-06L10.5114 2.3209e-06L5.44169 9.00172L-1.9482e-06 9.00172L5.06631 1.11863e-06Z" fill="#087A9F"/>
<path d="M0 9.00122L5.44169 9.00122L7.78709 13.1683L5.06631 17.9994L0 9.00122Z" fill="#087A9F"/>
<path d="M0 9.00122L5.44169 9.00122L7.78709 13.1683L5.06631 17.9994L0 9.00122Z" fill="white" fill-opacity="0.4"/>
<path d="M5.06738 18L15.2034 2.2291e-06L17.9242 4.83461L10.5125 18L5.06738 18Z" fill="#087A9F"/>
<path d="M20.6439 1.19674e-06L25.7136 9.00173L20.2684 9.00172L15.2021 0L20.6439 1.19674e-06Z" fill="#087A9F"/>
<path d="M20.6439 1.19674e-06L25.7136 9.00173L20.2684 9.00172L15.2021 0L20.6439 1.19674e-06Z" fill="white" fill-opacity="0.4"/>
<path d="M20.6438 17.9994L15.2021 17.9994L20.2684 9.00122L25.7135 9.00122L20.6438 17.9994Z" fill="#087A9F"/>
</svg>

After

Width:  |  Height:  |  Size: 883 B

View File

@@ -0,0 +1,2 @@
export { default as GitHubLogoIcon } from './GitHubLogo.svg';
export { default as LogoIcon } from './Logo.svg';

16
demos/cases/BasicInit.jsx Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
.wx-22L1T8uz {
width: 100%;
height: 100%;
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
.wrapper.wx-0moLF6Ul {
height: 100%;
overflow: hidden;
}

50
demos/cases/GanttForm.jsx Normal file
View File

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

View File

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

View File

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

37
demos/cases/GanttGrid.jsx Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

12
demos/cases/GanttSort.css Normal file
View File

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

31
demos/cases/GanttSort.jsx Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
.gt-cell > .wx-gantt .wx-summary .wx-progress-marker {
display: none;
}

View File

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

View File

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

View File

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

30
demos/cases/GanttText.jsx Normal file
View File

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

View File

@@ -0,0 +1,4 @@
.gtcell.wx-2rSWAdWv {
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, 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>
</>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

12
demos/cases/GanttZoom.css Normal file
View File

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

34
demos/cases/GanttZoom.jsx Normal file
View File

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

View File

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

View File

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

View File

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

232
demos/common/Index.css Normal file
View File

@@ -0,0 +1,232 @@
.wx-demos.layout {
box-sizing: border-box;
display: flex;
height: 100%;
width: 100%;
}
.wx-demos.page-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: auto;
}
.wx-demos.page-content-header {
height: 70px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 40px;
padding: 16px;
border-bottom: 1px solid #ebebeb;
}
.wx-demos.header-title-box {
display: flex;
align-items: center;
gap: 16px;
}
.wx-demos.header-actions-container {
display: flex;
align-items: center;
gap: 16px;
}
.wx-demos.layout.active {
flex-direction: row;
}
.wx-demos.sidebar {
width: 0;
height: 100%;
overflow: hidden;
transition: width 0.3s;
}
.wx-demos.sidebar-header {
display: flex;
justify-content: space-between;
position: sticky;
top: 0px;
background-color: #fbfbfb;
padding: 19px 16px 19px 18px;
}
.wx-demos.box-title {
display: flex;
align-items: center;
gap: 12px;
}
.wx-demos.box-title img:hover,
.wx-demos.box-title img:focus {
opacity: 0.6;
}
.wx-demos.box-title img:active {
opacity: 0.8;
}
.wx-demos.box-title .wx-demos.separator {
width: 1px;
height: 20px;
background: #ebebeb;
}
.wx-demos.sidebar.active {
width: 300px;
}
.wx-demos.sidebar-content {
display: flex;
flex-direction: column;
width: 300px;
gap: 16px;
height: 100%;
border-right: 1px solid #ebebeb;
overflow-y: auto;
font-family: Roboto, Arial, Helvetica, sans-serif;
font-size: 16px;
line-height: 20px;
background-color: #fbfbfb;
border-bottom: 1px solid #ebebeb;
}
.wx-demos.btn-box button.toggle-btn {
display: flex;
gap: 8px;
border: 1px solid #ebebeb;
color: #42454d;
}
.wx-demos.btn-box button.toggle-btn:hover,
.wx-demos.btn-box button.toggle-btn:focus {
border: 1px solid #ebebeb;
color: #42454d;
background: #f7f7f7;
}
.wx-demos.btn-box button.toggle-btn:active {
background: #f1f1f1;
}
.wx-demos a {
display: flex;
text-decoration: none;
}
.wx-demos.wrapper-content {
flex: 1;
height: calc(100% - 70px);
}
.wx-demos.wrapper-content .wx-demos.content {
width: 100%;
height: 100%;
transition:
transform 0.3s,
width 0.3s;
overflow-y: auto;
}
.wx-demos.box-links {
display: flex;
flex-direction: column;
gap: 6px;
}
.wx-demos.hint {
font-size: 16px;
font-weight: 500;
line-height: 24px;
color: #42454d;
}
.wx-demos.title {
margin: 0;
font-size: 18px;
font-weight: 500;
line-height: 24px;
color: #42454d;
}
.wx-demos.segmented-box div.segmented-themes {
padding: 2px;
border-radius: 4px;
}
.wx-demos.segmented-box div.segmented-themes button {
font-size: 14px;
font-weight: 400;
line-height: 18px;
color: #595b66;
background-color: transparent;
}
.wx-demos.segmented-box div.segmented-themes button.wx-selected {
border-radius: 2px;
font-weight: 500;
color: #42454d;
background: #fff;
box-shadow: 0px 0px 7px 0px rgba(66, 69, 76, 0.07);
}
.wx-demos.content h4 {
font-size: 16px;
font-weight: 300;
margin-bottom: 12px;
border-bottom: var(--wx-border);
}
.wx-demos.content h3 {
font-size: 18px;
margin: 12px 0;
font-weight: normal;
}
.wx-demos.content .demo-box {
margin: 20px;
}
.wx-demos.content .demo-box + .demo-box {
margin-top: 40px;
}
.wx-demos.content .demo-code {
font-family: monospace;
line-height: 30px;
}
.wx-demos.content .demo-status {
height: 30px;
line-height: 30px;
background: rgba(0, 0, 0, 0.15);
padding-left: 10px;
}
.wx-demos.content .demo-toolbar {
border: 2px solid var(--wx-background-alt);
max-width: 600px;
}
.wx-demos.demo {
display: flex;
align-items: center;
height: 37px;
font-weight: 400;
padding-left: 16px;
border-left: 4px solid transparent;
color: #595b66;
list-style: none;
cursor: pointer;
text-decoration: none;
}
.wx-demos.demo.active {
border-left-color: #087a9f;
}
.wx-demos.demo.active,
.wx-demos.demo:hover {
font-weight: 500;
color: #42454d;
background-color: #f1f1f1;
}

183
demos/common/Index.jsx Normal file
View File

@@ -0,0 +1,183 @@
import { useState, useEffect, useCallback } from 'react';
import { HashRouter, NavLink, useNavigate } from 'react-router-dom';
import Router from './Router';
import { links } from '../routes';
import { GitHubLogoIcon, LogoIcon } from '../assets/icons';
import './Index.css';
function DemoExplorerContent({
productTag,
publicName,
skins,
Globals,
Button,
Segmented,
}) {
const navigate = useNavigate();
const [skin, setSkin] = useState(skins[0].id);
const [title, setTitle] = useState('');
const [githubLink, setGithubLink] = useState('');
const [show, setShow] = useState(false);
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];
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);
const parts = currentPath.split('/');
if (parts[1]) {
navigate(`/${parts[1]}/${value}`);
}
};
const toggleSidebar = () => {
setShow(!show);
};
return (
<div className={`wx-demos layout ${show ? 'active' : ''}`}>
<div
className={`wx-demos sidebar ${show ? 'active' : ''}`}
role="tabpanel"
>
<div className="wx-demos sidebar-content">
<div className="wx-demos sidebar-header">
<div className="wx-demos box-title">
<a
href="https://svar.dev/react/"
target="_blank"
rel="noopener noreferrer"
>
<img src={LogoIcon} alt="Logo icon" className="box-title-img" />
</a>
<div className="wx-demos separator"></div>
<a
href={`https://svar.dev/react/${productTag}/`}
target="_blank"
rel="noopener noreferrer"
>
<h1 className="wx-demos title">React {publicName}</h1>
</a>
</div>
<div className="wx-demos btn-box">
<Button
type="secondary"
icon="wxi-angle-left"
css="toggle-btn"
onClick={toggleSidebar}
></Button>
</div>
</div>
<div className="wx-demos box-links">
{links.map((data) => (
<NavLink
key={data[0]}
to={data[0].replace(':skin', skin)}
className={({ isActive }) =>
`wx-demos demo ${isActive ? 'active' : ''}`
}
>
{data[1]}
</NavLink>
))}
</div>
</div>
</div>
<div className="wx-demos page-content">
<div className="wx-demos page-content-header">
<div className="wx-demos header-title-box">
{!show && (
<div className="wx-demos btn-box">
<Button
type="secondary"
icon="wxi-angle-right"
css="toggle-btn"
onClick={toggleSidebar}
/>
</div>
)}
<div className="wx-demos hint">{title}</div>
</div>
<div className="wx-demos header-actions-container">
<div className="wx-demos segmented-box">
<Segmented
value={skin}
options={skins}
css="segmented-themes"
onChange={handleSkinChange}
/>
</div>
<div className="wx-demos btn-box">
<a href={githubLink} target="_blank" rel="noopener noreferrer">
<Button type="secondary" css="toggle-btn">
<img src={GitHubLogoIcon} alt="GitHub icon" />
See code on GitHub
</Button>
</a>
</div>
</div>
</div>
<div
className="wx-demos wrapper-content"
onClick={() => setShow(false)}
role="none"
>
<div
className={`wx-demos content wx-${skin}-theme`}
role="none"
data-wx-portal-root="true"
>
<Globals>
<Router skin={skin} onRouteChange={handleRouteChange} />
</Globals>
</div>
</div>
</div>
</div>
);
}
export default function DemoExplorer(props) {
const skins = props.skins;
return (
<>
{skins.map((skin) => (
<skin.Component key={skin.id} />
))}
<HashRouter>
<DemoExplorerContent {...props} />
</HashRouter>
</>
);
}

31
demos/common/Router.jsx Normal file
View File

@@ -0,0 +1,31 @@
import { useEffect } from 'react';
import {
Routes,
Route,
Navigate,
useLocation,
useNavigate,
} from 'react-router-dom';
import { links } from '../routes';
export default function Router({ onRouteChange }) {
const location = useLocation();
const navigate = useNavigate();
useEffect(() => {
if (location.pathname === '/') {
navigate('/base/willow', { replace: true });
} else {
onRouteChange(location.pathname);
}
}, [location.pathname, onRouteChange, navigate]);
return (
<Routes>
<Route path="/" element={<Navigate to="/base/willow" replace />} />
{links.map(([path, , Component]) => (
<Route key={path} path={path} element={<Component />} />
))}
</Routes>
);
}

View File

@@ -0,0 +1,9 @@
.text.wx-4RuF7aAO {
display: flex;
align-items: center;
cursor: pointer;
}
.text.wx-4RuF7aAO:hover {
text-decoration: underline;
color: var(--wx-color-link);
}

View File

@@ -0,0 +1,15 @@
import { useContext, useMemo } from 'react';
import { context } from '@svar-ui/react-core';
import './AddTaskCell.css';
export default function AddTaskCell() {
const i18n = useContext(context.i18n);
const _ = useMemo(() => i18n.getGroup('gantt'), []);
const text = useMemo(() => _('Add task'), []);
return (
<div className="wx-4RuF7aAO text" data-action="add-task">
{text}
</div>
);
}

View File

@@ -0,0 +1,16 @@
import { useMemo } from 'react';
import UserStub from './UserStub.jsx';
import { users } from '../data';
function AvatarCell(props) {
const { row } = props;
const user = useMemo(
() => users.find((u) => u.id == row.assigned),
[row.assigned, users],
);
return <UserStub user={user} />;
}
export default AvatarCell;

66
demos/custom/Form.css Normal file
View File

@@ -0,0 +1,66 @@
.backdrop.wx-QkE5vh0y {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
z-index: 5;
background: var(--wx-modal-backdrop);
}
.modal.wx-QkE5vh0y {
position: relative;
width: 340px;
padding: 20px;
border-radius: 6px;
box-shadow:
0 4px 4px rgba(0, 0, 0, 0.12),
0 0 10px rgba(0, 0, 0, 0.06);
background-color: var(--wx-background);
font-family: var(--wx-font-family);
font-size: var(--wx-font-size);
color: var(--wx-color-font);
}
.title.wx-QkE5vh0y {
margin: 0;
}
.close.wx-QkE5vh0y {
position: absolute;
top: 20px;
right: 20px;
cursor: pointer;
font-weight: 700;
transition: color 0.15s ease-in;
}
.close.wx-QkE5vh0y:hover {
color: rgb(255, 122, 122);
}
.body.wx-QkE5vh0y {
margin: 20px 0 0 0;
}
.button.wx-QkE5vh0y {
padding: 10px;
margin: 1.5em 0 0 0;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 2px;
font-family: var(--wx-font-family);
font-size: var(--wx-font-size);
border-radius: 3px;
cursor: pointer;
}
.button.wx-QkE5vh0y:focus {
outline: none;
opacity: 0.7;
}
.danger.wx-QkE5vh0y {
color: var(--wx-color-danger-font);
background-color: var(--wx-color-danger);
}

141
demos/custom/Form.jsx Normal file
View File

@@ -0,0 +1,141 @@
import { useEffect, useRef, useState } from 'react';
import {
Field,
Text,
TextArea,
Select,
Slider,
DatePicker,
} from '@svar-ui/react-core';
import { useWritableProp } from '@svar-ui/lib-react';
import './Form.css';
export default function Form(props) {
const [task, setTask] = useWritableProp(props.task);
const { taskTypes, onAction } = props;
const nodeRef = useRef(null);
const [left, setLeft] = useState();
const [top, setTop] = useState();
useEffect(() => {
if (nodeRef.current) {
setLeft((window.innerWidth - nodeRef.current.offsetWidth) / 2);
setTop((window.innerHeight - nodeRef.current.offsetHeight) / 2);
}
}, []);
function deleteTask() {
onAction && onAction({ action: 'delete-task', data: { id: task.id } });
onAction && onAction({ action: 'close-form' });
}
function onClose() {
onAction && onAction({ action: 'close-form' });
}
function handleChange({ value }, key) {
let t = task;
if (key === 'type' && value === 'milestone') {
delete t.end;
t.duration = 0;
} else if (t.start > t.end) {
t.start = t.end;
t.duration = 1;
t.end = 0;
}
t = {
...t,
[key]: value,
};
setTask(t);
onAction &&
onAction({
action: 'update-task',
data: { id: t.id, task: t },
});
}
return (
<div className="wx-QkE5vh0y backdrop">
<div className="wx-QkE5vh0y modal" style={{ left, top }} ref={nodeRef}>
<div className="wx-QkE5vh0y header">
<h3 className="wx-QkE5vh0y title">Edit task</h3>
<i className="wx-QkE5vh0y close wxi-close" onClick={onClose}></i>
</div>
<div className="wx-QkE5vh0y body">
<Field label="Name">
{({ id }) => (
<Text
id={id}
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')}
/>
)}
</Field>
{taskTypes.length > 1 ? (
<Field label="Type">
{({ id }) => (
<Select
id={id}
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')}
/>
)}
</Field>
{task.type !== 'milestone' ? (
<>
<Field label="End date">
{({ id }) => (
<DatePicker
id={id}
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')}
/>
)}
</Field>
</>
) : null}
<button className="wx-QkE5vh0y button danger" onClick={deleteTask}>
Delete
</button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,23 @@
button.wx-BzRGIq8x {
font-size: 10px;
position: relative;
z-index: 2;
font: var(--wx-gantt-bar-font);
}
.text-right.wx-BzRGIq8x {
left: 100%;
top: -2px;
}
.text-left.wx-BzRGIq8x {
right: 100%;
top: -2px;
}
.text-right.wx-BzRGIq8x,
.text-left.wx-BzRGIq8x,
button.wx-BzRGIq8x {
padding: 0 2px;
font-size: 12px;
}

View File

@@ -0,0 +1,35 @@
import './MyTaskContent.css';
function MyTaskContent({ data, onAction }) {
function doClick(ev) {
ev.stopPropagation();
onAction({
action: 'custom-click',
data: {
clicked: !data.clicked,
id: data.id,
},
});
}
return (
<>
{data.type !== 'milestone' ? (
<>
<div className="wx-BzRGIq8x wx-text-out text-right">
{data.text || ''}
</div>
<button className="wx-BzRGIq8x" onClick={doClick}>
{data.clicked ? 'Was clicked' : 'Click Me'}
</button>
</>
) : (
<div className="wx-BzRGIq8x wx-text-out text-left">
{data.text || ''}
</div>
)}
</>
);
}
export default MyTaskContent;

View File

@@ -0,0 +1,21 @@
.data.wx-SfydHtKO {
white-space: nowrap;
background-color: var(--wx-tooltip-background);
padding: 3px 8px;
}
.text.wx-SfydHtKO {
font-family: var(--wx-font-family);
color: var(--wx-color-primary-font);
font-size: 13px;
text-transform: capitalize;
margin-bottom: 5px;
}
.text:last-child.wx-SfydHtKO {
margin-bottom: 0;
}
.caption.wx-SfydHtKO {
font-weight: 700;
}

View File

@@ -0,0 +1,32 @@
import { format } from 'date-fns';
import './MyTooltipContent.css';
function MyTooltipContent(props) {
const { data } = props;
const mask = 'yyyy.MM.dd';
return (
<>
{data ? (
<div className="wx-SfydHtKO data">
<div className="wx-SfydHtKO text">
<span className="wx-SfydHtKO caption">{data.type}: </span>
{data.text}
</div>
<div className="wx-SfydHtKO text">
<span className="wx-SfydHtKO caption">start: </span>
{format(data.start, mask)}
</div>
{data.end ? (
<div className="wx-SfydHtKO text">
<span className="wx-SfydHtKO caption">end: </span>
{format(data.end, mask)}
</div>
) : null}
</div>
) : null}
</>
);
}
export default MyTooltipContent;

View File

@@ -0,0 +1,10 @@
.text.wx-4qJ64Gcp,
.date.wx-4qJ64Gcp {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.date.wx-4qJ64Gcp {
color: #5f5f5f;
font-size: 12px;
}

View File

@@ -0,0 +1,13 @@
import { useMemo } from 'react';
import './NameAndDateCell.css';
export default function NameAndDateCell({ row }) {
const date = useMemo(() => row.start.toLocaleDateString(), [row.start]);
return (
<>
<div className="wx-4qJ64Gcp text">{row.text}</div>
<div className="wx-4qJ64Gcp date">{date}</div>
</>
);
}

25
demos/custom/UserStub.css Normal file
View File

@@ -0,0 +1,25 @@
.container.wx-Md6hA5Em {
display: flex;
align-items: center;
padding: 0 4px;
}
.avatar.wx-Md6hA5Em {
width: 40px;
}
.user-avatar.wx-Md6hA5Em {
width: 30px;
height: 30px;
border-radius: 50%;
background-color: #dfe2e6;
text-align: center;
}
.user-photo.wx-Md6hA5Em {
display: block;
width: 100%;
height: 100%;
border-radius: 50%;
background-color: #dfe2e6;
}

27
demos/custom/UserStub.jsx Normal file
View File

@@ -0,0 +1,27 @@
import { useMemo } from 'react';
import './UserStub.css';
function UserStub(props) {
const { user } = props;
const url = 'https://svar.dev/demos/grid/assets/avatars/';
const avatar = useMemo(
() => (user ? `${url}${user.label.replace(' ', '_')}.png` : ''),
[user],
);
return (
<div className="wx-Md6hA5Em container">
<div className="wx-Md6hA5Em avatar">
{avatar ? (
<div className="wx-Md6hA5Em user-avatar">
<img className="wx-Md6hA5Em user-photo" alt="" src={avatar} />
</div>
) : null}
</div>
<div>{user?.label ?? ''}</div>
</div>
);
}
export default UserStub;

View File

@@ -0,0 +1,20 @@
import { Combo } from '@svar-ui/react-core';
import UserStub from './UserStub.jsx';
function UsersCustomCombo(props) {
const { value, options, onChange } = props;
return (
<Combo
options={options}
value={value}
onChange={onChange}
clear
placeholder="Assign to the person"
>
{({ option }) => <UserStub user={option} />}
</Combo>
);
}
export default UsersCustomCombo;

Some files were not shown because too many files have changed in this diff Show More