@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.
- package/README.md +24 -263
- package/dist/cli/commands/swap.d.ts +3 -0
- package/dist/cli/lib/beta.d.ts +2 -0
- package/dist/cli/lib/config.d.ts +2 -0
- package/dist/cli.js +85 -126
- package/dist/core/examples/apps.d.ts +1 -0
- package/dist/core/examples/components.d.ts +1 -0
- package/dist/core/examples/execution.d.ts +1 -0
- package/dist/core/examples/file-metadata.d.ts +1 -0
- package/dist/core/examples/files.d.ts +1 -0
- package/dist/core/examples/folders.d.ts +1 -0
- package/dist/core/examples/hidden-files.d.ts +1 -0
- package/dist/core/examples/icons.d.ts +1 -0
- package/dist/core/examples/permissions.d.ts +1 -0
- package/dist/types/items.d.ts +2 -0
- package/docs/builtin/paths.json +54 -0
- package/docs/cli/create.md +28 -0
- package/docs/cli/local-server.md +38 -0
- package/docs/cli/login.md +32 -0
- package/docs/cli/paths.json +30 -0
- package/docs/cli/publish.md +31 -0
- package/docs/cli/run.md +26 -0
- package/docs/cli/serve.md +43 -0
- package/docs/cli/swap.md +30 -0
- package/docs/client/paths.json +38 -0
- package/package.json +8 -10
- package/resources/AGENTS.md +39 -0
- package/src/built-in/AppManager/example.ts +86 -0
- package/src/built-in/CDEManager/example.ts +214 -0
- package/src/built-in/CDEManager/examples/FileEditing/example.ts +226 -0
- package/src/built-in/CDEManager/examples/FileMoves/example.ts +186 -0
- package/src/built-in/CDEManager/examples/FileOperations/example.ts +295 -0
- package/src/built-in/CDEManager/examples/FileTransfers/example.ts +210 -0
- package/src/built-in/CDEManager/examples/FolderManagement/example.ts +230 -0
- package/src/built-in/CDEManager/examples/MetadataSchema/example.ts +230 -0
- package/src/built-in/CDEManager/examples/MetadataViews/example.ts +166 -0
- package/src/built-in/CDEManager/examples/ProjectConfig/example.ts +177 -0
- package/src/built-in/FileList/example.ts +107 -0
- package/src/built-in/TabbedNavigation/example.ts +115 -0
- package/src/built-in/ViewportsManager/example.ts +106 -0
- package/src/cli/templates/{shared/app → app}/vite.config.js +42 -23
- package/src/cli/templates/{shared/cloud → cloud-component}/vite.config.js +46 -27
- package/src/cli/templates/shared/AGENTS.md +8 -30
- package/src/cli/templates/shared/CLAUDE.md +1 -1
- package/AGENTS.md +0 -86
- package/CONTEXT.md +0 -258
- package/dist/built-in/index.d.ts +0 -723
- package/dist/cli/commands/create-tests.d.ts +0 -3
- package/dist/cli/commands/serve-tests.d.ts +0 -3
- package/docs/access-backend-data.md +0 -19
- package/docs/app-architecture.md +0 -94
- package/docs/app-layout.md +0 -139
- package/docs/app-wiring.md +0 -123
- package/docs/bim-components/coding-conventions.md +0 -128
- package/docs/bim-components/element-collections.md +0 -69
- package/docs/bim-components/expose-events.md +0 -84
- package/docs/bim-components/library-examples.md +0 -28
- package/docs/bim-components/observable-collections.md +0 -83
- package/docs/bim-components/overview.md +0 -160
- package/docs/bim-components/per-frame-updates.md +0 -20
- package/docs/bim-components/save-and-restore-state.md +0 -21
- package/docs/bim-components/setup-and-cleanup.md +0 -64
- package/docs/bim-components/type-conventions.md +0 -49
- package/docs/bim-components/user-driven-object-creation.md +0 -56
- package/docs/cli-setup.md +0 -54
- package/docs/cloud-components.md +0 -74
- package/docs/connect-logic-to-ui.md +0 -113
- package/docs/previewing.md +0 -45
- package/docs/publishing.md +0 -35
- package/docs/scaffolding.md +0 -71
- package/docs/ui-components/async-actions.md +0 -18
- package/docs/ui-components/confirmation-dialog.md +0 -39
- package/docs/ui-components/data-table.md +0 -185
- package/docs/ui-components/display-text.md +0 -39
- package/docs/ui-components/inline-form.md +0 -44
- package/docs/ui-components/library-examples.md +0 -24
- package/docs/ui-components/overview.md +0 -194
- package/docs/ui-components/rendering-patterns.md +0 -129
- package/docs/ui-components/sections-layout.md +0 -123
- package/docs/update-grid-elements.md +0 -46
- package/docs/using-colors.md +0 -32
- package/docs/using-icons.md +0 -37
- package/src/cli/templates/bim/src/bim-components/CloudRunner/src/index.ts +0 -1
- package/src/cli/templates/bim/src/bim-components/CloudRunner/src/types.ts +0 -5
- package/src/cli/templates/bim/src/bim-components/index.ts +0 -1
- package/src/cli/templates/bim/src/globals.ts +0 -1
- package/src/cli/templates/bim/src/setups/index.ts +0 -3
- package/src/cli/templates/bim/src/ui-components/app-info-section/index.ts +0 -26
- package/src/cli/templates/bim/src/ui-components/app-info-section/src/index.ts +0 -1
- package/src/cli/templates/bim/src/ui-components/cloud-runner-section/index.ts +0 -37
- package/src/cli/templates/bim/src/ui-components/cloud-runner-section/src/index.ts +0 -1
- package/src/cli/templates/bim/src/ui-components/index.ts +0 -2
- package/src/cli/templates/bim-beta/CONTEXT.md +0 -241
- package/src/cli/templates/bim-beta/package.json +0 -25
- package/src/cli/templates/bim-beta/src/app.ts +0 -16
- package/src/cli/templates/bim-beta/src/bim-components/CloudRunner/index.ts +0 -91
- package/src/cli/templates/bim-beta/src/main.ts +0 -89
- package/src/cli/templates/bim-beta/src/setups/cloud-runner.ts +0 -13
- package/src/cli/templates/bim-beta/src/setups/ui-manager.ts +0 -27
- package/src/cli/templates/bim-beta/src/setups/viewports-manager.ts +0 -22
- package/src/cli/templates/bim-beta/src/ui-components/app-info-section/src/types.ts +0 -15
- package/src/cli/templates/bim-beta/src/ui-components/cloud-runner-section/src/types.ts +0 -14
- package/src/cli/templates/cloud/_thatopen +0 -5
- package/src/cli/templates/cloud/package.json +0 -22
- package/src/cli/templates/cloud-test/CONTEXT.md +0 -56
- package/src/cli/templates/cloud-test/src/main.ts +0 -565
- package/src/cli/templates/default/CONTEXT.md +0 -92
- package/src/cli/templates/default/package.json +0 -18
- package/src/cli/templates/default/src/main.ts +0 -62
- package/src/cli/templates/test/CONTEXT.md +0 -53
- package/src/cli/templates/test/package.json +0 -25
- package/src/cli/templates/test/src/main.ts +0 -955
- /package/src/cli/templates/{bim/CONTEXT.md → app/README.md} +0 -0
- /package/src/cli/templates/{shared/app → app}/index.html +0 -0
- /package/src/cli/templates/{bim → app}/package.json +0 -0
- /package/src/cli/templates/{bim → app}/src/app.ts +0 -0
- /package/src/cli/templates/{bim → app}/src/bim-components/CloudRunner/index.ts +0 -0
- /package/src/cli/templates/{bim-beta → app}/src/bim-components/CloudRunner/src/index.ts +0 -0
- /package/src/cli/templates/{bim-beta → app}/src/bim-components/CloudRunner/src/types.ts +0 -0
- /package/src/cli/templates/{bim-beta → app}/src/bim-components/index.ts +0 -0
- /package/src/cli/templates/{bim-beta → app}/src/globals.ts +0 -0
- /package/src/cli/templates/{bim → app}/src/main.ts +0 -0
- /package/src/cli/templates/{bim → app}/src/setups/cloud-runner.ts +0 -0
- /package/src/cli/templates/{bim-beta → app}/src/setups/index.ts +0 -0
- /package/src/cli/templates/{bim → app}/src/setups/ui-manager.ts +0 -0
- /package/src/cli/templates/{bim → app}/src/setups/viewports-manager.ts +0 -0
- /package/src/cli/templates/{bim-beta → app}/src/ui-components/app-info-section/index.ts +0 -0
- /package/src/cli/templates/{bim-beta → app}/src/ui-components/app-info-section/src/index.ts +0 -0
- /package/src/cli/templates/{bim → app}/src/ui-components/app-info-section/src/types.ts +0 -0
- /package/src/cli/templates/{bim-beta → app}/src/ui-components/cloud-runner-section/index.ts +0 -0
- /package/src/cli/templates/{bim-beta → app}/src/ui-components/cloud-runner-section/src/index.ts +0 -0
- /package/src/cli/templates/{bim → app}/src/ui-components/cloud-runner-section/src/types.ts +0 -0
- /package/src/cli/templates/{bim-beta → app}/src/ui-components/index.ts +0 -0
- /package/src/cli/templates/{shared/app → app}/tsconfig.json +0 -0
- /package/src/cli/templates/{cloud/CONTEXT.md → cloud-component/README.md} +0 -0
- /package/src/cli/templates/{cloud-test → cloud-component}/_thatopen +0 -0
- /package/src/cli/templates/{cloud → cloud-component}/declarations.json +0 -0
- /package/src/cli/templates/{cloud-test → cloud-component}/package.json +0 -0
- /package/src/cli/templates/{cloud → cloud-component}/src/main.ts +0 -0
- /package/src/cli/templates/{shared/cloud → cloud-component}/tsconfig.json +0 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/* MD
|
|
2
|
+
## File operations — inline edit, rename, view, download, archive
|
|
3
|
+
---
|
|
4
|
+
BIM coordinators reviewing a model drop repeat the same small actions throughout
|
|
5
|
+
their session: correcting a discipline tag that changed after a scope revision,
|
|
6
|
+
renaming a file to match the agreed convention, opening it alongside the structural
|
|
7
|
+
model to check for clashes, handing it off to a sub-consultant, or removing a
|
|
8
|
+
superseded version from the live set. If every panel in the platform handles these
|
|
9
|
+
separately, two panels looking at the same file at the same time can show
|
|
10
|
+
contradictory states — one says "uploading", another still shows the old name.
|
|
11
|
+
|
|
12
|
+
CDEManager gives all of these operations a single shared state. Every action moves
|
|
13
|
+
through a known transition so any part of the platform — the file table, the viewer,
|
|
14
|
+
the side panel — reacts to the same source of truth without coordinating with every
|
|
15
|
+
other panel individually.
|
|
16
|
+
|
|
17
|
+
This tutorial covers selecting files and editing a metadata value inline; cancelling
|
|
18
|
+
an in-progress edit; committing the edit and reading the updated value back from the
|
|
19
|
+
state; renaming a file optimistically and observing the local update; opening files
|
|
20
|
+
in the viewer, adding a second file to the active view, simulating load completion,
|
|
21
|
+
and removing files from the view one at a time; closing the viewer while keeping the
|
|
22
|
+
selection intact; and, with live credentials, downloading a selection and archiving a
|
|
23
|
+
file through the state machine.
|
|
24
|
+
|
|
25
|
+
By the end, you'll have a working demonstration of every per-file state machine flow
|
|
26
|
+
— inline edit, rename, viewer, download, and archive — with all mock-data flows
|
|
27
|
+
running without any backend connection.
|
|
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
|
+
Most flows in this example run against mock data without a backend connection.
|
|
40
|
+
However, operations like editing metadata and renaming files are permission-gated —
|
|
41
|
+
the platform normally loads those permissions when the app starts. We simulate that
|
|
42
|
+
startup step here so the demo flows work in isolation. We also register viewer groups
|
|
43
|
+
before setting any files because the transition into the viewer validates the group
|
|
44
|
+
configuration at the moment of the event.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
const components = new OBC.Components();
|
|
48
|
+
const cde = components.get(CDEManager);
|
|
49
|
+
|
|
50
|
+
const output = document.getElementById("output") as HTMLPreElement;
|
|
51
|
+
const log = (msg: string) => { console.log(msg); output.textContent += msg + "\n"; };
|
|
52
|
+
|
|
53
|
+
// The "ifc/frag" group allows multiple files open at the same time; "pdf" does not.
|
|
54
|
+
cde.viewerGroups = [
|
|
55
|
+
{ extensions: ["ifc", "frag"], multiple: true },
|
|
56
|
+
{ extensions: ["pdf"] },
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
// Simulate the platform startup step that loads user permissions.
|
|
60
|
+
// Without this, permission-gated events (edit, rename, archive) are silently ignored.
|
|
61
|
+
cde.ctx = {
|
|
62
|
+
client: {
|
|
63
|
+
checkPermissionBatch: async () => Array(5).fill({ hasPermission: true }),
|
|
64
|
+
getProjectData: async () => ({ project: { title: "" }, users: [] }),
|
|
65
|
+
listFolders: async () => [],
|
|
66
|
+
updateFile: async (id: string) => ({ item: { _id: id } }),
|
|
67
|
+
updateFileVersionMetadata: async () => ({}),
|
|
68
|
+
} as any,
|
|
69
|
+
projectId: "mock",
|
|
70
|
+
};
|
|
71
|
+
await cde.loadFolders();
|
|
72
|
+
// Clear the context after loading permissions — mock flows don't make API calls.
|
|
73
|
+
// With a live client, operations like rename would also confirm via onRenameCompleted.
|
|
74
|
+
cde.ctx = undefined;
|
|
75
|
+
|
|
76
|
+
const now = new Date();
|
|
77
|
+
const mockFiles: CDEFile[] = [
|
|
78
|
+
{
|
|
79
|
+
_id: "f1", name: "column.ifc", folderId: null,
|
|
80
|
+
createdBy: "alice", createdAt: now,
|
|
81
|
+
metadata: { discipline: "STR", phase: "DD" },
|
|
82
|
+
} as CDEFile,
|
|
83
|
+
{
|
|
84
|
+
_id: "f2", name: "foundations.ifc", folderId: null,
|
|
85
|
+
createdBy: "bob", createdAt: now,
|
|
86
|
+
metadata: { discipline: "STR", phase: "CD" },
|
|
87
|
+
} as CDEFile,
|
|
88
|
+
];
|
|
89
|
+
cde.files = mockFiles;
|
|
90
|
+
|
|
91
|
+
cde.onMachineStateChanged.add((s) => log(` → ${s.kind}`));
|
|
92
|
+
cde.onFilesChanged.add((files) => log(` [onFilesChanged] ${files.length} file(s)`));
|
|
93
|
+
cde.onRenameCompleted.add(({ success, error }) =>
|
|
94
|
+
log(success ? " [onRenameCompleted] server confirmed" : ` [onRenameCompleted] rolled back — ${error}`)
|
|
95
|
+
);
|
|
96
|
+
cde.onDownloadReady.add(({ blob, filename }) =>
|
|
97
|
+
log(` [onDownloadReady] "${filename}" — ${blob.size} bytes`)
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
/* MD
|
|
101
|
+
### ✏️ Inline metadata edit
|
|
102
|
+
When a coordinator spots a wrong discipline tag, they should be able to correct it
|
|
103
|
+
right in the file row rather than navigating to a separate form. The edit stays
|
|
104
|
+
pending — visible as an unsaved diff — so the UI can show exactly what will change
|
|
105
|
+
before anything is written.
|
|
106
|
+
|
|
107
|
+
Entering the edit state carries the selected files along, so the form always knows
|
|
108
|
+
which file it is editing. Cancelling discards all pending values and returns to the
|
|
109
|
+
exact same selection — nothing is sent to the backend.
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
log("\n--- inline edit: cancel ---");
|
|
113
|
+
cde.sendMachineEvent({ type: "SELECT", files: [mockFiles[0]] });
|
|
114
|
+
cde.sendMachineEvent({ type: "START_EDIT" });
|
|
115
|
+
cde.sendMachineEvent({ type: "UPDATE_METADATA", key: "phase", value: "SD" });
|
|
116
|
+
|
|
117
|
+
if (cde.machineState.kind === "editing") {
|
|
118
|
+
log(` pending phase: ${cde.machineState.pendingMetadata.phase}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
cde.sendMachineEvent({ type: "CANCEL_EDIT" });
|
|
122
|
+
// The original mockFiles[0] reference is unchanged — nothing was applied.
|
|
123
|
+
log(` phase after cancel: ${mockFiles[0].metadata.phase}`);
|
|
124
|
+
|
|
125
|
+
/* MD
|
|
126
|
+
Committing the edit applies the pending values to the file in memory and returns to
|
|
127
|
+
the selection, with the file already reflecting its new values. With a live backend,
|
|
128
|
+
the changes are persisted before the transition completes; without one, the local
|
|
129
|
+
update still happens. Either way, the updated file is in the machine state's
|
|
130
|
+
selection — not in the original array reference.
|
|
131
|
+
*/
|
|
132
|
+
|
|
133
|
+
log("\n--- inline edit: save ---");
|
|
134
|
+
// Re-enter from the current "selected" state.
|
|
135
|
+
cde.sendMachineEvent({ type: "START_EDIT" });
|
|
136
|
+
cde.sendMachineEvent({ type: "UPDATE_METADATA", key: "phase", value: "SD" });
|
|
137
|
+
cde.sendMachineEvent({ type: "SAVE" });
|
|
138
|
+
|
|
139
|
+
// _saveFileMetadata without a client applies the edit locally and returns synchronously.
|
|
140
|
+
if (cde.machineState.kind === "selected") {
|
|
141
|
+
log(` phase after save: ${cde.machineState.files[0].metadata.phase}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* MD
|
|
145
|
+
### 🏷️ Optimistic rename
|
|
146
|
+
Renaming a file can't wait for a server round-trip — the user should see the new
|
|
147
|
+
name appear immediately. The rename applies locally the moment it is triggered, then
|
|
148
|
+
a confirmation arrives once the backend has saved it. If the server rejects the
|
|
149
|
+
change, the name reverts automatically and a rollback event is fired so the UI can
|
|
150
|
+
notify the user.
|
|
151
|
+
|
|
152
|
+
Without a live backend, the local rename still applies and the file list updates —
|
|
153
|
+
only the server confirmation step is skipped, so the rollback path is never reached.
|
|
154
|
+
*/
|
|
155
|
+
|
|
156
|
+
log("\n--- rename immediate ---");
|
|
157
|
+
cde.sendMachineEvent({ type: "RENAME_FILE_IMMEDIATE", fileId: "f1", name: "column-v2.ifc" });
|
|
158
|
+
|
|
159
|
+
const renamedFile = cde.files.find(f => f._id === "f1")!;
|
|
160
|
+
log(` local name: ${renamedFile.name}`);
|
|
161
|
+
// onRenameCompleted fires only with a live client — skipped here.
|
|
162
|
+
|
|
163
|
+
// Restore the original name so the view-flow demo works with "column.ifc".
|
|
164
|
+
cde.sendMachineEvent({ type: "RENAME_FILE_IMMEDIATE", fileId: "f1", name: "column.ifc" });
|
|
165
|
+
|
|
166
|
+
/* MD
|
|
167
|
+
### 👁️ Viewer
|
|
168
|
+
Opening files in the viewer is a state transition, not a direct method call. This
|
|
169
|
+
keeps the viewer component decoupled from the file table — any part of the platform
|
|
170
|
+
can request a view by sending an event, and the viewer just reacts to the state
|
|
171
|
+
change. No direct reference from one component to the other is required.
|
|
172
|
+
|
|
173
|
+
When a viewer group supports multiple files, a second can be added while the first
|
|
174
|
+
is still loading. Each file tracks its own load status independently, so the viewer
|
|
175
|
+
can show a spinner per model without needing its own state management.
|
|
176
|
+
*/
|
|
177
|
+
|
|
178
|
+
log("\n--- view: open and add ---");
|
|
179
|
+
cde.sendMachineEvent({ type: "SELECT", files: [mockFiles[0]] });
|
|
180
|
+
cde.sendMachineEvent({ type: "VIEW" });
|
|
181
|
+
|
|
182
|
+
// The viewer calls this once its geometry is ready — here we simulate it.
|
|
183
|
+
cde.sendMachineEvent({ type: "SET_VIEW_FILE_STATUS", fileId: "f1", status: "ready" });
|
|
184
|
+
|
|
185
|
+
cde.sendMachineEvent({ type: "ADD_TO_VIEW", file: mockFiles[1] });
|
|
186
|
+
|
|
187
|
+
if (cde.machineState.kind === "viewing") {
|
|
188
|
+
log(` files in viewer: ${cde.machineState.files.length}`);
|
|
189
|
+
log(` f1: ${cde.machineState.fileStatus["f1"]}, f2: ${cde.machineState.fileStatus["f2"]}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
cde.sendMachineEvent({ type: "SET_VIEW_FILE_STATUS", fileId: "f2", status: "ready" });
|
|
193
|
+
|
|
194
|
+
/* MD
|
|
195
|
+
Removing the last file from the viewer closes it entirely — the state moves to idle
|
|
196
|
+
rather than back to a selection, since there is nothing left to select. Removing one
|
|
197
|
+
of several files leaves the rest open.
|
|
198
|
+
|
|
199
|
+
Alternatively, closing the viewer explicitly returns to the selection state with the
|
|
200
|
+
same files still highlighted — useful when the user wants to go back to the file
|
|
201
|
+
table without losing which row was active.
|
|
202
|
+
*/
|
|
203
|
+
|
|
204
|
+
log("\n--- view: remove one by one ---");
|
|
205
|
+
cde.sendMachineEvent({ type: "REMOVE_FROM_VIEW", fileId: "f1" });
|
|
206
|
+
log(` after removing f1: ${cde.machineState.kind}`);
|
|
207
|
+
|
|
208
|
+
cde.sendMachineEvent({ type: "REMOVE_FROM_VIEW", fileId: "f2" });
|
|
209
|
+
log(` after removing last: ${cde.machineState.kind}`);
|
|
210
|
+
|
|
211
|
+
log("\n--- view: close keeps selection ---");
|
|
212
|
+
cde.sendMachineEvent({ type: "SELECT", files: [mockFiles[0]] });
|
|
213
|
+
cde.sendMachineEvent({ type: "VIEW" });
|
|
214
|
+
cde.sendMachineEvent({ type: "CLOSE_VIEW" });
|
|
215
|
+
log(` state: ${cde.machineState.kind}`);
|
|
216
|
+
if (cde.machineState.kind === "selected") {
|
|
217
|
+
log(` selection preserved: ${cde.machineState.files.length} file(s)`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* MD
|
|
221
|
+
### 🌐 Download and archive (requires credentials)
|
|
222
|
+
Download has to fetch file content from the backend, and archive needs the account
|
|
223
|
+
to have delete access on the project — so both flows require live credentials. Pass
|
|
224
|
+
them as query parameters to run this section:
|
|
225
|
+
|
|
226
|
+
example.html?accessToken=<token>&apiUrl=https://...&projectId=<id>
|
|
227
|
+
|
|
228
|
+
We create a throwaway file here so the demo is self-contained and leaves no
|
|
229
|
+
permanent changes in the project.
|
|
230
|
+
*/
|
|
231
|
+
|
|
232
|
+
const params = new URLSearchParams(location.search);
|
|
233
|
+
const accessToken = params.get("accessToken");
|
|
234
|
+
const apiUrl = params.get("apiUrl");
|
|
235
|
+
const projectId = params.get("projectId");
|
|
236
|
+
|
|
237
|
+
if (accessToken && apiUrl && projectId) {
|
|
238
|
+
const { EngineServicesClient } = await import("thatopen-services");
|
|
239
|
+
const client = new EngineServicesClient(accessToken, apiUrl);
|
|
240
|
+
// Replace the mock context with the real one.
|
|
241
|
+
cde.ctx = { client, projectId };
|
|
242
|
+
|
|
243
|
+
// loadFolders also resolves user permissions — needed before archive.
|
|
244
|
+
await cde.loadFolders();
|
|
245
|
+
log(`\nProject: "${cde.projectName}", canDelete: ${cde.userPermissions.canDelete}`);
|
|
246
|
+
|
|
247
|
+
// Create a throwaway file so the demo doesn't touch existing project data.
|
|
248
|
+
const blob = new Blob([JSON.stringify({ demo: true })], { type: "application/json" });
|
|
249
|
+
const { item } = await client.createFile({
|
|
250
|
+
file: blob, name: `__example-test__-${Date.now()}.json`, versionTag: "v1", projectId,
|
|
251
|
+
});
|
|
252
|
+
const liveFile: CDEFile = {
|
|
253
|
+
...item,
|
|
254
|
+
createdAt: new Date(item.createdAt),
|
|
255
|
+
createdBy: "",
|
|
256
|
+
metadata: {},
|
|
257
|
+
} as CDEFile;
|
|
258
|
+
cde.files = [liveFile];
|
|
259
|
+
log(`Created test file: ${item._id}`);
|
|
260
|
+
|
|
261
|
+
// --- Download ---
|
|
262
|
+
log("\n--- download ---");
|
|
263
|
+
cde.sendMachineEvent({ type: "SELECT", files: [liveFile] });
|
|
264
|
+
// onDownloadReady fires with { blob, filename } — multiple selected files are zipped.
|
|
265
|
+
await new Promise<void>(resolve => {
|
|
266
|
+
cde.onDownloadReady.add(() => resolve());
|
|
267
|
+
cde.sendMachineEvent({ type: "DOWNLOAD" });
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// --- Archive ---
|
|
271
|
+
// ARCHIVE_FILE is silently ignored if the account lacks delete permission.
|
|
272
|
+
if (cde.userPermissions.canDelete) {
|
|
273
|
+
log("\n--- archive ---");
|
|
274
|
+
cde.sendMachineEvent({ type: "SELECT", files: [liveFile] });
|
|
275
|
+
await new Promise<void>(resolve => {
|
|
276
|
+
cde.onMachineStateChanged.add((s) => {
|
|
277
|
+
if (s.kind === "idle" || s.kind === "error") resolve();
|
|
278
|
+
});
|
|
279
|
+
cde.sendMachineEvent({ type: "ARCHIVE_FILE" });
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
if (cde.machineState.kind === "error") {
|
|
283
|
+
log(` archive failed: ${cde.machineState.reason}`);
|
|
284
|
+
cde.sendMachineEvent({ type: "DISMISS" });
|
|
285
|
+
await client.archiveFile(item._id as string);
|
|
286
|
+
} else {
|
|
287
|
+
log(` archived. Files in cde: ${cde.files.length}`);
|
|
288
|
+
}
|
|
289
|
+
} else {
|
|
290
|
+
log("Archive skipped — no delete permission. Cleaning up directly.");
|
|
291
|
+
await client.archiveFile(item._id as string);
|
|
292
|
+
}
|
|
293
|
+
} else {
|
|
294
|
+
log("\nDownload and archive skipped — pass ?accessToken=&apiUrl=&projectId= to run.");
|
|
295
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/* MD
|
|
2
|
+
## File uploads, versioning, and the download cache
|
|
3
|
+
---
|
|
4
|
+
A site coordinator drops a batch of files onto the CDE every Monday after the
|
|
5
|
+
weekend review: some are entirely new models, some are revised versions of files
|
|
6
|
+
already in the project, and sometimes the metadata group they upload into should
|
|
7
|
+
be stamped automatically onto the new files. If the platform makes the coordinator
|
|
8
|
+
sort this out manually, it slows the handoff and introduces naming errors. And if
|
|
9
|
+
an upload fails halfway through a batch, the file table should land in a safe state
|
|
10
|
+
rather than freeze at a loading spinner.
|
|
11
|
+
|
|
12
|
+
CDEManager's upload method compares each incoming file against the current list by
|
|
13
|
+
base name and extension. Files with no match are created from scratch; files that
|
|
14
|
+
match an existing entry produce a new version instead. When a metadata group is
|
|
15
|
+
active, the group's values are stamped onto every file in the batch without any
|
|
16
|
+
extra action from the user. The state machine moves through a dedicated uploading
|
|
17
|
+
state so any component in the platform can show a progress indicator by reading a
|
|
18
|
+
single source of truth.
|
|
19
|
+
|
|
20
|
+
This tutorial covers uploading new files and reading the result from the
|
|
21
|
+
upload-completed event; uploading a file whose name matches an existing entry and
|
|
22
|
+
confirming it is versioned rather than duplicated; attaching metadata automatically
|
|
23
|
+
when the upload runs inside an active metadata group; recovering cleanly from an
|
|
24
|
+
upload error through the state machine; and reading file content through the
|
|
25
|
+
in-memory cache that avoids re-fetching the same version twice.
|
|
26
|
+
|
|
27
|
+
By the end, you'll have a working demonstration of the full upload lifecycle —
|
|
28
|
+
create, version, auto-metadata, error recovery — plus the cache hit and miss
|
|
29
|
+
behavior that the viewer relies on when loading models.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import * as OBC from "@thatopen/components";
|
|
33
|
+
// In production platform apps, CDEManager is loaded through the platform client:
|
|
34
|
+
// const cde = await client.initBuiltInComponent(CDEManager.uuid, components);
|
|
35
|
+
// Here we import directly from source for local testing.
|
|
36
|
+
import { CDEManager } from "../../index";
|
|
37
|
+
import type { CDEFile } from "../../src/types";
|
|
38
|
+
|
|
39
|
+
/* MD
|
|
40
|
+
### 🛠️ Setting up
|
|
41
|
+
All flows in this example use a minimal mock client so they run without a backend
|
|
42
|
+
connection. Unlike mock flows that clear the client context after loading permissions,
|
|
43
|
+
upload and download operations require an active client throughout — so the context
|
|
44
|
+
stays set for the full example.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
const components = new OBC.Components();
|
|
48
|
+
const cde = components.get(CDEManager);
|
|
49
|
+
|
|
50
|
+
const output = document.getElementById("output") as HTMLPreElement;
|
|
51
|
+
const log = (msg: string) => { console.log(msg); output.textContent += msg + "\n"; };
|
|
52
|
+
|
|
53
|
+
let fileCounter = 0;
|
|
54
|
+
// Track created version records so the versioning demo can find them.
|
|
55
|
+
const mockVersions: Record<string, { tag: string }[]> = {};
|
|
56
|
+
|
|
57
|
+
const mockClient = {
|
|
58
|
+
checkPermissionBatch: async () => Array(5).fill({ hasPermission: true }),
|
|
59
|
+
getProjectData: async () => ({ project: { title: "Demo Project" }, users: [] }),
|
|
60
|
+
listFolders: async () => [],
|
|
61
|
+
createFile: async ({ name, versionTag, parentFolderId }: any) => {
|
|
62
|
+
const id = `file-${fileCounter++}`;
|
|
63
|
+
mockVersions[id] = [{ tag: versionTag }];
|
|
64
|
+
return {
|
|
65
|
+
item: { _id: id, name, folderId: parentFolderId ?? null, createdAt: new Date().toISOString(), createdBy: "mock-user", itemType: "FILE" },
|
|
66
|
+
version: { tag: versionTag },
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
listVersions: async (fileId: string) => mockVersions[fileId] ?? [],
|
|
70
|
+
createVersion: async (fileId: string, _file: File, tag: string) => {
|
|
71
|
+
mockVersions[fileId] = [...(mockVersions[fileId] ?? []), { tag }];
|
|
72
|
+
return { tag };
|
|
73
|
+
},
|
|
74
|
+
updateFileVersionMetadata: async () => ({}),
|
|
75
|
+
downloadFile: async (_id: string) => ({ arrayBuffer: async () => new ArrayBuffer(4096) }),
|
|
76
|
+
archiveFile: async () => ({}),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
cde.ctx = { client: mockClient as any, projectId: "mock" };
|
|
80
|
+
await cde.loadFolders();
|
|
81
|
+
|
|
82
|
+
cde.onMachineStateChanged.add((s) => log(` → ${s.kind}`));
|
|
83
|
+
|
|
84
|
+
/* MD
|
|
85
|
+
### 📤 Uploading new files
|
|
86
|
+
Calling the upload method transitions the machine to an uploading state, carries
|
|
87
|
+
out the operation, then returns to idle and fires a completion event. The event
|
|
88
|
+
payload separates newly created files from versioned ones, so the UI can show a
|
|
89
|
+
targeted confirmation — "2 files added" versus "1 file updated" — without parsing
|
|
90
|
+
the whole file list.
|
|
91
|
+
*/
|
|
92
|
+
|
|
93
|
+
cde.onUploadCompleted.add(({ created, versioned }) => {
|
|
94
|
+
log(` [onUploadCompleted] created: ${created.length}, versioned: ${versioned.length}`);
|
|
95
|
+
for (const f of created) log(` + ${f.name}`);
|
|
96
|
+
for (const f of versioned) log(` ↑ ${f.name}`);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
log("\n--- upload: create new files ---");
|
|
100
|
+
const f1 = new File([new ArrayBuffer(100)], "column.ifc", { type: "application/octet-stream" });
|
|
101
|
+
const f2 = new File([new ArrayBuffer(50)], "report.pdf");
|
|
102
|
+
await cde.uploadFiles([f1, f2]);
|
|
103
|
+
log(` cde.files after upload: ${cde.files.length}`);
|
|
104
|
+
|
|
105
|
+
/* MD
|
|
106
|
+
### 🔄 Versioning an existing file
|
|
107
|
+
When the incoming file's base name and extension match an entry already in the list,
|
|
108
|
+
the upload method creates a new version rather than a duplicate. The file counter in
|
|
109
|
+
the version tag is computed from the existing version history, so the tag always
|
|
110
|
+
follows on from whatever the project already has.
|
|
111
|
+
*/
|
|
112
|
+
|
|
113
|
+
log("\n--- upload: version existing file ---");
|
|
114
|
+
// "column.ifc" is already in cde.files from the previous upload.
|
|
115
|
+
const f1v2 = new File([new ArrayBuffer(120)], "column.ifc", { type: "application/octet-stream" });
|
|
116
|
+
await cde.uploadFiles([f1v2]);
|
|
117
|
+
log(` cde.files after version upload: ${cde.files.length}`);
|
|
118
|
+
|
|
119
|
+
const colFile = cde.files.find(f => f.name === "column.ifc")!;
|
|
120
|
+
log(` column.ifc version tag: ${(colFile as any).versionTag}`);
|
|
121
|
+
|
|
122
|
+
/* MD
|
|
123
|
+
### 🏷️ Auto-metadata from a metadata group
|
|
124
|
+
When metadata grouping is active and a group is selected, every file uploaded in
|
|
125
|
+
that session inherits the group's metadata values automatically. The coordinator
|
|
126
|
+
selects the "Structural SD phase" group, drops files, and every new file arrives
|
|
127
|
+
already tagged — no post-upload editing required.
|
|
128
|
+
*/
|
|
129
|
+
|
|
130
|
+
log("\n--- upload: auto-metadata ---");
|
|
131
|
+
cde.sendMachineEvent({ type: "SET_GROUPS_VIEW_MODE", mode: "metadata" });
|
|
132
|
+
cde.selectedMetadataGroup = { discipline: "STR", phase: "SD" };
|
|
133
|
+
|
|
134
|
+
const f3 = new File([new ArrayBuffer(80)], "foundations-v2.ifc", { type: "application/octet-stream" });
|
|
135
|
+
await cde.uploadFiles([f3]);
|
|
136
|
+
|
|
137
|
+
const uploaded = cde.files.find(f => f.name === "foundations-v2.ifc")!;
|
|
138
|
+
log(` discipline: ${uploaded.metadata.discipline}, phase: ${uploaded.metadata.phase}`);
|
|
139
|
+
|
|
140
|
+
// Reset back to groups view for subsequent sections.
|
|
141
|
+
cde.sendMachineEvent({ type: "SET_GROUPS_VIEW_MODE", mode: "groups" });
|
|
142
|
+
cde.selectedMetadataGroup = null;
|
|
143
|
+
|
|
144
|
+
/* MD
|
|
145
|
+
### ⚠️ Recovering from an upload error
|
|
146
|
+
If the backend rejects the upload, the machine moves to an error state rather than
|
|
147
|
+
silently returning to idle. The error state carries the reason message so the UI can
|
|
148
|
+
surface it, and sending a dismiss event returns to idle so the user can try again.
|
|
149
|
+
No manual state cleanup is needed.
|
|
150
|
+
*/
|
|
151
|
+
|
|
152
|
+
log("\n--- upload: error recovery ---");
|
|
153
|
+
cde.ctx = {
|
|
154
|
+
client: {
|
|
155
|
+
...mockClient,
|
|
156
|
+
createFile: async () => { throw new Error("Upload failed — server unavailable"); },
|
|
157
|
+
} as any,
|
|
158
|
+
projectId: "mock",
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const fBroken = new File([new ArrayBuffer(10)], "broken.ifc", { type: "application/octet-stream" });
|
|
162
|
+
await cde.uploadFiles([fBroken]);
|
|
163
|
+
|
|
164
|
+
if (cde.machineState.kind === "error") {
|
|
165
|
+
log(` error reason: ${cde.machineState.reason}`);
|
|
166
|
+
cde.sendMachineEvent({ type: "DISMISS" });
|
|
167
|
+
}
|
|
168
|
+
log(` state after dismiss: ${cde.machineState.kind}`);
|
|
169
|
+
|
|
170
|
+
// Restore the working mock client.
|
|
171
|
+
cde.ctx = { client: mockClient as any, projectId: "mock" };
|
|
172
|
+
|
|
173
|
+
/* MD
|
|
174
|
+
### 💾 File download cache
|
|
175
|
+
The download method on CDEManager keeps a buffer cache in memory so the viewer
|
|
176
|
+
does not re-fetch the same file version twice during a session. The cache logs each
|
|
177
|
+
access — a miss triggers a download, a hit returns the stored buffer immediately.
|
|
178
|
+
Only extensions in `cacheExtensions` are stored; all others are downloaded every
|
|
179
|
+
time so the cache stays focused on the large binary files that benefit most from it.
|
|
180
|
+
|
|
181
|
+
Note: files uploaded in the same session are already placed in the cache by the
|
|
182
|
+
upload method. The demo below uses a file injected directly — simulating a file
|
|
183
|
+
that arrived from a previous session load — to show a clean miss on the first
|
|
184
|
+
access.
|
|
185
|
+
*/
|
|
186
|
+
|
|
187
|
+
log("\n--- download cache ---");
|
|
188
|
+
|
|
189
|
+
// A file loaded from the backend (not uploaded this session) starts with no cache entry.
|
|
190
|
+
const serverFile = {
|
|
191
|
+
_id: "server-ifc", name: "arch.ifc", folderId: null, versionTag: "v3",
|
|
192
|
+
createdBy: "carol", createdAt: new Date(), metadata: {},
|
|
193
|
+
} as CDEFile;
|
|
194
|
+
cde.files = [...cde.files, serverFile];
|
|
195
|
+
|
|
196
|
+
// First access — cache miss, buffer fetched and stored.
|
|
197
|
+
const buf1 = await cde.downloadFile(serverFile);
|
|
198
|
+
log(` arch.ifc first access (miss): ${buf1.byteLength} bytes`);
|
|
199
|
+
|
|
200
|
+
// Second access — cache hit, no network call.
|
|
201
|
+
const buf2 = await cde.downloadFile(serverFile);
|
|
202
|
+
log(` arch.ifc second access (hit): ${buf2.byteLength} bytes`);
|
|
203
|
+
|
|
204
|
+
// PDF extension is not in cacheExtensions — fetched but not stored.
|
|
205
|
+
const pdfFile = cde.files.find(f => f.name === "report.pdf")!;
|
|
206
|
+
const bufPdf = await cde.downloadFile(pdfFile);
|
|
207
|
+
log(` report.pdf access (skip): ${bufPdf.byteLength} bytes`);
|
|
208
|
+
|
|
209
|
+
log(`\n cacheExtensions: [${[...cde.cacheExtensions].join(", ")}]`);
|
|
210
|
+
log(` cacheMaxBytes: ${(cde.cacheMaxBytes / 1024 / 1024).toFixed(0)} MB`);
|