@itwin/map-layers 3.0.0-extension.0 → 3.1.0-dev.5

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 (72) hide show
  1. package/CHANGELOG.md +82 -1
  2. package/README.md +1 -1
  3. package/lib/cjs/MapLayerPreferences.d.ts +89 -0
  4. package/lib/cjs/MapLayerPreferences.d.ts.map +1 -0
  5. package/lib/cjs/MapLayerPreferences.js +312 -0
  6. package/lib/cjs/MapLayerPreferences.js.map +1 -0
  7. package/lib/cjs/mapLayers.d.ts +18 -13
  8. package/lib/cjs/mapLayers.d.ts.map +1 -1
  9. package/lib/cjs/mapLayers.js +25 -17
  10. package/lib/cjs/mapLayers.js.map +1 -1
  11. package/lib/cjs/public/locales/en/mapLayers.json +5 -2
  12. package/lib/cjs/ui/MapLayersUiItemsProvider.js +1 -1
  13. package/lib/cjs/ui/MapLayersUiItemsProvider.js.map +1 -1
  14. package/lib/cjs/ui/widget/AttachLayerPopupButton.d.ts.map +1 -1
  15. package/lib/cjs/ui/widget/AttachLayerPopupButton.js +5 -3
  16. package/lib/cjs/ui/widget/AttachLayerPopupButton.js.map +1 -1
  17. package/lib/cjs/ui/widget/BasemapPanel.d.ts.map +1 -1
  18. package/lib/cjs/ui/widget/BasemapPanel.js +5 -4
  19. package/lib/cjs/ui/widget/BasemapPanel.js.map +1 -1
  20. package/lib/cjs/ui/widget/MapLayerManager.d.ts +2 -2
  21. package/lib/cjs/ui/widget/MapLayerManager.d.ts.map +1 -1
  22. package/lib/cjs/ui/widget/MapLayerManager.js +24 -10
  23. package/lib/cjs/ui/widget/MapLayerManager.js.map +1 -1
  24. package/lib/cjs/ui/widget/MapLayerManager.scss +0 -7
  25. package/lib/cjs/ui/widget/MapManagerSettings.d.ts.map +1 -1
  26. package/lib/cjs/ui/widget/MapManagerSettings.js +51 -22
  27. package/lib/cjs/ui/widget/MapManagerSettings.js.map +1 -1
  28. package/lib/cjs/ui/widget/MapManagerSettings.scss +1 -1
  29. package/lib/cjs/ui/widget/MapUrlDialog.d.ts.map +1 -1
  30. package/lib/cjs/ui/widget/MapUrlDialog.js +231 -161
  31. package/lib/cjs/ui/widget/MapUrlDialog.js.map +1 -1
  32. package/lib/cjs/ui/widget/SubLayersTree.d.ts.map +1 -1
  33. package/lib/cjs/ui/widget/SubLayersTree.js +1 -1
  34. package/lib/cjs/ui/widget/SubLayersTree.js.map +1 -1
  35. package/lib/cjs/ui/widget/TransparencyPopupButton.js +2 -2
  36. package/lib/cjs/ui/widget/TransparencyPopupButton.js.map +1 -1
  37. package/lib/esm/MapLayerPreferences.d.ts +89 -0
  38. package/lib/esm/MapLayerPreferences.d.ts.map +1 -0
  39. package/lib/esm/MapLayerPreferences.js +308 -0
  40. package/lib/esm/MapLayerPreferences.js.map +1 -0
  41. package/lib/esm/mapLayers.d.ts +18 -13
  42. package/lib/esm/mapLayers.d.ts.map +1 -1
  43. package/lib/esm/mapLayers.js +26 -18
  44. package/lib/esm/mapLayers.js.map +1 -1
  45. package/lib/esm/public/locales/en/mapLayers.json +5 -2
  46. package/lib/esm/ui/MapLayersUiItemsProvider.js +1 -1
  47. package/lib/esm/ui/MapLayersUiItemsProvider.js.map +1 -1
  48. package/lib/esm/ui/widget/AttachLayerPopupButton.d.ts.map +1 -1
  49. package/lib/esm/ui/widget/AttachLayerPopupButton.js +6 -4
  50. package/lib/esm/ui/widget/AttachLayerPopupButton.js.map +1 -1
  51. package/lib/esm/ui/widget/BasemapPanel.d.ts.map +1 -1
  52. package/lib/esm/ui/widget/BasemapPanel.js +6 -5
  53. package/lib/esm/ui/widget/BasemapPanel.js.map +1 -1
  54. package/lib/esm/ui/widget/MapLayerManager.d.ts +2 -2
  55. package/lib/esm/ui/widget/MapLayerManager.d.ts.map +1 -1
  56. package/lib/esm/ui/widget/MapLayerManager.js +21 -7
  57. package/lib/esm/ui/widget/MapLayerManager.js.map +1 -1
  58. package/lib/esm/ui/widget/MapLayerManager.scss +0 -7
  59. package/lib/esm/ui/widget/MapManagerSettings.d.ts.map +1 -1
  60. package/lib/esm/ui/widget/MapManagerSettings.js +53 -24
  61. package/lib/esm/ui/widget/MapManagerSettings.js.map +1 -1
  62. package/lib/esm/ui/widget/MapManagerSettings.scss +1 -1
  63. package/lib/esm/ui/widget/MapUrlDialog.d.ts.map +1 -1
  64. package/lib/esm/ui/widget/MapUrlDialog.js +233 -163
  65. package/lib/esm/ui/widget/MapUrlDialog.js.map +1 -1
  66. package/lib/esm/ui/widget/SubLayersTree.d.ts.map +1 -1
  67. package/lib/esm/ui/widget/SubLayersTree.js +1 -1
  68. package/lib/esm/ui/widget/SubLayersTree.js.map +1 -1
  69. package/lib/esm/ui/widget/TransparencyPopupButton.js +2 -2
  70. package/lib/esm/ui/widget/TransparencyPopupButton.js.map +1 -1
  71. package/lib/public/locales/en/mapLayers.json +5 -2
  72. package/package.json +34 -39
