@thatopen/services 0.6.0 → 0.8.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.
Files changed (73) hide show
  1. package/README.md +62 -62
  2. package/dist/built-in/index.d.ts +485 -100
  3. package/dist/cli/lib/revit-addin.d.ts +8 -0
  4. package/dist/cli.js +135 -24
  5. package/dist/index.cjs.js +2 -2
  6. package/dist/index.es.js +41 -50
  7. package/dist/vite-env.d.ts +1 -1
  8. package/docs/ai-quickstart.md +138 -4
  9. package/docs/cli/create.md +29 -29
  10. package/docs/cli/local-server.md +38 -38
  11. package/docs/cli/login.md +32 -32
  12. package/docs/cli/publish.md +31 -31
  13. package/docs/cli/run.md +26 -26
  14. package/docs/cli/serve.md +43 -43
  15. package/docs/cli/swap.md +30 -30
  16. package/docs/flow-architecture.md +1 -1
  17. package/docs/flow-plugin-guide.md +27 -1
  18. package/docs/platform-token.md +81 -0
  19. package/docs/revit-collab-quickstart.md +115 -35
  20. package/docs/rhino-interop-quickstart.md +458 -0
  21. package/package.json +1 -1
  22. package/resources/AGENTS.md +1 -1
  23. package/src/built-in/CDEManager/example.ts +214 -214
  24. package/src/built-in/CDEManager/examples/FileEditing/example.ts +226 -226
  25. package/src/built-in/CDEManager/examples/FileMoves/example.ts +186 -186
  26. package/src/built-in/CDEManager/examples/FileOperations/example.ts +295 -295
  27. package/src/built-in/CDEManager/examples/FileTransfers/example.ts +210 -210
  28. package/src/built-in/CDEManager/examples/FolderManagement/example.ts +230 -230
  29. package/src/built-in/CDEManager/examples/MetadataSchema/example.ts +230 -230
  30. package/src/built-in/CDEManager/examples/MetadataViews/example.ts +166 -166
  31. package/src/built-in/CDEManager/examples/ProjectConfig/example.ts +177 -177
  32. package/src/built-in/UIManager/src/app/example.ts +97 -97
  33. package/src/built-in/UIManager/src/app/examples/AppState/example.ts +140 -140
  34. package/src/built-in/UIManager/src/app/examples/Banner/example.ts +159 -159
  35. package/src/built-in/UIManager/src/app/examples/Contexts/example.ts +123 -123
  36. package/src/built-in/UIManager/src/app/examples/FileDrop/example.ts +124 -124
  37. package/src/built-in/UIManager/src/app/examples/FloatingLayout/example.ts +90 -90
  38. package/src/built-in/UIManager/src/app/examples/Modal/example.ts +163 -163
  39. package/src/built-in/UIManager/src/app/examples/Notifications/example.ts +133 -133
  40. package/src/built-in/UIManager/src/app/examples/ProjectFileStorage/example.ts +150 -150
  41. package/src/built-in/UIManager/src/app/examples/Sidebar/example.ts +102 -102
  42. package/src/built-in/UIManager/src/clashes-panel/example.ts +183 -183
  43. package/src/built-in/UIManager/src/git-history-panel/example.ts +70 -70
  44. package/src/built-in/UIManager/src/models-list/example.ts +122 -122
  45. package/src/built-in/UIManager/src/topics-panel/example.ts +170 -170
  46. package/src/built-in/index.ts +485 -100
  47. package/src/cli/templates/app/index.html +35 -35
  48. package/src/cli/templates/app/package.json +32 -32
  49. package/src/cli/templates/app/src/bim-components/CloudRunner/index.ts +95 -95
  50. package/src/cli/templates/app/src/bim-components/CloudRunner/src/index.ts +1 -1
  51. package/src/cli/templates/app/src/bim-components/CloudRunner/src/types.ts +5 -5
  52. package/src/cli/templates/app/src/bim-components/index.ts +1 -1
  53. package/src/cli/templates/app/src/globals.ts +1 -1
  54. package/src/cli/templates/app/src/main.history.ts +191 -0
  55. package/src/cli/templates/app/src/ui-components/app-info-section/index.ts +26 -26
  56. package/src/cli/templates/app/src/ui-components/app-info-section/src/index.ts +1 -1
  57. package/src/cli/templates/app/src/ui-components/app-info-section/src/types.ts +15 -15
  58. package/src/cli/templates/app/src/ui-components/cloud-runner-section/index.ts +37 -37
  59. package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/index.ts +1 -1
  60. package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/types.ts +14 -14
  61. package/src/cli/templates/app/src/ui-components/index.ts +2 -2
  62. package/src/cli/templates/app/tsconfig.json +16 -16
  63. package/src/cli/templates/app/vite.config.js +57 -57
  64. package/src/cli/templates/cloud-component/README.md +205 -205
  65. package/src/cli/templates/cloud-component/_thatopen +5 -5
  66. package/src/cli/templates/cloud-component/declarations.json +4 -4
  67. package/src/cli/templates/cloud-component/package.json +21 -21
  68. package/src/cli/templates/cloud-component/src/main.ts +70 -70
  69. package/src/cli/templates/cloud-component/tsconfig.json +16 -16
  70. package/src/cli/templates/cloud-component/vite.config.js +46 -46
  71. package/src/cli/templates/shared/AGENTS.md +8 -8
  72. package/src/cli/templates/shared/CLAUDE.md +1 -1
  73. 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}`);