From 0dba95e71b63c2c9c11329ec9ec90e81d22806a2 Mon Sep 17 00:00:00 2001 From: Michel Dastous Date: Tue, 22 Feb 2022 15:36:22 -0500 Subject: [PATCH 01/10] Allow blank connections applications use map-layers widget 's UserPreferences --- .../map-layers/src/MapLayerPreferences.ts | 12 ++--- .../src/test/MapLayerSettingsSerivce.test.ts | 54 +++++++++++++++++++ .../map-layers/src/test/MapUrlDialog.test.tsx | 27 ++++++++++ .../src/test/UserPreferencesMock.ts | 44 ++++++++++----- .../map-layers/src/ui/widget/MapUrlDialog.tsx | 40 ++++++++------ 5 files changed, 143 insertions(+), 34 deletions(-) diff --git a/extensions/map-layers/src/MapLayerPreferences.ts b/extensions/map-layers/src/MapLayerPreferences.ts index b1be541770b5..29ba15206823 100644 --- a/extensions/map-layers/src/MapLayerPreferences.ts +++ b/extensions/map-layers/src/MapLayerPreferences.ts @@ -52,7 +52,7 @@ export class MapLayerPreferences { * @param source source to be stored on the setting service * @param storeOnIModel if true store the settings object on the model, if false store it on the project */ - public static async storeSource(source: MapLayerSource, storeOnIModel: boolean, iTwinId: GuidString, iModelId: GuidString): Promise { + public static async storeSource(source: MapLayerSource, storeOnIModel?: boolean, iTwinId?: GuidString, iModelId?: GuidString): Promise { if (!MapLayersUI.iTwinConfig) return false; const accessToken = undefined !== IModelApp.authorizationClient ? (await IModelApp.authorizationClient.getAccessToken()) : undefined; @@ -91,7 +91,7 @@ export class MapLayerPreferences { * @param projectId * @param iModelId */ - public static async replaceSource(oldSource: MapLayerSource, newSource: MapLayerSource, projectId: GuidString, iModelId: GuidString): Promise { + public static async replaceSource(oldSource: MapLayerSource, newSource: MapLayerSource, projectId?: GuidString, iModelId?: GuidString): Promise { if (!MapLayersUI.iTwinConfig) return; const accessToken = undefined !== IModelApp.authorizationClient ? (await IModelApp.authorizationClient.getAccessToken()) : undefined; @@ -139,7 +139,7 @@ export class MapLayerPreferences { * @param iTwinId * @param iModelId */ - public static async deleteByName(source: MapLayerSource, iTwinId: GuidString, iModelId: GuidString): Promise { + public static async deleteByName(source: MapLayerSource, iTwinId?: GuidString, iModelId?: GuidString): Promise { if (!MapLayersUI.iTwinConfig) return; const accessToken = undefined !== IModelApp.authorizationClient ? (await IModelApp.authorizationClient.getAccessToken()) : undefined; @@ -177,7 +177,7 @@ export class MapLayerPreferences { * @param iModelId * @param storeOnIModel */ - private static async delete(url: string, name: string, iTwinId: GuidString, iModelId: GuidString, storeOnIModel: boolean): Promise { + private static async delete(url: string, name: string, iTwinId?: GuidString, iModelId?: GuidString, storeOnIModel?: boolean): Promise { if (!MapLayersUI.iTwinConfig) return true; const accessToken = undefined !== IModelApp.authorizationClient ? (await IModelApp.authorizationClient.getAccessToken()) : undefined; @@ -261,7 +261,7 @@ export class MapLayerPreferences { * @param projectId * @param iModelId */ - public static async getByUrl(url: string, projectId: string, iModelId?: string): Promise { + public static async getByUrl(url: string, projectId?: string, iModelId?: string): Promise { if (!MapLayersUI.iTwinConfig) return undefined; @@ -292,7 +292,7 @@ export class MapLayerPreferences { * @param iModelId id of the iModel * @throws if any of the calls to grab settings fail. */ - public static async getSources(projectId: GuidString, iModelId: GuidString): Promise { + public static async getSources(projectId?: GuidString, iModelId?: GuidString): Promise { if (!MapLayersUI.iTwinConfig) return []; const accessToken = undefined !== IModelApp.authorizationClient ? (await IModelApp.authorizationClient.getAccessToken()) : undefined; diff --git a/extensions/map-layers/src/test/MapLayerSettingsSerivce.test.ts b/extensions/map-layers/src/test/MapLayerSettingsSerivce.test.ts index 8302609906db..f594c699e22a 100644 --- a/extensions/map-layers/src/test/MapLayerSettingsSerivce.test.ts +++ b/extensions/map-layers/src/test/MapLayerSettingsSerivce.test.ts @@ -38,6 +38,11 @@ describe("MapLayerPreferences", () => { let sources = await MapLayerPreferences.getSources(iTwinId, iModelId); let foundSource = sources.some((value) => { return value.name === testName; }); chai.assert.isFalse(foundSource, "expect not to find the source as it has not been stored yet"); + + sources = await MapLayerPreferences.getSources(); + foundSource = sources.some((value) => { return value.name === testName; }); + chai.assert.isFalse(foundSource, "expect not to find the source as it has not been stored yet"); + const success = await MapLayerPreferences.storeSource(layer!, false, iTwinId, iModelId); chai.assert.isTrue(success); @@ -46,6 +51,27 @@ describe("MapLayerPreferences", () => { chai.assert.isTrue(foundSource); }); + it("should store and retrieve layer without iTwinId and iModelId", async () => { + const layer = MapLayerSource.fromJSON({ + url: "test12345", + name: testName, + formatId: "test12345", + transparentBackground: true, + }); + + chai.assert.isDefined(layer); + let sources = await MapLayerPreferences.getSources(); + let foundSource = sources.some((value) => { return value.name === testName; }); + chai.assert.isFalse(foundSource, "expect not to find the source as it has not been stored yet"); + + const success = await MapLayerPreferences.storeSource(layer!); + chai.assert.isTrue(success); + + sources = await MapLayerPreferences.getSources(); + foundSource = sources.some((value) => { return value.name === testName; }); + chai.assert.isTrue(foundSource); + }); + it("should not be able to store model setting if same setting exists as project setting", async () => { const layer = MapLayerSource.fromJSON({ url: "test12345", @@ -72,6 +98,19 @@ describe("MapLayerPreferences", () => { chai.assert.isTrue(success); }); + it("should be able to store the same settings twice without iTwinId and iModelId", async () => { + const layer = MapLayerSource.fromJSON({ + url: "test12345", + name: testName, + formatId: "test12345", + transparentBackground: true, + }); + let success = await MapLayerPreferences.storeSource(layer!); + chai.assert.isTrue(success); + success = await MapLayerPreferences.storeSource(layer!); + chai.assert.isTrue(success); + }); + it("should be able to delete a mapSource stored on project and imodel level", async () => { const layer = MapLayerSource.fromJSON({ url: "test12345", @@ -90,4 +129,19 @@ describe("MapLayerPreferences", () => { await MapLayerPreferences.deleteByName(layer!, iTwinId, iModelId); chai.assert.isUndefined(await MapLayerPreferences.getByUrl(layer!.url, iTwinId, iModelId)); }); + + it("should be able to delete a mapSource stored without iTwinId and iModelId", async () => { + const layer = MapLayerSource.fromJSON({ + url: "test12345", + name: testName, + formatId: "test12345", + transparentBackground: true, + }); + + chai.assert.isDefined(layer); + + chai.assert.isTrue(await MapLayerPreferences.storeSource(layer!)); + await MapLayerPreferences.deleteByName(layer!); + chai.assert.isUndefined(await MapLayerPreferences.getByUrl(layer!.url)); + }); }); diff --git a/extensions/map-layers/src/test/MapUrlDialog.test.tsx b/extensions/map-layers/src/test/MapUrlDialog.test.tsx index f51ecb0ee1b7..f248cc841e36 100644 --- a/extensions/map-layers/src/test/MapUrlDialog.test.tsx +++ b/extensions/map-layers/src/test/MapUrlDialog.test.tsx @@ -3,6 +3,7 @@ * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ +import { Guid } from "@itwin/core-bentley"; import { EmptyLocalization, ImageMapLayerSettings, MapSubLayerProps } from "@itwin/core-common"; import { DisplayStyle3dState, IModelApp, IModelConnection, MapLayerAuthType, MapLayerSource, MapLayerSourceStatus, MockRender, NotifyMessageDetails, OutputMessagePriority, ScreenViewport, ViewState3d } from "@itwin/core-frontend"; import { Select } from "@itwin/itwinui-react"; @@ -11,6 +12,7 @@ import * as enzyme from "enzyme"; import * as React from "react"; import * as sinon from "sinon"; import * as moq from "typemoq"; +import { MapLayersUI } from "../mapLayers"; import { MapUrlDialog } from "../ui/widget/MapUrlDialog"; import { TestUtils } from "./TestUtils"; @@ -116,11 +118,17 @@ describe("MapUrlDialog", () => { beforeEach(() => { displayStyleMock.reset(); displayStyleMock.setup((ds) => ds.attachMapLayerSettings(moq.It.isAny(), moq.It.isAny(), moq.It.isAny())); + imodelMock.reset(); + imodelMock.setup((iModel) => iModel.iModelId).returns(() => "fakeGuid"); + imodelMock.setup((iModel) => iModel.iTwinId).returns(() => "fakeGuid"); + viewMock.reset(); viewMock.setup((view) => view.iModel).returns(() => imodelMock.object); viewportMock.reset(); + viewportMock.setup((viewport) => viewport.iModel).returns(() => viewMock.object.iModel); viewportMock.setup((viewport) => viewport.view).returns(() => viewMock.object); viewportMock.setup((viewport) => viewport.displayStyle).returns(() => displayStyleMock.object); + }); const mockModalUrlDialogOk = () => { @@ -180,4 +188,23 @@ describe("MapUrlDialog", () => { it("attach a layer requiring EsriToken", async () => { await testAddAuthLayer(MapLayerAuthType.EsriToken); }); + + it("should not display user preferences options if iTwinConfig is undefined ", () => { + + const component = enzyme.mount(); + const allRadios = component.find('input[type="radio"]'); + expect(allRadios.length).to.equals(0); + }); + + it("should display user preferences options if iTwinConfig is defined ", () => { + sandbox.stub(MapLayersUI, "iTwinConfig").get(() => ({ + get: undefined, + save: undefined, + delete: undefined, + })); + const component = enzyme.mount(); + const allRadios= component.find('input[type="radio"]'); + expect(allRadios.length).to.equals(2); + }); + }); diff --git a/extensions/map-layers/src/test/UserPreferencesMock.ts b/extensions/map-layers/src/test/UserPreferencesMock.ts index 638cf067b519..e02fa74c0883 100644 --- a/extensions/map-layers/src/test/UserPreferencesMock.ts +++ b/extensions/map-layers/src/test/UserPreferencesMock.ts @@ -9,14 +9,16 @@ import { ITwinIdArg, PreferenceArg, PreferenceKeyArg, TokenArg } from "@itwin/co let iModelPrefs: Map | undefined; let iTwinPrefs: Map | undefined; +let blankPrefs: Map | undefined; // Preferences for applications with blank connections export function setup() { - if (undefined === iModelPrefs || undefined === iTwinPrefs) { + if (undefined === iModelPrefs || undefined === iTwinPrefs || undefined === blankPrefs) { iModelPrefs = new Map(); iTwinPrefs = new Map(); + blankPrefs = new Map(); } const getStub = async (arg: PreferenceKeyArg & ITwinIdArg & TokenArg) => { - if (undefined === iModelPrefs || undefined === iTwinPrefs) + if (undefined === iModelPrefs || undefined === iTwinPrefs || undefined === blankPrefs) throw new Error("The user preferences mock is not properly setup - please run the `setup` method."); // If the arg.key isn't set, expect the return of all values since the only namespace used is the MapLayer's one. @@ -39,27 +41,42 @@ export function setup() { returnVal = iTwinPrefs.get(arg.key); } + if (undefined !== returnVal) + return returnVal; + + if (!arg.key) + return Array.from(blankPrefs.values()); + returnVal = blankPrefs.get(arg.key); + return returnVal; }; const deleteStub = async (arg: PreferenceKeyArg & ITwinIdArg & TokenArg) => { - if (undefined === iModelPrefs || undefined === iTwinPrefs) + if (undefined === iModelPrefs || undefined === iTwinPrefs || undefined === blankPrefs) throw new Error("The user preferences mock is not properly setup - please run the `setup` method."); - if (arg.iModelId) - iModelPrefs.delete(arg.key); - if (arg.iTwinId) - iTwinPrefs.delete(arg.key); + if (arg.iModelId || arg.iTwinId) { + if (arg.iModelId) + iModelPrefs.delete(arg.key); + if (arg.iTwinId) + iTwinPrefs.delete(arg.key); + } else { + blankPrefs.delete(arg.key); + } + }; const saveStub = async (arg: PreferenceArg & ITwinIdArg & TokenArg) => { - if (undefined === iModelPrefs || undefined === iTwinPrefs) + if (undefined === iModelPrefs || undefined === iTwinPrefs || undefined === blankPrefs) throw new Error("The user preferences mock is not properly setup - please run the `setup` method."); - - if (arg.iModelId) - iModelPrefs.set(arg.key, arg.content); - if (arg.iTwinId) - iTwinPrefs.set(arg.key, arg.content); + if (arg.iModelId || arg.iTwinId) { + if (arg.iModelId) + iModelPrefs.set(arg.key, arg.content); + if (arg.iTwinId) + iTwinPrefs.set(arg.key, arg.content); + } else { + blankPrefs.set(arg.key, arg.content); + } }; sinon.stub(MapLayersUI, "iTwinConfig").get(() => ({ @@ -72,4 +89,5 @@ export function setup() { export function restore() { iModelPrefs = undefined; iTwinPrefs = undefined; + blankPrefs = undefined; } diff --git a/extensions/map-layers/src/ui/widget/MapUrlDialog.tsx b/extensions/map-layers/src/ui/widget/MapUrlDialog.tsx index 381f83cbf8d0..14b5e7db28e9 100644 --- a/extensions/map-layers/src/ui/widget/MapUrlDialog.tsx +++ b/extensions/map-layers/src/ui/widget/MapUrlDialog.tsx @@ -6,6 +6,7 @@ import { DialogButtonType, SpecialKey } from "@itwin/appui-abstract"; import { ModalDialogManager } from "@itwin/appui-react"; +import { Guid } from "@itwin/core-bentley"; import { ImageMapLayerProps } from "@itwin/core-common"; import { IModelApp, MapLayerAuthType, MapLayerImageryProviderStatus, MapLayerSource, @@ -119,7 +120,12 @@ export function MapUrlDialog(props: MapUrlDialogProps) { return types; }); - const [isSettingsStorageAvailable] = React.useState(MapLayersUI.iTwinConfig && props?.activeViewport?.iModel?.iTwinId && props?.activeViewport?.iModel?.iModelId); + const [isSettingsStorageAvailable] = React.useState(MapLayersUI.iTwinConfig); + const [hasImodelContext] = React.useState ( + props?.activeViewport?.iModel?.iTwinId !== undefined + && props.activeViewport.iModel.iTwinId !== Guid.empty + && props?.activeViewport?.iModel?.iModelId !== undefined + && props?.activeViewport.iModel.iModelId !== Guid.empty); // Even though the settings storage is available, // we don't always want to enable it in the UI. @@ -230,8 +236,8 @@ export function MapUrlDialog(props: MapUrlDialogProps) { // Update service settings if storage is available and we are not prompting user for credentials if (!settingsStorageDisabled && !props.layerRequiringCredentials) { - const storeOnIModel = "Model" === settingsStorage; - if (!(await MapLayerPreferences.storeSource(source, storeOnIModel, vp.iModel.iTwinId!, vp.iModel.iModelId!))) { + const storeOnIModel = (hasImodelContext ? "Model" === settingsStorage : undefined); + if (!(await MapLayerPreferences.storeSource(source, storeOnIModel, vp.iModel.iTwinId, vp.iModel.iModelId))) { const msgError = MapLayersUiItemsProvider.localization.getLocalizedString("mapLayers:Messages.MapLayerPreferencesStoreFailed"); IModelApp.notifications.outputMessage(new NotifyMessageDetails(OutputMessagePriority.Error, msgError)); } @@ -255,7 +261,7 @@ export function MapUrlDialog(props: MapUrlDialogProps) { onOkResult(); return true; - }, [isOverlay, onOkResult, props?.activeViewport, props.layerRequiringCredentials, settingsStorage, settingsStorageDisabled]); + }, [hasImodelContext, isOverlay, onOkResult, props?.activeViewport, props.layerRequiringCredentials, settingsStorage, settingsStorageDisabled]); // Validate the layer source and attempt to attach (or update) the layer. // Returns true if no further input is needed from end-user (i.e. close the dialog) @@ -328,9 +334,9 @@ export function MapUrlDialog(props: MapUrlDialogProps) { if (props.mapLayerSourceToEdit !== undefined) { const vp = props.activeViewport; void (async () => { - if (isSettingsStorageAvailable && vp) { + if (isSettingsStorageAvailable) { try { - await MapLayerPreferences.replaceSource(props.mapLayerSourceToEdit!, source, vp.iModel.iTwinId!, vp.iModel.iModelId!); + await MapLayerPreferences.replaceSource(props.mapLayerSourceToEdit!, source, vp?.iModel.iTwinId, vp?.iModel.iModelId); } catch (err: any) { const errorMessage = IModelApp.localization.getLocalizedString("mapLayers:Messages.MapLayerEditError", { layerName: props.mapLayerSourceToEdit?.name }); IModelApp.notifications.outputMessage(new NotifyMessageDetails(OutputMessagePriority.Error, errorMessage)); @@ -503,15 +509,19 @@ export function MapUrlDialog(props: MapUrlDialogProps) { } {/* Store settings options, not shown when editing a layer */} - {isSettingsStorageAvailable &&
- - + {isSettingsStorageAvailable && +
+ {hasImodelContext && +
+ + +
}
}
From 4614ebaebbece765893dcc24a138e8ecdd4cb293 Mon Sep 17 00:00:00 2001 From: Michel Dastous Date: Thu, 24 Feb 2022 09:29:19 -0500 Subject: [PATCH 02/10] Make MapLayerManager fetch preferences even though thre is no iTwinId / iModelId --- .../map-layers/src/ui/widget/MapLayerManager.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/extensions/map-layers/src/ui/widget/MapLayerManager.tsx b/extensions/map-layers/src/ui/widget/MapLayerManager.tsx index ffb7be956864..c2c290903757 100644 --- a/extensions/map-layers/src/ui/widget/MapLayerManager.tsx +++ b/extensions/map-layers/src/ui/widget/MapLayerManager.tsx @@ -187,14 +187,12 @@ export function MapLayerManager(props: MapLayerManagerProps) { const sourceLayers = await MapLayerSources.create(undefined, (fetchPublicMapLayerSources && !hideExternalMapLayersSection)); const iModel = IModelApp.viewManager.selectedView ? IModelApp.viewManager.selectedView.iModel : undefined; - if (iModel && iModel.iTwinId && iModel.iModelId) { - try { - const preferenceSources = await MapLayerPreferences.getSources(iModel.iTwinId, iModel.iModelId); - for (const source of preferenceSources) - await MapLayerSources.addSourceToMapLayerSources(source); - } catch (err) { - IModelApp.notifications.outputMessage(new NotifyMessageDetails(OutputMessagePriority.Error, IModelApp.localization.getLocalizedString("mapLayers:CustomAttach.ErrorLoadingLayers"), BentleyError.getErrorMessage(err))); - } + try { + const preferenceSources = await MapLayerPreferences.getSources(iModel?.iTwinId, iModel?.iModelId); + for (const source of preferenceSources) + await MapLayerSources.addSourceToMapLayerSources(source); + } catch (err) { + IModelApp.notifications.outputMessage(new NotifyMessageDetails(OutputMessagePriority.Error, IModelApp.localization.getLocalizedString("mapLayers:CustomAttach.ErrorLoadingLayers"), BentleyError.getErrorMessage(err))); } if (!isMounted.current) { From 26cef843b5e09b4171957f9e9ad7835c6f2b904e Mon Sep 17 00:00:00 2001 From: Michel Dastous Date: Tue, 1 Mar 2022 15:06:58 -0500 Subject: [PATCH 03/10] Made iTwinid mandatory. Also renamed any instance of projectId to iTwinid. --- .../map-layers/src/MapLayerPreferences.ts | 16 ++++----- .../src/test/MapLayerSettingsSerivce.test.ts | 34 +++++++++---------- .../src/ui/widget/MapLayerManager.tsx | 5 ++- .../map-layers/src/ui/widget/MapUrlDialog.tsx | 4 +-- 4 files changed, 31 insertions(+), 28 deletions(-) diff --git a/extensions/map-layers/src/MapLayerPreferences.ts b/extensions/map-layers/src/MapLayerPreferences.ts index 29ba15206823..9debc5e1e9cd 100644 --- a/extensions/map-layers/src/MapLayerPreferences.ts +++ b/extensions/map-layers/src/MapLayerPreferences.ts @@ -52,7 +52,7 @@ export class MapLayerPreferences { * @param source source to be stored on the setting service * @param storeOnIModel if true store the settings object on the model, if false store it on the project */ - public static async storeSource(source: MapLayerSource, storeOnIModel?: boolean, iTwinId?: GuidString, iModelId?: GuidString): Promise { + public static async storeSource(source: MapLayerSource, iTwinId: GuidString, iModelId?: GuidString, storeOnIModel?: boolean): Promise { if (!MapLayersUI.iTwinConfig) return false; const accessToken = undefined !== IModelApp.authorizationClient ? (await IModelApp.authorizationClient.getAccessToken()) : undefined; @@ -139,7 +139,7 @@ export class MapLayerPreferences { * @param iTwinId * @param iModelId */ - public static async deleteByName(source: MapLayerSource, iTwinId?: GuidString, iModelId?: GuidString): Promise { + public static async deleteByName(source: MapLayerSource, iTwinId: GuidString, iModelId?: GuidString): Promise { if (!MapLayersUI.iTwinConfig) return; const accessToken = undefined !== IModelApp.authorizationClient ? (await IModelApp.authorizationClient.getAccessToken()) : undefined; @@ -177,7 +177,7 @@ export class MapLayerPreferences { * @param iModelId * @param storeOnIModel */ - private static async delete(url: string, name: string, iTwinId?: GuidString, iModelId?: GuidString, storeOnIModel?: boolean): Promise { + private static async delete(url: string, name: string, iTwinId: GuidString, iModelId?: GuidString, storeOnIModel?: boolean): Promise { if (!MapLayersUI.iTwinConfig) return true; const accessToken = undefined !== IModelApp.authorizationClient ? (await IModelApp.authorizationClient.getAccessToken()) : undefined; @@ -261,7 +261,7 @@ export class MapLayerPreferences { * @param projectId * @param iModelId */ - public static async getByUrl(url: string, projectId?: string, iModelId?: string): Promise { + public static async getByUrl(url: string, iTwinId: string, iModelId?: string): Promise { if (!MapLayersUI.iTwinConfig) return undefined; @@ -271,7 +271,7 @@ export class MapLayerPreferences { accessToken, namespace: MapLayerPreferences._preferenceNamespace, key: "", - iTwinId: projectId, + iTwinId, iModelId, }); @@ -292,7 +292,7 @@ export class MapLayerPreferences { * @param iModelId id of the iModel * @throws if any of the calls to grab settings fail. */ - public static async getSources(projectId?: GuidString, iModelId?: GuidString): Promise { + public static async getSources(iTwinId: GuidString, iModelId?: GuidString): Promise { if (!MapLayersUI.iTwinConfig) return []; const accessToken = undefined !== IModelApp.authorizationClient ? (await IModelApp.authorizationClient.getAccessToken()) : undefined; @@ -304,7 +304,7 @@ export class MapLayerPreferences { accessToken, namespace: MapLayerPreferences._preferenceNamespace, key: "", - iTwinId: projectId, + iTwinId, }); if (undefined !== userResultByProject) mapLayerList.push(userResultByProject); @@ -317,7 +317,7 @@ export class MapLayerPreferences { accessToken, namespace: MapLayerPreferences._preferenceNamespace, key: "", - iTwinId: projectId, + iTwinId, iModelId, }); if (undefined !== userResultByIModel) diff --git a/extensions/map-layers/src/test/MapLayerSettingsSerivce.test.ts b/extensions/map-layers/src/test/MapLayerSettingsSerivce.test.ts index f594c699e22a..4505bb1217b6 100644 --- a/extensions/map-layers/src/test/MapLayerSettingsSerivce.test.ts +++ b/extensions/map-layers/src/test/MapLayerSettingsSerivce.test.ts @@ -39,11 +39,11 @@ describe("MapLayerPreferences", () => { let foundSource = sources.some((value) => { return value.name === testName; }); chai.assert.isFalse(foundSource, "expect not to find the source as it has not been stored yet"); - sources = await MapLayerPreferences.getSources(); + sources = await MapLayerPreferences.getSources(iTwinId); foundSource = sources.some((value) => { return value.name === testName; }); chai.assert.isFalse(foundSource, "expect not to find the source as it has not been stored yet"); - const success = await MapLayerPreferences.storeSource(layer!, false, iTwinId, iModelId); + const success = await MapLayerPreferences.storeSource(layer!, iTwinId, iModelId, false); chai.assert.isTrue(success); sources = await MapLayerPreferences.getSources(iTwinId, iModelId); @@ -51,7 +51,7 @@ describe("MapLayerPreferences", () => { chai.assert.isTrue(foundSource); }); - it("should store and retrieve layer without iTwinId and iModelId", async () => { + it("should store and retrieve layer without ModelId", async () => { const layer = MapLayerSource.fromJSON({ url: "test12345", name: testName, @@ -60,14 +60,14 @@ describe("MapLayerPreferences", () => { }); chai.assert.isDefined(layer); - let sources = await MapLayerPreferences.getSources(); + let sources = await MapLayerPreferences.getSources(iTwinId); let foundSource = sources.some((value) => { return value.name === testName; }); chai.assert.isFalse(foundSource, "expect not to find the source as it has not been stored yet"); - const success = await MapLayerPreferences.storeSource(layer!); + const success = await MapLayerPreferences.storeSource(layer!, iTwinId); chai.assert.isTrue(success); - sources = await MapLayerPreferences.getSources(); + sources = await MapLayerPreferences.getSources(iTwinId); foundSource = sources.some((value) => { return value.name === testName; }); chai.assert.isTrue(foundSource); }); @@ -79,9 +79,9 @@ describe("MapLayerPreferences", () => { formatId: "test12345", transparentBackground: true, }); - let success = await MapLayerPreferences.storeSource(layer!, false, iTwinId, iModelId); + let success = await MapLayerPreferences.storeSource(layer!, iTwinId, iModelId, false); chai.assert.isTrue(success); - success = await MapLayerPreferences.storeSource(layer!, true, iTwinId, iModelId); + success = await MapLayerPreferences.storeSource(layer!, iTwinId, iModelId, true); chai.assert.isFalse(success, "cannot store the iModel setting that conflicts with an iTwin setting"); }); @@ -92,9 +92,9 @@ describe("MapLayerPreferences", () => { formatId: "test12345", transparentBackground: true, }); - let success = await MapLayerPreferences.storeSource(layer!, true, iTwinId, iModelId); + let success = await MapLayerPreferences.storeSource(layer!, iTwinId, iModelId, true); chai.assert.isTrue(success); - success = await MapLayerPreferences.storeSource(layer!, false, iTwinId, iModelId); + success = await MapLayerPreferences.storeSource(layer!, iTwinId, iModelId, false); chai.assert.isTrue(success); }); @@ -105,9 +105,9 @@ describe("MapLayerPreferences", () => { formatId: "test12345", transparentBackground: true, }); - let success = await MapLayerPreferences.storeSource(layer!); + let success = await MapLayerPreferences.storeSource(layer!, iTwinId); chai.assert.isTrue(success); - success = await MapLayerPreferences.storeSource(layer!); + success = await MapLayerPreferences.storeSource(layer!, iTwinId); chai.assert.isTrue(success); }); @@ -121,11 +121,11 @@ describe("MapLayerPreferences", () => { chai.assert.isDefined(layer); - chai.assert.isTrue(await MapLayerPreferences.storeSource(layer!, true, iTwinId, iModelId)); + chai.assert.isTrue(await MapLayerPreferences.storeSource(layer!, iTwinId, iModelId, true)); await MapLayerPreferences.deleteByName(layer!, iTwinId, iModelId); chai.assert.isUndefined(await MapLayerPreferences.getByUrl(layer!.url, iTwinId, iModelId)); - chai.assert.isTrue(await MapLayerPreferences.storeSource(layer!, false, iTwinId, iModelId)); + chai.assert.isTrue(await MapLayerPreferences.storeSource(layer!, iTwinId, iModelId, true)); await MapLayerPreferences.deleteByName(layer!, iTwinId, iModelId); chai.assert.isUndefined(await MapLayerPreferences.getByUrl(layer!.url, iTwinId, iModelId)); }); @@ -140,8 +140,8 @@ describe("MapLayerPreferences", () => { chai.assert.isDefined(layer); - chai.assert.isTrue(await MapLayerPreferences.storeSource(layer!)); - await MapLayerPreferences.deleteByName(layer!); - chai.assert.isUndefined(await MapLayerPreferences.getByUrl(layer!.url)); + chai.assert.isTrue(await MapLayerPreferences.storeSource(layer!, iTwinId)); + await MapLayerPreferences.deleteByName(layer!, iTwinId); + chai.assert.isUndefined(await MapLayerPreferences.getByUrl(layer!.url, iTwinId)); }); }); diff --git a/extensions/map-layers/src/ui/widget/MapLayerManager.tsx b/extensions/map-layers/src/ui/widget/MapLayerManager.tsx index c2c290903757..1817b262e16f 100644 --- a/extensions/map-layers/src/ui/widget/MapLayerManager.tsx +++ b/extensions/map-layers/src/ui/widget/MapLayerManager.tsx @@ -188,7 +188,10 @@ export function MapLayerManager(props: MapLayerManagerProps) { const iModel = IModelApp.viewManager.selectedView ? IModelApp.viewManager.selectedView.iModel : undefined; try { - const preferenceSources = await MapLayerPreferences.getSources(iModel?.iTwinId, iModel?.iModelId); + const preferenceSources = ( iModel?.iTwinId === undefined + ? [] + : await MapLayerPreferences.getSources(iModel?.iTwinId, iModel?.iModelId) + ); for (const source of preferenceSources) await MapLayerSources.addSourceToMapLayerSources(source); } catch (err) { diff --git a/extensions/map-layers/src/ui/widget/MapUrlDialog.tsx b/extensions/map-layers/src/ui/widget/MapUrlDialog.tsx index 14b5e7db28e9..f7849e1a3caf 100644 --- a/extensions/map-layers/src/ui/widget/MapUrlDialog.tsx +++ b/extensions/map-layers/src/ui/widget/MapUrlDialog.tsx @@ -120,7 +120,7 @@ export function MapUrlDialog(props: MapUrlDialogProps) { return types; }); - const [isSettingsStorageAvailable] = React.useState(MapLayersUI.iTwinConfig); + const [isSettingsStorageAvailable] = React.useState(MapLayersUI.iTwinConfig && props?.activeViewport?.iModel?.iTwinId); const [hasImodelContext] = React.useState ( props?.activeViewport?.iModel?.iTwinId !== undefined && props.activeViewport.iModel.iTwinId !== Guid.empty @@ -237,7 +237,7 @@ export function MapUrlDialog(props: MapUrlDialogProps) { // Update service settings if storage is available and we are not prompting user for credentials if (!settingsStorageDisabled && !props.layerRequiringCredentials) { const storeOnIModel = (hasImodelContext ? "Model" === settingsStorage : undefined); - if (!(await MapLayerPreferences.storeSource(source, storeOnIModel, vp.iModel.iTwinId, vp.iModel.iModelId))) { + if (vp.iModel.iTwinId && !(await MapLayerPreferences.storeSource(source, vp.iModel.iTwinId, vp.iModel.iModelId, storeOnIModel))) { const msgError = MapLayersUiItemsProvider.localization.getLocalizedString("mapLayers:Messages.MapLayerPreferencesStoreFailed"); IModelApp.notifications.outputMessage(new NotifyMessageDetails(OutputMessagePriority.Error, msgError)); } From 0cc7458578778aa736e197d344f1f8fee9faece3 Mon Sep 17 00:00:00 2001 From: Michel Dastous Date: Tue, 1 Mar 2022 15:15:48 -0500 Subject: [PATCH 04/10] Removed no longer needed blank connections Map to UserPreferencesMock --- .../src/test/UserPreferencesMock.ts | 41 ++++++------------- 1 file changed, 13 insertions(+), 28 deletions(-) diff --git a/extensions/map-layers/src/test/UserPreferencesMock.ts b/extensions/map-layers/src/test/UserPreferencesMock.ts index e02fa74c0883..1bd274daa32b 100644 --- a/extensions/map-layers/src/test/UserPreferencesMock.ts +++ b/extensions/map-layers/src/test/UserPreferencesMock.ts @@ -11,14 +11,14 @@ let iModelPrefs: Map | undefined; let iTwinPrefs: Map | undefined; let blankPrefs: Map | undefined; // Preferences for applications with blank connections export function setup() { - if (undefined === iModelPrefs || undefined === iTwinPrefs || undefined === blankPrefs) { + if (undefined === iModelPrefs || undefined === iTwinPrefs) { iModelPrefs = new Map(); iTwinPrefs = new Map(); blankPrefs = new Map(); } const getStub = async (arg: PreferenceKeyArg & ITwinIdArg & TokenArg) => { - if (undefined === iModelPrefs || undefined === iTwinPrefs || undefined === blankPrefs) + if (undefined === iModelPrefs || undefined === iTwinPrefs) throw new Error("The user preferences mock is not properly setup - please run the `setup` method."); // If the arg.key isn't set, expect the return of all values since the only namespace used is the MapLayer's one. @@ -41,42 +41,28 @@ export function setup() { returnVal = iTwinPrefs.get(arg.key); } - if (undefined !== returnVal) - return returnVal; - - if (!arg.key) - return Array.from(blankPrefs.values()); - returnVal = blankPrefs.get(arg.key); - return returnVal; }; const deleteStub = async (arg: PreferenceKeyArg & ITwinIdArg & TokenArg) => { - if (undefined === iModelPrefs || undefined === iTwinPrefs || undefined === blankPrefs) + if (undefined === iModelPrefs || undefined === iTwinPrefs) throw new Error("The user preferences mock is not properly setup - please run the `setup` method."); - if (arg.iModelId || arg.iTwinId) { - if (arg.iModelId) - iModelPrefs.delete(arg.key); - if (arg.iTwinId) - iTwinPrefs.delete(arg.key); - } else { - blankPrefs.delete(arg.key); - } + if (arg.iModelId) + iModelPrefs.delete(arg.key); + if (arg.iTwinId) + iTwinPrefs.delete(arg.key); }; const saveStub = async (arg: PreferenceArg & ITwinIdArg & TokenArg) => { - if (undefined === iModelPrefs || undefined === iTwinPrefs || undefined === blankPrefs) + if (undefined === iModelPrefs || undefined === iTwinPrefs) throw new Error("The user preferences mock is not properly setup - please run the `setup` method."); - if (arg.iModelId || arg.iTwinId) { - if (arg.iModelId) - iModelPrefs.set(arg.key, arg.content); - if (arg.iTwinId) - iTwinPrefs.set(arg.key, arg.content); - } else { - blankPrefs.set(arg.key, arg.content); - } + + if (arg.iModelId) + iModelPrefs.set(arg.key, arg.content); + if (arg.iTwinId) + iTwinPrefs.set(arg.key, arg.content); }; sinon.stub(MapLayersUI, "iTwinConfig").get(() => ({ @@ -89,5 +75,4 @@ export function setup() { export function restore() { iModelPrefs = undefined; iTwinPrefs = undefined; - blankPrefs = undefined; } From aed337fe75ae04e65559d24b0ff88f0d54283e58 Mon Sep 17 00:00:00 2001 From: Michel Dastous Date: Tue, 1 Mar 2022 15:17:27 -0500 Subject: [PATCH 05/10] Removed UserPreferencesMock leftover --- extensions/map-layers/src/test/UserPreferencesMock.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/extensions/map-layers/src/test/UserPreferencesMock.ts b/extensions/map-layers/src/test/UserPreferencesMock.ts index 1bd274daa32b..638cf067b519 100644 --- a/extensions/map-layers/src/test/UserPreferencesMock.ts +++ b/extensions/map-layers/src/test/UserPreferencesMock.ts @@ -9,12 +9,10 @@ import { ITwinIdArg, PreferenceArg, PreferenceKeyArg, TokenArg } from "@itwin/co let iModelPrefs: Map | undefined; let iTwinPrefs: Map | undefined; -let blankPrefs: Map | undefined; // Preferences for applications with blank connections export function setup() { if (undefined === iModelPrefs || undefined === iTwinPrefs) { iModelPrefs = new Map(); iTwinPrefs = new Map(); - blankPrefs = new Map(); } const getStub = async (arg: PreferenceKeyArg & ITwinIdArg & TokenArg) => { @@ -52,7 +50,6 @@ export function setup() { iModelPrefs.delete(arg.key); if (arg.iTwinId) iTwinPrefs.delete(arg.key); - }; const saveStub = async (arg: PreferenceArg & ITwinIdArg & TokenArg) => { From ff9a0408bb3391f01fe1bc4edae985f3351c3ded Mon Sep 17 00:00:00 2001 From: Michel Dastous Date: Tue, 1 Mar 2022 15:58:39 -0500 Subject: [PATCH 06/10] More renaming projectId-ITwinId --- extensions/map-layers/src/MapLayerPreferences.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/extensions/map-layers/src/MapLayerPreferences.ts b/extensions/map-layers/src/MapLayerPreferences.ts index 9debc5e1e9cd..45846a132420 100644 --- a/extensions/map-layers/src/MapLayerPreferences.ts +++ b/extensions/map-layers/src/MapLayerPreferences.ts @@ -88,10 +88,10 @@ export class MapLayerPreferences { * * @param oldSource * @param newSource - * @param projectId + * @param iTwinId * @param iModelId */ - public static async replaceSource(oldSource: MapLayerSource, newSource: MapLayerSource, projectId?: GuidString, iModelId?: GuidString): Promise { + public static async replaceSource(oldSource: MapLayerSource, newSource: MapLayerSource, iTwinId: GuidString, iModelId?: GuidString): Promise { if (!MapLayersUI.iTwinConfig) return; const accessToken = undefined !== IModelApp.authorizationClient ? (await IModelApp.authorizationClient.getAccessToken()) : undefined; @@ -102,7 +102,7 @@ export class MapLayerPreferences { accessToken, namespace: MapLayerPreferences._preferenceNamespace, key: oldSource.name, - iTwinId: projectId, + iTwinId, iModelId, }); } catch (_err) { @@ -110,7 +110,7 @@ export class MapLayerPreferences { accessToken, namespace: MapLayerPreferences._preferenceNamespace, key: oldSource.name, - iTwinId: projectId, + iTwinId, }); storeOnIModel = true; } @@ -125,7 +125,7 @@ export class MapLayerPreferences { await MapLayersUI.iTwinConfig.save({ accessToken, key: `${MapLayerPreferences._preferenceNamespace}.${newSource.name}`, - iTwinId: projectId, + iTwinId, iModelId: storeOnIModel ? iModelId : undefined, content: mapLayerSetting, }); @@ -258,7 +258,7 @@ export class MapLayerPreferences { /** Attempts to get a map layer based off a specific url. * @param url - * @param projectId + * @param iTwinId * @param iModelId */ public static async getByUrl(url: string, iTwinId: string, iModelId?: string): Promise { @@ -288,7 +288,7 @@ export class MapLayerPreferences { } /** Get all MapLayerSources from the user's preferences, iTwin setting and iModel settings. - * @param projectId id of the project + * @param iTwinId id of the iTwiin * @param iModelId id of the iModel * @throws if any of the calls to grab settings fail. */ From 4d58361d42d150203988a5247501dd3e399de19c Mon Sep 17 00:00:00 2001 From: Michel Dastous Date: Tue, 1 Mar 2022 16:00:17 -0500 Subject: [PATCH 07/10] iTwinId now mandatory for replaceSource --- extensions/map-layers/src/ui/widget/MapUrlDialog.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/map-layers/src/ui/widget/MapUrlDialog.tsx b/extensions/map-layers/src/ui/widget/MapUrlDialog.tsx index 396a13ed18b8..f66236327255 100644 --- a/extensions/map-layers/src/ui/widget/MapUrlDialog.tsx +++ b/extensions/map-layers/src/ui/widget/MapUrlDialog.tsx @@ -333,9 +333,9 @@ export function MapUrlDialog(props: MapUrlDialogProps) { if (props.mapLayerSourceToEdit !== undefined) { const vp = props.activeViewport; void (async () => { - if (isSettingsStorageAvailable) { + if (isSettingsStorageAvailable && vp?.iModel?.iTwinId) { try { - await MapLayerPreferences.replaceSource(props.mapLayerSourceToEdit!, source, vp?.iModel.iTwinId, vp?.iModel.iModelId); + await MapLayerPreferences.replaceSource(props.mapLayerSourceToEdit!, source, vp.iModel.iTwinId, vp?.iModel.iModelId); } catch (err: any) { const errorMessage = IModelApp.localization.getLocalizedString("mapLayers:Messages.MapLayerEditError", { layerName: props.mapLayerSourceToEdit?.name }); IModelApp.notifications.outputMessage(new NotifyMessageDetails(OutputMessagePriority.Error, errorMessage)); From e7c0e44dde27e0e9b240d5cc9d25e638f201a06d Mon Sep 17 00:00:00 2001 From: Michel Dastous Date: Tue, 1 Mar 2022 16:17:46 -0500 Subject: [PATCH 08/10] changelog --- .../geo-blankConn_Preferences_2022-03-01-21-13.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 common/changes/@itwin/map-layers/geo-blankConn_Preferences_2022-03-01-21-13.json diff --git a/common/changes/@itwin/map-layers/geo-blankConn_Preferences_2022-03-01-21-13.json b/common/changes/@itwin/map-layers/geo-blankConn_Preferences_2022-03-01-21-13.json new file mode 100644 index 000000000000..bdd4bd67eb30 --- /dev/null +++ b/common/changes/@itwin/map-layers/geo-blankConn_Preferences_2022-03-01-21-13.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/map-layers", + "comment": "User Preferences is now supported for Blank Connection configurations.", + "type": "none" + } + ], + "packageName": "@itwin/map-layers" +} \ No newline at end of file From 2ff2099756e25dfe4293188db569f79ea0c9ce6c Mon Sep 17 00:00:00 2001 From: Michel Dastous Date: Tue, 1 Mar 2022 16:33:01 -0500 Subject: [PATCH 09/10] lint error --- extensions/map-layers/src/test/MapUrlDialog.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/extensions/map-layers/src/test/MapUrlDialog.test.tsx b/extensions/map-layers/src/test/MapUrlDialog.test.tsx index f248cc841e36..7891838be787 100644 --- a/extensions/map-layers/src/test/MapUrlDialog.test.tsx +++ b/extensions/map-layers/src/test/MapUrlDialog.test.tsx @@ -3,7 +3,6 @@ * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ -import { Guid } from "@itwin/core-bentley"; import { EmptyLocalization, ImageMapLayerSettings, MapSubLayerProps } from "@itwin/core-common"; import { DisplayStyle3dState, IModelApp, IModelConnection, MapLayerAuthType, MapLayerSource, MapLayerSourceStatus, MockRender, NotifyMessageDetails, OutputMessagePriority, ScreenViewport, ViewState3d } from "@itwin/core-frontend"; import { Select } from "@itwin/itwinui-react"; From d804385dd3894d1b3e06760a0a0b754a3e988b91 Mon Sep 17 00:00:00 2001 From: Arun George <11051042+aruniverse@users.noreply.github.com> Date: Tue, 1 Mar 2022 16:50:48 -0500 Subject: [PATCH 10/10] Update extensions/map-layers/src/MapLayerPreferences.ts Co-authored-by: MarcBedard8 <31048177+MarcBedard8@users.noreply.github.com> --- extensions/map-layers/src/MapLayerPreferences.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/map-layers/src/MapLayerPreferences.ts b/extensions/map-layers/src/MapLayerPreferences.ts index 45846a132420..98ce8c34ec2a 100644 --- a/extensions/map-layers/src/MapLayerPreferences.ts +++ b/extensions/map-layers/src/MapLayerPreferences.ts @@ -288,7 +288,7 @@ export class MapLayerPreferences { } /** Get all MapLayerSources from the user's preferences, iTwin setting and iModel settings. - * @param iTwinId id of the iTwiin + * @param iTwinId id of the iTwin * @param iModelId id of the iModel * @throws if any of the calls to grab settings fail. */