@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.
Files changed (76) hide show
  1. package/README.md +62 -62
  2. package/dist/cli/commands/rhino.d.ts +11 -0
  3. package/dist/cli/lib/plugin-install.d.ts +24 -0
  4. package/dist/cli.js +124 -1
  5. package/dist/core/client.d.ts +1 -0
  6. package/dist/core/platform-client.d.ts +7 -0
  7. package/dist/index.cjs.js +2 -2
  8. package/dist/index.es.js +382 -370
  9. package/dist/vite-env.d.ts +1 -1
  10. package/docs/ai-quickstart.md +130 -130
  11. package/docs/cli/create.md +29 -29
  12. package/docs/cli/local-server.md +38 -38
  13. package/docs/cli/login.md +32 -32
  14. package/docs/cli/publish.md +31 -31
  15. package/docs/cli/run.md +26 -26
  16. package/docs/cli/serve.md +43 -43
  17. package/docs/cli/swap.md +30 -30
  18. package/docs/cloud/GaussianSplatConverter.md +43 -43
  19. package/docs/cloud/PointCloudConverter.md +53 -53
  20. package/docs/revit-collab-quickstart.md +256 -256
  21. package/package.json +95 -95
  22. package/resources/AGENTS.md +40 -40
  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/ClashesManager/example.ts +283 -283
  33. package/src/built-in/UIManager/src/app/example.ts +97 -97
  34. package/src/built-in/UIManager/src/app/examples/AppState/example.ts +140 -140
  35. package/src/built-in/UIManager/src/app/examples/Banner/example.ts +159 -159
  36. package/src/built-in/UIManager/src/app/examples/Contexts/example.ts +123 -123
  37. package/src/built-in/UIManager/src/app/examples/FileDrop/example.ts +124 -124
  38. package/src/built-in/UIManager/src/app/examples/FloatingLayout/example.ts +90 -90
  39. package/src/built-in/UIManager/src/app/examples/Modal/example.ts +163 -163
  40. package/src/built-in/UIManager/src/app/examples/Notifications/example.ts +133 -133
  41. package/src/built-in/UIManager/src/app/examples/Sidebar/example.ts +102 -102
  42. package/src/cli/templates/app/README.md +217 -217
  43. package/src/cli/templates/app/index.html +35 -35
  44. package/src/cli/templates/app/package.json +32 -32
  45. package/src/cli/templates/app/src/app.ts +37 -37
  46. package/src/cli/templates/app/src/assets/tool-placeholder.ts +50 -50
  47. package/src/cli/templates/app/src/bim-components/CloudRunner/index.ts +95 -95
  48. package/src/cli/templates/app/src/bim-components/CloudRunner/src/index.ts +1 -1
  49. package/src/cli/templates/app/src/bim-components/CloudRunner/src/types.ts +5 -5
  50. package/src/cli/templates/app/src/bim-components/index.ts +1 -1
  51. package/src/cli/templates/app/src/globals.ts +1 -1
  52. package/src/cli/templates/app/src/main.ts +199 -199
  53. package/src/cli/templates/app/src/setups/reality-capture/lib/hidden-tiles-plugin.ts +88 -88
  54. package/src/cli/templates/app/src/setups/reality-capture/lib/point-tile-plugin.ts +166 -166
  55. package/src/cli/templates/app/src/setups/reality-capture/lib/render-frame.ts +31 -31
  56. package/src/cli/templates/app/src/setups/reality-capture/lib/splat-tile-plugin.ts +51 -51
  57. package/src/cli/templates/app/src/setups/reality-capture-viewer.ts +1383 -1383
  58. package/src/cli/templates/app/src/ui-components/app-info-section/index.ts +26 -26
  59. package/src/cli/templates/app/src/ui-components/app-info-section/src/index.ts +1 -1
  60. package/src/cli/templates/app/src/ui-components/app-info-section/src/types.ts +15 -15
  61. package/src/cli/templates/app/src/ui-components/cloud-runner-section/index.ts +37 -37
  62. package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/index.ts +1 -1
  63. package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/types.ts +14 -14
  64. package/src/cli/templates/app/src/ui-components/index.ts +2 -2
  65. package/src/cli/templates/app/tsconfig.json +16 -16
  66. package/src/cli/templates/app/vite.config.js +57 -57
  67. package/src/cli/templates/cloud-component/README.md +205 -205
  68. package/src/cli/templates/cloud-component/_thatopen +5 -5
  69. package/src/cli/templates/cloud-component/declarations.json +4 -4
  70. package/src/cli/templates/cloud-component/package.json +21 -21
  71. package/src/cli/templates/cloud-component/src/main.ts +70 -70
  72. package/src/cli/templates/cloud-component/tsconfig.json +16 -16
  73. package/src/cli/templates/cloud-component/vite.config.js +46 -46
  74. package/src/cli/templates/shared/AGENTS.md +8 -8
  75. package/src/cli/templates/shared/CLAUDE.md +1 -1
  76. package/src/cli/templates/shared/_gitignore +5 -5
