@spfx-extensions/package 1.6.2 → 1.6.3

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 (81) hide show
  1. package/.npmignore +16 -16
  2. package/.nvmrc +1 -1
  3. package/LICENSE +674 -674
  4. package/README.md +166 -166
  5. package/config/config-scripts/rename-solution.mjs +22 -0
  6. package/config/config-scripts/update-version.mjs +23 -0
  7. package/config/config-scripts/webpack-patch.mjs +134 -0
  8. package/config/config.json +27 -27
  9. package/config/deploy-azure-storage.json +6 -6
  10. package/config/heft.json +58 -0
  11. package/config/package-solution.json +3 -3
  12. package/config/rig.json +7 -0
  13. package/config/sass.json +3 -2
  14. package/config/serve.json +18 -18
  15. package/config/typescript.json +7 -0
  16. package/config/webpack-patch.json +6 -0
  17. package/config/write-manifests.json +3 -3
  18. package/dist/8d1029da-85e6-48cc-aaaf-37a5bbc0b9be.manifest.json +3 -3
  19. package/dist/d6ca1fc2-0591-4c6d-8a25-cae3262c017b.manifest.json +6 -6
  20. package/dist/debug/83e13c11-682e-4eaa-9ae0-74617ca28f96/ClientSideInstance.xml +8 -8
  21. package/dist/debug/83e13c11-682e-4eaa-9ae0-74617ca28f96/Extension_8d1029da-85e6-48cc-aaaf-37a5bbc0b9be.xml +1 -1
  22. package/dist/debug/83e13c11-682e-4eaa-9ae0-74617ca28f96/WebPart_d6ca1fc2-0591-4c6d-8a25-cae3262c017b.xml +1 -1
  23. package/dist/debug/83e13c11-682e-4eaa-9ae0-74617ca28f96/elements.xml +8 -8
  24. package/dist/debug/AppManifest.xml +1 -1
  25. package/dist/debug/ClientSideAssets/spfx-extension-application-customizer_a3a347c8bd029396ec50.js +2 -0
  26. package/dist/debug/ClientSideAssets/spfx-extensionloader-web-part_3b393b3ec530c336b801.js +2 -0
  27. package/dist/debug/ClientSideAssets/spfx-extensions-classiccustomaction.js +1 -0
  28. package/dist/debug/ClientSideAssets/spfx-extensions-classicwrapper.js +1 -0
  29. package/dist/debug/ClientSideAssets/spfx-extensions-core.js +1 -0
  30. package/dist/debug/ClientSideAssets/spfx-extensions-coreconfigurator.js +175 -0
  31. package/dist/debug/ClientSideAssets/spfx-extensions-loader_6e8eb68a4b27d7c6f742.js +2 -0
  32. package/dist/debug/ClientSideAssets.xml +1 -1
  33. package/dist/debug/ClientSideAssets.xml.config.xml +1 -1
  34. package/dist/debug/_rels/AppManifest.xml.rels +1 -1
  35. package/dist/debug/_rels/ClientSideAssets.xml.rels +1 -1
  36. package/dist/debug/feature_83e13c11-682e-4eaa-9ae0-74617ca28f96.xml +1 -1
  37. package/dist/debug/feature_83e13c11-682e-4eaa-9ae0-74617ca28f96.xml.config.xml +1 -1
  38. package/dist/deploy/sp-fx-extensions.sppkg +0 -0
  39. package/dist/spfx-extension-application-customizer_a3a347c8bd029396ec50.js +2 -0
  40. package/dist/spfx-extensionloader-web-part_3b393b3ec530c336b801.js +2 -0
  41. package/dist/spfx-extensions-classiccustomaction.js +1 -0
  42. package/dist/spfx-extensions-classicwrapper.js +1 -0
  43. package/dist/spfx-extensions-core.js +1 -0
  44. package/dist/spfx-extensions-coreconfigurator.js +175 -0
  45. package/dist/spfx-extensions-loader_6e8eb68a4b27d7c6f742.js +2 -0
  46. package/gulpfile.js +80 -196
  47. package/package.json +68 -63
  48. package/sharepoint/assets/ClientSideInstance.xml +8 -8
  49. package/sharepoint/assets/elements.xml +8 -8
  50. package/src/@types/globals.d.ts +17 -14
  51. package/src/extensions/spfxExtension/SpfxExtensionApplicationCustomizer.manifest.json +17 -17
  52. package/src/extensions/spfxExtension/SpfxExtensionApplicationCustomizer.ts +70 -70
  53. package/src/extensions/spfxExtension/loc/en-us.js +4 -4
  54. package/src/extensions/spfxExtension/loc/myStrings.d.ts +8 -8
  55. package/src/index.ts +1 -1
  56. package/src/services/initCoreService.ts +68 -60
  57. package/src/utilities/constants.ts +3 -3
  58. package/src/webparts/spfxExtensionloader/SpfxExtensionloaderWebPart.manifest.json +34 -34
  59. package/src/webparts/spfxExtensionloader/SpfxExtensionloaderWebPart.module.scss +185 -185
  60. package/src/webparts/spfxExtensionloader/SpfxExtensionloaderWebPart.ts +767 -767
  61. package/src/webparts/spfxExtensionloader/ai.json +37 -37
  62. package/src/webparts/spfxExtensionloader/loc/en-us.js +15 -15
  63. package/src/webparts/spfxExtensionloader/loc/mystrings.d.ts +19 -19
  64. package/tsconfig.json +25 -25
  65. package/dist/debug/ClientSideAssets/spfx-extension-application-customizer_2f30086b4bcdf92ad9a2.js +0 -2
  66. package/dist/debug/ClientSideAssets/spfx-extension-core.js +0 -1
  67. package/dist/debug/ClientSideAssets/spfx-extension-coreconfigurator.js +0 -175
  68. package/dist/debug/ClientSideAssets/spfx-extension-loader_b4583689ca9ee8de6592.js +0 -2
  69. package/dist/debug/ClientSideAssets/spfx-extension-wrapper.js +0 -1
  70. package/dist/debug/ClientSideAssets/spfx-extensionloader-web-part_e3a6a93dc899524b9f3f.js +0 -3
  71. package/dist/spfx-extension-application-customizer_2f30086b4bcdf92ad9a2.js +0 -2
  72. package/dist/spfx-extension-core.js +0 -1
  73. package/dist/spfx-extension-coreconfigurator.js +0 -175
  74. package/dist/spfx-extension-loader_b4583689ca9ee8de6592.js +0 -2
  75. package/dist/spfx-extension-wrapper.js +0 -1
  76. package/dist/spfx-extensionloader-web-part_e3a6a93dc899524b9f3f.js +0 -3
  77. package/src/webparts/spfxExtensionloader/SpfxExtensionloaderWebPart.module.scss.ts +0 -24
  78. /package/dist/debug/ClientSideAssets/{3be36e80-4431-4b52-99c5-0a339b4e696e_color.png → d6ca1fc2-0591-4c6d-8a25-cae3262c017b_color.png} +0 -0
  79. /package/dist/debug/ClientSideAssets/{3be36e80-4431-4b52-99c5-0a339b4e696e_outline.png → d6ca1fc2-0591-4c6d-8a25-cae3262c017b_outline.png} +0 -0
  80. /package/teams/{3be36e80-4431-4b52-99c5-0a339b4e696e_color.png → d6ca1fc2-0591-4c6d-8a25-cae3262c017b_color.png} +0 -0
  81. /package/teams/{3be36e80-4431-4b52-99c5-0a339b4e696e_outline.png → d6ca1fc2-0591-4c6d-8a25-cae3262c017b_outline.png} +0 -0
