@ifc-lite/viewer 1.17.4 → 1.18.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/.turbo/turbo-build.log +20 -17
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +630 -0
- package/DESKTOP_CONTRACT_VERSION +1 -1
- package/dist/assets/{basketViewActivator-BmnNtVfZ.js → basketViewActivator-Cm1QEk_R.js} +1 -1
- package/dist/assets/drawing-2d-DoxKMqbO.js +257 -0
- package/dist/assets/{exporters-ChAtBmlj.js → exporters-B_OBqIyD.js} +3479 -2845
- package/dist/assets/{geometry.worker-BQ0rzNo-.js → geometry.worker-xHHy-9DV.js} +1 -1
- package/dist/assets/ids-DQ5jY0E8.js +1 -0
- package/dist/assets/ifc-lite_bg-ADjKXSms.wasm +0 -0
- package/dist/assets/{index-Co8E2-FE.js → index-BKq-M3Mk.js} +55873 -40593
- package/dist/assets/index-COnQRuqY.css +1 -0
- package/dist/assets/{native-bridge-BRvbckFQ.js → native-bridge-SHXiQwFW.js} +104 -104
- package/dist/assets/sandbox-jez21HtV.js +9627 -0
- package/dist/assets/{server-client-BV8zHZ7Y.js → server-client-ncOQVNso.js} +1 -1
- package/dist/assets/{wasm-bridge-g01g7T9b.js → wasm-bridge-DyfBSB8z.js} +1 -1
- package/dist/index.html +8 -7
- package/index.html +1 -0
- package/package.json +13 -13
- package/src/App.tsx +16 -2
- package/src/apache-arrow.d.ts +30 -0
- package/src/components/viewer/AddElementPanel.tsx +758 -0
- package/src/components/viewer/BulkPropertyEditor.tsx +7 -0
- package/src/components/viewer/CesiumOverlay.tsx +62 -19
- package/src/components/viewer/ChatPanel.tsx +259 -93
- package/src/components/viewer/CommandPalette.tsx +56 -7
- package/src/components/viewer/EntityContextMenu.tsx +168 -4
- package/src/components/viewer/ExportChangesButton.tsx +25 -5
- package/src/components/viewer/ExportDialog.tsx +19 -1
- package/src/components/viewer/MainToolbar.tsx +73 -13
- package/src/components/viewer/PropertiesPanel.tsx +237 -23
- package/src/components/viewer/SearchInline.tsx +669 -0
- package/src/components/viewer/SearchModal.filter.builder.tsx +766 -0
- package/src/components/viewer/SearchModal.filter.tsx +514 -0
- package/src/components/viewer/SearchModal.text.tsx +388 -0
- package/src/components/viewer/SearchModal.tsx +235 -0
- package/src/components/viewer/SettingsPage.tsx +252 -101
- package/src/components/viewer/ThemeSwitch.tsx +63 -7
- package/src/components/viewer/ToolOverlays.tsx +5 -0
- package/src/components/viewer/ViewerLayout.tsx +25 -4
- package/src/components/viewer/Viewport.tsx +25 -3
- package/src/components/viewer/ViewportContainer.tsx +51 -64
- package/src/components/viewer/ViewportOverlays.tsx +5 -2
- package/src/components/viewer/annotations/AnnotationDropInput.tsx +203 -0
- package/src/components/viewer/annotations/AnnotationLayer.tsx +287 -0
- package/src/components/viewer/annotations/AnnotationPin.tsx +90 -0
- package/src/components/viewer/annotations/AnnotationPopover.tsx +296 -0
- package/src/components/viewer/bcf/BCFTopicDetail.tsx +4 -4
- package/src/components/viewer/chat/ModelSelector.tsx +90 -54
- package/src/components/viewer/lists/ListPanel.tsx +14 -21
- package/src/components/viewer/properties/GeoreferencingPanel.tsx +113 -51
- package/src/components/viewer/properties/LocationMap.tsx +9 -7
- package/src/components/viewer/properties/ModelMetadataPanel.tsx +1 -1
- package/src/components/viewer/properties/RawStepCard.tsx +332 -0
- package/src/components/viewer/properties/RawStepRow.tsx +261 -0
- package/src/components/viewer/properties/ScheduleCard.tsx +224 -0
- package/src/components/viewer/properties/TaskEditCard.tsx +510 -0
- package/src/components/viewer/properties/raw-step-format.ts +193 -0
- package/src/components/viewer/schedule/AnimationSettingsPopover.tsx +542 -0
- package/src/components/viewer/schedule/GanttDependencyArrows.tsx +89 -0
- package/src/components/viewer/schedule/GanttDragTooltip.tsx +48 -0
- package/src/components/viewer/schedule/GanttEmptyState.tsx +97 -0
- package/src/components/viewer/schedule/GanttPanel.tsx +295 -0
- package/src/components/viewer/schedule/GanttTaskBar.tsx +199 -0
- package/src/components/viewer/schedule/GanttTaskTree.tsx +250 -0
- package/src/components/viewer/schedule/GanttTimeline.tsx +305 -0
- package/src/components/viewer/schedule/GanttToolbar.tsx +406 -0
- package/src/components/viewer/schedule/GenerateAdvancedPanel.tsx +147 -0
- package/src/components/viewer/schedule/GenerateScheduleDialog.tsx +392 -0
- package/src/components/viewer/schedule/HeightStrategyPanel.tsx +120 -0
- package/src/components/viewer/schedule/generate-schedule.test.ts +439 -0
- package/src/components/viewer/schedule/generate-schedule.ts +648 -0
- package/src/components/viewer/schedule/schedule-animator.test.ts +452 -0
- package/src/components/viewer/schedule/schedule-animator.ts +488 -0
- package/src/components/viewer/schedule/schedule-selection.test.ts +148 -0
- package/src/components/viewer/schedule/schedule-selection.ts +163 -0
- package/src/components/viewer/schedule/schedule-utils.ts +223 -0
- package/src/components/viewer/schedule/useConstructionSequence.ts +156 -0
- package/src/components/viewer/schedule/useGanttBarDrag.test.ts +90 -0
- package/src/components/viewer/schedule/useGanttBarDrag.ts +305 -0
- package/src/components/viewer/schedule/useGanttSelection3DHighlight.ts +152 -0
- package/src/components/viewer/schedule/useOverlayCompositor.ts +108 -0
- package/src/components/viewer/selectionHandlers.ts +446 -0
- package/src/components/viewer/tools/AddElementOverlay.tsx +540 -0
- package/src/components/viewer/tools/SectionCapControls.tsx +237 -0
- package/src/components/viewer/tools/SectionPanel.tsx +39 -18
- package/src/components/viewer/useAnimationLoop.ts +9 -1
- package/src/components/viewer/useDuplicateShortcut.ts +77 -0
- package/src/components/viewer/useMouseControls.ts +9 -1
- package/src/components/viewer/useRenderUpdates.ts +1 -1
- package/src/hooks/ids/idsDataAccessor.ts +60 -24
- package/src/hooks/ingest/viewerModelIngest.ts +7 -2
- package/src/hooks/useIfcFederation.ts +326 -71
- package/src/hooks/useIfcLoader.ts +23 -10
- package/src/hooks/useKeyboardShortcuts.ts +25 -0
- package/src/hooks/useSandbox.ts +1 -1
- package/src/hooks/useSearchIndex.ts +125 -0
- package/src/hooks/useViewControls.ts +13 -5
- package/src/index.css +550 -10
- package/src/lib/desktop-entitlement.ts +2 -4
- package/src/lib/geo/cesium-bridge.ts +15 -7
- package/src/lib/geo/effective-georef.test.ts +73 -0
- package/src/lib/geo/effective-georef.ts +111 -0
- package/src/lib/geo/reproject.ts +105 -19
- package/src/lib/llm/byok-guard.test.ts +77 -0
- package/src/lib/llm/byok-guard.ts +39 -0
- package/src/lib/llm/free-models.test.ts +0 -6
- package/src/lib/llm/models.ts +104 -42
- package/src/lib/llm/stream-client.ts +74 -110
- package/src/lib/llm/stream-direct.test.ts +130 -0
- package/src/lib/llm/stream-direct.ts +316 -0
- package/src/lib/llm/system-prompt.test.ts +14 -0
- package/src/lib/llm/system-prompt.ts +102 -1
- package/src/lib/llm/types.ts +20 -2
- package/src/lib/recent-files.ts +38 -4
- package/src/lib/scripts/templates/bim-globals.d.ts +136 -114
- package/src/lib/scripts/templates/construction-schedule.ts +223 -0
- package/src/lib/scripts/templates.ts +7 -0
- package/src/lib/search/common-ifc-types.ts +36 -0
- package/src/lib/search/filter-evaluate.test.ts +537 -0
- package/src/lib/search/filter-evaluate.ts +610 -0
- package/src/lib/search/filter-rules.test.ts +119 -0
- package/src/lib/search/filter-rules.ts +198 -0
- package/src/lib/search/filter-schema.test.ts +233 -0
- package/src/lib/search/filter-schema.ts +146 -0
- package/src/lib/search/recent-searches.test.ts +116 -0
- package/src/lib/search/recent-searches.ts +93 -0
- package/src/lib/search/result-export.test.ts +101 -0
- package/src/lib/search/result-export.ts +104 -0
- package/src/lib/search/saved-filters.test.ts +118 -0
- package/src/lib/search/saved-filters.ts +154 -0
- package/src/lib/search/tier0-scan.test.ts +196 -0
- package/src/lib/search/tier0-scan.ts +237 -0
- package/src/lib/search/tier1-index.test.ts +242 -0
- package/src/lib/search/tier1-index.ts +448 -0
- package/src/main.tsx +1 -10
- package/src/sdk/adapters/export-adapter.test.ts +434 -1
- package/src/sdk/adapters/export-adapter.ts +404 -1
- package/src/sdk/adapters/export-schedule-splice.test.ts +127 -0
- package/src/sdk/adapters/export-schedule-splice.ts +87 -0
- package/src/sdk/adapters/model-compat.ts +8 -2
- package/src/sdk/adapters/schedule-adapter.ts +73 -0
- package/src/sdk/adapters/store-adapter.ts +201 -0
- package/src/sdk/adapters/visibility-adapter.ts +3 -0
- package/src/sdk/local-backend.ts +16 -8
- package/src/services/api-keys.ts +73 -0
- package/src/services/desktop-export.ts +3 -1
- package/src/services/desktop-native-metadata.ts +41 -18
- package/src/services/file-dialog.ts +4 -1
- package/src/services/tauri-modules.d.ts +25 -0
- package/src/store/basketVisibleSet.ts +3 -0
- package/src/store/constants.ts +20 -2
- package/src/store/globalId.ts +4 -1
- package/src/store/index.ts +82 -6
- package/src/store/slices/addElementMeshes.ts +365 -0
- package/src/store/slices/addElementSlice.ts +275 -0
- package/src/store/slices/annotationsSlice.test.ts +133 -0
- package/src/store/slices/annotationsSlice.ts +251 -0
- package/src/store/slices/cesiumSlice.ts +5 -0
- package/src/store/slices/chatSlice.test.ts +6 -76
- package/src/store/slices/chatSlice.ts +17 -58
- package/src/store/slices/dataSlice.test.ts +23 -4
- package/src/store/slices/dataSlice.ts +1 -1
- package/src/store/slices/modelSlice.test.ts +67 -9
- package/src/store/slices/modelSlice.ts +39 -7
- package/src/store/slices/mutationSlice.ts +964 -3
- package/src/store/slices/overlayCompositor.test.ts +164 -0
- package/src/store/slices/overlaySlice.test.ts +93 -0
- package/src/store/slices/overlaySlice.ts +151 -0
- package/src/store/slices/pinboardSlice.test.ts +6 -1
- package/src/store/slices/playbackSlice.ts +128 -0
- package/src/store/slices/schedule-edit-helpers.test.ts +97 -0
- package/src/store/slices/schedule-edit-helpers.ts +179 -0
- package/src/store/slices/scheduleSlice.test.ts +694 -0
- package/src/store/slices/scheduleSlice.ts +1330 -0
- package/src/store/slices/searchSlice.test.ts +342 -0
- package/src/store/slices/searchSlice.ts +341 -0
- package/src/store/slices/sectionSlice.test.ts +87 -7
- package/src/store/slices/sectionSlice.ts +151 -5
- package/src/store/slices/selectionSlice.test.ts +46 -0
- package/src/store/slices/selectionSlice.ts +20 -0
- package/src/store/slices/uiSlice.ts +28 -5
- package/src/store/types.ts +26 -0
- package/src/store.ts +14 -0
- package/src/utils/nativeSpatialDataStore.ts +4 -1
- package/src/utils/viewportUtils.ts +7 -2
- package/src/vite-env.d.ts +0 -4
- package/dist/assets/drawing-2d-gWfpdfYe.js +0 -257
- package/dist/assets/ids-B4jTqB1O.js +0 -1
- package/dist/assets/ifc-lite_bg-BX4E7TX8.wasm +0 -0
- package/dist/assets/index-DckuDqlv.css +0 -1
- package/dist/assets/sandbox-DZiNLNMk.js +0 -5933
- package/src/components/viewer/UpgradePage.tsx +0 -71
- package/src/lib/desktop/ClerkDesktopEntitlementSync.tsx +0 -175
- package/src/lib/llm/ClerkChatSync.tsx +0 -74
- package/src/lib/llm/clerk-auth.ts +0 -62
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
import { describe, it } from 'node:test';
|
|
6
|
+
import assert from 'node:assert';
|
|
7
|
+
import { create } from 'zustand';
|
|
8
|
+
import type { ScheduleExtraction, ScheduleTaskInfo } from '@ifc-lite/parser';
|
|
9
|
+
import {
|
|
10
|
+
computeScheduleRange,
|
|
11
|
+
computeHiddenProductIds,
|
|
12
|
+
computeActiveProductIds,
|
|
13
|
+
countGeneratedTasks,
|
|
14
|
+
taskStartEpoch,
|
|
15
|
+
taskFinishEpoch,
|
|
16
|
+
} from './scheduleSlice.js';
|
|
17
|
+
import { createScheduleSlice, type ScheduleSlice } from './scheduleSlice.js';
|
|
18
|
+
|
|
19
|
+
function makeExtraction(): ScheduleExtraction {
|
|
20
|
+
return {
|
|
21
|
+
hasSchedule: true,
|
|
22
|
+
workSchedules: [],
|
|
23
|
+
sequences: [],
|
|
24
|
+
tasks: [
|
|
25
|
+
{
|
|
26
|
+
expressId: 20,
|
|
27
|
+
globalId: 'task-a',
|
|
28
|
+
name: 'Foundations',
|
|
29
|
+
isMilestone: false,
|
|
30
|
+
childGlobalIds: [],
|
|
31
|
+
productExpressIds: [1, 2],
|
|
32
|
+
productGlobalIds: ['w1', 'w2'],
|
|
33
|
+
controllingScheduleGlobalIds: [],
|
|
34
|
+
taskTime: {
|
|
35
|
+
scheduleStart: '2024-01-01T00:00:00Z',
|
|
36
|
+
scheduleFinish: '2024-01-11T00:00:00Z',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
expressId: 21,
|
|
41
|
+
globalId: 'task-b',
|
|
42
|
+
name: 'Framing',
|
|
43
|
+
isMilestone: false,
|
|
44
|
+
childGlobalIds: [],
|
|
45
|
+
productExpressIds: [3, 4],
|
|
46
|
+
productGlobalIds: ['w3', 'w4'],
|
|
47
|
+
controllingScheduleGlobalIds: [],
|
|
48
|
+
taskTime: {
|
|
49
|
+
scheduleStart: '2024-01-15T00:00:00Z',
|
|
50
|
+
scheduleFinish: '2024-01-25T00:00:00Z',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
// No task time — never hides its products.
|
|
55
|
+
expressId: 22,
|
|
56
|
+
globalId: 'task-c',
|
|
57
|
+
name: 'Sitework (no time)',
|
|
58
|
+
isMilestone: false,
|
|
59
|
+
childGlobalIds: [],
|
|
60
|
+
productExpressIds: [5],
|
|
61
|
+
productGlobalIds: ['w5'],
|
|
62
|
+
controllingScheduleGlobalIds: [],
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe('computeScheduleRange', () => {
|
|
69
|
+
it('returns null for null data', () => {
|
|
70
|
+
assert.strictEqual(computeScheduleRange(null), null);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('returns null for an extraction with no tasks', () => {
|
|
74
|
+
assert.strictEqual(
|
|
75
|
+
computeScheduleRange({ hasSchedule: false, workSchedules: [], sequences: [], tasks: [] }),
|
|
76
|
+
null,
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('spans the earliest start and latest finish', () => {
|
|
81
|
+
const range = computeScheduleRange(makeExtraction());
|
|
82
|
+
assert.strictEqual(range?.synthetic, false);
|
|
83
|
+
assert.strictEqual(range?.start, Date.parse('2024-01-01T00:00:00Z'));
|
|
84
|
+
assert.strictEqual(range?.end, Date.parse('2024-01-25T00:00:00Z'));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('falls back to a synthetic range when no task has dates', () => {
|
|
88
|
+
const range = computeScheduleRange({
|
|
89
|
+
hasSchedule: true,
|
|
90
|
+
workSchedules: [],
|
|
91
|
+
sequences: [],
|
|
92
|
+
tasks: [{
|
|
93
|
+
expressId: 1, globalId: 'x', name: 'x', isMilestone: false,
|
|
94
|
+
childGlobalIds: [], productExpressIds: [], productGlobalIds: [],
|
|
95
|
+
controllingScheduleGlobalIds: [],
|
|
96
|
+
}],
|
|
97
|
+
});
|
|
98
|
+
assert.strictEqual(range?.synthetic, true);
|
|
99
|
+
assert.ok(range!.end > range!.start);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('computeHiddenProductIds', () => {
|
|
104
|
+
const data = makeExtraction();
|
|
105
|
+
const beforeStart = Date.parse('2023-12-30T00:00:00Z');
|
|
106
|
+
const duringA = Date.parse('2024-01-05T00:00:00Z');
|
|
107
|
+
const duringB = Date.parse('2024-01-20T00:00:00Z');
|
|
108
|
+
const afterAll = Date.parse('2024-02-01T00:00:00Z');
|
|
109
|
+
|
|
110
|
+
it('hides all task-bound products before any task starts', () => {
|
|
111
|
+
const hidden = computeHiddenProductIds(data, beforeStart);
|
|
112
|
+
assert.strictEqual(hidden.has(1), true);
|
|
113
|
+
assert.strictEqual(hidden.has(2), true);
|
|
114
|
+
assert.strictEqual(hidden.has(3), true);
|
|
115
|
+
assert.strictEqual(hidden.has(4), true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('reveals products whose task has started', () => {
|
|
119
|
+
const hidden = computeHiddenProductIds(data, duringA);
|
|
120
|
+
assert.strictEqual(hidden.has(1), false);
|
|
121
|
+
assert.strictEqual(hidden.has(2), false);
|
|
122
|
+
assert.strictEqual(hidden.has(3), true);
|
|
123
|
+
assert.strictEqual(hidden.has(4), true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('reveals later tasks once time advances', () => {
|
|
127
|
+
const hidden = computeHiddenProductIds(data, duringB);
|
|
128
|
+
assert.strictEqual(hidden.has(3), false);
|
|
129
|
+
assert.strictEqual(hidden.has(4), false);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('never hides products whose task has no scheduled time', () => {
|
|
133
|
+
const hidden = computeHiddenProductIds(data, beforeStart);
|
|
134
|
+
assert.strictEqual(hidden.has(5), false);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('reveals everything after schedule completes', () => {
|
|
138
|
+
const hidden = computeHiddenProductIds(data, afterAll);
|
|
139
|
+
assert.strictEqual(hidden.size, 0);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('schedule filter: only tasks controlled by the active schedule contribute', () => {
|
|
143
|
+
const filtered = {
|
|
144
|
+
hasSchedule: true,
|
|
145
|
+
workSchedules: [],
|
|
146
|
+
sequences: [],
|
|
147
|
+
tasks: [
|
|
148
|
+
{
|
|
149
|
+
expressId: 20, globalId: 'task-a', name: 'A', isMilestone: false,
|
|
150
|
+
childGlobalIds: [], productExpressIds: [1], productGlobalIds: ['w1'],
|
|
151
|
+
controllingScheduleGlobalIds: ['sched-A'],
|
|
152
|
+
taskTime: { scheduleStart: '2024-01-01T00:00:00Z', scheduleFinish: '2024-01-05T00:00:00Z' },
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
expressId: 21, globalId: 'task-b', name: 'B', isMilestone: false,
|
|
156
|
+
childGlobalIds: [], productExpressIds: [2], productGlobalIds: ['w2'],
|
|
157
|
+
controllingScheduleGlobalIds: ['sched-B'],
|
|
158
|
+
taskTime: { scheduleStart: '2024-01-10T00:00:00Z', scheduleFinish: '2024-01-15T00:00:00Z' },
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
};
|
|
162
|
+
// Before any task starts — schedule A filter hides only A's products.
|
|
163
|
+
const hiddenA = computeHiddenProductIds(filtered, Date.parse('2023-12-30T00:00:00Z'), 'sched-A');
|
|
164
|
+
assert.strictEqual(hiddenA.has(1), true);
|
|
165
|
+
assert.strictEqual(hiddenA.has(2), false, 'task-b is out of scope for sched-A');
|
|
166
|
+
|
|
167
|
+
// Empty / null filter falls back to "all tasks in scope".
|
|
168
|
+
const hiddenAll = computeHiddenProductIds(filtered, Date.parse('2023-12-30T00:00:00Z'));
|
|
169
|
+
assert.strictEqual(hiddenAll.has(1), true);
|
|
170
|
+
assert.strictEqual(hiddenAll.has(2), true);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('schedule filter: tasks with no controllingScheduleGlobalIds are always in-scope', () => {
|
|
174
|
+
const unattached = {
|
|
175
|
+
hasSchedule: true,
|
|
176
|
+
workSchedules: [],
|
|
177
|
+
sequences: [],
|
|
178
|
+
tasks: [{
|
|
179
|
+
expressId: 20, globalId: 'task', name: 'orphan', isMilestone: false,
|
|
180
|
+
childGlobalIds: [], productExpressIds: [9], productGlobalIds: ['w9'],
|
|
181
|
+
controllingScheduleGlobalIds: [], // no controlling schedule
|
|
182
|
+
taskTime: { scheduleStart: '2024-01-01T00:00:00Z', scheduleFinish: '2024-01-05T00:00:00Z' },
|
|
183
|
+
}],
|
|
184
|
+
};
|
|
185
|
+
const hidden = computeHiddenProductIds(unattached, Date.parse('2023-12-30T00:00:00Z'), 'sched-A');
|
|
186
|
+
assert.strictEqual(hidden.has(9), true, 'orphan task still contributes when filter is applied');
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe('computeActiveProductIds', () => {
|
|
191
|
+
const data = makeExtraction();
|
|
192
|
+
it('marks products as active during their task window', () => {
|
|
193
|
+
const active = computeActiveProductIds(data, Date.parse('2024-01-05T00:00:00Z'));
|
|
194
|
+
assert.strictEqual(active.has(1), true);
|
|
195
|
+
assert.strictEqual(active.has(2), true);
|
|
196
|
+
assert.strictEqual(active.has(3), false);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('returns empty when between tasks', () => {
|
|
200
|
+
const active = computeActiveProductIds(data, Date.parse('2024-01-13T00:00:00Z'));
|
|
201
|
+
assert.strictEqual(active.size, 0);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('task time helpers', () => {
|
|
206
|
+
it('computes finish from duration when ScheduleFinish is missing', () => {
|
|
207
|
+
const task = {
|
|
208
|
+
expressId: 1, globalId: 'x', name: 'x', isMilestone: false,
|
|
209
|
+
childGlobalIds: [], productExpressIds: [], productGlobalIds: [],
|
|
210
|
+
controllingScheduleGlobalIds: [],
|
|
211
|
+
taskTime: { scheduleStart: '2024-01-01T00:00:00Z', scheduleDuration: 'P5D' },
|
|
212
|
+
};
|
|
213
|
+
assert.strictEqual(taskStartEpoch(task), Date.parse('2024-01-01T00:00:00Z'));
|
|
214
|
+
assert.strictEqual(taskFinishEpoch(task), Date.parse('2024-01-06T00:00:00Z'));
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe('countGeneratedTasks', () => {
|
|
219
|
+
const mkTask = (expressId: number | undefined, globalId: string) => ({
|
|
220
|
+
expressId: expressId as number,
|
|
221
|
+
globalId,
|
|
222
|
+
name: globalId,
|
|
223
|
+
isMilestone: false,
|
|
224
|
+
childGlobalIds: [],
|
|
225
|
+
productExpressIds: [],
|
|
226
|
+
productGlobalIds: [],
|
|
227
|
+
controllingScheduleGlobalIds: [],
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('returns 0 for null / empty data', () => {
|
|
231
|
+
assert.strictEqual(countGeneratedTasks(null), 0);
|
|
232
|
+
assert.strictEqual(countGeneratedTasks(undefined), 0);
|
|
233
|
+
assert.strictEqual(countGeneratedTasks({
|
|
234
|
+
hasSchedule: false, workSchedules: [], sequences: [], tasks: [],
|
|
235
|
+
}), 0);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('counts only tasks with expressId <= 0 or missing', () => {
|
|
239
|
+
const data: ScheduleExtraction = {
|
|
240
|
+
hasSchedule: true, workSchedules: [], sequences: [],
|
|
241
|
+
tasks: [
|
|
242
|
+
mkTask(42, 'parsed'), // extracted — already in STEP
|
|
243
|
+
mkTask(0, 'generated-a'), // generated
|
|
244
|
+
mkTask(undefined, 'generated-b'), // generated (missing id)
|
|
245
|
+
mkTask(100, 'parsed-2'), // extracted
|
|
246
|
+
],
|
|
247
|
+
};
|
|
248
|
+
assert.strictEqual(countGeneratedTasks(data), 2);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('agrees with the export partitioning rule (no tasks with expressId>0 counted)', () => {
|
|
252
|
+
// Regression guard: if injectScheduleIntoStep's filter ever diverges from
|
|
253
|
+
// this helper, the badge count and the actual injected set get out of
|
|
254
|
+
// sync. Keep them lockstep.
|
|
255
|
+
const data: ScheduleExtraction = {
|
|
256
|
+
hasSchedule: true, workSchedules: [], sequences: [],
|
|
257
|
+
tasks: [
|
|
258
|
+
mkTask(1, 'a'), mkTask(2, 'b'), mkTask(3, 'c'),
|
|
259
|
+
],
|
|
260
|
+
};
|
|
261
|
+
assert.strictEqual(countGeneratedTasks(data), 0);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// ─── editing (P1) ──────────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Boot a bare scheduleSlice in a test-only zustand store. We don't need
|
|
269
|
+
* the other slices for mutator tests — the cross-slice dirty/version
|
|
270
|
+
* fields are referenced defensively via `as unknown as` casts so they
|
|
271
|
+
* simply no-op when absent, and that's fine for our assertions.
|
|
272
|
+
*/
|
|
273
|
+
function bootScheduleStore() {
|
|
274
|
+
return create<ScheduleSlice>()((set, get, api) => createScheduleSlice(set, get, api));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function mkTask(over: Partial<ScheduleTaskInfo> & { globalId: string }): ScheduleTaskInfo {
|
|
278
|
+
const defaults = {
|
|
279
|
+
expressId: 1,
|
|
280
|
+
name: `T-${over.globalId}`,
|
|
281
|
+
isMilestone: false,
|
|
282
|
+
childGlobalIds: [],
|
|
283
|
+
productExpressIds: [],
|
|
284
|
+
productGlobalIds: [],
|
|
285
|
+
controllingScheduleGlobalIds: [],
|
|
286
|
+
};
|
|
287
|
+
// Spread `over` LAST so callers win every contested field (including globalId).
|
|
288
|
+
return { ...defaults, ...over };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function mkExtraction(tasks: ScheduleTaskInfo[]): ScheduleExtraction {
|
|
292
|
+
return { hasSchedule: true, workSchedules: [], sequences: [], tasks };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
describe('scheduleSlice editing — updateTask', () => {
|
|
296
|
+
it('patches name / predefinedType without touching unrelated fields', () => {
|
|
297
|
+
const store = bootScheduleStore();
|
|
298
|
+
store.getState().setScheduleData(mkExtraction([
|
|
299
|
+
mkTask({ globalId: 'a', name: 'Old', predefinedType: 'CONSTRUCTION', identification: 'IDENT' }),
|
|
300
|
+
]));
|
|
301
|
+
store.getState().updateTask('a', { name: 'New', predefinedType: 'INSTALLATION' });
|
|
302
|
+
|
|
303
|
+
const t = store.getState().scheduleData!.tasks[0];
|
|
304
|
+
assert.strictEqual(t.name, 'New');
|
|
305
|
+
assert.strictEqual(t.predefinedType, 'INSTALLATION');
|
|
306
|
+
assert.strictEqual(t.identification, 'IDENT');
|
|
307
|
+
assert.strictEqual(store.getState().scheduleIsEdited, true);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('isMilestone → true collapses duration and finish', () => {
|
|
311
|
+
const store = bootScheduleStore();
|
|
312
|
+
store.getState().setScheduleData(mkExtraction([
|
|
313
|
+
mkTask({
|
|
314
|
+
globalId: 'm', isMilestone: false,
|
|
315
|
+
taskTime: {
|
|
316
|
+
scheduleStart: '2024-05-01T08:00:00',
|
|
317
|
+
scheduleFinish: '2024-05-05T08:00:00',
|
|
318
|
+
scheduleDuration: 'P4D',
|
|
319
|
+
},
|
|
320
|
+
}),
|
|
321
|
+
]));
|
|
322
|
+
store.getState().updateTask('m', { isMilestone: true });
|
|
323
|
+
const t = store.getState().scheduleData!.tasks[0];
|
|
324
|
+
assert.strictEqual(t.isMilestone, true);
|
|
325
|
+
assert.strictEqual(t.taskTime?.scheduleDuration, 'PT0S');
|
|
326
|
+
assert.strictEqual(t.taskTime?.scheduleFinish, '2024-05-01T08:00:00');
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
describe('scheduleSlice editing — updateTaskTime', () => {
|
|
331
|
+
it('start + finish → derived duration (days)', () => {
|
|
332
|
+
const store = bootScheduleStore();
|
|
333
|
+
store.getState().setScheduleData(mkExtraction([
|
|
334
|
+
mkTask({ globalId: 'a', taskTime: { scheduleStart: '2024-05-01T08:00:00' } }),
|
|
335
|
+
]));
|
|
336
|
+
store.getState().updateTaskTime('a', {
|
|
337
|
+
scheduleStart: '2024-05-01T08:00:00',
|
|
338
|
+
scheduleFinish: '2024-05-06T08:00:00',
|
|
339
|
+
});
|
|
340
|
+
const t = store.getState().scheduleData!.tasks[0];
|
|
341
|
+
assert.strictEqual(t.taskTime?.scheduleDuration, 'P5D');
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('start + duration → derived finish', () => {
|
|
345
|
+
const store = bootScheduleStore();
|
|
346
|
+
store.getState().setScheduleData(mkExtraction([
|
|
347
|
+
mkTask({ globalId: 'a' }),
|
|
348
|
+
]));
|
|
349
|
+
store.getState().updateTaskTime('a', {
|
|
350
|
+
scheduleStart: '2024-05-01T08:00:00',
|
|
351
|
+
scheduleDuration: 'P5D',
|
|
352
|
+
});
|
|
353
|
+
const t = store.getState().scheduleData!.tasks[0];
|
|
354
|
+
assert.strictEqual(t.taskTime?.scheduleFinish, '2024-05-06T08:00:00');
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('rejects finish-before-start (no mutation)', () => {
|
|
358
|
+
const store = bootScheduleStore();
|
|
359
|
+
store.getState().setScheduleData(mkExtraction([
|
|
360
|
+
mkTask({
|
|
361
|
+
globalId: 'a',
|
|
362
|
+
taskTime: {
|
|
363
|
+
scheduleStart: '2024-05-05T08:00:00',
|
|
364
|
+
scheduleFinish: '2024-05-10T08:00:00',
|
|
365
|
+
},
|
|
366
|
+
}),
|
|
367
|
+
]));
|
|
368
|
+
store.getState().updateTaskTime('a', { scheduleFinish: '2024-05-01T08:00:00' });
|
|
369
|
+
// Value unchanged.
|
|
370
|
+
const t = store.getState().scheduleData!.tasks[0];
|
|
371
|
+
assert.strictEqual(t.taskTime?.scheduleFinish, '2024-05-10T08:00:00');
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
describe('scheduleSlice editing — assign / unassign products', () => {
|
|
376
|
+
it('assign appends + dedupes', () => {
|
|
377
|
+
const store = bootScheduleStore();
|
|
378
|
+
store.getState().setScheduleData(mkExtraction([
|
|
379
|
+
mkTask({ globalId: 'a', productExpressIds: [1], productGlobalIds: ['1'] }),
|
|
380
|
+
]));
|
|
381
|
+
store.getState().assignProductsToTask('a', [1, 2, 3]);
|
|
382
|
+
const t = store.getState().scheduleData!.tasks[0];
|
|
383
|
+
assert.deepStrictEqual(t.productExpressIds.sort(), [1, 2, 3]);
|
|
384
|
+
// Calling again with same set is idempotent.
|
|
385
|
+
store.getState().assignProductsToTask('a', [2, 3]);
|
|
386
|
+
assert.deepStrictEqual(
|
|
387
|
+
store.getState().scheduleData!.tasks[0].productExpressIds.sort(),
|
|
388
|
+
[1, 2, 3],
|
|
389
|
+
);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('unassign drops targeted ids only', () => {
|
|
393
|
+
const store = bootScheduleStore();
|
|
394
|
+
store.getState().setScheduleData(mkExtraction([
|
|
395
|
+
mkTask({
|
|
396
|
+
globalId: 'a',
|
|
397
|
+
productExpressIds: [1, 2, 3],
|
|
398
|
+
productGlobalIds: ['1', '2', '3'],
|
|
399
|
+
}),
|
|
400
|
+
]));
|
|
401
|
+
store.getState().unassignProductsFromTask('a', [2]);
|
|
402
|
+
const t = store.getState().scheduleData!.tasks[0];
|
|
403
|
+
assert.deepStrictEqual(t.productExpressIds, [1, 3]);
|
|
404
|
+
assert.deepStrictEqual(t.productGlobalIds, ['1', '3']);
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
describe('scheduleSlice editing — deleteTask', () => {
|
|
409
|
+
it('removes the task and cascades sequences referring to it', () => {
|
|
410
|
+
const store = bootScheduleStore();
|
|
411
|
+
const data: ScheduleExtraction = {
|
|
412
|
+
hasSchedule: true, workSchedules: [], tasks: [
|
|
413
|
+
mkTask({ globalId: 'a' }),
|
|
414
|
+
mkTask({ globalId: 'b' }),
|
|
415
|
+
],
|
|
416
|
+
sequences: [
|
|
417
|
+
{ globalId: 'seq-ab', relatingTaskGlobalId: 'a', relatedTaskGlobalId: 'b', sequenceType: 'FINISH_START' },
|
|
418
|
+
],
|
|
419
|
+
};
|
|
420
|
+
store.getState().setScheduleData(data);
|
|
421
|
+
store.getState().deleteTask('a');
|
|
422
|
+
const s = store.getState().scheduleData!;
|
|
423
|
+
assert.strictEqual(s.tasks.length, 1);
|
|
424
|
+
assert.strictEqual(s.tasks[0].globalId, 'b');
|
|
425
|
+
assert.strictEqual(s.sequences.length, 0);
|
|
426
|
+
assert.strictEqual(store.getState().scheduleIsEdited, true);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('cascades into descendant tasks', () => {
|
|
430
|
+
const store = bootScheduleStore();
|
|
431
|
+
store.getState().setScheduleData({
|
|
432
|
+
hasSchedule: true, workSchedules: [], sequences: [],
|
|
433
|
+
tasks: [
|
|
434
|
+
mkTask({ globalId: 'parent', childGlobalIds: ['child1', 'child2'] }),
|
|
435
|
+
mkTask({ globalId: 'child1', parentGlobalId: 'parent' }),
|
|
436
|
+
mkTask({ globalId: 'child2', parentGlobalId: 'parent', childGlobalIds: ['grand'] }),
|
|
437
|
+
mkTask({ globalId: 'grand', parentGlobalId: 'child2' }),
|
|
438
|
+
mkTask({ globalId: 'unrelated' }),
|
|
439
|
+
],
|
|
440
|
+
});
|
|
441
|
+
store.getState().deleteTask('parent');
|
|
442
|
+
const s = store.getState().scheduleData!;
|
|
443
|
+
const remaining = s.tasks.map(t => t.globalId).sort();
|
|
444
|
+
assert.deepStrictEqual(remaining, ['unrelated']);
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
describe('scheduleSlice editing — undo / redo', () => {
|
|
449
|
+
it('undo restores pre-edit state; redo reapplies', () => {
|
|
450
|
+
const store = bootScheduleStore();
|
|
451
|
+
store.getState().setScheduleData(mkExtraction([
|
|
452
|
+
mkTask({ globalId: 'a', name: 'Original' }),
|
|
453
|
+
]));
|
|
454
|
+
store.getState().updateTask('a', { name: 'Changed' });
|
|
455
|
+
assert.strictEqual(store.getState().scheduleData!.tasks[0].name, 'Changed');
|
|
456
|
+
|
|
457
|
+
store.getState().undoScheduleEdit();
|
|
458
|
+
assert.strictEqual(store.getState().scheduleData!.tasks[0].name, 'Original');
|
|
459
|
+
assert.strictEqual(store.getState().scheduleIsEdited, false);
|
|
460
|
+
|
|
461
|
+
store.getState().redoScheduleEdit();
|
|
462
|
+
assert.strictEqual(store.getState().scheduleData!.tasks[0].name, 'Changed');
|
|
463
|
+
assert.strictEqual(store.getState().scheduleIsEdited, true);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('transactions coalesce rapid edits into a single undo step', () => {
|
|
467
|
+
const store = bootScheduleStore();
|
|
468
|
+
store.getState().setScheduleData(mkExtraction([
|
|
469
|
+
mkTask({
|
|
470
|
+
globalId: 'a',
|
|
471
|
+
taskTime: {
|
|
472
|
+
scheduleStart: '2024-05-01T08:00:00',
|
|
473
|
+
scheduleFinish: '2024-05-02T08:00:00',
|
|
474
|
+
},
|
|
475
|
+
}),
|
|
476
|
+
]));
|
|
477
|
+
store.getState().beginScheduleTransaction('drag');
|
|
478
|
+
store.getState().updateTaskTime('a', { scheduleStart: '2024-05-01T09:00:00' });
|
|
479
|
+
store.getState().updateTaskTime('a', { scheduleStart: '2024-05-01T10:00:00' });
|
|
480
|
+
store.getState().updateTaskTime('a', { scheduleStart: '2024-05-01T12:00:00' });
|
|
481
|
+
store.getState().endScheduleTransaction();
|
|
482
|
+
assert.strictEqual(store.getState().scheduleUndoStack.length, 1);
|
|
483
|
+
store.getState().undoScheduleEdit();
|
|
484
|
+
assert.strictEqual(
|
|
485
|
+
store.getState().scheduleData!.tasks[0].taskTime?.scheduleStart,
|
|
486
|
+
'2024-05-01T08:00:00',
|
|
487
|
+
);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('transaction state is store-scoped — two stores do not alias', () => {
|
|
491
|
+
// Regression: transaction state used to live at module scope, which
|
|
492
|
+
// meant a transaction opened on one store leaked into a second store
|
|
493
|
+
// instantiated in the same process (tests, multi-session, hot-reload).
|
|
494
|
+
// Now that state lives inside the slice, each store owns its own window.
|
|
495
|
+
const storeA = bootScheduleStore();
|
|
496
|
+
const storeB = bootScheduleStore();
|
|
497
|
+
storeA.getState().setScheduleData(mkExtraction([
|
|
498
|
+
mkTask({
|
|
499
|
+
globalId: 'a',
|
|
500
|
+
taskTime: {
|
|
501
|
+
scheduleStart: '2024-05-01T08:00:00',
|
|
502
|
+
scheduleFinish: '2024-05-02T08:00:00',
|
|
503
|
+
},
|
|
504
|
+
}),
|
|
505
|
+
]));
|
|
506
|
+
storeB.getState().setScheduleData(mkExtraction([
|
|
507
|
+
mkTask({
|
|
508
|
+
globalId: 'b',
|
|
509
|
+
taskTime: {
|
|
510
|
+
scheduleStart: '2024-05-01T08:00:00',
|
|
511
|
+
scheduleFinish: '2024-05-02T08:00:00',
|
|
512
|
+
},
|
|
513
|
+
}),
|
|
514
|
+
]));
|
|
515
|
+
|
|
516
|
+
// Open a transaction on A. B should see a clean transaction state.
|
|
517
|
+
storeA.getState().beginScheduleTransaction('drag');
|
|
518
|
+
assert.strictEqual(storeA.getState().scheduleTransaction.active, true);
|
|
519
|
+
assert.strictEqual(storeB.getState().scheduleTransaction.active, false);
|
|
520
|
+
|
|
521
|
+
// Edits on B should produce independent undo entries — not suppressed
|
|
522
|
+
// by A's open transaction.
|
|
523
|
+
storeB.getState().updateTaskTime('b', { scheduleStart: '2024-05-01T09:00:00' });
|
|
524
|
+
storeB.getState().updateTaskTime('b', { scheduleStart: '2024-05-01T10:00:00' });
|
|
525
|
+
// Each edit on B gets its own snapshot (2 total) because B is not in
|
|
526
|
+
// a transaction. If the module-level global were still here, A's
|
|
527
|
+
// transaction would suppress B's snapshots and we'd see 0.
|
|
528
|
+
assert.strictEqual(storeB.getState().scheduleUndoStack.length, 2);
|
|
529
|
+
|
|
530
|
+
storeA.getState().endScheduleTransaction();
|
|
531
|
+
assert.strictEqual(storeA.getState().scheduleTransaction.active, false);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
// ── P1.4 — operation-based undo replay symmetry ─────────────────────
|
|
535
|
+
|
|
536
|
+
it('undo → redo → undo is byte-identical on field edits', () => {
|
|
537
|
+
// Property test — after N undos + N redos + N undos the state must
|
|
538
|
+
// equal the state after the first N undos, byte-for-byte. Proves the
|
|
539
|
+
// field-patch descriptor's inverse-capture keeps undo/redo symmetric.
|
|
540
|
+
// If this breaks, users lose edits on second-undo after a redo.
|
|
541
|
+
const store = bootScheduleStore();
|
|
542
|
+
store.getState().setScheduleData(mkExtraction([
|
|
543
|
+
mkTask({
|
|
544
|
+
globalId: 'a', name: 'A0', identification: 'id0',
|
|
545
|
+
taskTime: {
|
|
546
|
+
scheduleStart: '2024-05-01T08:00:00',
|
|
547
|
+
scheduleFinish: '2024-05-02T08:00:00',
|
|
548
|
+
},
|
|
549
|
+
}),
|
|
550
|
+
]));
|
|
551
|
+
store.getState().updateTask('a', { name: 'A1' });
|
|
552
|
+
store.getState().updateTaskTime('a', { scheduleStart: '2024-05-01T12:00:00' });
|
|
553
|
+
|
|
554
|
+
store.getState().undoScheduleEdit();
|
|
555
|
+
store.getState().undoScheduleEdit();
|
|
556
|
+
const afterUndos = JSON.stringify(store.getState().scheduleData);
|
|
557
|
+
assert.strictEqual(store.getState().scheduleIsEdited, false);
|
|
558
|
+
|
|
559
|
+
store.getState().redoScheduleEdit();
|
|
560
|
+
store.getState().redoScheduleEdit();
|
|
561
|
+
assert.strictEqual(store.getState().scheduleData!.tasks[0].name, 'A1');
|
|
562
|
+
assert.strictEqual(store.getState().scheduleIsEdited, true);
|
|
563
|
+
|
|
564
|
+
store.getState().undoScheduleEdit();
|
|
565
|
+
store.getState().undoScheduleEdit();
|
|
566
|
+
assert.strictEqual(
|
|
567
|
+
JSON.stringify(store.getState().scheduleData),
|
|
568
|
+
afterUndos,
|
|
569
|
+
'second undo pass must be byte-identical to the first',
|
|
570
|
+
);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it('updateTaskTime rejects finish < start without pushing a snapshot', () => {
|
|
574
|
+
// Rejection is silent but MUST leave the stack unchanged, otherwise
|
|
575
|
+
// the user sees a redo-empty undo chip even though nothing committed.
|
|
576
|
+
const store = bootScheduleStore();
|
|
577
|
+
store.getState().setScheduleData(mkExtraction([
|
|
578
|
+
mkTask({
|
|
579
|
+
globalId: 'a',
|
|
580
|
+
taskTime: {
|
|
581
|
+
scheduleStart: '2024-05-01T08:00:00',
|
|
582
|
+
scheduleFinish: '2024-05-02T08:00:00',
|
|
583
|
+
},
|
|
584
|
+
}),
|
|
585
|
+
]));
|
|
586
|
+
store.getState().updateTaskTime('a', { scheduleFinish: '2024-04-01T00:00:00' });
|
|
587
|
+
assert.strictEqual(store.getState().scheduleUndoStack.length, 0);
|
|
588
|
+
assert.strictEqual(
|
|
589
|
+
store.getState().scheduleData!.tasks[0].taskTime?.scheduleFinish,
|
|
590
|
+
'2024-05-02T08:00:00',
|
|
591
|
+
);
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
describe('scheduleSlice editing — addTask', () => {
|
|
596
|
+
it('appends at the end when no predecessor is given', () => {
|
|
597
|
+
const store = bootScheduleStore();
|
|
598
|
+
store.getState().setScheduleData(mkExtraction([
|
|
599
|
+
mkTask({ globalId: 'a', name: 'A' }),
|
|
600
|
+
mkTask({ globalId: 'b', name: 'B' }),
|
|
601
|
+
]));
|
|
602
|
+
const newGid = store.getState().addTask();
|
|
603
|
+
const s = store.getState().scheduleData!;
|
|
604
|
+
assert.strictEqual(s.tasks.length, 3);
|
|
605
|
+
assert.strictEqual(s.tasks[2].globalId, newGid);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it('inserts after the given predecessor', () => {
|
|
609
|
+
const store = bootScheduleStore();
|
|
610
|
+
store.getState().setScheduleData(mkExtraction([
|
|
611
|
+
mkTask({ globalId: 'a', name: 'A' }),
|
|
612
|
+
mkTask({ globalId: 'b', name: 'B' }),
|
|
613
|
+
mkTask({ globalId: 'c', name: 'C' }),
|
|
614
|
+
]));
|
|
615
|
+
const newGid = store.getState().addTask({ afterGlobalId: 'a' });
|
|
616
|
+
const names = store.getState().scheduleData!.tasks.map(t => t.globalId);
|
|
617
|
+
assert.deepStrictEqual(names, ['a', newGid, 'b', 'c']);
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
it('auto-selects the new task for immediate rename', () => {
|
|
621
|
+
const store = bootScheduleStore();
|
|
622
|
+
store.getState().setScheduleData(mkExtraction([mkTask({ globalId: 'a' })]));
|
|
623
|
+
const newGid = store.getState().addTask();
|
|
624
|
+
const sel = Array.from(store.getState().selectedTaskGlobalIds);
|
|
625
|
+
assert.deepStrictEqual(sel, [newGid]);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it('flips scheduleIsEdited so the export badge lights up', () => {
|
|
629
|
+
const store = bootScheduleStore();
|
|
630
|
+
store.getState().setScheduleData(mkExtraction([mkTask({ globalId: 'a' })]));
|
|
631
|
+
store.getState().addTask();
|
|
632
|
+
assert.strictEqual(store.getState().scheduleIsEdited, true);
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it('synthesises a work schedule when none exists', () => {
|
|
636
|
+
const store = bootScheduleStore();
|
|
637
|
+
store.getState().setScheduleData({
|
|
638
|
+
hasSchedule: true, workSchedules: [], sequences: [], tasks: [],
|
|
639
|
+
});
|
|
640
|
+
store.getState().addTask();
|
|
641
|
+
const s = store.getState().scheduleData!;
|
|
642
|
+
assert.strictEqual(s.workSchedules.length, 1);
|
|
643
|
+
assert.strictEqual(s.workSchedules[0].taskGlobalIds.length, 1);
|
|
644
|
+
assert.strictEqual(s.workSchedules[0].taskGlobalIds[0], s.tasks[0].globalId);
|
|
645
|
+
});
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
describe('scheduleSlice editing — moveTask', () => {
|
|
649
|
+
it('moves a task to the requested index', () => {
|
|
650
|
+
const store = bootScheduleStore();
|
|
651
|
+
store.getState().setScheduleData(mkExtraction([
|
|
652
|
+
mkTask({ globalId: 'a' }),
|
|
653
|
+
mkTask({ globalId: 'b' }),
|
|
654
|
+
mkTask({ globalId: 'c' }),
|
|
655
|
+
mkTask({ globalId: 'd' }),
|
|
656
|
+
]));
|
|
657
|
+
store.getState().moveTask('a', 2);
|
|
658
|
+
const order = store.getState().scheduleData!.tasks.map(t => t.globalId);
|
|
659
|
+
assert.deepStrictEqual(order, ['b', 'c', 'a', 'd']);
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
it('moves backwards too (larger index to smaller)', () => {
|
|
663
|
+
const store = bootScheduleStore();
|
|
664
|
+
store.getState().setScheduleData(mkExtraction([
|
|
665
|
+
mkTask({ globalId: 'a' }),
|
|
666
|
+
mkTask({ globalId: 'b' }),
|
|
667
|
+
mkTask({ globalId: 'c' }),
|
|
668
|
+
]));
|
|
669
|
+
store.getState().moveTask('c', 0);
|
|
670
|
+
const order = store.getState().scheduleData!.tasks.map(t => t.globalId);
|
|
671
|
+
assert.deepStrictEqual(order, ['c', 'a', 'b']);
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it('reflects move in the work schedule taskGlobalIds', () => {
|
|
675
|
+
const store = bootScheduleStore();
|
|
676
|
+
store.getState().setScheduleData({
|
|
677
|
+
hasSchedule: true, sequences: [],
|
|
678
|
+
workSchedules: [{
|
|
679
|
+
expressId: 0, globalId: 'ws', kind: 'WorkSchedule', name: 'WS',
|
|
680
|
+
taskGlobalIds: ['a', 'b', 'c'],
|
|
681
|
+
}],
|
|
682
|
+
tasks: [
|
|
683
|
+
mkTask({ globalId: 'a' }),
|
|
684
|
+
mkTask({ globalId: 'b' }),
|
|
685
|
+
mkTask({ globalId: 'c' }),
|
|
686
|
+
],
|
|
687
|
+
});
|
|
688
|
+
store.getState().moveTask('c', 0);
|
|
689
|
+
assert.deepStrictEqual(
|
|
690
|
+
store.getState().scheduleData!.workSchedules[0].taskGlobalIds,
|
|
691
|
+
['c', 'a', 'b'],
|
|
692
|
+
);
|
|
693
|
+
});
|
|
694
|
+
});
|