@itwin/saved-views-react 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +5 -20
  2. package/lib/SavedView.d.ts +20 -12
  3. package/lib/SavedViewTile/SavedViewOptions.js +10 -10
  4. package/lib/SavedViewTile/SavedViewTile.css +8 -1
  5. package/lib/SavedViewTile/SavedViewTile.d.ts +4 -3
  6. package/lib/SavedViewTile/SavedViewTile.js +4 -8
  7. package/lib/SavedViewTile/SavedViewTileContext.d.ts +0 -1
  8. package/lib/SavedViewTile/SavedViewTileContext.js +1 -1
  9. package/lib/SavedViewsClient/ITwinSavedViewsClient.d.ts +20 -22
  10. package/lib/SavedViewsClient/ITwinSavedViewsClient.js +81 -40
  11. package/lib/SavedViewsClient/SavedViewsClient.d.ts +55 -40
  12. package/lib/SavedViewsWidget/SavedViewGroupTile/SavedViewGroupTile.js +2 -3
  13. package/lib/SavedViewsWidget/SavedViewGroupTile/SavedViewGroupTileContext.d.ts +0 -1
  14. package/lib/SavedViewsWidget/SavedViewGroupTile/SavedViewGroupTileContext.js +1 -1
  15. package/lib/SavedViewsWidget/SavedViewsExpandableBlockWidget.d.ts +5 -3
  16. package/lib/SavedViewsWidget/SavedViewsExpandableBlockWidget.js +3 -3
  17. package/lib/SavedViewsWidget/SavedViewsFolderWidget.d.ts +5 -4
  18. package/lib/SavedViewsWidget/SavedViewsFolderWidget.js +7 -7
  19. package/lib/applySavedView.d.ts +33 -31
  20. package/lib/applySavedView.js +58 -23
  21. package/lib/captureSavedViewData.d.ts +12 -16
  22. package/lib/captureSavedViewData.js +76 -62
  23. package/lib/captureSavedViewThumbnail.d.ts +1 -1
  24. package/lib/captureSavedViewThumbnail.js +8 -7
  25. package/lib/createViewState.d.ts +8 -7
  26. package/lib/createViewState.js +16 -24
  27. package/lib/index.d.ts +10 -10
  28. package/lib/index.js +7 -7
  29. package/lib/translation/SavedViewTypes.d.ts +1 -1
  30. package/lib/translation/clipVectorsLegacyExtractor.js +4 -0
  31. package/lib/translation/displayStyleExtractor.js +8 -3
  32. package/lib/translation/extractionUtilities.d.ts +9 -1
  33. package/lib/translation/extractionUtilities.js +16 -0
  34. package/lib/useSavedViews.d.ts +171 -38
  35. package/lib/useSavedViews.js +460 -491
  36. package/lib/utils.d.ts +8 -1
  37. package/lib/utils.js +13 -0
  38. package/package.json +6 -6
@@ -3,127 +3,260 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  * Copyright (c) Bentley Systems, Incorporated. All rights reserved.
4
4
  * See LICENSE.md in the project root for license terms and full copyright notice.
5
5
  *--------------------------------------------------------------------------------------------*/
6
- import { useCallback, useEffect, useRef, useState, } from "react";
6
+ import { useEffect, useMemo, useRef, useState, } from "react";
7
+ import { useControlledState } from "./utils.js";
8
+ // #endregion
9
+ // #region useSavedViews
10
+ const emptyState = Object.freeze({
11
+ savedViews: new Map(),
12
+ savedViewData: new Map(),
13
+ groups: new Map(),
14
+ tags: new Map(),
15
+ thumbnails: new Map(),
16
+ });
7
17
  /**
8
- * Pulls Saved View data from a store and provides means to update and synchronize the data back to it. Interaction with
9
- * the store is performed via {@linkcode SavedViewsClient} interface which could communicate, for instance, with
10
- * [iTwin Saved Views API](https://developer.bentley.com/apis/savedviews/overview/) using `ITwinSavedViewsClient`.
18
+ * Provides basic functionality to help get started with Saved Views.
11
19
  *
12
20
  * @remarks
13
- * Note on the current implementation limitations. While the result of the first update action is reflected immediately,
14
- * subsequent actions are put in a queue and executed serially. This may cause the UI to feel sluggish when user makes
15
- * changes to Saved Views faster than the Saved Views store can be updated.
21
+ * When hook arguments change, it does not automatically clear the {@linkcode UseSavedViewsResult.store}.
22
+ * If you want more control over the state, provide an external store via {@linkcode UseSavedViewsArgs.state}
23
+ * and {@linkcode UseSavedViewsArgs.setState} arguments.
24
+ *
25
+ * @example
26
+ * const { iTwinId, iModelId, client, viewport } = props;
27
+ * const savedViews = useSavedViews({ iTwinId, iModelId, client });
28
+ * const [isLoading, setIsLoading] = useState(true);
29
+ * useEffect(
30
+ * () => {
31
+ * return savedViews.startLoadingData(() => { setIsLoading(false); });
32
+ * },
33
+ * [],
34
+ * );
35
+ *
36
+ * if (isLoading) {
37
+ * return <MyLoadingState />;
38
+ * }
39
+ *
40
+ * const handleOpenSavedView = async (savedViewId) => {
41
+ * const savedViewData = await savedViews.lookupSavedViewData(savedViewId);
42
+ * await applySavedView(iModel, viewport, savedViewData);
43
+ * };
44
+ *
45
+ * return <MySavedViewsWidget savedViews={savedViews} onOpenSavedView={handleOpenSavedView} />;
16
46
  */
