@itwin/saved-views-react 0.1.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/LICENSE.md +9 -0
- package/README.md +131 -0
- package/lib/LayeredDropdownMenu/LayeredDropdownMenu.css +11 -0
- package/lib/LayeredDropdownMenu/LayeredDropdownMenu.d.ts +39 -0
- package/lib/LayeredDropdownMenu/LayeredDropdownMenu.js +53 -0
- package/lib/SavedView.d.ts +18 -0
- package/lib/SavedView.js +1 -0
- package/lib/SavedViewTile/SavedViewOptions.css +26 -0
- package/lib/SavedViewTile/SavedViewOptions.d.ts +139 -0
- package/lib/SavedViewTile/SavedViewOptions.js +173 -0
- package/lib/SavedViewTile/SavedViewTile.css +89 -0
- package/lib/SavedViewTile/SavedViewTile.d.ts +55 -0
- package/lib/SavedViewTile/SavedViewTile.js +110 -0
- package/lib/SavedViewTile/SavedViewTileContext.d.ts +14 -0
- package/lib/SavedViewTile/SavedViewTileContext.js +20 -0
- package/lib/SavedViewsClient/ITwinSavedViewsClient.d.ts +30 -0
- package/lib/SavedViewsClient/ITwinSavedViewsClient.js +132 -0
- package/lib/SavedViewsClient/SavedViewsClient.d.ts +72 -0
- package/lib/SavedViewsClient/SavedViewsClient.js +1 -0
- package/lib/SavedViewsContext.d.ts +13 -0
- package/lib/SavedViewsContext.js +38 -0
- package/lib/SavedViewsWidget/SavedViewGroupTile/SavedViewGroupOptions.d.ts +9 -0
- package/lib/SavedViewsWidget/SavedViewGroupTile/SavedViewGroupOptions.js +14 -0
- package/lib/SavedViewsWidget/SavedViewGroupTile/SavedViewGroupTile.d.ts +14 -0
- package/lib/SavedViewsWidget/SavedViewGroupTile/SavedViewGroupTile.js +37 -0
- package/lib/SavedViewsWidget/SavedViewGroupTile/SavedViewGroupTileContext.d.ts +14 -0
- package/lib/SavedViewsWidget/SavedViewGroupTile/SavedViewGroupTileContext.js +20 -0
- package/lib/SavedViewsWidget/SavedViewsExpandableBlockWidget.css +50 -0
- package/lib/SavedViewsWidget/SavedViewsExpandableBlockWidget.d.ts +36 -0
- package/lib/SavedViewsWidget/SavedViewsExpandableBlockWidget.js +36 -0
- package/lib/SavedViewsWidget/SavedViewsFolderWidget.d.ts +14 -0
- package/lib/SavedViewsWidget/SavedViewsFolderWidget.js +60 -0
- package/lib/StickyExpandableBlock/StickyExpandableBlock.css +20 -0
- package/lib/StickyExpandableBlock/StickyExpandableBlock.d.ts +29 -0
- package/lib/StickyExpandableBlock/StickyExpandableBlock.js +63 -0
- package/lib/TileGrid/TileGrid.css +28 -0
- package/lib/TileGrid/TileGrid.d.ts +48 -0
- package/lib/TileGrid/TileGrid.js +32 -0
- package/lib/api/clients/IModelQueryClient.d.ts +10 -0
- package/lib/api/clients/IModelQueryClient.js +45 -0
- package/lib/api/clients/ISavedViewsClient.d.ts +9 -0
- package/lib/api/clients/ISavedViewsClient.js +16 -0
- package/lib/api/utilities/SavedViewTypes.d.ts +48 -0
- package/lib/api/utilities/SavedViewTypes.js +1 -0
- package/lib/api/utilities/translation/ModelsAndCategoriesHelper.d.ts +3 -0
- package/lib/api/utilities/translation/ModelsAndCategoriesHelper.js +57 -0
- package/lib/api/utilities/translation/RgbColor.d.ts +29 -0
- package/lib/api/utilities/translation/RgbColor.js +1 -0
- package/lib/api/utilities/translation/SavedViewTranslation.d.ts +22 -0
- package/lib/api/utilities/translation/SavedViewTranslation.js +246 -0
- package/lib/api/utilities/translation/SavedViewsExtensionHandlers.d.ts +13 -0
- package/lib/api/utilities/translation/SavedViewsExtensionHandlers.js +42 -0
- package/lib/api/utilities/translation/clipVectorsExtractor.d.ts +5 -0
- package/lib/api/utilities/translation/clipVectorsExtractor.js +56 -0
- package/lib/api/utilities/translation/displayStyleExtractor.d.ts +17 -0
- package/lib/api/utilities/translation/displayStyleExtractor.js +499 -0
- package/lib/api/utilities/translation/extensionExtractor.d.ts +18 -0
- package/lib/api/utilities/translation/extensionExtractor.js +79 -0
- package/lib/api/utilities/translation/extractionUtilities.d.ts +209 -0
- package/lib/api/utilities/translation/extractionUtilities.js +515 -0
- package/lib/api/utilities/translation/urlConverter.d.ts +7 -0
- package/lib/api/utilities/translation/urlConverter.js +42 -0
- package/lib/api/utilities/translation/viewExtractorSavedViewToLegacySavedView.d.ts +35 -0
- package/lib/api/utilities/translation/viewExtractorSavedViewToLegacySavedView.js +298 -0
- package/lib/experimental.d.ts +4 -0
- package/lib/experimental.js +8 -0
- package/lib/index.d.ts +12 -0
- package/lib/index.js +14 -0
- package/lib/localization.d.ts +52 -0
- package/lib/localization.js +51 -0
- package/lib/ui/viewlist/ModelCategoryOverrideProvider.d.ts +31 -0
- package/lib/ui/viewlist/ModelCategoryOverrideProvider.js +88 -0
- package/lib/useSavedViews.d.ts +52 -0
- package/lib/useSavedViews.js +514 -0
- package/lib/utils.d.ts +1 -0
- package/lib/utils.js +7 -0
- package/package.json +75 -0
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState, } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* Pulls Saved View data from a store and provides means to update and synchronize the data back to it. Interaction with
|
|
5
|
+
* the store is performed via {@linkcode SavedViewsClient} interface which could communicate, for instance, with
|
|
6
|
+
* [iTwin Saved Views API](https://developer.bentley.com/apis/savedviews/overview/) using `ITwinSavedViewsClient`.
|
|
7
|
+
*
|
|
8
|
+
* Note on the current implementation limitations. While the result of the first update action is reflected immediately,
|
|
9
|
+
* subsequent actions are put in a queue and executed serially. This may cause the UI to feel sluggish when user makes
|
|
10
|
+
* changes to Saved Views faster than the Saved Views store can be updated.
|
|
11
|
+
*/
|
|
12
|
+
export function useSavedViews(args) {
|
|
13
|
+
const onUpdateInProgress = useEvent(args.onUpdateInProgress ?? (() => { }));
|
|
14
|
+
const onUpdateComplete = useEvent(args.onUpdateComplete ?? (() => { }));
|
|
15
|
+
// eslint-disable-next-line no-console
|
|
16
|
+
const onUpdateError = useEvent(args.onUpdateError ?? ((error) => console.error(error)));
|
|
17
|
+
const [state, setState] = useState();
|
|
18
|
+
const providerRef = useRef({
|
|
19
|
+
mostRecentState: state,
|
|
20
|
+
actionQueue: [],
|
|
21
|
+
abortController: new AbortController(),
|
|
22
|
+
});
|
|
23
|
+
providerRef.current.mostRecentState = state;
|
|
24
|
+
const [provider, setProvider] = useState();
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const observer = new IntersectionObserver((entries) => {
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
if (!entry.isIntersecting) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const savedViewId = entry.target.dataset.savedViewId;
|
|
32
|
+
if (savedViewId) {
|
|
33
|
+
void (async () => {
|
|
34
|
+
let thumbnailUrl;
|
|
35
|
+
try {
|
|
36
|
+
thumbnailUrl = await args.client.getThumbnailUrl({ savedViewId, signal });
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
thumbnailUrl = undefined;
|
|
40
|
+
}
|
|
41
|
+
if (signal.aborted) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
setState((prev) => {
|
|
45
|
+
if (!prev) {
|
|
46
|
+
return prev;
|
|
47
|
+
}
|
|
48
|
+
const newState = { ...prev };
|
|
49
|
+
newState.savedViews = new Map(prev.savedViews);
|
|
50
|
+
const prevSavedView = prev.savedViews.get(savedViewId);
|
|
51
|
+
if (!prevSavedView) {
|
|
52
|
+
return prev;
|
|
53
|
+
}
|
|
54
|
+
const newSavedView = { ...prevSavedView };
|
|
55
|
+
newSavedView.thumbnail = thumbnailUrl;
|
|
56
|
+
newState.savedViews.set(savedViewId, newSavedView);
|
|
57
|
+
return newState;
|
|
58
|
+
});
|
|
59
|
+
})();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
const abortController = new AbortController();
|
|
64
|
+
const signal = abortController.signal;
|
|
65
|
+
providerRef.current.abortController = abortController;
|
|
66
|
+
providerRef.current.actionQueue = [];
|
|
67
|
+
void (async () => {
|
|
68
|
+
try {
|
|
69
|
+
const result = await args.client.getSavedViewInfo({
|
|
70
|
+
iTwinId: args.iTwinId,
|
|
71
|
+
iModelId: args.iModelId,
|
|
72
|
+
signal: signal,
|
|
73
|
+
});
|
|
74
|
+
if (signal.aborted) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
setState({
|
|
78
|
+
savedViews: new Map(result.savedViews.map((savedView) => {
|
|
79
|
+
if (savedView.thumbnail === undefined) {
|
|
80
|
+
savedView.thumbnail = _jsx(ThumbnailPlaceholder, { savedViewId: savedView.id, observer: observer });
|
|
81
|
+
}
|
|
82
|
+
return [savedView.id, savedView];
|
|
83
|
+
})),
|
|
84
|
+
groups: new Map(result.groups.map((group) => [group.id, group])),
|
|
85
|
+
tags: new Map(result.tags.map((tag) => [tag.id, tag])),
|
|
86
|
+
thumbnails: new Map(),
|
|
87
|
+
});
|
|
88
|
+
setProvider(createSavedViewActions(args.iTwinId, args.iModelId, args.client, setState, providerRef, onUpdateInProgress, onUpdateComplete, onUpdateError));
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (error.name !== "AbortError") {
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
})();
|
|
96
|
+
return () => {
|
|
97
|
+
abortController.abort();
|
|
98
|
+
observer.disconnect();
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
102
|
+
[args.client, args.iModelId, args.iTwinId]);
|
|
103
|
+
if (state === undefined || provider === undefined) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
savedViews: state.savedViews,
|
|
108
|
+
groups: state.groups,
|
|
109
|
+
tags: state.tags,
|
|
110
|
+
actions: provider,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// Loosely based on useEvent proposal https://github.com/reactjs/rfcs/blob/useevent/text/0000-useevent.md
|
|
114
|
+
function useEvent(handleEvent) {
|
|
115
|
+
const handleEventRef = useRef(handleEvent);
|
|
116
|
+
handleEventRef.current = handleEvent;
|
|
117
|
+
return useCallback((...args) => handleEventRef.current(...args), []);
|
|
118
|
+
}
|
|
119
|
+
function createSavedViewActions(iTwinId, iModelId, client, setState, ref, onUpdateInProgress, onUpdateComplete, onUpdateError) {
|
|
120
|
+
const signal = ref.current.abortController.signal;
|
|
121
|
+
return {
|
|
122
|
+
createSavedView: actionWrapper(async (savedViewName, savedViewData) => {
|
|
123
|
+
const savedView = await client.createSavedView({
|
|
124
|
+
iTwinId: iTwinId,
|
|
125
|
+
iModelId: iModelId,
|
|
126
|
+
savedView: {
|
|
127
|
+
displayName: savedViewName,
|
|
128
|
+
},
|
|
129
|
+
savedViewData,
|
|
130
|
+
signal,
|
|
131
|
+
});
|
|
132
|
+
updateSavedViews((savedViews) => savedViews.set(savedView.id, savedView));
|
|
133
|
+
}),
|
|
134
|
+
renameSavedView: actionWrapper(async (savedViewId, newName) => {
|
|
135
|
+
let prevName;
|
|
136
|
+
updateSavedView(savedViewId, (savedView) => {
|
|
137
|
+
prevName = savedView.displayName;
|
|
138
|
+
savedView.displayName = newName;
|
|
139
|
+
});
|
|
140
|
+
try {
|
|
141
|
+
const savedView = await client.updateSavedView({
|
|
142
|
+
savedView: { id: savedViewId, displayName: newName },
|
|
143
|
+
signal,
|
|
144
|
+
});
|
|
145
|
+
updateSavedView(savedViewId, () => savedView);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
if (prevName !== undefined) {
|
|
149
|
+
const restoredDisplayName = prevName;
|
|
150
|
+
updateSavedView(savedViewId, (savedView) => {
|
|
151
|
+
savedView.displayName = restoredDisplayName;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}),
|
|
157
|
+
shareSavedView: actionWrapper(async (savedViewId, share) => {
|
|
158
|
+
const savedView = ref.current.mostRecentState.savedViews.get(savedViewId);
|
|
159
|
+
if (!savedView || savedView.shared === share) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
updateSavedView(savedViewId, (savedView) => {
|
|
163
|
+
savedView.shared = share;
|
|
164
|
+
});
|
|
165
|
+
try {
|
|
166
|
+
const savedView = await client.updateSavedView({ savedView: { id: savedViewId, shared: share }, signal });
|
|
167
|
+
updateSavedView(savedView.id, () => savedView);
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
updateSavedView(savedViewId, (savedView) => {
|
|
171
|
+
savedView.shared = !share;
|
|
172
|
+
});
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
}),
|
|
176
|
+
deleteSavedView: actionWrapper(async (savedViewId) => {
|
|
177
|
+
let prevSavedView;
|
|
178
|
+
updateSavedViews((savedViews) => {
|
|
179
|
+
prevSavedView = savedViews.get(savedViewId);
|
|
180
|
+
savedViews.delete(savedViewId);
|
|
181
|
+
});
|
|
182
|
+
try {
|
|
183
|
+
await client.deleteSavedView({ savedViewId, signal });
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
if (prevSavedView !== undefined) {
|
|
187
|
+
const restoredSavedView = prevSavedView;
|
|
188
|
+
updateSavedViews((savedViews) => {
|
|
189
|
+
// The deleted view will return at the last position in the enumeration
|
|
190
|
+
savedViews.set(savedViewId, restoredSavedView);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}),
|
|
195
|
+
createGroup: actionWrapper(async (groupName) => {
|
|
196
|
+
const group = await client.createGroup({
|
|
197
|
+
iTwinId: iTwinId,
|
|
198
|
+
iModelId: iModelId,
|
|
199
|
+
group: { displayName: groupName },
|
|
200
|
+
signal,
|
|
201
|
+
});
|
|
202
|
+
updateGroups((groups) => groups.set(group.id, group));
|
|
203
|
+
}),
|
|
204
|
+
renameGroup: actionWrapper(async (groupId, newName) => {
|
|
205
|
+
let prevName;
|
|
206
|
+
updateGroup(groupId, (group) => {
|
|
207
|
+
prevName = group.displayName;
|
|
208
|
+
group.displayName = newName;
|
|
209
|
+
});
|
|
210
|
+
try {
|
|
211
|
+
const group = await client.updateGroup({ group: { id: groupId, displayName: newName }, signal });
|
|
212
|
+
updateGroup(group.id, () => group);
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
if (prevName !== undefined) {
|
|
216
|
+
const restoredDisplayName = prevName;
|
|
217
|
+
updateGroup(groupId, (group) => {
|
|
218
|
+
group.displayName = restoredDisplayName;
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
}),
|
|
224
|
+
shareGroup: actionWrapper(async (groupId, share) => {
|
|
225
|
+
const group = ref.current.mostRecentState.groups.get(groupId);
|
|
226
|
+
if (!group || group.shared === share) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
updateGroup(groupId, (group) => {
|
|
230
|
+
group.shared = share;
|
|
231
|
+
});
|
|
232
|
+
try {
|
|
233
|
+
const group = await client.updateGroup({ group: { id: groupId, shared: share }, signal });
|
|
234
|
+
updateGroup(group.id, () => group);
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
updateGroup(groupId, (group) => {
|
|
238
|
+
group.shared = !share;
|
|
239
|
+
});
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
}),
|
|
243
|
+
moveToGroup: actionWrapper(async (savedViewId, groupId) => {
|
|
244
|
+
let prevGroupId;
|
|
245
|
+
updateSavedView(savedViewId, (savedView) => {
|
|
246
|
+
prevGroupId = savedView.groupId;
|
|
247
|
+
savedView.groupId = groupId;
|
|
248
|
+
});
|
|
249
|
+
try {
|
|
250
|
+
const savedView = await client.updateSavedView({ savedView: { id: savedViewId, groupId }, signal });
|
|
251
|
+
updateSavedView(savedView.id, () => savedView);
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
const restoredGroupId = prevGroupId;
|
|
255
|
+
updateSavedView(savedViewId, (savedView) => {
|
|
256
|
+
savedView.groupId = restoredGroupId;
|
|
257
|
+
});
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
}),
|
|
261
|
+
moveToNewGroup: actionWrapper(async (savedViewId, groupName) => {
|
|
262
|
+
const group = await client.createGroup({
|
|
263
|
+
iTwinId: iTwinId,
|
|
264
|
+
iModelId: iModelId,
|
|
265
|
+
group: { displayName: groupName },
|
|
266
|
+
signal,
|
|
267
|
+
});
|
|
268
|
+
if (signal.aborted) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
updateGroups((groups) => groups.set(group.id, group));
|
|
272
|
+
let prevGroupId;
|
|
273
|
+
updateSavedView(savedViewId, (savedView) => {
|
|
274
|
+
prevGroupId = savedView.groupId;
|
|
275
|
+
savedView.groupId = group.id;
|
|
276
|
+
});
|
|
277
|
+
try {
|
|
278
|
+
const savedView = await client.updateSavedView({ savedView: { id: savedViewId, groupId: group.id }, signal });
|
|
279
|
+
updateSavedView(savedView.id, () => savedView);
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
updateSavedView(savedViewId, (savedView) => {
|
|
283
|
+
savedView.groupId = prevGroupId;
|
|
284
|
+
});
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
}),
|
|
288
|
+
deleteGroup: actionWrapper(async (groupId) => {
|
|
289
|
+
let prevGroup;
|
|
290
|
+
updateGroups((groups) => {
|
|
291
|
+
prevGroup = groups.get(groupId);
|
|
292
|
+
groups.delete(groupId);
|
|
293
|
+
});
|
|
294
|
+
try {
|
|
295
|
+
await client.deleteGroup({ groupId, signal });
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
if (prevGroup) {
|
|
299
|
+
const restoredGroup = prevGroup;
|
|
300
|
+
updateGroups((groups) => {
|
|
301
|
+
// The deleted group will return at the last position in the enumeration
|
|
302
|
+
groups.set(groupId, restoredGroup);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
}),
|
|
308
|
+
addTag: actionWrapper(async (savedViewId, tagId) => {
|
|
309
|
+
let savedView = ref.current.mostRecentState.savedViews.get(savedViewId);
|
|
310
|
+
if (!savedView) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const tagIds = savedView.tagIds?.slice() ?? [];
|
|
314
|
+
tagIds.push(tagId);
|
|
315
|
+
tagIds.sort((a, b) => {
|
|
316
|
+
const aDisplayName = ref.current.mostRecentState.tags.get(a)?.displayName;
|
|
317
|
+
const bDisplayName = ref.current.mostRecentState.tags.get(b)?.displayName;
|
|
318
|
+
return aDisplayName?.toLowerCase().localeCompare(bDisplayName?.toLowerCase() ?? "") ?? -1;
|
|
319
|
+
});
|
|
320
|
+
let prevTagIds;
|
|
321
|
+
updateSavedView(savedViewId, (savedView) => {
|
|
322
|
+
prevTagIds = savedView.tagIds;
|
|
323
|
+
savedView.tagIds = tagIds;
|
|
324
|
+
});
|
|
325
|
+
try {
|
|
326
|
+
savedView = await client.updateSavedView({ savedView: { id: savedViewId, tagIds }, signal });
|
|
327
|
+
updateSavedView(savedView.id, () => savedView);
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
updateSavedView(savedViewId, (savedView) => {
|
|
331
|
+
savedView.tagIds = prevTagIds;
|
|
332
|
+
});
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
335
|
+
}),
|
|
336
|
+
addNewTag: actionWrapper(async (savedViewId, tagName) => {
|
|
337
|
+
let savedView = ref.current.mostRecentState.savedViews.get(savedViewId);
|
|
338
|
+
if (!savedView) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const tag = await client.createTag({
|
|
342
|
+
iTwinId: iTwinId,
|
|
343
|
+
iModelId: iModelId,
|
|
344
|
+
displayName: tagName,
|
|
345
|
+
signal,
|
|
346
|
+
});
|
|
347
|
+
updateTags((tags) => {
|
|
348
|
+
tags.set(tag.id, tag);
|
|
349
|
+
});
|
|
350
|
+
const tagIds = savedView.tagIds?.slice() ?? [];
|
|
351
|
+
tagIds.push(tag.id);
|
|
352
|
+
tagIds.sort((a, b) => {
|
|
353
|
+
const aDisplayName = ref.current.mostRecentState.tags.get(a)?.displayName;
|
|
354
|
+
const bDisplayName = ref.current.mostRecentState.tags.get(b)?.displayName;
|
|
355
|
+
return aDisplayName?.toLowerCase().localeCompare(bDisplayName?.toLowerCase() ?? "") ?? -1;
|
|
356
|
+
});
|
|
357
|
+
let prevTagIds;
|
|
358
|
+
updateSavedView(savedViewId, (savedView) => {
|
|
359
|
+
prevTagIds = savedView.tagIds;
|
|
360
|
+
savedView.tagIds = tagIds;
|
|
361
|
+
});
|
|
362
|
+
try {
|
|
363
|
+
savedView = await client.updateSavedView({ savedView: { id: savedViewId, tagIds }, signal });
|
|
364
|
+
updateSavedView(savedView.id, () => savedView);
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
updateSavedView(savedViewId, (savedView) => {
|
|
368
|
+
savedView.tagIds = prevTagIds;
|
|
369
|
+
});
|
|
370
|
+
throw error;
|
|
371
|
+
}
|
|
372
|
+
}),
|
|
373
|
+
removeTag: actionWrapper(async (savedViewId, tagId) => {
|
|
374
|
+
let savedView = ref.current.mostRecentState.savedViews.get(savedViewId);
|
|
375
|
+
if (!savedView) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const tagIds = (savedView.tagIds ?? []).filter((id) => id !== tagId);
|
|
379
|
+
if (tagIds.length === (savedView.tagIds?.length ?? 0)) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
let prevTagIds;
|
|
383
|
+
updateSavedView(savedViewId, (savedView) => {
|
|
384
|
+
prevTagIds = savedView.tagIds;
|
|
385
|
+
savedView.tagIds = tagIds;
|
|
386
|
+
});
|
|
387
|
+
try {
|
|
388
|
+
savedView = await client.updateSavedView({ savedView: { id: savedViewId, tagIds }, signal });
|
|
389
|
+
updateSavedView(savedView.id, () => savedView);
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
updateSavedView(savedViewId, (savedView) => {
|
|
393
|
+
savedView.tagIds = prevTagIds;
|
|
394
|
+
});
|
|
395
|
+
throw error;
|
|
396
|
+
}
|
|
397
|
+
}),
|
|
398
|
+
};
|
|
399
|
+
/** Serializes action execution and notifies when action processing begins and ends. Silences cancellation errors. */
|
|
400
|
+
function actionWrapper(callback) {
|
|
401
|
+
return async (...args) => {
|
|
402
|
+
ref.current.actionQueue.push(async () => {
|
|
403
|
+
if (signal.aborted) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
await callback(...args);
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
if (error && typeof error === "object" && "name" in error && error.name === "AbortError") {
|
|
411
|
+
// It's a cancellation error
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
throw error;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
// If there are no other queued actions, start executing the queue
|
|
419
|
+
if (ref.current.actionQueue.length === 1) {
|
|
420
|
+
onUpdateInProgress();
|
|
421
|
+
// By the time the first action completes, other actions may have been queued
|
|
422
|
+
while (ref.current.actionQueue.length > 0) {
|
|
423
|
+
try {
|
|
424
|
+
await ref.current.actionQueue[0]();
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
onUpdateError(error);
|
|
428
|
+
}
|
|
429
|
+
finally {
|
|
430
|
+
ref.current.actionQueue.shift();
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
onUpdateComplete();
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function updateSavedViews(callback) {
|
|
438
|
+
if (signal.aborted) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
setState((prev) => {
|
|
442
|
+
const store = { ...prev };
|
|
443
|
+
store.savedViews = new Map(prev.savedViews);
|
|
444
|
+
callback(store.savedViews);
|
|
445
|
+
return store;
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
function updateSavedView(savedViewId, callback) {
|
|
449
|
+
if (signal.aborted) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
setState((prev) => {
|
|
453
|
+
const store = { ...prev };
|
|
454
|
+
store.savedViews = new Map(prev.savedViews);
|
|
455
|
+
const prevSavedView = store.savedViews.get(savedViewId);
|
|
456
|
+
if (!prevSavedView) {
|
|
457
|
+
return prev;
|
|
458
|
+
}
|
|
459
|
+
const savedView = { ...prevSavedView };
|
|
460
|
+
store.savedViews.set(savedViewId, callback(savedView) ?? savedView);
|
|
461
|
+
return store;
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
function updateGroups(callback) {
|
|
465
|
+
if (signal.aborted) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
setState((prev) => {
|
|
469
|
+
const store = { ...prev };
|
|
470
|
+
store.groups = new Map(prev.groups);
|
|
471
|
+
callback(store.groups);
|
|
472
|
+
return store;
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
function updateGroup(groupId, callback) {
|
|
476
|
+
if (signal.aborted) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
setState((prev) => {
|
|
480
|
+
const store = { ...prev };
|
|
481
|
+
store.groups = new Map(prev.groups);
|
|
482
|
+
const prevGroup = store.groups.get(groupId);
|
|
483
|
+
if (!prevGroup) {
|
|
484
|
+
return prev;
|
|
485
|
+
}
|
|
486
|
+
const group = { ...prevGroup };
|
|
487
|
+
store.groups.set(groupId, callback(group) ?? group);
|
|
488
|
+
return store;
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
function updateTags(callback) {
|
|
492
|
+
if (signal.aborted) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
setState((prev) => {
|
|
496
|
+
const store = { ...prev };
|
|
497
|
+
store.tags = new Map(prev.tags);
|
|
498
|
+
callback(store.tags);
|
|
499
|
+
return store;
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
function ThumbnailPlaceholder(props) {
|
|
504
|
+
const divRef = useRef(null);
|
|
505
|
+
useEffect(() => {
|
|
506
|
+
const div = divRef.current;
|
|
507
|
+
if (!div) {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
props.observer.observe(div);
|
|
511
|
+
return () => props.observer.unobserve(div);
|
|
512
|
+
}, [props.observer]);
|
|
513
|
+
return _jsx("div", { ref: divRef, "data-saved-view-id": props.savedViewId });
|
|
514
|
+
}
|
package/lib/utils.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function trimInputString(string: string): string;
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/*---------------------------------------------------------------------------------------------
|
|
2
|
+
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
|
|
3
|
+
* See LICENSE.md in the project root for license terms and full copyright notice.
|
|
4
|
+
*--------------------------------------------------------------------------------------------*/
|
|
5
|
+
export function trimInputString(string) {
|
|
6
|
+
return string.replace(/\s+/g, " ");
|
|
7
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@itwin/saved-views-react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/iTwin/saved-views.git",
|
|
8
|
+
"directory": "packages/saved-views-react"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/iTwin/saved-views",
|
|
11
|
+
"author": {
|
|
12
|
+
"name": "Bentley Systems, Inc.",
|
|
13
|
+
"url": "https://www.bentley.com"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./experimental": {
|
|
22
|
+
"types": "./lib/experimental.d.ts",
|
|
23
|
+
"default": "./lib/experimental.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"./lib",
|
|
28
|
+
"./public"
|
|
29
|
+
],
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@itwin/core-bentley": "^4.0.6",
|
|
32
|
+
"@itwin/core-common": "^4.0.6",
|
|
33
|
+
"@itwin/core-frontend": "^4.0.6",
|
|
34
|
+
"@itwin/core-geometry": "^4.0.6",
|
|
35
|
+
"@testing-library/react": "^12.1.5",
|
|
36
|
+
"@types/react": "^17.0.39",
|
|
37
|
+
"@types/recursive-readdir": "^2.2.2",
|
|
38
|
+
"@vitejs/plugin-react-swc": "^3.5.0",
|
|
39
|
+
"@vitest/coverage-v8": "^1.0.4",
|
|
40
|
+
"cpx2": "^5.0.0",
|
|
41
|
+
"happy-dom": "^10.5.2",
|
|
42
|
+
"npm-run-all": "^4.1.5",
|
|
43
|
+
"postcss-cli": "^10.1.0",
|
|
44
|
+
"postcss-modules": "^6.0.0",
|
|
45
|
+
"react": "^17.0.0",
|
|
46
|
+
"react-dom": "^17.0.0",
|
|
47
|
+
"recursive-readdir": "^2.2.3",
|
|
48
|
+
"typescript": "^5.1.6",
|
|
49
|
+
"vite": "^5.0.12",
|
|
50
|
+
"vitest": "^1.0.4"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"@itwin/core-common": "^4.0.6",
|
|
54
|
+
"@itwin/core-frontend": "^4.0.6",
|
|
55
|
+
"react": "^17.0.0 || ^18.0.0",
|
|
56
|
+
"react-dom": "^17.0.0 || ^18.0.0"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@itwin/itwinui-icons-react": "^2.4.0",
|
|
60
|
+
"@itwin/itwinui-react": "^3.0.11",
|
|
61
|
+
"fuse.js": "^6.6.2",
|
|
62
|
+
"@itwin/saved-views-client": "0.1.0"
|
|
63
|
+
},
|
|
64
|
+
"scripts": {
|
|
65
|
+
"build": "run-p build:*",
|
|
66
|
+
"build:transpile": "run-s build:transpile:*",
|
|
67
|
+
"build:transpile:client": "cd ../saved-views-client && npm run build",
|
|
68
|
+
"build:transpile:react": "tsc --project ./tsconfig.build.json",
|
|
69
|
+
"build:css": "postcss src/**/*.css --base ./src --dir ./lib",
|
|
70
|
+
"test": "vitest run --passWithNoTests",
|
|
71
|
+
"test:cover": "vitest run --coverage --passWithNoTests",
|
|
72
|
+
"test:watch": "vitest watch",
|
|
73
|
+
"typecheck": "tsc --noEmit"
|
|
74
|
+
}
|
|
75
|
+
}
|