@thatopen/services 0.0.3 → 0.0.5

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 (140) hide show
  1. package/README.md +24 -263
  2. package/dist/cli/commands/swap.d.ts +3 -0
  3. package/dist/cli/lib/beta.d.ts +2 -0
  4. package/dist/cli/lib/config.d.ts +2 -0
  5. package/dist/cli.js +85 -126
  6. package/dist/core/examples/apps.d.ts +1 -0
  7. package/dist/core/examples/components.d.ts +1 -0
  8. package/dist/core/examples/execution.d.ts +1 -0
  9. package/dist/core/examples/file-metadata.d.ts +1 -0
  10. package/dist/core/examples/files.d.ts +1 -0
  11. package/dist/core/examples/folders.d.ts +1 -0
  12. package/dist/core/examples/hidden-files.d.ts +1 -0
  13. package/dist/core/examples/icons.d.ts +1 -0
  14. package/dist/core/examples/permissions.d.ts +1 -0
  15. package/dist/types/items.d.ts +2 -0
  16. package/docs/builtin/paths.json +54 -0
  17. package/docs/cli/create.md +28 -0
  18. package/docs/cli/local-server.md +38 -0
  19. package/docs/cli/login.md +32 -0
  20. package/docs/cli/paths.json +30 -0
  21. package/docs/cli/publish.md +31 -0
  22. package/docs/cli/run.md +26 -0
  23. package/docs/cli/serve.md +43 -0
  24. package/docs/cli/swap.md +30 -0
  25. package/docs/client/paths.json +38 -0
  26. package/package.json +8 -10
  27. package/resources/AGENTS.md +39 -0
  28. package/src/built-in/AppManager/example.ts +86 -0
  29. package/src/built-in/CDEManager/example.ts +214 -0
  30. package/src/built-in/CDEManager/examples/FileEditing/example.ts +226 -0
  31. package/src/built-in/CDEManager/examples/FileMoves/example.ts +186 -0
  32. package/src/built-in/CDEManager/examples/FileOperations/example.ts +295 -0
  33. package/src/built-in/CDEManager/examples/FileTransfers/example.ts +210 -0
  34. package/src/built-in/CDEManager/examples/FolderManagement/example.ts +230 -0
  35. package/src/built-in/CDEManager/examples/MetadataSchema/example.ts +230 -0
  36. package/src/built-in/CDEManager/examples/MetadataViews/example.ts +166 -0
  37. package/src/built-in/CDEManager/examples/ProjectConfig/example.ts +177 -0
  38. package/src/built-in/FileList/example.ts +107 -0
  39. package/src/built-in/TabbedNavigation/example.ts +115 -0
  40. package/src/built-in/ViewportsManager/example.ts +106 -0
  41. package/src/cli/templates/{shared/app → app}/vite.config.js +42 -23
  42. package/src/cli/templates/{shared/cloud → cloud-component}/vite.config.js +46 -27
  43. package/src/cli/templates/shared/AGENTS.md +8 -30
  44. package/src/cli/templates/shared/CLAUDE.md +1 -1
  45. package/AGENTS.md +0 -86
  46. package/CONTEXT.md +0 -258
  47. package/dist/built-in/index.d.ts +0 -723
  48. package/dist/cli/commands/create-tests.d.ts +0 -3
  49. package/dist/cli/commands/serve-tests.d.ts +0 -3
  50. package/docs/access-backend-data.md +0 -19
  51. package/docs/app-architecture.md +0 -94
  52. package/docs/app-layout.md +0 -139
  53. package/docs/app-wiring.md +0 -123
  54. package/docs/bim-components/coding-conventions.md +0 -128
  55. package/docs/bim-components/element-collections.md +0 -69
  56. package/docs/bim-components/expose-events.md +0 -84
  57. package/docs/bim-components/library-examples.md +0 -28
  58. package/docs/bim-components/observable-collections.md +0 -83
  59. package/docs/bim-components/overview.md +0 -160
  60. package/docs/bim-components/per-frame-updates.md +0 -20
  61. package/docs/bim-components/save-and-restore-state.md +0 -21
  62. package/docs/bim-components/setup-and-cleanup.md +0 -64
  63. package/docs/bim-components/type-conventions.md +0 -49
  64. package/docs/bim-components/user-driven-object-creation.md +0 -56
  65. package/docs/cli-setup.md +0 -54
  66. package/docs/cloud-components.md +0 -74
  67. package/docs/connect-logic-to-ui.md +0 -113
  68. package/docs/previewing.md +0 -45
  69. package/docs/publishing.md +0 -35
  70. package/docs/scaffolding.md +0 -71
  71. package/docs/ui-components/async-actions.md +0 -18
  72. package/docs/ui-components/confirmation-dialog.md +0 -39
  73. package/docs/ui-components/data-table.md +0 -185
  74. package/docs/ui-components/display-text.md +0 -39
  75. package/docs/ui-components/inline-form.md +0 -44
  76. package/docs/ui-components/library-examples.md +0 -24
  77. package/docs/ui-components/overview.md +0 -194
  78. package/docs/ui-components/rendering-patterns.md +0 -129
  79. package/docs/ui-components/sections-layout.md +0 -123
  80. package/docs/update-grid-elements.md +0 -46
  81. package/docs/using-colors.md +0 -32
  82. package/docs/using-icons.md +0 -37
  83. package/src/cli/templates/bim/src/bim-components/CloudRunner/src/index.ts +0 -1
  84. package/src/cli/templates/bim/src/bim-components/CloudRunner/src/types.ts +0 -5
  85. package/src/cli/templates/bim/src/bim-components/index.ts +0 -1
  86. package/src/cli/templates/bim/src/globals.ts +0 -1
  87. package/src/cli/templates/bim/src/setups/index.ts +0 -3
  88. package/src/cli/templates/bim/src/ui-components/app-info-section/index.ts +0 -26
  89. package/src/cli/templates/bim/src/ui-components/app-info-section/src/index.ts +0 -1
  90. package/src/cli/templates/bim/src/ui-components/cloud-runner-section/index.ts +0 -37
  91. package/src/cli/templates/bim/src/ui-components/cloud-runner-section/src/index.ts +0 -1
  92. package/src/cli/templates/bim/src/ui-components/index.ts +0 -2
  93. package/src/cli/templates/bim-beta/CONTEXT.md +0 -241
  94. package/src/cli/templates/bim-beta/package.json +0 -25
  95. package/src/cli/templates/bim-beta/src/app.ts +0 -16
  96. package/src/cli/templates/bim-beta/src/bim-components/CloudRunner/index.ts +0 -91
  97. package/src/cli/templates/bim-beta/src/main.ts +0 -89
  98. package/src/cli/templates/bim-beta/src/setups/cloud-runner.ts +0 -13
  99. package/src/cli/templates/bim-beta/src/setups/ui-manager.ts +0 -27
  100. package/src/cli/templates/bim-beta/src/setups/viewports-manager.ts +0 -22
  101. package/src/cli/templates/bim-beta/src/ui-components/app-info-section/src/types.ts +0 -15
  102. package/src/cli/templates/bim-beta/src/ui-components/cloud-runner-section/src/types.ts +0 -14
  103. package/src/cli/templates/cloud/_thatopen +0 -5
  104. package/src/cli/templates/cloud/package.json +0 -22
  105. package/src/cli/templates/cloud-test/CONTEXT.md +0 -56
  106. package/src/cli/templates/cloud-test/src/main.ts +0 -565
  107. package/src/cli/templates/default/CONTEXT.md +0 -92
  108. package/src/cli/templates/default/package.json +0 -18
  109. package/src/cli/templates/default/src/main.ts +0 -62
  110. package/src/cli/templates/test/CONTEXT.md +0 -53
  111. package/src/cli/templates/test/package.json +0 -25
  112. package/src/cli/templates/test/src/main.ts +0 -955
  113. /package/src/cli/templates/{bim/CONTEXT.md → app/README.md} +0 -0
  114. /package/src/cli/templates/{shared/app → app}/index.html +0 -0
  115. /package/src/cli/templates/{bim → app}/package.json +0 -0
  116. /package/src/cli/templates/{bim → app}/src/app.ts +0 -0
  117. /package/src/cli/templates/{bim → app}/src/bim-components/CloudRunner/index.ts +0 -0
  118. /package/src/cli/templates/{bim-beta → app}/src/bim-components/CloudRunner/src/index.ts +0 -0
  119. /package/src/cli/templates/{bim-beta → app}/src/bim-components/CloudRunner/src/types.ts +0 -0
  120. /package/src/cli/templates/{bim-beta → app}/src/bim-components/index.ts +0 -0
  121. /package/src/cli/templates/{bim-beta → app}/src/globals.ts +0 -0
  122. /package/src/cli/templates/{bim → app}/src/main.ts +0 -0
  123. /package/src/cli/templates/{bim → app}/src/setups/cloud-runner.ts +0 -0
  124. /package/src/cli/templates/{bim-beta → app}/src/setups/index.ts +0 -0
  125. /package/src/cli/templates/{bim → app}/src/setups/ui-manager.ts +0 -0
  126. /package/src/cli/templates/{bim → app}/src/setups/viewports-manager.ts +0 -0
  127. /package/src/cli/templates/{bim-beta → app}/src/ui-components/app-info-section/index.ts +0 -0
  128. /package/src/cli/templates/{bim-beta → app}/src/ui-components/app-info-section/src/index.ts +0 -0
  129. /package/src/cli/templates/{bim → app}/src/ui-components/app-info-section/src/types.ts +0 -0
  130. /package/src/cli/templates/{bim-beta → app}/src/ui-components/cloud-runner-section/index.ts +0 -0
  131. /package/src/cli/templates/{bim-beta → app}/src/ui-components/cloud-runner-section/src/index.ts +0 -0
  132. /package/src/cli/templates/{bim → app}/src/ui-components/cloud-runner-section/src/types.ts +0 -0
  133. /package/src/cli/templates/{bim-beta → app}/src/ui-components/index.ts +0 -0
  134. /package/src/cli/templates/{shared/app → app}/tsconfig.json +0 -0
  135. /package/src/cli/templates/{cloud/CONTEXT.md → cloud-component/README.md} +0 -0
  136. /package/src/cli/templates/{cloud-test → cloud-component}/_thatopen +0 -0
  137. /package/src/cli/templates/{cloud → cloud-component}/declarations.json +0 -0
  138. /package/src/cli/templates/{cloud-test → cloud-component}/package.json +0 -0
  139. /package/src/cli/templates/{cloud → cloud-component}/src/main.ts +0 -0
  140. /package/src/cli/templates/{shared/cloud → cloud-component}/tsconfig.json +0 -0
