construction-gantt 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -0
- package/dist/index.cjs +603 -36
- package/dist/index.d.cts +8 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +605 -38
- package/package.json +1 -1
- package/src/Gantt.css +38 -0
- package/src/Gantt.tsx +366 -10
- package/src/editing/dragLink.ts +47 -0
- package/src/editing/useEditState.ts +98 -0
- package/src/editing/usePreviewEngine.ts +47 -0
- package/src/index.ts +1 -0
- package/src/mspdi/parse.ts +37 -3
- package/src/mspdi/serialize.ts +50 -2
- package/src/visibility.ts +3 -5
package/dist/index.cjs
CHANGED
|
@@ -1907,7 +1907,182 @@ function reducer(state, action) {
|
|
|
1907
1907
|
};
|
|
1908
1908
|
}
|
|
1909
1909
|
|
|
1910
|
-
__insertCSS(".construction-gantt-non-working{background:rgba(241,245,249,.65)}.construction-gantt-marker-today{background:#ef4444;color:#fff;font-weight:600}.construction-gantt-marker-milestone{background:#2563eb;color:#fff;font-weight:500}.bode-baseline-ghost{display:flex;align-items:center;gap:6px;height:60%;margin-top:15%;padding:0 8px;border-radius:3px;font-size:9px;font-style:italic;color:#fff;opacity:.85;white-space:nowrap}.bode-baseline-0{background:#6b7280}.bode-baseline-1{background:#3b82f6}.bode-baseline-2{background:#8b5cf6}.bode-baseline-3{background:#14b8a6;color:#0f172a}.bode-baseline-4{background:#f59e0b;color:#0f172a}.bode-baseline-5{background:#ec4899}.bode-baseline-6{background:#06b6d4;color:#0f172a}.bode-baseline-7{background:#84cc16;color:#0f172a}.bode-baseline-8{background:#f97316;color:#0f172a}.bode-baseline-9{background:#a855f7}.bode-baseline-10{background:#dc2626}");
|
|
1910
|
+
__insertCSS(".construction-gantt-non-working{background:rgba(241,245,249,.65)}.construction-gantt-marker-today{background:#ef4444;color:#fff;font-weight:600}.construction-gantt-marker-milestone{background:#2563eb;color:#fff;font-weight:500}.bode-baseline-ghost{display:flex;align-items:center;gap:6px;height:60%;margin-top:15%;padding:0 8px;border-radius:3px;font-size:9px;font-style:italic;color:#fff;opacity:.85;white-space:nowrap}.bode-baseline-0{background:#6b7280}.bode-baseline-1{background:#3b82f6}.bode-baseline-2{background:#8b5cf6}.bode-baseline-3{background:#14b8a6;color:#0f172a}.bode-baseline-4{background:#f59e0b;color:#0f172a}.bode-baseline-5{background:#ec4899}.bode-baseline-6{background:#06b6d4;color:#0f172a}.bode-baseline-7{background:#84cc16;color:#0f172a}.bode-baseline-8{background:#f97316;color:#0f172a}.bode-baseline-9{background:#a855f7}.bode-baseline-10{background:#dc2626}.bode-edit-preview{height:60%;margin-top:15%;background:repeating-linear-gradient(45deg,rgba(59,130,246,.35) 0,rgba(59,130,246,.35) 6px,rgba(59,130,246,.15) 6px,rgba(59,130,246,.15) 12px);border:1.5px dashed rgba(59,130,246,.7);border-radius:3px;pointer-events:none}.construction-gantt-drag-handle{position:absolute;right:-5px;top:50%;transform:translateY(-50%);width:10px;height:10px;border-radius:50%;background:rgba(59,130,246,.85);cursor:crosshair;opacity:0;transition:opacity .15s;z-index:10}.wx-task:hover .construction-gantt-drag-handle{opacity:1}");
|
|
1911
|
+
|
|
1912
|
+
const DRAG_INITIAL = {
|
|
1913
|
+
status: 'idle'
|
|
1914
|
+
};
|
|
1915
|
+
function startDrag(sourceId, startX, startY) {
|
|
1916
|
+
return {
|
|
1917
|
+
status: 'dragging',
|
|
1918
|
+
sourceId,
|
|
1919
|
+
startX,
|
|
1920
|
+
startY,
|
|
1921
|
+
cursorX: startX,
|
|
1922
|
+
cursorY: startY
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
function moveDrag(state, cursorX, cursorY) {
|
|
1926
|
+
if (state.status !== 'dragging') return state;
|
|
1927
|
+
return {
|
|
1928
|
+
...state,
|
|
1929
|
+
cursorX,
|
|
1930
|
+
cursorY
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
function cancelDrag(_state) {
|
|
1934
|
+
return DRAG_INITIAL;
|
|
1935
|
+
}
|
|
1936
|
+
function isDragInvalid(sourceId, targetId, existingLinks, summaryIds) {
|
|
1937
|
+
if (String(sourceId) === String(targetId)) return true;
|
|
1938
|
+
if ([
|
|
1939
|
+
...summaryIds
|
|
1940
|
+
].some((id)=>String(id) === String(targetId))) return true;
|
|
1941
|
+
return existingLinks.some((l)=>String(l.source) === String(sourceId) && String(l.target) === String(targetId));
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
function parseFieldValue(field, value) {
|
|
1945
|
+
switch(field){
|
|
1946
|
+
case 'text':
|
|
1947
|
+
return {
|
|
1948
|
+
text: value
|
|
1949
|
+
};
|
|
1950
|
+
case 'start':
|
|
1951
|
+
{
|
|
1952
|
+
const d = new Date(`${value}T08:00:00`);
|
|
1953
|
+
if (Number.isNaN(d.getTime())) return {};
|
|
1954
|
+
return {
|
|
1955
|
+
start: d
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
case 'end':
|
|
1959
|
+
{
|
|
1960
|
+
const d = new Date(`${value}T08:00:00`);
|
|
1961
|
+
if (Number.isNaN(d.getTime())) return {};
|
|
1962
|
+
return {
|
|
1963
|
+
end: d
|
|
1964
|
+
};
|
|
1965
|
+
}
|
|
1966
|
+
case 'duration':
|
|
1967
|
+
{
|
|
1968
|
+
const n = Number(value);
|
|
1969
|
+
if (!Number.isFinite(n) || n < 0) return {};
|
|
1970
|
+
return {
|
|
1971
|
+
duration: Math.round(n * 8 * 60)
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
case 'progress':
|
|
1975
|
+
{
|
|
1976
|
+
const n = Number(value);
|
|
1977
|
+
if (!Number.isFinite(n)) return {};
|
|
1978
|
+
return {
|
|
1979
|
+
progress: Math.min(100, Math.max(0, n))
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
case 'scheduleMode':
|
|
1983
|
+
{
|
|
1984
|
+
if (value !== 'auto' && value !== 'manual') return {};
|
|
1985
|
+
return {
|
|
1986
|
+
scheduleMode: value
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
function useEditState() {
|
|
1992
|
+
const [state, setState] = react.useState({
|
|
1993
|
+
activeCell: null,
|
|
1994
|
+
dirtyValue: ''
|
|
1995
|
+
});
|
|
1996
|
+
// stateRef lets commitCell read the latest state without stale closures.
|
|
1997
|
+
const stateRef = react.useRef(state);
|
|
1998
|
+
stateRef.current = state;
|
|
1999
|
+
const activateCell = react.useCallback((taskId, field, initialValue)=>{
|
|
2000
|
+
setState({
|
|
2001
|
+
activeCell: {
|
|
2002
|
+
taskId,
|
|
2003
|
+
field
|
|
2004
|
+
},
|
|
2005
|
+
dirtyValue: initialValue
|
|
2006
|
+
});
|
|
2007
|
+
}, []);
|
|
2008
|
+
const setValue = react.useCallback((value)=>{
|
|
2009
|
+
setState((prev)=>({
|
|
2010
|
+
...prev,
|
|
2011
|
+
dirtyValue: value
|
|
2012
|
+
}));
|
|
2013
|
+
}, []);
|
|
2014
|
+
const commitCell = react.useCallback((onTaskEdit)=>{
|
|
2015
|
+
const { activeCell, dirtyValue } = stateRef.current;
|
|
2016
|
+
if (activeCell === null) return;
|
|
2017
|
+
const patch = parseFieldValue(activeCell.field, dirtyValue);
|
|
2018
|
+
onTaskEdit?.(activeCell.taskId, patch);
|
|
2019
|
+
setState({
|
|
2020
|
+
activeCell: null,
|
|
2021
|
+
dirtyValue: ''
|
|
2022
|
+
});
|
|
2023
|
+
}, []);
|
|
2024
|
+
const cancelCell = react.useCallback(()=>{
|
|
2025
|
+
setState({
|
|
2026
|
+
activeCell: null,
|
|
2027
|
+
dirtyValue: ''
|
|
2028
|
+
});
|
|
2029
|
+
}, []);
|
|
2030
|
+
return {
|
|
2031
|
+
activeCell: state.activeCell,
|
|
2032
|
+
dirtyValue: state.dirtyValue,
|
|
2033
|
+
activateCell,
|
|
2034
|
+
setValue,
|
|
2035
|
+
commitCell,
|
|
2036
|
+
cancelCell
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
function usePreviewEngine(committed, activeCell, dirtyValue, debounceMs = 80) {
|
|
2041
|
+
const [ghostProject, setGhostProject] = react.useState(null);
|
|
2042
|
+
const timerRef = react.useRef(null);
|
|
2043
|
+
react.useEffect(()=>{
|
|
2044
|
+
const CPM_IRRELEVANT_FIELDS = new Set([
|
|
2045
|
+
'text',
|
|
2046
|
+
'progress',
|
|
2047
|
+
'scheduleMode'
|
|
2048
|
+
]);
|
|
2049
|
+
if (activeCell === null || CPM_IRRELEVANT_FIELDS.has(activeCell.field)) {
|
|
2050
|
+
setGhostProject(null);
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
2053
|
+
if (timerRef.current !== null) clearTimeout(timerRef.current);
|
|
2054
|
+
timerRef.current = setTimeout(()=>{
|
|
2055
|
+
const patch = parseFieldValue(activeCell.field, dirtyValue);
|
|
2056
|
+
if (Object.keys(patch).length === 0) {
|
|
2057
|
+
setGhostProject(null); // invalid input — suppress ghost
|
|
2058
|
+
return;
|
|
2059
|
+
}
|
|
2060
|
+
const patchedTasks = committed.tasks.map((t)=>t.id === activeCell.taskId ? {
|
|
2061
|
+
...t,
|
|
2062
|
+
...patch
|
|
2063
|
+
} : t);
|
|
2064
|
+
setGhostProject(schedule({
|
|
2065
|
+
...committed,
|
|
2066
|
+
tasks: patchedTasks
|
|
2067
|
+
}));
|
|
2068
|
+
}, debounceMs);
|
|
2069
|
+
return ()=>{
|
|
2070
|
+
if (timerRef.current !== null) clearTimeout(timerRef.current);
|
|
2071
|
+
};
|
|
2072
|
+
}, [
|
|
2073
|
+
committed,
|
|
2074
|
+
activeCell,
|
|
2075
|
+
dirtyValue,
|
|
2076
|
+
debounceMs
|
|
2077
|
+
]);
|
|
2078
|
+
// Clear immediately when cell deactivates (don't wait for debounce timeout).
|
|
2079
|
+
react.useEffect(()=>{
|
|
2080
|
+
if (activeCell === null) setGhostProject(null);
|
|
2081
|
+
}, [
|
|
2082
|
+
activeCell
|
|
2083
|
+
]);
|
|
2084
|
+
return ghostProject;
|
|
2085
|
+
}
|
|
1911
2086
|
|
|
1912
2087
|
// Render-only visibility filter for <Gantt> tasks.
|
|
1913
2088
|
//
|
|
@@ -1916,11 +2091,9 @@ __insertCSS(".construction-gantt-non-working{background:rgba(241,245,249,.65)}.c
|
|
|
1916
2091
|
// This is the canonical example: the engine always runs on the full task
|
|
1917
2092
|
// set; the visibility filter applies only to the rendered output.
|
|
1918
2093
|
//
|
|
1919
|
-
//
|
|
1920
|
-
//
|
|
1921
|
-
// task's early-start collapse to 0
|
|
1922
|
-
// by structure — the filter sits AFTER schedule() in the pipeline, so
|
|
1923
|
-
// computed fields on the visible tasks reflect the full schedule.
|
|
2094
|
+
// The filter sits AFTER schedule() in the pipeline so computed fields on
|
|
2095
|
+
// the visible tasks reflect the full schedule — hiding a predecessor must
|
|
2096
|
+
// not cause a dependent task's early-start to collapse to 0.
|
|
1924
2097
|
/**
|
|
1925
2098
|
* Return the subset of `tasks` whose ids appear in `visibleTaskIds`.
|
|
1926
2099
|
*
|
|
@@ -1938,7 +2111,84 @@ __insertCSS(".construction-gantt-non-working{background:rgba(241,245,249,.65)}.c
|
|
|
1938
2111
|
return tasks.filter((t)=>visibleTaskIds.has(t.id));
|
|
1939
2112
|
}
|
|
1940
2113
|
|
|
1941
|
-
const
|
|
2114
|
+
const DEFAULT_EDIT_COLUMNS = [
|
|
2115
|
+
{
|
|
2116
|
+
id: 'text',
|
|
2117
|
+
header: 'Task Name',
|
|
2118
|
+
field: 'text',
|
|
2119
|
+
width: 220
|
|
2120
|
+
},
|
|
2121
|
+
{
|
|
2122
|
+
id: 'start',
|
|
2123
|
+
header: 'Start',
|
|
2124
|
+
field: 'start',
|
|
2125
|
+
width: 100,
|
|
2126
|
+
align: 'center'
|
|
2127
|
+
},
|
|
2128
|
+
{
|
|
2129
|
+
id: 'end',
|
|
2130
|
+
header: 'Finish',
|
|
2131
|
+
field: 'end',
|
|
2132
|
+
width: 100,
|
|
2133
|
+
align: 'center'
|
|
2134
|
+
},
|
|
2135
|
+
{
|
|
2136
|
+
id: 'duration',
|
|
2137
|
+
header: 'Days',
|
|
2138
|
+
field: 'duration',
|
|
2139
|
+
width: 60,
|
|
2140
|
+
align: 'right'
|
|
2141
|
+
},
|
|
2142
|
+
{
|
|
2143
|
+
id: 'progress',
|
|
2144
|
+
header: '%',
|
|
2145
|
+
field: 'progress',
|
|
2146
|
+
width: 50,
|
|
2147
|
+
align: 'right'
|
|
2148
|
+
}
|
|
2149
|
+
];
|
|
2150
|
+
function useDragLink() {
|
|
2151
|
+
const [dragState, setDragState] = react.useState(DRAG_INITIAL);
|
|
2152
|
+
const dragRef = react.useRef(dragState);
|
|
2153
|
+
dragRef.current = dragState;
|
|
2154
|
+
const onBarMouseDown = react.useCallback((sourceId, e)=>{
|
|
2155
|
+
e.preventDefault();
|
|
2156
|
+
setDragState(startDrag(sourceId, e.clientX, e.clientY));
|
|
2157
|
+
}, []);
|
|
2158
|
+
const onMouseMove = react.useCallback((e)=>{
|
|
2159
|
+
setDragState((s)=>moveDrag(s, e.clientX, e.clientY));
|
|
2160
|
+
}, []);
|
|
2161
|
+
const onMouseUp = react.useCallback((e, project, onLinkCreate)=>{
|
|
2162
|
+
const current = dragRef.current;
|
|
2163
|
+
if (current.status !== 'dragging') return;
|
|
2164
|
+
const el = document.elementFromPoint(e.clientX, e.clientY);
|
|
2165
|
+
const barEl = el?.closest('[data-task-id]');
|
|
2166
|
+
const targetAttr = barEl?.getAttribute('data-task-id') ?? null;
|
|
2167
|
+
// Resolve to a real task — rejects phantom row ids (e.g. "t1__baseline_0")
|
|
2168
|
+
// and preserves the original typed TaskId (fixes numeric id loss via DOM attribute).
|
|
2169
|
+
const targetTask = targetAttr ? project.tasks.find((t)=>String(t.id) === targetAttr) : undefined;
|
|
2170
|
+
if (!targetTask) {
|
|
2171
|
+
setDragState(cancelDrag());
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
2174
|
+
const summaryIds = new Set(project.tasks.filter((t)=>t.type === 'summary').map((t)=>t.id));
|
|
2175
|
+
if (isDragInvalid(current.sourceId, targetTask.id, project.links, summaryIds)) {
|
|
2176
|
+
setDragState(cancelDrag());
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
onLinkCreate?.(current.sourceId, targetTask.id, 'FS');
|
|
2180
|
+
setDragState(DRAG_INITIAL);
|
|
2181
|
+
}, []);
|
|
2182
|
+
const cancelDragLink = react.useCallback(()=>setDragState(DRAG_INITIAL), []);
|
|
2183
|
+
return {
|
|
2184
|
+
dragState,
|
|
2185
|
+
onBarMouseDown,
|
|
2186
|
+
onMouseMove,
|
|
2187
|
+
onMouseUp,
|
|
2188
|
+
cancelDragLink
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2191
|
+
const Gantt = /*#__PURE__*/ react.forwardRef(function Gantt({ project, height = 500, cellWidth = 48, cellHeight = 42, preScheduled = false, markers, baselineIndex, baselineIndices, showBaselineBars, columns, visibleTaskIds, editMode = false, onTaskEdit, onLinkCreate, onLinkDelete }, ref) {
|
|
1942
2192
|
const containerRef = react.useRef(null);
|
|
1943
2193
|
const effectiveBaselineIndices = react.useMemo(()=>resolveEffectiveBaselineIndices(baselineIndices, baselineIndex), [
|
|
1944
2194
|
baselineIndices,
|
|
@@ -1958,6 +2208,37 @@ const Gantt = /*#__PURE__*/ react.forwardRef(function Gantt({ project, height =
|
|
|
1958
2208
|
scheduled.calendars,
|
|
1959
2209
|
scheduled.defaultCalendarId
|
|
1960
2210
|
]);
|
|
2211
|
+
const editState = useEditState();
|
|
2212
|
+
const editStateRef = react.useRef(editState);
|
|
2213
|
+
editStateRef.current = editState;
|
|
2214
|
+
const onTaskEditRef = react.useRef(onTaskEdit);
|
|
2215
|
+
onTaskEditRef.current = onTaskEdit;
|
|
2216
|
+
const editGhostProject = usePreviewEngine(scheduled, editState.activeCell, editState.dirtyValue);
|
|
2217
|
+
const { dragState, onBarMouseDown, onMouseMove, onMouseUp, cancelDragLink } = useDragLink();
|
|
2218
|
+
react.useEffect(()=>{
|
|
2219
|
+
if (!editMode || dragState.status !== 'dragging') return;
|
|
2220
|
+
const handleMove = (e)=>onMouseMove(e);
|
|
2221
|
+
const handleUp = (e)=>onMouseUp(e, scheduled, onLinkCreate);
|
|
2222
|
+
const handleEsc = (e)=>{
|
|
2223
|
+
if (e.key === 'Escape') cancelDragLink();
|
|
2224
|
+
};
|
|
2225
|
+
window.addEventListener('mousemove', handleMove);
|
|
2226
|
+
window.addEventListener('mouseup', handleUp);
|
|
2227
|
+
window.addEventListener('keydown', handleEsc);
|
|
2228
|
+
return ()=>{
|
|
2229
|
+
window.removeEventListener('mousemove', handleMove);
|
|
2230
|
+
window.removeEventListener('mouseup', handleUp);
|
|
2231
|
+
window.removeEventListener('keydown', handleEsc);
|
|
2232
|
+
};
|
|
2233
|
+
}, [
|
|
2234
|
+
editMode,
|
|
2235
|
+
dragState.status,
|
|
2236
|
+
onMouseMove,
|
|
2237
|
+
onMouseUp,
|
|
2238
|
+
cancelDragLink,
|
|
2239
|
+
scheduled,
|
|
2240
|
+
onLinkCreate
|
|
2241
|
+
]);
|
|
1961
2242
|
// Resolve effective indices → actual Baseline records, dropping any that
|
|
1962
2243
|
// don't exist on the project. Preserves caller order so phantom rows
|
|
1963
2244
|
// render in the array order the consumer passed.
|
|
@@ -1966,11 +2247,12 @@ const Gantt = /*#__PURE__*/ react.forwardRef(function Gantt({ project, height =
|
|
|
1966
2247
|
effectiveBaselineIndices
|
|
1967
2248
|
]);
|
|
1968
2249
|
const ghostBarsEnabled = resolvedBaselines.length > 0 && (showBaselineBars ?? true);
|
|
1969
|
-
const svarTasks = react.useMemo(()=>buildSvarTasks(renderableTasks, resolvedBaselines, calendar, ghostBarsEnabled), [
|
|
2250
|
+
const svarTasks = react.useMemo(()=>buildSvarTasks(renderableTasks, resolvedBaselines, calendar, ghostBarsEnabled, editGhostProject ?? undefined), [
|
|
1970
2251
|
renderableTasks,
|
|
1971
2252
|
resolvedBaselines,
|
|
1972
2253
|
calendar,
|
|
1973
|
-
ghostBarsEnabled
|
|
2254
|
+
ghostBarsEnabled,
|
|
2255
|
+
editGhostProject
|
|
1974
2256
|
]);
|
|
1975
2257
|
const svarLinks = react.useMemo(()=>scheduled.links.map(toSvarLink), [
|
|
1976
2258
|
scheduled.links
|
|
@@ -1990,12 +2272,59 @@ const Gantt = /*#__PURE__*/ react.forwardRef(function Gantt({ project, height =
|
|
|
1990
2272
|
// undefined → don't pass columns to SVAR (use SVAR defaults).
|
|
1991
2273
|
// [] → pass false to SVAR (hide grid entirely).
|
|
1992
2274
|
// [...] → convert each column.
|
|
2275
|
+
// In editMode, inject interactive cell renderers for known editable fields.
|
|
2276
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: editState.activeCell is intentionally used as a dep — editStateRef/onTaskEditRef are accessed at event time via refs, so only activeCell (which changes cell-to-cell) drives column recompute
|
|
1993
2277
|
const svarColumns = react.useMemo(()=>{
|
|
1994
|
-
if (
|
|
1995
|
-
|
|
1996
|
-
|
|
2278
|
+
if (!editMode) {
|
|
2279
|
+
if (columns === undefined) return undefined;
|
|
2280
|
+
if (columns.length === 0) return false;
|
|
2281
|
+
return columns.map(toSvarColumn);
|
|
2282
|
+
}
|
|
2283
|
+
const effectiveCols = columns ?? DEFAULT_EDIT_COLUMNS;
|
|
2284
|
+
if (effectiveCols.length === 0) return false;
|
|
2285
|
+
return effectiveCols.map((col)=>{
|
|
2286
|
+
if (col.render || !col.field || !EDITABLE_FIELDS.has(col.field)) {
|
|
2287
|
+
return toSvarColumn(col);
|
|
2288
|
+
}
|
|
2289
|
+
const base = toSvarColumn(col);
|
|
2290
|
+
return {
|
|
2291
|
+
...base,
|
|
2292
|
+
cell: buildEditableCell(col.field, editStateRef, onTaskEditRef)
|
|
2293
|
+
};
|
|
2294
|
+
});
|
|
1997
2295
|
}, [
|
|
1998
|
-
columns
|
|
2296
|
+
columns,
|
|
2297
|
+
editMode,
|
|
2298
|
+
editState.activeCell
|
|
2299
|
+
]);
|
|
2300
|
+
const taskTemplate = react.useMemo(()=>{
|
|
2301
|
+
if (!editMode) return ConstructionBar;
|
|
2302
|
+
// Wrap ConstructionBar with a drag handle at the right edge of each task bar.
|
|
2303
|
+
const EditableBar = ({ data })=>/*#__PURE__*/ jsxRuntime.jsxs("div", {
|
|
2304
|
+
"data-task-id": data.id !== undefined ? String(data.id) : undefined,
|
|
2305
|
+
style: {
|
|
2306
|
+
position: 'relative',
|
|
2307
|
+
width: '100%',
|
|
2308
|
+
height: '100%'
|
|
2309
|
+
},
|
|
2310
|
+
children: [
|
|
2311
|
+
/*#__PURE__*/ jsxRuntime.jsx(ConstructionBar, {
|
|
2312
|
+
data: data
|
|
2313
|
+
}),
|
|
2314
|
+
!data.is_baseline_ghost && !data.is_edit_preview && // biome-ignore lint/a11y/noStaticElementInteractions: drag handle — pointer-down initiates link drag; keyboard alternative (Escape) handled at window level
|
|
2315
|
+
/*#__PURE__*/ jsxRuntime.jsx("div", {
|
|
2316
|
+
className: "construction-gantt-drag-handle",
|
|
2317
|
+
onMouseDown: (e)=>{
|
|
2318
|
+
if (data.id !== undefined) onBarMouseDown(data.id, e);
|
|
2319
|
+
},
|
|
2320
|
+
title: "Drag to create link"
|
|
2321
|
+
})
|
|
2322
|
+
]
|
|
2323
|
+
});
|
|
2324
|
+
return EditableBar;
|
|
2325
|
+
}, [
|
|
2326
|
+
editMode,
|
|
2327
|
+
onBarMouseDown
|
|
1999
2328
|
]);
|
|
2000
2329
|
react.useImperativeHandle(ref, ()=>({
|
|
2001
2330
|
async exportPNG (options) {
|
|
@@ -2053,28 +2382,67 @@ const Gantt = /*#__PURE__*/ react.forwardRef(function Gantt({ project, height =
|
|
|
2053
2382
|
height,
|
|
2054
2383
|
visibleTaskIds
|
|
2055
2384
|
]);
|
|
2056
|
-
return
|
|
2385
|
+
return(// biome-ignore lint/a11y/noStaticElementInteractions: gantt container — link-delete click is an optional editing affordance, not a primary interaction target
|
|
2386
|
+
// biome-ignore lint/a11y/useKeyWithClickEvents: keyboard alternative (Delete key) is out of scope for v0.4; Escape already handled in drag listener
|
|
2387
|
+
/*#__PURE__*/ jsxRuntime.jsxs("div", {
|
|
2057
2388
|
ref: containerRef,
|
|
2058
2389
|
style: {
|
|
2390
|
+
position: 'relative',
|
|
2059
2391
|
height
|
|
2060
2392
|
},
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2393
|
+
onClick: editMode && onLinkDelete ? (e)=>{
|
|
2394
|
+
// SVAR-internal: dependency arrows render as <polyline class="wx-line ..."> with
|
|
2395
|
+
// data-link-id holding the link id. Re-verify on SVAR upgrades by searching
|
|
2396
|
+
// node_modules/@svar-ui/react-gantt/dist/index.es.js for "wx-line" + "data-link-id".
|
|
2397
|
+
const el = e.target.closest('.wx-line');
|
|
2398
|
+
if (!el) return;
|
|
2399
|
+
const linkId = el.getAttribute('data-link-id');
|
|
2400
|
+
if (linkId) onLinkDelete(linkId);
|
|
2401
|
+
} : undefined,
|
|
2402
|
+
children: [
|
|
2403
|
+
/*#__PURE__*/ jsxRuntime.jsx(reactGantt.Gantt, {
|
|
2404
|
+
tasks: svarTasks,
|
|
2405
|
+
links: svarLinks,
|
|
2406
|
+
start: scheduled.start,
|
|
2407
|
+
end: projectEnd,
|
|
2408
|
+
cellWidth: cellWidth,
|
|
2409
|
+
cellHeight: cellHeight,
|
|
2410
|
+
markers: svarMarkers,
|
|
2411
|
+
highlightTime: highlightTime,
|
|
2412
|
+
taskTemplate: taskTemplate,
|
|
2413
|
+
...svarColumns !== undefined ? {
|
|
2414
|
+
columns: svarColumns
|
|
2415
|
+
} : {}
|
|
2416
|
+
}),
|
|
2417
|
+
editMode && dragState.status === 'dragging' && /*#__PURE__*/ jsxRuntime.jsx("svg", {
|
|
2418
|
+
"aria-hidden": "true",
|
|
2419
|
+
style: {
|
|
2420
|
+
position: 'fixed',
|
|
2421
|
+
inset: 0,
|
|
2422
|
+
width: '100vw',
|
|
2423
|
+
height: '100vh',
|
|
2424
|
+
pointerEvents: 'none',
|
|
2425
|
+
zIndex: 9999
|
|
2426
|
+
},
|
|
2427
|
+
children: /*#__PURE__*/ jsxRuntime.jsx("line", {
|
|
2428
|
+
x1: dragState.startX,
|
|
2429
|
+
y1: dragState.startY,
|
|
2430
|
+
x2: dragState.cursorX,
|
|
2431
|
+
y2: dragState.cursorY,
|
|
2432
|
+
stroke: "rgba(59,130,246,0.8)",
|
|
2433
|
+
strokeWidth: 2,
|
|
2434
|
+
strokeDasharray: "6 3"
|
|
2435
|
+
})
|
|
2436
|
+
})
|
|
2437
|
+
]
|
|
2438
|
+
}));
|
|
2076
2439
|
});
|
|
2077
2440
|
const ConstructionBar = ({ data })=>{
|
|
2441
|
+
if (data.is_edit_preview) {
|
|
2442
|
+
return /*#__PURE__*/ jsxRuntime.jsx("div", {
|
|
2443
|
+
className: "bode-edit-preview"
|
|
2444
|
+
});
|
|
2445
|
+
}
|
|
2078
2446
|
// Phantom baseline row — render a slim outlined ghost bar.
|
|
2079
2447
|
if (data.is_baseline_ghost) {
|
|
2080
2448
|
const baselineIdx = data.baseline_index ?? 0;
|
|
@@ -2298,6 +2666,20 @@ function makeBaselinePhantom(t, baseline, calendar) {
|
|
|
2298
2666
|
is_ahead: startVariance <= -30
|
|
2299
2667
|
};
|
|
2300
2668
|
}
|
|
2669
|
+
function makeEditPreviewPhantom(liveTask, ghostTask) {
|
|
2670
|
+
return {
|
|
2671
|
+
id: `${liveTask.id}__edit_preview`,
|
|
2672
|
+
text: 'Preview',
|
|
2673
|
+
start: ghostTask.start,
|
|
2674
|
+
end: ghostTask.end,
|
|
2675
|
+
duration: ghostTask.duration,
|
|
2676
|
+
progress: 0,
|
|
2677
|
+
type: 'task',
|
|
2678
|
+
parent: liveTask.parent,
|
|
2679
|
+
is_baseline_ghost: true,
|
|
2680
|
+
is_edit_preview: true
|
|
2681
|
+
};
|
|
2682
|
+
}
|
|
2301
2683
|
function toSvarLink(l) {
|
|
2302
2684
|
return {
|
|
2303
2685
|
id: l.id,
|
|
@@ -2398,10 +2780,24 @@ function getProjectEnd(p) {
|
|
|
2398
2780
|
* gets its own.
|
|
2399
2781
|
*
|
|
2400
2782
|
* Exported for testing. Not part of the public surface.
|
|
2401
|
-
*/ function buildSvarTasks(renderableTasks, resolvedBaselines, calendar, ghostBarsEnabled) {
|
|
2783
|
+
*/ function buildSvarTasks(renderableTasks, resolvedBaselines, calendar, ghostBarsEnabled, editGhostProject) {
|
|
2784
|
+
const ghostById = new Map(editGhostProject?.tasks.map((t)=>[
|
|
2785
|
+
t.id,
|
|
2786
|
+
t
|
|
2787
|
+
]) ?? []);
|
|
2402
2788
|
if (!ghostBarsEnabled || resolvedBaselines.length === 0) {
|
|
2403
2789
|
const primary = resolvedBaselines[0];
|
|
2404
|
-
|
|
2790
|
+
const out = renderableTasks.map((t)=>toSvarTask(t, primary, calendar));
|
|
2791
|
+
if (ghostById.size > 0) {
|
|
2792
|
+
for (const t of renderableTasks){
|
|
2793
|
+
if (t.type === 'summary') continue;
|
|
2794
|
+
const ghost = ghostById.get(t.id);
|
|
2795
|
+
if (ghost && (ghost.start.getTime() !== t.start.getTime() || ghost.end.getTime() !== t.end.getTime())) {
|
|
2796
|
+
out.push(makeEditPreviewPhantom(t, ghost));
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
return out;
|
|
2405
2801
|
}
|
|
2406
2802
|
const out = [];
|
|
2407
2803
|
for (const t of renderableTasks){
|
|
@@ -2412,6 +2808,11 @@ function getProjectEnd(p) {
|
|
|
2412
2808
|
const phantom = makeBaselinePhantom(t, b, calendar);
|
|
2413
2809
|
if (phantom) out.push(phantom);
|
|
2414
2810
|
}
|
|
2811
|
+
// Edit preview phantom goes last (renders below baseline phantoms).
|
|
2812
|
+
const ghost = ghostById.get(t.id);
|
|
2813
|
+
if (ghost && (ghost.start.getTime() !== t.start.getTime() || ghost.end.getTime() !== t.end.getTime())) {
|
|
2814
|
+
out.push(makeEditPreviewPhantom(t, ghost));
|
|
2815
|
+
}
|
|
2415
2816
|
}
|
|
2416
2817
|
return out;
|
|
2417
2818
|
}
|
|
@@ -2483,6 +2884,95 @@ function toSvarMarker(m) {
|
|
|
2483
2884
|
};
|
|
2484
2885
|
return config;
|
|
2485
2886
|
}
|
|
2887
|
+
function getInputType(field) {
|
|
2888
|
+
if (field === 'start' || field === 'end') return 'date';
|
|
2889
|
+
if (field === 'duration' || field === 'progress') return 'number';
|
|
2890
|
+
return 'text';
|
|
2891
|
+
}
|
|
2892
|
+
function getInputValue(task, field) {
|
|
2893
|
+
switch(field){
|
|
2894
|
+
case 'text':
|
|
2895
|
+
return task.text;
|
|
2896
|
+
case 'start':
|
|
2897
|
+
return formatShortDate(task.start);
|
|
2898
|
+
case 'end':
|
|
2899
|
+
return formatShortDate(task.end);
|
|
2900
|
+
case 'duration':
|
|
2901
|
+
return String(Math.round(task.duration / 60 / 8));
|
|
2902
|
+
case 'progress':
|
|
2903
|
+
return String(task.progress);
|
|
2904
|
+
case 'scheduleMode':
|
|
2905
|
+
return task.scheduleMode;
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
const EDITABLE_FIELDS = new Set([
|
|
2909
|
+
'text',
|
|
2910
|
+
'start',
|
|
2911
|
+
'end',
|
|
2912
|
+
'duration',
|
|
2913
|
+
'progress',
|
|
2914
|
+
'scheduleMode'
|
|
2915
|
+
]);
|
|
2916
|
+
function buildEditableCell(field, editStateRef, onTaskEditRef) {
|
|
2917
|
+
return ({ row })=>{
|
|
2918
|
+
const editState = editStateRef.current;
|
|
2919
|
+
const task = row;
|
|
2920
|
+
if (task.is_baseline_ghost || task.is_edit_preview) {
|
|
2921
|
+
return /*#__PURE__*/ jsxRuntime.jsx("span", {});
|
|
2922
|
+
}
|
|
2923
|
+
const isReadOnly = task.type === 'summary' && (field === 'start' || field === 'end' || field === 'duration');
|
|
2924
|
+
const isActive = editState.activeCell?.taskId === task.id && editState.activeCell?.field === field;
|
|
2925
|
+
if (isActive) {
|
|
2926
|
+
return /*#__PURE__*/ jsxRuntime.jsx("input", {
|
|
2927
|
+
// biome-ignore lint/a11y/noAutofocus: intentional — cell was clicked
|
|
2928
|
+
autoFocus: true,
|
|
2929
|
+
type: getInputType(field),
|
|
2930
|
+
defaultValue: editState.dirtyValue,
|
|
2931
|
+
style: {
|
|
2932
|
+
width: '100%',
|
|
2933
|
+
boxSizing: 'border-box'
|
|
2934
|
+
},
|
|
2935
|
+
onChange: (e)=>editStateRef.current.setValue(e.target.value),
|
|
2936
|
+
onBlur: ()=>editStateRef.current.commitCell(onTaskEditRef.current),
|
|
2937
|
+
onKeyDown: (e)=>{
|
|
2938
|
+
if (e.key === 'Enter' || e.key === 'Tab') {
|
|
2939
|
+
e.preventDefault();
|
|
2940
|
+
editStateRef.current.commitCell(onTaskEditRef.current);
|
|
2941
|
+
} else if (e.key === 'Escape') {
|
|
2942
|
+
editStateRef.current.cancelCell();
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
}, `${editState.activeCell?.taskId}-${field}`);
|
|
2946
|
+
}
|
|
2947
|
+
const displayValue = (()=>{
|
|
2948
|
+
if (field === 'start' || field === 'end') {
|
|
2949
|
+
const d = task[field];
|
|
2950
|
+
return d instanceof Date ? formatShortDate(d) : '';
|
|
2951
|
+
}
|
|
2952
|
+
if (field === 'duration') return String(Math.round((task.duration ?? 0) / 60 / 8));
|
|
2953
|
+
if (field === 'progress') return String(task.progress ?? 0);
|
|
2954
|
+
return String(task[field] ?? '');
|
|
2955
|
+
})();
|
|
2956
|
+
if (isReadOnly || task.id === undefined) {
|
|
2957
|
+
return /*#__PURE__*/ jsxRuntime.jsx("span", {
|
|
2958
|
+
children: displayValue
|
|
2959
|
+
});
|
|
2960
|
+
}
|
|
2961
|
+
const taskId = task.id;
|
|
2962
|
+
const activate = ()=>editStateRef.current.activateCell(taskId, field, getInputValue(task, field));
|
|
2963
|
+
return(// biome-ignore lint/a11y/useKeyWithClickEvents: cell activation via keyboard handled by the input that renders on activate
|
|
2964
|
+
// biome-ignore lint/a11y/noStaticElementInteractions: grid cell — role="gridcell" would be on the parent SVAR element
|
|
2965
|
+
/*#__PURE__*/ jsxRuntime.jsx("span", {
|
|
2966
|
+
style: {
|
|
2967
|
+
cursor: 'text',
|
|
2968
|
+
display: 'block',
|
|
2969
|
+
width: '100%'
|
|
2970
|
+
},
|
|
2971
|
+
onClick: activate,
|
|
2972
|
+
children: displayValue
|
|
2973
|
+
}));
|
|
2974
|
+
};
|
|
2975
|
+
}
|
|
2486
2976
|
function buildHighlightTime(calendar) {
|
|
2487
2977
|
if (!calendar) return undefined;
|
|
2488
2978
|
return (date, unit)=>{
|
|
@@ -2528,6 +3018,7 @@ const KNOWN_TASK_FIELDS = new Set([
|
|
|
2528
3018
|
'Finish',
|
|
2529
3019
|
'Duration',
|
|
2530
3020
|
'ConstraintType',
|
|
3021
|
+
'ConstraintDate',
|
|
2531
3022
|
'Milestone',
|
|
2532
3023
|
'Summary',
|
|
2533
3024
|
'OutlineLevel',
|
|
@@ -2711,6 +3202,7 @@ function parseMspdi(xml) {
|
|
|
2711
3202
|
}
|
|
2712
3203
|
const tasks = [];
|
|
2713
3204
|
const links = [];
|
|
3205
|
+
const outlineStack = [];
|
|
2714
3206
|
// Per-task baseline snapshots, keyed by baseline Number (BaselineIndex).
|
|
2715
3207
|
// Flattened into project.baselines below.
|
|
2716
3208
|
const baselineAccum = new Map();
|
|
@@ -2731,7 +3223,10 @@ function parseMspdi(xml) {
|
|
|
2731
3223
|
const isMilestone = String(raw.Milestone ?? '0') === '1';
|
|
2732
3224
|
const isSummary = String(raw.Summary ?? '0') === '1';
|
|
2733
3225
|
const taskType = isSummary ? 'summary' : isMilestone ? 'milestone' : 'task';
|
|
2734
|
-
|
|
3226
|
+
const outlineLevel = Math.max(1, Number(raw.OutlineLevel ?? '1'));
|
|
3227
|
+
const parentId = outlineLevel > 1 ? outlineStack[outlineLevel - 2] : undefined;
|
|
3228
|
+
const constraint = parseConstraint(raw);
|
|
3229
|
+
const parsedTask = {
|
|
2735
3230
|
id: uid,
|
|
2736
3231
|
text: name,
|
|
2737
3232
|
type: taskType,
|
|
@@ -2739,8 +3234,13 @@ function parseMspdi(xml) {
|
|
|
2739
3234
|
duration,
|
|
2740
3235
|
start,
|
|
2741
3236
|
end,
|
|
2742
|
-
progress: 0
|
|
2743
|
-
}
|
|
3237
|
+
progress: Number(raw.PercentComplete ?? '0')
|
|
3238
|
+
};
|
|
3239
|
+
if (parentId !== undefined) parsedTask.parent = parentId;
|
|
3240
|
+
if (constraint !== undefined) parsedTask.constraint = constraint;
|
|
3241
|
+
tasks.push(parsedTask);
|
|
3242
|
+
outlineStack[outlineLevel - 1] = uid;
|
|
3243
|
+
outlineStack.length = outlineLevel;
|
|
2744
3244
|
// Walk predecessor links nested inside the task.
|
|
2745
3245
|
const preds = Array.isArray(raw.PredecessorLink) ? raw.PredecessorLink : raw.PredecessorLink !== undefined ? [
|
|
2746
3246
|
raw.PredecessorLink
|
|
@@ -3142,9 +3642,31 @@ const MSPDI_TYPE_TO_DEPENDENCY = {
|
|
|
3142
3642
|
2: 'SF',
|
|
3143
3643
|
3: 'SS'
|
|
3144
3644
|
};
|
|
3645
|
+
const MSPDI_CONSTRAINT_TO_INTERNAL = {
|
|
3646
|
+
0: 'ASAP',
|
|
3647
|
+
1: 'ALAP',
|
|
3648
|
+
2: 'MSO',
|
|
3649
|
+
3: 'MFO',
|
|
3650
|
+
4: 'SNET',
|
|
3651
|
+
5: 'SNLT',
|
|
3652
|
+
6: 'FNET',
|
|
3653
|
+
7: 'FNLT'
|
|
3654
|
+
};
|
|
3145
3655
|
function mspdiTypeToDependencyType(t) {
|
|
3146
3656
|
return MSPDI_TYPE_TO_DEPENDENCY[t] ?? 'FS';
|
|
3147
3657
|
}
|
|
3658
|
+
function parseConstraint(raw) {
|
|
3659
|
+
const value = Number(raw.ConstraintType ?? '0');
|
|
3660
|
+
const type = MSPDI_CONSTRAINT_TO_INTERNAL[value];
|
|
3661
|
+
if (!type || type === 'ASAP') return undefined;
|
|
3662
|
+
const constraint = {
|
|
3663
|
+
type
|
|
3664
|
+
};
|
|
3665
|
+
if (raw.ConstraintDate !== undefined && raw.ConstraintDate !== '') {
|
|
3666
|
+
constraint.date = parseMspdiDate(String(raw.ConstraintDate));
|
|
3667
|
+
}
|
|
3668
|
+
return constraint;
|
|
3669
|
+
}
|
|
3148
3670
|
function parseMspdiDate(s) {
|
|
3149
3671
|
// MSPDI emits ISO 8601 like `2026-01-05T08:00:00` (no timezone in
|
|
3150
3672
|
// practice; MS Project writes local time). We treat it as local.
|
|
@@ -3254,6 +3776,7 @@ function serializeMspdi(project, options = {}) {
|
|
|
3254
3776
|
// Group baselines by taskId. Each task may have one snapshot per baseline
|
|
3255
3777
|
// (0-10), emitted as <Baseline> children nested inside the task.
|
|
3256
3778
|
const baselinesByTask = buildBaselinesByTask(project.baselines);
|
|
3779
|
+
const outlineLevelByTask = buildOutlineLevels(project.tasks);
|
|
3257
3780
|
const tasksOut = project.tasks.map((t, idx)=>{
|
|
3258
3781
|
const taskOut = {
|
|
3259
3782
|
UID: String(t.id),
|
|
@@ -3262,11 +3785,15 @@ function serializeMspdi(project, options = {}) {
|
|
|
3262
3785
|
Start: formatMspdiDate(t.start),
|
|
3263
3786
|
Finish: formatMspdiDate(t.end),
|
|
3264
3787
|
Duration: formatMspdiDuration(t.duration),
|
|
3265
|
-
ConstraintType:
|
|
3788
|
+
ConstraintType: constraintTypeToMspdi(t.constraint?.type),
|
|
3266
3789
|
Milestone: t.type === 'milestone' ? 1 : 0,
|
|
3267
3790
|
Summary: t.type === 'summary' ? 1 : 0,
|
|
3268
|
-
OutlineLevel: 1
|
|
3791
|
+
OutlineLevel: outlineLevelByTask.get(t.id) ?? 1,
|
|
3792
|
+
PercentComplete: t.progress
|
|
3269
3793
|
};
|
|
3794
|
+
if (t.constraint?.date) {
|
|
3795
|
+
taskOut.ConstraintDate = formatMspdiDate(t.constraint.date);
|
|
3796
|
+
}
|
|
3270
3797
|
const incoming = linksByTarget.get(String(t.id));
|
|
3271
3798
|
if (incoming?.length) {
|
|
3272
3799
|
taskOut.PredecessorLink = incoming.map(toMspdiLink);
|
|
@@ -3343,6 +3870,46 @@ function dependencyTypeToMspdi(t) {
|
|
|
3343
3870
|
return 3;
|
|
3344
3871
|
}
|
|
3345
3872
|
}
|
|
3873
|
+
function constraintTypeToMspdi(t) {
|
|
3874
|
+
switch(t){
|
|
3875
|
+
case undefined:
|
|
3876
|
+
case 'ASAP':
|
|
3877
|
+
return 0;
|
|
3878
|
+
case 'ALAP':
|
|
3879
|
+
return 1;
|
|
3880
|
+
case 'MSO':
|
|
3881
|
+
return 2;
|
|
3882
|
+
case 'MFO':
|
|
3883
|
+
return 3;
|
|
3884
|
+
case 'SNET':
|
|
3885
|
+
return 4;
|
|
3886
|
+
case 'SNLT':
|
|
3887
|
+
return 5;
|
|
3888
|
+
case 'FNET':
|
|
3889
|
+
return 6;
|
|
3890
|
+
case 'FNLT':
|
|
3891
|
+
return 7;
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
function buildOutlineLevels(tasks) {
|
|
3895
|
+
const parentById = new Map(tasks.map((t)=>[
|
|
3896
|
+
t.id,
|
|
3897
|
+
t.parent
|
|
3898
|
+
]));
|
|
3899
|
+
const cache = new Map();
|
|
3900
|
+
function depth(taskId) {
|
|
3901
|
+
const cached = cache.get(taskId);
|
|
3902
|
+
if (cached !== undefined) return cached;
|
|
3903
|
+
const parentId = parentById.get(taskId);
|
|
3904
|
+
const value = parentId === undefined ? 1 : depth(parentId) + 1;
|
|
3905
|
+
cache.set(taskId, value);
|
|
3906
|
+
return value;
|
|
3907
|
+
}
|
|
3908
|
+
return new Map(tasks.map((t)=>[
|
|
3909
|
+
t.id,
|
|
3910
|
+
depth(t.id)
|
|
3911
|
+
]));
|
|
3912
|
+
}
|
|
3346
3913
|
function formatMspdiDate(d) {
|
|
3347
3914
|
// MSPDI emits local time without timezone (e.g. `2026-01-05T08:00:00`).
|
|
3348
3915
|
const pad = (n)=>String(n).padStart(2, '0');
|