@@ -4,13 +4,15 @@
4
4
  *--------------------------------------------------------------------------------------------*/
5
5
  // cSpell:ignore Modeless WMTS
6
6
  import * as React from "react";
7
- import { Input, LabeledInput, ProgressLinear, Radio, Select } from "@itwin/itwinui-react";
8
- import { Dialog, Icon, InputStatus } from "@itwin/core-react";
7
+ import { Dialog, Icon } from "@itwin/core-react";
9
8
  import { ModalDialogManager } from "@itwin/appui-react";
9
+ import { Input, LabeledInput, ProgressLinear, Radio, Select } from "@itwin/itwinui-react";
10
10
  import { MapLayersUiItemsProvider } from "../MapLayersUiItemsProvider";
11
- import { IModelApp, MapLayerImageryProviderStatus, MapLayerSettingsService, MapLayerSource, MapLayerSourceStatus, NotifyMessageDetails, OutputMessagePriority, } from "@itwin/core-frontend";
11
+ import { IModelApp, MapLayerAuthType, MapLayerImageryProviderStatus, MapLayerSource, MapLayerSourceStatus, NotifyMessageDetails, OutputMessagePriority, } from "@itwin/core-frontend";
12
12
  import "./MapUrlDialog.scss";
13
13
  import { DialogButtonType, SpecialKey } from "@itwin/appui-abstract";
