diff --git a/how-to/integrate-with-excel/client/src/apps.ts b/how-to/integrate-with-excel/client/src/apps.ts index b8846d69cc..20ac414ce3 100644 --- a/how-to/integrate-with-excel/client/src/apps.ts +++ b/how-to/integrate-with-excel/client/src/apps.ts @@ -7,7 +7,7 @@ import { AppManifestType, getCurrentSync } from "@openfin/workspace-platform"; * @returns List of app definitions. */ export function getApps(): App[] { - return [OPENFIN_INFORMATION_APP, FDC3_BROADCAST, INTEROP_BROADCAST]; + return [OPENFIN_INFORMATION_APP, FDC3_BROADCAST, INTEROP_BROADCAST, EXCEL_WINDOW]; } /** @@ -92,6 +92,29 @@ const INTEROP_BROADCAST: App = { tags: ["view", "interop", "tools"] }; +/** + * App definition for the FDC3 Interop Broadcast View. + */ +const EXCEL_WINDOW: App = { + appId: "excel-window", + title: "Excel Window", + description: + "A window that launches Excel and listens for changes. This is an alternative to the Excel Home integration.", + manifest: "http://localhost:8080/windows/excel.window.fin.json", + manifestType: "window", + icons: [ + { + src: "http://localhost:8080/common/images/icon-blue.png" + } + ], + contactEmail: "contact@example.com", + supportEmail: "support@example.com", + publisher: "OpenFin", + intents: [], + images: [], + tags: ["window", "excel", "tools"] +}; + /** * Launch the passed app using its manifest type to determine how to launch it. * @param app The app to launch. @@ -99,13 +122,21 @@ const INTEROP_BROADCAST: App = { */ export async function launchApp( app: App -): Promise { +): Promise< + OpenFin.Platform | OpenFin.Identity | OpenFin.View | OpenFin.Application | OpenFin.Window | undefined +> { if (!app.manifest) { console.error(`No manifest was provided for type ${app.manifestType}`); return; } - let ret: OpenFin.Platform | OpenFin.Identity | OpenFin.View | OpenFin.Application | undefined; + let ret: + | OpenFin.Platform + | OpenFin.Identity + | OpenFin.View + | OpenFin.Application + | OpenFin.Window + | undefined; console.log("Application launch requested:", app); @@ -127,6 +158,14 @@ export async function launchApp( break; } + case "window": { + const manifest: OpenFin.WindowCreationOptions = await fetch(app.manifest).then(async (response) => + response.json() + ); + ret = await fin.Window.create(manifest); + break; + } + default: { ret = await fin.Application.startFromManifest(app.manifest); break; diff --git a/how-to/integrate-with-excel/client/src/excel-integration.ts b/how-to/integrate-with-excel/client/src/excel-integration.ts index 66424df56a..c94cca0d7e 100644 --- a/how-to/integrate-with-excel/client/src/excel-integration.ts +++ b/how-to/integrate-with-excel/client/src/excel-integration.ts @@ -1,12 +1,11 @@ -import type OpenFin from "@openfin/core"; -import { enableLogging, getExcelApplication, type Cell, type ExcelApplication } from "@openfin/excel"; import { CLITemplate, type HomeDispatchedSearchResult, type HomeSearchResponse, type HomeSearchResult } from "@openfin/workspace"; -import type { ExcelAssetSettings, ExcelSettings, ExcelWorksheetSettings } from "./shapes"; +import { init as initExcel, closeDown as closeDownExcel, launchExcel } from "./excel"; +import type { ExcelAssetSettings, ExcelSettings } from "./shapes"; /** * Implement the integration for Excel. @@ -24,18 +23,6 @@ export class ExcelIntegration { */ private _settings: ExcelSettings | undefined; - /** - * The Excel application interop. - * @internal - */ - private _excel: ExcelApplication | undefined; - - /** - * The interop clients for the different contexts. - * @internal - */ - private _interopClients?: { [id: string]: OpenFin.InteropClient }; - /** * Initialize the module. * @param settings The settings for the integration. @@ -44,19 +31,7 @@ export class ExcelIntegration { public async initialize(settings: ExcelSettings): Promise { this._settings = settings; - const brokerClient = fin.Interop.connectSync(fin.me.identity.uuid, {}); - const contextGroups = await brokerClient.getContextGroups(); - this._interopClients = {}; - for (const contextGroup of contextGroups) { - const contextClient = fin.Interop.connectSync(fin.me.identity.uuid, {}); - await contextClient.joinContextGroup(contextGroup.id); - await contextClient.addContextHandler(async (ctx) => { - await this.handleContext(contextGroup.id, ctx); - }); - this._interopClients[contextGroup.id] = contextClient; - } - - enableLogging(); + await initExcel(settings); } /** @@ -64,10 +39,7 @@ export class ExcelIntegration { * @returns Nothing. */ public async closedown(): Promise { - for (const client in this._interopClients) { - await this._interopClients[client].removeFromContextGroup(); - } - this._interopClients = {}; + await closeDownExcel(); } /** @@ -76,19 +48,19 @@ export class ExcelIntegration { * @returns The list of results and new filters. */ public async getSearchResults(query: string): Promise { - let filteredAssets: ExcelAssetSettings[] = []; - if (this._settings?.assets) { - if (query.length >= 3) { - filteredAssets = this._settings.assets.filter((a) => - a.title.toLowerCase().includes(query.toLowerCase()) - ); - } else { - filteredAssets = this._settings.assets; - } + if (this._settings?.asset && query.length < 3) { + return { results: [this.createResult(this._settings.asset)] }; + } + if ( + this._settings?.asset && + query.length >= 3 && + this._settings.asset.title.toLowerCase().includes(query.toLowerCase()) + ) { + return { results: [this.createResult(this._settings.asset)] }; } return { - results: filteredAssets.map((a) => this.createResult(a)) + results: [] }; } @@ -103,45 +75,7 @@ export class ExcelIntegration { result.action.trigger === "user-action" && result.data.workbook ) { - const excelAsset = result.data as ExcelAssetSettings; - - await fin.System.launchExternalProcess({ - alias: excelAsset.workbook - }); - - // The workbook is not always available immediately, so start a background process - // to wait for the workbook being ready - - let tryCount = 0; - const intervalId = window.setInterval(async () => { - const excel = await this.getExcel(); - if (excel) { - const workbooks = await excel.getWorkbooks(); - - if (workbooks.length === 0) { - if (tryCount === 10) { - window.clearInterval(intervalId); - } else { - tryCount++; - } - } else { - window.clearInterval(intervalId); - for (const workbook of workbooks) { - const name = await workbook.getName(); - if (name === excelAsset.workbook) { - for (const worksheetSettings of excelAsset.worksheets) { - const worksheet = await workbook.getWorksheetByName(worksheetSettings.name); - if (worksheet) { - await worksheet.addEventListener("change", async (cells) => { - await this.handleCellChanges(excelAsset, worksheetSettings, cells); - }); - } - } - } - } - } - } - }, 1000); + await launchExcel(result.data as ExcelAssetSettings); return true; } @@ -173,97 +107,4 @@ export class ExcelIntegration { templateContent: excelAsset.description }; } - - /** - * Get the excel application. - * @returns The application. - * @internal - */ - private async getExcel(): Promise { - try { - this._excel = await getExcelApplication(); - return this._excel; - } catch (err) { - console.error("Error getting Excel application", err); - } - } - - /** - * Handle the cell changes. - * @param excelAsset The asset to use for processing the cell changes. - * @param worksheet The asset to use for processing the cell changes. - * @param cells The cells that have changed. - */ - private async handleCellChanges( - excelAsset: ExcelAssetSettings, - worksheet: ExcelWorksheetSettings, - cells: Cell[] - ): Promise { - if (this._interopClients && worksheet.cellHandlers) { - for (const cell of cells) { - const cellHandler = worksheet.cellHandlers.find((c) => c.cell === cell.address); - - if (cellHandler) { - const client = this._interopClients[cellHandler.contextGroup]; - if ( - client && - (cellHandler.types.includes("instrument") || cellHandler.types.includes("fdc3.instrument")) - ) { - await client.setContext({ - type: "fdc3.instrument", - id: { - ticker: cell.value, - _source: `excel.${excelAsset.workbook}.${worksheet.name}` - } - }); - } - } - } - } - } - - /** - * Handle a context. - * @param contextGroup The group receiving the context. - * @param context The context being received. - */ - private async handleContext(contextGroup: string, context: OpenFin.Context): Promise { - if (this._settings?.assets) { - const excel = await this.getExcel(); - if (excel) { - const workbooks = await excel.getWorkbooks(); - for (const workbook of workbooks) { - const workbookName = await workbook.getName(); - - const connectedWorkbook = this._settings?.assets.find((a) => a.workbook === workbookName); - if (connectedWorkbook?.worksheets) { - for (const worksheetSettings of connectedWorkbook.worksheets) { - if (worksheetSettings.cellHandlers) { - const incomingSource = `excel.${workbookName}.${worksheetSettings.name}`; - - if (incomingSource !== context?.id?._source) { - const cellHandlers = worksheetSettings.cellHandlers?.filter( - (ch) => ch.contextGroup === contextGroup && ch.types.includes(context.type) - ); - for (const cellHandler of cellHandlers) { - const worksheet = await workbook.getWorksheetByName(worksheetSettings.name); - - if (worksheet) { - let cellValue: string | undefined; - if (context.type === "fdc3.instrument" || context.type === "instrument") { - cellValue = context.id?.ticker; - } - if (cellValue !== undefined) { - await worksheet.setCellValues(cellHandler.cell, [[cellValue]]); - } - } - } - } - } - } - } - } - } - } - } } diff --git a/how-to/integrate-with-excel/client/src/excel-window.ts b/how-to/integrate-with-excel/client/src/excel-window.ts new file mode 100644 index 0000000000..5ef61a4ca4 --- /dev/null +++ b/how-to/integrate-with-excel/client/src/excel-window.ts @@ -0,0 +1,59 @@ +import { init as initExcel, launchExcel } from "./excel"; +import type { ExcelSettings } from "./shapes"; + +window.addEventListener("DOMContentLoaded", async () => { + const root = "http://localhost:8080"; + const excelSettings: ExcelSettings = { + appAsset: { + alias: "excel-interop-example.xlsx", + version: "0.0.5", + src: `${root}/assets/excel-interop-example.zip`, + target: "excel-interop-example.xlsx" + }, + icon: `${root}/assets/excel.svg`, + asset: { + title: "Excel Interop Example", + description: "Demonstrate interop with Excel workbook", + workbook: "excel-interop-example.xlsx", + worksheets: [ + { + name: "Sheet1", + cellHandlers: [ + { + cell: "$B$3", + types: ["instrument", "fdc3.instrument"], + contextGroup: "green" + }, + { + cell: "$B$4", + types: ["instrument", "fdc3.instrument"], + contextGroup: "purple" + }, + { + cell: "$B$5", + types: ["instrument", "fdc3.instrument"], + contextGroup: "orange" + }, + { + cell: "$B$6", + types: ["instrument", "fdc3.instrument"], + contextGroup: "red" + }, + { + cell: "$B$7", + types: ["instrument", "fdc3.instrument"], + contextGroup: "pink" + }, + { + cell: "$B$8", + types: ["instrument", "fdc3.instrument"], + contextGroup: "yellow" + } + ] + } + ] + } + }; + await initExcel(excelSettings); + await launchExcel(excelSettings.asset); +}); diff --git a/how-to/integrate-with-excel/client/src/excel.ts b/how-to/integrate-with-excel/client/src/excel.ts new file mode 100644 index 0000000000..9c0c18e29a --- /dev/null +++ b/how-to/integrate-with-excel/client/src/excel.ts @@ -0,0 +1,242 @@ +import type OpenFin from "@openfin/core"; +import { enableLogging, getExcelApplication, type Cell, type ExcelApplication } from "@openfin/excel"; +import type { ExcelAssetSettings, ExcelSettings, ExcelWorksheetSettings } from "./shapes"; + +let excel: ExcelApplication | undefined; +/** + * The interop clients for the different contexts. + */ +let interopClients: { [id: string]: OpenFin.InteropClient } | undefined; + +let settings: ExcelSettings | undefined; + +let workbookFound: boolean; + +/** + * Initialize the Excel interop. + * @param excelSettings The settings to use for initializing the Excel interop. + */ +export async function init(excelSettings: ExcelSettings): Promise { + if (!settings) { + settings = excelSettings; + const brokerClient = fin.Interop.connectSync(fin.me.identity.uuid, {}); + const contextGroups = await brokerClient.getContextGroups(); + interopClients = {}; + for (const contextGroup of contextGroups) { + const contextClient = fin.Interop.connectSync(fin.me.identity.uuid, {}); + await contextClient.joinContextGroup(contextGroup.id); + await contextClient.addContextHandler(async (ctx) => { + await handleContext(contextGroup.id, ctx); + }); + interopClients[contextGroup.id] = contextClient; + } + + enableLogging(); + } +} + +/** + * Get the Excel application. + * @param assetSettings The settings to use for getting the Excel application. + * @returns The Excel application. + */ +export async function launchExcel(assetSettings: ExcelAssetSettings): Promise { + const assetAvailable = await getAppAsset(); + if (!assetAvailable) { + return false; + } + await fin.System.launchExternalProcess({ + alias: settings?.appAsset.alias + }); + // The workbook is not always available immediately, so start a background process + // to wait for the workbook being ready + + await listenToExcel(assetSettings); + return true; +} + +/** + * Listen to the Excel application. + * @param assetSettings The settings to use for listening to the Excel application. + * @param tryCount The number of times the function has been called. + * @returns A promise that resolves when the Excel application is available. + */ +async function listenToExcel(assetSettings: ExcelAssetSettings, tryCount = 0): Promise { + if (tryCount === 10) { + console.error("Excel workbook not available after 10 attempts"); + return; + } + if (workbookFound) { + console.info("Workbook already found"); + return; + } + setTimeout(async () => { + const excelInstance = await getExcel(); + if (excelInstance) { + const workbooks = await excelInstance.getWorkbooks(); + if (workbooks.length === 0) { + tryCount++; + await listenToExcel(assetSettings, tryCount); + } else { + workbookFound = false; + for (const workbook of workbooks) { + const name = await workbook.getName(); + if (name === assetSettings.workbook) { + for (const worksheetSettings of assetSettings.worksheets) { + const worksheet = await workbook.getWorksheetByName(worksheetSettings.name); + if (worksheet) { + workbookFound = true; + await worksheet.addEventListener("change", async (cells) => { + await handleCellChanges(assetSettings, worksheetSettings, cells); + }); + } + } + } + } + if (!workbookFound) { + tryCount++; + await listenToExcel(assetSettings, tryCount); + } + } + } + }, 1000); +} + +/** + * Do any cleanup that is required. + */ +export async function closeDown(): Promise { + for (const client in interopClients) { + await interopClients[client].removeFromContextGroup(); + } + interopClients = {}; +} + +/** + * Gets the configured app asset and ensures it is available. + * @returns A boolean indicating if the app asset is available. + */ +async function getAppAsset(): Promise { + let availableAppAsset: OpenFin.AppAssetInfo | undefined; + try { + if (settings?.appAsset !== undefined) { + availableAppAsset = await fin.System.getAppAssetInfo({ alias: settings.appAsset.alias }); + } + } catch (appAssetError) { + console.debug( + `App asset info for alias: ${settings?.appAsset.alias} is not available. Response from getAppAssetInfo`, + appAssetError + ); + } + if ( + (availableAppAsset === undefined || settings?.appAsset.version !== availableAppAsset.version) && + settings?.appAsset !== undefined + ) { + console.info(`App asset with alias: ${settings?.appAsset.alias} does not exist in memory. Fetching it.`); + try { + await fin.System.downloadAsset(settings.appAsset, (progress) => { + const downloadedPercent = Math.floor((progress.downloadedBytes / progress.totalBytes) * 100); + console.info( + `Downloaded ${downloadedPercent}% of app asset with alias of ${settings?.appAsset.alias}` + ); + }); + } catch (error) { + console.error(`Error trying to download app asset with alias: ${settings?.appAsset.alias}`, error); + return false; + } + } + return true; +} + +/** + * Get the excel application. + * @returns The application. + * @internal + */ +async function getExcel(): Promise { + try { + excel = await getExcelApplication(); + return excel; + } catch (err) { + console.error("Error getting Excel application", err); + } +} + +/** + * Handle the cell changes. + * @param excelAsset The asset to use for processing the cell changes. + * @param worksheet The asset to use for processing the cell changes. + * @param cells The cells that have changed. + */ +async function handleCellChanges( + excelAsset: ExcelAssetSettings, + worksheet: ExcelWorksheetSettings, + cells: Cell[] +): Promise { + if (interopClients && worksheet.cellHandlers) { + for (const cell of cells) { + const cellHandler = worksheet.cellHandlers.find((c) => c.cell === cell.address); + + if (cellHandler) { + const client = interopClients[cellHandler.contextGroup]; + if ( + client && + (cellHandler.types.includes("instrument") || cellHandler.types.includes("fdc3.instrument")) + ) { + await client.setContext({ + type: "fdc3.instrument", + id: { + ticker: cell.value, + _source: `excel.${excelAsset.workbook}.${worksheet.name}` + } + }); + } + } + } + } +} + +/** + * Handle a context. + * @param contextGroup The group receiving the context. + * @param context The context being received. + */ +async function handleContext(contextGroup: string, context: OpenFin.Context): Promise { + if (settings?.asset) { + const excelInstance = await getExcel(); + if (excelInstance) { + const workbooks = await excelInstance.getWorkbooks(); + for (const workbook of workbooks) { + const workbookName = await workbook.getName(); + + const connectedWorkbook = settings?.asset; + if (connectedWorkbook?.worksheets) { + for (const worksheetSettings of connectedWorkbook.worksheets) { + if (worksheetSettings.cellHandlers) { + const incomingSource = `excel.${workbookName}.${worksheetSettings.name}`; + + if (incomingSource !== context?.id?._source) { + const cellHandlers = worksheetSettings.cellHandlers?.filter( + (ch) => ch.contextGroup === contextGroup && ch.types.includes(context.type) + ); + for (const cellHandler of cellHandlers) { + const worksheet = await workbook.getWorksheetByName(worksheetSettings.name); + + if (worksheet) { + let cellValue: string | undefined; + if (context.type === "fdc3.instrument" || context.type === "instrument") { + cellValue = context.id?.ticker; + } + if (cellValue !== undefined) { + await worksheet.setCellValues(cellHandler.cell, [[cellValue]]); + } + } + } + } + } + } + } + } + } + } +} diff --git a/how-to/integrate-with-excel/client/src/provider.ts b/how-to/integrate-with-excel/client/src/provider.ts index 27af9aee23..8d5fb451cd 100644 --- a/how-to/integrate-with-excel/client/src/provider.ts +++ b/how-to/integrate-with-excel/client/src/provider.ts @@ -99,8 +99,8 @@ async function initializeWorkspaceComponents(): Promise { title: app.title, icon: app.icons[0]?.src, data: app, - label: "View", - actions: [{ name: "Launch View", hotkey: "enter" }], + label: "App", + actions: [{ name: "Launch App", hotkey: "enter" }], description: app.description, shortDescription: app.description, template: CLITemplate.SimpleText, diff --git a/how-to/integrate-with-excel/client/src/shapes.ts b/how-to/integrate-with-excel/client/src/shapes.ts index 8104210e01..1d19b40367 100644 --- a/how-to/integrate-with-excel/client/src/shapes.ts +++ b/how-to/integrate-with-excel/client/src/shapes.ts @@ -1,3 +1,5 @@ +import type { OpenFin } from "@openfin/core"; + /** * Custom settings for the application. */ @@ -18,9 +20,14 @@ export interface ExcelSettings { icon: string; /** - * The list of assets for the home integration. + * The asset for the excel integration. + */ + asset: ExcelAssetSettings; + + /** + * The app asset information. */ - assets: ExcelAssetSettings[]; + appAsset: OpenFin.AppAssetInfo; } /** diff --git a/how-to/integrate-with-excel/client/webpack.config.js b/how-to/integrate-with-excel/client/webpack.config.js index 93e9dbff13..e757032de3 100644 --- a/how-to/integrate-with-excel/client/webpack.config.js +++ b/how-to/integrate-with-excel/client/webpack.config.js @@ -20,5 +20,32 @@ module.exports = [ filename: 'provider.bundle.js', path: path.resolve(__dirname, '..', 'public', 'js') } + }, + { + entry: './client/src/excel-window.ts', + devtool: 'source-map', + module: { + rules: [ + { + test: /\.tsx?$/, + use: 'ts-loader', + exclude: /node_modules/ + } + ] + }, + resolve: { + extensions: ['.tsx', '.ts', '.js'] + }, + externals: { fin: 'fin' }, + output: { + filename: 'excel.bundle.js', + library: { + type: 'module' + }, + path: path.resolve(__dirname, '..', 'public', 'js') + }, + experiments: { + outputModule: true + } } ]; diff --git a/how-to/integrate-with-excel/public/manifest.fin.json b/how-to/integrate-with-excel/public/manifest.fin.json index ad4d6f6f28..a8b0449f42 100644 --- a/how-to/integrate-with-excel/public/manifest.fin.json +++ b/how-to/integrate-with-excel/public/manifest.fin.json @@ -39,15 +39,7 @@ "name": "Integrate With Excel - v19.1.0", "target": ["desktop", "start-menu"] }, - "appAssets": [ - { - "alias": "excel-interop-example.xlsx", - "version": "0.0.5", - "src": "http://localhost:8080/assets/excel-interop-example.zip", - "target": "excel-interop-example.xlsx", - "forceDownload": true - } - ], + "appAssets": [], "supportInformation": { "company": "OpenFin", "product": "Workspace Starter - Integrate With Excel - Client", @@ -56,51 +48,55 @@ }, "customSettings": { "excel": { + "appAsset": { + "alias": "excel-interop-example.xlsx", + "version": "0.0.6", + "src": "http://localhost:8080/assets/excel-interop-example.zip", + "target": "excel-interop-example.xlsx" + }, "icon": "http://localhost:8080/assets/excel.svg", - "assets": [ - { - "title": "Excel Interop Example", - "description": "Demonstrate interop with Excel workbook", - "workbook": "excel-interop-example.xlsx", - "worksheets": [ - { - "name": "Sheet1", - "cellHandlers": [ - { - "cell": "$B$3", - "types": ["instrument", "fdc3.instrument"], - "contextGroup": "green" - }, - { - "cell": "$B$4", - "types": ["instrument", "fdc3.instrument"], - "contextGroup": "purple" - }, - { - "cell": "$B$5", - "types": ["instrument", "fdc3.instrument"], - "contextGroup": "orange" - }, - { - "cell": "$B$6", - "types": ["instrument", "fdc3.instrument"], - "contextGroup": "red" - }, - { - "cell": "$B$7", - "types": ["instrument", "fdc3.instrument"], - "contextGroup": "pink" - }, - { - "cell": "$B$8", - "types": ["instrument", "fdc3.instrument"], - "contextGroup": "yellow" - } - ] - } - ] - } - ] + "asset": { + "title": "Excel Interop Example", + "description": "Demonstrate interop with Excel workbook", + "workbook": "excel-interop-example.xlsx", + "worksheets": [ + { + "name": "Sheet1", + "cellHandlers": [ + { + "cell": "$B$3", + "types": ["instrument", "fdc3.instrument"], + "contextGroup": "green" + }, + { + "cell": "$B$4", + "types": ["instrument", "fdc3.instrument"], + "contextGroup": "purple" + }, + { + "cell": "$B$5", + "types": ["instrument", "fdc3.instrument"], + "contextGroup": "orange" + }, + { + "cell": "$B$6", + "types": ["instrument", "fdc3.instrument"], + "contextGroup": "red" + }, + { + "cell": "$B$7", + "types": ["instrument", "fdc3.instrument"], + "contextGroup": "pink" + }, + { + "cell": "$B$8", + "types": ["instrument", "fdc3.instrument"], + "contextGroup": "yellow" + } + ] + } + ] + } } } } diff --git a/how-to/integrate-with-excel/public/windows/excel.html b/how-to/integrate-with-excel/public/windows/excel.html new file mode 100644 index 0000000000..f74d4f5d66 --- /dev/null +++ b/how-to/integrate-with-excel/public/windows/excel.html @@ -0,0 +1,20 @@ + + + + + + Alternative Excel Example + + + + + + + +
Example Excel Launcher and Listener.
+
+ If Excel is launched through this and the Excel entry in Home there will be multiple context messages + published as both entries are listening. +
+ + diff --git a/how-to/integrate-with-excel/public/windows/excel.window.fin.json b/how-to/integrate-with-excel/public/windows/excel.window.fin.json new file mode 100644 index 0000000000..ead7fa044d --- /dev/null +++ b/how-to/integrate-with-excel/public/windows/excel.window.fin.json @@ -0,0 +1,11 @@ +{ + "url": "http://localhost:8080/windows/excel.html", + "name": "excel", + "permissions": { + "System": { + "launchExternalProcess": true, + "downloadAsset": true + } + }, + "autoShow": false +} diff --git a/how-to/workspace-platform-starter/CHANGELOG.md b/how-to/workspace-platform-starter/CHANGELOG.md index 62940f1396..f425a61e55 100644 --- a/how-to/workspace-platform-starter/CHANGELOG.md +++ b/how-to/workspace-platform-starter/CHANGELOG.md @@ -28,9 +28,15 @@ - Updated example-notification-service-app to show raising a notification that has a call to action that passes data back to itself (or a new instance if it is closed) though a context listener. - Updated the WPS Platform Override Module so that our workspace platform override will log an error and then throw an exception if saving, updating or deleting a page or a workspace fails. The exception is required so that the default workspace platform code knows that the indicator should show failure instead of success. - Updated integration template that is used when you use npm run generate-module integrations "Your Integration" so that it includes example results and examples of handling result clicks so that you have an easier starting point. -- Updated example-notification-source so that you can specify a url to post notifications to (it will wrap the request in a message property and send it to the server you specify via post: { url: "" } settings. Our example node-starter supports messages being posted to . +- Updated example-notification-source so that you can specify a url to post notifications to. It will wrap the request in a message property and send it to the server you specify via post: { url: "" } settings. Our example node-starter supports messages being posted to . - Updated the notification service example app so that if you select the endpoint example action it will post a notification to the notification source create endpoint. - BUGFIX - Updated Snapshot launching logic as there was a bug when updating a page's layout (to ensure view names are regenerated when using app naming conventions) in a multi page window which resulted in the page layout being assigned at the wrong level. +- Enhancement - Previously if you were generating a log module you needed to avoid using the injected createLogger function as you would end up calling yourself and ending up in a loop. The module logic has been updated to pass an alternative createLogger function to log modules that only logs to the console (so that it can still be used for debugging) but doesn't call itself or other log sinks (log modules). +- Enhancement - You may want to enable snap but not have auto window registration. This may be because you only want snap for your native windows or you might want to have your own rules (via a custom platform provider module) on which windows should be tracked (outside of the default rules snap has such as includeInSnapshots false meaning don't track). The snapProvider config now lets you specify "enableAutoWindowRegistration": false to disable auto tracking. +- Example Added - Added an additional [Platform Override Example](./client/src/modules/platform-override/snap-window-selection-override/README.md) that shows how you can enable Snap but disable auto window tracking so that you can plug in your own logic to determine which windows should be tracked by the snap server. +- Enhancement - Apps now launch on the monitor that Stock, Dock and Home are on in a multi monitor setup. Added support for specifying a calling identity when launching an app. This is now used by the apps integration module for launching apps, called by the launch app action used by the dock, called by the launch app function triggered by Store. +- Updated the example content creation rule to support specifying a default view placement position when window.open is called from inside a view. Defaulted it to "stack-right" so that it has the same behavior as Chrome/Edge when window.open is called without a target (opens a new tab to the right hand side of the tab). It is now turned on by default in the main manifest (search for view-position-content-creation)Turned it on by default (the example app content Content Creation Examples will now work). See [How to Customize Content Creation Rules](./docs/how-to-customize-content-creation-rules.md). +- Change - Updated the platform mapper (configurable option to make snapshots smaller when sending to custom endpoints) so that it no longer strips out snapshotDetails as we received feedback that it resulted in monitor placement not working as it should. ## v19.0.0 diff --git a/how-to/workspace-platform-starter/client/src/framework/actions.ts b/how-to/workspace-platform-starter/client/src/framework/actions.ts index a1ebe3c9ae..bc7fb9e04e 100644 --- a/how-to/workspace-platform-starter/client/src/framework/actions.ts +++ b/how-to/workspace-platform-starter/client/src/framework/actions.ts @@ -14,13 +14,11 @@ import { createLogger } from "./logger-provider"; import * as menuProvider from "./menu"; import { closedownModules, initializeModules, loadModules } from "./modules"; import type { ActionHelpers, Actions, ActionsProviderOptions } from "./shapes/actions-shapes"; -import type { LaunchPreference, PlatformApp } from "./shapes/app-shapes"; -import type { WindowPositioningOptions } from "./shapes/browser-shapes"; +import type { PlatformApp } from "./shapes/app-shapes"; import type { ModuleEntry, ModuleHelpers } from "./shapes/module-shapes"; import * as shareProvider from "./share"; import * as themeProvider from "./themes"; import { isEmpty, isStringValue } from "./utils"; -import { getWindowPositionUsingStrategy } from "./utils-position"; import * as homeComponent from "./workspace/home"; import * as notificationsComponent from "./workspace/notifications"; @@ -30,7 +28,6 @@ let modules: ModuleEntry[] | undefined; const customActionMap: CustomActionsMap = {}; let platformActionMap: CustomActionsMap | undefined; let isInitialized: boolean = false; -let windowPositioningOptions: WindowPositioningOptions | undefined; /** * These Ids are for actions built in to the platform. @@ -48,13 +45,11 @@ export const PLATFORM_ACTION_IDS = { * Initialize the actions provider. * @param options Options for the actions provider. * @param helpers Module helpers to pass to any loaded modules. - * @param windowPositioning Options for positioning windows. * @returns The platform action map. */ export async function init( options: ActionsProviderOptions | undefined, - helpers: ModuleHelpers, - windowPositioning: WindowPositioningOptions + helpers: ModuleHelpers ): Promise { if (isInitialized) { logger.error("The actions can only be used once when configuring the platform"); @@ -73,10 +68,6 @@ export async function init( updateToolbarButtons }); } - if (!isEmpty(windowPositioning)) { - logger.info("Initializing with window positioning options", windowPositioning); - windowPositioningOptions = windowPositioning; - } await buildActions(); @@ -256,12 +247,7 @@ async function getPlatformActions(): Promise { */ async function launchAppAction(app: PlatformApp, clientIdentity: OpenFin.Identity): Promise { if (!isEmpty(app)) { - let launchPreference: LaunchPreference | undefined; - const bounds = await getWindowPositionUsingStrategy(windowPositioningOptions, clientIdentity); - if (!isEmpty(bounds)) { - launchPreference = { bounds }; - } - await launch(app, launchPreference); + await launch(app, undefined, clientIdentity); } else { logger.error("Unable to do launch app action as an app object was not passed"); } diff --git a/how-to/workspace-platform-starter/client/src/framework/integrations.ts b/how-to/workspace-platform-starter/client/src/framework/integrations.ts index fc4e21b1ae..1188d84941 100644 --- a/how-to/workspace-platform-starter/client/src/framework/integrations.ts +++ b/how-to/workspace-platform-starter/client/src/framework/integrations.ts @@ -31,6 +31,7 @@ import * as templateHelpers from "./templates"; import { createButton, createContainer, createHelp, createImage, createText, createTitle } from "./templates"; import { isEmpty, isStringValue } from "./utils"; import { getVersionInfo } from "./version"; +import { getHomeIdentity } from "./workspace/identity"; const logger = createLogger("Integrations"); @@ -340,6 +341,14 @@ export async function itemSelection( const foundIntegration = integrationModules.find((hi) => hi.definition.id === result.data?.providerId); if (foundIntegration?.implementation?.itemSelection) { + if (result.dispatcherIdentity.uuid === result.dispatcherIdentity.name) { + // If the dispatcher is the same as the name, then it would be the provider + // and the provider is routing the message from Home + // we provide the home name to the dispatcherIdentity.name + // to allow the integration to determine the monitor Home is on. + const homeIdentity = getHomeIdentity(); + result.dispatcherIdentity.name = homeIdentity.name; + } return foundIntegration.implementation.itemSelection(result, lastResponse); } } diff --git a/how-to/workspace-platform-starter/client/src/framework/launch.ts b/how-to/workspace-platform-starter/client/src/framework/launch.ts index 023d7d8ed8..4dd1c4d04c 100644 --- a/how-to/workspace-platform-starter/client/src/framework/launch.ts +++ b/how-to/workspace-platform-starter/client/src/framework/launch.ts @@ -12,6 +12,7 @@ import * as endpointProvider from "./endpoint"; import { createLogger } from "./logger-provider"; import { MANIFEST_TYPES } from "./manifest-types"; import { bringViewToFront, bringWindowToFront, doesViewExist, doesWindowExist } from "./platform/browser"; +import { getSettings } from "./settings"; import type { NativeLaunchOptions, PlatformApp, @@ -22,20 +23,25 @@ import type { PreferenceConstraintUrl, HostLaunchOptions } from "./shapes/app-shapes"; +import type { WindowPositioningOptions } from "./shapes/browser-shapes"; import * as snapProvider from "./snap"; import { formatError, getCommandLineArgs, isEmpty, isStringValue, objectClone, randomUUID } from "./utils"; +import { getWindowPositionOptions, getWindowPositionUsingStrategy } from "./utils-position"; const logger = createLogger("Launch"); +let windowPositioningOptions: WindowPositioningOptions | undefined; /** * Launch an application in the way specified by its manifest type. * @param platformApp The application to launch. * @param launchPreference launchPreferences if updatable for your the application. + * @param callerIdentity optionally pass information related to the caller to consider when launching. * @returns Identifiers specific to the type of application launched. */ export async function launch( platformApp: PlatformApp, - launchPreference?: UpdatableLaunchPreference + launchPreference?: UpdatableLaunchPreference, + callerIdentity?: OpenFin.Identity ): Promise { try { logger.info("Application launch requested", platformApp); @@ -66,7 +72,7 @@ export async function launch( } case MANIFEST_TYPES.InlineView.id: case MANIFEST_TYPES.View.id: { - const platformIdentity = await launchView(app, launchPreference); + const platformIdentity = await launchView(app, launchPreference, callerIdentity); if (platformIdentity) { platformAppIdentities.push(platformIdentity); } @@ -74,7 +80,7 @@ export async function launch( } case MANIFEST_TYPES.Window.id: case MANIFEST_TYPES.InlineWindow.id: { - const platformIdentity = await launchWindow(app, launchPreference); + const platformIdentity = await launchWindow(app, launchPreference, callerIdentity); if (platformIdentity) { platformAppIdentities.push(platformIdentity); } @@ -106,10 +112,14 @@ export async function launch( } case MANIFEST_TYPES.Endpoint.id: { if (endpointProvider.hasEndpoint(app.manifest)) { - const identity = await endpointProvider.requestResponse<{ payload: PlatformApp }, OpenFin.Identity>( - app.manifest, - { payload: app } - ); + const identity = await endpointProvider.requestResponse< + { + payload: PlatformApp; + launchPreference?: UpdatableLaunchPreference; + callerIdentity?: OpenFin.Identity; + }, + OpenFin.Identity + >(app.manifest, { payload: app, launchPreference, callerIdentity }); if (isEmpty(identity)) { logger.warn( `App with id: ${app.appId} encountered when launched using endpoint: ${app.manifest}.` @@ -316,11 +326,13 @@ function getManifestEndpointId(appId?: string): string { * Launch a window for the platform app. * @param windowApp The app to launch the window for. * @param launchPreference Optional custom launch preferences + * @param callerIdentity optionally pass information related to the caller to consider when launching. * @returns The identity of the window launched. */ async function launchWindow( windowApp: PlatformApp, - launchPreference?: UpdatableLaunchPreference + launchPreference?: UpdatableLaunchPreference, + callerIdentity?: OpenFin.Identity ): Promise { if (isEmpty(windowApp)) { logger.warn("No app was passed to launchWindow"); @@ -391,7 +403,12 @@ async function launchWindow( const canUpdateUrl = isAppPreferenceUpdatable(windowApp, "url"); if (!isEmpty(canUpdateBounds) && !isEmpty(launchPreference?.bounds)) { - appLaunchPreference.bounds = { ...appLaunchPreference.bounds, ...launchPreference?.bounds }; + const callerIdentityBasedLocation = await getCallerIdentityBasedPosition(callerIdentity); + appLaunchPreference.bounds = { + ...appLaunchPreference.bounds, + ...callerIdentityBasedLocation, + ...launchPreference?.bounds + }; } if (!isEmpty(canUpdateCentered) && !isEmpty(launchPreference?.defaultCentered)) { @@ -481,11 +498,13 @@ async function launchWindow( * Launch a view for the platform app. * @param viewApp The app to launch the view for. * @param launchPreference The preferences (if supported) that you would like to apply + * @param callerIdentity optionally pass information related to the caller to consider when launching. * @returns The identity of the view launched. */ async function launchView( viewApp: PlatformApp, - launchPreference?: UpdatableLaunchPreference + launchPreference?: UpdatableLaunchPreference, + callerIdentity?: OpenFin.Identity ): Promise { if (isEmpty(viewApp)) { logger.warn("No app was passed to launchView"); @@ -560,8 +579,13 @@ async function launchView( const canUpdateInterop = isAppPreferenceUpdatable(viewApp, "interop"); const canUpdateUrl = isAppPreferenceUpdatable(viewApp, "url"); - if (!isEmpty(canUpdateBounds) && !isEmpty(launchPreference?.bounds)) { - appLaunchPreference.bounds = { ...appLaunchPreference.bounds, ...launchPreference?.bounds }; + if (!isEmpty(canUpdateBounds) && (!isEmpty(launchPreference?.bounds) || !isEmpty(callerIdentity))) { + const callerIdentityBasedLocation = await getCallerIdentityBasedPosition(callerIdentity); + appLaunchPreference.bounds = { + ...appLaunchPreference.bounds, + ...callerIdentityBasedLocation, + ...launchPreference?.bounds + }; } if (!isEmpty(canUpdateCentered) && !isEmpty(launchPreference?.defaultCentered)) { @@ -1147,3 +1171,22 @@ function updateInstanceIds(layout: T): T { }) ); } + +/** + * Returns undefined or left/top positioning based on the position of the next window given the monitor of the caller. + * @param callerIdentity The identity of the caller to consider when determining x/y of new window + * @returns Left/Top co-ordinates to consider when launching a new window + */ +async function getCallerIdentityBasedPosition( + callerIdentity?: OpenFin.Identity +): Promise<{ left: number; top: number } | undefined> { + if (isEmpty(callerIdentity)) { + return; + } + if (isEmpty(windowPositioningOptions)) { + const settings = await getSettings(); + windowPositioningOptions = await getWindowPositionOptions(settings?.browserProvider); + } + const bounds = await getWindowPositionUsingStrategy(windowPositioningOptions, callerIdentity); + return bounds; +} diff --git a/how-to/workspace-platform-starter/client/src/framework/modules.ts b/how-to/workspace-platform-starter/client/src/framework/modules.ts index c78c847507..df5872e72b 100644 --- a/how-to/workspace-platform-starter/client/src/framework/modules.ts +++ b/how-to/workspace-platform-starter/client/src/framework/modules.ts @@ -90,6 +90,7 @@ export async function loadModules< const modules = moduleList?.modules; if (Array.isArray(modules)) { for (const moduleDefinition of modules) { + moduleDefinition.moduleType = moduleType; const module = await loadModule(moduleDefinition, moduleType); if (module) { loaded.push(module); @@ -210,13 +211,22 @@ export async function initializeModule< if (!moduleEntry.isInitialized) { if (moduleEntry.implementation?.initialize) { try { - logger.info(`Initializing module '${moduleEntry.definition.id}'`); + logger.info( + `Initializing module '${moduleEntry.definition.id}' of type '${moduleEntry.definition.moduleType}'` + ); + const moduleCreateLogger = + moduleEntry.definition.moduleType === "log" ? createLocalLogger : createLogger; + const moduleHelpers = { ...helpers, getNotificationClient: getNotificationClientProxy(moduleEntry.definition), getEndpointClient: getEndpointClientProxy(moduleEntry.definition) }; - await moduleEntry.implementation.initialize(moduleEntry.definition, createLogger, moduleHelpers); + await moduleEntry.implementation.initialize( + moduleEntry.definition, + moduleCreateLogger, + moduleHelpers + ); moduleEntry.isInitialized = true; } catch (err) { logger.error(`Error initializing module ${moduleEntry.definition.id}`, err); @@ -294,7 +304,8 @@ export function getDefaultHelpers(): ModuleHelpers { getDialogClient, launchApp: async ( appId: string, - launchPreference?: UpdatableLaunchPreference + launchPreference?: UpdatableLaunchPreference, + callerIdentity?: OpenFin.Identity ): Promise => { logger.info(`launchApp: Looking up appId: ${appId}`); const app = await getApp(appId); @@ -303,7 +314,7 @@ export function getDefaultHelpers(): ModuleHelpers { logger.warn(`launchApp: The specified appId: ${appId} is not listed in this platform.`); } else { logger.info(`launchApp: Launching app with appId: ${appId}`); - result = await launch(app, launchPreference); + result = await launch(app, launchPreference, callerIdentity); logger.info(`launchApp: App with appId: ${appId} launched.`); } return result; @@ -456,3 +467,26 @@ async function getDialogClient(): Promise { showConfirmation: Dialog.showConfirmation }; } + +/** + * Create local logger. + * @param group The group to encapsulate the loge entries with. + * @returns The local logger that uses the console and doesn't send messages to the log modules. + */ +export function createLocalLogger(group: string): Logger { + console.info( + `${group}: Creating local logger for a log module. Log modules will not be able to use the logger to send messages to log modules as it would be sending messages to itself as well as other log sinks. This will be logged to the console of the provider.` + ); + return { + info: (message: unknown, ...optionalParams: unknown[]) => + console.log(`${group}: ${message}`, ...optionalParams), + warn: (message: unknown, ...optionalParams: unknown[]) => + console.warn(`${group}: ${message}`, ...optionalParams), + error: (message: unknown, ...optionalParams: unknown[]) => + console.error(`${group}: ${message}`, ...optionalParams), + trace: (message: unknown, ...optionalParams: unknown[]) => + console.debug(`${group}: ${message}`, ...optionalParams), + debug: (message: unknown, ...optionalParams: unknown[]) => + console.debug(`${group}: ${message}`, ...optionalParams) + }; +} diff --git a/how-to/workspace-platform-starter/client/src/framework/platform/platform.ts b/how-to/workspace-platform-starter/client/src/framework/platform/platform.ts index 5037215b53..18d89ddc81 100644 --- a/how-to/workspace-platform-starter/client/src/framework/platform/platform.ts +++ b/how-to/workspace-platform-starter/client/src/framework/platform/platform.ts @@ -169,13 +169,7 @@ async function setupPlatform(manifestSettings: CustomSettings | undefined): Prom browser.defaultViewOptions = customSettings?.browserProvider?.defaultViewOptions; } - logger.info("Specifying following browser options", browser); - const windowPositioningOptions = await getWindowPositionOptions(customSettings?.browserProvider); - const customActions = await actionsProvider.init( - customSettings?.actionsProvider, - helpers, - windowPositioningOptions - ); + const customActions = await actionsProvider.init(customSettings?.actionsProvider, helpers); const theme = await getThemes(); await lowCodeIntegrationProvider.init(customSettings?.lowCodeIntegrationProvider); @@ -193,7 +187,7 @@ async function setupPlatform(manifestSettings: CustomSettings | undefined): Prom browser.defaultWindowOptions = browser.defaultWindowOptions ?? {}; await contentCreationProvider.populateRules(browser.defaultWindowOptions); } - + const windowPositioningOptions = await getWindowPositionOptions(customSettings?.browserProvider); await interopProvider.init(customSettings?.platformProvider, windowPositioningOptions, helpers); const platform = getCurrentSync(); diff --git a/how-to/workspace-platform-starter/client/src/framework/shapes/module-shapes.ts b/how-to/workspace-platform-starter/client/src/framework/shapes/module-shapes.ts index 669ae40907..c38277259d 100644 --- a/how-to/workspace-platform-starter/client/src/framework/shapes/module-shapes.ts +++ b/how-to/workspace-platform-starter/client/src/framework/shapes/module-shapes.ts @@ -69,6 +69,11 @@ export interface ModuleDefinition { * Custom data for the module. */ data?: O; + + /** + * This is specified by the provider while loading the modules. + */ + moduleType?: ModuleTypes; } /** @@ -188,11 +193,13 @@ export interface ModuleHelpers { * the current user. * @param appId The id of the application that is registered against the currently running platform * @param launchPreference If the app supports launch preferences then these can be passed. + * @param callerIdentity The identity of the caller to consider when determining x/y of new window. * @returns An array of the platform identities that related from the launch or nothing if nothing was launched. */ launchApp?( appId: string, - launchPreference?: UpdatableLaunchPreference + launchPreference?: UpdatableLaunchPreference, + callerIdentity?: OpenFin.Identity ): Promise; /** diff --git a/how-to/workspace-platform-starter/client/src/framework/shapes/snap-shapes.ts b/how-to/workspace-platform-starter/client/src/framework/shapes/snap-shapes.ts index 208d6755f3..76c0383416 100644 --- a/how-to/workspace-platform-starter/client/src/framework/shapes/snap-shapes.ts +++ b/how-to/workspace-platform-starter/client/src/framework/shapes/snap-shapes.ts @@ -1,5 +1,5 @@ import type OpenFin from "@openfin/core"; -import type { LayoutClient, ServerOptions } from "@openfin/snap-sdk"; +import type { LayoutClient, ServerOptions, SnapServer } from "@openfin/snap-sdk"; /** * The options for the snap provider. */ @@ -14,6 +14,11 @@ export interface SnapProviderOptions { */ id?: string; + /** + * Should the snap server auto-register created windows (using the default snap rules). Defaults to true. + */ + enableAutoWindowRegistration?: boolean; + /** * The asset for the Snap server. */ @@ -53,4 +58,10 @@ export interface SnapProvider { * @returns List of existing app ids with their windows. */ prepareToApplyDecoratedSnapshot(): Promise; + + /** + * Get the snap server if needed for custom behaviors. + * @returns The snap server if available. + */ + getSnapServer(): Promise; } diff --git a/how-to/workspace-platform-starter/client/src/framework/snap.ts b/how-to/workspace-platform-starter/client/src/framework/snap.ts index acbaa025f7..580e818854 100644 --- a/how-to/workspace-platform-starter/client/src/framework/snap.ts +++ b/how-to/workspace-platform-starter/client/src/framework/snap.ts @@ -117,7 +117,9 @@ export async function init(options: SnapProviderOptions | undefined): Promise { + return server; +} diff --git a/how-to/workspace-platform-starter/client/src/framework/workspace/identity.ts b/how-to/workspace-platform-starter/client/src/framework/workspace/identity.ts new file mode 100644 index 0000000000..d2c5aee05b --- /dev/null +++ b/how-to/workspace-platform-starter/client/src/framework/workspace/identity.ts @@ -0,0 +1,27 @@ +import type { OpenFin } from "@openfin/core"; + +const workspaceUUID: string = "openfin-workspace"; + +/** + * Returns the identity of Workspace Store. + * @returns The identity of Workspace Store. + */ +export function getStoreIdentity(): OpenFin.Identity { + return { uuid: workspaceUUID, name: "openfin-storefront" }; +} + +/** + * Returns the identity of Workspace Home. + * @returns The identity of Workspace Home. + */ +export function getHomeIdentity(): OpenFin.Identity { + return { uuid: workspaceUUID, name: "openfin-home" }; +} + +/** + * Returns the identity of Workspace Dock. + * @returns The identity of Workspace Dock. + */ +export function getDockIdentity(): OpenFin.Identity { + return { uuid: workspaceUUID, name: "openfin-dock" }; +} diff --git a/how-to/workspace-platform-starter/client/src/framework/workspace/store.ts b/how-to/workspace-platform-starter/client/src/framework/workspace/store.ts index e054aa4f54..48a1596b21 100644 --- a/how-to/workspace-platform-starter/client/src/framework/workspace/store.ts +++ b/how-to/workspace-platform-starter/client/src/framework/workspace/store.ts @@ -25,6 +25,7 @@ import type { StorefrontSettingsNavigationItem } from "../shapes/store-shapes"; import { isEmpty, isStringValue, randomUUID } from "../utils"; +import { getStoreIdentity } from "./identity"; const TOP_ROW_LIMIT = 4; const MIDDLE_ROW_LIMIT = 6; @@ -59,7 +60,8 @@ export async function register( getFooter: async () => getFooter(), getApps: async () => addButtons(await getApps({ private: false })), launchApp: async (app) => { - await launch(app as PlatformApp); + // this request has come from the store. + await launch(app as PlatformApp, undefined, getStoreIdentity()); } }); diff --git a/how-to/workspace-platform-starter/client/src/modules/content-creation/view-position/content-creation.ts b/how-to/workspace-platform-starter/client/src/modules/content-creation/view-position/content-creation.ts index ed6d178b47..b345504fbd 100644 --- a/how-to/workspace-platform-starter/client/src/modules/content-creation/view-position/content-creation.ts +++ b/how-to/workspace-platform-starter/client/src/modules/content-creation/view-position/content-creation.ts @@ -85,7 +85,7 @@ export class ViewPositionContentCreationProvider // You can of course locate the view elsewhere as shown using the view-position // feature flag which could be passed to the window.open call - const viewPosition = event.parsedFeatures["view-position"]; + const viewPosition = event.parsedFeatures["view-position"] ?? this._settings?.defaultViewPosition; if (isStringValue(viewPosition) && !isEmpty(event.viewIdentity)) { if ( viewPosition === "right" || diff --git a/how-to/workspace-platform-starter/client/src/modules/content-creation/view-position/shapes.ts b/how-to/workspace-platform-starter/client/src/modules/content-creation/view-position/shapes.ts index 517c76382b..5b0e978efa 100644 --- a/how-to/workspace-platform-starter/client/src/modules/content-creation/view-position/shapes.ts +++ b/how-to/workspace-platform-starter/client/src/modules/content-creation/view-position/shapes.ts @@ -8,4 +8,9 @@ export interface ViewPositionContentCreationSettings { * Rules for the content creation. */ rules?: OpenFin.ContentCreationRule[]; + + /** + * Presents the option of having a default behavior if the window.open features doesn't specify one and a view is targeted. + */ + defaultViewPosition?: "right" | "left" | "top" | "bottom" | "stack-left" | "stack-right"; } diff --git a/how-to/workspace-platform-starter/client/src/modules/integrations/apps/integration.ts b/how-to/workspace-platform-starter/client/src/modules/integrations/apps/integration.ts index 17e1e93125..ea1dbff7eb 100644 --- a/how-to/workspace-platform-starter/client/src/modules/integrations/apps/integration.ts +++ b/how-to/workspace-platform-starter/client/src/modules/integrations/apps/integration.ts @@ -249,7 +249,7 @@ export class AppProvider implements IntegrationModule { if (data?.app?.appId) { handled = true; - await this._integrationHelpers.launchApp(data.app.appId); + await this._integrationHelpers.launchApp(data.app.appId, undefined, result.dispatcherIdentity); } } } diff --git a/how-to/workspace-platform-starter/client/src/modules/interop-override/wps-interop-override/broker/wps-interop-override.ts b/how-to/workspace-platform-starter/client/src/modules/interop-override/wps-interop-override/broker/wps-interop-override.ts index 3436bd79cb..76578d9277 100644 --- a/how-to/workspace-platform-starter/client/src/modules/interop-override/wps-interop-override/broker/wps-interop-override.ts +++ b/how-to/workspace-platform-starter/client/src/modules/interop-override/wps-interop-override/broker/wps-interop-override.ts @@ -17,7 +17,6 @@ import { MANIFEST_TYPES } from "workspace-platform-starter/manifest-types"; import type { ConnectionClient, EndpointClient } from "workspace-platform-starter/shapes"; import type { AppsForIntent, - LaunchPreference, PlatformApp, PlatformAppIdentifier } from "workspace-platform-starter/shapes/app-shapes"; @@ -44,7 +43,6 @@ import { randomUUID, sanitizeString } from "workspace-platform-starter/utils"; -import { getWindowPositionUsingStrategy } from "workspace-platform-starter/utils-position"; import { AppIdHelper } from "./app-id-helper"; import { AppIntentHelper } from "./app-intent-helper"; import { getAppsMetaData } from "./app-meta-data-helper"; @@ -665,15 +663,7 @@ export async function getConstructorOverride( } } else { if (isEmpty(platformIdentities)) { - let launchPreference: LaunchPreference | undefined; - const bounds = await getWindowPositionUsingStrategy( - options.windowPositionOptions, - clientIdentity - ); - if (!isEmpty(bounds)) { - launchPreference = { bounds }; - } - platformIdentities = await launch(requestedApp?.appId, launchPreference); + platformIdentities = await launch(requestedApp?.appId, undefined, clientIdentity); } else { focusApp = true; } @@ -954,12 +944,7 @@ export async function getConstructorOverride( } if (platformIdentities.length === 0) { - let launchPreference: LaunchPreference | undefined; - const bounds = await getWindowPositionUsingStrategy(options.windowPositionOptions, clientIdentity); - if (!isEmpty(bounds)) { - launchPreference = { bounds }; - } - platformIdentities = await launch(app.appId, launchPreference); + platformIdentities = await launch(app.appId, undefined, clientIdentity); if (!platformIdentities?.length) { throw new Error(ResolveError.IntentDeliveryFailed); } diff --git a/how-to/workspace-platform-starter/client/src/modules/platform-override/snap-window-selection-override/README.md b/how-to/workspace-platform-starter/client/src/modules/platform-override/snap-window-selection-override/README.md new file mode 100644 index 0000000000..da2ea02e4c --- /dev/null +++ b/how-to/workspace-platform-starter/client/src/modules/platform-override/snap-window-selection-override/README.md @@ -0,0 +1,51 @@ +# Snap Url Validator + +## What is this? + +This is another example of how you can plug in custom platform logic through a platform override module. + +This example is related to the following scenario: + +- I (Platform Owner) want to use Snap SDK to snap windows together +- I don't want every type of window to be tracked by the Snap Server and I need more granular control over the default behavior of excluding windows that are not included in snapshots (includeInSnapshots false in window options). In this scenario I (the platform owner) want to specify urls that should not be tracked (rather than it being driven by Window Options). + +## How do I do it? + +First you need to [enable Snap SDK](../../../../../docs/how-to-configure-snap.md). Then you need to specify that you want to disable the `"enableAutoWindowRegistration": true,` setting by setting it to false. Snap will now start up but not enable auto window registration (as you want your custom logic to drive this decision). + +You then use this module (or something similar, remember this is an example) to track everything except urls that have been specified in the module's custom data definition. + +## How is it configured? + +This example module is defined as a platform override module in a manifest or settings service: + +```json +{ + "id": "snap-window-selection-override", + "icon": "http://localhost:8080/favicon.ico", + "title": "Snap Window Selection Override", + "description": "Snap Window Selection Override", + "enabled": true, + "url": "http://localhost:8080/js/modules/platform-override/snap-window-selection-override.bundle.js", + "data": { + "excludeUrls": ["*/platform/*", "*/common/windows/*", "*/common/views/*"] + } +} +``` + +The example above is specifying that it doesn't want platform windows or common windows or views tracked (for cases where a window is inside the view folder as an additional host for a specific view). + +## How can I test this? + +- You would add this module in the manifest.fin.json file in the public folder. +- You would make sure that snap is enabled and auto window registration is off. +- You would launch the platform using npm run client +- You would then launch the call app and some other apps through home (or store). +- You would see that they snap (if you hold down Ctrl when you click on a window to drag which is the new default for the snapProvider in the main manifest). +- You would launch the IRS RFQ app through Home or Store. You will notice that it doesn't snap when you hold down Ctrl and click and drag the RFQ Window (by selecting the title) and moving it to a Browser window. +- Quit the platform. +- Disable this module by setting enabled to false. +- Go to the snapProvider settings and set window registration to true: `"enableAutoWindowRegistration": true,` +- Relaunch the platform and after startup launch the call app and the IRS RFQ app. They will now snap when you hold Ctrl and drag a window. + +[How To Customize Your Platform Override](../../../../../docs/how-to-customize-your-platform-override.md) diff --git a/how-to/workspace-platform-starter/client/src/modules/platform-override/snap-window-selection-override/index.ts b/how-to/workspace-platform-starter/client/src/modules/platform-override/snap-window-selection-override/index.ts new file mode 100644 index 0000000000..11a64908b7 --- /dev/null +++ b/how-to/workspace-platform-starter/client/src/modules/platform-override/snap-window-selection-override/index.ts @@ -0,0 +1,9 @@ +import type { ModuleImplementation, ModuleTypes } from "workspace-platform-starter/shapes/module-shapes"; +import { SnapWindowSelectionOverride } from "./platform-override"; + +/** + * Define the entry points for the module. + */ +export const entryPoints: { [type in ModuleTypes]?: ModuleImplementation } = { + platformOverride: new SnapWindowSelectionOverride() +}; diff --git a/how-to/workspace-platform-starter/client/src/modules/platform-override/snap-window-selection-override/platform-override.ts b/how-to/workspace-platform-starter/client/src/modules/platform-override/snap-window-selection-override/platform-override.ts new file mode 100644 index 0000000000..0116245dc4 --- /dev/null +++ b/how-to/workspace-platform-starter/client/src/modules/platform-override/snap-window-selection-override/platform-override.ts @@ -0,0 +1,138 @@ +// eslint-disable-next-line max-classes-per-file +import type OpenFin from "@openfin/core"; +import type { WorkspacePlatformProvider } from "@openfin/workspace-platform"; +import type { Logger, LoggerCreator } from "workspace-platform-starter/shapes/logger-shapes"; +import type { ModuleDefinition } from "workspace-platform-starter/shapes/module-shapes"; +import type { + PlatformOverride, + PlatformOverrideHelpers, + PlatformOverrideOptions +} from "workspace-platform-starter/shapes/platform-shapes"; +import type { SnapWindowSelectionOverrideOptions } from "./shapes"; + +/** + * Implementation for the snap window selection override platform override. + */ +export class SnapWindowSelectionOverride implements PlatformOverride { + /** + * The module definition including settings. + * @internal + */ + private _definition: ModuleDefinition | undefined; + + /** + * The logger for displaying information from the module. + * @internal + */ + private _logger?: Logger; + + /** + * Helper methods for the module. + * @internal + */ + private _helpers: PlatformOverrideHelpers | undefined; + + /** + * Initialize the module. + * @param definition The definition of the module from configuration include custom options. + * @param loggerCreator For logging entries. + * @param helpers Helper methods for the module to interact with the application core. + * @returns Nothing. + */ + public async initialize( + definition: ModuleDefinition, + loggerCreator: LoggerCreator, + helpers: PlatformOverrideHelpers + ): Promise { + this._definition = definition; + this._logger = loggerCreator("SnapWindowSelectionOverride"); + this._helpers = helpers; + + this._logger.info("Initializing"); + + // TODO: Add code here to allocate any module resources + // You can access the configured options e.g. definition.data?.exampleProp + } + + /** + * Close down any resources being used by the module. + * @returns Nothing. + */ + public async closedown(): Promise { + this._logger?.info("Closedown"); + + // TODO: Add code here to free up any module resources + } + + /** + * Get the override constructor for the platform override (useful if you wish this implementation to be layered with other implementations and passed to the platform's initialization object as part of an array). + * @param options The options for the platform override defined as part of the platform. + * @returns The override constructor to be used in an array. + */ + public async getConstructorOverride( + options: PlatformOverrideOptions + ): Promise> { + return (Base: OpenFin.Constructor) => { + // use settings passed through the module definition in your override or the default options passed with the function call + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const moduleData = this._definition?.data ?? {}; + const logger = this._logger; + const helpers = this._helpers; + /** + * Extend the Platform Override. + */ + return class CustomPlatformOverride extends Base { + /** + * Constructor for the interop override. + */ + constructor() { + super(); + // this is just an example to show a reference to the options, module data and local reference to the passed helpers. + logger?.info( + `Options passed: ${JSON.stringify(options)} and module data: ${JSON.stringify(moduleData)} with session id: ${helpers?.sessionId}` + ); + } + + /** + * Creates a window with the given options. + * @param windowOptions The options for the window. + * @param identity The identity of the window. + * @returns The created window. + */ + public async createWindow( + windowOptions: OpenFin.PlatformWindowCreationOptions, + identity?: OpenFin.Identity + ): Promise { + const createdWindow = await super.createWindow(windowOptions, identity); + // This example is for cases where snap autoWindowRegistration is disabled and you want to have custom logic to determine if a window should be tracked or not by snap. + // This function is only called for the creation of platform windows and not native applications. + const getSnapClient = await helpers?.getSnapClient(); + const snapEnabled = await getSnapClient?.isEnabled(); + const snapServer = await getSnapClient?.getSnapServer(); + if (!snapEnabled || !snapServer) { + return createdWindow; + } + let track = true; + // Check if the window should be excluded from snap tracking + if ( + Array.isArray(moduleData?.excludeUrls) && + moduleData.excludeUrls.length > 0 && + windowOptions.url + ) { + // Check if the entry in the array is part of the current url of windowOptions.url + const url = windowOptions.url; + track = !moduleData.excludeUrls.some((pattern) => { + const regex = new RegExp(pattern.replace(/\*/g, ".*")); + return regex.test(url); + }); + } + if (track) { + const nativeId = await createdWindow.getNativeId(); + await snapServer.registerWindow(createdWindow.identity.name, nativeId); + } + return createdWindow; + } + }; + }; + } +} diff --git a/how-to/workspace-platform-starter/client/src/modules/platform-override/snap-window-selection-override/shapes.ts b/how-to/workspace-platform-starter/client/src/modules/platform-override/snap-window-selection-override/shapes.ts new file mode 100644 index 0000000000..bd790217f8 --- /dev/null +++ b/how-to/workspace-platform-starter/client/src/modules/platform-override/snap-window-selection-override/shapes.ts @@ -0,0 +1,9 @@ +/** + * Options for the snap window selection override platform override. + */ +export interface SnapWindowSelectionOverrideOptions { + /** + * A list of urls that would result in a window not being tracked by the snap server. + */ + excludeUrls?: string[]; +} diff --git a/how-to/workspace-platform-starter/client/src/modules/platform-override/wps-platform-override/platform/platform-mapper.ts b/how-to/workspace-platform-starter/client/src/modules/platform-override/wps-platform-override/platform/platform-mapper.ts index a81780a572..b102a74cdf 100644 --- a/how-to/workspace-platform-starter/client/src/modules/platform-override/wps-platform-override/platform/platform-mapper.ts +++ b/how-to/workspace-platform-starter/client/src/modules/platform-override/wps-platform-override/platform/platform-mapper.ts @@ -185,7 +185,7 @@ export function mapPlatformWorkspaceToStorage( } } // Skip to make platform dependent and remove below - // { propName: "snapshotDetails", defaultValue: "" }, + // { propName: "snapshotDetails", defaultValue: "" } ]; for (const workspaceSnapshotDefault of workspaceSnapshotDefaults) { @@ -196,9 +196,6 @@ export function mapPlatformWorkspaceToStorage( ); } - // remove platform dependent props - removeProp(clone.snapshot, "snapshotDetails"); - return clone; } diff --git a/how-to/workspace-platform-starter/client/starter-modules.webpack.config.js b/how-to/workspace-platform-starter/client/starter-modules.webpack.config.js index b4f65cfea8..932ac4d925 100644 --- a/how-to/workspace-platform-starter/client/starter-modules.webpack.config.js +++ b/how-to/workspace-platform-starter/client/starter-modules.webpack.config.js @@ -804,6 +804,28 @@ const configs = [ experiments: { outputModule: true } + }, + { + entry: './client/src/modules/platform-override/snap-window-selection-override/index.ts', + devtool: 'source-map', + module: { + rules: [loaderRule] + }, + resolve: { + extensions: ['.tsx', '.ts', '.js'], + alias + }, + externals: { fin: 'fin' }, + output: { + filename: 'snap-window-selection-override.bundle.js', + library: { + type: 'module' + }, + path: path.resolve(__dirname, '..', 'public', 'js', 'modules', 'platform-override') + }, + experiments: { + outputModule: true + } } ]; diff --git a/how-to/workspace-platform-starter/client/types/module/shapes/module-shapes.d.ts b/how-to/workspace-platform-starter/client/types/module/shapes/module-shapes.d.ts index d136f4866f..b3688382d1 100644 --- a/how-to/workspace-platform-starter/client/types/module/shapes/module-shapes.d.ts +++ b/how-to/workspace-platform-starter/client/types/module/shapes/module-shapes.d.ts @@ -60,6 +60,10 @@ export interface ModuleDefinition { * Custom data for the module. */ data?: O; + /** + * This is specified by the provider while loading the modules. + */ + moduleType?: ModuleTypes; } /** * Helper methods and data to pass to the modules during initialization. @@ -162,11 +166,13 @@ export interface ModuleHelpers { * the current user. * @param appId The id of the application that is registered against the currently running platform * @param launchPreference If the app supports launch preferences then these can be passed. + * @param callerIdentity The identity of the caller to consider when determining x/y of new window. * @returns An array of the platform identities that related from the launch or nothing if nothing was launched. */ launchApp?( appId: string, - launchPreference?: UpdatableLaunchPreference + launchPreference?: UpdatableLaunchPreference, + callerIdentity?: OpenFin.Identity ): Promise; /** * Launch a page in the workspace. diff --git a/how-to/workspace-platform-starter/client/types/module/shapes/snap-shapes.d.ts b/how-to/workspace-platform-starter/client/types/module/shapes/snap-shapes.d.ts index 24d14d05a8..3946296086 100644 --- a/how-to/workspace-platform-starter/client/types/module/shapes/snap-shapes.d.ts +++ b/how-to/workspace-platform-starter/client/types/module/shapes/snap-shapes.d.ts @@ -1,5 +1,5 @@ import type OpenFin from "@openfin/core"; -import type { LayoutClient, ServerOptions } from "@openfin/snap-sdk"; +import type { LayoutClient, ServerOptions, SnapServer } from "@openfin/snap-sdk"; /** * The options for the snap provider. */ @@ -12,6 +12,10 @@ export interface SnapProviderOptions { * The id to use for launching the server. */ id?: string; + /** + * Should the snap server auto-register created windows (using the default snap rules). Defaults to true. + */ + enableAutoWindowRegistration?: boolean; /** * The asset for the Snap server. */ @@ -48,4 +52,9 @@ export interface SnapProvider { * @returns List of existing app ids with their windows. */ prepareToApplyDecoratedSnapshot(): Promise; + /** + * Get the snap server if needed for custom behaviors. + * @returns The snap server if available. + */ + getSnapServer(): Promise; } diff --git a/how-to/workspace-platform-starter/docs/how-to-configure-snap.md b/how-to/workspace-platform-starter/docs/how-to-configure-snap.md index bfe7a138af..2b328f5fc5 100644 --- a/how-to/workspace-platform-starter/docs/how-to-configure-snap.md +++ b/how-to/workspace-platform-starter/docs/how-to-configure-snap.md @@ -16,6 +16,7 @@ To enable snap support in your platform you can add the following in your manife "snapProvider": { "enabled": true, "id": "workspace-platform-starter", + "enableAutoWindowRegistration": true, "serverAssetInfo": { "src": "https://cdn.openfin.co/release/snap/0.5.0/snap.zip", "alias": "openfin-snap", diff --git a/how-to/workspace-platform-starter/docs/how-to-customize-your-platform-override.md b/how-to/workspace-platform-starter/docs/how-to-customize-your-platform-override.md index 039af14135..a96e204569 100644 --- a/how-to/workspace-platform-starter/docs/how-to-customize-your-platform-override.md +++ b/how-to/workspace-platform-starter/docs/how-to-customize-your-platform-override.md @@ -86,5 +86,6 @@ This now opens up the capability to add your own logic to the platform (you want - [Default Workspace Starter Platform Override Module](../client/src/modules/platform-override/wps-platform-override/) - [Example Workspace Platform Override That Validates App Urls and Access](../client/src/modules/platform-override/application-url-and-access-validator/) +- [Example Workspace Platform Override That Has Custom Snap SDK Logic To Determine Which Windows Can Snap](../client/src/modules/platform-override/snap-window-selection-override/) [<- Back to Table Of Contents](../README.md) diff --git a/how-to/workspace-platform-starter/public/manifest.fin.json b/how-to/workspace-platform-starter/public/manifest.fin.json index 2e310ed7e3..1fc05b4c48 100644 --- a/how-to/workspace-platform-starter/public/manifest.fin.json +++ b/how-to/workspace-platform-starter/public/manifest.fin.json @@ -100,17 +100,6 @@ "initialLanguage": "en-US" }, "modules": [ - { - "id": "application-url-and-access-validator", - "icon": "http://localhost:8080/favicon.ico", - "title": "Application Url and Access Validator", - "description": "This is an example platform override module that shows how you could validate a saved page, workspace or an application snapshot (if it combines apps) to ensure that it is using the latest url for the applications used and that the user still has access to that app.", - "enabled": false, - "url": "http://localhost:8080/js/modules/platform-override/application-url-and-access-validator.bundle.js", - "data": { - "deniedAccessUrl": "http://localhost:8080/common/views/platform/no-access/no-access.html" - } - }, { "id": "default-wps-platform", "icon": "http://localhost:8080/favicon.ico", @@ -298,8 +287,13 @@ "enabled": true, "url": "http://localhost:8080/js/modules/endpoint/example-notification-source.bundle.js", "data": { - "websocket": { "url": "" }, - "longpoll": { "url": "", "intervalInSeconds": 5 }, + "websocket": { + "url": "" + }, + "longpoll": { + "url": "", + "intervalInSeconds": 5 + }, "post": { "url": "" }, @@ -1811,6 +1805,7 @@ "snapProvider": { "enabled": false, "id": "workspace-platform-starter", + "enableAutoWindowRegistration": true, "serverAssetInfo": { "src": "https://cdn.openfin.co/release/snap/1.0.0/snap.zip", "alias": "openfin-snap", @@ -1831,9 +1826,10 @@ "id": "view-position-content-creation", "icon": "http://localhost:8080/favicon.ico", "title": "View Position Content Creation", - "enabled": false, + "enabled": true, "url": "http://localhost:8080/js/modules/content-creation/view-position.bundle.js", "data": { + "defaultViewPosition": "stack-right", "rules": [ { "behavior": "window", @@ -1843,10 +1839,6 @@ "defaultHeight": 600 } }, - { - "behavior": "view", - "match": ["https://www.scientificamerican.com/*"] - }, { "behavior": "browser", "match": ["https://www.livescience.com/*"] @@ -1854,6 +1846,10 @@ { "behavior": "block", "match": ["https://www.sciencenews.org/*"] + }, + { + "behavior": "view", + "match": ["https://*", "http://*"] } ] } diff --git a/how-to/workspace-platform-starter/public/schemas/settings.schema.json b/how-to/workspace-platform-starter/public/schemas/settings.schema.json index 08d94a1d7c..677e5c3dd6 100644 --- a/how-to/workspace-platform-starter/public/schemas/settings.schema.json +++ b/how-to/workspace-platform-starter/public/schemas/settings.schema.json @@ -2075,6 +2075,10 @@ "description": "Url to more information.", "type": "string" }, + "moduleType": { + "$ref": "#/definitions/ModuleTypes", + "description": "This is specified by the provider while loading the modules." + }, "moduleUrl": { "description": "This is the old property, it will be remapped to url.", "type": "string" @@ -2313,6 +2317,10 @@ "description": "Url to more information.", "type": "string" }, + "moduleType": { + "$ref": "#/definitions/ModuleTypes", + "description": "This is specified by the provider while loading the modules." + }, "title": { "description": "The title of the module.", "type": "string" @@ -3081,6 +3089,10 @@ "description": "Url to more information.", "type": "string" }, + "moduleType": { + "$ref": "#/definitions/ModuleTypes", + "description": "This is specified by the provider while loading the modules." + }, "title": { "description": "The title of the module.", "type": "string" @@ -3210,6 +3222,26 @@ }, "type": "object" }, + "ModuleTypes": { + "description": "The possible module types.", + "enum": [ + "actions", + "analytics", + "auth", + "conditions", + "contentCreation", + "endpoint", + "initOptions", + "integrations", + "interopOverride", + "lifecycle", + "log", + "menus", + "platformOverride", + "share" + ], + "type": "string" + }, "MonitorDetails": { "$ref": "#/definitions/__type_49" }, @@ -4318,6 +4350,10 @@ "additionalProperties": false, "description": "The options for the snap provider.", "properties": { + "enableAutoWindowRegistration": { + "description": "Should the snap server auto-register created windows (using the default snap rules). Defaults to true.", + "type": "boolean" + }, "enabled": { "description": "Is snap enabled, defaults to false.", "type": "boolean" diff --git a/how-to/workspace-platform-starter/public/schemas/wps.manifest.schema.json b/how-to/workspace-platform-starter/public/schemas/wps.manifest.schema.json index ce7de58c17..e58e905b03 100644 --- a/how-to/workspace-platform-starter/public/schemas/wps.manifest.schema.json +++ b/how-to/workspace-platform-starter/public/schemas/wps.manifest.schema.json @@ -1948,6 +1948,10 @@ "description": "Url to more information.", "type": "string" }, + "moduleType": { + "$ref": "#/definitions/ModuleTypes", + "description": "This is specified by the provider while loading the modules." + }, "moduleUrl": { "description": "This is the old property, it will be remapped to url.", "type": "string" @@ -2180,6 +2184,10 @@ "description": "Url to more information.", "type": "string" }, + "moduleType": { + "$ref": "#/definitions/ModuleTypes", + "description": "This is specified by the provider while loading the modules." + }, "title": { "description": "The title of the module.", "type": "string" @@ -2879,6 +2887,10 @@ "description": "Url to more information.", "type": "string" }, + "moduleType": { + "$ref": "#/definitions/ModuleTypes", + "description": "This is specified by the provider while loading the modules." + }, "title": { "description": "The title of the module.", "type": "string" @@ -3000,6 +3012,26 @@ }, "type": "object" }, + "ModuleTypes": { + "description": "The possible module types.", + "enum": [ + "actions", + "analytics", + "auth", + "conditions", + "contentCreation", + "endpoint", + "initOptions", + "integrations", + "interopOverride", + "lifecycle", + "log", + "menus", + "platformOverride", + "share" + ], + "type": "string" + }, "MonitorDetails": { "$ref": "#/definitions/__type_49" }, @@ -4329,6 +4361,10 @@ "additionalProperties": false, "description": "The options for the snap provider.", "properties": { + "enableAutoWindowRegistration": { + "description": "Should the snap server auto-register created windows (using the default snap rules). Defaults to true.", + "type": "boolean" + }, "enabled": { "description": "Is snap enabled, defaults to false.", "type": "boolean"