diff --git a/docs/docs/cmd/spo/applicationcustomizer/applicationcustomizer-add.mdx b/docs/docs/cmd/spo/applicationcustomizer/applicationcustomizer-add.mdx index c7fc5440e13..3d35ee7d9f1 100644 --- a/docs/docs/cmd/spo/applicationcustomizer/applicationcustomizer-add.mdx +++ b/docs/docs/cmd/spo/applicationcustomizer/applicationcustomizer-add.mdx @@ -2,7 +2,7 @@ import Global from '../../_global.mdx'; # spo applicationcustomizer add -Add an application customizer to a site +Add an Application Customizer to a site ## Usage @@ -14,29 +14,32 @@ m365 spo applicationcustomizer add [options] ```md definition-list `-t, --title ` -: The title of the application customizer. +: The title of the Application Customizer. `-u, --webUrl <webUrl>` : URL of the site. `-i, --clientSideComponentId <clientSideComponentId>` -: Client-side component ID of the application customizer (GUID). +: Client-side component ID of the Application Customizer (GUID). `--description [description]` -: Description of the application customizer. +: Description of the Application Customizer. `--clientSideComponentProperties [clientSideComponentProperties]` -: JSON string with application customizer properties. +: JSON string with Application Customizer properties. + +`--hostProperties [hostProperties]` +: Set the host properties of the Application Customizer. `-s, --scope [scope]` -: Scope of the application customizer. Allowed values: `Site`, `Web`. Defaults to `Site`. +: Scope of the Application Customizer. Allowed values: `Site`, `Web`. Defaults to `Site`. ``` <Global /> ## Remarks -Running this command from the Windows Command Shell (cmd.exe) or PowerShell for Windows OS XP, 7, 8, 8.1 without bash installed might require additional formatting for command options that have JSON, XML or JavaScript values because the command shell treats quotes differently. For example, this is how an application customizer can be created from the Windows cmd.exe: +Running this command from the Windows Command Shell (cmd.exe) or PowerShell for Windows OS XP, 7, 8, 8.1 without bash installed might require additional formatting for command options that have JSON, XML or JavaScript values because the command shell treats quotes differently. For example, this is how an Application Customizer can be created from the Windows cmd.exe: ```sh m365 spo applicationcustomizer add --webUrl https://contoso.sharepoint.com/sites/test --title "YourAppCustomizer" --clientSideComponentId b41916e7-e69d-467f-b37f-ff8ecf8f99f2 --clientSideComponentProperties '{\"testMessage\":\"Test message\"}' @@ -46,21 +49,21 @@ Note, how the clientSideComponentProperties option has escaped double quotes `'{ :::warning[Escaping JSON in PowerShell] -When using the `--clientSideComponentProperties` option it's possible to enter a JSON string. In PowerShell 5 to 7.2 [specific escaping rules](./../../../user-guide/using-cli.mdx#escaping-double-quotes-in-powershell) apply due to an issue. Remember that you can also use [file tokens](./../../../user-guide/using-cli.mdx#passing-complex-content-into-cli-options) instead. +When using the `--clientSideComponentProperties` or `--hostProperties` options it's possible to enter a JSON string. In PowerShell 5 to 7.2 [specific escaping rules](./../../../user-guide/using-cli.mdx#escaping-double-quotes-in-powershell) apply due to an issue. Remember that you can also use [file tokens](./../../../user-guide/using-cli.mdx#passing-complex-content-into-cli-options) instead. ::: -This command can be used for configuring an application customizer on a specific site. To configure an application customizer tenant-wide, view our dedicated [spo tenant applicationcustomizer add](../tenant/tenant-applicationcustomizer-add.mdx) command. +This command can be used for configuring an Application Customizer on a specific site. To configure an Application Customizer tenant-wide, view our dedicated [spo tenant applicationcustomizer add](../tenant/tenant-applicationcustomizer-add.mdx) command. ## Examples -Adds an application customizer to the sales site. +Adds an Application Customizer to the sales site. ```sh m365 spo applicationcustomizer add --title 'Some customizer' --clientSideComponentId 799883f5-7962-4384-a10a-105adaec6ffc --webUrl https://contoso.sharepoint.com/sites/sales ``` -Adds an application customizer to the sales site with some properties. +Adds an Application Customizer to the sales site with some properties. ```sh m365 spo applicationcustomizer add --title 'Some customizer' --clientSideComponentId 799883f5-7962-4384-a10a-105adaec6ffc --clientSideComponentProperties '{ "someProperty": "Some value" }' --webUrl https://contoso.sharepoint.com/sites/sales --scope 'Site' diff --git a/docs/docs/cmd/spo/applicationcustomizer/applicationcustomizer-set.mdx b/docs/docs/cmd/spo/applicationcustomizer/applicationcustomizer-set.mdx index 28dbada5202..db93a52cd9e 100644 --- a/docs/docs/cmd/spo/applicationcustomizer/applicationcustomizer-set.mdx +++ b/docs/docs/cmd/spo/applicationcustomizer/applicationcustomizer-set.mdx @@ -29,11 +29,14 @@ m365 spo applicationcustomizer set [options] : The new title of the Application Customizer. `--description [description]` -: Description of the application customizer. Specify an empty string to clear. +: Description of the Application Customizer. Specify an empty string to clear. `-p, --clientSideComponentProperties [clientSideComponentProperties]` : The Client Side Component properties of the Application Customizer. +`--hostProperties [hostProperties]` +: Set the host properties for the Application Customizer. Set to an empty string to clear the value. + `-s, --scope [scope]` : The scope where to lookup the Application Customizer. Allowed values: `Site`, `Web`, and `All`. Defaults to `All`. ``` @@ -42,7 +45,7 @@ m365 spo applicationcustomizer set [options] ## Remarks -Running this command from the Windows Command Shell (cmd.exe) or PowerShell for Windows OS XP, 7, 8, 8.1 without bash installed might require additional formatting for clientSideComponentProperties option that has JSON value because the command shell treats quotes differently. For example, this is how Application Customizer can be updated from the Windows cmd.exe: +Running this command from the Windows Command Shell (cmd.exe) or PowerShell for Windows OS XP, 7, 8, 8.1 without bash installed might require additional formatting for `clientSideComponentProperties` and `hostProperties` options that have JSON value because the command shell treats quotes differently. For example, this is how Application Customizer can be updated from the Windows cmd.exe: ```sh m365 spo applicationcustomizer set --webUrl https://contoso.sharepoint.com/sites/sales --id b41916e7-e69d-467f-b37f-ff8ecf8f99f2 --newTitle "Some customizer" --clientSideComponentProperties '{\"testMessage\":\"Test message\"}' @@ -52,21 +55,21 @@ Note, how the clientSideComponentProperties option (--clientSideComponentPropert :::warning[Escaping JSON in PowerShell] -When using the `--clientSideComponentProperties` option it's possible to enter a JSON string. In PowerShell 5 to 7.2 [specific escaping rules](./../../../user-guide/using-cli.mdx#escaping-double-quotes-in-powershell) apply due to an issue. Remember that you can also use [file tokens](./../../../user-guide/using-cli.mdx#passing-complex-content-into-cli-options) instead. +When using the `--clientSideComponentProperties` or `--hostProperties` options it's possible to enter a JSON string. In PowerShell 5 to 7.2 [specific escaping rules](./../../../user-guide/using-cli.mdx#escaping-double-quotes-in-powershell) apply due to an issue. Remember that you can also use [file tokens](./../../../user-guide/using-cli.mdx#passing-complex-content-into-cli-options) instead. ::: -This command can be used for updating an application customizer on a specific site. To update an application customizer that's installed tenant-wide, view our dedicated [spo tenant applicationcustomizer set](../tenant/tenant-applicationcustomizer-set.mdx) command. +This command can be used for updating an Application Customizer on a specific site. To update an Application Customizer that's installed tenant-wide, view our dedicated [spo tenant applicationcustomizer set](../tenant/tenant-applicationcustomizer-set.mdx) command. ## Examples -Updates the title of an application customizer on the sales site. +Updates the title of an Application Customizer on the sales site. ```sh m365 spo applicationcustomizer set --id 058140e3-0e37-44fc-a1d3-79c487d371a3 --newTitle "Some customizer" --webUrl https://contoso.sharepoint.com/sites/sales ``` -Updates the properties of an application customizer on the sales site. +Updates the properties of an Application Customizer on the sales site. ```sh m365 spo applicationcustomizer set --id 058140e3-0e37-44fc-a1d3-79c487d371a3 --clientSideComponentProperties '{ "testMessage": "Test message" }' --webUrl https://contoso.sharepoint.com/sites/sales diff --git a/docs/docs/cmd/spo/tenant/tenant-applicationcustomizer-add.mdx b/docs/docs/cmd/spo/tenant/tenant-applicationcustomizer-add.mdx index 6c8e76492c6..afcc8350d72 100644 --- a/docs/docs/cmd/spo/tenant/tenant-applicationcustomizer-add.mdx +++ b/docs/docs/cmd/spo/tenant/tenant-applicationcustomizer-add.mdx @@ -2,7 +2,7 @@ import Global from '../../_global.mdx'; # spo tenant applicationcustomizer add -Add an application customizer as a tenant-wide extension +Add an Application Customizer as a tenant-wide extension ## Usage @@ -17,13 +17,16 @@ m365 spo tenant applicationcustomizer add [options] : The title of the Application Customizer. `-i, --clientSideComponentId <clientSideComponentId>` -: The Client Side Component Id (GUID) of the application customizer. +: The Client Side Component Id (GUID) of the Application Customizer. `-p, --clientSideComponentProperties [clientSideComponentProperties]` -: The Client Side Component properties of the application customizer. +: The Client Side Component properties of the Application Customizer. + +`--hostProperties [hostProperties]` +: Set the host properties of the Application Customizer. `-w, --webTemplate [webTemplate]` -: Optionally add a web template (e.g. STS#3, SITEPAGEPUBLISHING#0, etc) as a filter for what kind of sites the application customizer is registered on. +: Optionally add a web template (e.g. STS#3, SITEPAGEPUBLISHING#0, etc) as a filter for what kind of sites the Application Customizer is registered on. ``` <Global /> @@ -40,7 +43,7 @@ Note, how the clientSideComponentProperties option has escaped double quotes `'{ :::warning[Escaping JSON in PowerShell] -When using the `--clientSideComponentProperties` option it's possible to enter a JSON string. In PowerShell 5 to 7.2 [specific escaping rules](./../../../user-guide/using-cli.mdx#escaping-double-quotes-in-powershell) apply due to an issue. Remember that you can also use [file tokens](./../../../user-guide/using-cli.mdx#passing-complex-content-into-cli-options) instead. +When using the `--clientSideComponentProperties` or `--hostProperties` options it's possible to enter a JSON string. In PowerShell 5 to 7.2 [specific escaping rules](./../../../user-guide/using-cli.mdx#escaping-double-quotes-in-powershell) apply due to an issue. Remember that you can also use [file tokens](./../../../user-guide/using-cli.mdx#passing-complex-content-into-cli-options) instead. ::: @@ -50,20 +53,20 @@ To use this command, you need to be a SharePoint Admin. ::: -This command can be used for configuring a tenant-wide application customizer. To configure an application customizer on a specific site, view our dedicated [spo applicationcustomizer add](../applicationcustomizer/applicationcustomizer-add.mdx) command. +This command can be used for configuring a tenant-wide Application Customizer. To configure an Application Customizer on a specific site, view our dedicated [spo applicationcustomizer add](../applicationcustomizer/applicationcustomizer-add.mdx) command. ## Examples -Adds an application customizer that's deployed tenant wide +Adds an Application Customizer that's deployed tenant wide ```sh -m365 spo tenant applicationcustomizer add --title "Some customizer" --clientSideComponentId 799883f5-7962-4384-a10a-105adaec6ffc +m365 spo tenant applicationcustomizer add --title "Some customizer" --clientSideComponentId 799883f5-7962-4384-a10a-105adaec6ffc ``` -Adds an application customizer that is configured for all communication sites. +Adds an Application Customizer that is configured for all communication sites. ```sh -m365 spo tenant applicationcustomizer add --title "Some customizer" --clientSideComponentId 799883f5-7962-4384-a10a-105adaec6ffc --webTemplate "SITEPAGEPUBLISHING#0" +m365 spo tenant applicationcustomizer add --title "Some customizer" --clientSideComponentId 799883f5-7962-4384-a10a-105adaec6ffc --webTemplate "SITEPAGEPUBLISHING#0" ``` ## Response diff --git a/docs/docs/cmd/spo/tenant/tenant-applicationcustomizer-set.mdx b/docs/docs/cmd/spo/tenant/tenant-applicationcustomizer-set.mdx index 0a5eff36e05..694c482b7aa 100644 --- a/docs/docs/cmd/spo/tenant/tenant-applicationcustomizer-set.mdx +++ b/docs/docs/cmd/spo/tenant/tenant-applicationcustomizer-set.mdx @@ -31,25 +31,28 @@ m365 spo tenant applicationcustomizer set [options] `-p, --clientSideComponentProperties [clientSideComponentProperties]` : The Client Side Component properties of the Application Customizer. +`--hostProperties [hostProperties]` +: Set the host properties of the Application Customizer. Set to an empty string to clear the value. + `-w, --webTemplate [webTemplate]` -: Optionally add a web template (e.g. STS#3, SITEPAGEPUBLISHING#0, etc) as a filter for what kind of sites the application customizer is registered on. +: Optionally add a web template (e.g. STS#3, SITEPAGEPUBLISHING#0, etc) as a filter for what kind of sites the Application Customizer is registered on. ``` <Global /> ## Remarks -Running this command from the Windows Command Shell (cmd.exe) or PowerShell for Windows OS XP, 7, 8, 8.1 without bash installed might require additional formatting for clientSideComponentProperties option that has JSON value because the command shell treats quotes differently. For example, this is how Application Customizer can be updated from the Windows cmd.exe: +Running this command from the Windows Command Shell (cmd.exe) or PowerShell for Windows OS XP, 7, 8, 8.1 without bash installed might require additional formatting for `clientSideComponentProperties` and `hostProperties` options that have JSON values because the command shell treats quotes differently. For example, this is how Application Customizer can be updated from the Windows cmd.exe: ```sh m365 spo tenant applicationcustomizer set --id 3 --clientSideComponentProperties '{\"someProperty\":\"Some value\"}' ``` -Note, how the clientSideComponentProperties option (--clientSideComponentProperties) has escaped double quotes `'{\"someProperty\":\"Some value\"}'` compared to execution from bash `'{"someProperty": "Some value"}'`. +Note, how the `--clientSideComponentProperties` option has escaped double quotes `'{\"someProperty\":\"Some value\"}'` compared to execution from bash `'{"someProperty": "Some value"}'`. :::warning[Escaping JSON in PowerShell] -When using the `--clientSideComponentProperties` option it's possible to enter a JSON string. In PowerShell 5 to 7.2 [specific escaping rules](./../../../user-guide/using-cli.mdx#escaping-double-quotes-in-powershell) apply due to an issue. Remember that you can also use [file tokens](./../../../user-guide/using-cli.mdx#passing-complex-content-into-cli-options) instead. +When using the `--clientSideComponentProperties` or `--hostProperties` options it's possible to enter a JSON string. In PowerShell 5 to 7.2 [specific escaping rules](./../../../user-guide/using-cli.mdx#escaping-double-quotes-in-powershell) apply due to an issue. Remember that you can also use [file tokens](./../../../user-guide/using-cli.mdx#passing-complex-content-into-cli-options) instead. ::: diff --git a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-add.spec.ts b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-add.spec.ts index 2bce53ee3e7..afda74b1b0a 100644 --- a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-add.spec.ts +++ b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-add.spec.ts @@ -95,12 +95,8 @@ describe(commands.APPLICATIONCUSTOMIZER_ADD, () => { }); it('adds the application customizer to a specific site without specifying clientSideComponentProperties', async () => { - sinon.stub(request, 'post').callsFake(async (opts) => { - if (opts.url === 'https://contoso.sharepoint.com/_api/Web/UserCustomActions' - && opts.data['Location'] === 'ClientSideExtension.ApplicationCustomizer' - && opts.data['ClientSideComponentId'] === clientSideComponentId - && opts.data['Name'] === title - && opts.data['ClientSideComponentProperties'] === undefined) { + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === 'https://contoso.sharepoint.com/_api/Web/UserCustomActions') { return; } @@ -108,16 +104,20 @@ describe(commands.APPLICATIONCUSTOMIZER_ADD, () => { }); await command.action(logger, { options: { webUrl: webUrl, title: title, clientSideComponentId: clientSideComponentId, scope: 'Web' } }); + assert.deepStrictEqual(postStub.firstCall.args[0].data, { + Title: title, + Name: title, + Description: undefined, + Location: 'ClientSideExtension.ApplicationCustomizer', + ClientSideComponentId: clientSideComponentId, + HostProperties: '' + }); assert(loggerLogToStderrSpy.notCalled); }); it('adds the application customizer to a specific site while specifying clientSideComponentProperties', async () => { - sinon.stub(request, 'post').callsFake(async (opts) => { - if (opts.url === 'https://contoso.sharepoint.com/_api/Site/UserCustomActions' - && opts.data['Location'] === 'ClientSideExtension.ApplicationCustomizer' - && opts.data['ClientSideComponentId'] === clientSideComponentId - && opts.data['ClientSideComponentProperties'] === clientSideComponentProperties - && opts.data['Name'] === title) { + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === 'https://contoso.sharepoint.com/_api/Site/UserCustomActions') { return customActionAddResponse; } @@ -125,6 +125,36 @@ describe(commands.APPLICATIONCUSTOMIZER_ADD, () => { }); await command.action(logger, { options: { webUrl: webUrl, title: title, clientSideComponentId: clientSideComponentId, description: description, clientSideComponentProperties: clientSideComponentProperties, verbose: true } }); + assert.deepStrictEqual(postStub.firstCall.args[0].data, { + Title: title, + Name: title, + Description: description, + Location: 'ClientSideExtension.ApplicationCustomizer', + ClientSideComponentId: clientSideComponentId, + ClientSideComponentProperties: clientSideComponentProperties, + HostProperties: '' + }); + assert(loggerLogToStderrSpy.called); + }); + + it('adds the application customizer to a specific site while specifying hostProperties', async () => { + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === 'https://contoso.sharepoint.com/_api/Site/UserCustomActions') { + return customActionAddResponse; + } + + throw customActionError; + }); + + await command.action(logger, { options: { webUrl: webUrl, title: title, clientSideComponentId: clientSideComponentId, description: description, hostProperties: clientSideComponentProperties, verbose: true } }); + assert.deepStrictEqual(postStub.firstCall.args[0].data, { + Title: title, + Name: title, + Description: description, + Location: 'ClientSideExtension.ApplicationCustomizer', + ClientSideComponentId: clientSideComponentId, + HostProperties: clientSideComponentProperties + }); assert(loggerLogToStderrSpy.called); }); @@ -148,8 +178,13 @@ describe(commands.APPLICATIONCUSTOMIZER_ADD, () => { assert.notStrictEqual(actual, true); }); + it('fails validation if the hostProperties option is not a valid json string', async () => { + const actual = await command.validate({ options: { webUrl: webUrl, title: title, clientSideComponentId: clientSideComponentId, hostProperties: 'invalid json string' } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + it('passes validation if all options are passed', async () => { - const actual = await command.validate({ options: { webUrl: webUrl, title: title, clientSideComponentId: clientSideComponentId, clientSideComponentProperties: clientSideComponentProperties, scope: 'Site' } }, commandInfo); + const actual = await command.validate({ options: { webUrl: webUrl, title: title, clientSideComponentId: clientSideComponentId, clientSideComponentProperties: clientSideComponentProperties, hostProperties: clientSideComponentProperties, scope: 'Site' } }, commandInfo); assert.strictEqual(actual, true); }); }); diff --git a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-add.ts b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-add.ts index 9bf69dd0b45..458560f336f 100644 --- a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-add.ts +++ b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-add.ts @@ -16,6 +16,7 @@ interface Options extends GlobalOptions { clientSideComponentId: string; description?: string; clientSideComponentProperties?: string; + hostProperties?: string; scope?: string; } @@ -55,6 +56,9 @@ class SpoApplicationCustomizerAddCommand extends SpoCommand { { option: '--clientSideComponentProperties [clientSideComponentProperties]' }, + { + option: '--hostProperties [hostProperties]' + }, { option: '-s, --scope [scope]', autocomplete: SpoApplicationCustomizerAddCommand.scopes } @@ -66,6 +70,7 @@ class SpoApplicationCustomizerAddCommand extends SpoCommand { Object.assign(this.telemetryProperties, { description: typeof args.options.description !== 'undefined', clientSideComponentProperties: typeof args.options.clientSideComponentProperties !== 'undefined', + hostProperties: typeof args.options.hostProperties !== 'undefined', scope: typeof args.options.scope !== 'undefined' }); }); @@ -94,6 +99,15 @@ class SpoApplicationCustomizerAddCommand extends SpoCommand { } } + if (args.options.hostProperties) { + try { + JSON.parse(args.options.hostProperties); + } + catch (e) { + return `An error has occurred while parsing hostProperties: ${e}`; + } + } + if (args.options.scope && SpoApplicationCustomizerAddCommand.scopes.indexOf(args.options.scope) < 0) { return `${args.options.scope} is not a valid value for allowedMembers. Valid values are ${SpoApplicationCustomizerAddCommand.scopes.join(', ')}`; } @@ -120,6 +134,8 @@ class SpoApplicationCustomizerAddCommand extends SpoCommand { requestBody.ClientSideComponentProperties = args.options.clientSideComponentProperties; } + requestBody.HostProperties = args.options.hostProperties || ''; + const scope = args.options.scope || 'Site'; const requestOptions: CliRequestOptions = { diff --git a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-set.spec.ts b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-set.spec.ts index ddbf0714360..0b3fba8251b 100644 --- a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-set.spec.ts +++ b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-set.spec.ts @@ -24,6 +24,7 @@ describe(commands.APPLICATIONCUSTOMIZER_SET, () => { const newTitle = 'New Title'; const description = 'Site guided tour customizer'; const clientSideComponentProperties = '{"testMessage":"Updated message"}'; + const hostProperties = '{"preAllocatedApplicationCustomizerTopHeight":"50","preAllocatedApplicationCustomizerBottomHeight":"50"}'; let log: any[]; let logger: Logger; @@ -213,6 +214,16 @@ describe(commands.APPLICATIONCUSTOMIZER_SET, () => { assert.notStrictEqual(actual, true); }); + it('fails validation if the clientSideComponentProperties option is not a valid json string', async () => { + const actual = await command.validate({ options: { webUrl: webUrl, id: id, clientSideComponentProperties: 'invalid json string' } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('fails validation if the hostProperties option is not a valid json string', async () => { + const actual = await command.validate({ options: { webUrl: webUrl, id: id, hostProperties: 'invalid json string' } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + it('handles error when no application customizer with the specified id found', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { if (opts.url?.startsWith('https://contoso.sharepoint.com/_api/') && opts.url?.endsWith(`/UserCustomActions(guid'${id}')`)) { @@ -376,7 +387,25 @@ describe(commands.APPLICATIONCUSTOMIZER_SET, () => { assert(updateCallsSpy.calledOnce); assert.deepStrictEqual(updateCallsSpy.firstCall.args[0].data, { - Description: '' + Description: '', + HostProperties: undefined + }); + }); + + it('should update the application customizer from the site when hostProperties is provided', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://contoso.sharepoint.com/_api/Web/UserCustomActions(guid'${id}')`) { + return singleResponse.value[0]; + } + throw 'Invalid request: ' + opts.url; + }); + + const updateCallsSpy: sinon.SinonStub = defaultUpdateCallsStub(); + await command.action(logger, { options: { id: id, webUrl: webUrl, scope: 'Web', hostProperties: hostProperties } }); + + assert(updateCallsSpy.calledOnce); + assert.deepStrictEqual(updateCallsSpy.firstCall.args[0].data, { + HostProperties: hostProperties }); }); }); \ No newline at end of file diff --git a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-set.ts b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-set.ts index fa59eb8ef28..4a047995c44 100644 --- a/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-set.ts +++ b/src/m365/spo/commands/applicationcustomizer/applicationcustomizer-set.ts @@ -21,6 +21,7 @@ interface Options extends GlobalOptions { newTitle?: string; description?: string; clientSideComponentProperties?: string; + hostProperties?: string; scope?: string; } @@ -67,6 +68,9 @@ class SpoApplicationCustomizerSetCommand extends SpoCommand { { option: '-p, --clientSideComponentProperties [clientSideComponentProperties]' }, + { + option: '--hostProperties [hostProperties]' + }, { option: '-s, --scope [scope]', autocomplete: this.allowedScopes } @@ -82,6 +86,7 @@ class SpoApplicationCustomizerSetCommand extends SpoCommand { newTitle: typeof args.options.newTitle !== 'undefined', description: typeof args.options.description !== 'undefined', clientSideComponentProperties: typeof args.options.clientSideComponentProperties !== 'undefined', + hostProperties: typeof args.options.hostProperties !== 'undefined', scope: typeof args.options.scope !== 'undefined' }); }); @@ -102,7 +107,25 @@ class SpoApplicationCustomizerSetCommand extends SpoCommand { return `'${args.options.scope}' is not a valid application customizer scope. Allowed values are: ${this.allowedScopes.join(',')}`; } - if (!args.options.newTitle && args.options.description === undefined && !args.options.clientSideComponentProperties) { + if (args.options.clientSideComponentProperties) { + try { + JSON.parse(args.options.clientSideComponentProperties); + } + catch (e) { + return `An error has occurred while parsing clientSideComponentProperties: ${e}`; + } + } + + if (args.options.hostProperties) { + try { + JSON.parse(args.options.hostProperties); + } + catch (e) { + return `An error has occurred while parsing hostProperties: ${e}`; + } + } + + if (!args.options.newTitle && args.options.description === undefined && !args.options.clientSideComponentProperties && args.options.hostProperties === undefined) { return `Please specify an option to be updated`; } @@ -128,7 +151,7 @@ class SpoApplicationCustomizerSetCommand extends SpoCommand { } private async updateAppCustomizer(logger: Logger, options: Options, appCustomizer: CustomAction): Promise<void> { - const { clientSideComponentProperties, webUrl, newTitle, description }: Options = options; + const { clientSideComponentProperties, hostProperties, webUrl, newTitle, description }: Options = options; if (this.verbose) { await logger.logToStderr(`Updating application customizer with ID '${appCustomizer.Id}' on the site '${webUrl}'...`); @@ -148,6 +171,8 @@ class SpoApplicationCustomizerSetCommand extends SpoCommand { requestBody.ClientSideComponentProperties = clientSideComponentProperties; } + requestBody.HostProperties = hostProperties; + const requestOptions: CliRequestOptions = { url: `${webUrl}/_api/${appCustomizer.Scope.toString() === '2' ? 'Site' : 'Web'}/UserCustomActions('${appCustomizer.Id}')`, headers: { diff --git a/src/m365/spo/commands/tenant/tenant-applicationcustomizer-add.spec.ts b/src/m365/spo/commands/tenant/tenant-applicationcustomizer-add.spec.ts index a6147546388..3fc86def52f 100644 --- a/src/m365/spo/commands/tenant/tenant-applicationcustomizer-add.spec.ts +++ b/src/m365/spo/commands/tenant/tenant-applicationcustomizer-add.spec.ts @@ -136,6 +136,35 @@ describe(commands.TENANT_APPLICATIONCUSTOMIZER_ADD, () => { assert.strictEqual(executeCommandCalled, true); }); + it('adds a tenant-wide application customizer including hostProperties', async () => { + let actualHostProperties: string | undefined; + sinon.stub(cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise<any> => { + if (command === spoListItemListCommand) { + if (args.options.listUrl === `${urlUtil.getServerRelativeSiteUrl(appCatalogUrl)}/Lists/ComponentManifests`) { + return { 'stdout': JSON.stringify(solutionResponse) }; + } + if (args.options.listUrl === `${urlUtil.getServerRelativeSiteUrl(appCatalogUrl)}/AppCatalog`) { + return { 'stdout': JSON.stringify(applicationResponse) }; + } + } + if (command === spoTenantAppCatalogUrlGetCommand) { + return { 'stdout': appCatalogUrl }; + } + throw 'Invalid request'; + }); + + sinon.stub(cli, 'executeCommand').callsFake(async (command, args): Promise<void> => { + if (command === spoListItemAddCommand) { + actualHostProperties = args.options.TenantWideExtensionHostProperties; + return; + } + throw 'Invalid request'; + }); + + await command.action(logger, { options: { clientSideComponentId: clientSideComponentId, title: customizerTitle, hostProperties: '{ "preAllocatedApplicationCustomizerTopHeight": "50", "preAllocatedApplicationCustomizerBottomHeight": "50" }', verbose: true } }); + assert.strictEqual(actualHostProperties, '{ "preAllocatedApplicationCustomizerTopHeight": "50", "preAllocatedApplicationCustomizerBottomHeight": "50" }'); + }); + it('throws an error when no app catalog is found', async () => { sinon.stub(cli, 'executeCommandWithOutput').callsFake(async (command): Promise<any> => { if (command === spoTenantAppCatalogUrlGetCommand) { @@ -254,6 +283,16 @@ describe(commands.TENANT_APPLICATIONCUSTOMIZER_ADD, () => { assert.notStrictEqual(actual, true); }); + it('fails validation if the clientSideComponentProperties option is not a valid json string', async () => { + const actual = await command.validate({ options: { title: customizerTitle, clientSideComponentId: clientSideComponentId, clientSideComponentProperties: 'invalid json string' } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('fails validation if the hostProperties option is not a valid json string', async () => { + const actual = await command.validate({ options: { title: customizerTitle, clientSideComponentId: clientSideComponentId, hostProperties: 'invalid json string' } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + it('passes validation when all properties are specified', async () => { const actual = await command.validate({ options: { title: customizerTitle, clientSideComponentId: clientSideComponentId, webTemplate: webTemplate, clientSideComponentProperties: clientSideComponentProperties } }, commandInfo); assert.strictEqual(actual, true); diff --git a/src/m365/spo/commands/tenant/tenant-applicationcustomizer-add.ts b/src/m365/spo/commands/tenant/tenant-applicationcustomizer-add.ts index 1e901a884ec..ee17cda833a 100644 --- a/src/m365/spo/commands/tenant/tenant-applicationcustomizer-add.ts +++ b/src/m365/spo/commands/tenant/tenant-applicationcustomizer-add.ts @@ -19,6 +19,7 @@ interface Options extends GlobalOptions { title: string; clientSideComponentId: string; clientSideComponentProperties?: string; + hostProperties?: string; webTemplate?: string; } @@ -43,6 +44,7 @@ class SpoTenantApplicationCustomizerAddCommand extends SpoCommand { this.telemetry.push((args: CommandArgs) => { Object.assign(this.telemetryProperties, { clientSideComponentProperties: typeof args.options.clientSideComponentProperties !== 'undefined', + hostProperties: typeof args.options.hostProperties !== 'undefined', webTemplate: typeof args.options.webTemplate !== 'undefined' }); }); @@ -59,6 +61,9 @@ class SpoTenantApplicationCustomizerAddCommand extends SpoCommand { { option: '-p, --clientSideComponentProperties [clientSideComponentProperties]' }, + { + option: '--hostProperties [hostProperties]' + }, { option: '-w, --webTemplate [webTemplate]' } @@ -72,6 +77,24 @@ class SpoTenantApplicationCustomizerAddCommand extends SpoCommand { return `${args.options.clientSideComponentId} is not a valid GUID`; } + if (args.options.clientSideComponentProperties) { + try { + JSON.parse(args.options.clientSideComponentProperties); + } + catch (e) { + return `An error has occurred while parsing clientSideComponentProperties: ${e}`; + } + } + + if (args.options.hostProperties) { + try { + JSON.parse(args.options.hostProperties); + } + catch (e) { + return `An error has occurred while parsing hostProperties: ${e}`; + } + } + return true; } ); @@ -190,6 +213,7 @@ class SpoTenantApplicationCustomizerAddCommand extends SpoCommand { TenantWideExtensionSequence: 0, TenantWideExtensionListTemplate: 0, TenantWideExtensionComponentProperties: options.clientSideComponentProperties || '', + TenantWideExtensionHostProperties: options.hostProperties || '', TenantWideExtensionWebTemplate: options.webTemplate || '', TenantWideExtensionDisabled: false, verbose: this.verbose, diff --git a/src/m365/spo/commands/tenant/tenant-applicationcustomizer-set.spec.ts b/src/m365/spo/commands/tenant/tenant-applicationcustomizer-set.spec.ts index e710d66017f..3d664658dc8 100644 --- a/src/m365/spo/commands/tenant/tenant-applicationcustomizer-set.spec.ts +++ b/src/m365/spo/commands/tenant/tenant-applicationcustomizer-set.spec.ts @@ -23,6 +23,7 @@ describe(commands.TENANT_APPLICATIONCUSTOMIZER_SET, () => { const id = 3; const clientSideComponentId = '7096cded-b83d-4eab-96f0-df477ed7c0bc'; const clientSideComponentProperties = '{ "someProperty": "Some value" }'; + const hostProperties = '{"preAllocatedApplicationCustomizerTopHeight":"50","preAllocatedApplicationCustomizerBottomHeight":"50"}'; const webTemplate = "GROUP#0"; const spoUrl = 'https://contoso.sharepoint.com'; const appCatalogUrl = 'https://contoso.sharepoint.com/sites/apps'; @@ -207,6 +208,16 @@ describe(commands.TENANT_APPLICATIONCUSTOMIZER_SET, () => { assert.notStrictEqual(actual, true); }); + it('fails validation if the clientSideComponentProperties option is not a valid json string', async () => { + const actual = await command.validate({ options: { id: id, clientSideComponentProperties: 'invalid json string' } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('fails validation if the hostProperties option is not a valid json string', async () => { + const actual = await command.validate({ options: { id: id, hostProperties: 'invalid json string' } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + it('passes validation if clientSideComponentId is valid', async () => { const actual = await command.validate({ options: { clientSideComponentId: clientSideComponentId, newTitle: newTitle } }, commandInfo); assert.strictEqual(actual, true); @@ -391,6 +402,33 @@ describe(commands.TENANT_APPLICATIONCUSTOMIZER_SET, () => { assert.deepEqual(executeCallsStub.firstCall.args[0].data, { formValues: [{ FieldName: 'Title', FieldValue: 'New customizer' }] }); }); + it('updates host properties of an application customizer by id', async () => { + defaultGetCallStub("Id eq '3'"); + const executeCallsStub: sinon.SinonStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === `https://contoso.sharepoint.com/sites/apps/_api/web/GetList('%2Fsites%2Fapps%2Flists%2FTenantWideExtensions')/Items(3)/ValidateUpdateListItem()`) { + return { + value: [ + { + FieldName: "TenantWideExtensionHostProperties", + FieldValue: hostProperties + } + ] + }; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { + options: { + id: id, + hostProperties: hostProperties + } + }); + + assert.deepEqual(executeCallsStub.firstCall.args[0].data, { formValues: [{ FieldName: 'TenantWideExtensionHostProperties', FieldValue: hostProperties }] }); + }); + it('updates client side component id of an application customizer by title', async () => { sinon.stub(cli, 'executeCommandWithOutput').callsFake(async (command, args): Promise<any> => { if (command === spoListItemListCommand) { diff --git a/src/m365/spo/commands/tenant/tenant-applicationcustomizer-set.ts b/src/m365/spo/commands/tenant/tenant-applicationcustomizer-set.ts index 6da4d009581..c6169eb98ce 100644 --- a/src/m365/spo/commands/tenant/tenant-applicationcustomizer-set.ts +++ b/src/m365/spo/commands/tenant/tenant-applicationcustomizer-set.ts @@ -30,6 +30,7 @@ interface Options extends GlobalOptions { newTitle?: string; newClientSideComponentId?: string; clientSideComponentProperties?: string; + hostProperties?: string; webTemplate?: string; } @@ -60,6 +61,7 @@ class SpoTenantApplicationCustomizerSetCommand extends SpoCommand { newTitle: typeof args.options.newTitle !== 'undefined', newClientSideComponentId: typeof args.options.newClientSideComponentId !== 'undefined', clientSideComponentProperties: typeof args.options.clientSideComponentProperties !== 'undefined', + hostProperties: typeof args.options.hostProperties !== 'undefined', webTemplate: typeof args.options.webTemplate !== 'undefined' }); }); @@ -85,6 +87,9 @@ class SpoTenantApplicationCustomizerSetCommand extends SpoCommand { { option: '-p, --clientSideComponentProperties [clientSideComponentProperties]' }, + { + option: '--hostProperties [hostProperties]' + }, { option: '-w, --webTemplate [webTemplate]' } @@ -106,7 +111,25 @@ class SpoTenantApplicationCustomizerSetCommand extends SpoCommand { return `${args.options.newClientSideComponentId} is not a valid GUID`; } - if (!args.options.newTitle && !args.options.newClientSideComponentId && !args.options.clientSideComponentProperties && !args.options.webTemplate) { + if (args.options.clientSideComponentProperties) { + try { + JSON.parse(args.options.clientSideComponentProperties); + } + catch (e) { + return `An error has occurred while parsing clientSideComponentProperties: ${e}`; + } + } + + if (args.options.hostProperties) { + try { + JSON.parse(args.options.hostProperties); + } + catch (e) { + return `An error has occurred while parsing hostProperties: ${e}`; + } + } + + if (!args.options.newTitle && !args.options.newClientSideComponentId && !args.options.clientSideComponentProperties && args.options.hostProperties === undefined && !args.options.webTemplate) { return `Please specify an option to be updated`; } @@ -238,7 +261,7 @@ class SpoTenantApplicationCustomizerSetCommand extends SpoCommand { } private async updateTenantWideExtension(appCatalogUrl: string, options: Options, listServerRelativeUrl: string, itemId: number, logger: Logger): Promise<void> { - const { title, id, clientSideComponentId, newTitle, newClientSideComponentId, clientSideComponentProperties, webTemplate } = options; + const { title, id, clientSideComponentId, newTitle, newClientSideComponentId, clientSideComponentProperties, hostProperties, webTemplate } = options; if (this.verbose) { await logger.logToStderr(`Updating tenant-wide application customizer: "${title || id || clientSideComponentId}"...`); @@ -267,6 +290,13 @@ class SpoTenantApplicationCustomizerSetCommand extends SpoCommand { }); } + if (hostProperties !== undefined) { + formValues.push({ + FieldName: 'TenantWideExtensionHostProperties', + FieldValue: hostProperties + }); + } + if (webTemplate !== undefined) { formValues.push({ FieldName: 'TenantWideExtensionWebTemplate',