Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow blank connections applications use map-layers widget 's UserPreferences #3256

Merged
merged 11 commits into from
Mar 2, 2022
Merged
Original file line number Diff line number Diff line change
@@ -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"
}
30 changes: 15 additions & 15 deletions extensions/map-layers/src/MapLayerPreferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean> {
public static async storeSource(source: MapLayerSource, iTwinId: GuidString, iModelId?: GuidString, storeOnIModel?: boolean): Promise<boolean> {
if (!MapLayersUI.iTwinConfig)
return false;
const accessToken = undefined !== IModelApp.authorizationClient ? (await IModelApp.authorizationClient.getAccessToken()) : undefined;
Expand Down Expand Up @@ -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<void> {
public static async replaceSource(oldSource: MapLayerSource, newSource: MapLayerSource, iTwinId: GuidString, iModelId?: GuidString): Promise<void> {
if (!MapLayersUI.iTwinConfig)
return;
const accessToken = undefined !== IModelApp.authorizationClient ? (await IModelApp.authorizationClient.getAccessToken()) : undefined;
Expand All @@ -102,15 +102,15 @@ export class MapLayerPreferences {
accessToken,
namespace: MapLayerPreferences._preferenceNamespace,
key: oldSource.name,
iTwinId: projectId,
iTwinId,
iModelId,
});
} catch (_err) {
await MapLayersUI.iTwinConfig.delete({
accessToken,
namespace: MapLayerPreferences._preferenceNamespace,
key: oldSource.name,
iTwinId: projectId,
iTwinId,
});
storeOnIModel = true;
}
Expand All @@ -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,
});
Expand All @@ -139,7 +139,7 @@ export class MapLayerPreferences {
* @param iTwinId
* @param iModelId
*/
public static async deleteByName(source: MapLayerSource, iTwinId: GuidString, iModelId: GuidString): Promise<void> {
public static async deleteByName(source: MapLayerSource, iTwinId: GuidString, iModelId?: GuidString): Promise<void> {
if (!MapLayersUI.iTwinConfig)
return;
const accessToken = undefined !== IModelApp.authorizationClient ? (await IModelApp.authorizationClient.getAccessToken()) : undefined;
Expand Down Expand Up @@ -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<boolean> {
private static async delete(url: string, name: string, iTwinId: GuidString, iModelId?: GuidString, storeOnIModel?: boolean): Promise<boolean> {
if (!MapLayersUI.iTwinConfig)
return true;
const accessToken = undefined !== IModelApp.authorizationClient ? (await IModelApp.authorizationClient.getAccessToken()) : undefined;
Expand Down Expand Up @@ -258,10 +258,10 @@ 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, projectId: string, iModelId?: string): Promise<MapLayerPreferencesContent | undefined> {
public static async getByUrl(url: string, iTwinId: string, iModelId?: string): Promise<MapLayerPreferencesContent | undefined> {
if (!MapLayersUI.iTwinConfig)
return undefined;

Expand All @@ -271,7 +271,7 @@ export class MapLayerPreferences {
accessToken,
namespace: MapLayerPreferences._preferenceNamespace,
key: "",
iTwinId: projectId,
iTwinId,
iModelId,
});

Expand All @@ -288,11 +288,11 @@ 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 iTwin
* @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<MapLayerSource[]> {
public static async getSources(iTwinId: GuidString, iModelId?: GuidString): Promise<MapLayerSource[]> {
if (!MapLayersUI.iTwinConfig)
return [];
const accessToken = undefined !== IModelApp.authorizationClient ? (await IModelApp.authorizationClient.getAccessToken()) : undefined;
Expand All @@ -304,7 +304,7 @@ export class MapLayerPreferences {
accessToken,
namespace: MapLayerPreferences._preferenceNamespace,
key: "",
iTwinId: projectId,
iTwinId,
});
if (undefined !== userResultByProject)
mapLayerList.push(userResultByProject);
Expand All @@ -317,7 +317,7 @@ export class MapLayerPreferences {
accessToken,
namespace: MapLayerPreferences._preferenceNamespace,
key: "",
iTwinId: projectId,
iTwinId,
iModelId,
});
if (undefined !== userResultByIModel)
Expand Down
68 changes: 61 additions & 7 deletions extensions/map-layers/src/test/MapLayerSettingsSerivce.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,50 @@ 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");
const success = await MapLayerPreferences.storeSource(layer!, false, iTwinId, iModelId);

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!, iTwinId, iModelId, false);
chai.assert.isTrue(success);

sources = await MapLayerPreferences.getSources(iTwinId, iModelId);
foundSource = sources.some((value) => { return value.name === testName; });
chai.assert.isTrue(foundSource);
});

it("should store and retrieve layer without ModelId", async () => {
const layer = MapLayerSource.fromJSON({
url: "test12345",
name: testName,
formatId: "test12345",
transparentBackground: true,
});

chai.assert.isDefined(layer);
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!, iTwinId);
chai.assert.isTrue(success);

sources = await MapLayerPreferences.getSources(iTwinId);
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",
name: testName,
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");
});

Expand All @@ -66,9 +92,22 @@ 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);
});

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!, iTwinId);
chai.assert.isTrue(success);
success = await MapLayerPreferences.storeSource(layer!, iTwinId);
chai.assert.isTrue(success);
});