14
+ import { MapLayerPreferences } from "../../MapLayerPreferences";
15
+ import { MapLayersUI } from "../../mapLayers";
14
16
  export const MAP_TYPES = {
15
17
  wms: "WMS",
16
18
  arcGis: "ArcGIS",
@@ -19,9 +21,8 @@ export const MAP_TYPES = {
19
21
  };
20
22
  // eslint-disable-next-line @typescript-eslint/naming-convention
21
23
  export function MapUrlDialog(props) {
22
- var _a, _b, _c, _d, _e, _f;
24
+ var _a, _b, _c, _d, _e;
23
25
  const { isOverlay, onOkResult, mapTypesOptions } = props;
24
- const supportWmsAuthentication = ((mapTypesOptions === null || mapTypesOptions === void 0 ? void 0 : mapTypesOptions.supportWmsAuthentication) ? true : false);
25
26
  const getMapUrlFromProps = React.useCallback(() => {
26
27
  var _a;
27
28
  if (props.mapLayerSourceToEdit) {
@@ -62,10 +63,9 @@ export function MapUrlDialog(props) {
62
63
  const [modelSettingsLabel] = React.useState(MapLayersUiItemsProvider.localization.getLocalizedString("mapLayers:CustomAttach.StoreOnModelSettings"));
63
64
  const [missingCredentialsLabel] = React.useState(MapLayersUiItemsProvider.localization.getLocalizedString("mapLayers:CustomAttach.MissingCredentials"));
64
65
  const [invalidCredentialsLabel] = React.useState(MapLayersUiItemsProvider.localization.getLocalizedString("mapLayers:CustomAttach.InvalidCredentials"));
65
- const [serverRequireCredentials, setServerRequireCredentials] = React.useState((_a = props.layerRequiringCredentials) !== null && _a !== void 0 ? _a : false);
66
+ const [serverRequireCredentials, setServerRequireCredentials] = React.useState(false);
66
67
  const [invalidCredentialsProvided, setInvalidCredentialsProvided] = React.useState(false);
67
68
  const [layerAttachPending, setLayerAttachPending] = React.useState(false);
68
- const [warningMessage, setWarningMessage] = React.useState(props.layerRequiringCredentials ? missingCredentialsLabel : undefined);
69
69
  const [mapUrl, setMapUrl] = React.useState(getMapUrlFromProps());
70
70
  const [mapName, setMapName] = React.useState(getMapNameFromProps());
71
71
  const [userName, setUserName] = React.useState("");
@@ -75,8 +75,9 @@ export function MapUrlDialog(props) {
75
75
  const [passwordRequiredLabel] = React.useState(MapLayersUiItemsProvider.localization.getLocalizedString("mapLayers:AuthenticationInputs.PasswordRequired"));
76
76
  const [userNameLabel] = React.useState(MapLayersUiItemsProvider.localization.getLocalizedString("mapLayers:AuthenticationInputs.Username"));
77
77
  const [userNameRequiredLabel] = React.useState(MapLayersUiItemsProvider.localization.getLocalizedString("mapLayers:AuthenticationInputs.UsernameRequired"));
78
- const [settingsStorage, setSettingsStorageRadio] = React.useState("ITwin");
79
- const [mapType, setMapType] = React.useState((_b = getFormatFromProps()) !== null && _b !== void 0 ? _b : MAP_TYPES.arcGis);
78
+ const [settingsStorage, setSettingsStorageRadio] = React.useState("iTwin");
79
+ const [layerAuthMethod, setLayerAuthMethod] = React.useState(MapLayerAuthType.None);
80
+ const [mapType, setMapType] = React.useState((_a = getFormatFromProps()) !== null && _a !== void 0 ? _a : MAP_TYPES.arcGis);
80
81
  // 'isMounted' is used to prevent any async operation once the hook has been
81
82
  // unloaded. Otherwise we get a 'Can't perform a React state update on an unmounted component.' warning in the console.
82
83
  const isMounted = React.useRef(false);
@@ -96,15 +97,10 @@ export function MapUrlDialog(props) {
96
97
  types.push({ value: MAP_TYPES.tileUrl, label: MAP_TYPES.tileUrl });
97
98
  return types;
98
99
  });
99
- const [isSettingsStorageAvailable] = React.useState(((_d = (_c = props === null || props === void 0 ? void 0 : props.activeViewport) === null || _c === void 0 ? void 0 : _c.iModel) === null || _d === void 0 ? void 0 : _d.iTwinId) && ((_f = (_e = props === null || props === void 0 ? void 0 : props.activeViewport) === null || _e === void 0 ? void 0 : _e.iModel) === null || _f === void 0 ? void 0 : _f.iModelId));
100
+ const [isSettingsStorageAvailable] = React.useState(MapLayersUI.iTwinConfig && ((_c = (_b = props === null || props === void 0 ? void 0 : props.activeViewport) === null || _b === void 0 ? void 0 : _b.iModel) === null || _c === void 0 ? void 0 : _c.iTwinId) && ((_e = (_d = props === null || props === void 0 ? void 0 : props.activeViewport) === null || _d === void 0 ? void 0 : _d.iModel) === null || _e === void 0 ? void 0 : _e.iModelId));
100
101
  // Even though the settings storage is available,
101
102
  // we don't always want to enable it in the UI.
102
103
  const [settingsStorageDisabled] = React.useState(!isSettingsStorageAvailable || props.mapLayerSourceToEdit !== undefined || props.layerRequiringCredentials !== undefined);
103
- const isAuthSupported = React.useCallback(() => {
104
- return ((mapType === MAP_TYPES.wms || mapType === MAP_TYPES.wms) && supportWmsAuthentication)
105
- || mapType === MAP_TYPES.arcGis;
106
- }, [mapType, supportWmsAuthentication]);
107
- // const [layerIdxToEdit] = React.useState((): number | undefined => {
108
104
  const [layerRequiringCredentialsIdx] = React.useState(() => {
109
105
  var _a;
110
106
  if (props.layerRequiringCredentials === undefined || !props.layerRequiringCredentials.name || !props.layerRequiringCredentials.url) {
@@ -119,20 +115,15 @@ export function MapUrlDialog(props) {
119
115
  }
120
116
  });
121
117
  // Update warning message based on the dialog state and server response
122
- React.useEffect(() => {
123
- if (invalidCredentialsProvided) {
124
- setWarningMessage(invalidCredentialsLabel);
125
- }
126
- else if (serverRequireCredentials && (!userName || !password)) {
127
- setWarningMessage(missingCredentialsLabel);
128
- }
129
- else {
130
- setWarningMessage(undefined);
131
- }
132
- }, [invalidCredentialsProvided, invalidCredentialsLabel, missingCredentialsLabel, serverRequireCredentials, userName, password, setWarningMessage]);
133
118
  const handleMapTypeSelection = React.useCallback((newValue) => {
134
119
  setMapType(newValue);
135
- }, [setMapType]);
120
+ // Reset few states
121
+ if (invalidCredentialsProvided)
122
+ setInvalidCredentialsProvided(false);
123
+ if (layerAuthMethod !== MapLayerAuthType.None) {
124
+ setLayerAuthMethod(MapLayerAuthType.None);
125
+ }
126
+ }, [invalidCredentialsProvided, layerAuthMethod]);
136
127
  const handleCancel = React.useCallback(() => {
137
128
  if (props.onCancelResult) {
138
129
  props.onCancelResult();
@@ -150,86 +141,109 @@ export function MapUrlDialog(props) {
150
141
  if (invalidCredentialsProvided)
151
142
  setInvalidCredentialsProvided(false);
152
143
  }, [setPassword, invalidCredentialsProvided, setInvalidCredentialsProvided]);
153
- const doAttach = React.useCallback(async (source) => {
154
- // Returns a promise, When true, the dialog should closed
155
- return new Promise((resolve, _reject) => {
156
- const vp = props === null || props === void 0 ? void 0 : props.activeViewport;
157
- if (vp === undefined || source === undefined) {
158
- resolve(true);
159
- return;
160
- }
144
+ // return true if authorization is needed
145
+ const updateAuthState = React.useCallback((sourceValidation) => {
146
+ var _a, _b;
147
+ const sourceRequireAuth = (sourceValidation.status === MapLayerSourceStatus.RequireAuth);
148
+ const invalidCredentials = (sourceValidation.status === MapLayerSourceStatus.InvalidCredentials);
149
+ if (sourceRequireAuth && ((_a = sourceValidation.authInfo) === null || _a === void 0 ? void 0 : _a.authMethod) !== undefined) {
150
+ setLayerAuthMethod((_b = sourceValidation.authInfo) === null || _b === void 0 ? void 0 : _b.authMethod);
151
+ }
152
+ if (invalidCredentials) {
153
+ setInvalidCredentialsProvided(true);
154
+ }
155
+ else if (invalidCredentialsProvided) {
156
+ setInvalidCredentialsProvided(false); // flag reset
157
+ }
158
+ return sourceRequireAuth || invalidCredentials;
159
+ }, [invalidCredentialsProvided]);
160
+ const updateAttachedLayer = React.useCallback(async (source, validation) => {
161
+ const vp = props === null || props === void 0 ? void 0 : props.activeViewport;
162
+ if (vp === undefined || source === undefined || layerRequiringCredentialsIdx === undefined) {
163
+ const error = MapLayersUiItemsProvider.localization.getLocalizedString("mapLayers:Messages.MapLayerAttachMissingViewOrSource");
164
+ const msg = MapLayersUiItemsProvider.localization.getLocalizedString("mapLayers:Messages.MapLayerAttachError", { error, sourceUrl: source.url });
165
+ IModelApp.notifications.outputMessage(new NotifyMessageDetails(OutputMessagePriority.Error, msg));
166
+ return true;
167
+ }
168
+ // Layer is already attached,
169
+ vp.displayStyle.changeMapLayerProps({
170
+ subLayers: validation.subLayers,
171
+ }, layerRequiringCredentialsIdx, isOverlay);
172
+ vp.displayStyle.changeMapLayerCredentials(layerRequiringCredentialsIdx, isOverlay, source.userName, source.password);
173
+ // Reset the provider's status
174
+ const provider = vp.getMapLayerImageryProvider(layerRequiringCredentialsIdx, isOverlay);
175
+ if (provider && provider.status !== MapLayerImageryProviderStatus.Valid) {
176
+ provider.status = MapLayerImageryProviderStatus.Valid;
177
+ }
178
+ vp.invalidateRenderPlan();
179
+ // This handler will close the layer source handler, and therefore the MapUrl dialog.
180
+ // don't call it if the dialog needs to remains open.
181
+ onOkResult();
182
+ return true;
183
+ }, [isOverlay, layerRequiringCredentialsIdx, onOkResult, props.activeViewport]);
184
+ // Returns true if no further input is needed from end-user.
185
+ const doAttach = React.useCallback(async (source, validation) => {
186
+ const vp = props === null || props === void 0 ? void 0 : props.activeViewport;
187
+ if (vp === undefined || source === undefined) {
188
+ const error = MapLayersUiItemsProvider.localization.getLocalizedString("mapLayers:Messages.MapLayerAttachMissingViewOrSource");
189
+ const msg = MapLayersUiItemsProvider.localization.getLocalizedString("mapLayers:Messages.MapLayerAttachError", { error, sourceUrl: source.url });
190
+ IModelApp.notifications.outputMessage(new NotifyMessageDetails(OutputMessagePriority.Error, msg));
191
+ return true;
192
+ }
193
+ // Update service settings if storage is available and we are not prompting user for credentials
194
+ if (!settingsStorageDisabled && !props.layerRequiringCredentials) {
161
195
  const storeOnIModel = "Model" === settingsStorage;
162
- source.validateSource(true).then(async (validation) => {
163
- if (validation.status === MapLayerSourceStatus.Valid
164
- || validation.status === MapLayerSourceStatus.RequireAuth
165
- || validation.status === MapLayerSourceStatus.InvalidCredentials) {
166
- const sourceRequireAuth = (validation.status === MapLayerSourceStatus.RequireAuth);
167
- const invalidCredentials = (validation.status === MapLayerSourceStatus.InvalidCredentials);
168
- const closeDialog = !sourceRequireAuth && !invalidCredentials;
169
- resolve(closeDialog);
170
- if (sourceRequireAuth && !serverRequireCredentials) {
171
- setServerRequireCredentials(true);
172
- }
173
- if (invalidCredentials) {
174
- setInvalidCredentialsProvided(true);
175
- return;
176
- }
177
- else if (invalidCredentialsProvided) {
178
- setInvalidCredentialsProvided(false); // flag reset
179
- }
180
- if (validation.status === MapLayerSourceStatus.Valid) {
181
- // Attach layer and update settings service (only if editing)
182
- if (layerRequiringCredentialsIdx !== undefined) {
183
- // Update username / password
184
- vp.displayStyle.changeMapLayerProps({
185
- subLayers: validation.subLayers,
186
- }, layerRequiringCredentialsIdx, isOverlay);
187
- vp.displayStyle.changeMapLayerCredentials(layerRequiringCredentialsIdx, isOverlay, source.userName, source.password);
188
- // Reset the provider's status
189
- const provider = vp.getMapLayerImageryProvider(layerRequiringCredentialsIdx, isOverlay);
190
- if (provider && provider.status !== MapLayerImageryProviderStatus.Valid) {
191
- provider.status = MapLayerImageryProviderStatus.Valid;
192
- }
193
- }
194
- else {
195
- // Update service settings if storage is available and we are not prompting user for credentials
196
- if (!settingsStorageDisabled && !props.layerRequiringCredentials) {
197
- if (!(await MapLayerSettingsService.storeSourceInSettingsService(source, storeOnIModel, vp.iModel.iTwinId, vp.iModel.iModelId)))
198
- return;
199
- }
200
- const layerSettings = source.toLayerSettings(validation.subLayers);
201
- if (layerSettings) {
202
- vp.displayStyle.attachMapLayerSettings(layerSettings, isOverlay, undefined);
203
- const msg = IModelApp.localization.getLocalizedString("mapLayers:Messages.MapLayerAttached", { sourceName: source.name, sourceUrl: source.url });
204
- IModelApp.notifications.outputMessage(new NotifyMessageDetails(OutputMessagePriority.Info, msg));
205
- }
206
- else {
207
- const msgError = MapLayersUiItemsProvider.localization.getLocalizedString("mapLayers:Messages.MapLayerLayerSettingsConversionError");
208
- const msg = MapLayersUiItemsProvider.localization.getLocalizedString("mapLayers:CustomAttach.MapLayerAttachError", { error: msgError, sourceUrl: source.url });
209
- IModelApp.notifications.outputMessage(new NotifyMessageDetails(OutputMessagePriority.Error, msg));
210
- }
211
- }
212
- vp.invalidateRenderPlan();
213
- }
214
- if (closeDialog) {
215
- // This handler will close the layer source handler, and therefore the MapUrl dialog.
216
- // don't call it if the dialog needs to remains open.
217
- onOkResult();
218
- }
196
+ if (!(await MapLayerPreferences.storeSource(source, storeOnIModel, vp.iModel.iTwinId, vp.iModel.iModelId))) {
197
+ const msgError = MapLayersUiItemsProvider.localization.getLocalizedString("mapLayers:Messages.MapLayerPreferencesStoreFailed");
198
+ IModelApp.notifications.outputMessage(new NotifyMessageDetails(OutputMessagePriority.Error, msgError));
199
+ }
200
+ }
201
+ const layerSettings = source.toLayerSettings(validation.subLayers);
202
+ if (layerSettings) {
203
+ vp.displayStyle.attachMapLayerSettings(layerSettings, isOverlay, undefined);
204
+ const msg = IModelApp.localization.getLocalizedString("mapLayers:Messages.MapLayerAttached", { sourceName: source.name, sourceUrl: source.url });
205
+ IModelApp.notifications.outputMessage(new NotifyMessageDetails(OutputMessagePriority.Info, msg));
206
+ }
207
+ else {
208
+ const msgError = MapLayersUiItemsProvider.localization.getLocalizedString("mapLayers:Messages.MapLayerLayerSettingsConversionError");
209
+ const msg = MapLayersUiItemsProvider.localization.getLocalizedString("mapLayers:CustomAttach.MapLayerAttachError", { error: msgError, sourceUrl: source.url });
210
+ IModelApp.notifications.outputMessage(new NotifyMessageDetails(OutputMessagePriority.Error, msg));
211
+ }
212
+ vp.invalidateRenderPlan();
213
+ // This handler will close the layer source handler, and therefore the MapUrl dialog.
214
+ // don't call it if the dialog needs to remains open.
215
+ onOkResult();
216
+ return true;
217
+ }, [isOverlay, onOkResult, props === null || props === void 0 ? void 0 : props.activeViewport, props.layerRequiringCredentials, settingsStorage, settingsStorageDisabled]);
218
+ // Validate the layer source and attempt to attach (or update) the layer.
219
+ // Returns true if no further input is needed from end-user (i.e. close the dialog)
220
+ const attemptAttachSource = React.useCallback(async (source) => {
221
+ try {
222
+ const validation = await source.validateSource(true);
223
+ if (validation.status === MapLayerSourceStatus.Valid) {
224
+ if (layerRequiringCredentialsIdx === undefined) {
225
+ return await doAttach(source, validation);
219
226
  }
220
227
  else {
221
- const msg = MapLayersUiItemsProvider.localization.getLocalizedString("mapLayers:CustomAttach.ValidationError");
222
- IModelApp.notifications.outputMessage(new NotifyMessageDetails(OutputMessagePriority.Error, `${msg} ${source.url}`));
223
- resolve(true);
228
+ return await updateAttachedLayer(source, validation);
224
229
  }
225
- resolve(false);
226
- }).catch((error) => {
227
- const msg = MapLayersUiItemsProvider.localization.getLocalizedString("mapLayers:CustomAttach.MapLayerAttachError", { error, sourceUrl: source.url });
228
- IModelApp.notifications.outputMessage(new NotifyMessageDetails(OutputMessagePriority.Error, msg));
229
- resolve(true);
230
- });
231
- });
232
- }, [props.activeViewport, props.layerRequiringCredentials, settingsStorage, serverRequireCredentials, invalidCredentialsProvided, onOkResult, layerRequiringCredentialsIdx, isOverlay, settingsStorageDisabled]);
230
+ }
231
+ else if (updateAuthState(validation)) {
232
+ return false;
233
+ }
234
+ else {
235
+ const msg = MapLayersUiItemsProvider.localization.getLocalizedString("mapLayers:CustomAttach.ValidationError");
236
+ IModelApp.notifications.outputMessage(new NotifyMessageDetails(OutputMessagePriority.Error, `${msg} ${source.url}`));
237
+ return true;
238
+ }
239
+ return false;
240
+ }
241
+ catch (error) {
242
+ const msg = MapLayersUiItemsProvider.localization.getLocalizedString("mapLayers:Messages.MapLayerAttachError", { error, sourceUrl: source.url });
243
+ IModelApp.notifications.outputMessage(new NotifyMessageDetails(OutputMessagePriority.Error, msg));
244
+ return true;
245
+ }
246
+ }, [updateAuthState, doAttach, layerRequiringCredentialsIdx, updateAttachedLayer]);
233
247
  const onNameChange = React.useCallback((event) => {
234
248
  setMapName(event.target.value);
235
249
  }, [setMapName]);
@@ -239,70 +253,101 @@ export function MapUrlDialog(props) {
239
253
  const onUrlChange = React.useCallback((event) => {
240
254
  setMapUrl(event.target.value);
241
255
  }, [setMapUrl]);
242
- const handleOk = React.useCallback(() => {
256
+ const createSource = React.useCallback(() => {
243
257
  let source;
244
258
  if (mapUrl && mapName) {
245
259
  source = MapLayerSource.fromJSON({
246
260
  url: mapUrl,
247
261
  name: mapName,
248
262
  formatId: mapType,
249
- userName,
250
- password,
263
+ userName: userName || undefined,
264
+ password: password || undefined
251
265
  });
252
- if (source === undefined || props.mapLayerSourceToEdit) {
253
- ModalDialogManager.closeDialog();
254
- if (source === undefined) {
255
- // Close the dialog and inform end user something went wrong.
256
- const msgError = MapLayersUiItemsProvider.localization.getLocalizedString("mapLayers:Messages.MapLayerLayerSourceCreationFailed");
257
- const msg = MapLayersUiItemsProvider.localization.getLocalizedString("mapLayers:CustomAttach.MapLayerAttachError", { error: msgError, sourceUrl: mapUrl });
258
- IModelApp.notifications.outputMessage(new NotifyMessageDetails(OutputMessagePriority.Error, msg));
259
- return;
260
- }
261
- // Simply change the source definition in the setting service
262
- if (props.mapLayerSourceToEdit !== undefined) {
263
- const vp = props.activeViewport;
264
- void (async () => {
265
- var _a;
266
- if (isSettingsStorageAvailable && vp) {
267
- if (!(await MapLayerSettingsService.replaceSourceInSettingsService(props.mapLayerSourceToEdit, source, vp.iModel.iTwinId, vp.iModel.iModelId))) {
268
- const errorMessage = IModelApp.localization.getLocalizedString("mapLayers:Messages.MapLayerEditError", { layerName: (_a = props.mapLayerSourceToEdit) === null || _a === void 0 ? void 0 : _a.name });
269
- IModelApp.notifications.outputMessage(new NotifyMessageDetails(OutputMessagePriority.Error, errorMessage));
270
- return;
271
- }
266
+ }
267
+ return source;
268
+ }, [mapName, mapType, mapUrl, password, userName]);
269
+ const handleOk = React.useCallback(() => {
270
+ const source = createSource();
271
+ if (source === undefined || props.mapLayerSourceToEdit) {
272
+ ModalDialogManager.closeDialog();
273
+ if (source === undefined) {
274
+ // Close the dialog and inform end user something went wrong.
275
+ const msgError = MapLayersUiItemsProvider.localization.getLocalizedString("mapLayers:Messages.MapLayerLayerSourceCreationFailed");
276
+ const msg = MapLayersUiItemsProvider.localization.getLocalizedString("mapLayers:Messages.MapLayerAttachError", { error: msgError, sourceUrl: mapUrl });
277
+ IModelApp.notifications.outputMessage(new NotifyMessageDetails(OutputMessagePriority.Error, msg));
278
+ return;
279
+ }
280
+ // Simply change the source definition in the setting service
281
+ if (props.mapLayerSourceToEdit !== undefined) {
282
+ const vp = props.activeViewport;
283
+ void (async () => {
284
+ var _a;
285
+ if (isSettingsStorageAvailable && vp) {
286
+ try {
287
+ await MapLayerPreferences.replaceSource(props.mapLayerSourceToEdit, source, vp.iModel.iTwinId, vp.iModel.iModelId);
288
+ }
289
+ catch (err) {
290
+ const errorMessage = IModelApp.localization.getLocalizedString("mapLayers:Messages.MapLayerEditError", { layerName: (_a = props.mapLayerSourceToEdit) === null || _a === void 0 ? void 0 : _a.name });
291
+ IModelApp.notifications.outputMessage(new NotifyMessageDetails(OutputMessagePriority.Error, errorMessage));
292
+ return;
272
293
  }
273
- })();
274
- return;
294
+ }
295
+ })();
296
+ return;
297
+ }
298
+ }
299
+ setLayerAttachPending(true);
300
+ // Attach source asynchronously.
301
+ void (async () => {
302
+ try {
303
+ const closeDialog = await attemptAttachSource(source);
304
+ if (isMounted.current) {
305
+ setLayerAttachPending(false);
306
+ }
307
+ // In theory the modal dialog should always get closed by the parent
308
+ // AttachLayerPanel's 'onOkResult' handler. We close it here just in case.
309
+ if (closeDialog) {
310
+ ModalDialogManager.closeDialog();
275
311
  }
276
312
  }
277
- setLayerAttachPending(true);
278
- void (async () => {
279
- // Code below is executed in the an async manner but
280
- // I don't necessarily want to mark the handler as async
281
- // so Im wrapping it un in a void wrapper.
313
+ catch (_error) {
314
+ ModalDialogManager.closeDialog();
315
+ }
316
+ })();
317
+ }, [createSource, props.mapLayerSourceToEdit, props.activeViewport, mapUrl, isSettingsStorageAvailable, attemptAttachSource]);
318
+ // The first time the dialog is loaded and we already know the layer requires auth. (i.e ImageryProvider already made an attempt)
319
+ // makes a request to discover the authentification types and adjust UI accordingly (i.e. username/password fields, Oauth popup)
320
+ // Without this effect, user would have to manually click the 'OK' button in order to trigger the layer connection.
321
+ React.useEffect(() => {
322
+ // Attach source asynchronously.
323
+ void (async () => {
324
+ var _a, _b;
325
+ if (((_a = props.layerRequiringCredentials) === null || _a === void 0 ? void 0 : _a.url) !== undefined && ((_b = props.layerRequiringCredentials) === null || _b === void 0 ? void 0 : _b.name) !== undefined) {
282
326
  try {
283
- const closeDialog = await doAttach(source);
284
- if (isMounted.current) {
285
- setLayerAttachPending(false);
286
- }
287
- // In theory the modal dialog should always get closed by the parent
288
- // AttachLayerPanel's 'onOkResult' handler. We close it here just in case.
289
- if (closeDialog) {
290
- ModalDialogManager.closeDialog();
327
+ const source = MapLayerSource.fromJSON({ url: props.layerRequiringCredentials.url, name: props.layerRequiringCredentials.name, formatId: props.layerRequiringCredentials.formatId });
328
+ if (source !== undefined) {
329
+ setLayerAttachPending(true);
330
+ const validation = await source.validateSource(true);
331
+ if (isMounted.current) {
332
+ setLayerAttachPending(false);
333
+ }
334
+ updateAuthState(validation);
291
335
  }
292
336
  }
293
- catch (_error) {
294
- ModalDialogManager.closeDialog();
295
- }
296
- })();
297
- }
298
- }, [mapUrl, mapName, mapType, userName, password, props.mapLayerSourceToEdit, props.activeViewport, isSettingsStorageAvailable, doAttach]);
337
+ catch (_error) { }
338
+ }
339
+ })();
340
+ // Only run this effect when the dialog is initialized, otherwise it will it creates undesirable side-effects when 'OK' button is clicked.
341
+ // eslint-disable-next-line react-hooks/exhaustive-deps
342
+ }, []);
299
343
  const dialogContainer = React.useRef(null);
300
344
  const readyToSave = React.useCallback(() => {
301
345
  const credentialsSet = !!userName && !!password;
302
346
  return (!!mapUrl && !!mapName)
303
- && (!serverRequireCredentials || (serverRequireCredentials && credentialsSet) && !layerAttachPending)
347
+ && !layerAttachPending
348
+ && (!serverRequireCredentials || credentialsSet)
304
349
  && !invalidCredentialsProvided;
305
- }, [mapUrl, mapName, userName, password, layerAttachPending, invalidCredentialsProvided, serverRequireCredentials]);
350
+ }, [userName, password, mapUrl, mapName, serverRequireCredentials, layerAttachPending, invalidCredentialsProvided]);
306
351
  const buttonCluster = React.useMemo(() => [
307
352
  { type: DialogButtonType.OK, onClick: handleOk, disabled: !readyToSave() },
308
353
  { type: DialogButtonType.Cancel, onClick: handleCancel },
@@ -313,33 +358,58 @@ export function MapUrlDialog(props) {
313
358
  handleOk();
314
359
  }
315
360
  }, [handleOk, readyToSave]);
361
+ //
362
+ // Monitors authentication method changes
363
+ React.useEffect(() => {
364
+ setServerRequireCredentials(layerAuthMethod === MapLayerAuthType.Basic || layerAuthMethod === MapLayerAuthType.EsriToken);
365
+ }, [layerAuthMethod]);
366
+ // Utility function to get warning message section
367
+ function renderWarningMessage() {
368
+ let node;
369
+ let warningMessage;
370
+ // Get the proper warning message
371
+ if (invalidCredentialsProvided) {
372
+ warningMessage = invalidCredentialsLabel;
373
+ }
374
+ else if (serverRequireCredentials && (!userName || !password)) {
375
+ warningMessage = missingCredentialsLabel;
376
+ }
377
+ // Sometimes we want to add an extra node, such as a button
378
+ let extraNode;
379
+ if (warningMessage !== undefined) {
380
+ return (React.createElement("div", { className: "map-layer-source-warnMessage" },
381
+ React.createElement(Icon, { className: "map-layer-source-warnMessage-icon", iconSpec: "icon-status-warning" }),
382
+ React.createElement("span", { className: "map-layer-source-warnMessage-label" }, warningMessage),
383
+ extraNode));
384
+ }
385
+ else {
386
+ return (React.createElement("span", { className: "map-layer-source-placeholder" }, "\u00A0"));
387
+ }
388
+ return node;
389
+ }
316
390
  return (React.createElement("div", { ref: dialogContainer },
317
391
  React.createElement(Dialog, { className: "map-layer-url-dialog", title: dialogTitle, opened: true, resizable: true, movable: true, modal: true, buttonCluster: buttonCluster, onClose: handleCancel, onEscape: handleCancel, minHeight: 120, maxWidth: 600, titleStyle: { paddingLeft: "10px" }, footerStyle: { paddingBottom: "10px", paddingRight: "10px" }, trapFocus: false },
318
392
  React.createElement("div", { className: "map-layer-url-dialog-content" },
319
393
  React.createElement("div", { className: "map-layer-source-url" },
320
394
  React.createElement("span", { className: "map-layer-source-label" }, typeLabel),
321
- React.createElement(Select, { className: "map-layer-source-select", options: mapTypes, value: mapType, disabled: props.layerRequiringCredentials !== undefined || props.mapLayerSourceToEdit !== undefined, onChange: handleMapTypeSelection }),
395
+ React.createElement(Select, { className: "map-layer-source-select", options: mapTypes, value: mapType, disabled: props.layerRequiringCredentials !== undefined || props.mapLayerSourceToEdit !== undefined || layerAttachPending, onChange: handleMapTypeSelection, size: "small" }),
322
396
  React.createElement("span", { className: "map-layer-source-label" }, nameLabel),
323
- React.createElement(Input, { className: "map-layer-source-input", placeholder: nameInputPlaceHolder, onChange: onNameChange, value: mapName, disabled: props.layerRequiringCredentials !== undefined }),
397
+ React.createElement(Input, { className: "map-layer-source-input", placeholder: nameInputPlaceHolder, onChange: onNameChange, value: mapName, disabled: props.layerRequiringCredentials !== undefined || layerAttachPending }),
324
398
  React.createElement("span", { className: "map-layer-source-label" }, urlLabel),
325
- React.createElement(Input, { className: "map-layer-source-input", placeholder: urlInputPlaceHolder, onKeyPress: handleOnKeyDown, onChange: onUrlChange, disabled: props.mapLayerSourceToEdit !== undefined, value: mapUrl }),
326
- isAuthSupported() && props.mapLayerSourceToEdit === undefined &&
399
+ React.createElement(Input, { className: "map-layer-source-input", placeholder: urlInputPlaceHolder, onKeyPress: handleOnKeyDown, onChange: onUrlChange, disabled: props.mapLayerSourceToEdit !== undefined || layerAttachPending, value: mapUrl }),
400
+ serverRequireCredentials
401
+ && (layerAuthMethod === MapLayerAuthType.Basic || layerAuthMethod === MapLayerAuthType.EsriToken)
402
+ && props.mapLayerSourceToEdit === undefined &&
327
403
  React.createElement(React.Fragment, null,
328
404
  React.createElement("span", { className: "map-layer-source-label" }, userNameLabel),
329
- React.createElement(LabeledInput, { className: "map-layer-source-input", displayStyle: "inline", placeholder: serverRequireCredentials ? userNameRequiredLabel : userNameLabel, status: !userName && serverRequireCredentials ? InputStatus.Warning : undefined, onChange: onUsernameChange }),
405
+ React.createElement(LabeledInput, { className: "map-layer-source-input", displayStyle: "inline", placeholder: serverRequireCredentials ? userNameRequiredLabel : userNameLabel, status: !userName && serverRequireCredentials ? "warning" : undefined, disabled: layerAttachPending, onChange: onUsernameChange, size: "small" }),
330
406
  React.createElement("span", { className: "map-layer-source-label" }, passwordLabel),
331
- React.createElement(LabeledInput, { className: "map-layer-source-input", displayStyle: "inline", type: "password", placeholder: serverRequireCredentials ? passwordRequiredLabel : passwordLabel, status: !password && serverRequireCredentials ? InputStatus.Warning : undefined, onChange: onPasswordChange, onKeyPress: handleOnKeyDown })),
407
+ React.createElement(LabeledInput, { className: "map-layer-source-input", displayStyle: "inline", type: "password", placeholder: serverRequireCredentials ? passwordRequiredLabel : passwordLabel, status: !password && serverRequireCredentials ? "warning" : undefined, disabled: layerAttachPending, onChange: onPasswordChange, onKeyPress: handleOnKeyDown, size: "small" })),
332
408
  isSettingsStorageAvailable && React.createElement("div", { title: settingsStorageDisabled ? noSaveSettingsWarning : "" },
333
409
  React.createElement(Radio, { disabled: settingsStorageDisabled, name: "settingsStorage", value: "iTwin", label: iTwinSettingsLabel, checked: settingsStorage === "iTwin", onChange: onRadioChange }),
334
410
  React.createElement(Radio, { disabled: settingsStorageDisabled, name: "settingsStorage", value: "Model", label: modelSettingsLabel, checked: settingsStorage === "Model", onChange: onRadioChange })))),
335
- React.createElement("div", { className: "map-layer-source-warnMessage" }, warningMessage ?
336
- React.createElement(React.Fragment, null,
337
- React.createElement(Icon, { className: "map-layer-source-warnMessage-icon", iconSpec: "icon-status-warning" }),
338
- React.createElement("span", { className: "map-layer-source-warnMessage-label" }, warningMessage))
339
- :
340
- // Place holder to avoid dialog resize
341
- React.createElement("span", { className: "map-layer-source-placeholder" }, "\u00A0")),
342
- layerAttachPending &&
411
+ renderWarningMessage(),
412
+ (layerAttachPending) &&
343
413
  React.createElement("div", { className: "map-layer-source-progressBar" },
344
414
  React.createElement(ProgressLinear, { indeterminate: true })))));
345
415
  }