@thatopen/services 0.3.16 → 0.5.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/README.md +62 -62
- package/dist/cli/commands/rhino.d.ts +11 -0
- package/dist/cli/lib/plugin-install.d.ts +24 -0
- package/dist/cli.js +124 -1
- package/dist/core/client.d.ts +1 -0
- package/dist/core/platform-client.d.ts +7 -0
- package/dist/index.cjs.js +2 -2
- package/dist/index.es.js +382 -370
- package/dist/vite-env.d.ts +1 -1
- package/docs/ai-quickstart.md +130 -130
- package/docs/cli/create.md +29 -29
- package/docs/cli/local-server.md +38 -38
- package/docs/cli/login.md +32 -32
- package/docs/cli/publish.md +31 -31
- package/docs/cli/run.md +26 -26
- package/docs/cli/serve.md +43 -43
- package/docs/cli/swap.md +30 -30
- package/docs/cloud/GaussianSplatConverter.md +43 -43
- package/docs/cloud/PointCloudConverter.md +53 -53
- package/docs/revit-collab-quickstart.md +256 -256
- package/package.json +95 -95
- package/resources/AGENTS.md +40 -40
- package/src/built-in/CDEManager/example.ts +214 -214
- package/src/built-in/CDEManager/examples/FileEditing/example.ts +226 -226
- package/src/built-in/CDEManager/examples/FileMoves/example.ts +186 -186
- package/src/built-in/CDEManager/examples/FileOperations/example.ts +295 -295
- package/src/built-in/CDEManager/examples/FileTransfers/example.ts +210 -210
- package/src/built-in/CDEManager/examples/FolderManagement/example.ts +230 -230
- package/src/built-in/CDEManager/examples/MetadataSchema/example.ts +230 -230
- package/src/built-in/CDEManager/examples/MetadataViews/example.ts +166 -166
- package/src/built-in/CDEManager/examples/ProjectConfig/example.ts +177 -177
- package/src/built-in/ClashesManager/example.ts +283 -283
- package/src/built-in/UIManager/src/app/example.ts +97 -97
- package/src/built-in/UIManager/src/app/examples/AppState/example.ts +140 -140
- package/src/built-in/UIManager/src/app/examples/Banner/example.ts +159 -159
- package/src/built-in/UIManager/src/app/examples/Contexts/example.ts +123 -123
- package/src/built-in/UIManager/src/app/examples/FileDrop/example.ts +124 -124
- package/src/built-in/UIManager/src/app/examples/FloatingLayout/example.ts +90 -90
- package/src/built-in/UIManager/src/app/examples/Modal/example.ts +163 -163
- package/src/built-in/UIManager/src/app/examples/Notifications/example.ts +133 -133
- package/src/built-in/UIManager/src/app/examples/Sidebar/example.ts +102 -102
- package/src/cli/templates/app/README.md +217 -217
- package/src/cli/templates/app/index.html +35 -35
- package/src/cli/templates/app/package.json +32 -32
- package/src/cli/templates/app/src/app.ts +37 -37
- package/src/cli/templates/app/src/assets/tool-placeholder.ts +50 -50
- package/src/cli/templates/app/src/bim-components/CloudRunner/index.ts +95 -95
- package/src/cli/templates/app/src/bim-components/CloudRunner/src/index.ts +1 -1
- package/src/cli/templates/app/src/bim-components/CloudRunner/src/types.ts +5 -5
- package/src/cli/templates/app/src/bim-components/index.ts +1 -1
- package/src/cli/templates/app/src/globals.ts +1 -1
- package/src/cli/templates/app/src/main.ts +199 -199
- package/src/cli/templates/app/src/setups/reality-capture/lib/hidden-tiles-plugin.ts +88 -88
- package/src/cli/templates/app/src/setups/reality-capture/lib/point-tile-plugin.ts +166 -166
- package/src/cli/templates/app/src/setups/reality-capture/lib/render-frame.ts +31 -31
- package/src/cli/templates/app/src/setups/reality-capture/lib/splat-tile-plugin.ts +51 -51
- package/src/cli/templates/app/src/setups/reality-capture-viewer.ts +1383 -1383
- package/src/cli/templates/app/src/ui-components/app-info-section/index.ts +26 -26
- package/src/cli/templates/app/src/ui-components/app-info-section/src/index.ts +1 -1
- package/src/cli/templates/app/src/ui-components/app-info-section/src/types.ts +15 -15
- package/src/cli/templates/app/src/ui-components/cloud-runner-section/index.ts +37 -37
- package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/index.ts +1 -1
- package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/types.ts +14 -14
- package/src/cli/templates/app/src/ui-components/index.ts +2 -2
- package/src/cli/templates/app/tsconfig.json +16 -16
- package/src/cli/templates/app/vite.config.js +57 -57
- package/src/cli/templates/cloud-component/README.md +205 -205
- package/src/cli/templates/cloud-component/_thatopen +5 -5
- package/src/cli/templates/cloud-component/declarations.json +4 -4
- package/src/cli/templates/cloud-component/package.json +21 -21
- package/src/cli/templates/cloud-component/src/main.ts +70 -70
- package/src/cli/templates/cloud-component/tsconfig.json +16 -16
- package/src/cli/templates/cloud-component/vite.config.js +46 -46
- package/src/cli/templates/shared/AGENTS.md +8 -8
- package/src/cli/templates/shared/CLAUDE.md +1 -1
- package/src/cli/templates/shared/_gitignore +5 -5
|
@@ -1,230 +1,230 @@
|
|
|
1
|
-
/* MD
|
|
2
|
-
## Folder management — create, rename, archive, and commit
|
|
3
|
-
---
|
|
4
|
-
A project reaches the construction phase and the coordination team needs to
|
|
5
|
-
restructure the folder hierarchy: add a "Facades" discipline folder, rename
|
|
6
|
-
"Structural" to its official abbreviation, and archive two superseded folders that
|
|
7
|
-
have been merged. Doing these one at a time through separate API calls means the
|
|
8
|
-
folder tree flickers between invalid states as each change lands — a freshly created
|
|
9
|
-
folder appears before its sibling is renamed, and the archived folders briefly
|
|
10
|
-
disappear before the new one is visible.
|
|
11
|
-
|
|
12
|
-
CDEManager stages all folder changes in memory first. Adds, renames, and archives
|
|
13
|
-
accumulate in a `pendingChanges` list before anything is sent to the backend. The
|
|
14
|
-
full diff is readable from the machine state at any point, so the UI can preview
|
|
15
|
-
exactly what the folder tree will look like after the commit. Cancelling throws away
|
|
16
|
-
all staged changes in one step. Saving applies everything in the right dependency
|
|
17
|
-
order and fires a single completion event with the counts.
|
|
18
|
-
|
|
19
|
-
This tutorial covers entering folder edit mode; staging new folders with a temporary
|
|
20
|
-
ID that gets resolved to a real ID on save; staging renames and observing how
|
|
21
|
-
renaming back to the original value automatically removes the change; the
|
|
22
|
-
archive-folder toggle behavior, where sending the event a second time unstages the
|
|
23
|
-
archive; removing a staged temp folder and all its staged children in one event;
|
|
24
|
-
reverting all staged changes for a single folder; and committing the full batch.
|
|
25
|
-
|
|
26
|
-
By the end, you'll have a working demonstration of every folder-management state
|
|
27
|
-
machine flow — stage, inspect, revert, and commit — against mock data with no
|
|
28
|
-
backend connection needed for the staging sections.
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
import * as OBC from "@thatopen/components";
|
|
32
|
-
// In production platform apps, CDEManager is loaded through the platform client:
|
|
33
|
-
// const cde = await client.initBuiltInComponent(CDEManager.uuid, components);
|
|
34
|
-
// Here we import directly from source for local testing.
|
|
35
|
-
import { CDEManager } from "../../index";
|
|
36
|
-
|
|
37
|
-
/* MD
|
|
38
|
-
### 🛠️ Setting up
|
|
39
|
-
The save path requires a live client context — `_saveFolderChanges` returns early if
|
|
40
|
-
no context is set and the machine would stay stuck in `savingFolderChanges` forever.
|
|
41
|
-
We keep the mock context active throughout the entire example so the commit section
|
|
42
|
-
works correctly.
|
|
43
|
-
*/
|
|
44
|
-
|
|
45
|
-
const components = new OBC.Components();
|
|
46
|
-
const cde = components.get(CDEManager);
|
|
47
|
-
|
|
48
|
-
const output = document.getElementById("output") as HTMLPreElement;
|
|
49
|
-
const log = (msg: string) => { console.log(msg); output.textContent += msg + "\n"; };
|
|
50
|
-
|
|
51
|
-
let folderIdx = 0;
|
|
52
|
-
const mockClient = {
|
|
53
|
-
checkPermissionBatch: async () => Array(5).fill({ hasPermission: true }),
|
|
54
|
-
getProjectData: async () => ({ project: { title: "" }, users: [] }),
|
|
55
|
-
listFolders: async () => [],
|
|
56
|
-
archiveFolder: async (_id: string) => ({}),
|
|
57
|
-
updateFolder: async (_id: string, _updates: any) => ({}),
|
|
58
|
-
createFolder: async (name: string, parentId?: string) => ({
|
|
59
|
-
_id: `created-${folderIdx++}`, name, parentId: parentId ?? null,
|
|
60
|
-
}),
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
cde.ctx = { client: mockClient as any, projectId: "mock" };
|
|
64
|
-
await cde.loadFolders();
|
|
65
|
-
|
|
66
|
-
const folders = [
|
|
67
|
-
{ _id: "folder-str", name: "Structural", parentId: null },
|
|
68
|
-
{ _id: "folder-arch", name: "Architecture", parentId: null },
|
|
69
|
-
{ _id: "folder-mep", name: "MEP", parentId: "folder-arch" },
|
|
70
|
-
];
|
|
71
|
-
cde.folders = folders as any;
|
|
72
|
-
|
|
73
|
-
cde.onMachineStateChanged.add((s) => log(` → ${s.kind}`));
|
|
74
|
-
cde.onFolderChangesCompleted.add(({ created, renamed, archived }) =>
|
|
75
|
-
log(` [onFolderChangesCompleted] created: ${created}, renamed: ${renamed}, archived: ${archived}`)
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
/* MD
|
|
79
|
-
### 📁 Staging folder changes
|
|
80
|
-
New folders are added with a temporary ID assigned by the caller — the UI can use it
|
|
81
|
-
to identify the row in the pending list before the real ID arrives from the backend.
|
|
82
|
-
Renames and archives for existing folders are keyed by their real ID. All three
|
|
83
|
-
change kinds accumulate in the same `pendingChanges` array, so the UI can render a
|
|
84
|
-
single diff view for the whole operation.
|
|
85
|
-
|
|
86
|
-
Cancelling discards everything and returns to idle. Unlike file bulk editing, folder
|
|
87
|
-
editing has no concept of "return to previous selection" — it always returns to idle.
|
|
88
|
-
*/
|
|
89
|
-
|
|
90
|
-
log("\n--- stage changes and cancel ---");
|
|
91
|
-
cde.sendMachineEvent({ type: "START_EDIT_FOLDERS" });
|
|
92
|
-
|
|
93
|
-
// New root-level folder.
|
|
94
|
-
cde.sendMachineEvent({ type: "ADD_FOLDER", tempId: "temp_facades", name: "Facades", parentId: null });
|
|
95
|
-
// New folder nested inside the temp folder above.
|
|
96
|
-
cde.sendMachineEvent({ type: "ADD_FOLDER", tempId: "temp_facades_sub", name: "Glazing", parentId: "temp_facades" });
|
|
97
|
-
|
|
98
|
-
// Rename an existing folder.
|
|
99
|
-
cde.sendMachineEvent({ type: "RENAME_FOLDER", folderId: "folder-str", name: "STR" });
|
|
100
|
-
|
|
101
|
-
// Archive an existing folder.
|
|
102
|
-
cde.sendMachineEvent({ type: "ARCHIVE_FOLDER", folderId: "folder-mep" });
|
|
103
|
-
|
|
104
|
-
if (cde.machineState.kind === "editingFolders") {
|
|
105
|
-
const changes = cde.machineState.pendingChanges;
|
|
106
|
-
const creates = changes.filter(c => c.kind === "create").length;
|
|
107
|
-
const renames = changes.filter(c => c.kind === "rename").length;
|
|
108
|
-
const archives = changes.filter(c => c.kind === "archive").length;
|
|
109
|
-
log(` pending: ${creates} create(s), ${renames} rename(s), ${archives} archive(s)`);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
cde.sendMachineEvent({ type: "CANCEL_FOLDER_CHANGES" });
|
|
113
|
-
log(` state after cancel: ${cde.machineState.kind}`);
|
|
114
|
-
log(` folders unchanged: ${cde.folders.length}`);
|
|
115
|
-
|
|
116
|
-
/* MD
|
|
117
|
-
### 🔍 Smart rename diff and archive toggle
|
|
118
|
-
Renaming an existing folder back to its current name automatically removes the
|
|
119
|
-
rename change from the pending list — the same smart diffing used for file metadata.
|
|
120
|
-
This keeps the pending list clean so the commit only sends real changes to the
|
|
121
|
-
backend.
|
|
122
|
-
|
|
123
|
-
Archiving the same existing folder twice toggles the staged archive off. This is
|
|
124
|
-
different from the file-editing archive, which needs an explicit unarchive event to
|
|
125
|
-
remove the mark. For folders, sending the archive event again is the undo gesture.
|
|
126
|
-
*/
|
|
127
|
-
|
|
128
|
-
log("\n--- smart rename diff ---");
|
|
129
|
-
cde.sendMachineEvent({ type: "START_EDIT_FOLDERS" });
|
|
130
|
-
|
|
131
|
-
cde.sendMachineEvent({ type: "RENAME_FOLDER", folderId: "folder-str", name: "STR" });
|
|
132
|
-
if (cde.machineState.kind === "editingFolders") {
|
|
133
|
-
log(` after rename "STR": ${cde.machineState.pendingChanges.filter(c => c.kind === "rename").length} rename(s)`);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Renaming back to the original name removes the change automatically.
|
|
137
|
-
cde.sendMachineEvent({ type: "RENAME_FOLDER", folderId: "folder-str", name: "Structural" });
|
|
138
|
-
if (cde.machineState.kind === "editingFolders") {
|
|
139
|
-
log(` after revert to "Structural": ${cde.machineState.pendingChanges.filter(c => c.kind === "rename").length} rename(s)`);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
log("\n--- archive toggle ---");
|
|
143
|
-
cde.sendMachineEvent({ type: "ARCHIVE_FOLDER", folderId: "folder-arch" });
|
|
144
|
-
if (cde.machineState.kind === "editingFolders") {
|
|
145
|
-
log(` after first ARCHIVE_FOLDER: ${cde.machineState.pendingChanges.filter(c => c.kind === "archive").length} archive(s)`);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
cde.sendMachineEvent({ type: "ARCHIVE_FOLDER", folderId: "folder-arch" });
|
|
149
|
-
if (cde.machineState.kind === "editingFolders") {
|
|
150
|
-
log(` after second ARCHIVE_FOLDER (toggle): ${cde.machineState.pendingChanges.filter(c => c.kind === "archive").length} archive(s)`);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
cde.sendMachineEvent({ type: "CANCEL_FOLDER_CHANGES" });
|
|
154
|
-
|
|
155
|
-
/* MD
|
|
156
|
-
### 🗑️ Removing temp folders and reverting existing ones
|
|
157
|
-
When a staged new folder is archived or reset, it is removed from the pending list
|
|
158
|
-
entirely — there is nothing to archive on the backend yet because it was never
|
|
159
|
-
created. Any folders staged as children of the removed folder are removed along with
|
|
160
|
-
it, so the pending list never contains orphaned children.
|
|
161
|
-
|
|
162
|
-
For existing folders, a reset event clears all staged changes for that folder at
|
|
163
|
-
once — both a pending rename and a pending archive in the same step — without
|
|
164
|
-
affecting any other staged changes.
|
|
165
|
-
*/
|
|
166
|
-
|
|
167
|
-
log("\n--- remove temp folder cascade ---");
|
|
168
|
-
cde.sendMachineEvent({ type: "START_EDIT_FOLDERS" });
|
|
169
|
-
|
|
170
|
-
// Stage a temp parent with two children.
|
|
171
|
-
cde.sendMachineEvent({ type: "ADD_FOLDER", tempId: "temp_p", name: "Parent", parentId: null });
|
|
172
|
-
cde.sendMachineEvent({ type: "ADD_FOLDER", tempId: "temp_c1", name: "Child A", parentId: "temp_p" });
|
|
173
|
-
cde.sendMachineEvent({ type: "ADD_FOLDER", tempId: "temp_c2", name: "Child B", parentId: "temp_p" });
|
|
174
|
-
|
|
175
|
-
if (cde.machineState.kind === "editingFolders") {
|
|
176
|
-
log(` staged before archive: ${cde.machineState.pendingChanges.length} change(s)`);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Archiving a temp folder removes it AND all its staged children.
|
|
180
|
-
cde.sendMachineEvent({ type: "ARCHIVE_FOLDER", folderId: "temp_p" });
|
|
181
|
-
|
|
182
|
-
if (cde.machineState.kind === "editingFolders") {
|
|
183
|
-
log(` staged after archive temp parent: ${cde.machineState.pendingChanges.length} change(s)`);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
log("\n--- reset existing folder ---");
|
|
187
|
-
cde.sendMachineEvent({ type: "RENAME_FOLDER", folderId: "folder-str", name: "STR" });
|
|
188
|
-
cde.sendMachineEvent({ type: "ARCHIVE_FOLDER", folderId: "folder-str" });
|
|
189
|
-
|
|
190
|
-
if (cde.machineState.kind === "editingFolders") {
|
|
191
|
-
const changes = cde.machineState.pendingChanges;
|
|
192
|
-
log(` before reset: ${changes.filter(c => c.kind === "rename" || c.kind === "archive").length} change(s) for folder-str`);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// RESET_FOLDER removes all staged changes for folder-str in one event.
|
|
196
|
-
cde.sendMachineEvent({ type: "RESET_FOLDER", folderId: "folder-str" });
|
|
197
|
-
|
|
198
|
-
if (cde.machineState.kind === "editingFolders") {
|
|
199
|
-
const changes = cde.machineState.pendingChanges;
|
|
200
|
-
log(` after reset: ${changes.filter(c => c.kind === "rename" || c.kind === "archive").length} change(s) for folder-str`);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
cde.sendMachineEvent({ type: "CANCEL_FOLDER_CHANGES" });
|
|
204
|
-
|
|
205
|
-
/* MD
|
|
206
|
-
### 💾 Committing folder changes
|
|
207
|
-
Saving applies all staged changes to the backend in the correct order: archives
|
|
208
|
-
first, then renames on folders that were not archived, then creates in parent-before-
|
|
209
|
-
child order so that nested new folders resolve to real IDs. After everything is
|
|
210
|
-
persisted, the folder list reloads from the backend and a single completion event
|
|
211
|
-
reports how many folders were created, renamed, and archived.
|
|
212
|
-
*/
|
|
213
|
-
|
|
214
|
-
log("\n--- commit ---");
|
|
215
|
-
cde.sendMachineEvent({ type: "START_EDIT_FOLDERS" });
|
|
216
|
-
|
|
217
|
-
cde.sendMachineEvent({ type: "ADD_FOLDER", tempId: "temp_facades", name: "Facades", parentId: null });
|
|
218
|
-
cde.sendMachineEvent({ type: "RENAME_FOLDER", folderId: "folder-str", name: "STR" });
|
|
219
|
-
cde.sendMachineEvent({ type: "ARCHIVE_FOLDER", folderId: "folder-mep" });
|
|
220
|
-
|
|
221
|
-
if (cde.machineState.kind === "editingFolders") {
|
|
222
|
-
log(` pending before save: ${cde.machineState.pendingChanges.length} change(s)`);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
await new Promise<void>(resolve => {
|
|
226
|
-
cde.onFolderChangesCompleted.add(() => resolve());
|
|
227
|
-
cde.sendMachineEvent({ type: "SAVE_FOLDER_CHANGES" });
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
log(` state after commit: ${cde.machineState.kind}`);
|
|
1
|
+
/* MD
|
|
2
|
+
## Folder management — create, rename, archive, and commit
|
|
3
|
+
---
|
|
4
|
+
A project reaches the construction phase and the coordination team needs to
|
|
5
|
+
restructure the folder hierarchy: add a "Facades" discipline folder, rename
|
|
6
|
+
"Structural" to its official abbreviation, and archive two superseded folders that
|
|
7
|
+
have been merged. Doing these one at a time through separate API calls means the
|
|
8
|
+
folder tree flickers between invalid states as each change lands — a freshly created
|
|
9
|
+
folder appears before its sibling is renamed, and the archived folders briefly
|
|
10
|
+
disappear before the new one is visible.
|
|
11
|
+
|
|
12
|
+
CDEManager stages all folder changes in memory first. Adds, renames, and archives
|
|
13
|
+
accumulate in a `pendingChanges` list before anything is sent to the backend. The
|
|
14
|
+
full diff is readable from the machine state at any point, so the UI can preview
|
|
15
|
+
exactly what the folder tree will look like after the commit. Cancelling throws away
|
|
16
|
+
all staged changes in one step. Saving applies everything in the right dependency
|
|
17
|
+
order and fires a single completion event with the counts.
|
|
18
|
+
|
|
19
|
+
This tutorial covers entering folder edit mode; staging new folders with a temporary
|
|
20
|
+
ID that gets resolved to a real ID on save; staging renames and observing how
|
|
21
|
+
renaming back to the original value automatically removes the change; the
|
|
22
|
+
archive-folder toggle behavior, where sending the event a second time unstages the
|
|
23
|
+
archive; removing a staged temp folder and all its staged children in one event;
|
|
24
|
+
reverting all staged changes for a single folder; and committing the full batch.
|
|
25
|
+
|
|
26
|
+
By the end, you'll have a working demonstration of every folder-management state
|
|
27
|
+
machine flow — stage, inspect, revert, and commit — against mock data with no
|
|
28
|
+
backend connection needed for the staging sections.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import * as OBC from "@thatopen/components";
|
|
32
|
+
// In production platform apps, CDEManager is loaded through the platform client:
|
|
33
|
+
// const cde = await client.initBuiltInComponent(CDEManager.uuid, components);
|
|
34
|
+
// Here we import directly from source for local testing.
|
|
35
|
+
import { CDEManager } from "../../index";
|
|
36
|
+
|
|
37
|
+
/* MD
|
|
38
|
+
### 🛠️ Setting up
|
|
39
|
+
The save path requires a live client context — `_saveFolderChanges` returns early if
|
|
40
|
+
no context is set and the machine would stay stuck in `savingFolderChanges` forever.
|
|
41
|
+
We keep the mock context active throughout the entire example so the commit section
|
|
42
|
+
works correctly.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
const components = new OBC.Components();
|
|
46
|
+
const cde = components.get(CDEManager);
|
|
47
|
+
|
|
48
|
+
const output = document.getElementById("output") as HTMLPreElement;
|
|
49
|
+
const log = (msg: string) => { console.log(msg); output.textContent += msg + "\n"; };
|
|
50
|
+
|
|
51
|
+
let folderIdx = 0;
|
|
52
|
+
const mockClient = {
|
|
53
|
+
checkPermissionBatch: async () => Array(5).fill({ hasPermission: true }),
|
|
54
|
+
getProjectData: async () => ({ project: { title: "" }, users: [] }),
|
|
55
|
+
listFolders: async () => [],
|
|
56
|
+
archiveFolder: async (_id: string) => ({}),
|
|
57
|
+
updateFolder: async (_id: string, _updates: any) => ({}),
|
|
58
|
+
createFolder: async (name: string, parentId?: string) => ({
|
|
59
|
+
_id: `created-${folderIdx++}`, name, parentId: parentId ?? null,
|
|
60
|
+
}),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
cde.ctx = { client: mockClient as any, projectId: "mock" };
|
|
64
|
+
await cde.loadFolders();
|
|
65
|
+
|
|
66
|
+
const folders = [
|
|
67
|
+
{ _id: "folder-str", name: "Structural", parentId: null },
|
|
68
|
+
{ _id: "folder-arch", name: "Architecture", parentId: null },
|
|
69
|
+
{ _id: "folder-mep", name: "MEP", parentId: "folder-arch" },
|
|
70
|
+
];
|
|
71
|
+
cde.folders = folders as any;
|
|
72
|
+
|
|
73
|
+
cde.onMachineStateChanged.add((s) => log(` → ${s.kind}`));
|
|
74
|
+
cde.onFolderChangesCompleted.add(({ created, renamed, archived }) =>
|
|
75
|
+
log(` [onFolderChangesCompleted] created: ${created}, renamed: ${renamed}, archived: ${archived}`)
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
/* MD
|
|
79
|
+
### 📁 Staging folder changes
|
|
80
|
+
New folders are added with a temporary ID assigned by the caller — the UI can use it
|
|
81
|
+
to identify the row in the pending list before the real ID arrives from the backend.
|
|
82
|
+
Renames and archives for existing folders are keyed by their real ID. All three
|
|
83
|
+
change kinds accumulate in the same `pendingChanges` array, so the UI can render a
|
|
84
|
+
single diff view for the whole operation.
|
|
85
|
+
|
|
86
|
+
Cancelling discards everything and returns to idle. Unlike file bulk editing, folder
|
|
87
|
+
editing has no concept of "return to previous selection" — it always returns to idle.
|
|
88
|
+
*/
|
|
89
|
+
|
|
90
|
+
log("\n--- stage changes and cancel ---");
|
|
91
|
+
cde.sendMachineEvent({ type: "START_EDIT_FOLDERS" });
|
|
92
|
+
|
|
93
|
+
// New root-level folder.
|
|
94
|
+
cde.sendMachineEvent({ type: "ADD_FOLDER", tempId: "temp_facades", name: "Facades", parentId: null });
|
|
95
|
+
// New folder nested inside the temp folder above.
|
|
96
|
+
cde.sendMachineEvent({ type: "ADD_FOLDER", tempId: "temp_facades_sub", name: "Glazing", parentId: "temp_facades" });
|
|
97
|
+
|
|
98
|
+
// Rename an existing folder.
|
|
99
|
+
cde.sendMachineEvent({ type: "RENAME_FOLDER", folderId: "folder-str", name: "STR" });
|
|
100
|
+
|
|
101
|
+
// Archive an existing folder.
|
|
102
|
+
cde.sendMachineEvent({ type: "ARCHIVE_FOLDER", folderId: "folder-mep" });
|
|
103
|
+
|
|
104
|
+
if (cde.machineState.kind === "editingFolders") {
|
|
105
|
+
const changes = cde.machineState.pendingChanges;
|
|
106
|
+
const creates = changes.filter(c => c.kind === "create").length;
|
|
107
|
+
const renames = changes.filter(c => c.kind === "rename").length;
|
|
108
|
+
const archives = changes.filter(c => c.kind === "archive").length;
|
|
109
|
+
log(` pending: ${creates} create(s), ${renames} rename(s), ${archives} archive(s)`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
cde.sendMachineEvent({ type: "CANCEL_FOLDER_CHANGES" });
|
|
113
|
+
log(` state after cancel: ${cde.machineState.kind}`);
|
|
114
|
+
log(` folders unchanged: ${cde.folders.length}`);
|
|
115
|
+
|
|
116
|
+
/* MD
|
|
117
|
+
### 🔍 Smart rename diff and archive toggle
|
|
118
|
+
Renaming an existing folder back to its current name automatically removes the
|
|
119
|
+
rename change from the pending list — the same smart diffing used for file metadata.
|
|
120
|
+
This keeps the pending list clean so the commit only sends real changes to the
|
|
121
|
+
backend.
|
|
122
|
+
|
|
123
|
+
Archiving the same existing folder twice toggles the staged archive off. This is
|
|
124
|
+
different from the file-editing archive, which needs an explicit unarchive event to
|
|
125
|
+
remove the mark. For folders, sending the archive event again is the undo gesture.
|
|
126
|
+
*/
|
|
127
|
+
|
|
128
|
+
log("\n--- smart rename diff ---");
|
|
129
|
+
cde.sendMachineEvent({ type: "START_EDIT_FOLDERS" });
|
|
130
|
+
|
|
131
|
+
cde.sendMachineEvent({ type: "RENAME_FOLDER", folderId: "folder-str", name: "STR" });
|
|
132
|
+
if (cde.machineState.kind === "editingFolders") {
|
|
133
|
+
log(` after rename "STR": ${cde.machineState.pendingChanges.filter(c => c.kind === "rename").length} rename(s)`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Renaming back to the original name removes the change automatically.
|
|
137
|
+
cde.sendMachineEvent({ type: "RENAME_FOLDER", folderId: "folder-str", name: "Structural" });
|
|
138
|
+
if (cde.machineState.kind === "editingFolders") {
|
|
139
|
+
log(` after revert to "Structural": ${cde.machineState.pendingChanges.filter(c => c.kind === "rename").length} rename(s)`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
log("\n--- archive toggle ---");
|
|
143
|
+
cde.sendMachineEvent({ type: "ARCHIVE_FOLDER", folderId: "folder-arch" });
|
|
144
|
+
if (cde.machineState.kind === "editingFolders") {
|
|
145
|
+
log(` after first ARCHIVE_FOLDER: ${cde.machineState.pendingChanges.filter(c => c.kind === "archive").length} archive(s)`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
cde.sendMachineEvent({ type: "ARCHIVE_FOLDER", folderId: "folder-arch" });
|
|
149
|
+
if (cde.machineState.kind === "editingFolders") {
|
|
150
|
+
log(` after second ARCHIVE_FOLDER (toggle): ${cde.machineState.pendingChanges.filter(c => c.kind === "archive").length} archive(s)`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
cde.sendMachineEvent({ type: "CANCEL_FOLDER_CHANGES" });
|
|
154
|
+
|
|
155
|
+
/* MD
|
|
156
|
+
### 🗑️ Removing temp folders and reverting existing ones
|
|
157
|
+
When a staged new folder is archived or reset, it is removed from the pending list
|
|
158
|
+
entirely — there is nothing to archive on the backend yet because it was never
|
|
159
|
+
created. Any folders staged as children of the removed folder are removed along with
|
|
160
|
+
it, so the pending list never contains orphaned children.
|
|
161
|
+
|
|
162
|
+
For existing folders, a reset event clears all staged changes for that folder at
|
|
163
|
+
once — both a pending rename and a pending archive in the same step — without
|
|
164
|
+
affecting any other staged changes.
|
|
165
|
+
*/
|
|
166
|
+
|
|
167
|
+
log("\n--- remove temp folder cascade ---");
|
|
168
|
+
cde.sendMachineEvent({ type: "START_EDIT_FOLDERS" });
|
|
169
|
+
|
|
170
|
+
// Stage a temp parent with two children.
|
|
171
|
+
cde.sendMachineEvent({ type: "ADD_FOLDER", tempId: "temp_p", name: "Parent", parentId: null });
|
|
172
|
+
cde.sendMachineEvent({ type: "ADD_FOLDER", tempId: "temp_c1", name: "Child A", parentId: "temp_p" });
|
|
173
|
+
cde.sendMachineEvent({ type: "ADD_FOLDER", tempId: "temp_c2", name: "Child B", parentId: "temp_p" });
|
|
174
|
+
|
|
175
|
+
if (cde.machineState.kind === "editingFolders") {
|
|
176
|
+
log(` staged before archive: ${cde.machineState.pendingChanges.length} change(s)`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Archiving a temp folder removes it AND all its staged children.
|
|
180
|
+
cde.sendMachineEvent({ type: "ARCHIVE_FOLDER", folderId: "temp_p" });
|
|
181
|
+
|
|
182
|
+
if (cde.machineState.kind === "editingFolders") {
|
|
183
|
+
log(` staged after archive temp parent: ${cde.machineState.pendingChanges.length} change(s)`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
log("\n--- reset existing folder ---");
|
|
187
|
+
cde.sendMachineEvent({ type: "RENAME_FOLDER", folderId: "folder-str", name: "STR" });
|
|
188
|
+
cde.sendMachineEvent({ type: "ARCHIVE_FOLDER", folderId: "folder-str" });
|
|
189
|
+
|
|
190
|
+
if (cde.machineState.kind === "editingFolders") {
|
|
191
|
+
const changes = cde.machineState.pendingChanges;
|
|
192
|
+
log(` before reset: ${changes.filter(c => c.kind === "rename" || c.kind === "archive").length} change(s) for folder-str`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// RESET_FOLDER removes all staged changes for folder-str in one event.
|
|
196
|
+
cde.sendMachineEvent({ type: "RESET_FOLDER", folderId: "folder-str" });
|
|
197
|
+
|
|
198
|
+
if (cde.machineState.kind === "editingFolders") {
|
|
199
|
+
const changes = cde.machineState.pendingChanges;
|
|
200
|
+
log(` after reset: ${changes.filter(c => c.kind === "rename" || c.kind === "archive").length} change(s) for folder-str`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
cde.sendMachineEvent({ type: "CANCEL_FOLDER_CHANGES" });
|
|
204
|
+
|
|
205
|
+
/* MD
|
|
206
|
+
### 💾 Committing folder changes
|
|
207
|
+
Saving applies all staged changes to the backend in the correct order: archives
|
|
208
|
+
first, then renames on folders that were not archived, then creates in parent-before-
|
|
209
|
+
child order so that nested new folders resolve to real IDs. After everything is
|
|
210
|
+
persisted, the folder list reloads from the backend and a single completion event
|
|
211
|
+
reports how many folders were created, renamed, and archived.
|
|
212
|
+
*/
|
|
213
|
+
|
|
214
|
+
log("\n--- commit ---");
|
|
215
|
+
cde.sendMachineEvent({ type: "START_EDIT_FOLDERS" });
|
|
216
|
+
|
|
217
|
+
cde.sendMachineEvent({ type: "ADD_FOLDER", tempId: "temp_facades", name: "Facades", parentId: null });
|
|
218
|
+
cde.sendMachineEvent({ type: "RENAME_FOLDER", folderId: "folder-str", name: "STR" });
|
|
219
|
+
cde.sendMachineEvent({ type: "ARCHIVE_FOLDER", folderId: "folder-mep" });
|
|
220
|
+
|
|
221
|
+
if (cde.machineState.kind === "editingFolders") {
|
|
222
|
+
log(` pending before save: ${cde.machineState.pendingChanges.length} change(s)`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
await new Promise<void>(resolve => {
|
|
226
|
+
cde.onFolderChangesCompleted.add(() => resolve());
|
|
227
|
+
cde.sendMachineEvent({ type: "SAVE_FOLDER_CHANGES" });
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
log(` state after commit: ${cde.machineState.kind}`);
|