@@ -0,0 +1,214 @@
1
+ /* MD
2
+ ## CDEManager
3
+ ---
4
+ CDEManager is the central state machine for a Common Data Environment. It holds
5
+ files, folders, metadata schemas, viewer groups, and user permissions, and exposes a
6
+ typed state machine that all file operations — selecting, editing, uploading,
7
+ viewing — route through. It has no DOM or Three.js dependencies and can run in Node,
8
+ a browser, or a server-side worker wherever `@thatopen/components` is available.
9
+
10
+ This example shows how to initialize CDEManager, populate it with mock and live data,
11
+ subscribe to its events, navigate the state machine, and build file names from a
12
+ naming schema.
13
+
14
+ ### 🖖 Importing our libraries
15
+ First, install all necessary dependencies:
16
+ */
17
+
18
+ // In production platform apps, CDEManager is loaded through the platform client:
19
+ // const cde = await client.initBuiltInComponent(CDEManager.uuid, components);
20
+ // Here we import directly from source for local testing.
21
+ import * as OBC from "@thatopen/components";
22
+ import { CDEManager } from "./index";
23
+ import type { CDEFile } from "./src/types";
24
+
25
+ /* MD
26
+ ### 🚀 Initializing CDEManager
27
+ CDEManager is an `OBC.Component`, so you get it from the shared `components`
28
+ registry with `components.get(CDEManager)`. The constructor registers it
29
+ automatically — calling `get` a second time returns the same instance.
30
+ */
31
+
32
+ const components = new OBC.Components();
33
+ const cde = components.get(CDEManager);
34
+
35
+ const output = document.getElementById("output") as HTMLPreElement;
36
+ const log = (msg: string) => {
37
+ console.log(msg);
38
+ output.textContent += msg + "\n";
39
+ };
40
+
41
+ /* MD
42
+ ### 📋 Subscribing to events
43
+ All CDEManager events are `OBC.Event<T>` instances. Subscribe before setting data
44
+ so you capture the first trigger.
45
+ */
46
+
47
+ cde.onFilesChanged.add((files) => {
48
+ log(`[event] onFilesChanged: ${files.length} file(s)`);
49
+ });
50
+
51
+ cde.onFoldersChanged.add((folders) => {
52
+ log(`[event] onFoldersChanged: ${folders.length} folder(s)`);
53
+ });
54
+
55
+ cde.onMachineStateChanged.add((state) => {
56
+ log(`[event] machineState → ${state.kind}`);
57
+ });
58
+
59
+ cde.onSelectedGroupChanged.add((group) => {
60
+ log(`[event] selectedGroup → ${group ?? "(none)"}`);
61
+ });
62
+
63
+ /* MD
64
+ ### 📂 Populating with mock data
65
+ Setting `cde.files` and `cde.folders` directly triggers `onFilesChanged` and
66
+ `onFoldersChanged`. This lets you build and test views without a live backend.
67
+ */
68
+
69
+ const mockFolder = { _id: "folder-str", name: "Structural", parentId: null };
70
+ cde.folders = [mockFolder];
71
+
72
+ const now = new Date();
73
+ const mockFiles: CDEFile[] = [
74
+ {
75
+ _id: "file-col", name: "column.ifc",
76
+ folderId: "folder-str", createdBy: "alice@example.com",
77
+ createdAt: now, metadata: { discipline: "STR", phase: "DD" },
78
+ itemType: "FILE",
79
+ } as CDEFile,
80
+ {
81
+ _id: "file-fnd", name: "foundations.ifc",
82
+ folderId: "folder-str", createdBy: "bob@example.com",
83
+ createdAt: now, metadata: { discipline: "STR", phase: "CD" },
84
+ itemType: "FILE",
85
+ } as CDEFile,
86
+ {
87
+ _id: "file-pdf", name: "report.pdf",
88
+ folderId: null, createdBy: "alice@example.com",
89
+ createdAt: now, metadata: {},
90
+ itemType: "FILE",
91
+ } as CDEFile,
92
+ ];
93
+ cde.files = mockFiles;
94
+
95
+ /* MD
96
+ ### 🔍 Filtering files: visibleFiles vs getSelectedFiles
97
+ `visibleFiles` excludes files whose folder was marked as a system folder by
98
+ `loadFolders` (folders whose names start with `__`). With mock data set directly,
99
+ no system folders are marked, so all files are always visible.
100
+
101
+ `getSelectedFiles` is the folder-scoped view used by the UI table. When
102
+ `selectedGroup` is `null` it returns root files only (no folderId); when set to a
103
+ folder id it returns files in that folder.
104
+ */
105
+
106
+ log(`\nvisibleFiles (all, no system folders excluded): ${cde.visibleFiles.length}`);
107
+
108
+ // getSelectedFiles respects selectedGroup, not visibleFiles.
109
+ cde.selectedGroup = null;
110
+ log(`getSelectedFiles (selectedGroup=null → root only): ${cde.getSelectedFiles().length}`);
111
+
112
+ cde.selectedGroup = "folder-str";
113
+ log(`getSelectedFiles (selectedGroup=folder-str): ${cde.getSelectedFiles().length}`);
114
+
115
+ /* MD
116
+ ### 🎛️ State machine navigation
117
+ Every UI-level action goes through `sendMachineEvent`. The current state is readable
118
+ via `cde.machineState`. Events that aren't valid for the current state are silently
119
+ ignored — checking `machineState.kind` before sending prevents unexpected no-ops.
120
+
121
+ The `"selected"` state stores the selected files in `machineState.files`. This is
122
+ separate from `getSelectedFiles()`, which always returns the current folder view
123
+ regardless of machine state.
124
+ */
125
+
126
+ log(`\nmachineState before SELECT: ${cde.machineState.kind}`);
127
+
128
+ cde.sendMachineEvent({ type: "SELECT", files: [mockFiles[0], mockFiles[1]] });
129
+ // machineState.files holds the selection — getSelectedFiles() returns the folder view.
130
+ const sel = cde.machineState as { kind: string; files?: CDEFile[] };
131
+ log(`machineState.files: ${sel.files?.length ?? 0} file(s) selected`);
132
+
133
+ cde.sendMachineEvent({ type: "DESELECT" });
134
+ log(`machineState after DESELECT: ${cde.machineState.kind}`);
135
+
136
+ /* MD
137
+ ### 🏷️ Viewer groups and canView
138
+ `viewerGroups` declares which file extensions can be loaded side by side.
139
+ `multiple: true` allows multi-file loads; omitting it means only one file at a time.
140
+ `canView` checks whether a set of files belongs to one group and respects the flag.
141
+ */
142
+
143
+ cde.viewerGroups = [
144
+ { extensions: ["ifc", "frag"], multiple: true },
145
+ { extensions: ["pdf"] },
146
+ ];
147
+
148
+ const ifcFiles = mockFiles.filter(f => f.name.endsWith(".ifc"));
149
+ log(`\ncanView([column.ifc, foundations.ifc]): ${cde.canView(ifcFiles)}`);
150
+ log(`canView([report.pdf]): ${cde.canView([mockFiles[2]])}`);
151
+ // Mixed types never share a viewer group, regardless of multiple.
152
+ log(`canView([column.ifc, report.pdf]): ${cde.canView([mockFiles[0], mockFiles[2]])}`);
153
+
154
+ /* MD
155
+ ### 📝 Naming schema
156
+ A naming schema assembles a computed file name from metadata fields. Segments define
157
+ the order and an optional transform; `separator` joins them. If a metadata key is
158
+ absent, `missingMetadataPlaceholder` is used so missing values stay visible.
159
+ */
160
+
161
+ cde.missingMetadataPlaceholder = "XX";
162
+ cde.setNamingSchema({
163
+ segments: [
164
+ { key: "discipline" },
165
+ { key: "phase" },
166
+ ],
167
+ separator: "-",
168
+ });
169
+
170
+ // column.ifc has both fields → "STR-DD"
171
+ log(`\nbuildFileName(column.ifc): "${cde.buildFileName(mockFiles[0])}"`);
172
+ // report.pdf has no metadata → "XX-XX"
173
+ log(`buildFileName(report.pdf): "${cde.buildFileName(mockFiles[2])}"`);
174
+
175
+ /* MD
176
+ ### 🌐 Live data (requires credentials)
177
+ The live section below connects to a real backend. Pass credentials as query
178
+ parameters when opening example.html:
179
+
180
+ example.html?accessToken=<token>&apiUrl=https://...&projectId=<id>
181
+
182
+ Without them, this section is skipped.
183
+
184
+ `loadFolders()` does more than just fetch folders — it also loads user permissions
185
+ and project members in the same call, then excludes any `__`-prefixed system
186
+ folders from `cde.folders`.
187
+ */
188
+
189
+ const params = new URLSearchParams(location.search);
190
+ const accessToken = params.get("accessToken");
191
+ const apiUrl = params.get("apiUrl");
192
+ const projectId = params.get("projectId");
193
+
194
+ if (accessToken && apiUrl && projectId) {
195
+ const { EngineServicesClient } = await import("thatopen-services");
196
+ const client = new EngineServicesClient(accessToken, apiUrl);
197
+ cde.ctx = { client, projectId };
198
+
199
+ await cde.loadFolders();
200
+ log(`\nloadFolders: ${cde.folders.length} visible folder(s), project: "${cde.projectName}"`);
201
+ log(`userPermissions: ${JSON.stringify(cde.userPermissions)}`);
202
+
203
+ await cde.loadMetadataSchema();
204
+ log(`loadMetadataSchema: ${Object.keys(cde.metadataSchema).length} field(s) defined`);
205
+ } else {
206
+ log("\nLive data skipped — pass ?accessToken=&apiUrl=&projectId= to run the full cycle.");
207
+ }
208
+
209
+ /* MD
210
+ ### 🎉 Wrap up
211
+ CDEManager handles state, events, filtering, and naming entirely in memory. The
212
+ backend connection through `cde.ctx` is optional — everything above `loadFolders`
213
+ works offline with mock data. See platform_cde for the full production wiring.
214
+ */
@@ -0,0 +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
+ }
@@ -0,0 +1,186 @@
1
+ /* MD
2
+ ## Drag-and-drop file and folder moves
3
+ ---
4
+ A coordinator restructures the project after a scope change: structural files move
5
+ from the root into a dedicated discipline folder, a MEP sub-folder needs to be
6
+ promoted to the top level, and a few documents need to come back to root to appear
7
+ in the general view. Drag-and-drop is the natural gesture for all of this, but
8
+ each drag session has intermediate stages — the file is "in the air", hovering over
9
+ a candidate target — and every panel watching the file table should see the right
10
+ state without coordinating directly with the drag component.
11
+
12
+ CDEManager models drag-and-drop as a sequence of typed state transitions. Picking
13
+ up files moves the machine to a dragging state that carries the files being dragged
14
+ and where they came from. Dropping onto a target kicks off the API call and advances
15
+ to a moving state. Releasing outside any drop zone cancels the drag and returns the
16
+ machine to exactly the state it was in before the drag started — idle or a selection.
17
+ Folder drags work the same way, with their own separate completion event.
18
+
19
+ This tutorial covers starting a file drag from idle and from an active selection;
20
+ cancelling the drag and confirming which state the machine returns to; committing
21
+ a drop onto a folder and reading the moved files from the completion event; moving
22
+ files back to the project root by passing a null target folder; and starting,
23
+ cancelling, and committing a folder drag.
24
+
25
+ By the end, you'll have a working demonstration of the full drag-and-drop cycle for
26
+ both files and folders — start, cancel, and commit — with all state transitions and
27
+ completion events verified against mock data.
28
+ */
29
+
30
+ import * as OBC from "@thatopen/components";
31
+ // In production platform apps, CDEManager is loaded through the platform client:
32
+ // const cde = await client.initBuiltInComponent(CDEManager.uuid, components);
33
+ // Here we import directly from source for local testing.
34
+ import { CDEManager } from "../../index";
35
+ import type { CDEFile } from "../../src/types";
36
+
37
+ /* MD
38
+ ### 🛠️ Setting up
39
+ File and folder moves are permission-gated, so we simulate the platform startup step
40
+ that loads user permissions. The mock client also provides `updateItem` for file
41
+ moves and `resolveAccessToken` for folder moves, which use different backend paths.
42
+ */
43
+
44
+ const components = new OBC.Components();
45
+ const cde = components.get(CDEManager);
46
+
47
+ const output = document.getElementById("output") as HTMLPreElement;
48
+ const log = (msg: string) => { console.log(msg); output.textContent += msg + "\n"; };
49
+
50
+ const mockClient = {
51
+ checkPermissionBatch: async () => Array(5).fill({ hasPermission: true }),
52
+ getProjectData: async () => ({ project: { title: "" }, users: [] }),
53
+ listFolders: async () => [],
54
+ updateItem: async (_id: string, _updates: any) => ({}),
55
+ resolveAccessToken: async () => "mock-token",
56
+ context: { apiUrl: "https://mock.api" },
57
+ };
58
+
59
+ cde.ctx = { client: mockClient as any, projectId: "mock" };
60
+ await cde.loadFolders();
61
+
62
+ const folders = [
63
+ { _id: "folder-str", name: "Structural", parentId: null },
64
+ { _id: "folder-arch", name: "Architecture", parentId: null },
65
+ { _id: "folder-mep", name: "MEP", parentId: "folder-arch" },
66
+ ];
67
+ cde.folders = folders as any;
68
+
69
+ const now = new Date();
70
+ const mockFiles: CDEFile[] = [
71
+ { _id: "f1", name: "column.ifc", folderId: null, createdBy: "alice", createdAt: now, metadata: {} } as CDEFile,
72
+ { _id: "f2", name: "foundations.ifc", folderId: null, createdBy: "alice", createdAt: now, metadata: {} } as CDEFile,
73
+ { _id: "f3", name: "duct.ifc", folderId: "folder-arch", createdBy: "bob", createdAt: now, metadata: {} } as CDEFile,
74
+ ];
75
+ cde.files = mockFiles;
76
+
77
+ cde.onMachineStateChanged.add((s) => log(` → ${s.kind}`));
78
+ cde.onFilesMovedCompleted.add(({ files, targetFolderName }) =>
79
+ log(` [onFilesMovedCompleted] ${files.length} file(s) → "${targetFolderName ?? "root"}"`)
80
+ );
81
+ cde.onFolderMovedCompleted.add(({ folderName, targetFolderName }) =>
82
+ log(` [onFolderMovedCompleted] "${folderName}" → "${targetFolderName ?? "root"}"`)
83
+ );
84
+
85
+ /* MD
86
+ ### 🖱️ File drag — cancel
87
+ Starting a drag records the dragged files and the folder they came from. Cancelling
88
+ — for example when the user releases the mouse outside any drop target — returns the
89
+ machine to the state it was in when the drag started. If the drag started from idle,
90
+ it returns to idle. If it started from a selection, the same selection is restored.
91
+ */
92
+
93
+ log("\n--- file drag: cancel from idle ---");
94
+ cde.sendMachineEvent({ type: "START_DRAG", files: [mockFiles[0]], sourceFolderId: null });
95
+
96
+ if (cde.machineState.kind === "dragging") {
97
+ log(` dragging: ${cde.machineState.files.length} file(s) from "${cde.machineState.sourceFolderId ?? "root"}"`);
98
+ }
99
+
100
+ cde.sendMachineEvent({ type: "END_DRAG" });
101
+ log(` state after cancel: ${cde.machineState.kind}`);
102
+
103
+ log("\n--- file drag: cancel from selection ---");
104
+ cde.sendMachineEvent({ type: "SELECT", files: [mockFiles[0], mockFiles[1]] });
105
+ // Drag starts from "selected" — previousState carries the selection.
106
+ cde.sendMachineEvent({ type: "START_DRAG", files: [mockFiles[0]], sourceFolderId: null });
107
+ cde.sendMachineEvent({ type: "END_DRAG" });
108
+ log(` state after cancel: ${cde.machineState.kind}`);
109
+ if (cde.machineState.kind === "selected") {
110
+ log(` selection preserved: ${cde.machineState.files.length} file(s)`);
111
+ }
112
+
113
+ /* MD
114
+ ### 📁 File drag — drop onto a folder
115
+ When the user drops files onto a valid folder target, the machine moves to a moving
116
+ state and the API call runs. On completion, the moved files reflect their new folder
117
+ and the machine returns to idle. The completion event carries both the updated files
118
+ and the target folder name, so the UI can show a confirmation without reading the
119
+ whole file list.
120
+ */
121
+
122
+ log("\n--- file drag: drop onto folder ---");
123
+ cde.sendMachineEvent({ type: "DESELECT" });
124
+ cde.sendMachineEvent({ type: "START_DRAG", files: [mockFiles[0], mockFiles[1]], sourceFolderId: null });
125
+
126
+ await new Promise<void>(resolve => {
127
+ cde.onFilesMovedCompleted.add(() => resolve());
128
+ cde.sendMachineEvent({ type: "DROP_FILES", targetFolderId: "folder-str" });
129
+ });
130
+
131
+ log(` state after move: ${cde.machineState.kind}`);
132
+ const f1Updated = cde.files.find(f => f._id === "f1")!;
133
+ log(` f1 folderId: ${(f1Updated as any).folderId}`);
134
+
135
+ /* MD
136
+ ### 🏠 File drag — drop at root
137
+ Dropping files without specifying a target folder moves them to the project root.
138
+ The completion event's `targetFolderName` is null for root drops, which the UI can
139
+ use to display "Moved to root" instead of a folder name.
140
+ */
141
+
142
+ log("\n--- file drag: drop at root ---");
143
+ cde.sendMachineEvent({ type: "START_DRAG", files: [mockFiles[2]], sourceFolderId: "folder-arch" });
144
+
145
+ await new Promise<void>(resolve => {
146
+ cde.onFilesMovedCompleted.add(() => resolve());
147
+ cde.sendMachineEvent({ type: "DROP_FILES", targetFolderId: null });
148
+ });
149
+
150
+ log(` state after move: ${cde.machineState.kind}`);
151
+ const f3Updated = cde.files.find(f => f._id === "f3")!;
152
+ log(` f3 folderId: ${(f3Updated as any).folderId ?? "root"}`);
153
+
154
+ /* MD
155
+ ### 📂 Folder drag — cancel and commit
156
+ Folder drags follow the same state sequence as file drags. Cancelling a folder drag
157
+ returns to the previous state unchanged. Committing calls a dedicated backend
158
+ endpoint and fires a separate completion event with the folder names involved, which
159
+ the UI uses for confirmation messages.
160
+ */
161
+
162
+ log("\n--- folder drag: cancel ---");
163
+ cde.sendMachineEvent({ type: "START_FOLDER_DRAG", folderId: "folder-mep", parentFolderId: "folder-arch" });
164
+
165
+ if (cde.machineState.kind === "draggingFolder") {
166
+ log(` draggingFolder: "${cde.machineState.folderId}"`);
167
+ }
168
+
169
+ cde.sendMachineEvent({ type: "END_FOLDER_DRAG" });
170
+ log(` state after cancel: ${cde.machineState.kind}`);
171
+
172
+ log("\n--- folder drag: commit ---");
173
+ // _moveFolder calls fetch directly; intercept it for the mock.
174
+ const realFetch = globalThis.fetch;
175
+ globalThis.fetch = async () => new Response("{}", { status: 200 });
176
+
177
+ cde.sendMachineEvent({ type: "START_FOLDER_DRAG", folderId: "folder-mep", parentFolderId: "folder-arch" });
178
+
179
+ await new Promise<void>(resolve => {
180
+ cde.onFolderMovedCompleted.add(() => resolve());
181
+ // Move MEP out of Architecture and into Structural.
182
+ cde.sendMachineEvent({ type: "DROP_FOLDER", targetFolderId: "folder-str" });
183
+ });
184
+
185
+ globalThis.fetch = realFetch;
186
+ log(` state after commit: ${cde.machineState.kind}`);