@@ -1,767 +1,767 @@
1
- import { DisplayMode, Environment, EnvironmentType, ServiceScope, Version } from "@microsoft/sp-core-library";
2
- import {
3
- IPropertyPaneConditionalGroup,
4
- type IPropertyPaneConfiguration,
5
- IPropertyPaneCustomFieldProps,
6
- IPropertyPaneDropdownOption,
7
- IPropertyPaneDropdownProps,
8
- IPropertyPaneField,
9
- IPropertyPaneGroup,
10
- PropertyPaneButton,
11
- PropertyPaneDropdown,
12
- PropertyPaneFieldType,
13
- PropertyPaneLabel
14
- } from "@microsoft/sp-property-pane";
15
- import { BaseClientSideWebPart, IWebPartPropertiesMetadata, WebPartContext } from "@microsoft/sp-webpart-base";
16
- import { ITopActions, ITopActionsField } from "@microsoft/sp-top-actions";
17
- import { SPFxExtensionAppConfig, SPFxExtensionAppDefinition, SPFxExtensionAppIcon, SPFxExtensionAppInstance, SPFxExtensionAppRuntimeConfig, SPFxExtensionAppSearchableData } from "@spfx-extensions/core";
18
- import { APP_BUTTON_LABEL, EDIT_PAGE_AND_SELECT_WEBPART, SELECT_WEBPART, SPFXPREFIX } from "../../utilities/constants";
19
- import {
20
- ThemeProvider,
21
- type IReadonlyTheme,
22
- } from '@microsoft/sp-component-base';
23
- //import * as strings from "SpfxExtensionloaderWebPartStrings";
24
- import styles from "./SpfxExtensionloaderWebPart.module.scss";
25
-
26
- export interface ISpfxExtensionloaderWebPartProps extends SPFxExtensionAppSearchableData {
27
- selectedApp: string;
28
- SPFxExtensionAppConfiguration: SPFxExtensionAppConfig | undefined;
29
- topActions: ITopActionsField[];
30
- }
31
-
32
- type propertyPath = keyof ISpfxExtensionloaderWebPartProps;
33
-
34
-
35
-
36
- export default class SpfxExtensionloaderWebPart extends BaseClientSideWebPart<ISpfxExtensionloaderWebPartProps> {
37
-
38
- configuratorUrl = "/sites/appcatalog/SPFxExtensionsData/SitePages/SPFxExtensionsConfigurator.aspx";
39
- coreInitPromise = new Promise((resolve) => {
40
- import(/* webpackChunkName: "spfx-extension-loader" */"../../services/initCoreService").then(({ initCore }) => {
41
- const envType =
42
- Environment.type === EnvironmentType.SharePoint
43
- ? "SharePoint"
44
- : "ClassicSharePoint";
45
- initCore(envType).then(() => {
46
- this.configuratorUrl = window.__SPFxExtensions.Utils.ConfiguratorPageUrl;
47
- resolve(true);
48
- }).catch((e) => {
49
- console.error(SPFXPREFIX, "Initializing SPFxExtensions Core from WebPart failed", e);
50
- })
51
- }).catch((e) => {
52
- console.error(SPFXPREFIX, "Importing SPFxExtensions Core from WebPart failed", e);
53
- })
54
- });
55
-
56
- SPFxExtensionInstance: SPFxExtensionAppInstance | undefined;
57
- allApps: IPropertyPaneDropdownOption[] = [];
58
- dropDownProps: Partial<IPropertyPaneDropdownProps> = {
59
- options: [],
60
- selectedKey: "",
61
- disabled: true,
62
- };
63
- appDescription = "";
64
- hideAppSelectorWhenAppLoaded = false;
65
- hideConfiguratorButton = false;
66
- configDomElement: HTMLElement | undefined;
67
- themeProvider: ThemeProvider | undefined;
68
- serviceScope: ServiceScope | undefined;
69
- appButtonElements: HTMLElement[] = [];
70
-
71
- webpartSectionElement = document.createElement("section");
72
- webpartSectionTitle = document.createElement("header");
73
- appButtonsWrapper = document.createElement("div");
74
- appButtonsContainer = document.createElement("div");
75
- // token/registration returned by AddAppEventListener so we can remove it
76
- private appAddedListenerRegistration: unknown | undefined;
77
-
78
- // for some reason onRender these properties are not available if accessing `this` on edit mode
79
- // so we copy them in onInit
80
- webPartContext!: WebPartContext;
81
- webPartComponentId!: string;
82
- webPartInstanceId!: string;
83
- webPartWidth!: number;
84
-
85
- public async onInit() {
86
- if (DEBUG) {
87
- console.debug(SPFXPREFIX, "onInit", this.instanceId);
88
- }
89
-
90
- this.themeProvider = this.context.serviceScope.consume(ThemeProvider.serviceKey);
91
- this.serviceScope = this.context.serviceScope;
92
-
93
- this.webPartContext = this.context;
94
- this.webPartInstanceId = this.instanceId;
95
- this.webPartComponentId = this.componentId;
96
- this.webPartWidth = this.width;
97
- // init is not truly awaited, sometimes render is called before init completes
98
- // so we offload it to a promise which we can await in render
99
- await this.coreInitPromise;
100
- }
101
-
102
-
103
- //#region App mounting and property forwarding
104
- openPropertyPane() {
105
- // if (this.context.propertyPane.isPropertyPaneOpen()) {
106
- // this.context.propertyPane.close();
107
- // }
108
- this.context.propertyPane.open();
109
- }
110
-
111
- closePropertyPane() {
112
- this.context.propertyPane.close();
113
- }
114
-
115
- isPropertyPaneOpen() {
116
- return this.context.propertyPane.isPropertyPaneOpen();
117
- }
118
-
119
- saveConfigValue(config: SPFxExtensionAppConfig, raiseEvent = true) {
120
- // const a = config.searchableText;
121
- // delete config.searchableText;
122
- // this.properties.searchableText = a;
123
- this.properties.SPFxExtensionAppConfiguration = config;
124
- if (raiseEvent) {
125
- this.SPFxExtensionInstance?.executeListeners("onConfigurationChange", config);
126
- }
127
- }
128
-
129
- getConfigValue(key?: string) {
130
- if (key) {
131
- let dataByKey = (this.properties[key as keyof ISpfxExtensionloaderWebPartProps] as SPFxExtensionAppConfig | undefined);
132
- if (typeof dataByKey === "undefined") {
133
- dataByKey = this.properties.SPFxExtensionAppConfiguration;
134
- }
135
- return dataByKey;
136
- }
137
- return this.properties.SPFxExtensionAppConfiguration;
138
- }
139
-
140
- getSearchData() {
141
- return {
142
- searchableText: this.properties.searchableText,
143
- searchableHtml: this.properties.searchableHtml,
144
- };
145
- }
146
-
147
- setSearchData(data: SPFxExtensionAppSearchableData) {
148
- this.properties.searchableText = data.searchableText;
149
- this.properties.searchableHtml = data.searchableHtml;
150
- }
151
-
152
- getTopActions() {
153
- return this.properties.topActions ?? [];
154
- }
155
-
156
- setTopActions(fields: ITopActionsField[]) {
157
- this.properties.topActions = fields ?? [];
158
- }
159
-
160
- getThemeProvider() {
161
- return this.themeProvider;
162
- }
163
-
164
- getConfigDomElement() {
165
- return this.configDomElement;
166
- }
167
-
168
- getContext() {
169
- return this.webPartContext;
170
- }
171
-
172
- getServiceScope() {
173
- return this.serviceScope;
174
- }
175
-
176
- private async mountApp(appId: string) {
177
- if (ISDEBUG) {
178
- console.debug(SPFXPREFIX, "Mounting app", appId, "at", this.domElement);
179
- }
180
- //clean HTML
181
- this.domElement.innerHTML = "";
182
- try {
183
- const runTimeConfig: SPFxExtensionAppRuntimeConfig = {
184
- domElement: this.domElement,
185
- //eslint-disable-next-line @typescript-eslint/no-explicit-any
186
- webpart: this as any,
187
- openPropertyPane: () => {
188
- this.openPropertyPane();
189
- },
190
- closePropertyPane: () => {
191
- this.closePropertyPane();
192
- },
193
- isPropertyPaneOpen: () => {
194
- return this.isPropertyPaneOpen();
195
- },
196
- saveConfigValue: (config: SPFxExtensionAppConfig, raise = true) => {
197
- this.saveConfigValue(config, raise);
198
- },
199
- getConfigValue: (key?: string) => {
200
- return this.getConfigValue(key);
201
- },
202
- getSearchableData: () => {
203
- return this.getSearchData();
204
- },
205
- setSearchableData: (data: SPFxExtensionAppSearchableData) => {
206
- this.setSearchData(data);
207
- },
208
- setTopActions: (actions: ITopActionsField[]) => {
209
- this.setTopActions(actions);
210
- },
211
- getTopActions: () => {
212
- return this.getTopActions();
213
- },
214
- getThemeProvider: () => {
215
- return this.getThemeProvider();
216
- },
217
- getConfigDomElement: () => {
218
- return this.getConfigDomElement();
219
- },
220
- getContext: () => {
221
- return this.getContext();
222
- },
223
- getServiceScope: () => {
224
- return this.getServiceScope();
225
- }
226
- };
227
- this.SPFxExtensionInstance = await window.__SPFxExtensions.InstantiateApp(appId, runTimeConfig);
228
- if (!this.SPFxExtensionInstance) {
229
- console.warn(SPFXPREFIX, "App instance is undefined, cannot mount app", appId);
230
- return;
231
- }
232
- const newApp = window.__SPFxExtensions.Apps.find((app) => app.id === appId);
233
- if (newApp) {
234
- this.appDescription = newApp.description;
235
- //spfx specific, for some reason refresh does not work properly (custom field is not rerendered)
236
- // this.context.propertyPane.refresh();
237
- // if (this.context.propertyPane.isPropertyPaneOpen()) {
238
- // this.context.propertyPane.close();
239
- // this.context.propertyPane.open();
240
- // }
241
- }
242
- }
243
- catch (err) {
244
- console.error(SPFXPREFIX, "Error while mounting appid", appId, err);
245
- const error = new Error(`${err}`);
246
- return error;
247
- }
248
- }
249
-
250
- private unmountApp() {
251
- if (this.SPFxExtensionInstance) {
252
- if (ISDEBUG) {
253
- console.debug(
254
- SPFXPREFIX,
255
- "Unmounting app",
256
- this.SPFxExtensionInstance.key,
257
- "at",
258
- this.SPFxExtensionInstance.domElement
259
- );
260
- }
261
- this.SPFxExtensionInstance.unmount?.();
262
- }
263
- this.SPFxExtensionInstance = undefined;
264
- this.domElement.innerHTML = "";
265
- }
266
- //#endregion
267
-
268
- //#region HTML Elements Rendering
269
-
270
- generateIconElement(icon?: SPFxExtensionAppIcon) {
271
- const iconElement = document.createElement("i");
272
- iconElement.classList.add(styles.icon);
273
-
274
- if (!icon) {
275
- iconElement.innerHTML = `<svg fill="currentColor" class="___12fm75w f1w7gpdv fez10in fg4l7m0" aria-hidden="true" width="28" height="28" viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg"><path d="M20.84 2.66a2.25 2.25 0 0 0-3.18 0L13.5 6.8v-.56c0-1.24-1-2.25-2.25-2.25h-7C3.01 4 2 5.01 2 6.25v18c0 .97.78 1.75 1.75 1.75h18c1.24 0 2.25-1 2.25-2.25v-7c0-1.24-1-2.25-2.25-2.25h-.56l4.16-4.15c.88-.88.88-2.3 0-3.19l-4.5-4.5ZM17.31 14.5H13.5v-3.8l3.8 3.8Zm1.41-10.78c.3-.3.77-.3 1.06 0l4.5 4.5c.3.3.3.77 0 1.06l-4.5 4.51c-.3.3-.77.3-1.06 0l-4.5-4.5a.75.75 0 0 1 0-1.07l4.5-4.5ZM12 6.25v8.25H3.5V6.25c0-.41.34-.75.75-.75h7c.41 0 .75.34.75.75Zm-8.5 17.5V16H12v8.5H4.25a.75.75 0 0 1-.75-.75Zm10-7.75h8.25c.41 0 .75.34.75.75v7c0 .42-.34.75-.75.75H13.5V16Z" fill="currentColor"></path></svg>`;
276
- return iconElement;
277
- }
278
-
279
- if (icon.iconType === "font" && icon.fontFamily) {
280
- iconElement.style.fontFamily = icon.fontFamily;
281
- iconElement.classList.add(styles.iconFont);
282
- }
283
-
284
- if (icon.iconType === "url") {
285
- const imageElement = document.createElement("img");
286
- imageElement.src = icon.iconData;
287
- // Accessibility: empty alt to mark decorative image; latest browsers handle this well
288
- imageElement.alt = "";
289
- iconElement.appendChild(imageElement);
290
- }
291
-
292
- if (icon.iconType === "svg") {
293
- iconElement.innerHTML = icon.iconData;
294
- }
295
-
296
- return iconElement;
297
- }
298
-
299
- createAndAppendAppButtons(app: SPFxExtensionAppDefinition) {
300
- const appButtonElement = document.createElement("button");
301
- appButtonElement.title = APP_BUTTON_LABEL;
302
- appButtonElement.ariaLabel = APP_BUTTON_LABEL;
303
- appButtonElement.className = styles.appButton;
304
-
305
- const icon = this.generateIconElement(app.icon);
306
- appButtonElement.append(icon, app.name);
307
- appButtonElement.title = app.name;
308
-
309
- appButtonElement.addEventListener("click", (ev) => {
310
- ev.stopPropagation();
311
- ev.preventDefault();
312
- this.properties.selectedApp = app.id;
313
- this.webpartSectionElement.remove();
314
- this.dropDownProps.selectedKey = app.id;
315
- // refresh to rerender the dropdown and description
316
- this.context.propertyPane.refresh();
317
- this.mountApp(app.id).catch(() => {
318
- // do nothing
319
- });
320
- });
321
-
322
- this.appButtonsContainer.appendChild(appButtonElement);
323
- // track for cleanup on dispose
324
- this.appButtonElements.push(appButtonElement);
325
- }
326
-
327
- createWebpartSection(button?: boolean) {
328
- this.webpartSectionElement.className = styles.applicationListSection;
329
- this.webpartSectionTitle.className = styles.header;
330
- this.webpartSectionElement.appendChild(this.webpartSectionTitle);
331
- if (button) {
332
- this.webpartSectionElement.appendChild(this.appButtonsWrapper);
333
- }
334
- }
335
-
336
- renderDisplayMode() {
337
- this.webpartSectionElement.ariaLabel = EDIT_PAGE_AND_SELECT_WEBPART;
338
- this.webpartSectionTitle.textContent = EDIT_PAGE_AND_SELECT_WEBPART;
339
- this.createWebpartSection();
340
- this.domElement.appendChild(this.webpartSectionElement);
341
- }
342
-
343
- async renderEditMode() {
344
- this.webpartSectionElement.ariaLabel = SELECT_WEBPART;
345
- this.webpartSectionTitle.textContent = SELECT_WEBPART;
346
- this.createWebpartSection(true);
347
-
348
- this.appButtonsContainer.className = styles.appButtonsContainer;
349
- // Clear any previously rendered buttons to avoid duplicates on re-render
350
- this.appButtonsContainer.innerHTML = "";
351
- this.appButtonsWrapper.appendChild(this.appButtonsContainer);
352
- this.appButtonsWrapper.className = styles.appButtonsWrapper;
353
- this.domElement.appendChild(this.webpartSectionElement);
354
-
355
- // Register once: store registration so we can remove it later
356
- if (!this.appAddedListenerRegistration) {
357
- this.appAddedListenerRegistration = window.__SPFxExtensions.AddAppEventListener("appAdded", (app: SPFxExtensionAppDefinition) => {
358
- if (app.isWebPartApp) {
359
- this.createAndAppendAppButtons(app);
360
- }
361
- });
362
- }
363
-
364
- window.__SPFxExtensions.Apps.filter((app) => app.registrationCompleted).forEach(
365
- (app) => {
366
- if (app.isWebPartApp) {
367
- this.createAndAppendAppButtons(app);
368
- }
369
- }
370
- );
371
- try {
372
- await window.__SPFxExtensions.Utils.spAppInitializationPromise;
373
- // this.domElement.appendChild(this.webpartSectionElement);
374
- window.__SPFxExtensions.Utils.appManifestPromises.forEach((promise) => {
375
- const buttonLoader = document.createElement("div");
376
- const loaderSpinner = document.createElement("span");
377
- buttonLoader.className = styles.buttonLoader;
378
- loaderSpinner.className = styles.loader;
379
- buttonLoader.appendChild(loaderSpinner);
380
-
381
- this.appButtonsContainer.append(buttonLoader);
382
- promise
383
- .catch(() => {
384
- //do nothing
385
- }).finally(() => {
386
- buttonLoader.remove();
387
- });
388
- });
389
- }
390
- catch (err) {
391
- console.error(SPFXPREFIX, "Error while awaiting app initialization", err);
392
- return new Error(`Error while awaiting app initialization: ${err}`);
393
- }
394
- }
395
-
396
- async renderEmptyApp() {
397
- if (DEBUG) {
398
- console.debug(SPFXPREFIX, "Rendering display or edit mode empty webpart");
399
- }
400
-
401
- //clean domElement
402
- this.domElement.innerHTML = "";
403
-
404
- this.domElement.className = styles.SPFxExtensionApp;
405
-
406
- if (this.displayMode === DisplayMode.Read) {
407
- this.renderDisplayMode();
408
- return;
409
- }
410
- return this.renderEditMode();
411
- }
412
- //#endregion HTML Elements Rendering
413
-
414
- public async render() {
415
- // might not be required anymore
416
- // initial testing shows that it works without this
417
- // required when adding same Webpart while another instance is already open and configuration pane is open as well.
418
- // if (this.context.propertyPane.isPropertyPaneOpen()) {
419
- // this.onPropertyPaneConfigurationStart();
420
- // }
421
- if (DEBUG) {
422
- console.debug(SPFXPREFIX, "render", this.instanceId);
423
- }
424
- await this.coreInitPromise;
425
- let possibleError: Error | undefined = undefined;
426
-
427
- try {
428
- //in live editing mode dispose is not called when in production build for some reason
429
- //we unmount and remount the app if applicable
430
- if (this.SPFxExtensionInstance) {
431
- if (this.SPFxExtensionInstance.unmountOnRender) {
432
- this.unmountApp();
433
- } else {
434
- this.SPFxExtensionInstance.executeListeners("onRender", undefined);
435
- return;
436
- }
437
- }
438
-
439
- if (this.properties.selectedApp && !this.SPFxExtensionInstance) {
440
- possibleError = await this.mountApp(this.properties.selectedApp);
441
- return;
442
- }
443
-
444
- possibleError = await this.renderEmptyApp();
445
- } finally {
446
- this.renderCompleted(possibleError, true);
447
- }
448
- }
449
-
450
- protected renderCompleted(error?: Error, didUpdate?: boolean): void {
451
- super.renderCompleted(error, didUpdate);
452
- }
453
-
454
- protected get isRenderAsync() {
455
- return true;
456
- }
457
-
458
- protected get propertiesMetadata(): IWebPartPropertiesMetadata {
459
- return {
460
- // selectedApp: {
461
- // isSearchablePlainText: true,
462
- // },
463
- // SPFxExtensionAppConfiguration: {
464
- // dynamicPropertyType: "object",
465
- // },
466
- searchableText: {
467
- isSearchablePlainText: true,
468
- },
469
- searchableHtml: {
470
- isHtmlString: true,
471
- },
472
- };
473
- }
474
-
475
- CustomWebpartConfigurationField(
476
- name: string
477
- ): IPropertyPaneField<IPropertyPaneCustomFieldProps> {
478
- return {
479
- type: PropertyPaneFieldType.Custom,
480
- targetProperty: name,
481
- properties: {
482
- key: name,
483
- onRender: (domElement, _context, _callBack) => {
484
- this.configDomElement = domElement;
485
- // when app instance is loaded forward the render event
486
- this.SPFxExtensionInstance?.instanceLoadPromise
487
- .then(() => {
488
- this.SPFxExtensionInstance?.executeListeners(
489
- "onConfigurationRender",
490
- {
491
- domElement,
492
- }
493
- );
494
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
495
- }).catch((err: any) => {
496
- console.error(SPFXPREFIX, "Error while awaiting app to load", err);
497
- });
498
- },
499
- onDispose: (domElement, _context) => {
500
- this.SPFxExtensionInstance?.executeListeners("onConfigurationClose", { domElement });
501
- this.configDomElement = undefined;
502
- },
503
- // context: this.context,
504
- },
505
- };
506
- }
507
-
508
- protected onPropertyPaneConfigurationStart(): void {
509
- if (ISDEBUG) {
510
- console.debug(SPFXPREFIX, "Property pane configuration start");
511
- }
512
- // wait for all the manifests to load
513
- window.__SPFxExtensions.AllAppAssetsLoadedPromise.then(() => {
514
- // register description if an app is matching this webpart
515
- const selectedApp = window.__SPFxExtensions.Apps.find(
516
- (app) => app.id === this.properties.selectedApp
517
- );
518
- if (selectedApp) {
519
- this.appDescription = selectedApp.description;
520
- this.hideAppSelectorWhenAppLoaded =
521
- selectedApp.hideAppSelectorWhenAppLoaded ?? false;
522
- this.hideConfiguratorButton = selectedApp.hideConfiguratorButton ?? false;
523
- }
524
-
525
- // Clear dropdown options in propertypane
526
- this.dropDownProps.options?.splice(0, this.dropDownProps.options?.length);
527
-
528
- const appOptionsInDropdown: IPropertyPaneDropdownOption[] = window.__SPFxExtensions.Apps.filter(
529
- (app) => app.isWebPartApp
530
- ).map((app) => {
531
- return {
532
- key: app.id,
533
- text: app.name,
534
- };
535
- });
536
-
537
- this.dropDownProps.options?.push(...appOptionsInDropdown);
538
-
539
- // select key
540
- this.dropDownProps.selectedKey = this.properties.selectedApp;
541
- // enable dropdown
542
- this.dropDownProps.disabled = false;
543
- // refresh to rerender the dropdown and description
544
- this.context.propertyPane.refresh();
545
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
546
- }).catch((err: any) => {
547
- console.error(SPFXPREFIX, "Error while awaiting all app assets to load", err);
548
- });
549
- }
550
-
551
- protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
552
- const configuratorButton: IPropertyPaneGroup | IPropertyPaneConditionalGroup = {
553
- groupFields: [
554
- PropertyPaneLabel("spfxExtensionLoaderLabel", {
555
- text: `App not working? Try refreshing the page. Or go to the configuration page.`,
556
- }),
557
- PropertyPaneButton("configuratorButton", {
558
- text: "Open Configurator",
559
- buttonType: 1,
560
- onClick: () => {
561
- window.open(`${this.configuratorUrl}?web=${this.context.pageContext.web.absoluteUrl}`, "_blank");
562
- }
563
- })
564
- ]
565
- };
566
- const cfgButtonGroup = this.hideConfiguratorButton ? [] : [configuratorButton];
567
-
568
- const appSelector: IPropertyPaneGroup | IPropertyPaneConditionalGroup = {
569
- groupFields: [
570
- PropertyPaneDropdown("selectedApp", {
571
- label: "App",
572
- disabled: this.dropDownProps.disabled,
573
- options: this.dropDownProps.options,
574
- selectedKey: this.dropDownProps.selectedKey,
575
- }),
576
- PropertyPaneLabel("selectedAppDecription", {
577
- text: this.appDescription,
578
- }),
579
- ],
580
- }
581
- const cfgAppSelector = this.hideAppSelectorWhenAppLoaded ? [] : [appSelector];
582
-
583
- return {
584
- pages: [
585
- {
586
- groups: [
587
- ...cfgButtonGroup,
588
- ...cfgAppSelector,
589
- {
590
- groupFields: [
591
- this.CustomWebpartConfigurationField(
592
- "SPFxExtensionAppConfiguration"
593
- ),
594
- ],
595
- },
596
- ],
597
- },
598
- ],
599
- };
600
- }
601
-
602
- public getTopActionsConfiguration(): ITopActions | undefined {
603
- return {
604
- topActions: this.properties.topActions ?? [],
605
- onExecute: (actionName: string, updatedValue: unknown) => {
606
- this.SPFxExtensionInstance?.executeListeners("onTopActionExecute", {
607
- actionName,
608
- updatedValue
609
- });
610
- }
611
- }
612
- }
613
-
614
- protected onPropertyPaneConfigurationComplete(): void {
615
- const isPaneOpen = this.context.propertyPane.isPropertyPaneOpen();
616
-
617
- if (DEBUG) {
618
- console.debug(SPFXPREFIX, "onPropertyPaneConfigurationComplete", isPaneOpen);
619
- }
620
-
621
-
622
- // notify close only if the pane is not open
623
- // complete event fires also when config is saved
624
- // This event method is invoked in the following cases:
625
-
626
- // When the CONFIGURATION_COMPLETE_TIMEOUT((currently the value is 5 secs) elapses after the last change.
627
-
628
- // When user clicks the "X" (close) button before the CONFIGURATION_COMPLETE_TIMEOUT elapses.
629
-
630
- // When user clicks the 'Apply' button before the CONFIGURATION_COMPLETE_TIMEOUT elapses.
631
-
632
- // When the user switches web parts then the current web part gets this event.
633
- if (!isPaneOpen && this.SPFxExtensionInstance) {
634
-
635
- this.SPFxExtensionInstance.executeListeners(
636
- "onConfigurationClose",
637
- { domElement: this.configDomElement }
638
- );
639
- }
640
- }
641
-
642
- protected onPropertyPaneFieldChanged(
643
- propertyPath: propertyPath,
644
- //eslint-disable-next-line @typescript-eslint/no-explicit-any
645
- oldValue: any,
646
- //eslint-disable-next-line @typescript-eslint/no-explicit-any
647
- newValue: any
648
- ): void {
649
- if (DEBUG) {
650
- console.debug(SPFXPREFIX, "onPropertyPaneFieldChanged", propertyPath, oldValue, newValue);
651
- }
652
- // if selected app changed unmount the old app
653
- if (propertyPath === "selectedApp") {
654
- if (oldValue && oldValue !== newValue && this.SPFxExtensionInstance) {
655
- const shouldUnmount = confirm(
656
- "You are about to switch app, this will erase all previous app configuration. Are you sure?"
657
- );
658
- if (!shouldUnmount) {
659
- this.properties[propertyPath] = oldValue;
660
- return;
661
- }
662
- this.unmountApp();
663
- }
664
- // if new app was selected, mount it
665
- if (newValue) {
666
- this.webpartSectionElement.remove();
667
- this.mountApp(newValue).catch(() => {
668
- // do nothing
669
- });
670
- }
671
- }
672
- }
673
-
674
- protected onDisplayModeChanged(oldDisplayMode: DisplayMode): void {
675
- if (DEBUG) {
676
- console.debug(SPFXPREFIX, "onDisplayModeChanged", oldDisplayMode);
677
- }
678
- const newDisplayMode = oldDisplayMode === DisplayMode.Edit ? "Read" : "Edit";
679
- this.SPFxExtensionInstance?.executeListeners("onDisplayModeChange", newDisplayMode);
680
- }
681
-
682
- protected onAfterPropertyPaneChangesApplied(): void {
683
- if (DEBUG) {
684
- console.debug(SPFXPREFIX, "onAfterPropertyPaneChangesApplied");
685
- }
686
- this.SPFxExtensionInstance?.executeListeners("onPropertyPaneChangesApplied", undefined);
687
- }
688
-
689
- protected onAfterResize(newWidth: number): void {
690
- if (DEBUG) {
691
- console.debug(SPFXPREFIX, "onAfterResize");
692
- }
693
- this.SPFxExtensionInstance?.executeListeners("onAfterResize", { newWidth });
694
- }
695
-
696
- protected onDispose(): void {
697
- if (DEBUG) {
698
- console.debug(SPFXPREFIX, "onDispose");
699
- }
700
- this.unmountApp();
701
- // Remove the global event listener if we registered it
702
- if (this.appAddedListenerRegistration && window.__SPFxExtensions.RemoveAppEventListener) {
703
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
704
- window.__SPFxExtensions.RemoveAppEventListener(this.appAddedListenerRegistration as any);
705
- this.appAddedListenerRegistration = undefined;
706
- }
707
- this.appButtonElements.forEach((button) => {
708
- button.remove();
709
- });
710
- this.appButtonElements.splice(0, this.appButtonElements.length);
711
- this.webpartSectionElement.remove();
712
- this.webpartSectionTitle.remove();
713
- this.appButtonsWrapper.remove();
714
- this.appButtonsContainer.remove();
715
- }
716
-
717
- // private _getEnvironmentMessage(): Promise<string> {
718
- // if (!!this.context.sdks.microsoftTeams) { // running in Teams, office.com or Outlook
719
- // return this.context.sdks.microsoftTeams.teamsJs.app.getContext()
720
- // .then(context => {
721
- // let environmentMessage: string = "";
722
- // switch (context.app.host.name) {
723
- // case "Office": // running in Office
724
- // environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentOffice : strings.AppOfficeEnvironment;
725
- // break;
726
- // case "Outlook": // running in Outlook
727
- // environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentOutlook : strings.AppOutlookEnvironment;
728
- // break;
729
- // case "Teams": // running in Teams
730
- // case "TeamsModern":
731
- // environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentTeams : strings.AppTeamsTabEnvironment;
732
- // break;
733
- // default:
734
- // environmentMessage = strings.UnknownEnvironment;
735
- // }
736
-
737
- // return environmentMessage;
738
- // });
739
- // }
740
-
741
- // return Promise.resolve(this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentSharePoint : strings.AppSharePointEnvironment);
742
- // }
743
-
744
- protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void {
745
- this.SPFxExtensionInstance?.executeListeners("onThemeChange", currentTheme);
746
- // if (!currentTheme) {
747
- // return;
748
- // }
749
-
750
- // this._isDarkTheme = !!currentTheme.isInverted;
751
- // const {
752
- // semanticColors
753
- // } = currentTheme;
754
-
755
- // if (semanticColors) {
756
- // this.domElement.style.setProperty("--bodyText", semanticColors.bodyText || null);
757
- // this.domElement.style.setProperty("--link", semanticColors.link || null);
758
- // this.domElement.style.setProperty("--linkHovered", semanticColors.linkHovered || null);
759
- // }
760
- }
761
-
762
- protected get dataVersion(): Version {
763
- return Version.parse("1.0");
764
- }
765
-
766
-
767
- }
1
+ import { DisplayMode, Environment, EnvironmentType, ServiceScope, Version } from "@microsoft/sp-core-library";
2
+ import {
3
+ IPropertyPaneConditionalGroup,
4
+ type IPropertyPaneConfiguration,
5
+ IPropertyPaneCustomFieldProps,
6
+ IPropertyPaneDropdownOption,
7
+ IPropertyPaneDropdownProps,
8
+ IPropertyPaneField,
9
+ IPropertyPaneGroup,
10
+ PropertyPaneButton,
11
+ PropertyPaneDropdown,
12
+ PropertyPaneFieldType,
13
+ PropertyPaneLabel
14
+ } from "@microsoft/sp-property-pane";
15
+ import { BaseClientSideWebPart, IWebPartPropertiesMetadata, WebPartContext } from "@microsoft/sp-webpart-base";
16
+ import { ITopActions, ITopActionsField } from "@microsoft/sp-top-actions";
17
+ import { SPFxExtensionAppConfig, SPFxExtensionAppDefinition, SPFxExtensionAppIcon, SPFxExtensionAppInstance, SPFxExtensionAppRuntimeConfig, SPFxExtensionAppSearchableData } from "@spfx-extensions/core";
18
+ import { APP_BUTTON_LABEL, EDIT_PAGE_AND_SELECT_WEBPART, SELECT_WEBPART, SPFXPREFIX } from "../../utilities/constants";
19
+ import {
20
+ ThemeProvider,
21
+ type IReadonlyTheme,
22
+ } from '@microsoft/sp-component-base';
23
+ //import * as strings from "SpfxExtensionloaderWebPartStrings";
24
+ import styles from "./SpfxExtensionloaderWebPart.module.scss";
25
+
26
+ export interface ISpfxExtensionloaderWebPartProps extends SPFxExtensionAppSearchableData {
27
+ selectedApp: string;
28
+ SPFxExtensionAppConfiguration: SPFxExtensionAppConfig | undefined;
29
+ topActions: ITopActionsField[];
30
+ }
31
+
32
+ type propertyPath = keyof ISpfxExtensionloaderWebPartProps;
33
+
34
+
35
+
36
+ export default class SpfxExtensionloaderWebPart extends BaseClientSideWebPart<ISpfxExtensionloaderWebPartProps> {
37
+
38
+ configuratorUrl = "/sites/appcatalog/SPFxExtensionsData/SitePages/SPFxExtensionsConfigurator.aspx";
39
+ coreInitPromise = new Promise((resolve) => {
40
+ import(/* webpackChunkName: "spfx-extensions-loader" */"../../services/initCoreService").then(({ initCore }) => {
41
+ const envType =
42
+ Environment.type === EnvironmentType.SharePoint
43
+ ? "SharePoint"
44
+ : "ClassicSharePoint";
45
+ initCore(envType).then(() => {
46
+ this.configuratorUrl = window.__SPFxExtensions.Utils.ConfiguratorPageUrl;
47
+ resolve(true);
48
+ }).catch((e) => {
49
+ console.error(SPFXPREFIX, "Initializing SPFxExtensions Core from WebPart failed", e);
50
+ })
51
+ }).catch((e) => {
52
+ console.error(SPFXPREFIX, "Importing SPFxExtensions Core from WebPart failed", e);
53
+ })
54
+ });
55
+
56
+ SPFxExtensionInstance: SPFxExtensionAppInstance | undefined;
57
+ allApps: IPropertyPaneDropdownOption[] = [];
58
+ dropDownProps: Partial<IPropertyPaneDropdownProps> = {
59
+ options: [],
60
+ selectedKey: "",
61
+ disabled: true,
62
+ };
63
+ appDescription = "";
64
+ hideAppSelectorWhenAppLoaded = false;
65
+ hideConfiguratorButton = false;
66
+ configDomElement: HTMLElement | undefined;
67
+ themeProvider: ThemeProvider | undefined;
68
+ serviceScope: ServiceScope | undefined;
69
+ appButtonElements: HTMLElement[] = [];
70
+
71
+ webpartSectionElement = document.createElement("section");
72
+ webpartSectionTitle = document.createElement("header");
73
+ appButtonsWrapper = document.createElement("div");
74
+ appButtonsContainer = document.createElement("div");
75
+ // token/registration returned by AddAppEventListener so we can remove it
76
+ private appAddedListenerRegistration: unknown | undefined;
77
+
78
+ // for some reason onRender these properties are not available if accessing `this` on edit mode
79
+ // so we copy them in onInit
80
+ webPartContext!: WebPartContext;
81
+ webPartComponentId!: string;
82
+ webPartInstanceId!: string;
83
+ webPartWidth!: number;
84
+
85
+ public async onInit() {
86
+ if (DEBUG) {
87
+ console.debug(SPFXPREFIX, "onInit", this.instanceId);
88
+ }
89
+
90
+ this.themeProvider = this.context.serviceScope.consume(ThemeProvider.serviceKey);
91
+ this.serviceScope = this.context.serviceScope;
92
+
93
+ this.webPartContext = this.context;
94
+ this.webPartInstanceId = this.instanceId;
95
+ this.webPartComponentId = this.componentId;
96
+ this.webPartWidth = this.width;
97
+ // init is not truly awaited, sometimes render is called before init completes
98
+ // so we offload it to a promise which we can await in render
99
+ await this.coreInitPromise;
100
+ }
101
+
102
+
103
+ //#region App mounting and property forwarding
104
+ openPropertyPane() {
105
+ // if (this.context.propertyPane.isPropertyPaneOpen()) {
106
+ // this.context.propertyPane.close();
107
+ // }
108
+ this.context.propertyPane.open();
109
+ }
110
+
111
+ closePropertyPane() {
112
+ this.context.propertyPane.close();
113
+ }
114
+
115
+ isPropertyPaneOpen() {
116
+ return this.context.propertyPane.isPropertyPaneOpen();
117
+ }
118
+
119
+ saveConfigValue(config: SPFxExtensionAppConfig, raiseEvent = true) {
120
+ // const a = config.searchableText;
121
+ // delete config.searchableText;
122
+ // this.properties.searchableText = a;
123
+ this.properties.SPFxExtensionAppConfiguration = config;
124
+ if (raiseEvent) {
125
+ this.SPFxExtensionInstance?.executeListeners("onConfigurationChange", config);
126
+ }
127
+ }
128
+
129
+ getConfigValue(key?: string) {
130
+ if (key) {
131
+ let dataByKey = (this.properties[key as keyof ISpfxExtensionloaderWebPartProps] as SPFxExtensionAppConfig | undefined);
132
+ if (typeof dataByKey === "undefined") {
133
+ dataByKey = this.properties.SPFxExtensionAppConfiguration;
134
+ }
135
+ return dataByKey;
136
+ }
137
+ return this.properties.SPFxExtensionAppConfiguration;
138
+ }
139
+
140
+ getSearchData() {
141
+ return {
142
+ searchableText: this.properties.searchableText,
143
+ searchableHtml: this.properties.searchableHtml,
144
+ };
145
+ }
146
+
147
+ setSearchData(data: SPFxExtensionAppSearchableData) {
148
+ this.properties.searchableText = data.searchableText;
149
+ this.properties.searchableHtml = data.searchableHtml;
150
+ }
151
+
152
+ getTopActions() {
153
+ return this.properties.topActions ?? [];
154
+ }
155
+
156
+ setTopActions(fields: ITopActionsField[]) {
157
+ this.properties.topActions = fields ?? [];
158
+ }
159
+
160
+ getThemeProvider() {
161
+ return this.themeProvider;
162
+ }
163
+
164
+ getConfigDomElement() {
165
+ return this.configDomElement;
166
+ }
167
+
168
+ getContext() {
169
+ return this.webPartContext;
170
+ }
171
+
172
+ getServiceScope() {
173
+ return this.serviceScope;
174
+ }
175
+
176
+ private async mountApp(appId: string) {
177
+ if (ISDEBUG) {
178
+ console.debug(SPFXPREFIX, "Mounting app", appId, "at", this.domElement);
179
+ }
180
+ //clean HTML
181
+ this.domElement.innerHTML = "";
182
+ try {
183
+ const runTimeConfig: SPFxExtensionAppRuntimeConfig = {
184
+ domElement: this.domElement,
185
+ //eslint-disable-next-line @typescript-eslint/no-explicit-any
186
+ webpart: this as any,
187
+ openPropertyPane: () => {
188
+ this.openPropertyPane();
189
+ },
190
+ closePropertyPane: () => {
191
+ this.closePropertyPane();
192
+ },
193
+ isPropertyPaneOpen: () => {
194
+ return this.isPropertyPaneOpen();
195
+ },
196
+ saveConfigValue: (config: SPFxExtensionAppConfig, raise = true) => {
197
+ this.saveConfigValue(config, raise);
198
+ },
199
+ getConfigValue: (key?: string) => {
200
+ return this.getConfigValue(key);
201
+ },
202
+ getSearchableData: () => {
203
+ return this.getSearchData();
204
+ },
205
+ setSearchableData: (data: SPFxExtensionAppSearchableData) => {
206
+ this.setSearchData(data);
207
+ },
208
+ setTopActions: (actions: ITopActionsField[]) => {
209
+ this.setTopActions(actions);
210
+ },
211
+ getTopActions: () => {
212
+ return this.getTopActions();
213
+ },
214
+ getThemeProvider: () => {
215
+ return this.getThemeProvider();
216
+ },
217
+ getConfigDomElement: () => {
218
+ return this.getConfigDomElement();
219
+ },
220
+ getContext: () => {
221
+ return this.getContext();
222
+ },
223
+ getServiceScope: () => {
224
+ return this.getServiceScope();
225
+ }
226
+ };
227
+ this.SPFxExtensionInstance = await window.__SPFxExtensions.InstantiateApp(appId, runTimeConfig);
228
+ if (!this.SPFxExtensionInstance) {
229
+ console.warn(SPFXPREFIX, "App instance is undefined, cannot mount app", appId);
230
+ return;
231
+ }
232
+ const newApp = window.__SPFxExtensions.Apps.find((app) => app.id === appId);
233
+ if (newApp) {
234
+ this.appDescription = newApp.description;
235
+ //spfx specific, for some reason refresh does not work properly (custom field is not rerendered)
236
+ // this.context.propertyPane.refresh();
237
+ // if (this.context.propertyPane.isPropertyPaneOpen()) {
238
+ // this.context.propertyPane.close();
239
+ // this.context.propertyPane.open();
240
+ // }
241
+ }
242
+ }
243
+ catch (err) {
244
+ console.error(SPFXPREFIX, "Error while mounting appid", appId, err);
245
+ const error = new Error(`${err}`);
246
+ return error;
247
+ }
248
+ }
249
+
250
+ private unmountApp() {
251
+ if (this.SPFxExtensionInstance) {
252
+ if (ISDEBUG) {
253
+ console.debug(
254
+ SPFXPREFIX,
255
+ "Unmounting app",
256
+ this.SPFxExtensionInstance.key,
257
+ "at",
258
+ this.SPFxExtensionInstance.domElement
259
+ );
260
+ }
261
+ this.SPFxExtensionInstance.unmount?.();
262
+ }
263
+ this.SPFxExtensionInstance = undefined;
264
+ this.domElement.innerHTML = "";
265
+ }
266
+ //#endregion
267
+
268
+ //#region HTML Elements Rendering
269
+
270
+ generateIconElement(icon?: SPFxExtensionAppIcon) {
271
+ const iconElement = document.createElement("i");
272
+ iconElement.classList.add(styles.icon);
273
+
274
+ if (!icon) {
275
+ iconElement.innerHTML = `<svg fill="currentColor" class="___12fm75w f1w7gpdv fez10in fg4l7m0" aria-hidden="true" width="28" height="28" viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg"><path d="M20.84 2.66a2.25 2.25 0 0 0-3.18 0L13.5 6.8v-.56c0-1.24-1-2.25-2.25-2.25h-7C3.01 4 2 5.01 2 6.25v18c0 .97.78 1.75 1.75 1.75h18c1.24 0 2.25-1 2.25-2.25v-7c0-1.24-1-2.25-2.25-2.25h-.56l4.16-4.15c.88-.88.88-2.3 0-3.19l-4.5-4.5ZM17.31 14.5H13.5v-3.8l3.8 3.8Zm1.41-10.78c.3-.3.77-.3 1.06 0l4.5 4.5c.3.3.3.77 0 1.06l-4.5 4.51c-.3.3-.77.3-1.06 0l-4.5-4.5a.75.75 0 0 1 0-1.07l4.5-4.5ZM12 6.25v8.25H3.5V6.25c0-.41.34-.75.75-.75h7c.41 0 .75.34.75.75Zm-8.5 17.5V16H12v8.5H4.25a.75.75 0 0 1-.75-.75Zm10-7.75h8.25c.41 0 .75.34.75.75v7c0 .42-.34.75-.75.75H13.5V16Z" fill="currentColor"></path></svg>`;
276
+ return iconElement;
277
+ }
278
+
279
+ if (icon.iconType === "font" && icon.fontFamily) {
280
+ iconElement.style.fontFamily = icon.fontFamily;
281
+ iconElement.classList.add(styles.iconFont);
282
+ }
283
+
284
+ if (icon.iconType === "url") {
285
+ const imageElement = document.createElement("img");
286
+ imageElement.src = icon.iconData;
287
+ // Accessibility: empty alt to mark decorative image; latest browsers handle this well
288
+ imageElement.alt = "";
289
+ iconElement.appendChild(imageElement);
290
+ }
291
+
292
+ if (icon.iconType === "svg") {
293
+ iconElement.innerHTML = icon.iconData;
294
+ }
295
+
296
+ return iconElement;
297
+ }
298
+
299
+ createAndAppendAppButtons(app: SPFxExtensionAppDefinition) {
300
+ const appButtonElement = document.createElement("button");
301
+ appButtonElement.title = APP_BUTTON_LABEL;
302
+ appButtonElement.ariaLabel = APP_BUTTON_LABEL;
303
+ appButtonElement.className = styles.appButton;
304
+
305
+ const icon = this.generateIconElement(app.icon);
306
+ appButtonElement.append(icon, app.name);
307
+ appButtonElement.title = app.name;
308
+
309
+ appButtonElement.addEventListener("click", (ev) => {
310
+ ev.stopPropagation();
311
+ ev.preventDefault();
312
+ this.properties.selectedApp = app.id;
313
+ this.webpartSectionElement.remove();
314
+ this.dropDownProps.selectedKey = app.id;
315
+ // refresh to rerender the dropdown and description
316
+ this.context.propertyPane.refresh();
317
+ this.mountApp(app.id).catch(() => {
318
+ // do nothing
319
+ });
320
+ });
321
+
322
+ this.appButtonsContainer.appendChild(appButtonElement);
323
+ // track for cleanup on dispose
324
+ this.appButtonElements.push(appButtonElement);
325
+ }
326
+
327
+ createWebpartSection(button?: boolean) {
328
+ this.webpartSectionElement.className = styles.applicationListSection;
329
+ this.webpartSectionTitle.className = styles.header;
330
+ this.webpartSectionElement.appendChild(this.webpartSectionTitle);
331
+ if (button) {
332
+ this.webpartSectionElement.appendChild(this.appButtonsWrapper);
333
+ }
334
+ }
335
+
336
+ renderDisplayMode() {
337
+ this.webpartSectionElement.ariaLabel = EDIT_PAGE_AND_SELECT_WEBPART;
338
+ this.webpartSectionTitle.textContent = EDIT_PAGE_AND_SELECT_WEBPART;
339
+ this.createWebpartSection();
340
+ this.domElement.appendChild(this.webpartSectionElement);
341
+ }
342
+
343
+ async renderEditMode() {
344
+ this.webpartSectionElement.ariaLabel = SELECT_WEBPART;
345
+ this.webpartSectionTitle.textContent = SELECT_WEBPART;
346
+ this.createWebpartSection(true);
347
+
348
+ this.appButtonsContainer.className = styles.appButtonsContainer;
349
+ // Clear any previously rendered buttons to avoid duplicates on re-render
350
+ this.appButtonsContainer.innerHTML = "";
351
+ this.appButtonsWrapper.appendChild(this.appButtonsContainer);
352
+ this.appButtonsWrapper.className = styles.appButtonsWrapper;
353
+ this.domElement.appendChild(this.webpartSectionElement);
354
+
355
+ // Register once: store registration so we can remove it later
356
+ if (!this.appAddedListenerRegistration) {
357
+ this.appAddedListenerRegistration = window.__SPFxExtensions.AddAppEventListener("appAdded", (app: SPFxExtensionAppDefinition) => {
358
+ if (app.isWebPartApp) {
359
+ this.createAndAppendAppButtons(app);
360
+ }
361
+ });
362
+ }
363
+
364
+ window.__SPFxExtensions.Apps.filter((app) => app.registrationCompleted).forEach(
365
+ (app) => {
366
+ if (app.isWebPartApp) {
367
+ this.createAndAppendAppButtons(app);
368
+ }
369
+ }
370
+ );
371
+ try {
372
+ await window.__SPFxExtensions.Utils.spAppInitializationPromise;
373
+ // this.domElement.appendChild(this.webpartSectionElement);
374
+ window.__SPFxExtensions.Utils.appManifestPromises.forEach((promise) => {
375
+ const buttonLoader = document.createElement("div");
376
+ const loaderSpinner = document.createElement("span");
377
+ buttonLoader.className = styles.buttonLoader;
378
+ loaderSpinner.className = styles.loader;
379
+ buttonLoader.appendChild(loaderSpinner);
380
+
381
+ this.appButtonsContainer.append(buttonLoader);
382
+ promise
383
+ .catch(() => {
384
+ //do nothing
385
+ }).finally(() => {
386
+ buttonLoader.remove();
387
+ });
388
+ });
389
+ }
390
+ catch (err) {
391
+ console.error(SPFXPREFIX, "Error while awaiting app initialization", err);
392
+ return new Error(`Error while awaiting app initialization: ${err}`);
393
+ }
394
+ }
395
+
396
+ async renderEmptyApp() {
397
+ if (DEBUG) {
398
+ console.debug(SPFXPREFIX, "Rendering display or edit mode empty webpart");
399
+ }
400
+
401
+ //clean domElement
402
+ this.domElement.innerHTML = "";
403
+
404
+ this.domElement.className = styles.SPFxExtensionApp;
405
+
406
+ if (this.displayMode === DisplayMode.Read) {
407
+ this.renderDisplayMode();
408
+ return;
409
+ }
410
+ return this.renderEditMode();
411
+ }
412
+ //#endregion HTML Elements Rendering
413
+
414
+ public async render() {
415
+ // might not be required anymore
416
+ // initial testing shows that it works without this
417
+ // required when adding same Webpart while another instance is already open and configuration pane is open as well.
418
+ // if (this.context.propertyPane.isPropertyPaneOpen()) {
419
+ // this.onPropertyPaneConfigurationStart();
420
+ // }
421
+ if (DEBUG) {
422
+ console.debug(SPFXPREFIX, "render", this.instanceId);
423
+ }
424
+ await this.coreInitPromise;
425
+ let possibleError: Error | undefined = undefined;
426
+
427
+ try {
428
+ //in live editing mode dispose is not called when in production build for some reason
429
+ //we unmount and remount the app if applicable
430
+ if (this.SPFxExtensionInstance) {
431
+ if (this.SPFxExtensionInstance.unmountOnRender) {
432
+ this.unmountApp();
433
+ } else {
434
+ this.SPFxExtensionInstance.executeListeners("onRender", undefined);
435
+ return;
436
+ }
437
+ }
438
+
439
+ if (this.properties.selectedApp && !this.SPFxExtensionInstance) {
440
+ possibleError = await this.mountApp(this.properties.selectedApp);
441
+ return;
442
+ }
443
+
444
+ possibleError = await this.renderEmptyApp();
445
+ } finally {
446
+ this.renderCompleted(possibleError, true);
447
+ }
448
+ }
449
+
450
+ protected renderCompleted(error?: Error, didUpdate?: boolean): void {
451
+ super.renderCompleted(error, didUpdate);
452
+ }
453
+
454
+ protected get isRenderAsync() {
455
+ return true;
456
+ }
457
+
458
+ protected get propertiesMetadata(): IWebPartPropertiesMetadata {
459
+ return {
460
+ // selectedApp: {
461
+ // isSearchablePlainText: true,
462
+ // },
463
+ // SPFxExtensionAppConfiguration: {
464
+ // dynamicPropertyType: "object",
465
+ // },
466
+ searchableText: {
467
+ isSearchablePlainText: true,
468
+ },
469
+ searchableHtml: {
470
+ isHtmlString: true,
471
+ },
472
+ };
473
+ }
474
+
475
+ CustomWebpartConfigurationField(
476
+ name: string
477
+ ): IPropertyPaneField<IPropertyPaneCustomFieldProps> {
478
+ return {
479
+ type: PropertyPaneFieldType.Custom,
480
+ targetProperty: name,
481
+ properties: {
482
+ key: name,
483
+ onRender: (domElement, _context, _callBack) => {
484
+ this.configDomElement = domElement;
485
+ // when app instance is loaded forward the render event
486
+ this.SPFxExtensionInstance?.instanceLoadPromise
487
+ .then(() => {
488
+ this.SPFxExtensionInstance?.executeListeners(
489
+ "onConfigurationRender",
490
+ {
491
+ domElement,
492
+ }
493
+ );
494
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
495
+ }).catch((err: any) => {
496
+ console.error(SPFXPREFIX, "Error while awaiting app to load", err);
497
+ });
498
+ },
499
+ onDispose: (domElement, _context) => {
500
+ this.SPFxExtensionInstance?.executeListeners("onConfigurationClose", { domElement });
501
+ this.configDomElement = undefined;
502
+ },
503
+ // context: this.context,
504
+ },
505
+ };
506
+ }
507
+
508
+ protected onPropertyPaneConfigurationStart(): void {
509
+ if (ISDEBUG) {
510
+ console.debug(SPFXPREFIX, "Property pane configuration start");
511
+ }
512
+ // wait for all the manifests to load
513
+ window.__SPFxExtensions.AllAppAssetsLoadedPromise.then(() => {
514
+ // register description if an app is matching this webpart
515
+ const selectedApp = window.__SPFxExtensions.Apps.find(
516
+ (app) => app.id === this.properties.selectedApp
517
+ );
518
+ if (selectedApp) {
519
+ this.appDescription = selectedApp.description;
520
+ this.hideAppSelectorWhenAppLoaded =
521
+ selectedApp.hideAppSelectorWhenAppLoaded ?? false;
522
+ this.hideConfiguratorButton = selectedApp.hideConfiguratorButton ?? false;
523
+ }
524
+
525
+ // Clear dropdown options in propertypane
526
+ this.dropDownProps.options?.splice(0, this.dropDownProps.options?.length);
527
+
528
+ const appOptionsInDropdown: IPropertyPaneDropdownOption[] = window.__SPFxExtensions.Apps.filter(
529
+ (app) => app.isWebPartApp
530
+ ).map((app) => {
531
+ return {
532
+ key: app.id,
533
+ text: app.name,
534
+ };
535
+ });
536
+
537
+ this.dropDownProps.options?.push(...appOptionsInDropdown);
538
+
539
+ // select key
540
+ this.dropDownProps.selectedKey = this.properties.selectedApp;
541
+ // enable dropdown
542
+ this.dropDownProps.disabled = false;
543
+ // refresh to rerender the dropdown and description
544
+ this.context.propertyPane.refresh();
545
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
546
+ }).catch((err: any) => {
547
+ console.error(SPFXPREFIX, "Error while awaiting all app assets to load", err);
548
+ });
549
+ }
550
+
551
+ protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
552
+ const configuratorButton: IPropertyPaneGroup | IPropertyPaneConditionalGroup = {
553
+ groupFields: [
554
+ PropertyPaneLabel("spfxExtensionLoaderLabel", {
555
+ text: `App not working? Try refreshing the page. Or go to the configuration page.`,
556
+ }),
557
+ PropertyPaneButton("configuratorButton", {
558
+ text: "Open Configurator",
559
+ buttonType: 1,
560
+ onClick: () => {
561
+ window.open(`${this.configuratorUrl}?web=${this.context.pageContext.web.absoluteUrl}`, "_blank");
562
+ }
563
+ })
564
+ ]
565
+ };
566
+ const cfgButtonGroup = this.hideConfiguratorButton ? [] : [configuratorButton];
567
+
568
+ const appSelector: IPropertyPaneGroup | IPropertyPaneConditionalGroup = {
569
+ groupFields: [
570
+ PropertyPaneDropdown("selectedApp", {
571
+ label: "App",
572
+ disabled: this.dropDownProps.disabled,
573
+ options: this.dropDownProps.options,
574
+ selectedKey: this.dropDownProps.selectedKey,
575
+ }),
576
+ PropertyPaneLabel("selectedAppDecription", {
577
+ text: this.appDescription,
578
+ }),
579
+ ],
580
+ }
581
+ const cfgAppSelector = this.hideAppSelectorWhenAppLoaded ? [] : [appSelector];
582
+
583
+ return {
584
+ pages: [
585
+ {
586
+ groups: [
587
+ ...cfgButtonGroup,
588
+ ...cfgAppSelector,
589
+ {
590
+ groupFields: [
591
+ this.CustomWebpartConfigurationField(
592
+ "SPFxExtensionAppConfiguration"
593
+ ),
594
+ ],
595
+ },
596
+ ],
597
+ },
598
+ ],
599
+ };
600
+ }
601
+
602
+ public getTopActionsConfiguration(): ITopActions | undefined {
603
+ return {
604
+ topActions: this.properties.topActions ?? [],
605
+ onExecute: (actionName: string, updatedValue: unknown) => {
606
+ this.SPFxExtensionInstance?.executeListeners("onTopActionExecute", {
607
+ actionName,
608
+ updatedValue
609
+ });
610
+ }
611
+ }
612
+ }
613
+
614
+ protected onPropertyPaneConfigurationComplete(): void {
615
+ const isPaneOpen = this.context.propertyPane.isPropertyPaneOpen();
616
+
617
+ if (DEBUG) {
618
+ console.debug(SPFXPREFIX, "onPropertyPaneConfigurationComplete", isPaneOpen);
619
+ }
620
+
621
+
622
+ // notify close only if the pane is not open
623
+ // complete event fires also when config is saved
624
+ // This event method is invoked in the following cases:
625
+
626
+ // When the CONFIGURATION_COMPLETE_TIMEOUT((currently the value is 5 secs) elapses after the last change.
627
+
628
+ // When user clicks the "X" (close) button before the CONFIGURATION_COMPLETE_TIMEOUT elapses.
629
+
630
+ // When user clicks the 'Apply' button before the CONFIGURATION_COMPLETE_TIMEOUT elapses.
631
+
632
+ // When the user switches web parts then the current web part gets this event.
633
+ if (!isPaneOpen && this.SPFxExtensionInstance) {
634
+
635
+ this.SPFxExtensionInstance.executeListeners(
636
+ "onConfigurationClose",
637
+ { domElement: this.configDomElement }
638
+ );
639
+ }
640
+ }
641
+
642
+ protected onPropertyPaneFieldChanged(
643
+ propertyPath: propertyPath,
644
+ //eslint-disable-next-line @typescript-eslint/no-explicit-any
645
+ oldValue: any,
646
+ //eslint-disable-next-line @typescript-eslint/no-explicit-any
647
+ newValue: any
648
+ ): void {
649
+ if (DEBUG) {
650
+ console.debug(SPFXPREFIX, "onPropertyPaneFieldChanged", propertyPath, oldValue, newValue);
651
+ }
652
+ // if selected app changed unmount the old app
653
+ if (propertyPath === "selectedApp") {
654
+ if (oldValue && oldValue !== newValue && this.SPFxExtensionInstance) {
655
+ const shouldUnmount = confirm(
656
+ "You are about to switch app, this will erase all previous app configuration. Are you sure?"
657
+ );
658
+ if (!shouldUnmount) {
659
+ this.properties[propertyPath] = oldValue;
660
+ return;
661
+ }
662
+ this.unmountApp();
663
+ }
664
+ // if new app was selected, mount it
665
+ if (newValue) {
666
+ this.webpartSectionElement.remove();
667
+ this.mountApp(newValue).catch(() => {
668
+ // do nothing
669
+ });
670
+ }
671
+ }
672
+ }
673
+
674
+ protected onDisplayModeChanged(oldDisplayMode: DisplayMode): void {
675
+ if (DEBUG) {
676
+ console.debug(SPFXPREFIX, "onDisplayModeChanged", oldDisplayMode);
677
+ }
678
+ const newDisplayMode = oldDisplayMode === DisplayMode.Edit ? "Read" : "Edit";
679
+ this.SPFxExtensionInstance?.executeListeners("onDisplayModeChange", newDisplayMode);
680
+ }
681
+
682
+ protected onAfterPropertyPaneChangesApplied(): void {
683
+ if (DEBUG) {
684
+ console.debug(SPFXPREFIX, "onAfterPropertyPaneChangesApplied");
685
+ }
686
+ this.SPFxExtensionInstance?.executeListeners("onPropertyPaneChangesApplied", undefined);
687
+ }
688
+
689
+ protected onAfterResize(newWidth: number): void {
690
+ if (DEBUG) {
691
+ console.debug(SPFXPREFIX, "onAfterResize");
692
+ }
693
+ this.SPFxExtensionInstance?.executeListeners("onAfterResize", { newWidth });
694
+ }
695
+
696
+ protected onDispose(): void {
697
+ if (DEBUG) {
698
+ console.debug(SPFXPREFIX, "onDispose");
699
+ }
700
+ this.unmountApp();
701
+ // Remove the global event listener if we registered it
702
+ if (this.appAddedListenerRegistration && window.__SPFxExtensions.RemoveAppEventListener) {
703
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
704
+ window.__SPFxExtensions.RemoveAppEventListener(this.appAddedListenerRegistration as any);
705
+ this.appAddedListenerRegistration = undefined;
706
+ }
707
+ this.appButtonElements.forEach((button) => {
708
+ button.remove();
709
+ });
710
+ this.appButtonElements.splice(0, this.appButtonElements.length);
711
+ this.webpartSectionElement.remove();
712
+ this.webpartSectionTitle.remove();
713
+ this.appButtonsWrapper.remove();
714
+ this.appButtonsContainer.remove();
715
+ }
716
+
717
+ // private _getEnvironmentMessage(): Promise<string> {
718
+ // if (!!this.context.sdks.microsoftTeams) { // running in Teams, office.com or Outlook
719
+ // return this.context.sdks.microsoftTeams.teamsJs.app.getContext()
720
+ // .then(context => {
721
+ // let environmentMessage: string = "";
722
+ // switch (context.app.host.name) {
723
+ // case "Office": // running in Office
724
+ // environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentOffice : strings.AppOfficeEnvironment;
725
+ // break;
726
+ // case "Outlook": // running in Outlook
727
+ // environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentOutlook : strings.AppOutlookEnvironment;
728
+ // break;
729
+ // case "Teams": // running in Teams
730
+ // case "TeamsModern":
731
+ // environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentTeams : strings.AppTeamsTabEnvironment;
732
+ // break;
733
+ // default:
734
+ // environmentMessage = strings.UnknownEnvironment;
735
+ // }
736
+
737
+ // return environmentMessage;
738
+ // });
739
+ // }
740
+
741
+ // return Promise.resolve(this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentSharePoint : strings.AppSharePointEnvironment);
742
+ // }
743
+
744
+ protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void {
745
+ this.SPFxExtensionInstance?.executeListeners("onThemeChange", currentTheme);
746
+ // if (!currentTheme) {
747
+ // return;
748
+ // }
749
+
750
+ // this._isDarkTheme = !!currentTheme.isInverted;
751
+ // const {
752
+ // semanticColors
753
+ // } = currentTheme;
754
+
755
+ // if (semanticColors) {
756
+ // this.domElement.style.setProperty("--bodyText", semanticColors.bodyText || null);
757
+ // this.domElement.style.setProperty("--link", semanticColors.link || null);
758
+ // this.domElement.style.setProperty("--linkHovered", semanticColors.linkHovered || null);
759
+ // }
760
+ }
761
+
762
+ protected get dataVersion(): Version {
763
+ return Version.parse("1.0");
764
+ }
765
+
766
+
767
+ }