v2.3.1
This commit is contained in:
+27
@@ -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?
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
npx lint-staged
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
singleQuote: true,
|
||||
semi: true,
|
||||
useTabs: false,
|
||||
tabWidth: 2,
|
||||
trailingComma: 'all',
|
||||
};
|
||||
@@ -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 |
@@ -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 |
@@ -0,0 +1,2 @@
|
||||
export { default as GitHubLogoIcon } from './GitHubLogo.svg';
|
||||
export { default as LogoIcon } from './Logo.svg';
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
Reference in New Issue
Block a user