@thatopen/services 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -62
- package/dist/built-in/index.d.ts +66 -41
- package/dist/cli/lib/revit-addin.d.ts +8 -0
- package/dist/cli.js +154 -13
- package/dist/core/client.d.ts +6 -0
- package/dist/core/platform-client.d.ts +139 -0
- package/dist/core/platform-client.live.test.d.ts +1 -0
- package/dist/core/platform-client.notifications.test.d.ts +1 -0
- package/dist/index.cjs.js +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +635 -445
- package/dist/types/notifications.d.ts +54 -0
- package/dist/vite-env.d.ts +1 -1
- package/docs/ai-quickstart.md +120 -1
- package/docs/cli/create.md +29 -29
- package/docs/cli/local-server.md +38 -38
- package/docs/cli/login.md +32 -32
- package/docs/cli/publish.md +31 -31
- package/docs/cli/run.md +26 -26
- package/docs/cli/serve.md +43 -43
- package/docs/cli/swap.md +30 -30
- package/docs/flow-plugin-guide.md +26 -0
- package/docs/revit-collab-quickstart.md +26 -17
- package/docs/rhino-interop-quickstart.md +74 -7
- package/package.json +1 -1
- package/resources/AGENTS.md +40 -40
- package/src/built-in/CDEManager/example.ts +214 -214
- package/src/built-in/CDEManager/examples/FileEditing/example.ts +226 -226
- package/src/built-in/CDEManager/examples/FileMoves/example.ts +186 -186
- package/src/built-in/CDEManager/examples/FileOperations/example.ts +295 -295
- package/src/built-in/CDEManager/examples/FileTransfers/example.ts +210 -210
- package/src/built-in/CDEManager/examples/FolderManagement/example.ts +230 -230
- package/src/built-in/CDEManager/examples/MetadataSchema/example.ts +230 -230
- package/src/built-in/CDEManager/examples/MetadataViews/example.ts +166 -166
- package/src/built-in/CDEManager/examples/ProjectConfig/example.ts +177 -177
- package/src/built-in/UIManager/src/app/example.ts +97 -97
- package/src/built-in/UIManager/src/app/examples/AppState/example.ts +140 -140
- package/src/built-in/UIManager/src/app/examples/Banner/example.ts +159 -159
- package/src/built-in/UIManager/src/app/examples/Contexts/example.ts +123 -123
- package/src/built-in/UIManager/src/app/examples/FileDrop/example.ts +124 -124
- package/src/built-in/UIManager/src/app/examples/FloatingLayout/example.ts +90 -90
- package/src/built-in/UIManager/src/app/examples/Modal/example.ts +163 -163
- package/src/built-in/UIManager/src/app/examples/Notifications/example.ts +133 -133
- package/src/built-in/UIManager/src/app/examples/ProjectFileStorage/example.ts +150 -150
- package/src/built-in/UIManager/src/app/examples/Sidebar/example.ts +102 -102
- package/src/built-in/UIManager/src/clashes-panel/example.ts +183 -183
- package/src/built-in/UIManager/src/git-history-panel/example.ts +70 -70
- package/src/built-in/UIManager/src/models-list/example.ts +122 -122
- package/src/built-in/UIManager/src/topics-panel/example.ts +170 -170
- package/src/built-in/index.ts +66 -41
- package/src/cli/templates/app/index.html +35 -35
- package/src/cli/templates/app/package.json +32 -32
- package/src/cli/templates/app/src/bim-components/CloudRunner/index.ts +95 -95
- package/src/cli/templates/app/src/bim-components/CloudRunner/src/index.ts +1 -1
- package/src/cli/templates/app/src/bim-components/CloudRunner/src/types.ts +5 -5
- package/src/cli/templates/app/src/bim-components/index.ts +1 -1
- package/src/cli/templates/app/src/globals.ts +1 -1
- package/src/cli/templates/app/src/main.history.ts +191 -0
- package/src/cli/templates/app/src/ui-components/app-info-section/index.ts +26 -26
- package/src/cli/templates/app/src/ui-components/app-info-section/src/index.ts +1 -1
- package/src/cli/templates/app/src/ui-components/app-info-section/src/types.ts +15 -15
- package/src/cli/templates/app/src/ui-components/cloud-runner-section/index.ts +37 -37
- package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/index.ts +1 -1
- package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/types.ts +14 -14
- package/src/cli/templates/app/src/ui-components/index.ts +2 -2
- package/src/cli/templates/app/tsconfig.json +16 -16
- package/src/cli/templates/app/vite.config.js +57 -57
- package/src/cli/templates/cloud-component/README.md +205 -205
- package/src/cli/templates/cloud-component/_thatopen +5 -5
- package/src/cli/templates/cloud-component/declarations.json +4 -4
- package/src/cli/templates/cloud-component/package.json +21 -21
- package/src/cli/templates/cloud-component/src/main.ts +70 -70
- package/src/cli/templates/cloud-component/tsconfig.json +16 -16
- package/src/cli/templates/cloud-component/vite.config.js +46 -46
- package/src/cli/templates/shared/AGENTS.md +8 -8
- package/src/cli/templates/shared/CLAUDE.md +1 -1
- package/src/cli/templates/shared/_gitignore +5 -5
|
@@ -1,177 +1,177 @@
|
|
|
1
|
-
/* MD
|
|
2
|
-
## Project configuration — read, write, and reload persistent data
|
|
3
|
-
---
|
|
4
|
-
A CDE application needs to store two kinds of configuration: project-wide settings
|
|
5
|
-
that all team members share (the metadata schema, custom naming rules) and personal
|
|
6
|
-
settings that belong to one user (which columns are visible, which metadata views
|
|
7
|
-
are pinned). Both live inside the project's file storage under a reserved system
|
|
8
|
-
folder, so they travel with the project and don't rely on a separate configuration
|
|
9
|
-
service.
|
|
10
|
-
|
|
11
|
-
CDEManager exposes a simple read/write API that handles the folder structure
|
|
12
|
-
automatically: finding or creating the right system folders, detecting whether a
|
|
13
|
-
file already exists and versioning it instead of duplicating, and returning the
|
|
14
|
-
parsed JSON on read. The metadata schema and user config are pre-wired shortcuts on
|
|
15
|
-
top of this same mechanism — they just target fixed paths.
|
|
16
|
-
|
|
17
|
-
This tutorial covers writing and reading arbitrary JSON under a named application
|
|
18
|
-
namespace; demonstrating that writing the same file twice creates a new version
|
|
19
|
-
rather than a duplicate; loading a user's personal config and verifying the round-
|
|
20
|
-
trip; and writing a metadata schema then loading it back through `loadMetadataSchema`
|
|
21
|
-
to confirm the schema is applied to CDEManager correctly.
|
|
22
|
-
|
|
23
|
-
By the end, you'll see the full persistence layer that backs the schema editor and
|
|
24
|
-
user preferences, and understand how any part of the platform can store structured
|
|
25
|
-
data without managing the folder hierarchy directly.
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
import * as OBC from "@thatopen/components";
|
|
29
|
-
// In production platform apps, CDEManager is loaded through the platform client:
|
|
30
|
-
// const cde = await client.initBuiltInComponent(CDEManager.uuid, components);
|
|
31
|
-
// Here we import directly from source for local testing.
|
|
32
|
-
import { CDEManager } from "../../index";
|
|
33
|
-
|
|
34
|
-
/* MD
|
|
35
|
-
### 🛠️ Setting up
|
|
36
|
-
Project data operations require an active client context throughout — they make real
|
|
37
|
-
API calls to create folders and files. We use a stateful mock that behaves like a
|
|
38
|
-
real backend: folders and files are created on demand and stored in memory, so reads
|
|
39
|
-
always return what was last written.
|
|
40
|
-
*/
|
|
41
|
-
|
|
42
|
-
const components = new OBC.Components();
|
|
43
|
-
const cde = components.get(CDEManager);
|
|
44
|
-
|
|
45
|
-
const output = document.getElementById("output") as HTMLPreElement;
|
|
46
|
-
const log = (msg: string) => { console.log(msg); output.textContent += msg + "\n"; };
|
|
47
|
-
|
|
48
|
-
// In-memory store that simulates the project file storage.
|
|
49
|
-
const store = {
|
|
50
|
-
folders: [] as any[],
|
|
51
|
-
files: [] as any[],
|
|
52
|
-
data: new Map<string, any>(),
|
|
53
|
-
fIdx: 0,
|
|
54
|
-
dIdx: 0,
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const mockClient = {
|
|
58
|
-
checkPermissionBatch: async () => Array(5).fill({ hasPermission: true }),
|
|
59
|
-
getProjectData: async () => ({
|
|
60
|
-
project: { title: "Demo Project" },
|
|
61
|
-
users: [],
|
|
62
|
-
currentUser: { user: { _id: "user-alice" } },
|
|
63
|
-
}),
|
|
64
|
-
listFolders: async ({ parentFolderId }: any) =>
|
|
65
|
-
store.folders.filter(f =>
|
|
66
|
-
parentFolderId !== undefined
|
|
67
|
-
? String(f.parentId) === String(parentFolderId)
|
|
68
|
-
: !f.parentId
|
|
69
|
-
),
|
|
70
|
-
createFolder: async (name: string, parentId?: string) => {
|
|
71
|
-
const folder = { _id: `folder-${store.fIdx++}`, name, parentId: parentId ?? null };
|
|
72
|
-
store.folders.push(folder);
|
|
73
|
-
return folder;
|
|
74
|
-
},
|
|
75
|
-
listFiles: async () => store.files,
|
|
76
|
-
createFile: async ({ file: blob, name, versionTag, parentFolderId }: any) => {
|
|
77
|
-
const id = `doc-${store.dIdx++}`;
|
|
78
|
-
store.data.set(id, JSON.parse(await blob.text()));
|
|
79
|
-
store.files.push({ _id: id, name, folderId: parentFolderId, versionTag });
|
|
80
|
-
return {
|
|
81
|
-
item: { _id: id, name, folderId: parentFolderId, createdAt: new Date().toISOString(), createdBy: "mock", itemType: "FILE" },
|
|
82
|
-
version: { tag: versionTag },
|
|
83
|
-
};
|
|
84
|
-
},
|
|
85
|
-
listVersions: async (fileId: string) => {
|
|
86
|
-
const f = store.files.find(f => f._id === fileId);
|
|
87
|
-
return f ? [{ _id: `v-${fileId}`, tag: f.versionTag ?? "v1" }] : [];
|
|
88
|
-
},
|
|
89
|
-
createVersion: async (fileId: string, blob: any, tag: string) => {
|
|
90
|
-
store.data.set(fileId, JSON.parse(await blob.text()));
|
|
91
|
-
const f = store.files.find(f => f._id === fileId);
|
|
92
|
-
if (f) f.versionTag = tag;
|
|
93
|
-
return { tag };
|
|
94
|
-
},
|
|
95
|
-
downloadFile: async (id: string) => ({
|
|
96
|
-
json: async () => store.data.get(id) ?? null,
|
|
97
|
-
arrayBuffer: async () => new ArrayBuffer(0),
|
|
98
|
-
}),
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
cde.ctx = { client: mockClient as any, projectId: "mock" };
|
|
102
|
-
await cde.loadFolders();
|
|
103
|
-
|
|
104
|
-
/* MD
|
|
105
|
-
### 📝 Writing and reading project data
|
|
106
|
-
`writeProjectData` takes an application namespace and a filename. The first time a
|
|
107
|
-
file is written, it is created under `__project_data/{app}/`. Subsequent writes to
|
|
108
|
-
the same path create a new version instead of a new file, keeping the full history
|
|
109
|
-
while the read path always returns the latest content.
|
|
110
|
-
*/
|
|
111
|
-
|
|
112
|
-
log("\n--- write and read ---");
|
|
113
|
-
const payload = { lastRun: "2026-06-10", status: "ok", count: 42 };
|
|
114
|
-
const tag1 = await cde.writeProjectData("my-app", "run-log.json", payload);
|
|
115
|
-
log(` write v1 tag: ${tag1}`);
|
|
116
|
-
|
|
117
|
-
const read1 = await cde.readProjectData("my-app", "run-log.json");
|
|
118
|
-
log(` read back: status="${read1.status}", count=${read1.count}`);
|
|
119
|
-
|
|
120
|
-
/* MD
|
|
121
|
-
### 🔄 Versioning on repeated writes
|
|
122
|
-
Writing to the same path a second time detects the existing file and creates a new
|
|
123
|
-
version. The read always returns the latest version, so the caller doesn't need to
|
|
124
|
-
track version tags — they just write and read at the same path.
|
|
125
|
-
*/
|
|
126
|
-
|
|
127
|
-
log("\n--- auto-versioning ---");
|
|
128
|
-
const updated = { ...payload, count: 99, lastRun: "2026-06-11" };
|
|
129
|
-
const tag2 = await cde.writeProjectData("my-app", "run-log.json", updated);
|
|
130
|
-
log(` write v2 tag: ${tag2}`);
|
|
131
|
-
|
|
132
|
-
const read2 = await cde.readProjectData("my-app", "run-log.json");
|
|
133
|
-
log(` read after update: count=${read2.count}`);
|
|
134
|
-
|
|
135
|
-
/* MD
|
|
136
|
-
### 👤 User configuration round-trip
|
|
137
|
-
`saveUserConfig` and `loadUserConfig` are thin wrappers around `writeProjectData`
|
|
138
|
-
and `readProjectData` that target a user-specific path (`cde/user_{id}.json`). They
|
|
139
|
-
accept and return the same config shape as the rest of the CDE — visible columns and
|
|
140
|
-
metadata views — so the schema editor and column picker can persist their state
|
|
141
|
-
without knowing the path structure.
|
|
142
|
-
*/
|
|
143
|
-
|
|
144
|
-
log("\n--- user config round-trip ---");
|
|
145
|
-
const userConfig = {
|
|
146
|
-
visibleColumns: ["name", "discipline", "phase", "createdAt"],
|
|
147
|
-
metadataViews: [{ id: "view-str", name: "Structural", fields: ["discipline", "phase"] }],
|
|
148
|
-
};
|
|
149
|
-
await cde.saveUserConfig(userConfig);
|
|
150
|
-
|
|
151
|
-
const loaded = await cde.loadUserConfig();
|
|
152
|
-
log(` visibleColumns: [${loaded?.visibleColumns?.join(", ")}]`);
|
|
153
|
-
log(` views: ${loaded?.metadataViews?.length} view(s)`);
|
|
154
|
-
|
|
155
|
-
/* MD
|
|
156
|
-
### 🗂️ Schema persistence via loadMetadataSchema
|
|
157
|
-
`loadMetadataSchema` reads `cde/config.json` from the project storage and applies
|
|
158
|
-
the `metadataSchema` section to CDEManager. Writing the config file manually (via
|
|
159
|
-
`writeProjectData`) then calling `loadMetadataSchema` is the setup path used by the
|
|
160
|
-
schema editor after a successful save — the same round-trip that happens in
|
|
161
|
-
production when the app boots.
|
|
162
|
-
*/
|
|
163
|
-
|
|
164
|
-
log("\n--- schema persistence ---");
|
|
165
|
-
const schemaPayload = {
|
|
166
|
-
metadataSchema: {
|
|
167
|
-
discipline: { label: "Discipline", type: "string", required: true },
|
|
168
|
-
phase: { label: "Phase", type: "string", required: false },
|
|
169
|
-
zone: { label: "Zone", type: "string", required: false },
|
|
170
|
-
},
|
|
171
|
-
};
|
|
172
|
-
await cde.writeProjectData("cde", "config.json", schemaPayload);
|
|
173
|
-
|
|
174
|
-
await cde.loadMetadataSchema();
|
|
175
|
-
log(` schema keys after load: [${Object.keys(cde.metadataSchema).join(", ")}]`);
|
|
176
|
-
log(` discipline.required: ${cde.metadataSchema["discipline"]?.required}`);
|
|
177
|
-
log(` zone exists: ${"zone" in cde.metadataSchema}`);
|
|
1
|
+
/* MD
|
|
2
|
+
## Project configuration — read, write, and reload persistent data
|
|
3
|
+
---
|
|
4
|
+
A CDE application needs to store two kinds of configuration: project-wide settings
|
|
5
|
+
that all team members share (the metadata schema, custom naming rules) and personal
|
|
6
|
+
settings that belong to one user (which columns are visible, which metadata views
|
|
7
|
+
are pinned). Both live inside the project's file storage under a reserved system
|
|
8
|
+
folder, so they travel with the project and don't rely on a separate configuration
|
|
9
|
+
service.
|
|
10
|
+
|
|
11
|
+
CDEManager exposes a simple read/write API that handles the folder structure
|
|
12
|
+
automatically: finding or creating the right system folders, detecting whether a
|
|
13
|
+
file already exists and versioning it instead of duplicating, and returning the
|
|
14
|
+
parsed JSON on read. The metadata schema and user config are pre-wired shortcuts on
|
|
15
|
+
top of this same mechanism — they just target fixed paths.
|
|
16
|
+
|
|
17
|
+
This tutorial covers writing and reading arbitrary JSON under a named application
|
|
18
|
+
namespace; demonstrating that writing the same file twice creates a new version
|
|
19
|
+
rather than a duplicate; loading a user's personal config and verifying the round-
|
|
20
|
+
trip; and writing a metadata schema then loading it back through `loadMetadataSchema`
|
|
21
|
+
to confirm the schema is applied to CDEManager correctly.
|
|
22
|
+
|
|
23
|
+
By the end, you'll see the full persistence layer that backs the schema editor and
|
|
24
|
+
user preferences, and understand how any part of the platform can store structured
|
|
25
|
+
data without managing the folder hierarchy directly.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import * as OBC from "@thatopen/components";
|
|
29
|
+
// In production platform apps, CDEManager is loaded through the platform client:
|
|
30
|
+
// const cde = await client.initBuiltInComponent(CDEManager.uuid, components);
|
|
31
|
+
// Here we import directly from source for local testing.
|
|
32
|
+
import { CDEManager } from "../../index";
|
|
33
|
+
|
|
34
|
+
/* MD
|
|
35
|
+
### 🛠️ Setting up
|
|
36
|
+
Project data operations require an active client context throughout — they make real
|
|
37
|
+
API calls to create folders and files. We use a stateful mock that behaves like a
|
|
38
|
+
real backend: folders and files are created on demand and stored in memory, so reads
|
|
39
|
+
always return what was last written.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
const components = new OBC.Components();
|
|
43
|
+
const cde = components.get(CDEManager);
|
|
44
|
+
|
|
45
|
+
const output = document.getElementById("output") as HTMLPreElement;
|
|
46
|
+
const log = (msg: string) => { console.log(msg); output.textContent += msg + "\n"; };
|
|
47
|
+
|
|
48
|
+
// In-memory store that simulates the project file storage.
|
|
49
|
+
const store = {
|
|
50
|
+
folders: [] as any[],
|
|
51
|
+
files: [] as any[],
|
|
52
|
+
data: new Map<string, any>(),
|
|
53
|
+
fIdx: 0,
|
|
54
|
+
dIdx: 0,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const mockClient = {
|
|
58
|
+
checkPermissionBatch: async () => Array(5).fill({ hasPermission: true }),
|
|
59
|
+
getProjectData: async () => ({
|
|
60
|
+
project: { title: "Demo Project" },
|
|
61
|
+
users: [],
|
|
62
|
+
currentUser: { user: { _id: "user-alice" } },
|
|
63
|
+
}),
|
|
64
|
+
listFolders: async ({ parentFolderId }: any) =>
|
|
65
|
+
store.folders.filter(f =>
|
|
66
|
+
parentFolderId !== undefined
|
|
67
|
+
? String(f.parentId) === String(parentFolderId)
|
|
68
|
+
: !f.parentId
|
|
69
|
+
),
|
|
70
|
+
createFolder: async (name: string, parentId?: string) => {
|
|
71
|
+
const folder = { _id: `folder-${store.fIdx++}`, name, parentId: parentId ?? null };
|
|
72
|
+
store.folders.push(folder);
|
|
73
|
+
return folder;
|
|
74
|
+
},
|
|
75
|
+
listFiles: async () => store.files,
|
|
76
|
+
createFile: async ({ file: blob, name, versionTag, parentFolderId }: any) => {
|
|
77
|
+
const id = `doc-${store.dIdx++}`;
|
|
78
|
+
store.data.set(id, JSON.parse(await blob.text()));
|
|
79
|
+
store.files.push({ _id: id, name, folderId: parentFolderId, versionTag });
|
|
80
|
+
return {
|
|
81
|
+
item: { _id: id, name, folderId: parentFolderId, createdAt: new Date().toISOString(), createdBy: "mock", itemType: "FILE" },
|
|
82
|
+
version: { tag: versionTag },
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
listVersions: async (fileId: string) => {
|
|
86
|
+
const f = store.files.find(f => f._id === fileId);
|
|
87
|
+
return f ? [{ _id: `v-${fileId}`, tag: f.versionTag ?? "v1" }] : [];
|
|
88
|
+
},
|
|
89
|
+
createVersion: async (fileId: string, blob: any, tag: string) => {
|
|
90
|
+
store.data.set(fileId, JSON.parse(await blob.text()));
|
|
91
|
+
const f = store.files.find(f => f._id === fileId);
|
|
92
|
+
if (f) f.versionTag = tag;
|
|
93
|
+
return { tag };
|
|
94
|
+
},
|
|
95
|
+
downloadFile: async (id: string) => ({
|
|
96
|
+
json: async () => store.data.get(id) ?? null,
|
|
97
|
+
arrayBuffer: async () => new ArrayBuffer(0),
|
|
98
|
+
}),
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
cde.ctx = { client: mockClient as any, projectId: "mock" };
|
|
102
|
+
await cde.loadFolders();
|
|
103
|
+
|
|
104
|
+
/* MD
|
|
105
|
+
### 📝 Writing and reading project data
|
|
106
|
+
`writeProjectData` takes an application namespace and a filename. The first time a
|
|
107
|
+
file is written, it is created under `__project_data/{app}/`. Subsequent writes to
|
|
108
|
+
the same path create a new version instead of a new file, keeping the full history
|
|
109
|
+
while the read path always returns the latest content.
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
log("\n--- write and read ---");
|
|
113
|
+
const payload = { lastRun: "2026-06-10", status: "ok", count: 42 };
|
|
114
|
+
const tag1 = await cde.writeProjectData("my-app", "run-log.json", payload);
|
|
115
|
+
log(` write v1 tag: ${tag1}`);
|
|
116
|
+
|
|
117
|
+
const read1 = await cde.readProjectData("my-app", "run-log.json");
|
|
118
|
+
log(` read back: status="${read1.status}", count=${read1.count}`);
|
|
119
|
+
|
|
120
|
+
/* MD
|
|
121
|
+
### 🔄 Versioning on repeated writes
|
|
122
|
+
Writing to the same path a second time detects the existing file and creates a new
|
|
123
|
+
version. The read always returns the latest version, so the caller doesn't need to
|
|
124
|
+
track version tags — they just write and read at the same path.
|
|
125
|
+
*/
|
|
126
|
+
|
|
127
|
+
log("\n--- auto-versioning ---");
|
|
128
|
+
const updated = { ...payload, count: 99, lastRun: "2026-06-11" };
|
|
129
|
+
const tag2 = await cde.writeProjectData("my-app", "run-log.json", updated);
|
|
130
|
+
log(` write v2 tag: ${tag2}`);
|
|
131
|
+
|
|
132
|
+
const read2 = await cde.readProjectData("my-app", "run-log.json");
|
|
133
|
+
log(` read after update: count=${read2.count}`);
|
|
134
|
+
|
|
135
|
+
/* MD
|
|
136
|
+
### 👤 User configuration round-trip
|
|
137
|
+
`saveUserConfig` and `loadUserConfig` are thin wrappers around `writeProjectData`
|
|
138
|
+
and `readProjectData` that target a user-specific path (`cde/user_{id}.json`). They
|
|
139
|
+
accept and return the same config shape as the rest of the CDE — visible columns and
|
|
140
|
+
metadata views — so the schema editor and column picker can persist their state
|
|
141
|
+
without knowing the path structure.
|
|
142
|
+
*/
|
|
143
|
+
|
|
144
|
+
log("\n--- user config round-trip ---");
|
|
145
|
+
const userConfig = {
|
|
146
|
+
visibleColumns: ["name", "discipline", "phase", "createdAt"],
|
|
147
|
+
metadataViews: [{ id: "view-str", name: "Structural", fields: ["discipline", "phase"] }],
|
|
148
|
+
};
|
|
149
|
+
await cde.saveUserConfig(userConfig);
|
|
150
|
+
|
|
151
|
+
const loaded = await cde.loadUserConfig();
|
|
152
|
+
log(` visibleColumns: [${loaded?.visibleColumns?.join(", ")}]`);
|
|
153
|
+
log(` views: ${loaded?.metadataViews?.length} view(s)`);
|
|
154
|
+
|
|
155
|
+
/* MD
|
|
156
|
+
### 🗂️ Schema persistence via loadMetadataSchema
|
|
157
|
+
`loadMetadataSchema` reads `cde/config.json` from the project storage and applies
|
|
158
|
+
the `metadataSchema` section to CDEManager. Writing the config file manually (via
|
|
159
|
+
`writeProjectData`) then calling `loadMetadataSchema` is the setup path used by the
|
|
160
|
+
schema editor after a successful save — the same round-trip that happens in
|
|
161
|
+
production when the app boots.
|
|
162
|
+
*/
|
|
163
|
+
|
|
164
|
+
log("\n--- schema persistence ---");
|
|
165
|
+
const schemaPayload = {
|
|
166
|
+
metadataSchema: {
|
|
167
|
+
discipline: { label: "Discipline", type: "string", required: true },
|
|
168
|
+
phase: { label: "Phase", type: "string", required: false },
|
|
169
|
+
zone: { label: "Zone", type: "string", required: false },
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
await cde.writeProjectData("cde", "config.json", schemaPayload);
|
|
173
|
+
|
|
174
|
+
await cde.loadMetadataSchema();
|
|
175
|
+
log(` schema keys after load: [${Object.keys(cde.metadataSchema).join(", ")}]`);
|
|
176
|
+
log(` discipline.required: ${cde.metadataSchema["discipline"]?.required}`);
|
|
177
|
+
log(` zone exists: ${"zone" in cde.metadataSchema}`);
|
|
@@ -1,97 +1,97 @@
|
|
|
1
|
-
/* MD
|
|
2
|
-
## top-app
|
|
3
|
-
---
|
|
4
|
-
Front-end developers building BIM web apps need to coordinate several async
|
|
5
|
-
initialization steps before the UI can be shown — loading the engine, preparing
|
|
6
|
-
fragment workers, registering loaders — and users need visible feedback on what
|
|
7
|
-
is happening during that wait, rather than a blank screen.
|
|
8
|
-
|
|
9
|
-
top-app is the platform's root shell element. It manages the full boot sequence,
|
|
10
|
-
tracks each async setup task individually, shows a labelled loading screen until
|
|
11
|
-
every task completes, and then mounts the app's visual layout.
|
|
12
|
-
|
|
13
|
-
This tutorial covers initializing the platform client and registering all
|
|
14
|
-
platform web components; registering two async setup tasks — the fragment worker
|
|
15
|
-
and the IFC loader — each with its own loading-screen label; declaring a named
|
|
16
|
-
layout area as a CSS grid template; wiring a 3D viewer into that layout; and
|
|
17
|
-
using a flag to keep the loading screen visible during development.
|
|
18
|
-
|
|
19
|
-
By the end, you'll have a working app shell that boots the engine, shows a
|
|
20
|
-
step-by-step loading screen, and mounts a 3D viewer once every setup task
|
|
21
|
-
resolves.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import { html } from "lit";
|
|
25
|
-
import * as THREE from "three";
|
|
26
|
-
import * as OBC from "@thatopen/components";
|
|
27
|
-
import * as OBF from "@thatopen/components-front";
|
|
28
|
-
import * as FRAGS from "@thatopen/fragments";
|
|
29
|
-
import * as BUI from "@thatopen/ui";
|
|
30
|
-
import { PlatformClient, UIManager } from "@thatopen/services";
|
|
31
|
-
import type { App } from "./index";
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const client = PlatformClient.fromPlatformContext();
|
|
35
|
-
|
|
36
|
-
// UIManager must be in the setup call: it registers all platform web components
|
|
37
|
-
// (top-app, top-viewer, top-viewer-tools, etc.) before the DOM renders.
|
|
38
|
-
const { components } = (await client.setup(
|
|
39
|
-
{ OBC, OBF, BUI, THREE, FRAGS },
|
|
40
|
-
{ uuid: UIManager.uuid },
|
|
41
|
-
)) as { components: OBC.Components };
|
|
42
|
-
|
|
43
|
-
components.get(UIManager).init();
|
|
44
|
-
|
|
45
|
-
const app = document.createElement("top-app") as unknown as App;
|
|
46
|
-
|
|
47
|
-
// setup runs synchronously and returns { components, client }.
|
|
48
|
-
// waitUntil registers async tasks that appear as labelled steps in the loading screen.
|
|
49
|
-
// All tasks run in parallel; top-app mounts only after every promise resolves.
|
|
50
|
-
app.setup = (waitUntil) => {
|
|
51
|
-
waitUntil(
|
|
52
|
-
(async () => {
|
|
53
|
-
const fragments = components.get(OBC.FragmentsManager);
|
|
54
|
-
const workerUrl = await FRAGS.FragmentsModels.getWorker();
|
|
55
|
-
fragments.init(await FRAGS.toClassicWorker(workerUrl), {
|
|
56
|
-
classicWorker: true,
|
|
57
|
-
});
|
|
58
|
-
})(),
|
|
59
|
-
"Fragments Core",
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
waitUntil(
|
|
63
|
-
(async () => {
|
|
64
|
-
const loader = components.get(OBC.IfcLoader);
|
|
65
|
-
await loader.setup();
|
|
66
|
-
})(),
|
|
67
|
-
"IFC Loader",
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
return { components, client };
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
// Each key must match an area name used in at least one layout template.
|
|
74
|
-
app.elements = {
|
|
75
|
-
viewer: () =>
|
|
76
|
-
html`<top-viewer><top-viewer-tools></top-viewer-tools></top-viewer>`,
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
// CSS grid-template shorthand. Area names are the keys for app.elements.
|
|
80
|
-
// "." creates an empty unnamed cell and is never rendered.
|
|
81
|
-
app.layouts = {
|
|
82
|
-
main: {
|
|
83
|
-
label: "Main",
|
|
84
|
-
icon: "solar:3d-square-bold",
|
|
85
|
-
template: `"viewer" 1fr / 1fr`,
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
app.layout = "main";
|
|
90
|
-
|
|
91
|
-
// forceLoading keeps the boot screen visible even after setup completes.
|
|
92
|
-
// Useful during development to inspect the loading UI without real async tasks.
|
|
93
|
-
// app.forceLoading = true;
|
|
94
|
-
|
|
95
|
-
const container = document.getElementById("that-open-app") ?? document.body;
|
|
96
|
-
container.appendChild(app);
|
|
97
|
-
document.body.style.margin = "0";
|
|
1
|
+
/* MD
|
|
2
|
+
## top-app
|
|
3
|
+
---
|
|
4
|
+
Front-end developers building BIM web apps need to coordinate several async
|
|
5
|
+
initialization steps before the UI can be shown — loading the engine, preparing
|
|
6
|
+
fragment workers, registering loaders — and users need visible feedback on what
|
|
7
|
+
is happening during that wait, rather than a blank screen.
|
|
8
|
+
|
|
9
|
+
top-app is the platform's root shell element. It manages the full boot sequence,
|
|
10
|
+
tracks each async setup task individually, shows a labelled loading screen until
|
|
11
|
+
every task completes, and then mounts the app's visual layout.
|
|
12
|
+
|
|
13
|
+
This tutorial covers initializing the platform client and registering all
|
|
14
|
+
platform web components; registering two async setup tasks — the fragment worker
|
|
15
|
+
and the IFC loader — each with its own loading-screen label; declaring a named
|
|
16
|
+
layout area as a CSS grid template; wiring a 3D viewer into that layout; and
|
|
17
|
+
using a flag to keep the loading screen visible during development.
|
|
18
|
+
|
|
19
|
+
By the end, you'll have a working app shell that boots the engine, shows a
|
|
20
|
+
step-by-step loading screen, and mounts a 3D viewer once every setup task
|
|
21
|
+
resolves.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { html } from "lit";
|
|
25
|
+
import * as THREE from "three";
|
|
26
|
+
import * as OBC from "@thatopen/components";
|
|
27
|
+
import * as OBF from "@thatopen/components-front";
|
|
28
|
+
import * as FRAGS from "@thatopen/fragments";
|
|
29
|
+
import * as BUI from "@thatopen/ui";
|
|
30
|
+
import { PlatformClient, UIManager } from "@thatopen/services";
|
|
31
|
+
import type { App } from "./index";
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
const client = PlatformClient.fromPlatformContext();
|
|
35
|
+
|
|
36
|
+
// UIManager must be in the setup call: it registers all platform web components
|
|
37
|
+
// (top-app, top-viewer, top-viewer-tools, etc.) before the DOM renders.
|
|
38
|
+
const { components } = (await client.setup(
|
|
39
|
+
{ OBC, OBF, BUI, THREE, FRAGS },
|
|
40
|
+
{ uuid: UIManager.uuid },
|
|
41
|
+
)) as { components: OBC.Components };
|
|
42
|
+
|
|
43
|
+
components.get(UIManager).init();
|
|
44
|
+
|
|
45
|
+
const app = document.createElement("top-app") as unknown as App;
|
|
46
|
+
|
|
47
|
+
// setup runs synchronously and returns { components, client }.
|
|
48
|
+
// waitUntil registers async tasks that appear as labelled steps in the loading screen.
|
|
49
|
+
// All tasks run in parallel; top-app mounts only after every promise resolves.
|
|
50
|
+
app.setup = (waitUntil) => {
|
|
51
|
+
waitUntil(
|
|
52
|
+
(async () => {
|
|
53
|
+
const fragments = components.get(OBC.FragmentsManager);
|
|
54
|
+
const workerUrl = await FRAGS.FragmentsModels.getWorker();
|
|
55
|
+
fragments.init(await FRAGS.toClassicWorker(workerUrl), {
|
|
56
|
+
classicWorker: true,
|
|
57
|
+
});
|
|
58
|
+
})(),
|
|
59
|
+
"Fragments Core",
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
waitUntil(
|
|
63
|
+
(async () => {
|
|
64
|
+
const loader = components.get(OBC.IfcLoader);
|
|
65
|
+
await loader.setup();
|
|
66
|
+
})(),
|
|
67
|
+
"IFC Loader",
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
return { components, client };
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Each key must match an area name used in at least one layout template.
|
|
74
|
+
app.elements = {
|
|
75
|
+
viewer: () =>
|
|
76
|
+
html`<top-viewer><top-viewer-tools></top-viewer-tools></top-viewer>`,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// CSS grid-template shorthand. Area names are the keys for app.elements.
|
|
80
|
+
// "." creates an empty unnamed cell and is never rendered.
|
|
81
|
+
app.layouts = {
|
|
82
|
+
main: {
|
|
83
|
+
label: "Main",
|
|
84
|
+
icon: "solar:3d-square-bold",
|
|
85
|
+
template: `"viewer" 1fr / 1fr`,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
app.layout = "main";
|
|
90
|
+
|
|
91
|
+
// forceLoading keeps the boot screen visible even after setup completes.
|
|
92
|
+
// Useful during development to inspect the loading UI without real async tasks.
|
|
93
|
+
// app.forceLoading = true;
|
|
94
|
+
|
|
95
|
+
const container = document.getElementById("that-open-app") ?? document.body;
|
|
96
|
+
container.appendChild(app);
|
|
97
|
+
document.body.style.margin = "0";
|