@@ -1,226 +1,226 @@
1
- /* MD
2
- ## Bulk file editing — stage, preview, revert, and commit
3
- ---
4
- A project manager preparing a deliverable handoff needs to update discipline tags
5
- across fifteen IFC files after a scope change, rename each to match the new naming
6
- convention, and archive three superseded models — all before anything is sent to the
7
- backend. Doing the changes one file at a time risks leaving the file list in a
8
- half-updated state if the session is interrupted, and there is no way to review
9
- exactly what will change before committing.
10
-
11
- CDEManager provides a staging mode where metadata edits, renames, and archive marks
12
- can be accumulated across any number of files before anything is written. The full
13
- diff is visible in the machine state at any point, so the UI can show a per-row
14
- preview of what is about to happen and let the user change their mind before
15
- confirming.
16
-
17
- This tutorial covers entering bulk edit mode from both the idle state and an active
18
- selection; staging metadata edits and renames for multiple files; observing how
19
- staging the original value back automatically clears a staged edit; reverting all
20
- staged changes for one file in a single step; staging and unstaging a file for
21
- archive; committing all staged changes and reading the result; and confirming that
22
- the state returns to the correct previous position after the commit.
23
-
24
- By the end, you'll have a working demonstration of the full bulk edit cycle — stage,
25
- preview, revert, commit, and cancel — with all transitions driven through the state
26
- machine.
27
- */
28
-
29
- import * as OBC from "@thatopen/components";
30
- // In production platform apps, CDEManager is loaded through the platform client:
31
- // const cde = await client.initBuiltInComponent(CDEManager.uuid, components);
32
- // Here we import directly from source for local testing.
33
- import { CDEManager } from "../../index";
34
- import type { CDEFile } from "../../src/types";
35
-
36
- /* MD
37
- ### 🛠️ Setting up
38
- All flows in this example run against mock data without a backend connection. Entering
39
- bulk edit mode is permission-gated, so we simulate the platform startup step that
40
- loads user permissions before running any demos.
41
- */
42
-
43
- const components = new OBC.Components();
44
- const cde = components.get(CDEManager);
45
-
46
- const output = document.getElementById("output") as HTMLPreElement;
47
- const log = (msg: string) => { console.log(msg); output.textContent += msg + "\n"; };
48
-
49
- // Simulate the platform startup step that loads permissions.
50
- cde.ctx = {
51
- client: {
52
- checkPermissionBatch: async () => Array(5).fill({ hasPermission: true }),
53
- getProjectData: async () => ({ project: { title: "" }, users: [] }),
54
- listFolders: async () => [],
55
- } as any,
56
- projectId: "mock",
57
- };
58
- await cde.loadFolders();
59
- // Clear the context — mock flows don't make API calls.
60
- cde.ctx = undefined;
61
-
62
- const now = new Date();
63
- const mockFiles: CDEFile[] = [
64
- {
65
- _id: "f1", name: "column.ifc", folderId: null,
66
- createdBy: "alice", createdAt: now,
67
- metadata: { discipline: "STR", phase: "DD" },
68
- } as CDEFile,
69
- {
70
- _id: "f2", name: "foundations.ifc", folderId: null,
71
- createdBy: "bob", createdAt: now,
72
- metadata: { discipline: "STR", phase: "CD" },
73
- } as CDEFile,
74
- {
75
- _id: "f3", name: "report.pdf", folderId: null,
76
- createdBy: "alice", createdAt: now,
77
- metadata: {},
78
- } as CDEFile,
79
- ];
80
- cde.files = mockFiles;
81
-
82
- cde.onMachineStateChanged.add((s) => log(` → ${s.kind}`));
83
- cde.onFilesChanged.add((files) => log(` [onFilesChanged] ${files.length} file(s)`));
84
-
85
- /* MD
86
- ### 📋 Staging edits across multiple files
87
- In bulk edit mode, every change accumulates in the machine state rather than being
88
- applied right away. The state carries a per-file record of staged metadata edits and
89
- renames, so the UI can display a diff table without any extra bookkeeping — it just
90
- reads the machine state.
91
-
92
- Cancelling discards all staged changes and returns to wherever the session started,
93
- leaving every file exactly as it was.
94
- */
95
-
96
- log("\n--- stage edits and cancel ---");
97
- cde.sendMachineEvent({ type: "START_EDIT_FILES" });
98
-
99
- cde.sendMachineEvent({ type: "UPDATE_FILE_METADATA", fileId: "f1", key: "discipline", value: "MEP" });
100
- cde.sendMachineEvent({ type: "UPDATE_FILE_METADATA", fileId: "f2", key: "phase", value: "SD" });
101
- cde.sendMachineEvent({ type: "RENAME_FILE", fileId: "f1", name: "column-revised.ifc" });
102
-
103
- if (cde.machineState.kind === "editingFiles") {
104
- const { pendingEdits, pendingRenames } = cde.machineState;
105
- log(` staged edits: ${Object.keys(pendingEdits).length} file(s)`);
106
- log(` staged renames: ${Object.keys(pendingRenames).length} file(s)`);
107
- }
108
-
109
- cde.sendMachineEvent({ type: "CANCEL_EDIT_FILES" });
110
- log(` state after cancel: ${cde.machineState.kind}`);
111
- // Files are untouched — nothing was committed.
112
- log(` f1 discipline: ${cde.files.find(f => f._id === "f1")!.metadata.discipline}`);
113
-
114
- /* MD
115
- ### 🔍 Smart diff: staging the original value clears the edit
116
- The staging system tracks whether a value actually differs from the file's current
117
- state. If a user edits a field and then edits it back to its original value, the
118
- edit disappears from the staged list on its own — the state machine does the diffing
119
- so the UI doesn't need to.
120
- */
121
-
122
- log("\n--- smart diff ---");
123
- cde.sendMachineEvent({ type: "START_EDIT_FILES" });
124
-
125
- cde.sendMachineEvent({ type: "UPDATE_FILE_METADATA", fileId: "f1", key: "discipline", value: "MEP" });
126
- if (cde.machineState.kind === "editingFiles") {
127
- log(` f1 staged: ${Object.keys(cde.machineState.pendingEdits).includes("f1")}`);
128
- }
129
-
130
- // Staging the original value back auto-removes f1 from pendingEdits.
131
- cde.sendMachineEvent({ type: "UPDATE_FILE_METADATA", fileId: "f1", key: "discipline", value: "STR" });
132
- if (cde.machineState.kind === "editingFiles") {
133
- log(` f1 staged after revert: ${Object.keys(cde.machineState.pendingEdits).includes("f1")}`);
134
- }
135
-
136
- cde.sendMachineEvent({ type: "CANCEL_EDIT_FILES" });
137
-
138
- /* MD
139
- ### ↩️ Reverting all staged changes for one file
140
- When a user wants to undo everything they staged for a particular file — not just
141
- one field — a single revert event wipes all of its staged edits, renames, and archive
142
- marks at once. Files with no staged changes are unaffected.
143
- */
144
-
145
- log("\n--- revert one file ---");
146
- cde.sendMachineEvent({ type: "START_EDIT_FILES" });
147
-
148
- cde.sendMachineEvent({ type: "UPDATE_FILE_METADATA", fileId: "f1", key: "discipline", value: "MEP" });
149
- cde.sendMachineEvent({ type: "RENAME_FILE", fileId: "f1", name: "column-revised.ifc" });
150
- cde.sendMachineEvent({ type: "ARCHIVE_FILE_EDIT", fileId: "f1" });
151
-
152
- if (cde.machineState.kind === "editingFiles") {
153
- const { pendingEdits, pendingRenames, pendingArchives } = cde.machineState;
154
- log(` before revert — edits: ${Object.keys(pendingEdits).length}, renames: ${Object.keys(pendingRenames).length}, archives: ${pendingArchives.length}`);
155
- }
156
-
157
- // REVERT_FILE_METADATA clears edits, renames, and archive mark for f1 in one event.
158
- cde.sendMachineEvent({ type: "REVERT_FILE_METADATA", fileId: "f1" });
159
-
160
- if (cde.machineState.kind === "editingFiles") {
161
- const { pendingEdits, pendingRenames, pendingArchives } = cde.machineState;
162
- log(` after revert — edits: ${Object.keys(pendingEdits).length}, renames: ${Object.keys(pendingRenames).length}, archives: ${pendingArchives.length}`);
163
- }
164
-
165
- cde.sendMachineEvent({ type: "CANCEL_EDIT_FILES" });
166
-
167
- /* MD
168
- ### 🗑️ Staging archives and committing
169
- Marking a file for archive in bulk edit mode stages the deletion — the file stays
170
- visible in the table, just flagged, so the user can see it in the preview and change
171
- their mind before committing. Unstaging removes the mark and the file is back to
172
- normal. Files staged for archive are excluded from metadata and rename operations
173
- when the commit runs, so staging both a rename and an archive on the same file never
174
- creates a conflict.
175
- */
176
-
177
- log("\n--- archive staging + commit ---");
178
- cde.sendMachineEvent({ type: "START_EDIT_FILES" });
179
-
180
- cde.sendMachineEvent({ type: "ARCHIVE_FILE_EDIT", fileId: "f3" });
181
- if (cde.machineState.kind === "editingFiles") {
182
- log(` pending archives: ${cde.machineState.pendingArchives.length}`);
183
- }
184
-
185
- // Unstaging gives the user a chance to reconsider before the commit.
186
- cde.sendMachineEvent({ type: "UNARCHIVE_FILE_EDIT", fileId: "f3" });
187
- if (cde.machineState.kind === "editingFiles") {
188
- log(` after unarchive: ${cde.machineState.pendingArchives.length}`);
189
- }
190
-
191
- // Stage f3 for archive again, and also rename f1.
192
- cde.sendMachineEvent({ type: "ARCHIVE_FILE_EDIT", fileId: "f3" });
193
- cde.sendMachineEvent({ type: "RENAME_FILE", fileId: "f1", name: "column-renamed.ifc" });
194
-
195
- log(` files before commit: ${cde.files.length}`);
196
- cde.sendMachineEvent({ type: "SAVE_FILE_EDITS" });
197
-
198
- // Without a live client, changes are applied locally and synchronously.
199
- log(` files after commit: ${cde.files.length}`);
200
- log(` f1 name: ${cde.files.find(f => f._id === "f1")?.name ?? "not found"}`);
201
- log(` f3 still in list: ${cde.files.some(f => f._id === "f3")}`);
202
-
203
- /* MD
204
- ### 🔁 Returning to the previous selection
205
- The state machine records where it came from when bulk edit mode is entered. Entering
206
- from an active selection returns there after committing — the same files are still
207
- highlighted in the table. Entering from idle returns to idle. Either way, the
208
- selection is never lost.
209
- */
210
-
211
- log("\n--- previousState: commit from selection ---");
212
- // Start fresh with the original two IFC files.
213
- const f1 = cde.files.find(f => f._id === "f1")!;
214
- const f2 = cde.files.find(f => f._id === "f2")!;
215
-
216
- cde.sendMachineEvent({ type: "SELECT", files: [f1, f2] });
217
- // Entering from "selected" records it as the return point.
218
- cde.sendMachineEvent({ type: "START_EDIT_FILES" });
219
-
220
- cde.sendMachineEvent({ type: "UPDATE_FILE_METADATA", fileId: "f1", key: "phase", value: "SD" });
221
- cde.sendMachineEvent({ type: "SAVE_FILE_EDITS" });
222
-
223
- log(` state after commit: ${cde.machineState.kind}`);
224
- if (cde.machineState.kind === "selected") {
225
- log(` selection preserved: ${cde.machineState.files.length} file(s)`);
226
- }
1
+ /* MD
2
+ ## Bulk file editing — stage, preview, revert, and commit
3
+ ---
4
+ A project manager preparing a deliverable handoff needs to update discipline tags
5
+ across fifteen IFC files after a scope change, rename each to match the new naming
6
+ convention, and archive three superseded models — all before anything is sent to the
7
+ backend. Doing the changes one file at a time risks leaving the file list in a
8
+ half-updated state if the session is interrupted, and there is no way to review
9
+ exactly what will change before committing.
10
+
11
+ CDEManager provides a staging mode where metadata edits, renames, and archive marks
12
+ can be accumulated across any number of files before anything is written. The full
13
+ diff is visible in the machine state at any point, so the UI can show a per-row
14
+ preview of what is about to happen and let the user change their mind before
15
+ confirming.
16
+
17
+ This tutorial covers entering bulk edit mode from both the idle state and an active
18
+ selection; staging metadata edits and renames for multiple files; observing how
19
+ staging the original value back automatically clears a staged edit; reverting all
20
+ staged changes for one file in a single step; staging and unstaging a file for
21
+ archive; committing all staged changes and reading the result; and confirming that
22
+ the state returns to the correct previous position after the commit.
23
+
24
+ By the end, you'll have a working demonstration of the full bulk edit cycle — stage,
25
+ preview, revert, commit, and cancel — with all transitions driven through the state
26
+ machine.
27
+ */
28
+
29
+ import * as OBC from "@thatopen/components";
30
+ // In production platform apps, CDEManager is loaded through the platform client:
31
+ // const cde = await client.initBuiltInComponent(CDEManager.uuid, components);
32
+ // Here we import directly from source for local testing.
33
+ import { CDEManager } from "../../index";
34
+ import type { CDEFile } from "../../src/types";
35
+
36
+ /* MD
37
+ ### 🛠️ Setting up
38
+ All flows in this example run against mock data without a backend connection. Entering
39
+ bulk edit mode is permission-gated, so we simulate the platform startup step that
40
+ loads user permissions before running any demos.
41
+ */
42
+
43
+ const components = new OBC.Components();
44
+ const cde = components.get(CDEManager);
45
+
46
+ const output = document.getElementById("output") as HTMLPreElement;
47
+ const log = (msg: string) => { console.log(msg); output.textContent += msg + "\n"; };
48
+
49
+ // Simulate the platform startup step that loads permissions.
50
+ cde.ctx = {
51
+ client: {
52
+ checkPermissionBatch: async () => Array(5).fill({ hasPermission: true }),
53
+ getProjectData: async () => ({ project: { title: "" }, users: [] }),
54
+ listFolders: async () => [],
55
+ } as any,
56
+ projectId: "mock",
57
+ };
58
+ await cde.loadFolders();
59
+ // Clear the context — mock flows don't make API calls.
60
+ cde.ctx = undefined;
61
+
62
+ const now = new Date();
63
+ const mockFiles: CDEFile[] = [
64
+ {
65
+ _id: "f1", name: "column.ifc", folderId: null,
66
+ createdBy: "alice", createdAt: now,
67
+ metadata: { discipline: "STR", phase: "DD" },
68
+ } as CDEFile,
69
+ {
70
+ _id: "f2", name: "foundations.ifc", folderId: null,
71
+ createdBy: "bob", createdAt: now,
72
+ metadata: { discipline: "STR", phase: "CD" },
73
+ } as CDEFile,
74
+ {
75
+ _id: "f3", name: "report.pdf", folderId: null,
76
+ createdBy: "alice", createdAt: now,
77
+ metadata: {},
78
+ } as CDEFile,
79
+ ];
80
+ cde.files = mockFiles;
81
+
82
+ cde.onMachineStateChanged.add((s) => log(` → ${s.kind}`));
83
+ cde.onFilesChanged.add((files) => log(` [onFilesChanged] ${files.length} file(s)`));
84
+
85
+ /* MD
86
+ ### 📋 Staging edits across multiple files
87
+ In bulk edit mode, every change accumulates in the machine state rather than being
88
+ applied right away. The state carries a per-file record of staged metadata edits and
89
+ renames, so the UI can display a diff table without any extra bookkeeping — it just
90
+ reads the machine state.
91
+
92
+ Cancelling discards all staged changes and returns to wherever the session started,
93
+ leaving every file exactly as it was.
94
+ */
95
+
96
+ log("\n--- stage edits and cancel ---");
97
+ cde.sendMachineEvent({ type: "START_EDIT_FILES" });
98
+
99
+ cde.sendMachineEvent({ type: "UPDATE_FILE_METADATA", fileId: "f1", key: "discipline", value: "MEP" });
100
+ cde.sendMachineEvent({ type: "UPDATE_FILE_METADATA", fileId: "f2", key: "phase", value: "SD" });
101
+ cde.sendMachineEvent({ type: "RENAME_FILE", fileId: "f1", name: "column-revised.ifc" });
102
+
103
+ if (cde.machineState.kind === "editingFiles") {
104
+ const { pendingEdits, pendingRenames } = cde.machineState;
105
+ log(` staged edits: ${Object.keys(pendingEdits).length} file(s)`);
106
+ log(` staged renames: ${Object.keys(pendingRenames).length} file(s)`);
107
+ }
108
+
109
+ cde.sendMachineEvent({ type: "CANCEL_EDIT_FILES" });
110
+ log(` state after cancel: ${cde.machineState.kind}`);
111
+ // Files are untouched — nothing was committed.
112
+ log(` f1 discipline: ${cde.files.find(f => f._id === "f1")!.metadata.discipline}`);
113
+
114
+ /* MD
115
+ ### 🔍 Smart diff: staging the original value clears the edit
116
+ The staging system tracks whether a value actually differs from the file's current
117
+ state. If a user edits a field and then edits it back to its original value, the
118
+ edit disappears from the staged list on its own — the state machine does the diffing
119
+ so the UI doesn't need to.
120
+ */
121
+
122
+ log("\n--- smart diff ---");
123
+ cde.sendMachineEvent({ type: "START_EDIT_FILES" });
124
+
125
+ cde.sendMachineEvent({ type: "UPDATE_FILE_METADATA", fileId: "f1", key: "discipline", value: "MEP" });
126
+ if (cde.machineState.kind === "editingFiles") {
127
+ log(` f1 staged: ${Object.keys(cde.machineState.pendingEdits).includes("f1")}`);
128
+ }
129
+
130
+ // Staging the original value back auto-removes f1 from pendingEdits.
131
+ cde.sendMachineEvent({ type: "UPDATE_FILE_METADATA", fileId: "f1", key: "discipline", value: "STR" });
132
+ if (cde.machineState.kind === "editingFiles") {
133
+ log(` f1 staged after revert: ${Object.keys(cde.machineState.pendingEdits).includes("f1")}`);
134
+ }
135
+
136
+ cde.sendMachineEvent({ type: "CANCEL_EDIT_FILES" });
137
+
138
+ /* MD
139
+ ### ↩️ Reverting all staged changes for one file
140
+ When a user wants to undo everything they staged for a particular file — not just
141
+ one field — a single revert event wipes all of its staged edits, renames, and archive
142
+ marks at once. Files with no staged changes are unaffected.
143
+ */
144
+
145
+ log("\n--- revert one file ---");
146
+ cde.sendMachineEvent({ type: "START_EDIT_FILES" });
147
+
148
+ cde.sendMachineEvent({ type: "UPDATE_FILE_METADATA", fileId: "f1", key: "discipline", value: "MEP" });
149
+ cde.sendMachineEvent({ type: "RENAME_FILE", fileId: "f1", name: "column-revised.ifc" });
150
+ cde.sendMachineEvent({ type: "ARCHIVE_FILE_EDIT", fileId: "f1" });
151
+
152
+ if (cde.machineState.kind === "editingFiles") {
153
+ const { pendingEdits, pendingRenames, pendingArchives } = cde.machineState;
154
+ log(` before revert — edits: ${Object.keys(pendingEdits).length}, renames: ${Object.keys(pendingRenames).length}, archives: ${pendingArchives.length}`);
155
+ }
156
+
157
+ // REVERT_FILE_METADATA clears edits, renames, and archive mark for f1 in one event.
158
+ cde.sendMachineEvent({ type: "REVERT_FILE_METADATA", fileId: "f1" });
159
+
160
+ if (cde.machineState.kind === "editingFiles") {
161
+ const { pendingEdits, pendingRenames, pendingArchives } = cde.machineState;
162
+ log(` after revert — edits: ${Object.keys(pendingEdits).length}, renames: ${Object.keys(pendingRenames).length}, archives: ${pendingArchives.length}`);
163
+ }
164
+
165
+ cde.sendMachineEvent({ type: "CANCEL_EDIT_FILES" });
166
+
167
+ /* MD
168
+ ### 🗑️ Staging archives and committing
169
+ Marking a file for archive in bulk edit mode stages the deletion — the file stays
170
+ visible in the table, just flagged, so the user can see it in the preview and change
171
+ their mind before committing. Unstaging removes the mark and the file is back to
172
+ normal. Files staged for archive are excluded from metadata and rename operations
173
+ when the commit runs, so staging both a rename and an archive on the same file never
174
+ creates a conflict.
175
+ */
176
+
177
+ log("\n--- archive staging + commit ---");
178
+ cde.sendMachineEvent({ type: "START_EDIT_FILES" });
179
+
180
+ cde.sendMachineEvent({ type: "ARCHIVE_FILE_EDIT", fileId: "f3" });
181
+ if (cde.machineState.kind === "editingFiles") {
182
+ log(` pending archives: ${cde.machineState.pendingArchives.length}`);
183
+ }
184
+
185
+ // Unstaging gives the user a chance to reconsider before the commit.
186
+ cde.sendMachineEvent({ type: "UNARCHIVE_FILE_EDIT", fileId: "f3" });
187
+ if (cde.machineState.kind === "editingFiles") {
188
+ log(` after unarchive: ${cde.machineState.pendingArchives.length}`);
189
+ }
190
+
191
+ // Stage f3 for archive again, and also rename f1.
192
+ cde.sendMachineEvent({ type: "ARCHIVE_FILE_EDIT", fileId: "f3" });
193
+ cde.sendMachineEvent({ type: "RENAME_FILE", fileId: "f1", name: "column-renamed.ifc" });
194
+
195
+ log(` files before commit: ${cde.files.length}`);
196
+ cde.sendMachineEvent({ type: "SAVE_FILE_EDITS" });
197
+
198
+ // Without a live client, changes are applied locally and synchronously.
199
+ log(` files after commit: ${cde.files.length}`);
200
+ log(` f1 name: ${cde.files.find(f => f._id === "f1")?.name ?? "not found"}`);
201
+ log(` f3 still in list: ${cde.files.some(f => f._id === "f3")}`);
202
+
203
+ /* MD
204
+ ### 🔁 Returning to the previous selection
205
+ The state machine records where it came from when bulk edit mode is entered. Entering
206
+ from an active selection returns there after committing — the same files are still
207
+ highlighted in the table. Entering from idle returns to idle. Either way, the
208
+ selection is never lost.
209
+ */
210
+
211
+ log("\n--- previousState: commit from selection ---");
212
+ // Start fresh with the original two IFC files.
213
+ const f1 = cde.files.find(f => f._id === "f1")!;
214
+ const f2 = cde.files.find(f => f._id === "f2")!;
215
+
216
+ cde.sendMachineEvent({ type: "SELECT", files: [f1, f2] });
217
+ // Entering from "selected" records it as the return point.
218
+ cde.sendMachineEvent({ type: "START_EDIT_FILES" });
219
+
220
+ cde.sendMachineEvent({ type: "UPDATE_FILE_METADATA", fileId: "f1", key: "phase", value: "SD" });
221
+ cde.sendMachineEvent({ type: "SAVE_FILE_EDITS" });
222
+
223
+ log(` state after commit: ${cde.machineState.kind}`);
224
+ if (cde.machineState.kind === "selected") {
225
+ log(` selection preserved: ${cde.machineState.files.length} file(s)`);
226
+ }