Expand All @@ -82,12 +121,27 @@ 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));
});

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!, iTwinId));
await MapLayerPreferences.deleteByName(layer!, iTwinId);
chai.assert.isUndefined(await MapLayerPreferences.getByUrl(layer!.url, iTwinId));
});
});
26 changes: 26 additions & 0 deletions extensions/map-layers/src/test/MapUrlDialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,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";

Expand Down Expand Up @@ -116,11 +117,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 = () => {
Expand Down Expand Up @@ -180,4 +187,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(<MapUrlDialog activeViewport={viewportMock.object} isOverlay={false} onOkResult={mockModalUrlDialogOk} />);
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(<MapUrlDialog activeViewport={viewportMock.object} isOverlay={false} onOkResult={mockModalUrlDialogOk} />);
const allRadios= component.find('input[type="radio"]');
expect(allRadios.length).to.equals(2);
});

});
17 changes: 9 additions & 8 deletions extensions/map-layers/src/ui/widget/MapLayerManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,14 +187,15 @@ 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 = ( iModel?.iTwinId === undefined
? []
: 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) {
Expand Down
40 changes: 25 additions & 15 deletions extensions/map-layers/src/ui/widget/MapUrlDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { MapLayerPreferences } from "../../MapLayerPreferences";
import { MapLayersUI } from "../../mapLayers";
import { MapTypesOptions } from "../Interfaces";
import "./MapUrlDialog.scss";
import { Guid } from "@itwin/core-bentley";

export const MAP_TYPES = {
wms: "WMS",
Expand Down Expand Up @@ -118,7 +119,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 && props?.activeViewport?.iModel?.iTwinId);
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.
Expand Down Expand Up @@ -229,8 +235,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 (vp.iModel.iTwinId && !(await MapLayerPreferences.storeSource(source, vp.iModel.iTwinId, vp.iModel.iModelId, storeOnIModel))) {
const msgError = MapLayersUI.localization.getLocalizedString("mapLayers:Messages.MapLayerPreferencesStoreFailed");
IModelApp.notifications.outputMessage(new NotifyMessageDetails(OutputMessagePriority.Error, msgError));
}
Expand All @@ -254,7 +260,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)
Expand Down Expand Up @@ -327,9 +333,9 @@ export function MapUrlDialog(props: MapUrlDialogProps) {
if (props.mapLayerSourceToEdit !== undefined) {
const vp = props.activeViewport;
void (async () => {
if (isSettingsStorageAvailable && vp) {
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));
Expand Down Expand Up @@ -502,15 +508,19 @@ export function MapUrlDialog(props: MapUrlDialogProps) {
}

{/* Store settings options, not shown when editing a layer */}
{isSettingsStorageAvailable && <div title={settingsStorageDisabled ? noSaveSettingsWarning : ""}>
<Radio disabled={settingsStorageDisabled}
name="settingsStorage" value="iTwin"
label={iTwinSettingsLabel} checked={settingsStorage === "iTwin"}
onChange={onRadioChange} />
<Radio disabled={settingsStorageDisabled}
name="settingsStorage" value="Model"
label={modelSettingsLabel} checked={settingsStorage === "Model"}
onChange={onRadioChange} />
{isSettingsStorageAvailable &&
<div title={settingsStorageDisabled ? noSaveSettingsWarning : ""}>
{hasImodelContext &&
<div>
<Radio disabled={settingsStorageDisabled}
name="settingsStorage" value="iTwin"
label={iTwinSettingsLabel} checked={settingsStorage === "iTwin"}
onChange={onRadioChange} />
<Radio disabled={settingsStorageDisabled}
name="settingsStorage" value="Model"
label={modelSettingsLabel} checked={settingsStorage === "Model"}
onChange={onRadioChange} />
</div> }
</div>}
</div>
</div>
Expand Down