17
- export function useSavedViews(args) {
18
- const onUpdateInProgress = useEvent(args.onUpdateInProgress ?? (() => { }));
19
- const onUpdateComplete = useEvent(args.onUpdateComplete ?? (() => { }));
20
- // eslint-disable-next-line no-console
21
- const onUpdateError = useEvent(args.onUpdateError ?? ((error) => console.error(error)));
22
- const [state, setState] = useState();
23
- const providerRef = useRef({
24
- mostRecentState: state,
25
- actionQueue: [],
26
- abortController: new AbortController(),
27
- });
28
- providerRef.current.mostRecentState = state;
29
- const [provider, setProvider] = useState();
30
- useEffect(() => {
31
- const observer = new IntersectionObserver((entries) => {
32
- for (const entry of entries) {
33
- if (!entry.isIntersecting) {
34
- continue;
47
+ export const useSavedViews = Object.assign((args) => {
48
+ const { iTwinId, iModelId, client } = args;
49
+ const [state, setState] = useControlledState(args.state ?? emptyState, args.state, args.setState);
50
+ const stateRef = useRef({ iTwinId, iModelId, client, state, setState });
51
+ stateRef.current = { iTwinId, iModelId, client, state, setState };
52
+ const [events] = useState({
53
+ ...createActions(stateRef),
54
+ startLoadingData: (callback) => {
55
+ const { iTwinId, iModelId, client, setState } = stateRef.current;
56
+ const abortController = new AbortController();
57
+ const signal = abortController.signal;
58
+ const observer = new CustomObserver((savedViewId) => {
59
+ void (async () => {
60
+ const loadSavedViewData = async () => {
61
+ let savedViewData;
62
+ try {
63
+ savedViewData = await client.getSavedViewDataById({ savedViewId, signal });
64
+ }
65
+ catch {
66
+ savedViewData = undefined;
67
+ }
68
+ if (savedViewData && !signal.aborted) {
69
+ setState((prev) => {
70
+ const newState = { ...prev };
71
+ newState.savedViewData = new Map(prev.savedViewData);
72
+ newState.savedViewData.set(savedViewId, savedViewData);
73
+ return newState;
74
+ });
75
+ }
76
+ };
77
+ const loadThumbnail = async () => {
78
+ let thumbnailUrl;
79
+ try {
80
+ thumbnailUrl = await client.getThumbnailUrl({ savedViewId, signal });
81
+ }
82
+ catch {
83
+ thumbnailUrl = undefined;
84
+ }
85
+ if (!signal.aborted) {
86
+ setState((prev) => {
87
+ const newState = { ...prev };
88
+ newState.thumbnails = new Map(prev.thumbnails);
89
+ newState.thumbnails.set(savedViewId, thumbnailUrl && _jsx("img", { src: thumbnailUrl }));
90
+ return newState;
91
+ });
92
+ }
93
+ };
94
+ await Promise.all([loadSavedViewData(), loadThumbnail()]);
95
+ })();
96
+ });
97
+ void (async () => {
98
+ try {
99
+ const result = await getSavedViewInfo(client, iTwinId, iModelId, signal);
100
+ signal.throwIfAborted();
101
+ setState((prev) => {
102
+ const newState = { ...prev };
103
+ newState.savedViews = new Map(prev.savedViews);
104
+ result.savedViews.forEach((savedView) => newState.savedViews.set(savedView.savedViewId, savedView));
105
+ newState.groups = new Map(prev.groups);
106
+ result.groups.forEach((group) => newState.groups.set(group.groupId, group));
107
+ newState.tags = new Map(prev.tags);
108
+ result.tags.forEach((tag) => newState.tags.set(tag.tagId, tag));
109
+ newState.thumbnails = new Map(prev.thumbnails);
110
+ result.savedViews
111
+ .filter(({ savedViewId }) => !prev.thumbnails.has(savedViewId))
112
+ .forEach(({ savedViewId }) => {
113
+ newState.thumbnails.set(savedViewId, _jsx(ThumbnailPlaceholder, { savedViewId: savedViewId, observer: observer }));
114
+ });
115
+ return newState;
116
+ });
117
+ callback?.();
35
118
  }
36
- const savedViewId = entry.target.dataset.savedViewId;
37
- if (savedViewId) {
38
- void (async () => {
119
+ catch (error) {
120
+ if (callback) {
121
+ callback(error);
122
+ }
123
+ else {
124
+ throw error;
125
+ }
126
+ }
127
+ })();
128
+ return () => {
129
+ abortController.abort();
130
+ observer.disconnect();
131
+ };
132
+ },
133
+ });
134
+ return useMemo(() => ({ ...events, store: state }), [events, state]);
135
+ }, (args) => {
136
+ const { iTwinId, iModelId, client } = args;
137
+ const [state, setState] = useControlledState(args.state ?? emptyState, args.state, args.setState);
138
+ const stateRef = useRef({ iTwinId, iModelId, client, state, setState });
139
+ stateRef.current = { iTwinId, iModelId, client, state, setState };
140
+ const [events] = useState({
141
+ ...createActions(stateRef),
142
+ startLoadingData: (callback) => {
143
+ const { iTwinId, iModelId, client, setState } = stateRef.current;
144
+ const abortController = new AbortController();
145
+ const signal = abortController.signal;
146
+ const observer = new CustomObserver((savedViewId) => {
147
+ void (async () => {
148
+ const loadSavedViewData = async () => {
149
+ let savedViewData;
150
+ try {
151
+ savedViewData = await client.getSavedViewDataById({ savedViewId, signal });
152
+ }
153
+ catch {
154
+ savedViewData = undefined;
155
+ }
156
+ if (savedViewData && !signal.aborted) {
157
+ setState((prev) => {
158
+ const newState = { ...prev };
159
+ newState.savedViewData = new Map(prev.savedViewData);
160
+ newState.savedViewData.set(savedViewId, savedViewData);
161
+ return newState;
162
+ });
163
+ }
164
+ };
165
+ const loadThumbnail = async () => {
39
166
  let thumbnailUrl;
40
167
  try {
41
- thumbnailUrl = await args.client.getThumbnailUrl({ savedViewId, signal });
168
+ thumbnailUrl = await client.getThumbnailUrl({ savedViewId, signal });
42
169
  }
43
170
  catch {
44
171
  thumbnailUrl = undefined;
45
172
  }
46
- if (signal.aborted) {
47
- return;
173
+ if (!signal.aborted) {
174
+ setState((prev) => {
175
+ const newState = { ...prev };
176
+ newState.thumbnails = new Map(prev.thumbnails);
177
+ newState.thumbnails.set(savedViewId, thumbnailUrl && _jsx("img", { src: thumbnailUrl }));
178
+ return newState;
179
+ });
48
180
  }
49
- setState((prev) => {
50
- if (!prev) {
51
- return prev;
52
- }
53
- const newState = { ...prev };
54
- newState.savedViews = new Map(prev.savedViews);
55
- const prevSavedView = prev.savedViews.get(savedViewId);
56
- if (!prevSavedView) {
57
- return prev;
58
- }
59
- const newSavedView = { ...prevSavedView };
60
- newSavedView.thumbnail = thumbnailUrl;
61
- newState.savedViews.set(savedViewId, newSavedView);
62
- return newState;
181
+ };
182
+ await Promise.all([loadSavedViewData(), loadThumbnail()]);
183
+ })();
184
+ });
185
+ void (async () => {
186
+ try {
187
+ const result = await getSavedViewInfo(client, iTwinId, iModelId, signal);
188
+ signal.throwIfAborted();
189
+ setState((prev) => {
190
+ const newState = { ...prev };
191
+ newState.savedViews = new Map(prev.savedViews);
192
+ result.savedViews.forEach((savedView) => newState.savedViews.set(savedView.savedViewId, savedView));
193
+ newState.groups = new Map(prev.groups);
194
+ result.groups.forEach((group) => newState.groups.set(group.groupId, group));
195
+ newState.tags = new Map(prev.tags);
196
+ result.tags.forEach((tag) => newState.tags.set(tag.tagId, tag));
197
+ newState.thumbnails = new Map(prev.thumbnails);
198
+ result.savedViews
199
+ .filter(({ savedViewId }) => !prev.thumbnails.has(savedViewId))
200
+ .forEach(({ savedViewId }) => {
201
+ newState.thumbnails.set(savedViewId, _jsx(ThumbnailPlaceholder, { savedViewId: savedViewId, observer: observer }));
63
202
  });
64
- })();
203
+ return newState;
204
+ });
205
+ callback?.();
65
206
  }
66
- }
67
- });
68
- const abortController = new AbortController();
69
- const signal = abortController.signal;
70
- providerRef.current.abortController = abortController;
71
- providerRef.current.actionQueue = [];
72
- void (async () => {
73
- try {
74
- const result = await getSavedViewInfo(args.client, args.iTwinId, args.iModelId, signal);
75
- if (signal.aborted) {
76
- return;
207
+ catch (error) {
208
+ if (callback) {
209
+ callback(error);
210
+ }
211
+ else {
212
+ throw error;
213
+ }
77
214
  }
78
- setState({
79
- savedViews: new Map(result.savedViews.map((savedView) => [
80
- savedView.id,
81
- {
82
- ...savedView,
83
- thumbnail: savedView.thumbnail
84
- ?? _jsx(ThumbnailPlaceholder, { savedViewId: savedView.id, observer: observer }),
85
- },
86
- ])),
87
- groups: new Map(result.groups.map((group) => [group.id, group])),
88
- tags: new Map(result.tags.map((tag) => [tag.id, tag])),
89
- thumbnails: new Map(),
90
- });
91
- setProvider(createSavedViewActions(args.iTwinId, args.iModelId, args.client, setState, providerRef, onUpdateInProgress, onUpdateComplete, onUpdateError));
92
- }
93
- catch (error) {
94
- if (!isAbortError(error)) {
95
- throw error;
215
+ })();
216
+ return () => {
217
+ abortController.abort();
218
+ observer.disconnect();
219
+ };
220
+ },
221
+ });
222
+ return useMemo(() => ({ ...events, store: state }), [events, state]);
223
+ }, {
224
+ /** Suggested initial state of custom Saved View stores. Immutable. */
225
+ emptyState,
226
+ });
227
+ class CustomObserver {
228
+ #observer;
229
+ #observedElements = new Map();
230
+ constructor(onObserved) {
231
+ this.#observer = new IntersectionObserver((entries) => {
232
+ for (const entry of entries) {
233
+ if (!entry.isIntersecting) {
234
+ continue;
235
+ }
236
+ const savedViewId = this.#observedElements.get(entry.target);
237
+ if (savedViewId) {
238
+ onObserved(savedViewId);
96
239
  }
97
240
  }
98
- })();
99
- return () => {
100
- abortController.abort();
101
- observer.disconnect();
102
- };
103
- },
104
- // eslint-disable-next-line react-hooks/exhaustive-deps
105
- [args.client, args.iModelId, args.iTwinId]);
106
- if (state === undefined || provider === undefined) {
107
- return undefined;
241
+ });
242
+ }
243
+ observe(savedViewId, element) {
244
+ this.#observedElements.set(element, savedViewId);
245
+ this.#observer.observe(element);
246
+ }
247
+ unobserve(element) {
248
+ this.#observer.unobserve(element);
249
+ this.#observedElements.delete(element);
250
+ }
251
+ disconnect() {
252
+ this.#observer.disconnect();
108
253
  }
109
- return {
110
- savedViews: state.savedViews,
111
- groups: state.groups,
112
- tags: state.tags,
113
- actions: provider,
114
- };
115
- }
116
- // Loosely based on useEvent proposal https://github.com/reactjs/rfcs/blob/useevent/text/0000-useevent.md
117
- function useEvent(handleEvent) {
118
- const handleEventRef = useRef(handleEvent);
119
- handleEventRef.current = handleEvent;
120
- return useCallback((...args) => handleEventRef.current(...args), []);
121
254
  }
122
255
  async function getSavedViewInfo(client, iTwinId, iModelId, signal) {
123
256
  const args = { iTwinId, iModelId, signal };
124
257
  const collectSavedViews = async () => {
125
258
  let savedViews = [];
126
- const iterable = client.getAllSavedViews(args);
259
+ const iterable = client.getSavedViews(args);
127
260
  for await (const page of iterable) {
128
261
  savedViews = savedViews.concat(page);
129
262
  }
@@ -131,427 +264,263 @@ async function getSavedViewInfo(client, iTwinId, iModelId, signal) {
131
264
  };
132
265
  const [savedViews, groups, tags] = await Promise.all([
133
266
  collectSavedViews(),
134
- client.getAllGroups(args),
135
- client.getAllTags(args),
267
+ client.getGroups(args),
268
+ client.getTags(args),
136
269
  ]);
137
- return { savedViews, groups, tags };
270
+ const comparator = (a, b) => a.displayName.localeCompare(b.displayName);
271
+ return {
272
+ savedViews: savedViews.sort(comparator),
273
+ groups: groups.sort(comparator),
274
+ tags: tags.sort(comparator),
275
+ };
276
+ }
277
+ function ThumbnailPlaceholder(props) {
278
+ const { savedViewId, observer } = props;
279
+ const divRef = useRef(null);
280
+ useEffect(() => {
281
+ const div = divRef.current;
282
+ if (!div) {
283
+ return;
284
+ }
285
+ observer.observe(savedViewId, div);
286
+ return () => observer.unobserve(div);
287
+ }, [savedViewId, observer]);
288
+ return _jsx("div", { ref: divRef });
138
289
  }
139
- function createSavedViewActions(iTwinId, iModelId, client, setState, ref, onUpdateInProgress, onUpdateComplete, onUpdateError) {
140
- const signal = ref.current.abortController.signal;
290
+ // #endregion
291
+ // #region createActions
292
+ function createActions(stateRef) {
293
+ // When an action is invoked, stateRef contains the most recent parameters that were passed to
294
+ // useSavedViews hook. We need to ensure that the same parameters are used throughout the
295
+ // operation, so we capture their current values at the beginning of each action.
141
296
  return {
142
- submitSavedView: actionWrapper(async (savedView) => {
143
- const newSavedView = "id" in savedView
144
- ? await client.updateSavedView({ savedView, signal })
145
- : await client.createSavedView({ iTwinId, iModelId, savedView, signal });
146
- updateSavedViews((savedViews) => {
147
- const entries = Array.from(savedViews.values());
148
- entries.push(newSavedView);
149
- entries.sort((a, b) => a.displayName.localeCompare(b.displayName));
150
- return new Map(entries.map((savedView) => [savedView.id, savedView]));
297
+ createSavedView: async (savedView, savedViewData) => {
298
+ const { iTwinId, iModelId, client, setState } = stateRef.current;
299
+ const newSavedView = await client.createSavedView({ ...savedView, iTwinId, iModelId, savedViewData });
300
+ setState((prev) => {
301
+ const savedViews = Array.from(prev.savedViews.values());
302
+ savedViews.splice(savedViews.findIndex(({ savedViewId }) => savedViewId === newSavedView.savedViewId), 1, newSavedView);
303
+ savedViews.sort((a, b) => a.displayName.localeCompare(b.displayName));
304
+ const newState = { ...prev };
305
+ newState.savedViews = new Map(savedViews.map((savedView) => [savedView.savedViewId, savedView]));
306
+ return newState;
151
307
  });
152
- return newSavedView.id;
153
- }),
154
- renameSavedView: actionWrapper(async (savedViewId, newName) => {
155
- if (!newName) {
156
- return;
157
- }
158
- const savedView = ref.current.mostRecentState.savedViews.get(savedViewId);
159
- if (!savedView || newName === savedView.displayName) {
160
- return;
161
- }
162
- let prevName;
163
- updateSavedView(savedViewId, (savedView) => {
164
- prevName = savedView.displayName;
165
- savedView.displayName = newName;
308
+ return newSavedView.savedViewId;
309
+ },
310
+ lookupSavedViewData: async (savedViewId) => {
311
+ const { client, setState } = stateRef.current;
312
+ const savedViewData = await client.getSavedViewDataById({ savedViewId });
313
+ setState((prev) => {
314
+ const newState = { ...prev };
315
+ newState.savedViewData = new Map(prev.savedViewData);
316
+ newState.savedViewData.set(savedViewId, savedViewData);
317
+ return newState;
166
318
  });
167
- try {
168
- await client.updateSavedView({
169
- savedView: { id: savedViewId, displayName: newName },
170
- signal,
171
- });
172
- }
173
- catch (error) {
174
- if (prevName !== undefined) {
175
- const restoredDisplayName = prevName;
176
- updateSavedView(savedViewId, (savedView) => {
177
- savedView.displayName = restoredDisplayName;
178
- });
179
- }
180
- throw error;
181
- }
182
- }),
183
- shareSavedView: actionWrapper(async (savedViewId, share) => {
184
- const savedView = ref.current.mostRecentState.savedViews.get(savedViewId);
185
- if (!savedView || savedView.shared === share) {
186
- return;
187
- }
188
- updateSavedView(savedViewId, (savedView) => {
189
- savedView.shared = share;
319
+ return savedViewData;
320
+ },
321
+ updateSavedView: async (savedViewId, savedView, savedViewData) => {
322
+ const { client, setState } = stateRef.current;
323
+ const updatedSavedView = await client.updateSavedView({ ...savedView, savedViewId, savedViewData });
324
+ setState((prev) => {
325
+ const savedViews = Array.from(prev.savedViews.values());
326
+ savedViews.splice(savedViews.findIndex(({ savedViewId }) => savedViewId === updatedSavedView.savedViewId), 1, updatedSavedView);
327
+ savedViews.sort((a, b) => a.displayName.localeCompare(b.displayName));
328
+ const newState = { ...prev };
329
+ newState.savedViews = new Map(savedViews.map((savedView) => [savedView.savedViewId, savedView]));
330
+ return newState;
190
331
  });
191
- try {
192
- await client.updateSavedView({ savedView: { id: savedViewId, shared: share }, signal });
193
- }
194
- catch (error) {
195
- updateSavedView(savedViewId, (savedView) => {
196
- savedView.shared = !share;
197
- });
198
- throw error;
199
- }
200
- }),
201
- deleteSavedView: actionWrapper(async (savedViewId) => {
202
- let prevSavedViews;
203
- updateSavedViews((savedViews) => {
204
- prevSavedViews = new Map(savedViews);
205
- savedViews.delete(savedViewId);
332
+ },
333
+ renameSavedView: async (savedViewId, newName) => {
334
+ const { client, setState } = stateRef.current;
335
+ await client.updateSavedView({ savedViewId, displayName: newName });
336
+ setState((prev) => {
337
+ const savedView = prev.savedViews.get(savedViewId);
338
+ if (!savedView) {
339
+ return prev;
340
+ }
341
+ const newSavedView = { ...savedView };
342
+ newSavedView.displayName = newName;
343
+ const newState = { ...prev };
344
+ newState.savedViews = new Map(prev.savedViews);
345
+ newState.savedViews.set(savedViewId, newSavedView);
346
+ savedView.displayName = newName;
347
+ return newState;
206
348
  });
207
- try {
208
- await client.deleteSavedView({ savedViewId, signal });
209
- }
210
- catch (error) {
211
- if (prevSavedViews !== undefined) {
212
- updateSavedViews(() => prevSavedViews);
349
+ },
350
+ shareSavedView: async (savedViewId, shared) => {
351
+ const { client, setState } = stateRef.current;
352
+ await client.updateSavedView({ savedViewId, shared });
353
+ setState((prev) => {
354
+ const savedView = prev.savedViews.get(savedViewId);
355
+ if (!savedView) {
356
+ return prev;
213
357
  }
214
- throw error;
215
- }
216
- }),
217
- createGroup: actionWrapper(async (groupName) => {
218
- const group = await client.createGroup({
219
- iTwinId: iTwinId,
220
- iModelId: iModelId,
221
- group: { displayName: groupName },
222
- signal,
358
+ const newSavedView = { ...savedView };
359
+ newSavedView.shared = shared;
360
+ const newState = { ...prev };
361
+ newState.savedViews = new Map(prev.savedViews);
362
+ newState.savedViews.set(savedViewId, newSavedView);
363
+ return newState;
223
364
  });
224
- updateGroups((groups) => {
225
- const entries = Array.from(groups.values());
226
- entries.push(group);
227
- entries.sort((a, b) => a.displayName.localeCompare(b.displayName));
228
- return new Map(entries.map((group) => [group.id, group]));
365
+ },
366
+ deleteSavedView: async (savedViewId) => {
367
+ const { client, setState } = stateRef.current;
368
+ await client.deleteSavedView({ savedViewId });
369
+ setState((prev) => {
370
+ const newState = { ...prev };
371
+ newState.savedViews = new Map(prev.savedViews);
372
+ newState.savedViews.delete(savedViewId);
373
+ return newState;
229
374
  });
230
- }),
231
- renameGroup: actionWrapper(async (groupId, newName) => {
232
- let prevName;
233
- updateGroup(groupId, (group) => {
234
- prevName = group.displayName;
235
- group.displayName = newName;
375
+ },
376
+ createGroup: async (groupName) => {
377
+ const { iTwinId, iModelId, client, setState } = stateRef.current;
378
+ const newGroup = await client.createGroup({ iTwinId, iModelId, displayName: groupName });
379
+ setState((prev) => {
380
+ const groups = Array.from(prev.groups.values());
381
+ groups.push(newGroup);
382
+ groups.sort((a, b) => a.displayName.localeCompare(b.displayName));
383
+ const newState = { ...prev };
384
+ newState.groups = new Map(groups.map((group) => [group.groupId, group]));
385
+ return newState;
236
386
  });
237
- try {
238
- const group = await client.updateGroup({ group: { id: groupId, displayName: newName }, signal });
239
- updateGroup(group.id, () => group);
240
- }
241
- catch (error) {
242
- if (prevName !== undefined) {
243
- const restoredDisplayName = prevName;
244
- updateGroup(groupId, (group) => {
245
- group.displayName = restoredDisplayName;
246
- });
387
+ return newGroup.groupId;
388
+ },
389
+ renameGroup: async (groupId, newName) => {
390
+ const { client, setState } = stateRef.current;
391
+ await client.updateGroup({ groupId, displayName: newName });
392
+ setState((prev) => {
393
+ const group = prev.groups.get(groupId);
394
+ if (!group) {
395
+ return prev;
247
396
  }
248
- throw error;
249
- }
250
- }),
251
- shareGroup: actionWrapper(async (groupId, share) => {
252
- const group = ref.current.mostRecentState.groups.get(groupId);
253
- if (!group || group.shared === share) {
254
- return;
255
- }
256
- updateGroup(groupId, (group) => {
257
- group.shared = share;
397
+ const newGroup = { ...group };
398
+ newGroup.displayName = newName;
399
+ const newState = { ...prev };
400
+ newState.groups = new Map(prev.groups);
401
+ newState.groups.set(groupId, newGroup);
402
+ return newState;
258
403
  });
259
- try {
260
- const group = await client.updateGroup({ group: { id: groupId, shared: share }, signal });
261
- updateGroup(group.id, () => group);
262
- }
263
- catch (error) {
264
- updateGroup(groupId, (group) => {
265
- group.shared = !share;
266
- });
267
- throw error;
268
- }
269
- }),
270
- moveToGroup: actionWrapper(async (savedViewId, groupId) => {
271
- let prevGroupId;
272
- updateSavedView(savedViewId, (savedView) => {
273
- prevGroupId = savedView.groupId;
274
- savedView.groupId = groupId;
404
+ },
405
+ shareGroup: async (groupId, shared) => {
406
+ const { client, setState } = stateRef.current;
407
+ await client.updateGroup({ groupId, shared });
408
+ setState((prev) => {
409
+ const group = prev.groups.get(groupId);
410
+ if (!group) {
411
+ return prev;
412
+ }
413
+ const newGroup = { ...group };
414
+ newGroup.shared = shared;
415
+ const newState = { ...prev };
416
+ newState.groups = new Map(prev.groups);
417
+ newState.groups.set(groupId, newGroup);
418
+ return newState;
275
419
  });
276
- try {
277
- await client.updateSavedView({ savedView: { id: savedViewId, groupId }, signal });
278
- }
279
- catch (error) {
280
- const restoredGroupId = prevGroupId;
281
- updateSavedView(savedViewId, (savedView) => {
282
- savedView.groupId = restoredGroupId;
283
- });
284
- throw error;
285
- }
286
- }),
287
- moveToNewGroup: actionWrapper(async (savedViewId, groupName) => {
288
- const group = await client.createGroup({
289
- iTwinId: iTwinId,
290
- iModelId: iModelId,
291
- group: { displayName: groupName },
292
- signal,
420
+ },
421
+ moveToGroup: async (savedViewId, groupId) => {
422
+ const { client, setState } = stateRef.current;
423
+ await client.updateSavedView({ savedViewId, groupId });
424
+ setState((prev) => {
425
+ const savedView = prev.savedViews.get(savedViewId);
426
+ if (!savedView) {
427
+ return prev;
428
+ }
429
+ const newSavedView = { ...savedView };
430
+ newSavedView.groupId = groupId;
431
+ const newState = { ...prev };
432
+ newState.savedViews = new Map(prev.savedViews);
433
+ newState.savedViews.set(savedViewId, newSavedView);
434
+ return newState;
293
435
  });
294
- if (signal.aborted) {
295
- return;
296
- }
297
- updateGroups((groups) => groups.set(group.id, group));
298
- let prevGroupId;
299
- updateSavedView(savedViewId, (savedView) => {
300
- prevGroupId = savedView.groupId;
301
- savedView.groupId = group.id;
436
+ },
437
+ deleteGroup: async (groupId) => {
438
+ const { client, setState } = stateRef.current;
439
+ await client.deleteGroup({ groupId });
440
+ setState((prev) => {
441
+ const newState = { ...prev };
442
+ newState.groups = new Map(prev.groups);
443
+ newState.groups.delete(groupId);
444
+ return newState;
302
445
  });
303
- try {
304
- await client.updateSavedView({ savedView: { id: savedViewId, groupId: group.id }, signal });
305
- }
306
- catch (error) {
307
- updateSavedView(savedViewId, (savedView) => {
308
- savedView.groupId = prevGroupId;
309
- });
310
- throw error;
311
- }
312
- }),
313
- deleteGroup: actionWrapper(async (groupId) => {
314
- let prevGroups;
315
- updateGroups((groups) => {
316
- prevGroups = new Map(groups);
317
- groups.delete(groupId);
446
+ },
447
+ createTag: async (tagName) => {
448
+ const { iTwinId, iModelId, client, setState } = stateRef.current;
449
+ const newTag = await client.createTag({ iTwinId, iModelId, displayName: tagName });
450
+ setState((prev) => {
451
+ const newState = { ...prev };
452
+ newState.tags = new Map(prev.tags);
453
+ newState.tags.set(newTag.tagId, newTag);
454
+ return newState;
318
455
  });
319
- try {
320
- await client.deleteGroup({ groupId, signal });
321
- }
322
- catch (error) {
323
- if (prevGroups) {
324
- updateGroups(() => prevGroups);
325
- }
326
- throw error;
327
- }
328
- }),
329
- addTag: actionWrapper(async (savedViewId, tagId) => {
330
- const savedView = ref.current.mostRecentState.savedViews.get(savedViewId);
456
+ return newTag.tagId;
457
+ },
458
+ addTag: async (savedViewId, tagId) => {
459
+ const { client, state, setState } = stateRef.current;
460
+ const savedView = state.savedViews.get(savedViewId);
331
461
  if (!savedView) {
332
462
  return;
333
463
  }
334
464
  const tagIds = savedView.tagIds?.slice() ?? [];
335
465
  tagIds.push(tagId);
336
- tagIds.sort((a, b) => {
337
- const aDisplayName = ref.current.mostRecentState.tags.get(a)?.displayName;
338
- const bDisplayName = ref.current.mostRecentState.tags.get(b)?.displayName;
339
- return aDisplayName?.toLowerCase().localeCompare(bDisplayName?.toLowerCase() ?? "") ?? -1;
340
- });
341
- let prevTagIds;
342
- updateSavedView(savedViewId, (savedView) => {
343
- prevTagIds = savedView.tagIds;
344
- savedView.tagIds = tagIds;
345
- });
346
- try {
347
- await client.updateSavedView({ savedView: { id: savedViewId, tagIds }, signal });
348
- }
349
- catch (error) {
350
- updateSavedView(savedViewId, (savedView) => {
351
- savedView.tagIds = prevTagIds;
352
- });
353
- throw error;
354
- }
355
- }),
356
- addNewTag: actionWrapper(async (savedViewId, tagName) => {
357
- const savedView = ref.current.mostRecentState.savedViews.get(savedViewId);
358
- if (!savedView) {
359
- return;
360
- }
361
- const tag = await client.createTag({
362
- iTwinId: iTwinId,
363
- iModelId: iModelId,
364
- displayName: tagName,
365
- signal,
366
- });
367
- updateTags((tags) => {
368
- tags.set(tag.id, tag);
369
- });
370
- const tagIds = savedView.tagIds?.slice() ?? [];
371
- tagIds.push(tag.id);
372
- tagIds.sort((a, b) => {
373
- const aDisplayName = ref.current.mostRecentState.tags.get(a)?.displayName;
374
- const bDisplayName = ref.current.mostRecentState.tags.get(b)?.displayName;
375
- return aDisplayName?.toLowerCase().localeCompare(bDisplayName?.toLowerCase() ?? "") ?? -1;
376
- });
377
- let prevTagIds;
378
- updateSavedView(savedViewId, (savedView) => {
379
- prevTagIds = savedView.tagIds;
380
- savedView.tagIds = tagIds;
466
+ await client.updateSavedView({ savedViewId, tagIds });
467
+ setState((prev) => {
468
+ const savedView = prev.savedViews.get(savedViewId);
469
+ if (!savedView) {
470
+ return prev;
471
+ }
472
+ const newSavedView = { ...savedView };
473
+ newSavedView.tagIds = savedView.tagIds?.slice() ?? [];
474
+ newSavedView.tagIds.push(tagId);
475
+ const newState = { ...prev };
476
+ newState.savedViews = new Map(prev.savedViews);
477
+ newState.savedViews.set(savedViewId, newSavedView);
478
+ return newState;
381
479
  });
382
- try {
383
- await client.updateSavedView({ savedView: { id: savedViewId, tagIds }, signal });
384
- }
385
- catch (error) {
386
- updateSavedView(savedViewId, (savedView) => {
387
- savedView.tagIds = prevTagIds;
388
- });
389
- throw error;
390
- }
391
- }),
392
- removeTag: actionWrapper(async (savedViewId, tagId) => {
393
- const savedView = ref.current.mostRecentState.savedViews.get(savedViewId);
394
- if (!savedView) {
480
+ },
481
+ removeTag: async (savedViewId, tagId) => {
482
+ const { client, state, setState } = stateRef.current;
483
+ const savedView = state.savedViews.get(savedViewId);
484
+ if (!savedView || !savedView.tagIds) {
395
485
  return;
396
486
  }
397
- const tagIds = (savedView.tagIds ?? []).filter((id) => id !== tagId);
398
- if (tagIds.length === (savedView.tagIds?.length ?? 0)) {
487
+ const newTagIds = savedView.tagIds.splice(savedView.tagIds.indexOf(tagId), 1) ?? [];
488
+ if (newTagIds.length === savedView.tagIds.length) {
399
489
  return;
400
490
  }
401
- let prevTagIds;
402
- updateSavedView(savedViewId, (savedView) => {
403
- prevTagIds = savedView.tagIds;
404
- savedView.tagIds = tagIds;
491
+ await client.updateSavedView({ savedViewId, tagIds: newTagIds });
492
+ setState((prev) => {
493
+ const savedView = prev.savedViews.get(savedViewId);
494
+ if (!savedView) {
495
+ return prev;
496
+ }
497
+ const newSavedView = { ...savedView };
498
+ newSavedView.tagIds = newTagIds;
499
+ const newState = { ...prev };
500
+ newState.savedViews = new Map(prev.savedViews);
501
+ newState.savedViews.set(savedViewId, newSavedView);
502
+ return newState;
405
503
  });
406
- try {
407
- await client.updateSavedView({ savedView: { id: savedViewId, tagIds }, signal });
408
- }
409
- catch (error) {
410
- updateSavedView(savedViewId, (savedView) => {
411
- savedView.tagIds = prevTagIds;
412
- });
413
- throw error;
414
- }
415
- }),
416
- uploadThumbnail: actionWrapper(async (savedViewId, imageDataUrl) => {
417
- let prevThumnbnail;
418
- updateSavedView(savedViewId, (savedView) => {
419
- prevThumnbnail = savedView.thumbnail;
420
- savedView.thumbnail = imageDataUrl;
504
+ },
505
+ deleteTag: async (tagId) => {
506
+ const { client, setState } = stateRef.current;
507
+ await client.deleteTag({ tagId });
508
+ setState((prev) => {
509
+ const newState = { ...prev };
510
+ newState.tags = new Map(prev.tags);
511
+ newState.tags.delete(tagId);
512
+ return newState;
421
513
  });
422
- try {
423
- await client.uploadThumbnail({ savedViewId, image: imageDataUrl, signal });
424
- }
425
- catch (error) {
426
- if (prevThumnbnail !== undefined) {
427
- updateSavedView(savedViewId, (savedView) => {
428
- savedView.thumbnail = prevThumnbnail;
429
- });
430
- }
431
- throw error;
432
- }
433
- }),
434
- };
435
- /**
436
- * Serializes action execution and notifies when action processing begins and ends. Continues to execute actions
437
- * after cancellation.
438
- */
439
- function actionWrapper(callback) {
440
- return async (...args) => {
441
- // eslint-disable-next-line no-async-promise-executor
442
- return new Promise(async (resolve, reject) => {
443
- ref.current.actionQueue.push(async () => {
444
- if (signal.aborted) {
445
- reject(signal.reason);
446
- return;
447
- }
448
- try {
449
- resolve(await callback(...args));
450
- }
451
- catch (error) {
452
- reject(error);
453
- if (isAbortError(error)) {
454
- // It's a cancellation error, no need to report it
455
- }
456
- else {
457
- try {
458
- onUpdateError(error);
459
- }
460
- catch { }
461
- }
462
- }
463
- });
464
- // If there are no other queued actions, start executing the queue
465
- if (ref.current.actionQueue.length === 1) {
466
- onUpdateInProgress();
467
- // By the time the first action completes, other actions may have been queued
468
- while (ref.current.actionQueue.length > 0) {
469
- await ref.current.actionQueue[0]();
470
- ref.current.actionQueue.shift();
471
- }
472
- onUpdateComplete();
473
- }
514
+ },
515
+ uploadThumbnail: async (savedViewId, imageDataUrl) => {
516
+ const { client, setState } = stateRef.current;
517
+ await client.uploadThumbnail({ savedViewId, image: imageDataUrl });
518
+ setState((prev) => {
519
+ const newState = { ...prev };
520
+ newState.thumbnails = new Map(prev.thumbnails);
521
+ newState.thumbnails.set(savedViewId, _jsx("img", { src: imageDataUrl }));
522
+ return newState;
474
523
  });
475
- };
476
- }
477
- function updateSavedViews(callback) {
478
- if (signal.aborted) {
479
- return;
480
- }
481
- setState((prev) => {
482
- const store = { ...prev };
483
- const savedViews = new Map(prev.savedViews);
484
- store.savedViews = callback(savedViews) ?? savedViews;
485
- return store;
486
- });
487
- }
488
- function updateSavedView(savedViewId, callback) {
489
- if (signal.aborted) {
490
- return;
491
- }
492
- setState((prev) => {
493
- const store = { ...prev };
494
- store.savedViews = new Map(prev.savedViews);
495
- const prevSavedView = store.savedViews.get(savedViewId);
496
- if (!prevSavedView) {
497
- return prev;
498
- }
499
- const savedView = { ...prevSavedView };
500
- store.savedViews.set(savedViewId, callback(savedView) ?? savedView);
501
- return store;
502
- });
503
- }
504
- function updateGroups(callback) {
505
- if (signal.aborted) {
506
- return;
507
- }
508
- setState((prev) => {
509
- const store = { ...prev };
510
- const groups = new Map(prev.groups);
511
- store.groups = callback(groups) ?? groups;
512
- return store;
513
- });
514
- }
515
- function updateGroup(groupId, callback) {
516
- if (signal.aborted) {
517
- return;
518
- }
519
- setState((prev) => {
520
- const store = { ...prev };
521
- store.groups = new Map(prev.groups);
522
- const prevGroup = store.groups.get(groupId);
523
- if (!prevGroup) {
524
- return prev;
525
- }
526
- const group = { ...prevGroup };
527
- store.groups.set(groupId, callback(group) ?? group);
528
- return store;
529
- });
530
- }
531
- function updateTags(callback) {
532
- if (signal.aborted) {
533
- return;
534
- }
535
- setState((prev) => {
536
- const store = { ...prev };
537
- store.tags = new Map(prev.tags);
538
- callback(store.tags);
539
- return store;
540
- });
541
- }
542
- }
543
- function isAbortError(error) {
544
- return error instanceof DOMException && error.name === "AbortError";
545
- }
546
- function ThumbnailPlaceholder(props) {
547
- const divRef = useRef(null);
548
- useEffect(() => {
549
- const div = divRef.current;
550
- if (!div) {
551
- return;
552
- }
553
- props.observer.observe(div);
554
- return () => props.observer.unobserve(div);
555
- }, [props.observer]);
556
- return _jsx("div", { ref: divRef, "data-saved-view-id": props.savedViewId });
524
+ },
525
+ };
557
526
  }