v2.4.3
This commit is contained in:
@@ -8,34 +8,27 @@ import {
|
||||
useImperativeHandle,
|
||||
} from 'react';
|
||||
import { ContextMenu as WxContextMenu } from '@svar-ui/react-menu';
|
||||
import {
|
||||
handleAction,
|
||||
defaultMenuOptions,
|
||||
isHandledAction,
|
||||
} from '@svar-ui/gantt-store';
|
||||
import { locale } from '@svar-ui/lib-dom';
|
||||
import { handleAction, getMenuOptions, isHandledAction } from '@svar-ui/gantt-store';
|
||||
import { locale, locateID } from '@svar-ui/lib-dom';
|
||||
import { en } from '@svar-ui/gantt-locales';
|
||||
import { en as coreEn } from '@svar-ui/core-locales';
|
||||
import { context } from '@svar-ui/react-core';
|
||||
import { useWritableProp, useStoreLater } from '@svar-ui/lib-react';
|
||||
import { useStoreLater } from '@svar-ui/lib-react';
|
||||
import './ContextMenu.css';
|
||||
|
||||
const ContextMenu = forwardRef(function ContextMenu(
|
||||
{
|
||||
options: optionsInit,
|
||||
options: optionsInit = [],
|
||||
api = null,
|
||||
resolver = null,
|
||||
filter = null,
|
||||
at = 'point',
|
||||
children,
|
||||
onClick,
|
||||
css
|
||||
css,
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
const ownMenu = useMemo(() => optionsInit ?? [...defaultMenuOptions], [optionsInit]);
|
||||
const [optionsProp] = useWritableProp(ownMenu);
|
||||
|
||||
const menuRef = useRef(null);
|
||||
const activeIdRef = useRef(null);
|
||||
|
||||
@@ -44,10 +37,12 @@ const ContextMenu = forwardRef(function ContextMenu(
|
||||
const l = useMemo(() => i18nCtx || locale({ ...en, ...coreEn }), [i18nCtx]);
|
||||
const _ = useMemo(() => l.getGroup('gantt'), [l]);
|
||||
|
||||
const rTaskTypesVal = useStoreLater(api, "taskTypes");
|
||||
const rTasksVal = useStoreLater(api, "_tasks");
|
||||
const rSelectedVal = useStoreLater(api, "selected");
|
||||
const rSelectedTasksVal = useStoreLater(api, "_selected");
|
||||
const taskTypesVal = useStoreLater(api, 'taskTypes');
|
||||
const selectedVal = useStoreLater(api, 'selected');
|
||||
const selectedTasksVal = useStoreLater(api, '_selected');
|
||||
const splitTasksVal = useStoreLater(api, 'splitTasks');
|
||||
|
||||
const fullOptions = useMemo(() => getMenuOptions({ splitTasks: true }), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!api) return;
|
||||
@@ -71,27 +66,26 @@ const ContextMenu = forwardRef(function ContextMenu(
|
||||
}
|
||||
|
||||
function getOptions() {
|
||||
const convertOption = optionsProp.find((o) => o.id === 'convert-task');
|
||||
const finalOptions = optionsInit.length
|
||||
? optionsInit
|
||||
: getMenuOptions({ splitTasks: splitTasksVal });
|
||||
const convertOption = finalOptions.find((o) => o.id === 'convert-task');
|
||||
if (convertOption) {
|
||||
convertOption.data = [];
|
||||
(rTaskTypesVal || []).forEach((t) => {
|
||||
(taskTypesVal || []).forEach((t) => {
|
||||
convertOption.data.push(convertOption.dataFactory(t));
|
||||
});
|
||||
}
|
||||
return applyLocaleFn(optionsProp);
|
||||
return applyLocaleFn(finalOptions);
|
||||
}
|
||||
|
||||
const cOptions = useMemo(() => {
|
||||
if (api) {
|
||||
return getOptions();
|
||||
}
|
||||
return null;
|
||||
}, [api, optionsProp, rTaskTypesVal, _]);
|
||||
return getOptions();
|
||||
}, [api, optionsInit, taskTypesVal, splitTasksVal, _]);
|
||||
|
||||
const selectedTasks = useMemo(
|
||||
() =>
|
||||
rSelectedTasksVal && rSelectedTasksVal.length ? rSelectedTasksVal : [],
|
||||
[rSelectedTasksVal],
|
||||
() => (selectedTasksVal && selectedTasksVal.length ? selectedTasksVal : []),
|
||||
[selectedTasksVal],
|
||||
);
|
||||
|
||||
const itemResolver = useCallback(
|
||||
@@ -102,40 +96,55 @@ const ContextMenu = forwardRef(function ContextMenu(
|
||||
task = result === true ? task : result;
|
||||
}
|
||||
if (task) {
|
||||
activeIdRef.current = task.id;
|
||||
if (!Array.isArray(rSelectedVal) || !rSelectedVal.includes(task.id)) {
|
||||
const segmentIndex = locateID(ev.target, 'data-segment');
|
||||
if (segmentIndex !== null)
|
||||
activeIdRef.current = { id: task.id, segmentIndex };
|
||||
else activeIdRef.current = task.id;
|
||||
|
||||
if (!Array.isArray(selectedVal) || !selectedVal.includes(task.id)) {
|
||||
api && api.exec && api.exec('select-task', { id: task.id });
|
||||
}
|
||||
}
|
||||
return task;
|
||||
},
|
||||
[api, resolver, rSelectedVal],
|
||||
[api, resolver, selectedVal],
|
||||
);
|
||||
|
||||
const menuAction = useCallback(
|
||||
(ev) => {
|
||||
const action = ev.action;
|
||||
if (action) {
|
||||
const isAction = isHandledAction(defaultMenuOptions, action.id);
|
||||
const isAction = isHandledAction(fullOptions, action.id);
|
||||
if (isAction) handleAction(api, action.id, activeIdRef.current, _);
|
||||
onClick && onClick(ev);
|
||||
}
|
||||
},
|
||||
[api, _, onClick],
|
||||
[api, _, onClick, fullOptions],
|
||||
);
|
||||
|
||||
const filterMenu = useCallback(
|
||||
(item, task) => {
|
||||
// for single selection from resolver _selected are empty
|
||||
// due to setAsyncState causing _selected to lag
|
||||
const tasks = selectedTasks.length ? selectedTasks : task ? [task] : [];
|
||||
|
||||
let result = filter ? filter(item, task) : true;
|
||||
if (item.check && result) {
|
||||
const isDisabled = tasks.some((t) => !item.check(t, rTasksVal));
|
||||
item.css = isDisabled ? 'wx-disabled' : '';
|
||||
let result = filter ? tasks.every((t) => filter(item, t)) : true;
|
||||
|
||||
if (result) {
|
||||
if (item.isHidden)
|
||||
result = !tasks.some((t) =>
|
||||
item.isHidden(t, api.getState(), activeIdRef.current),
|
||||
);
|
||||
if (item.isDisabled) {
|
||||
const disabled = tasks.some((t) =>
|
||||
item.isDisabled(t, api.getState(), activeIdRef.current),
|
||||
);
|
||||
item.disabled = disabled;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
[filter, selectedTasks, rTasksVal],
|
||||
[filter, selectedTasks, api],
|
||||
);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.wx-sidearea .wx-gantt-editor.wx-XkvqDXuw {
|
||||
width: 400px;
|
||||
width: 450px;
|
||||
}
|
||||
.wx-sidearea .wx-gantt-editor.wx-full-screen.wx-XkvqDXuw {
|
||||
width: 100%;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useMemo, useCallback, useContext } from 'react';
|
||||
import { Editor as WxEditor, registerEditorItem } from '@svar-ui/react-editor';
|
||||
import { Locale, RichSelect, Slider, Counter, TwoState } from '@svar-ui/react-core';
|
||||
import { defaultEditorItems, normalizeDates } from '@svar-ui/gantt-store';
|
||||
import { getEditorItems, prepareEditTask } from '@svar-ui/gantt-store';
|
||||
import { dateToString, locale } from '@svar-ui/lib-dom';
|
||||
import { en } from '@svar-ui/gantt-locales';
|
||||
import { en as coreEn } from '@svar-ui/core-locales';
|
||||
@@ -9,7 +9,7 @@ import { context } from '@svar-ui/react-core';
|
||||
|
||||
import Links from './editor/Links.jsx';
|
||||
import DateTimePicker from './editor/DateTimePicker.jsx';
|
||||
import { useStore, useWritableProp } from '@svar-ui/lib-react';
|
||||
import { useStore } from '@svar-ui/lib-react';
|
||||
|
||||
// helpers
|
||||
import { modeObserver } from '../helpers/modeResizeObserver';
|
||||
@@ -25,7 +25,7 @@ registerEditorItem('links', Links);
|
||||
|
||||
function Editor({
|
||||
api,
|
||||
items = defaultEditorItems,
|
||||
items = [],
|
||||
css = '',
|
||||
layout = 'default',
|
||||
readonly = false,
|
||||
@@ -34,6 +34,7 @@ function Editor({
|
||||
topBar = true,
|
||||
autoSave = true,
|
||||
focus = false,
|
||||
hotkeys = {},
|
||||
}) {
|
||||
const lFromCtx = useContext(context.i18n);
|
||||
const l = useMemo(() => lFromCtx || locale({ ...en, ...coreEn }), [lFromCtx]);
|
||||
@@ -91,28 +92,70 @@ function Editor({
|
||||
};
|
||||
}, [handleResize]);
|
||||
|
||||
const activeTask = useStore(api, "_activeTask");
|
||||
// const taskId = useStore(api, "activeTaskId");
|
||||
const taskId = useMemo(() => activeTask?.id, [activeTask]);
|
||||
const unit = useStore(api, "durationUnit");
|
||||
const unscheduledTasks = useStore(api, "unscheduledTasks");
|
||||
const taskTypes = useStore(api, "taskTypes");
|
||||
const activeTask = useStore(api, '_activeTask');
|
||||
const taskId = useStore(api, 'activeTask');
|
||||
const unscheduledTasks = useStore(api, 'unscheduledTasks');
|
||||
const links = useStore(api, 'links');
|
||||
const splitTasks = useStore(api, 'splitTasks');
|
||||
const segmentIndex = useMemo(
|
||||
() => splitTasks && taskId?.segmentIndex,
|
||||
[splitTasks, taskId],
|
||||
);
|
||||
const isSegment = useMemo(
|
||||
() => segmentIndex || segmentIndex === 0,
|
||||
[segmentIndex],
|
||||
);
|
||||
const baseItems = useMemo(
|
||||
() => getEditorItems({ unscheduledTasks }),
|
||||
[unscheduledTasks],
|
||||
);
|
||||
const undo = useStore(api, 'undo');
|
||||
|
||||
const [taskType, setTaskType] = useWritableProp(activeTask?.type);
|
||||
const taskUnscheduled = useMemo(() => activeTask?.unscheduled, [activeTask]);
|
||||
const [linksActionsMap, setLinksActionsMap] = useState({});
|
||||
const [inProgress, setInProgress] = useState(null);
|
||||
const [editorValues, setEditorValues] = useState();
|
||||
const [editorErrors, setEditorErrors] = useState(null);
|
||||
|
||||
const taskTypes = useStore(api, 'taskTypes');
|
||||
|
||||
const task = useMemo(() => {
|
||||
if (!activeTask) return null;
|
||||
let data;
|
||||
if (isSegment && activeTask.segments)
|
||||
data = { ...activeTask.segments[segmentIndex] };
|
||||
else data = { ...activeTask };
|
||||
|
||||
if (readonly) {
|
||||
// preserve parent to differentiate between segment and task
|
||||
let values = { parent: data.parent };
|
||||
baseItems.forEach(({ key, comp }) => {
|
||||
if (comp !== 'links') {
|
||||
const value = data[key];
|
||||
if (comp === 'date' && value instanceof Date) {
|
||||
values[key] = dateFormat(value);
|
||||
} else if (comp === 'slider' && key === 'progress') {
|
||||
values[key] = `${value}%`;
|
||||
} else {
|
||||
values[key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
return values;
|
||||
}
|
||||
return data || null;
|
||||
}, [activeTask, isSegment, segmentIndex, readonly, baseItems, dateFormat]);
|
||||
|
||||
useEffect(() => {
|
||||
setEditorValues(task);
|
||||
}, [task]);
|
||||
|
||||
useEffect(() => {
|
||||
setLinksActionsMap({});
|
||||
setEditorErrors(null);
|
||||
setInProgress(null);
|
||||
}, [taskId]);
|
||||
|
||||
const milestone = useMemo(() => taskType === 'milestone', [taskType]);
|
||||
const summary = useMemo(() => taskType === 'summary', [taskType]);
|
||||
|
||||
function prepareEditorItems(localItems, isUnscheduled) {
|
||||
const dates = { start: 1, end: 1, duration: 1 };
|
||||
|
||||
function prepareEditorItems(localItems, taskData) {
|
||||
return localItems.map((a) => {
|
||||
const item = { ...a };
|
||||
if (a.config) item.config = { ...item.config };
|
||||
@@ -122,7 +165,7 @@ function Editor({
|
||||
item.onLinksChange = handleLinksChange;
|
||||
}
|
||||
if (item.comp === 'select' && item.key === 'type') {
|
||||
let options = item.options ?? (taskTypes ? taskTypes : []);
|
||||
const options = item.options ?? (taskTypes ? taskTypes : []);
|
||||
item.options = options.map((t) => ({
|
||||
...t,
|
||||
label: _(t.label),
|
||||
@@ -137,78 +180,28 @@ function Editor({
|
||||
if (item.config?.placeholder)
|
||||
item.config.placeholder = _(item.config.placeholder);
|
||||
|
||||
if (unscheduledTasks && dates[item.key]) {
|
||||
if (isUnscheduled) {
|
||||
if (taskData) {
|
||||
if (item.isDisabled && item.isDisabled(taskData, api.getState())) {
|
||||
item.disabled = true;
|
||||
} else {
|
||||
delete item.disabled;
|
||||
}
|
||||
} else delete item.disabled;
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
function filterEditorItems(localItems) {
|
||||
return localItems.filter(({ comp, key, options }) => {
|
||||
switch (comp) {
|
||||
case 'date': {
|
||||
return (
|
||||
(!milestone || (key !== 'end' && key !== 'base_end')) && !summary
|
||||
);
|
||||
}
|
||||
case 'select': {
|
||||
return options.length > 1;
|
||||
}
|
||||
case 'twostate': {
|
||||
return unscheduledTasks && !summary;
|
||||
}
|
||||
case 'counter': {
|
||||
return !summary && !milestone;
|
||||
}
|
||||
case 'slider': {
|
||||
return !milestone;
|
||||
}
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const editorItems = useMemo(() => {
|
||||
const eItems = prepareEditorItems(items, taskUnscheduled);
|
||||
return filterEditorItems(eItems);
|
||||
}, [
|
||||
items,
|
||||
taskUnscheduled,
|
||||
milestone,
|
||||
summary,
|
||||
unscheduledTasks,
|
||||
taskTypes,
|
||||
_,
|
||||
api,
|
||||
autoSave,
|
||||
]);
|
||||
let eItems = items.length ? items : baseItems;
|
||||
eItems = prepareEditorItems(eItems, editorValues);
|
||||
if (!editorValues) return eItems;
|
||||
return eItems.filter(
|
||||
(item) => !item.isHidden || !item.isHidden(editorValues, api.getState()),
|
||||
);
|
||||
}, [items, baseItems, editorValues, taskTypes, _, api, autoSave]);
|
||||
|
||||
const task = useMemo(() => {
|
||||
if (readonly && activeTask) {
|
||||
let values = {};
|
||||
editorItems.forEach(({ key, comp }) => {
|
||||
if (comp !== 'links') {
|
||||
const value = activeTask[key];
|
||||
if (comp === 'date' && value instanceof Date) {
|
||||
values[key] = dateFormat(value);
|
||||
} else if (comp === 'slider' && key === 'progress') {
|
||||
values[key] = `${value}%`;
|
||||
} else {
|
||||
values[key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
return values;
|
||||
}
|
||||
return activeTask ? { ...activeTask } : null;
|
||||
}, [readonly, activeTask, editorItems, dateFormat]);
|
||||
const editorKeys = useMemo(
|
||||
() => editorItems.map((i) => i.key),
|
||||
[editorItems],
|
||||
);
|
||||
|
||||
function handleLinksChange({ id, action, data }) {
|
||||
setLinksActionsMap((prev) => ({
|
||||
@@ -219,64 +212,147 @@ function Editor({
|
||||
|
||||
const saveLinks = useCallback(() => {
|
||||
for (let link in linksActionsMap) {
|
||||
const { action, data } = linksActionsMap[link];
|
||||
api.exec(action, data);
|
||||
if (links.byId(link)) {
|
||||
const { action, data } = linksActionsMap[link];
|
||||
api.exec(action, data);
|
||||
}
|
||||
}
|
||||
}, [api, linksActionsMap]);
|
||||
}, [api, linksActionsMap, links]);
|
||||
|
||||
const deleteTask = useCallback(() => {
|
||||
api.exec('delete-task', { id: taskId });
|
||||
}, [api, taskId]);
|
||||
const id = taskId?.id || taskId;
|
||||
if (isSegment) {
|
||||
if (activeTask?.segments) {
|
||||
const segments = activeTask.segments.filter(
|
||||
(s, index) => index !== segmentIndex,
|
||||
);
|
||||
api.exec('update-task', {
|
||||
id,
|
||||
task: { segments },
|
||||
});
|
||||
}
|
||||
} else {
|
||||
api.exec('delete-task', { id });
|
||||
}
|
||||
}, [api, taskId, isSegment, activeTask, segmentIndex]);
|
||||
|
||||
const hide = useCallback(() => {
|
||||
api.exec('show-editor', { id: null });
|
||||
}, [api]);
|
||||
|
||||
const handleAction = useCallback((ev) => {
|
||||
const { item, changes } = ev;
|
||||
if (item.id === 'delete') {
|
||||
deleteTask();
|
||||
}
|
||||
if (item.id === 'save') {
|
||||
if (!changes.length) saveLinks();
|
||||
else hide();
|
||||
}
|
||||
if (item.comp) hide();
|
||||
}, [api, taskId, autoSave, saveLinks, deleteTask, hide]);
|
||||
const handleAction = useCallback(
|
||||
(ev) => {
|
||||
const { item, changes } = ev;
|
||||
if (item.id === 'delete') {
|
||||
deleteTask();
|
||||
}
|
||||
if (item.id === 'save') {
|
||||
if (!changes.length) saveLinks();
|
||||
else hide();
|
||||
}
|
||||
if (item.comp) hide();
|
||||
},
|
||||
[api, taskId, autoSave, saveLinks, deleteTask, hide],
|
||||
);
|
||||
|
||||
const normalizeTask = useCallback((t, key) => {
|
||||
if (unscheduledTasks && t.type === 'summary') t.unscheduled = false;
|
||||
const normalizeTask = useCallback(
|
||||
(t, key, input) => {
|
||||
if (unscheduledTasks && t.type === 'summary') t.unscheduled = false;
|
||||
|
||||
normalizeDates(t, unit, true, key);
|
||||
return t;
|
||||
}, [unscheduledTasks, unit]);
|
||||
prepareEditTask(t, api.getState(), key);
|
||||
if (!input) setInProgress(false);
|
||||
return t;
|
||||
},
|
||||
[unscheduledTasks, api],
|
||||
);
|
||||
|
||||
const handleChange = useCallback((ev) => {
|
||||
let { update, key, value } = ev;
|
||||
ev.update = normalizeTask({ ...update }, key);
|
||||
|
||||
if (!autoSave) {
|
||||
if (key === 'type') setTaskType(value);
|
||||
}
|
||||
}, [api, autoSave]);
|
||||
const save = useCallback(
|
||||
(values) => {
|
||||
values = {
|
||||
...values,
|
||||
unscheduled:
|
||||
unscheduledTasks && values.unscheduled && values.type !== 'summary',
|
||||
};
|
||||
delete values.links;
|
||||
delete values.data;
|
||||
|
||||
const handleSave = useCallback((ev) => {
|
||||
let { values } = ev;
|
||||
values = {
|
||||
...values,
|
||||
unscheduled:
|
||||
unscheduledTasks && values.unscheduled && values.type !== 'summary',
|
||||
};
|
||||
delete values.links;
|
||||
delete values.data;
|
||||
if (
|
||||
editorKeys.indexOf('duration') === -1 ||
|
||||
(values.segments && !values.duration)
|
||||
)
|
||||
delete values.duration;
|
||||
|
||||
api.exec('update-task', {
|
||||
id: taskId,
|
||||
task: values,
|
||||
});
|
||||
const data = {
|
||||
id: taskId?.id || taskId,
|
||||
task: values,
|
||||
...(isSegment && { segmentIndex }),
|
||||
};
|
||||
if (autoSave && inProgress) data.inProgress = inProgress;
|
||||
|
||||
if (!autoSave) saveLinks();
|
||||
}, [api, taskId, unscheduledTasks, autoSave, saveLinks]);
|
||||
api.exec('update-task', data);
|
||||
|
||||
if (!autoSave) saveLinks();
|
||||
},
|
||||
[
|
||||
api,
|
||||
taskId,
|
||||
unscheduledTasks,
|
||||
autoSave,
|
||||
saveLinks,
|
||||
editorKeys,
|
||||
isSegment,
|
||||
segmentIndex,
|
||||
inProgress,
|
||||
],
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(ev) => {
|
||||
let { update, key, input } = ev;
|
||||
|
||||
if (input) setInProgress(true);
|
||||
|
||||
ev.update = normalizeTask({ ...update }, key, input);
|
||||
|
||||
if (!autoSave) setEditorValues(ev.update);
|
||||
else if (!editorErrors && !input) {
|
||||
const item = editorItems.find((i) => i.key === key);
|
||||
const v = update[key];
|
||||
const isValid = !item.validation || item.validation(v);
|
||||
if (isValid && (!item.required || v)) save(ev.update);
|
||||
}
|
||||
},
|
||||
[autoSave, normalizeTask, editorErrors, editorItems, save],
|
||||
);
|
||||
|
||||
const handleSave = useCallback(
|
||||
(ev) => {
|
||||
if (!autoSave) save(ev.values);
|
||||
},
|
||||
[autoSave, save],
|
||||
);
|
||||
|
||||
const handleValidation = useCallback((check) => {
|
||||
// get all errors after onchange action
|
||||
setEditorErrors(check.errors);
|
||||
}, []);
|
||||
|
||||
const defaultHotkeys = useMemo(
|
||||
() =>
|
||||
undo
|
||||
? {
|
||||
'ctrl+z': (ev) => {
|
||||
ev.preventDefault();
|
||||
api.exec('undo');
|
||||
},
|
||||
'ctrl+y': (ev) => {
|
||||
ev.preventDefault();
|
||||
api.exec('redo');
|
||||
},
|
||||
}
|
||||
: {},
|
||||
[undo, api],
|
||||
);
|
||||
|
||||
return task ? (
|
||||
<Locale>
|
||||
@@ -293,7 +369,9 @@ function Editor({
|
||||
focus={focus}
|
||||
onAction={handleAction}
|
||||
onSave={handleSave}
|
||||
onValidation={handleValidation}
|
||||
onChange={handleChange}
|
||||
hotkeys={hotkeys && { ...defaultHotkeys, ...hotkeys }}
|
||||
/>
|
||||
</Locale>
|
||||
) : null;
|
||||
|
||||
@@ -5,17 +5,29 @@ import {
|
||||
useRef,
|
||||
useImperativeHandle,
|
||||
useState,
|
||||
useContext,
|
||||
} from 'react';
|
||||
|
||||
// core widgets lib
|
||||
import { Locale } from '@svar-ui/react-core';
|
||||
import { context } from '@svar-ui/react-core';
|
||||
|
||||
// locales
|
||||
import { locale as l } from '@svar-ui/lib-dom';
|
||||
import { en } from '@svar-ui/gantt-locales';
|
||||
import { en as coreEn } from '@svar-ui/core-locales';
|
||||
|
||||
// stores
|
||||
import { EventBusRouter } from '@svar-ui/lib-state';
|
||||
import { DataStore, defaultColumns, defaultTaskTypes } from '@svar-ui/gantt-store';
|
||||
import {
|
||||
DataStore,
|
||||
defaultColumns,
|
||||
defaultTaskTypes,
|
||||
parseTaskDates,
|
||||
normalizeZoom,
|
||||
normalizeLinks,
|
||||
} from '@svar-ui/gantt-store';
|
||||
|
||||
// context
|
||||
// context
|
||||
import StoreContext from '../context';
|
||||
|
||||
// store factory
|
||||
@@ -24,6 +36,13 @@ import { writable } from '@svar-ui/lib-react';
|
||||
// ui
|
||||
import Layout from './Layout.jsx';
|
||||
|
||||
// helpers
|
||||
import {
|
||||
prepareScales,
|
||||
prepareFormats,
|
||||
prepareColumns,
|
||||
prepareZoom,
|
||||
} from '../helpers/prepareConfig.js';
|
||||
|
||||
const camelize = (s) =>
|
||||
s
|
||||
@@ -32,8 +51,8 @@ const camelize = (s) =>
|
||||
.join('');
|
||||
|
||||
const defaultScales = [
|
||||
{ unit: 'month', step: 1, format: 'MMMM yyy' },
|
||||
{ unit: 'day', step: 1, format: 'd' },
|
||||
{ unit: 'month', step: 1, format: '%F %Y' },
|
||||
{ unit: 'day', step: 1, format: '%j' },
|
||||
];
|
||||
|
||||
const Gantt = forwardRef(function Gantt(
|
||||
@@ -58,10 +77,17 @@ const Gantt = forwardRef(function Gantt(
|
||||
cellBorders = 'full',
|
||||
zoom = false,
|
||||
baselines = false,
|
||||
highlightTime = null,
|
||||
highlightTime: highlightTimeProp = null,
|
||||
init = null,
|
||||
autoScale = true,
|
||||
unscheduledTasks = false,
|
||||
criticalPath = null,
|
||||
schedule = { type: 'forward' },
|
||||
projectStart = null,
|
||||
projectEnd = null,
|
||||
calendar = null,
|
||||
undo = false,
|
||||
splitTasks = false,
|
||||
...restProps
|
||||
},
|
||||
ref,
|
||||
@@ -70,9 +96,53 @@ const Gantt = forwardRef(function Gantt(
|
||||
const restPropsRef = useRef();
|
||||
restPropsRef.current = restProps;
|
||||
|
||||
|
||||
// init stores
|
||||
const dataStore = useMemo(() => new DataStore(writable), []);
|
||||
|
||||
// locale and formats
|
||||
// uses same logic as the Locale component
|
||||
const words = useMemo(() => ({ ...coreEn, ...en }), []);
|
||||
const i18nCtx = useContext(context.i18n);
|
||||
const locale = useMemo(() => {
|
||||
if (!i18nCtx) return l(words);
|
||||
return i18nCtx.extend(words, true);
|
||||
}, [i18nCtx, words]);
|
||||
|
||||
// prepare configuration objects
|
||||
const lCalendar = useMemo(() => locale.getRaw().calendar, [locale]);
|
||||
|
||||
const normalizedConfig = useMemo(() => {
|
||||
let config = {
|
||||
zoom: prepareZoom(zoom, lCalendar),
|
||||
scales: prepareScales(scales, lCalendar),
|
||||
columns: prepareColumns(columns, lCalendar),
|
||||
links: normalizeLinks(links),
|
||||
cellWidth,
|
||||
};
|
||||
if (config.zoom) {
|
||||
config = {
|
||||
...config,
|
||||
...normalizeZoom(
|
||||
config.zoom,
|
||||
prepareFormats(lCalendar, locale.getGroup('gantt')),
|
||||
config.scales,
|
||||
cellWidth,
|
||||
),
|
||||
};
|
||||
}
|
||||
return config;
|
||||
}, [zoom, scales, columns, links, cellWidth, lCalendar, locale]);
|
||||
|
||||
// parse task dates effect
|
||||
const parsedTasksRef = useRef(null);
|
||||
if (parsedTasksRef.current !== tasks) {
|
||||
parseTaskDates(tasks, { durationUnit, splitTasks, calendar });
|
||||
parsedTasksRef.current = tasks;
|
||||
}
|
||||
useEffect(() => {
|
||||
parseTaskDates(tasks, { durationUnit, splitTasks, calendar });
|
||||
}, [tasks, durationUnit, calendar, splitTasks]);
|
||||
|
||||
const firstInRoute = useMemo(() => dataStore.in, [dataStore]);
|
||||
|
||||
const lastInRouteRef = useRef(null);
|
||||
@@ -86,7 +156,6 @@ const Gantt = forwardRef(function Gantt(
|
||||
firstInRoute.setNext(lastInRouteRef.current);
|
||||
}
|
||||
|
||||
|
||||
// writable prop for two-way binding tableAPI
|
||||
const [tableAPI, setTableAPI] = useState(null);
|
||||
const tableAPIRef = useRef(null);
|
||||
@@ -112,11 +181,11 @@ const Gantt = forwardRef(function Gantt(
|
||||
waitRender
|
||||
? new Promise((res) => setTimeout(() => res(tableAPIRef.current), 1))
|
||||
: tableAPIRef.current,
|
||||
getHistory: () => dataStore.getHistory(),
|
||||
}),
|
||||
[dataStore, firstInRoute],
|
||||
);
|
||||
|
||||
|
||||
// expose API via ref
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
@@ -134,17 +203,17 @@ const Gantt = forwardRef(function Gantt(
|
||||
// const prev = dataStore.getState();
|
||||
dataStore.init({
|
||||
tasks,
|
||||
links,
|
||||
links: normalizedConfig.links,
|
||||
start,
|
||||
columns,
|
||||
columns: normalizedConfig.columns,
|
||||
end,
|
||||
lengthUnit,
|
||||
cellWidth,
|
||||
cellWidth: normalizedConfig.cellWidth,
|
||||
cellHeight,
|
||||
scaleHeight,
|
||||
scales,
|
||||
scales: normalizedConfig.scales,
|
||||
taskTypes,
|
||||
zoom,
|
||||
zoom: normalizedConfig.zoom,
|
||||
selected,
|
||||
activeTask,
|
||||
baselines,
|
||||
@@ -152,22 +221,28 @@ const Gantt = forwardRef(function Gantt(
|
||||
unscheduledTasks,
|
||||
markers,
|
||||
durationUnit,
|
||||
criticalPath,
|
||||
schedule,
|
||||
projectStart,
|
||||
projectEnd,
|
||||
calendar,
|
||||
undo,
|
||||
_weekStart: lCalendar.weekStart,
|
||||
splitTasks,
|
||||
});
|
||||
}
|
||||
initOnceRef.current++;
|
||||
}, [
|
||||
api,
|
||||
init,
|
||||
tasks,
|
||||
links,
|
||||
normalizedConfig,
|
||||
start,
|
||||
columns,
|
||||
end,
|
||||
lengthUnit,
|
||||
cellWidth,
|
||||
cellHeight,
|
||||
scaleHeight,
|
||||
scales,
|
||||
taskTypes,
|
||||
zoom,
|
||||
selected,
|
||||
activeTask,
|
||||
baselines,
|
||||
@@ -175,22 +250,31 @@ const Gantt = forwardRef(function Gantt(
|
||||
unscheduledTasks,
|
||||
markers,
|
||||
durationUnit,
|
||||
criticalPath,
|
||||
schedule,
|
||||
projectStart,
|
||||
projectEnd,
|
||||
calendar,
|
||||
undo,
|
||||
lCalendar,
|
||||
splitTasks,
|
||||
dataStore,
|
||||
]);
|
||||
|
||||
if (initOnceRef.current === 0) {
|
||||
dataStore.init({
|
||||
tasks,
|
||||
links,
|
||||
links: normalizedConfig.links,
|
||||
start,
|
||||
columns,
|
||||
columns: normalizedConfig.columns,
|
||||
end,
|
||||
lengthUnit,
|
||||
cellWidth,
|
||||
cellWidth: normalizedConfig.cellWidth,
|
||||
cellHeight,
|
||||
scaleHeight,
|
||||
scales,
|
||||
scales: normalizedConfig.scales,
|
||||
taskTypes,
|
||||
zoom,
|
||||
zoom: normalizedConfig.zoom,
|
||||
selected,
|
||||
activeTask,
|
||||
baselines,
|
||||
@@ -198,11 +282,31 @@ const Gantt = forwardRef(function Gantt(
|
||||
unscheduledTasks,
|
||||
markers,
|
||||
durationUnit,
|
||||
criticalPath,
|
||||
schedule,
|
||||
projectStart,
|
||||
projectEnd,
|
||||
calendar,
|
||||
undo,
|
||||
_weekStart: lCalendar.weekStart,
|
||||
splitTasks,
|
||||
});
|
||||
}
|
||||
|
||||
// highlightTime from calendar
|
||||
const highlightTime = useMemo(() => {
|
||||
if (calendar) {
|
||||
return (day, unit) => {
|
||||
if (unit == 'day' && !calendar.getDayHours(day)) return 'wx-weekend';
|
||||
if (unit == 'hour' && !calendar.getDayHours(day)) return 'wx-weekend';
|
||||
return '';
|
||||
};
|
||||
}
|
||||
return highlightTimeProp;
|
||||
}, [calendar, highlightTimeProp]);
|
||||
|
||||
return (
|
||||
<Locale words={en} optional={true}>
|
||||
<context.i18n.Provider value={locale}>
|
||||
<StoreContext.Provider value={api}>
|
||||
<Layout
|
||||
taskTemplate={taskTemplate}
|
||||
@@ -212,7 +316,7 @@ const Gantt = forwardRef(function Gantt(
|
||||
onTableAPIChange={setTableAPI}
|
||||
/>
|
||||
</StoreContext.Provider>
|
||||
</Locale>
|
||||
</context.i18n.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.wx-pseudo-rows.wx-jlbQoHOz {
|
||||
width: 100%;
|
||||
|
||||
@@ -14,6 +14,7 @@ import { modeObserver } from '../helpers/modeResizeObserver';
|
||||
import storeContext from '../context';
|
||||
import { useStore } from '@svar-ui/lib-react';
|
||||
import './Layout.css';
|
||||
import { flushSync } from 'react-dom'
|
||||
|
||||
function Layout(props) {
|
||||
const {
|
||||
@@ -21,20 +22,21 @@ function Layout(props) {
|
||||
readonly,
|
||||
cellBorders,
|
||||
highlightTime,
|
||||
onTableAPIChange
|
||||
onTableAPIChange,
|
||||
} = props;
|
||||
|
||||
const api = useContext(storeContext);
|
||||
|
||||
const rTasks = useStore(api, "_tasks");
|
||||
const rScales = useStore(api, "_scales");
|
||||
const rCellHeight = useStore(api, "cellHeight");
|
||||
const rColumns = useStore(api, "columns");
|
||||
const rScrollTask = useStore(api, "_scrollTask");
|
||||
const rTasks = useStore(api, '_tasks');
|
||||
const rScales = useStore(api, '_scales');
|
||||
const rCellHeight = useStore(api, 'cellHeight');
|
||||
const rColumns = useStore(api, 'columns');
|
||||
const rScrollTask = useStore(api, '_scrollTask');
|
||||
const undo = useStore(api, 'undo');
|
||||
const [compactMode, setCompactMode] = useState(false);
|
||||
let [gridWidth, setGridWidth] = useState(0);
|
||||
const [ganttWidth, setGanttWidth] = useState(undefined);
|
||||
const [ganttHeight, setGanttHeight] = useState(undefined);
|
||||
const [ganttWidth, setGanttWidth] = useState(0);
|
||||
const [ganttHeight, setGanttHeight] = useState(0);
|
||||
const [innerWidth, setInnerWidth] = useState(undefined);
|
||||
const [display, setDisplay] = useState('all');
|
||||
|
||||
@@ -57,7 +59,6 @@ function Layout(props) {
|
||||
[display],
|
||||
);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const ro = modeObserver(handleResize);
|
||||
ro.observe();
|
||||
@@ -81,7 +82,6 @@ function Layout(props) {
|
||||
return w;
|
||||
}, [rColumns, compactMode, display]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setGridWidth(gridColumnWidth);
|
||||
}, [gridColumnWidth]);
|
||||
@@ -125,6 +125,10 @@ function Layout(props) {
|
||||
};
|
||||
}, [chartRef.current, expandScale]);
|
||||
|
||||
useEffect(() => {
|
||||
expandScale();
|
||||
}, [fullWidth]);
|
||||
|
||||
const ganttDivRef = useRef(null);
|
||||
const pseudoRowsRef = useRef(null);
|
||||
|
||||
@@ -191,39 +195,33 @@ function Layout(props) {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
scrollToTask(rScrollTask);
|
||||
scrollToTask(rScrollTask);
|
||||
}, [rScrollTask]);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ganttDivRef.current;
|
||||
if (!el) return;
|
||||
const ganttDiv = ganttDivRef.current;
|
||||
const pseudoRows = pseudoRowsRef.current;
|
||||
if (!ganttDiv || !pseudoRows) return;
|
||||
const update = () => {
|
||||
setGanttHeight(el.offsetHeight);
|
||||
setGanttWidth(el.offsetWidth);
|
||||
flushSync(() => {
|
||||
setGanttHeight(ganttDiv.offsetHeight);
|
||||
setGanttWidth(ganttDiv.offsetWidth);
|
||||
setInnerWidth(pseudoRows.offsetWidth);
|
||||
});
|
||||
};
|
||||
update();
|
||||
const ro = new ResizeObserver(update);
|
||||
ro.observe(el);
|
||||
ro.observe(ganttDiv);
|
||||
return () => ro.disconnect();
|
||||
}, [ganttDivRef.current]);
|
||||
|
||||
useEffect(() => {
|
||||
const el = pseudoRowsRef.current;
|
||||
if (!el) return;
|
||||
const update = () => {
|
||||
setInnerWidth(el.offsetWidth);
|
||||
};
|
||||
update();
|
||||
const ro = new ResizeObserver(update);
|
||||
ro.observe(el);
|
||||
return () => ro.disconnect();
|
||||
}, [pseudoRowsRef.current]);
|
||||
|
||||
const layoutRef = useRef(null);
|
||||
const cleanupRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (cleanupRef.current) return;
|
||||
if (cleanupRef.current) {
|
||||
cleanupRef.current.destroy();
|
||||
cleanupRef.current = null;
|
||||
}
|
||||
const node = layoutRef.current;
|
||||
if (!node) return;
|
||||
|
||||
@@ -234,6 +232,8 @@ function Layout(props) {
|
||||
'ctrl+x': true,
|
||||
'ctrl+d': true,
|
||||
backspace: true,
|
||||
'ctrl+z': undo,
|
||||
'ctrl+y': undo,
|
||||
},
|
||||
exec: (ev) => {
|
||||
if (!ev.isInput) api.exec('hotkey', ev);
|
||||
@@ -244,7 +244,7 @@ function Layout(props) {
|
||||
cleanupRef.current?.destroy();
|
||||
cleanupRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
}, [undo]);
|
||||
|
||||
return (
|
||||
<div className="wx-jlbQoHOz wx-gantt" ref={ganttDivRef} onScroll={onScroll}>
|
||||
@@ -276,8 +276,7 @@ function Layout(props) {
|
||||
value={gridWidth}
|
||||
display={display}
|
||||
compactMode={compactMode}
|
||||
minValue="50"
|
||||
maxValue="800"
|
||||
containerWidth={ganttWidth}
|
||||
onMove={(value) => setGridWidth(value)}
|
||||
onDisplayChange={(display) => setDisplay(display)}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useMemo, useRef, useCallback } from 'react';
|
||||
import { useState, useMemo, useRef, useCallback, useEffect } from 'react';
|
||||
import { useWritableProp } from '@svar-ui/lib-react';
|
||||
import './Resizer.css';
|
||||
|
||||
@@ -7,16 +7,17 @@ function Resizer(props) {
|
||||
position = 'after',
|
||||
size = 4,
|
||||
dir = 'x',
|
||||
minValue = 0,
|
||||
maxValue = 0,
|
||||
onMove,
|
||||
onDisplayChange,
|
||||
compactMode,
|
||||
containerWidth = 0,
|
||||
leftThreshold = 50,
|
||||
rightThreshold = 50,
|
||||
} = props;
|
||||
|
||||
const [value, setValue] = useWritableProp(props.value ?? 0);
|
||||
const [display, setDisplay] = useWritableProp(props.display ?? 'all');
|
||||
|
||||
|
||||
function getBox(val) {
|
||||
let offset = 0;
|
||||
if (position == 'center') offset = size / 2;
|
||||
@@ -35,24 +36,52 @@ function Resizer(props) {
|
||||
}
|
||||
|
||||
const [active, setActive] = useState(false);
|
||||
const [initialPosition, setInitialPosition] = useState(null);
|
||||
|
||||
const startRef = useRef(0);
|
||||
const posRef = useRef();
|
||||
const timeoutRef = useRef();
|
||||
const displayRef = useRef(display);
|
||||
|
||||
useEffect(() => {
|
||||
displayRef.current = display;
|
||||
}, [display]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialPosition === null && value > 0) {
|
||||
setInitialPosition(value);
|
||||
}
|
||||
}, [initialPosition, value]);
|
||||
|
||||
function getEventPos(ev) {
|
||||
return dir == 'x' ? ev.clientX : ev.clientY;
|
||||
}
|
||||
|
||||
const move = useCallback((ev) => {
|
||||
const newPos = posRef.current + getEventPos(ev) - startRef.current;
|
||||
if (
|
||||
(!minValue || minValue <= newPos) &&
|
||||
(!maxValue || maxValue >= newPos)
|
||||
) {
|
||||
const move = useCallback(
|
||||
(ev) => {
|
||||
const newPos = posRef.current + getEventPos(ev) - startRef.current;
|
||||
|
||||
setValue(newPos);
|
||||
onMove(newPos)
|
||||
}
|
||||
}, []);
|
||||
let nextDisplay;
|
||||
|
||||
if (newPos <= leftThreshold) {
|
||||
nextDisplay = 'chart';
|
||||
} else if (containerWidth - newPos <= rightThreshold) {
|
||||
nextDisplay = 'grid';
|
||||
} else {
|
||||
nextDisplay = 'all';
|
||||
}
|
||||
|
||||
if (displayRef.current !== nextDisplay) {
|
||||
setDisplay(nextDisplay);
|
||||
displayRef.current = nextDisplay;
|
||||
}
|
||||
|
||||
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = setTimeout(() => onMove && onMove(newPos), 100);
|
||||
},
|
||||
[containerWidth, leftThreshold, rightThreshold, onMove],
|
||||
);
|
||||
|
||||
const up = useCallback(() => {
|
||||
document.body.style.cursor = '';
|
||||
@@ -69,6 +98,11 @@ function Resizer(props) {
|
||||
|
||||
const down = useCallback(
|
||||
(ev) => {
|
||||
// Prevent dragging when in normal mode and only one view is visible
|
||||
if (!compactMode && (display === 'grid' || display === 'chart')) {
|
||||
return;
|
||||
}
|
||||
|
||||
startRef.current = getEventPos(ev);
|
||||
|
||||
posRef.current = value;
|
||||
@@ -80,29 +114,40 @@ function Resizer(props) {
|
||||
window.addEventListener('mousemove', move);
|
||||
window.addEventListener('mouseup', up);
|
||||
},
|
||||
[cursor, move, up, value],
|
||||
[cursor, move, up, value, compactMode, display],
|
||||
);
|
||||
|
||||
function handleExpandLeft() {
|
||||
let newDisplay;
|
||||
if (compactMode) {
|
||||
newDisplay = display === 'chart' ? 'grid' : 'chart';
|
||||
} else {
|
||||
newDisplay = display === 'all' ? 'chart' : 'all';
|
||||
function resetToInitial() {
|
||||
setDisplay('all');
|
||||
if (initialPosition !== null) {
|
||||
setValue(initialPosition);
|
||||
if (onMove) onMove(initialPosition);
|
||||
}
|
||||
setDisplay(newDisplay);
|
||||
onDisplayChange(newDisplay);
|
||||
}
|
||||
|
||||
function handleExpand(direction) {
|
||||
if (compactMode) {
|
||||
const newDisplay = display === 'chart' ? 'grid' : 'chart';
|
||||
setDisplay(newDisplay);
|
||||
onDisplayChange(newDisplay);
|
||||
} else {
|
||||
if (display === 'grid' || display === 'chart') {
|
||||
resetToInitial();
|
||||
onDisplayChange('all');
|
||||
} else {
|
||||
const newDisplay = direction === 'left' ? 'chart' : 'grid';
|
||||
setDisplay(newDisplay);
|
||||
onDisplayChange(newDisplay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleExpandLeft() {
|
||||
handleExpand('left');
|
||||
}
|
||||
|
||||
function handleExpandRight() {
|
||||
let newDisplay;
|
||||
if (compactMode) {
|
||||
newDisplay = display === 'grid' ? 'chart' : 'grid';
|
||||
} else {
|
||||
newDisplay = display === 'all' ? 'grid' : 'all';
|
||||
}
|
||||
setDisplay(newDisplay);
|
||||
onDisplayChange(newDisplay);
|
||||
handleExpand('right');
|
||||
}
|
||||
|
||||
const b = useMemo(() => getBox(value), [value, position, size, dir]);
|
||||
@@ -122,25 +167,21 @@ function Resizer(props) {
|
||||
onMouseDown={down}
|
||||
style={{ width: b.size[0], height: b.size[1], cursor }}
|
||||
>
|
||||
<div className='wx-pFykzMlT wx-button-expand-box'>
|
||||
<div
|
||||
className='wx-pFykzMlT wx-button-expand-content wx-button-expand-left'
|
||||
>
|
||||
<div className="wx-pFykzMlT wx-button-expand-box">
|
||||
<div className="wx-pFykzMlT wx-button-expand-content wx-button-expand-left">
|
||||
<i
|
||||
className='wx-pFykzMlT wxi-menu-left'
|
||||
className="wx-pFykzMlT wxi-menu-left"
|
||||
onClick={handleExpandLeft}
|
||||
></i>
|
||||
</div>
|
||||
<div
|
||||
className='wx-pFykzMlT wx-button-expand-content wx-button-expand-right'
|
||||
>
|
||||
<div className="wx-pFykzMlT wx-button-expand-content wx-button-expand-right">
|
||||
<i
|
||||
className='wx-pFykzMlT wxi-menu-right'
|
||||
className="wx-pFykzMlT wxi-menu-right"
|
||||
onClick={handleExpandRight}
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
<div className='wx-pFykzMlT wx-resizer-line'></div>
|
||||
<div className="wx-pFykzMlT wx-resizer-line"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,50 +3,76 @@ import { Toolbar as WxToolbar } from '@svar-ui/react-toolbar';
|
||||
import { useStoreLater } from '@svar-ui/lib-react';
|
||||
import {
|
||||
handleAction,
|
||||
defaultToolbarButtons,
|
||||
getToolbarButtons,
|
||||
isHandledAction,
|
||||
} from '@svar-ui/gantt-store';
|
||||
import { locale } from '@svar-ui/lib-dom';
|
||||
import { en } from '@svar-ui/gantt-locales';
|
||||
import { context } from '@svar-ui/react-core';
|
||||
|
||||
export default function Toolbar({
|
||||
api = null,
|
||||
items = [...defaultToolbarButtons],
|
||||
}) {
|
||||
export default function Toolbar({ api = null, items = [] }) {
|
||||
const i18nCtx = useContext(context.i18n);
|
||||
const i18nLocal = useMemo(() => (i18nCtx ? i18nCtx : locale(en)), [i18nCtx]);
|
||||
const _ = useMemo(() => i18nLocal.getGroup('gantt'), [i18nLocal]);
|
||||
|
||||
const rSelected = useStoreLater(api, "_selected");
|
||||
const rTasks = useStoreLater(api, "_tasks");
|
||||
const _selected = useStoreLater(api, '_selected');
|
||||
const undo = useStoreLater(api, 'undo');
|
||||
const history = useStoreLater(api, 'history');
|
||||
const splitTasks = useStoreLater(api, 'splitTasks');
|
||||
|
||||
const historyActions = ['undo', 'redo'];
|
||||
|
||||
const finalItems = useMemo(() => {
|
||||
return items.map((b) => {
|
||||
const fullButtons = getToolbarButtons({ undo: true, splitTasks: true });
|
||||
const buttons = items.length
|
||||
? items
|
||||
: getToolbarButtons({
|
||||
undo,
|
||||
splitTasks,
|
||||
});
|
||||
return buttons.map((b) => {
|
||||
let item = { ...b, disabled: false };
|
||||
item.handler = isHandledAction(defaultToolbarButtons, item.id)
|
||||
item.handler = isHandledAction(fullButtons, item.id)
|
||||
? (it) => handleAction(api, it.id, null, _)
|
||||
: item.handler;
|
||||
if (item.text) item.text = _(item.text);
|
||||
if (item.menuText) item.menuText = _(item.menuText);
|
||||
return item;
|
||||
});
|
||||
}, [items, api, _]);
|
||||
}, [items, api, _, undo, splitTasks]);
|
||||
|
||||
const buttons = useMemo(() => {
|
||||
if (api) {
|
||||
if (rSelected?.length) {
|
||||
return finalItems.map((item) => {
|
||||
if (!item.check) return item;
|
||||
const isDisabled = rSelected.some(
|
||||
(task) => !item.check(task, rTasks),
|
||||
);
|
||||
return { ...item, disabled: isDisabled };
|
||||
const finalButtons = [];
|
||||
finalItems.forEach((item) => {
|
||||
const action = item.id;
|
||||
if (action === 'add-task') {
|
||||
finalButtons.push(item);
|
||||
} else if (!historyActions.includes(action)) {
|
||||
if (!_selected?.length || !api) return;
|
||||
finalButtons.push({
|
||||
...item,
|
||||
disabled:
|
||||
item.isDisabled &&
|
||||
_selected.some((task) => item.isDisabled(task, api.getState())),
|
||||
});
|
||||
} else if (historyActions.includes(action)) {
|
||||
finalButtons.push({
|
||||
...item,
|
||||
disabled: item.isDisabled(history),
|
||||
});
|
||||
}
|
||||
}
|
||||
return [{ ...finalItems[0], disabled: false }];
|
||||
}, [api, rSelected, rTasks, finalItems]);
|
||||
});
|
||||
// filter out consecutive separators
|
||||
return finalButtons.filter((button, index) => {
|
||||
if (api && button.isHidden)
|
||||
return !_selected.some((task) => button.isHidden(task, api.getState()));
|
||||
if (button.comp === 'separator') {
|
||||
const nextButton = finalButtons[index + 1];
|
||||
if (!nextButton || nextButton.comp === 'separator') return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, [api, _selected, history, finalItems]);
|
||||
|
||||
if (!i18nCtx) {
|
||||
return (
|
||||
|
||||
22
src/components/chart/BarSegments.css
Normal file
22
src/components/chart/BarSegments.css
Normal file
@@ -0,0 +1,22 @@
|
||||
.wx-segments.wx-GKbcLEGA {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.wx-segment.wx-GKbcLEGA {
|
||||
height: 100%;
|
||||
}
|
||||
.wx-segments.wx-GKbcLEGA::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
border-top: 1px dashed #7f7f7f;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.wx-progress-percent.wx-GKbcLEGA {
|
||||
background-color: var(--wx-gantt-task-fill-color);
|
||||
}
|
||||
60
src/components/chart/BarSegments.jsx
Normal file
60
src/components/chart/BarSegments.jsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import './BarSegments.css';
|
||||
|
||||
function BarSegments(props) {
|
||||
const { task, type } = props;
|
||||
|
||||
function segmentStyle(i) {
|
||||
const s = task.segments[i];
|
||||
return {
|
||||
left: `${s.$x}px`,
|
||||
top: '0px',
|
||||
width: `${s.$w}px`,
|
||||
height: '100%',
|
||||
};
|
||||
}
|
||||
|
||||
function getSegProgress(segmentIndex) {
|
||||
if (!task.progress) return 0;
|
||||
|
||||
const progress = (task.duration * task.progress) / 100;
|
||||
const segments = task.segments;
|
||||
let duration = 0,
|
||||
i = 0,
|
||||
result = null;
|
||||
do {
|
||||
const s = segments[i];
|
||||
if (i === segmentIndex) {
|
||||
if (duration > progress) result = 0;
|
||||
else result = Math.min((progress - duration) / s.duration, 1) * 100;
|
||||
}
|
||||
duration += s.duration;
|
||||
i++;
|
||||
} while (result === null && i < segments.length);
|
||||
return result || 0;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="wx-segments wx-GKbcLEGA">
|
||||
{task.segments.map((seg, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`wx-segment wx-bar wx-${type} wx-GKbcLEGA`}
|
||||
data-segment={i}
|
||||
style={segmentStyle(i)}
|
||||
>
|
||||
{task.progress ? (
|
||||
<div className="wx-progress-wrapper">
|
||||
<div
|
||||
className="wx-progress-percent wx-GKbcLEGA"
|
||||
style={{ width: `${getSegProgress(i)}%` }}
|
||||
></div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="wx-content">{seg.text || ''}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BarSegments;
|
||||
@@ -17,7 +17,9 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wx-bar.wx-GKbcLEGA {
|
||||
.wx-bar.wx-GKbcLEGA,
|
||||
.wx-bar.wx-GKbcLEGA .wx-segment {
|
||||
pointer-events: all;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
border-radius: var(--wx-gantt-bar-border-radius);
|
||||
@@ -37,22 +39,24 @@
|
||||
.wx-bar.wx-reorder-task.wx-GKbcLEGA {
|
||||
z-index: 3;
|
||||
}
|
||||
.wx-content.wx-GKbcLEGA {
|
||||
.wx-bar.wx-GKbcLEGA .wx-content {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.wx-task.wx-GKbcLEGA {
|
||||
.wx-task:not(.wx-split).wx-GKbcLEGA,
|
||||
.wx-task.wx-GKbcLEGA .wx-segment {
|
||||
color: var(--wx-gantt-task-font-color);
|
||||
background-color: var(--wx-gantt-task-color);
|
||||
border: var(--wx-gantt-task-border);
|
||||
}
|
||||
|
||||
.wx-task.wx-selected.wx-GKbcLEGA {
|
||||
.wx-task.wx-selected:not(.wx-split).wx-GKbcLEGA {
|
||||
border: 1px solid var(--wx-gantt-task-border-color);
|
||||
box-shadow: var(--wx-gantt-bar-shadow);
|
||||
}
|
||||
|
||||
.wx-task:hover.wx-GKbcLEGA {
|
||||
.wx-task:not(.wx-split):hover.wx-GKbcLEGA,
|
||||
.wx-task.wx-GKbcLEGA .wx-segment:hover {
|
||||
box-shadow: var(--wx-gantt-bar-shadow);
|
||||
}
|
||||
|
||||
@@ -71,7 +75,7 @@
|
||||
box-shadow: var(--wx-gantt-bar-shadow);
|
||||
}
|
||||
|
||||
.wx-milestone .wx-content.wx-GKbcLEGA {
|
||||
.wx-milestone.wx-GKbcLEGA .wx-content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -80,7 +84,7 @@
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.wx-bar:not(.wx-milestone) .wx-content.wx-GKbcLEGA {
|
||||
.wx-bar:not(.wx-milestone).wx-GKbcLEGA .wx-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
@@ -97,19 +101,19 @@
|
||||
border-color: var(--wx-gantt-milestone-color);
|
||||
}
|
||||
|
||||
.wx-milestone .wx-text-out.wx-GKbcLEGA {
|
||||
.wx-milestone.wx-GKbcLEGA .wx-text-out {
|
||||
padding: 0 2px;
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.wx-milestone .wx-content.wx-GKbcLEGA {
|
||||
.wx-milestone.wx-GKbcLEGA .wx-content {
|
||||
height: 100%;
|
||||
background-color: var(--wx-gantt-milestone-color);
|
||||
transform: rotate(45deg) scale(0.75);
|
||||
border-radius: var(--wx-gantt-milestone-border-radius);
|
||||
}
|
||||
|
||||
.wx-progress-wrapper.wx-GKbcLEGA {
|
||||
.wx-bar.wx-GKbcLEGA .wx-progress-wrapper {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -118,7 +122,7 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wx-progress-percent.wx-GKbcLEGA {
|
||||
.wx-bar.wx-GKbcLEGA .wx-progress-percent {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -190,14 +194,28 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wx-bar.wx-GKbcLEGA button.wx-button.wx-delete-button {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 0;
|
||||
}
|
||||
.wx-delete-button-icon {
|
||||
display: block;
|
||||
line-height: 14px;
|
||||
font-size: 10px;
|
||||
}
|
||||
.wx-bar.wx-GKbcLEGA .wx-delete-button.wx-left,
|
||||
.wx-link.wx-left.wx-GKbcLEGA {
|
||||
left: -16px;
|
||||
}
|
||||
|
||||
.wx-bar.wx-GKbcLEGA .wx-delete-button.wx-right,
|
||||
.wx-link.wx-right.wx-GKbcLEGA {
|
||||
right: -16px;
|
||||
}
|
||||
|
||||
.wx-link.wx-target:hover.wx-GKbcLEGA,
|
||||
.wx-link.wx-selected.wx-GKbcLEGA,
|
||||
.wx-bar:hover .wx-link.wx-target.wx-GKbcLEGA,
|
||||
@@ -206,11 +224,10 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wx-link.wx-selected.wx-GKbcLEGA {
|
||||
.wx-bar:not(.wx-split).wx-GKbcLEGA .wx-link.wx-selected {
|
||||
border-color: inherit;
|
||||
}
|
||||
|
||||
.wx-link.wx-selected .wx-inner.wx-GKbcLEGA {
|
||||
.wx-bar:not(.wx-split).wx-GKbcLEGA .wx-link.wx-selected .wx-inner {
|
||||
border-color: inherit;
|
||||
}
|
||||
|
||||
@@ -235,3 +252,55 @@
|
||||
outline: 1px solid var(--wx-color-primary);
|
||||
outline-offset: 1.6px;
|
||||
}
|
||||
/* critical path markers */
|
||||
.wx-task.wx-critical.wx-GKbcLEGA {
|
||||
background-color: var(--wx-gantt-task-critical-color);
|
||||
}
|
||||
.wx-task.wx-critical.wx-selected.wx-GKbcLEGA {
|
||||
border: 1px solid var(--wx-gantt-task-critical-color);
|
||||
}
|
||||
.wx-task.wx-critical .wx-progress-percent {
|
||||
background-color: var(--wx-gantt-task-critical-fill-color);
|
||||
}
|
||||
.wx-milestone.wx-critical.wx-GKbcLEGA .wx-content {
|
||||
background-color: var(--wx-gantt-critical-color);
|
||||
}
|
||||
.wx-milestone.wx-critical.wx-GKbcLEGA {
|
||||
border-color: var(--wx-gantt-critical-color);
|
||||
}
|
||||
.wx-summary.wx-critical.wx-GKbcLEGA {
|
||||
background-color: var(--wx-gantt-summary-critical-color);
|
||||
}
|
||||
.wx-summary.wx-critical .wx-progress-percent {
|
||||
background-color: var(--wx-gantt-summary-critical-fill-color);
|
||||
}
|
||||
.wx-summary.wx-critical.wx-selected.wx-GKbcLEGA {
|
||||
border: 1px solid var(--wx-gantt-summary-critical-color);
|
||||
}
|
||||
|
||||
/*split tasks*/
|
||||
.wx-split.wx-selected.wx-GKbcLEGA {
|
||||
border-color: var(--wx-gantt-task-border-color);
|
||||
}
|
||||
.wx-bars.wx-GKbcLEGA .wx-split.wx-bar {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
.wx-split.wx-GKbcLEGA .wx-link.wx-selected,
|
||||
.wx-split.wx-GKbcLEGA .wx-link.wx-selected .wx-inner {
|
||||
border-color: var(--wx-gantt-task-border-color);
|
||||
}
|
||||
|
||||
.wx-critical.wx-GKbcLEGA .wx-segment {
|
||||
background-color: var(--wx-gantt-task-critical-color);
|
||||
}
|
||||
.wx-critical.wx-selected.wx-GKbcLEGA .wx-segment {
|
||||
border: 1px solid var(--wx-gantt-task-critical-color);
|
||||
}
|
||||
.wx-critical.wx-GKbcLEGA .wx-segment .wx-progress-percent {
|
||||
background-color: var(--wx-gantt-task-critical-fill-color);
|
||||
}
|
||||
.wx-critical.wx-split.wx-GKbcLEGA .wx-link.wx-selected,
|
||||
.wx-critical.wx-split.wx-GKbcLEGA .wx-link.wx-selected .wx-inner {
|
||||
border-color: var(--wx-gantt-task-critical-color);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ import { locate, locateID } from '@svar-ui/lib-dom';
|
||||
import { getID } from '../../helpers/locate';
|
||||
import storeContext from '../../context';
|
||||
import { useStore, useStoreWithCounter } from '@svar-ui/lib-react';
|
||||
import { isSegmentMoveAllowed, extendDragOptions } from '@svar-ui/gantt-store';
|
||||
import { Button } from '@svar-ui/react-core';
|
||||
import Links from './Links.jsx';
|
||||
import BarSegments from './BarSegments.jsx';
|
||||
import './Bars.css';
|
||||
|
||||
function Bars(props) {
|
||||
@@ -18,14 +22,18 @@ function Bars(props) {
|
||||
|
||||
const api = useContext(storeContext);
|
||||
|
||||
const [rTasksValue, rTasksCounter] = useStoreWithCounter(api, "_tasks");
|
||||
const [rLinksValue, rLinksCounter] = useStoreWithCounter(api, "_links");
|
||||
const areaValue = useStore(api, "area");
|
||||
const scalesValue = useStore(api, "_scales");
|
||||
const taskTypesValue = useStore(api, "taskTypes");
|
||||
const baselinesValue = useStore(api, "baselines");
|
||||
const selectedValue = useStore(api, "_selected");
|
||||
const scrollTaskStore = useStore(api, "_scrollTask");
|
||||
const [rTasksValue, rTasksCounter] = useStoreWithCounter(api, '_tasks');
|
||||
const [rLinksValue, rLinksCounter] = useStoreWithCounter(api, '_links');
|
||||
const areaValue = useStore(api, 'area');
|
||||
const scalesValue = useStore(api, '_scales');
|
||||
const taskTypesValue = useStore(api, 'taskTypes');
|
||||
const baselinesValue = useStore(api, 'baselines');
|
||||
const selectedValue = useStore(api, '_selected');
|
||||
const scrollTaskStore = useStore(api, '_scrollTask');
|
||||
const criticalPath = useStore(api, 'criticalPath');
|
||||
const tree = useStore(api, 'tasks');
|
||||
const schedule = useStore(api, 'schedule');
|
||||
const splitTasks = useStore(api, 'splitTasks');
|
||||
|
||||
const tasks = useMemo(() => {
|
||||
if (!areaValue || !Array.isArray(rTasksValue)) return [];
|
||||
@@ -45,6 +53,8 @@ function Bars(props) {
|
||||
const [taskMove, setTaskMove] = useState(null);
|
||||
const progressFromRef = useRef(null);
|
||||
|
||||
const [selectedLink, setSelectedLink] = useState(null);
|
||||
|
||||
const [touched, setTouched] = useState(undefined);
|
||||
const touchTimerRef = useRef(null);
|
||||
|
||||
@@ -66,8 +76,7 @@ function Bars(props) {
|
||||
}, [hasFocus, selectedValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!scrollTaskStore)
|
||||
return;
|
||||
if (!scrollTaskStore) return;
|
||||
if (hasFocus && scrollTaskStore) {
|
||||
const { id } = scrollTaskStore;
|
||||
const node = containerRef.current?.querySelector(
|
||||
@@ -102,13 +111,16 @@ function Bars(props) {
|
||||
|
||||
const getMoveMode = useCallback(
|
||||
(node, e, task) => {
|
||||
if (e.target.classList.contains('wx-line')) return '';
|
||||
if (!task) task = api.getTask(getID(node));
|
||||
if (task.type === 'milestone' || task.type == 'summary') return '';
|
||||
if (task.type === 'milestone' || task.type === 'summary') return '';
|
||||
|
||||
const rect = node.getBoundingClientRect();
|
||||
const p = (e.clientX - rect.left) / rect.width;
|
||||
let delta = 0.2 / (rect.width > 200 ? rect.width / 200 : 1);
|
||||
const segmentNode = locate(e, 'data-segment');
|
||||
if (segmentNode) node = segmentNode;
|
||||
|
||||
const { left, width } = node.getBoundingClientRect();
|
||||
const p = (e.clientX - left) / width;
|
||||
let delta = 0.2 / (width > 200 ? width / 200 : 1);
|
||||
if (p < delta) return 'start';
|
||||
if (p > 1 - delta) return 'end';
|
||||
return '';
|
||||
@@ -122,7 +134,7 @@ function Bars(props) {
|
||||
const id = getID(node);
|
||||
const task = api.getTask(id);
|
||||
const css = point.target.classList;
|
||||
|
||||
if (point.target.closest('.wx-delete-button')) return;
|
||||
if (!readonly) {
|
||||
if (css.contains('wx-progress-marker')) {
|
||||
const { progress } = api.getTask(id);
|
||||
@@ -138,19 +150,29 @@ function Bars(props) {
|
||||
} else {
|
||||
const mode = getMoveMode(node, point, task) || 'move';
|
||||
|
||||
setTaskMove({
|
||||
const newTaskMove = {
|
||||
id,
|
||||
mode,
|
||||
x: clientX,
|
||||
dx: 0,
|
||||
l: task.$x,
|
||||
w: task.$w,
|
||||
});
|
||||
};
|
||||
|
||||
if (splitTasks && task.segments?.length) {
|
||||
const segNode = locate(point, 'data-segment');
|
||||
if (segNode) {
|
||||
newTaskMove.segmentIndex = segNode.dataset['segment'] * 1;
|
||||
extendDragOptions(task, newTaskMove);
|
||||
}
|
||||
}
|
||||
|
||||
setTaskMove(newTaskMove);
|
||||
}
|
||||
startDrag();
|
||||
}
|
||||
},
|
||||
[api, readonly, getMoveMode, startDrag],
|
||||
[api, readonly, getMoveMode, startDrag, splitTasks],
|
||||
);
|
||||
|
||||
const mousedown = useCallback(
|
||||
@@ -162,7 +184,7 @@ function Bars(props) {
|
||||
|
||||
down(node, e);
|
||||
},
|
||||
[readonly, lengthUnitWidth, totalWidth, taskMove, linkFrom],
|
||||
[down],
|
||||
);
|
||||
|
||||
const touchstart = useCallback(
|
||||
@@ -175,22 +197,32 @@ function Bars(props) {
|
||||
}, 300);
|
||||
}
|
||||
},
|
||||
[readonly],
|
||||
[down],
|
||||
);
|
||||
|
||||
const onSelectLink = useCallback(
|
||||
(id) => {
|
||||
setSelectedLink(id && { ...rLinksValue.find((link) => link.id === id) });
|
||||
},
|
||||
[rLinksValue],
|
||||
);
|
||||
|
||||
const up = useCallback(() => {
|
||||
if (progressFromRef.current) {
|
||||
const { dx, id, marker, value } = progressFromRef.current;
|
||||
progressFromRef.current = null;
|
||||
if (typeof value != 'undefined' && dx)
|
||||
api.exec('update-task', { id, task: { progress: value } });
|
||||
api.exec('update-task', {
|
||||
id,
|
||||
task: { progress: value },
|
||||
inProgress: false,
|
||||
});
|
||||
marker.classList.remove('wx-progress-in-drag');
|
||||
|
||||
ignoreNextClickRef.current = true;
|
||||
endDrag();
|
||||
} else if (taskMove) {
|
||||
const { id, mode, dx, l, w, start } = taskMove;
|
||||
const { id, mode, dx, l, w, start, segment, index } = taskMove;
|
||||
setTaskMove(null);
|
||||
if (start) {
|
||||
const diff = Math.round(dx / lengthUnitWidth);
|
||||
@@ -201,19 +233,23 @@ function Bars(props) {
|
||||
width: w,
|
||||
left: l,
|
||||
inProgress: false,
|
||||
...(segment && { segmentIndex: index }),
|
||||
});
|
||||
} else {
|
||||
let update = {};
|
||||
let task = api.getTask(id);
|
||||
if (mode == 'move') {
|
||||
if (segment) task = task.segments[index];
|
||||
|
||||
if (mode === 'move') {
|
||||
update.start = task.start;
|
||||
update.end = task.end;
|
||||
} else update[mode] = task[mode];
|
||||
|
||||
api.exec('update-task', {
|
||||
id,
|
||||
task: update,
|
||||
diff,
|
||||
task: update,
|
||||
...(segment && { segmentIndex: index }),
|
||||
});
|
||||
}
|
||||
ignoreNextClickRef.current = true;
|
||||
@@ -245,14 +281,18 @@ function Bars(props) {
|
||||
inProgress: true,
|
||||
});
|
||||
} else if (taskMove) {
|
||||
const { mode, l, w, x, id, start } = taskMove;
|
||||
onSelectLink(null);
|
||||
const { mode, l, w, x, id, start, segment, index } = taskMove;
|
||||
const task = api.getTask(id);
|
||||
const dx = clientX - x;
|
||||
if (
|
||||
(!start && Math.abs(dx) < 20) ||
|
||||
(mode === 'start' && w - dx < lengthUnitWidth) ||
|
||||
(mode === 'end' && w + dx < lengthUnitWidth) ||
|
||||
(mode == 'move' &&
|
||||
((dx < 0 && l + dx < 0) || (dx > 0 && l + w + dx > totalWidth)))
|
||||
(mode === 'move' &&
|
||||
((dx < 0 && l + dx < 0) ||
|
||||
(dx > 0 && l + w + dx > totalWidth))) ||
|
||||
(taskMove.segment && !isSegmentMoveAllowed(task, taskMove))
|
||||
)
|
||||
return;
|
||||
|
||||
@@ -270,20 +310,18 @@ function Bars(props) {
|
||||
width = w;
|
||||
}
|
||||
|
||||
let ev = {
|
||||
api.exec('drag-task', {
|
||||
id,
|
||||
width: width,
|
||||
left: left,
|
||||
inProgress: true,
|
||||
};
|
||||
...(segment && { segmentIndex: index }),
|
||||
});
|
||||
|
||||
api.exec('drag-task', ev);
|
||||
|
||||
const task = api.getTask(id);
|
||||
if (
|
||||
!nextTaskMove.start &&
|
||||
((mode == 'move' && task.$x == l) ||
|
||||
(mode != 'move' && task.$w == w))
|
||||
((mode === 'move' && task.$x == l) ||
|
||||
(mode !== 'move' && task.$w == w))
|
||||
) {
|
||||
ignoreNextClickRef.current = true;
|
||||
up();
|
||||
@@ -292,15 +330,27 @@ function Bars(props) {
|
||||
nextTaskMove.start = true;
|
||||
setTaskMove(nextTaskMove);
|
||||
} else {
|
||||
const mnode = locate(e);
|
||||
if (mnode) {
|
||||
const mode = getMoveMode(mnode, point);
|
||||
mnode.style.cursor = mode && !readonly ? 'col-resize' : 'pointer';
|
||||
const taskNode = locate(e);
|
||||
if (taskNode) {
|
||||
const task = api.getTask(getID(taskNode));
|
||||
const segNode = locate(e, 'data-segment');
|
||||
const barNode = segNode || taskNode;
|
||||
const mode = getMoveMode(barNode, point, task);
|
||||
barNode.style.cursor = mode && !readonly ? 'col-resize' : 'pointer';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[api, readonly, taskMove, lengthUnitWidth, totalWidth, getMoveMode],
|
||||
[
|
||||
api,
|
||||
readonly,
|
||||
taskMove,
|
||||
lengthUnitWidth,
|
||||
totalWidth,
|
||||
getMoveMode,
|
||||
onSelectLink,
|
||||
up,
|
||||
],
|
||||
);
|
||||
|
||||
const mousemove = useCallback(
|
||||
@@ -347,21 +397,22 @@ function Bars(props) {
|
||||
(e) => {
|
||||
if (!readonly) {
|
||||
const id = locateID(e.target);
|
||||
if (id && !e.target.classList.contains('wx-link'))
|
||||
api.exec('show-editor', { id });
|
||||
if (id && !e.target.classList.contains('wx-link')) {
|
||||
const segmentIndex = locateID(e.target, 'data-segment');
|
||||
api.exec('show-editor', {
|
||||
id,
|
||||
...(segmentIndex !== null && { segmentIndex }),
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[api, readonly],
|
||||
);
|
||||
|
||||
|
||||
const types = ['e2s', 's2s', 'e2e', 's2e'];
|
||||
const getLinkType = useCallback(
|
||||
(fromStart, toStart) => {
|
||||
return types[(fromStart ? 1 : 0) + (toStart ? 0 : 2)];
|
||||
},
|
||||
[],
|
||||
);
|
||||
const getLinkType = useCallback((fromStart, toStart) => {
|
||||
return types[(fromStart ? 1 : 0) + (toStart ? 0 : 2)];
|
||||
}, []);
|
||||
|
||||
const alreadyLinked = useCallback(
|
||||
(target, toStart) => {
|
||||
@@ -413,17 +464,32 @@ function Bars(props) {
|
||||
},
|
||||
});
|
||||
}
|
||||
} else if (css.contains('wx-delete-button-icon')) {
|
||||
api.exec('delete-link', { id: selectedLink.id });
|
||||
setSelectedLink(null);
|
||||
} else {
|
||||
let segmentIndex;
|
||||
const segmentNode = locate(e, 'data-segment');
|
||||
if (segmentNode) segmentIndex = segmentNode.dataset.segment * 1;
|
||||
api.exec('select-task', {
|
||||
id,
|
||||
toggle: e.ctrlKey || e.metaKey,
|
||||
range: e.shiftKey,
|
||||
segmentIndex,
|
||||
});
|
||||
}
|
||||
}
|
||||
removeLinkMarker();
|
||||
},
|
||||
[api, linkFrom, rLinksCounter],
|
||||
[
|
||||
api,
|
||||
linkFrom,
|
||||
rLinksCounter,
|
||||
selectedLink,
|
||||
alreadyLinked,
|
||||
getLinkType,
|
||||
removeLinkMarker,
|
||||
],
|
||||
);
|
||||
|
||||
const taskStyle = useCallback((task) => {
|
||||
@@ -454,15 +520,20 @@ function Bars(props) {
|
||||
[touched],
|
||||
);
|
||||
|
||||
const taskTypeIds = useMemo(
|
||||
() => taskTypesValue.map((t) => t.id),
|
||||
[taskTypesValue],
|
||||
);
|
||||
|
||||
const taskTypeCss = useCallback(
|
||||
(type) => {
|
||||
let css = taskTypesValue.some((t) => type === t.id) ? type : 'task';
|
||||
if (css !== 'task' && css !== 'milestone' && css !== 'summary')
|
||||
let css = taskTypeIds.includes(type) ? type : 'task';
|
||||
if (!['task', 'milestone', 'summary'].includes(type)) {
|
||||
css = `task ${css}`;
|
||||
}
|
||||
return css;
|
||||
},
|
||||
[taskTypesValue],
|
||||
[taskTypeIds],
|
||||
);
|
||||
|
||||
const forward = useCallback(
|
||||
@@ -472,6 +543,35 @@ function Bars(props) {
|
||||
[api],
|
||||
);
|
||||
|
||||
const isTaskCritical = useCallback(
|
||||
(taskId) => {
|
||||
return criticalPath && tree.byId(taskId).$critical;
|
||||
},
|
||||
[criticalPath, tree],
|
||||
);
|
||||
|
||||
const isLinkMarkerVisible = useCallback(
|
||||
(id) => {
|
||||
if (schedule?.auto) {
|
||||
const summaryIds = tree.getSummaryId(id, true);
|
||||
const linkFromSummaryIds = tree.getSummaryId(linkFrom.id, true);
|
||||
return (
|
||||
linkFrom?.id &&
|
||||
!(Array.isArray(summaryIds) ? summaryIds : [summaryIds]).includes(
|
||||
linkFrom.id,
|
||||
) &&
|
||||
!(
|
||||
Array.isArray(linkFromSummaryIds)
|
||||
? linkFromSummaryIds
|
||||
: [linkFromSummaryIds]
|
||||
).includes(id)
|
||||
);
|
||||
}
|
||||
return linkFrom;
|
||||
},
|
||||
[schedule, tree, linkFrom],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="wx-GKbcLEGA wx-bars"
|
||||
@@ -490,86 +590,124 @@ function Bars(props) {
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
<Links
|
||||
onSelectLink={onSelectLink}
|
||||
selectedLink={selectedLink}
|
||||
readonly={readonly}
|
||||
/>
|
||||
{tasks.map((task) => {
|
||||
if (task.$skip && task.$skip_baseline) return null;
|
||||
const barClass =
|
||||
`wx-bar wx-${taskTypeCss(task.type)}` +
|
||||
(touched && taskMove && task.id === taskMove.id ? ' wx-touch' : '') +
|
||||
(linkFrom && linkFrom.id === task.id ? ' wx-selected' : '') +
|
||||
(task.$reorder ? ' wx-reorder-task' : '');
|
||||
(isTaskCritical(task.id) ? ' wx-critical' : '') +
|
||||
(task.$reorder ? ' wx-reorder-task' : '') +
|
||||
(splitTasks && task.segments ? ' wx-split' : '');
|
||||
const leftLinkClass =
|
||||
'wx-link wx-left' +
|
||||
(linkFrom ? ' wx-visible' : '') +
|
||||
(!linkFrom || !alreadyLinked(task.id, true) ? ' wx-target' : '') +
|
||||
(!linkFrom ||
|
||||
(!alreadyLinked(task.id, true) && isLinkMarkerVisible(task.id))
|
||||
? ' wx-target'
|
||||
: '') +
|
||||
(linkFrom && linkFrom.id === task.id && linkFrom.start
|
||||
? ' wx-selected'
|
||||
: '');
|
||||
: '') +
|
||||
(isTaskCritical(task.id) ? ' wx-critical' : '');
|
||||
const rightLinkClass =
|
||||
'wx-link wx-right' +
|
||||
(linkFrom ? ' wx-visible' : '') +
|
||||
(!linkFrom || !alreadyLinked(task.id, false) ? ' wx-target' : '') +
|
||||
(!linkFrom ||
|
||||
(!alreadyLinked(task.id, false) && isLinkMarkerVisible(task.id))
|
||||
? ' wx-target'
|
||||
: '') +
|
||||
(linkFrom && linkFrom.id === task.id && !linkFrom.start
|
||||
? ' wx-selected'
|
||||
: '');
|
||||
: '') +
|
||||
(isTaskCritical(task.id) ? ' wx-critical' : '');
|
||||
return (
|
||||
<Fragment key={task.id}>
|
||||
{!task.$skip && <div
|
||||
className={'wx-GKbcLEGA ' + barClass}
|
||||
style={taskStyle(task)}
|
||||
data-tooltip-id={task.id}
|
||||
data-id={task.id}
|
||||
tabIndex={focused === task.id ? 0 : -1}
|
||||
>
|
||||
{!readonly ? (
|
||||
<div className={'wx-GKbcLEGA ' + leftLinkClass}>
|
||||
<div className="wx-GKbcLEGA wx-inner"></div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{task.type !== 'milestone' ? (
|
||||
<>
|
||||
{task.progress ? (
|
||||
<div className="wx-GKbcLEGA wx-progress-wrapper">
|
||||
<div
|
||||
className="wx-GKbcLEGA wx-progress-percent"
|
||||
style={{ width: `${task.progress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
) : null}
|
||||
{!readonly ? (
|
||||
<div
|
||||
className="wx-GKbcLEGA wx-progress-marker"
|
||||
style={{ left: `calc(${task.progress}% - 10px)` }}
|
||||
{!task.$skip && (
|
||||
<div
|
||||
className={'wx-GKbcLEGA ' + barClass}
|
||||
style={taskStyle(task)}
|
||||
data-tooltip-id={task.id}
|
||||
data-id={task.id}
|
||||
tabIndex={focused === task.id ? 0 : -1}
|
||||
>
|
||||
{!readonly ? (
|
||||
task.id === selectedLink?.target &&
|
||||
selectedLink?.type[2] === 's' ? (
|
||||
<Button
|
||||
type="danger"
|
||||
css="wx-left wx-delete-button wx-delete-link"
|
||||
>
|
||||
{task.progress}
|
||||
</div>
|
||||
) : null}
|
||||
{TaskTemplate ? (
|
||||
<TaskTemplate data={task} api={api} onAction={forward} />
|
||||
<i className="wxi-close wx-delete-button-icon"></i>
|
||||
</Button>
|
||||
) : (
|
||||
<div className="wx-GKbcLEGA wx-content">
|
||||
{task.text || ''}
|
||||
<div className={'wx-GKbcLEGA ' + leftLinkClass}>
|
||||
<div className="wx-GKbcLEGA wx-inner"></div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="wx-GKbcLEGA wx-content"></div>
|
||||
{TaskTemplate ? (
|
||||
<TaskTemplate data={task} api={api} onAction={forward} />
|
||||
) : (
|
||||
<div className="wx-GKbcLEGA wx-text-out">{task.text}</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
)
|
||||
) : null}
|
||||
|
||||
{!readonly ? (
|
||||
<div className={'wx-GKbcLEGA ' + rightLinkClass}>
|
||||
<div className="wx-GKbcLEGA wx-inner"></div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
}
|
||||
{task.type !== 'milestone' ? (
|
||||
<>
|
||||
{task.progress && !(splitTasks && task.segments) ? (
|
||||
<div className="wx-GKbcLEGA wx-progress-wrapper">
|
||||
<div
|
||||
className="wx-GKbcLEGA wx-progress-percent"
|
||||
style={{ width: `${task.progress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
) : null}
|
||||
{!readonly && !(splitTasks && task.segments) ? (
|
||||
<div
|
||||
className="wx-GKbcLEGA wx-progress-marker"
|
||||
style={{ left: `calc(${task.progress}% - 10px)` }}
|
||||
>
|
||||
{task.progress}
|
||||
</div>
|
||||
) : null}
|
||||
{TaskTemplate ? (
|
||||
<TaskTemplate data={task} api={api} onAction={forward} />
|
||||
) : splitTasks && task.segments ? (
|
||||
<BarSegments task={task} type={taskTypeCss(task.type)} />
|
||||
) : (
|
||||
<div className="wx-GKbcLEGA wx-content">
|
||||
{task.text || ''}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="wx-GKbcLEGA wx-content"></div>
|
||||
{TaskTemplate ? (
|
||||
<TaskTemplate data={task} api={api} onAction={forward} />
|
||||
) : (
|
||||
<div className="wx-GKbcLEGA wx-text-out">{task.text}</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!readonly ? (
|
||||
task.id === selectedLink?.target &&
|
||||
selectedLink?.type[2] === 'e' ? (
|
||||
<Button
|
||||
type="danger"
|
||||
css="wx-right wx-delete-button wx-delete-link"
|
||||
>
|
||||
<i className="wxi-close wx-delete-button-icon"></i>
|
||||
</Button>
|
||||
) : (
|
||||
<div className={'wx-GKbcLEGA ' + rightLinkClass}>
|
||||
<div className="wx-GKbcLEGA wx-inner"></div>
|
||||
</div>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
{baselinesValue && !task.$skip_baseline ? (
|
||||
<div
|
||||
className={
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
import {
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useContext, useEffect, useRef, useState } from 'react';
|
||||
import storeContext from '../../context';
|
||||
import { grid } from '@svar-ui/gantt-store';
|
||||
import { useStore } from '@svar-ui/lib-react';
|
||||
|
||||
|
||||
function CellGrid({ borders = '' }) {
|
||||
const api = useContext(storeContext);
|
||||
const cellWidth = useStore(api, "cellWidth");
|
||||
const cellHeight = useStore(api, "cellHeight");
|
||||
const cellWidth = useStore(api, 'cellWidth');
|
||||
const cellHeight = useStore(api, 'cellHeight');
|
||||
|
||||
const nodeRef = useRef(null);
|
||||
const [color, setColor] = useState('#e4e4e4');
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof getComputedStyle !== 'undefined' && nodeRef.current) {
|
||||
const border = getComputedStyle(nodeRef.current).getPropertyValue(
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
.wx-default.wx-mR7v2Xag {
|
||||
background: var(--wx-gantt-marker-color);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,12 +8,12 @@ import {
|
||||
} from 'react';
|
||||
import CellGrid from './CellGrid.jsx';
|
||||
import Bars from './Bars.jsx';
|
||||
import Links from './Links.jsx';
|
||||
import { hotkeys } from '@svar-ui/grid-store';
|
||||
import storeContext from '../../context';
|
||||
import { useStore, useStoreWithCounter } from '@svar-ui/lib-react';
|
||||
import './Chart.css';
|
||||
import TimeScales from '../TimeScale.jsx';
|
||||
import TimeScales from './TimeScale.jsx';
|
||||
import { useRenderTime } from '../../helpers/debug.js';
|
||||
|
||||
function Chart(props) {
|
||||
const {
|
||||
@@ -27,15 +27,14 @@ function Chart(props) {
|
||||
|
||||
const api = useContext(storeContext);
|
||||
|
||||
const [selected, selectedCounter] = useStoreWithCounter(api, "_selected");
|
||||
const rScrollLeft = useStore(api, "scrollLeft");
|
||||
const rScrollTop = useStore(api, "scrollTop");
|
||||
const cellHeight = useStore(api, "cellHeight");
|
||||
const cellWidth = useStore(api, "cellWidth");
|
||||
const scales = useStore(api, "_scales");
|
||||
const markers = useStore(api, "_markers");
|
||||
const rScrollTask = useStore(api, "_scrollTask");
|
||||
const zoom = useStore(api, "zoom");
|
||||
const [selected, selectedCounter] = useStoreWithCounter(api, '_selected');
|
||||
const rScrollTop = useStore(api, 'scrollTop');
|
||||
const cellHeight = useStore(api, 'cellHeight');
|
||||
const cellWidth = useStore(api, 'cellWidth');
|
||||
const scales = useStore(api, '_scales');
|
||||
const markers = useStore(api, '_markers');
|
||||
const rScrollTask = useStore(api, '_scrollTask');
|
||||
const zoom = useStore(api, 'zoom');
|
||||
|
||||
const [chartHeight, setChartHeight] = useState();
|
||||
const chartRef = useRef(null);
|
||||
@@ -95,7 +94,7 @@ function Chart(props) {
|
||||
|
||||
useEffect(() => {
|
||||
dataRequest();
|
||||
}, [chartHeight, rScrollTop, rScrollLeft]);
|
||||
}, [chartHeight, rScrollTop]);
|
||||
|
||||
const showTask = useCallback(
|
||||
(value) => {
|
||||
@@ -110,21 +109,20 @@ function Chart(props) {
|
||||
const task = api.getTask(id);
|
||||
if (task.$x + task.$w < el.scrollLeft) {
|
||||
api.exec('scroll-chart', { left: task.$x - (cellWidth || 0) });
|
||||
el.scrollLeft = task.$x - (cellWidth || 0)
|
||||
el.scrollLeft = task.$x - (cellWidth || 0);
|
||||
} else if (task.$x >= clientWidth + el.scrollLeft) {
|
||||
const width = clientWidth < task.$w ? cellWidth || 0 : task.$w;
|
||||
api.exec('scroll-chart', { left: task.$x - clientWidth + width });
|
||||
el.scrollLeft = task.$x - clientWidth + width;
|
||||
}
|
||||
},
|
||||
[api, cellWidth, rScrollLeft],
|
||||
[api, cellWidth],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
showTask(rScrollTask);
|
||||
}, [rScrollTask]);
|
||||
|
||||
|
||||
function onWheel(e) {
|
||||
if (zoom && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
@@ -156,10 +154,13 @@ function Chart(props) {
|
||||
: null;
|
||||
}, [scales, highlightTime]);
|
||||
|
||||
const handleHotkey = useCallback((ev) => {
|
||||
ev.eventSource = 'chart';
|
||||
api.exec('hotkey', ev);
|
||||
}, [api]);
|
||||
const handleHotkey = useCallback(
|
||||
(ev) => {
|
||||
ev.eventSource = 'chart';
|
||||
api.exec('hotkey', ev);
|
||||
},
|
||||
[api],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const el = chartRef.current;
|
||||
@@ -201,8 +202,10 @@ function Chart(props) {
|
||||
return () => {
|
||||
node.removeEventListener('wheel', handler);
|
||||
};
|
||||
}, [onWheel])
|
||||
}, [onWheel]);
|
||||
|
||||
useRenderTime("chart");
|
||||
|
||||
return (
|
||||
<div
|
||||
className="wx-mR7v2Xag wx-chart"
|
||||
@@ -210,7 +213,7 @@ function Chart(props) {
|
||||
ref={chartRef}
|
||||
onScroll={onScroll}
|
||||
>
|
||||
<TimeScales highlightTime={highlightTime} scales={scales} />
|
||||
<TimeScales highlightTime={highlightTime} scales={scales} />
|
||||
{markers && markers.length ? (
|
||||
<div
|
||||
className="wx-mR7v2Xag wx-markers"
|
||||
@@ -219,7 +222,7 @@ function Chart(props) {
|
||||
{markers.map((marker, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`wx-mR7v2Xag wx-marker ${marker.css || 'wx-default'}`}
|
||||
className={`wx-mR7v2Xag wx-marker ${marker.css || ''}`}
|
||||
style={{ left: `${marker.left}px` }}
|
||||
>
|
||||
<div className="wx-mR7v2Xag wx-content">{marker.text}</div>
|
||||
@@ -256,18 +259,17 @@ function Chart(props) {
|
||||
|
||||
{selected && selected.length
|
||||
? selected.map((obj, index) =>
|
||||
obj.$y ? (
|
||||
<div
|
||||
key={obj.id}
|
||||
className="wx-mR7v2Xag wx-selected"
|
||||
data-id={obj.id}
|
||||
style={selectStyle[index]}
|
||||
></div>
|
||||
) : null,
|
||||
)
|
||||
obj.$y ? (
|
||||
<div
|
||||
key={obj.id}
|
||||
className="wx-mR7v2Xag wx-selected"
|
||||
data-id={obj.id}
|
||||
style={selectStyle[index]}
|
||||
></div>
|
||||
) : null,
|
||||
)
|
||||
: null}
|
||||
|
||||
<Links />
|
||||
<Bars readonly={readonly} taskTemplate={taskTemplate} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,9 +10,28 @@
|
||||
user-select: auto;
|
||||
pointer-events: stroke;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
stroke: var(--wx-gantt-link-color);
|
||||
stroke-width: 2;
|
||||
z-index: 0;
|
||||
fill: transparent;
|
||||
}
|
||||
|
||||
.wx-line-selectable.wx-dkx3NwEn:hover {
|
||||
stroke: var(--wx-gantt-link-color-hovered);
|
||||
}
|
||||
|
||||
.wx-line-selectable.wx-critical.wx-dkx3NwEn:hover {
|
||||
stroke: var(--wx-gantt-link-critical-color-hovered);
|
||||
}
|
||||
|
||||
.wx-line-selectable.wx-dkx3NwEn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wx-line.wx-line-selected.wx-dkx3NwEn {
|
||||
stroke: var(--wx-color-danger);
|
||||
}
|
||||
|
||||
.wx-critical.wx-dkx3NwEn {
|
||||
stroke: var(--wx-gantt-link-critical-color);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,66 @@
|
||||
import { useContext } from 'react';
|
||||
import { useCallback, useContext, useEffect, useRef } from 'react';
|
||||
import storeContext from '../../context';
|
||||
import { useStore } from '@svar-ui/lib-react';
|
||||
import './Links.css';
|
||||
|
||||
export default function Links() {
|
||||
export default function Links({ onSelectLink, selectedLink, readonly }) {
|
||||
const api = useContext(storeContext);
|
||||
const links = useStore(api,"_links");
|
||||
const links = useStore(api, '_links');
|
||||
const criticalPath = useStore(api, 'criticalPath');
|
||||
|
||||
const selectedLineRef = useRef(null);
|
||||
|
||||
const onClickOutside = useCallback(
|
||||
(event) => {
|
||||
const css = event?.target?.classList;
|
||||
if (!css?.contains('wx-line') && !css?.contains('wx-delete-button')) {
|
||||
onSelectLink(null);
|
||||
}
|
||||
},
|
||||
[onSelectLink],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!readonly && selectedLink && selectedLineRef.current) {
|
||||
const handler = (event) => {
|
||||
if (
|
||||
selectedLineRef.current &&
|
||||
!selectedLineRef.current.contains(event.target)
|
||||
) {
|
||||
onClickOutside(event);
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', handler);
|
||||
return () => {
|
||||
document.removeEventListener('click', handler);
|
||||
};
|
||||
}
|
||||
}, [readonly, selectedLink, onClickOutside]);
|
||||
|
||||
return (
|
||||
<svg className="wx-dkx3NwEn wx-links">
|
||||
{(links || []).map((link) => (
|
||||
{(links || []).map((link) => {
|
||||
const className =
|
||||
'wx-dkx3NwEn wx-line' +
|
||||
(criticalPath && link.$critical ? ' wx-critical' : '') +
|
||||
(!readonly ? ' wx-line-selectable' : '');
|
||||
return (
|
||||
<polyline
|
||||
className={className}
|
||||
points={link.$p}
|
||||
key={link.id}
|
||||
onClick={() => !readonly && onSelectLink(link.id)}
|
||||
data-link-id={link.id}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{!readonly && selectedLink && (
|
||||
<polyline
|
||||
className="wx-dkx3NwEn wx-line"
|
||||
points={link.$p}
|
||||
key={link.id}
|
||||
ref={selectedLineRef}
|
||||
className="wx-dkx3NwEn wx-line wx-line-selected wx-line-selectable wx-delete-link"
|
||||
points={selectedLink.$p}
|
||||
/>
|
||||
))}
|
||||
)}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
30
src/components/chart/TimeScale.css
Normal file
30
src/components/chart/TimeScale.css
Normal file
@@ -0,0 +1,30 @@
|
||||
.wx-scale.wx-ZkvhDKir {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: var(--wx-background);
|
||||
box-shadow: var(--wx-timescale-shadow);
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.wx-row.wx-ZkvhDKir,
|
||||
.wx-cell.wx-ZkvhDKir {
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.wx-row.wx-ZkvhDKir {
|
||||
border-bottom: var(--wx-gantt-border);
|
||||
}
|
||||
|
||||
.wx-cell.wx-ZkvhDKir {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-right: var(--wx-timescale-border);
|
||||
font: var(--wx-timescale-font);
|
||||
color: var(--wx-timescale-font-color);
|
||||
}
|
||||
|
||||
.wx-cell.wx-weekend.wx-ZkvhDKir {
|
||||
background: var(--wx-gantt-holiday-background);
|
||||
color: var(--wx-gantt-holiday-color);
|
||||
}
|
||||
42
src/components/chart/TimeScale.jsx
Normal file
42
src/components/chart/TimeScale.jsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useContext } from 'react';
|
||||
import storeContext from '../../context';
|
||||
import { useStore } from '@svar-ui/lib-react';
|
||||
import './TimeScale.css';
|
||||
|
||||
function TimeScale(props) {
|
||||
const { highlightTime } = props;
|
||||
|
||||
const api = useContext(storeContext);
|
||||
const scales = useStore(api, '_scales');
|
||||
|
||||
return (
|
||||
<div className="wx-ZkvhDKir wx-scale" style={{ width: scales.width }}>
|
||||
{(scales?.rows || []).map((row, rowIdx) => (
|
||||
<div
|
||||
className="wx-ZkvhDKir wx-row"
|
||||
style={{ height: `${row.height}px` }}
|
||||
key={rowIdx}
|
||||
>
|
||||
{(row.cells || []).map((cell, cellIdx) => {
|
||||
const extraClass = highlightTime
|
||||
? highlightTime(cell.date, cell.unit)
|
||||
: '';
|
||||
const className =
|
||||
'wx-cell ' + (cell.css || '') + ' ' + (extraClass || '');
|
||||
return (
|
||||
<div
|
||||
className={'wx-ZkvhDKir ' + className}
|
||||
style={{ width: `${cell.width}px` }}
|
||||
key={cellIdx}
|
||||
>
|
||||
{cell.value}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TimeScale;
|
||||
@@ -18,6 +18,10 @@
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.wx-link-lag.wx-j93aYGQf {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.wx-wrapper.wx-j93aYGQf {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useMemo, useContext } from 'react';
|
||||
import { Field, Combo } from '@svar-ui/react-core';
|
||||
import { Field, Combo, Text } from '@svar-ui/react-core';
|
||||
import { context } from '@svar-ui/react-core';
|
||||
import { useStore } from '@svar-ui/lib-react';
|
||||
import './Links.css';
|
||||
@@ -8,8 +8,11 @@ export default function Links({ api, autoSave, onLinksChange }) {
|
||||
const i18n = useContext(context.i18n);
|
||||
const _ = i18n.getGroup('gantt');
|
||||
|
||||
const activeTask = useStore(api,"activeTask");
|
||||
const links = useStore(api,"_links");
|
||||
const activeTask = useStore(api, 'activeTask');
|
||||
const _activeTask = useStore(api, '_activeTask');
|
||||
const links = useStore(api, '_links');
|
||||
const schedule = useStore(api, 'schedule');
|
||||
const unscheduledTasks = useStore(api, 'unscheduledTasks');
|
||||
|
||||
const [linksData, setLinksData] = useState();
|
||||
|
||||
@@ -63,12 +66,11 @@ export default function Links({ api, autoSave, onLinksChange }) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleChange(ev, id) {
|
||||
const value = ev.value;
|
||||
function handleChange(id, update) {
|
||||
if (autoSave) {
|
||||
api.exec('update-link', {
|
||||
id,
|
||||
link: { type: value },
|
||||
link: update,
|
||||
});
|
||||
} else {
|
||||
setLinksData((prev) =>
|
||||
@@ -76,7 +78,7 @@ export default function Links({ api, autoSave, onLinksChange }) {
|
||||
...group,
|
||||
data: group.data.map((item) =>
|
||||
item.link.id === id
|
||||
? { ...item, link: { ...item.link, type: value } }
|
||||
? { ...item, link: { ...item.link, ...update } }
|
||||
: item,
|
||||
),
|
||||
})),
|
||||
@@ -87,7 +89,7 @@ export default function Links({ api, autoSave, onLinksChange }) {
|
||||
action: 'update-link',
|
||||
data: {
|
||||
id,
|
||||
link: { type: value },
|
||||
link: update,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -108,14 +110,31 @@ export default function Links({ api, autoSave, onLinksChange }) {
|
||||
{obj.task.text || ''}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{schedule?.auto && obj.link.type === 'e2s' ? (
|
||||
<td className="wx-j93aYGQf wx-cell wx-link-lag">
|
||||
<Text
|
||||
type="number"
|
||||
placeholder={_('Lag')}
|
||||
value={obj.link.lag}
|
||||
disabled={
|
||||
unscheduledTasks && _activeTask?.unscheduled
|
||||
}
|
||||
onChange={(ev) => {
|
||||
if (!ev.input)
|
||||
handleChange(obj.link.id, { lag: ev.value });
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
) : null}
|
||||
<td className="wx-j93aYGQf wx-cell">
|
||||
<div className="wx-j93aYGQf wx-wrapper">
|
||||
<Combo
|
||||
value={obj.link.type}
|
||||
placeholder={_('Select link type')}
|
||||
options={list}
|
||||
onChange={(e) => handleChange(e, obj.link.id)}
|
||||
onChange={(ev) =>
|
||||
handleChange(obj.link.id, { type: ev.value })
|
||||
}
|
||||
>
|
||||
{({ option }) => option.label}
|
||||
</Combo>
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { context } from '@svar-ui/react-core';
|
||||
import { locateID, locateAttr } from '@svar-ui/lib-dom';
|
||||
import { reorder } from '../../helpers/reorder';
|
||||
import { normalizeDates } from '@svar-ui/gantt-store';
|
||||
import { prepareEditTask } from '@svar-ui/gantt-store';
|
||||
import { Grid as WxGrid } from '@svar-ui/react-grid';
|
||||
import TextCell from './TextCell.jsx';
|
||||
import ActionCell from './ActionCell.jsx';
|
||||
@@ -18,7 +18,14 @@ import storeContext from '../../context';
|
||||
import './Grid.css';
|
||||
|
||||
export default function Grid(props) {
|
||||
const { readonly, compactMode, width = 0, display = 'all', columnWidth: columnWidthProp = 0, onTableAPIChange } = props;
|
||||
const {
|
||||
readonly,
|
||||
compactMode,
|
||||
width = 0,
|
||||
display = 'all',
|
||||
columnWidth: columnWidthProp = 0,
|
||||
onTableAPIChange,
|
||||
} = props;
|
||||
const [columnWidth, setColumnWidthProp] = useWritableProp(columnWidthProp);
|
||||
const [tableAPI, setTableAPI] = useState();
|
||||
|
||||
@@ -26,19 +33,19 @@ export default function Grid(props) {
|
||||
const _ = useMemo(() => i18n.getGroup('gantt'), [i18n]);
|
||||
const api = useContext(storeContext);
|
||||
|
||||
const scrollTopVal = useStore(api, "scrollTop");
|
||||
const cellHeightVal = useStore(api, "cellHeight");
|
||||
const scrollTask = useStore(api, "_scrollTask");
|
||||
const selectedVal = useStore(api, "_selected");
|
||||
const areaVal = useStore(api, "area");
|
||||
const rTasksVal = useStore(api, "_tasks");
|
||||
const scalesVal = useStore(api, "_scales");
|
||||
const columnsVal = useStore(api, "columns");
|
||||
const sortVal = useStore(api, "_sort");
|
||||
const durationUnitVal = useStore(api, "durationUnit");
|
||||
const scrollTopVal = useStore(api, 'scrollTop');
|
||||
const cellHeightVal = useStore(api, 'cellHeight');
|
||||
const scrollTask = useStore(api, '_scrollTask');
|
||||
const selectedVal = useStore(api, '_selected');
|
||||
const areaVal = useStore(api, 'area');
|
||||
const rTasksVal = useStore(api, '_tasks');
|
||||
const scalesVal = useStore(api, '_scales');
|
||||
const columnsVal = useStore(api, 'columns');
|
||||
const sortVal = useStore(api, '_sort');
|
||||
const calendarVal = useStore(api, 'calendar');
|
||||
const durationUnitVal = useStore(api, 'durationUnit');
|
||||
const splitTasksVal = useStore(api, 'splitTasks');
|
||||
|
||||
const touchYRef = useRef(null);
|
||||
const scrollRef = useRef(true);
|
||||
const [dragTask, setDragTask] = useState(null);
|
||||
|
||||
const tasks = useMemo(() => {
|
||||
@@ -61,7 +68,7 @@ export default function Grid(props) {
|
||||
api.exec(action, { id, mode: !task.open });
|
||||
}
|
||||
},
|
||||
[tasks]
|
||||
[tasks],
|
||||
);
|
||||
|
||||
const onClick = useCallback(
|
||||
@@ -102,8 +109,6 @@ export default function Grid(props) {
|
||||
return () => ro.disconnect();
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
const lastDetailRef = useRef(null);
|
||||
|
||||
const reorderTasks = useCallback(
|
||||
@@ -222,7 +227,6 @@ export default function Grid(props) {
|
||||
return {};
|
||||
}, [allTasks, sortVal]);
|
||||
|
||||
|
||||
const checkFlex = useCallback(() => {
|
||||
return cols.some((c) => c.flexgrow && !c.hidden);
|
||||
}, []); // cols defined later; will use latest value when invoked
|
||||
@@ -271,7 +275,6 @@ export default function Grid(props) {
|
||||
return filteredColumns;
|
||||
}, [display, cols, hasFlexCol, columnWidth, width, gridWidth]);
|
||||
|
||||
|
||||
const setColumnWidth = useCallback(
|
||||
(resized) => {
|
||||
if (!checkFlex()) {
|
||||
@@ -307,41 +310,11 @@ export default function Grid(props) {
|
||||
[api, readonly], // cols is defined later; relies on latest value at call time
|
||||
);
|
||||
|
||||
const endScroll = useCallback(() => {
|
||||
scrollRef.current = false;
|
||||
}, []);
|
||||
|
||||
const onTouchstart = useCallback(
|
||||
(e) => {
|
||||
scrollRef.current = true;
|
||||
touchYRef.current = e.touches[0].clientY + (scrollTopVal ?? 0);
|
||||
},
|
||||
[scrollTopVal],
|
||||
);
|
||||
|
||||
const onTouchmove = useCallback(
|
||||
(e) => {
|
||||
if (scrollRef.current) {
|
||||
const delta = (touchYRef.current ?? 0) - e.touches[0].clientY;
|
||||
api.exec('scroll-chart', { top: delta });
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[api],
|
||||
);
|
||||
|
||||
const sel = useMemo(
|
||||
() => (Array.isArray(selectedVal) ? selectedVal.map((o) => o.id) : []),
|
||||
[selectedVal],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => endScroll();
|
||||
window.addEventListener('touchend', handler);
|
||||
return () => window.removeEventListener('touchend', handler);
|
||||
}, [endScroll]);
|
||||
|
||||
const setScrollOffset = useCallback(() => {
|
||||
if (tableRef.current && allTasks !== null) {
|
||||
const body = tableRef.current.querySelector('.wx-body');
|
||||
@@ -373,7 +346,7 @@ export default function Grid(props) {
|
||||
|
||||
useEffect(() => {
|
||||
if (!scrollTask || !tableAPI) return;
|
||||
|
||||
|
||||
const { id } = scrollTask;
|
||||
const focusCell = tableAPI.getState().focusCell;
|
||||
if (
|
||||
@@ -387,7 +360,6 @@ export default function Grid(props) {
|
||||
column: focusCell.column,
|
||||
});
|
||||
}
|
||||
|
||||
}, [scrollTask, tableAPI]);
|
||||
|
||||
const startReorder = useCallback(
|
||||
@@ -438,13 +410,12 @@ export default function Grid(props) {
|
||||
if (!node) return;
|
||||
const action = reorder(node, {
|
||||
start: startReorder,
|
||||
touchStart: endScroll,
|
||||
end: endReorder,
|
||||
move: moveReorder,
|
||||
getTask: api.getTask,
|
||||
});
|
||||
return action.destroy;
|
||||
}, [api, startReorder, endScroll, endReorder, moveReorder]);
|
||||
}, [api, startReorder, endReorder, moveReorder]);
|
||||
|
||||
const handleHotkey = useCallback(
|
||||
(ev) => {
|
||||
@@ -479,69 +450,88 @@ export default function Grid(props) {
|
||||
adjustColumns,
|
||||
setColumnWidth,
|
||||
tasks,
|
||||
calendarVal,
|
||||
durationUnitVal,
|
||||
onTableAPIChange
|
||||
splitTasksVal,
|
||||
onTableAPIChange,
|
||||
};
|
||||
};
|
||||
setHandlersState();
|
||||
useEffect(() => {
|
||||
setHandlersState();
|
||||
}, [setTableAPI, handleHotkey, sortVal, api, adjustColumns, setColumnWidth, tasks, durationUnitVal, onTableAPIChange]);
|
||||
}, [
|
||||
setTableAPI,
|
||||
handleHotkey,
|
||||
sortVal,
|
||||
api,
|
||||
adjustColumns,
|
||||
setColumnWidth,
|
||||
tasks,
|
||||
calendarVal,
|
||||
durationUnitVal,
|
||||
splitTasksVal,
|
||||
onTableAPIChange,
|
||||
]);
|
||||
|
||||
const init = useCallback(
|
||||
(tapi) => {
|
||||
setTableAPI(tapi);
|
||||
tapi.intercept('hotkey', (ev) => handlersStateRef.current.handleHotkey(ev));
|
||||
tapi.intercept('scroll', () => false);
|
||||
tapi.intercept('select-row', () => false);
|
||||
tapi.intercept('sort-rows', (e) => {
|
||||
const sortVal = handlersStateRef.current.sortVal;
|
||||
const { key, add } = e;
|
||||
const keySort = sortVal ? sortVal.find((s) => s.key === key) : null;
|
||||
let order = 'asc';
|
||||
if (keySort)
|
||||
order = !keySort || keySort.order === 'asc' ? 'desc' : 'asc';
|
||||
const init = useCallback((tapi) => {
|
||||
setTableAPI(tapi);
|
||||
tapi.intercept('hotkey', (ev) => handlersStateRef.current.handleHotkey(ev));
|
||||
tapi.intercept('scroll', () => false);
|
||||
tapi.intercept('select-row', () => false);
|
||||
tapi.intercept('sort-rows', (e) => {
|
||||
const sortVal = handlersStateRef.current.sortVal;
|
||||
const { key, add } = e;
|
||||
const keySort = sortVal ? sortVal.find((s) => s.key === key) : null;
|
||||
let order = 'asc';
|
||||
if (keySort) order = !keySort || keySort.order === 'asc' ? 'desc' : 'asc';
|
||||
|
||||
api.exec('sort-tasks', {
|
||||
key,
|
||||
order,
|
||||
add,
|
||||
api.exec('sort-tasks', {
|
||||
key,
|
||||
order,
|
||||
add,
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
tapi.on('resize-column', () => {
|
||||
handlersStateRef.current.setColumnWidth(true);
|
||||
});
|
||||
|
||||
tapi.on('hide-column', (ev) => {
|
||||
if (!ev.mode) handlersStateRef.current.adjustColumns();
|
||||
handlersStateRef.current.setColumnWidth();
|
||||
});
|
||||
|
||||
tapi.intercept('update-cell', (e) => {
|
||||
const { id, column, value } = e;
|
||||
const task = handlersStateRef.current.tasks.find((t) => t.id === id);
|
||||
|
||||
if (task) {
|
||||
const update = { ...task };
|
||||
let v = value;
|
||||
if (v && !isNaN(v) && !(v instanceof Date)) v *= 1;
|
||||
update[column] = v;
|
||||
|
||||
prepareEditTask(
|
||||
update,
|
||||
{
|
||||
calendar: handlersStateRef.current.calendarVal,
|
||||
durationUnit: handlersStateRef.current.durationUnitVal,
|
||||
splitTasks: handlersStateRef.current.splitTasksVal,
|
||||
},
|
||||
column,
|
||||
);
|
||||
|
||||
api.exec('update-task', {
|
||||
id: id,
|
||||
task: update,
|
||||
});
|
||||
return false;
|
||||
});
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
tapi.on('resize-column', () => {
|
||||
handlersStateRef.current.setColumnWidth(true);
|
||||
});
|
||||
|
||||
tapi.on('hide-column', (ev) => {
|
||||
if (!ev.mode) handlersStateRef.current.adjustColumns();
|
||||
handlersStateRef.current.setColumnWidth();
|
||||
});
|
||||
|
||||
tapi.intercept('update-cell', (e) => {
|
||||
const { id, column, value } = e;
|
||||
const task = handlersStateRef.current.tasks.find((t) => t.id === id);
|
||||
|
||||
if (task) {
|
||||
const update = { ...task };
|
||||
let v = value;
|
||||
if (v && !isNaN(v) && !(v instanceof Date)) v *= 1;
|
||||
update[column] = v;
|
||||
|
||||
normalizeDates(update, handlersStateRef.current.durationUnitVal, true, column);
|
||||
|
||||
api.exec('update-task', {
|
||||
id: id,
|
||||
task: update,
|
||||
});
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
onTableAPIChange && onTableAPIChange(tapi);
|
||||
}, [],
|
||||
);
|
||||
onTableAPIChange && onTableAPIChange(tapi);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -553,8 +543,6 @@ export default function Grid(props) {
|
||||
ref={tableRef}
|
||||
style={tableStyle}
|
||||
className="wx-rHj6070p wx-table"
|
||||
onTouchStart={onTouchstart}
|
||||
onTouchMove={onTouchmove}
|
||||
onClick={onClick}
|
||||
onDoubleClick={onDblClick}
|
||||
>
|
||||
|
||||
40
src/helpers/debug.js
Normal file
40
src/helpers/debug.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useRef, useEffect } from 'react';
|
||||
|
||||
const renderMetrics = new Map();
|
||||
|
||||
export function useRenderTime(label) {
|
||||
const startTime = useRef(null);
|
||||
const renderCount = useRef(0);
|
||||
const rafRef = useRef(null);
|
||||
|
||||
const enabled =
|
||||
typeof window !== 'undefined' && window.__RENDER_METRICS_ENABLED__;
|
||||
|
||||
if (startTime.current === null) {
|
||||
startTime.current = performance.now();
|
||||
}
|
||||
renderCount.current++;
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
|
||||
cancelAnimationFrame(rafRef.current);
|
||||
|
||||
rafRef.current = requestAnimationFrame(() => {
|
||||
const metric = {
|
||||
label,
|
||||
time: performance.now() - startTime.current,
|
||||
renders: renderCount.current,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
renderMetrics.set(label, metric);
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('render-metric', { detail: metric }),
|
||||
);
|
||||
});
|
||||
|
||||
return () => cancelAnimationFrame(rafRef.current);
|
||||
});
|
||||
}
|
||||
73
src/helpers/prepareConfig.js
Normal file
73
src/helpers/prepareConfig.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import { dateToString } from '@svar-ui/lib-dom';
|
||||
|
||||
export function getUnitFormats(_) {
|
||||
return {
|
||||
year: '%Y',
|
||||
quarter: `${_('Q')} %Q`,
|
||||
month: '%M',
|
||||
week: `${_('Week')} %w`,
|
||||
day: '%M %j',
|
||||
hour: '%H:%i',
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeFormatter(value, calendarLocale) {
|
||||
return typeof value === 'function'
|
||||
? value
|
||||
: dateToString(value, calendarLocale);
|
||||
}
|
||||
|
||||
export function prepareScales(scales, calendarLocale) {
|
||||
return scales.map(({ format, ...scale }) => ({
|
||||
...scale,
|
||||
format: normalizeFormatter(format, calendarLocale),
|
||||
}));
|
||||
}
|
||||
|
||||
export function prepareFormats(calendarLocale, _) {
|
||||
const formats = getUnitFormats(_);
|
||||
for (let unit in formats) {
|
||||
formats[unit] = dateToString(formats[unit], calendarLocale);
|
||||
}
|
||||
return formats;
|
||||
}
|
||||
|
||||
export function prepareColumns(columns, calendarLocale) {
|
||||
if (!columns || !columns.length) return columns;
|
||||
const format = dateToString('%d-%m-%Y', calendarLocale);
|
||||
|
||||
return columns.map((col) => {
|
||||
if (col.template) return col;
|
||||
|
||||
if (col.id === 'start' || col.id == 'end') {
|
||||
return {
|
||||
...col,
|
||||
//store locale template for unscheduled tasks
|
||||
_template: (b) => format(b),
|
||||
template: (b) => format(b),
|
||||
};
|
||||
}
|
||||
if (col.id === 'duration') {
|
||||
return {
|
||||
...col,
|
||||
_template: (b) => b,
|
||||
template: (b) => b,
|
||||
};
|
||||
}
|
||||
return col;
|
||||
});
|
||||
}
|
||||
|
||||
export function prepareZoom(zoom, calendarLocale) {
|
||||
if (!zoom.levels) {
|
||||
return zoom;
|
||||
}
|
||||
|
||||
return {
|
||||
...zoom,
|
||||
levels: zoom.levels.map((level) => ({
|
||||
...level,
|
||||
scales: prepareScales(level.scales, calendarLocale),
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import Gantt from './components/Gantt.jsx';
|
||||
import Fullscreen from './components/Fullscreen.jsx';
|
||||
import Toolbar from './components/Toolbar.jsx';
|
||||
import ContextMenu from './components/ContextMenu.jsx';
|
||||
import Editor from './components/Editor.jsx';
|
||||
@@ -17,6 +16,9 @@ export {
|
||||
defaultMenuOptions,
|
||||
defaultColumns,
|
||||
defaultTaskTypes,
|
||||
getEditorItems,
|
||||
getToolbarButtons,
|
||||
getMenuOptions,
|
||||
registerScaleUnit,
|
||||
} from '@svar-ui/gantt-store';
|
||||
|
||||
@@ -24,7 +26,6 @@ export { registerEditorItem } from '@svar-ui/react-editor';
|
||||
|
||||
export {
|
||||
Gantt,
|
||||
Fullscreen,
|
||||
ContextMenu,
|
||||
HeaderMenu,
|
||||
Toolbar,
|
||||
|
||||
@@ -9,23 +9,31 @@
|
||||
var(--wx-font-family);
|
||||
--wx-gantt-bar-border-radius: 50px;
|
||||
--wx-gantt-milestone-border-radius: 3px;
|
||||
--wx-gantt-critical-color: #de3a3a;
|
||||
|
||||
--wx-gantt-task-color: #448aff;
|
||||
--wx-gantt-task-font-color: #fff;
|
||||
--wx-gantt-task-fill-color: #246cd9;
|
||||
--wx-gantt-task-border-color: #448aff;
|
||||
--wx-gantt-task-border: 1px solid #246cd9;
|
||||
--wx-gantt-task-critical-color: var(--wx-gantt-critical-color);
|
||||
--wx-gantt-task-critical-fill-color: #c83434;
|
||||
|
||||
--wx-gantt-summary-color: #1de9b6;
|
||||
--wx-gantt-summary-font-color: #5f5f5f;
|
||||
--wx-gantt-summary-fill-color: #00d19a;
|
||||
--wx-gantt-summary-border-color: #1de9b6;
|
||||
--wx-gantt-summary-border: 1px solid #00d19a;
|
||||
--wx-gantt-summary-critical-color: #d9306f;
|
||||
--wx-gantt-summary-critical-fill-color: #c32b64;
|
||||
|
||||
--wx-gantt-milestone-color: #d33daf;
|
||||
|
||||
--wx-gantt-select-color: rgb(201, 244, 240);
|
||||
--wx-gantt-link-color: #87a4bc;
|
||||
--wx-gantt-link-color-hovered: #6e777d;
|
||||
--wx-gantt-link-critical-color: var(--wx-gantt-critical-color);
|
||||
--wx-gantt-link-critical-color-hovered: #b22e2e;
|
||||
--wx-gantt-link-marker-background: #f0f0f0;
|
||||
--wx-gantt-link-marker-color: #87a4bc;
|
||||
|
||||
@@ -52,7 +60,6 @@
|
||||
/* timescale */
|
||||
--wx-timescale-font: 500 12px Roboto;
|
||||
--wx-timescale-font-color: #a6a6a6;
|
||||
--wx-timescale-text-transform: uppercase;
|
||||
--wx-timescale-shadow: 0px 3px 5px rgba(0, 0, 0, 0.1),
|
||||
0px 1px 2px rgba(0, 0, 0, 0.1);
|
||||
--wx-timescale-border: 1px solid transparent;
|
||||
@@ -87,23 +94,31 @@
|
||||
var(--wx-font-family);
|
||||
--wx-gantt-bar-border-radius: 50px;
|
||||
--wx-gantt-milestone-border-radius: 3px;
|
||||
--wx-gantt-critical-color: #de3a3a;
|
||||
|
||||
--wx-gantt-task-color: #448aff;
|
||||
--wx-gantt-task-font-color: #fff;
|
||||
--wx-gantt-task-fill-color: #246cd9;
|
||||
--wx-gantt-task-border-color: #448aff;
|
||||
--wx-gantt-task-border: 1px solid #246cd9;
|
||||
--wx-gantt-task-critical-color: var(--wx-gantt-critical-color);
|
||||
--wx-gantt-task-critical-fill-color: #c83434;
|
||||
|
||||
--wx-gantt-summary-color: #1de9b6;
|
||||
--wx-gantt-summary-font-color: #5f5f5f;
|
||||
--wx-gantt-summary-fill-color: #00d19a;
|
||||
--wx-gantt-summary-border-color: #1de9b6;
|
||||
--wx-gantt-summary-border: 1px solid #00d19a;
|
||||
--wx-gantt-summary-critical-color: #d9306f;
|
||||
--wx-gantt-summary-critical-fill-color: #c32b64;
|
||||
|
||||
--wx-gantt-milestone-color: #d33daf;
|
||||
|
||||
--wx-gantt-select-color: rgb(201, 244, 240);
|
||||
--wx-gantt-link-color: #87a4bc;
|
||||
--wx-gantt-link-color-hovered: #6e777d;
|
||||
--wx-gantt-link-critical-color: var(--wx-gantt-critical-color);
|
||||
--wx-gantt-link-critical-color-hovered: #b22e2e;
|
||||
--wx-gantt-link-marker-background: #f0f0f0;
|
||||
--wx-gantt-link-marker-color: #87a4bc;
|
||||
|
||||
@@ -130,7 +145,6 @@
|
||||
/* timescale */
|
||||
--wx-timescale-font: 500 12px Roboto;
|
||||
--wx-timescale-font-color: #a6a6a6;
|
||||
--wx-timescale-text-transform: uppercase;
|
||||
--wx-timescale-shadow: 0px 3px 5px rgba(0, 0, 0, 0.1),
|
||||
0px 1px 2px rgba(0, 0, 0, 0.1);
|
||||
--wx-timescale-border: 1px solid transparent;
|
||||
|
||||
@@ -9,23 +9,31 @@
|
||||
var(--wx-font-family);
|
||||
--wx-gantt-bar-border-radius: 3px;
|
||||
--wx-gantt-milestone-border-radius: 3px;
|
||||
--wx-gantt-critical-color: #de3a3a;
|
||||
|
||||
--wx-gantt-task-color: #3983eb;
|
||||
--wx-gantt-task-font-color: #fff;
|
||||
--wx-gantt-task-fill-color: #1f6bd9;
|
||||
--wx-gantt-task-border-color: #1f6bd9;
|
||||
--wx-gantt-task-border: 1px solid transparent;
|
||||
--wx-gantt-task-critical-color: var(--wx-gantt-critical-color);
|
||||
--wx-gantt-task-critical-fill-color: #c83434;
|
||||
|
||||
--wx-gantt-summary-color: #00ba94;
|
||||
--wx-gantt-summary-font-color: #ffffff;
|
||||
--wx-gantt-summary-fill-color: #099f81;
|
||||
--wx-gantt-summary-border-color: #099f81;
|
||||
--wx-gantt-summary-border: 1px solid transparent;
|
||||
--wx-gantt-summary-critical-color: #d9306f;
|
||||
--wx-gantt-summary-critical-fill-color: #c32b64;
|
||||
|
||||
--wx-gantt-milestone-color: #ad44ab;
|
||||
|
||||
--wx-gantt-select-color: #eaedf5;
|
||||
--wx-gantt-link-color: #9fa1ae;
|
||||
--wx-gantt-link-color-hovered: #6e777d;
|
||||
--wx-gantt-link-critical-color: var(--wx-gantt-critical-color);
|
||||
--wx-gantt-link-critical-color-hovered: #b22e2e;
|
||||
--wx-gantt-link-marker-background: #eaedf5;
|
||||
--wx-gantt-link-marker-color: #9fa1ae;
|
||||
|
||||
@@ -54,7 +62,6 @@
|
||||
--wx-timescale-font: var(--wx-font-weight-md) var(--wx-font-size-sm)
|
||||
var(--wx-font-family);
|
||||
--wx-timescale-font-color: var(--wx-color-font);
|
||||
--wx-timescale-text-transform: uppercase;
|
||||
--wx-timescale-shadow: none;
|
||||
--wx-timescale-border: var(--wx-gantt-border);
|
||||
|
||||
|
||||
@@ -11,18 +11,23 @@
|
||||
var(--wx-font-family);
|
||||
--wx-gantt-bar-border-radius: 3px;
|
||||
--wx-gantt-milestone-border-radius: 3px;
|
||||
--wx-gantt-critical-color: #de3a3a;
|
||||
|
||||
--wx-gantt-task-color: #37a9ef;
|
||||
--wx-gantt-task-font-color: #ffffffe5;
|
||||
--wx-gantt-task-fill-color: #098cdc;
|
||||
--wx-gantt-task-border-color: #098cdc;
|
||||
--wx-gantt-task-border: 1px solid transparent;
|
||||
--wx-gantt-task-critical-color: var(--wx-gantt-critical-color);
|
||||
--wx-gantt-task-critical-fill-color: #c83434;
|
||||
|
||||
--wx-gantt-summary-color: #00ba94;
|
||||
--wx-gantt-summary-font-color: #ffffffe5;
|
||||
--wx-gantt-summary-fill-color: #099f81;
|
||||
--wx-gantt-summary-border-color: #099f81;
|
||||
--wx-gantt-summary-border: 1px solid transparent;
|
||||
--wx-gantt-summary-critical-color: #d9306f;
|
||||
--wx-gantt-summary-critical-fill-color: #c32b64;
|
||||
|
||||
--wx-gantt-progress-marker-height: 26px;
|
||||
--wx-gantt-progress-border-color: #4b5359;
|
||||
@@ -37,6 +42,9 @@
|
||||
|
||||
--wx-gantt-select-color: #384047;
|
||||
--wx-gantt-link-color: #9fa1ae;
|
||||
--wx-gantt-link-color-hovered: #c8c7cf;
|
||||
--wx-gantt-link-critical-color: var(--wx-gantt-critical-color);
|
||||
--wx-gantt-link-critical-color-hovered: #b22e2e;
|
||||
--wx-gantt-link-marker-background: #384047;
|
||||
--wx-gantt-link-marker-color: #9fa1ae;
|
||||
|
||||
@@ -57,7 +65,6 @@
|
||||
--wx-timescale-font: var(--wx-font-weight-md) var(--wx-font-size-sm)
|
||||
var(--wx-font-family);
|
||||
--wx-timescale-font-color: var(--wx-color-font);
|
||||
--wx-timescale-text-transform: uppercase;
|
||||
--wx-timescale-shadow: none;
|
||||
--wx-timescale-border: var(--wx-border);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user