From f3bef50bda689c5ab52dd8bbca8f3cf49ff82430 Mon Sep 17 00:00:00 2001 From: Michael Milton Date: Mon, 8 May 2023 13:00:43 +1000 Subject: [PATCH] Enhances 'spo page set' command Closes #4840 --- docs/docs/cmd/spo/page/page-set.mdx | 23 +++++++++ src/m365/spo/commands/page/page-set.spec.ts | 57 +++++++++++++++++++++ src/m365/spo/commands/page/page-set.ts | 18 ++++++- 3 files changed, 96 insertions(+), 2 deletions(-) diff --git a/docs/docs/cmd/spo/page/page-set.mdx b/docs/docs/cmd/spo/page/page-set.mdx index ba3b36a6604..fc0fbd4081d 100644 --- a/docs/docs/cmd/spo/page/page-set.mdx +++ b/docs/docs/cmd/spo/page/page-set.mdx @@ -42,6 +42,9 @@ m365 spo page set [options] `--title [title]` : The title to set for the page. + +`--content [content]` +: JSON string containing page content ``` @@ -52,6 +55,20 @@ If you try to create a page with a name of a page that already exists, you will If you choose to promote the page using the `promoteAs` option or enable page comments, you will see the result only after publishing the page. +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 treat quotes differently. For example, this is how you can add page content from the Windows cmd.exe: + +```sh +m365 spo page set --name page.aspx --webUrl https://contoso.sharepoint.com/sites/a-team --content '[{\"controlType\": 4,\"id\": \"42b8afe8-dafe-4c11-bfbf-df5ef5b1feb7\",\"position\": {\"layoutIndex\": 1,\"zoneIndex\": 1,\"sectionIndex\": 1,\"sectionFactor\": 12,\"controlIndex\": 1},\"addedFromPersistedData\": true,\"innerHTML\":\"

Hello World

\"}]' +``` + +Note, how the content option has escaped double quotes `'[{\"controlType\": 4,\"id\": \"42b8afe8-dafe-4c11-bfbf-df5ef5b1feb7\",\"position\": {\"layoutIndex\": 1,\"zoneIndex\": 1,\"sectionIndex\": 1,\"sectionFactor\": 12,\"controlIndex\": 1},\"addedFromPersistedData\": true,\"innerHTML\":\"

Hello World

\"}]'` compared to execution from bash `''[{"controlType": 4,"id": "42b8afe8-dafe-4c11-bfbf-df5ef5b1feb7","position": {"layoutIndex": 1,"zoneIndex": 1,"sectionIndex": 1,"sectionFactor": 12,"controlIndex": 1},"addedFromPersistedData": true,"innerHTML":"

Hello World

"}]'`. + +:::caution Escaping JSON in PowerShell + + When using the `--content` 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. + +::: + ## Examples Change the layout of the existing page to _Article_ @@ -102,6 +119,12 @@ Set page description m365 spo page set --name page.aspx --webUrl https://contoso.sharepoint.com/sites/a-team --description "Description to add for the page" ``` +Set page content + +```sh +m365 spo page set --name page.aspx --webUrl https://contoso.sharepoint.com/sites/a-team --content '[{\"controlType\": 4,\"id\": \"42b8afe8-dafe-4c11-bfbf-df5ef5b1feb7\",\"position\": {\"layoutIndex\": 1,\"zoneIndex\": 1,\"sectionIndex\": 1,\"sectionFactor\": 12,\"controlIndex\": 1},\"addedFromPersistedData\": true,\"innerHTML\":\"

Hello World

\"}]' +``` + ## Response The command won't return a response on success. diff --git a/src/m365/spo/commands/page/page-set.spec.ts b/src/m365/spo/commands/page/page-set.spec.ts index 3340ac1c4b7..4c530518a24 100644 --- a/src/m365/spo/commands/page/page-set.spec.ts +++ b/src/m365/spo/commands/page/page-set.spec.ts @@ -484,6 +484,53 @@ describe(commands.PAGE_SET, () => { await command.action(logger, { options: { debug: true, name: 'page.aspx', webUrl: 'https://contoso.sharepoint.com/sites/team-a', title: newPageTitle } }); }); + it('updates page content', async () => { + sinonUtil.restore([request.post]); + + const newContent = [{ + "controlType": 4, + "position": { + "layoutIndex": 1, + "zoneIndex": 1, + "sectionIndex": 1, + "sectionFactor": 12, + "controlIndex": 1 + }, + "addedFromPersistedData": true, + "innerHTML": "

Text content

" + }]; + + const initialPage = { + Title: "article", + Id: 1, + TopicHeader: "TopicHeader", + AuthorByline: "AuthorByline", + Description: "Description", + BannerImageUrl: { + Description: '/_layouts/15/images/sitepagethumbnail.png', + Url: `https://contoso.sharepoint.com/_layouts/15/images/sitepagethumbnail.png` + }, + CanvasContent1: "{}", + LayoutWebpartsContent: "{}" + }; + + let data: string = ''; + sinon.stub(request, 'post').callsFake(async (opts) => { + if ((opts.url as string).includes(`/_api/sitepages/pages/GetByUrl('sitepages/page.aspx')/checkoutpage`)) { + return initialPage; + } + if ((opts.url as string).includes(`/_api/SitePages/Pages(1)/SavePage`) || + (opts.url as string).includes(`/_api/SitePages/Pages(1)/SavePageAsDraft`)) { + data = opts.data.CanvasContent1; + return {}; + } + throw 'Invalid request'; + }); + + await command.action(logger, { options: { debug: true, name: 'page.aspx', webUrl: 'https://contoso.sharepoint.com/sites/team-a', content: JSON.stringify(newContent) } }); + assert.deepStrictEqual(JSON.parse(data), newContent); + }); + it('publishes page', async () => { sinonUtil.restore([request.post]); @@ -780,4 +827,14 @@ describe(commands.PAGE_SET, () => { const actual = await command.validate({ options: { name: 'page.aspx', webUrl: 'https://contoso.sharepoint.com', commentsEnabled: false } }, commandInfo); assert.strictEqual(actual, true); }); + + it('fails validation if content is not valid JSON', async () => { + const actual = await command.validate({ options: { name: 'page.aspx', webUrl: 'https://contoso.sharepoint.com', content: "foo" } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('passes validation if content is valid JSON', async () => { + const actual = await command.validate({ options: { name: 'page.aspx', webUrl: 'https://contoso.sharepoint.com', content: "[]" } }, commandInfo); + assert.strictEqual(actual, true); + }); }); \ No newline at end of file diff --git a/src/m365/spo/commands/page/page-set.ts b/src/m365/spo/commands/page/page-set.ts index baa9a4ef625..4af3378b691 100644 --- a/src/m365/spo/commands/page/page-set.ts +++ b/src/m365/spo/commands/page/page-set.ts @@ -29,6 +29,7 @@ interface Options extends GlobalOptions { description?: string; title?: string; demoteFrom?: string; + content?: string; } class SpoPageSetCommand extends SpoCommand { @@ -59,7 +60,8 @@ class SpoPageSetCommand extends SpoCommand { publish: args.options.publish || false, publishMessage: typeof args.options.publishMessage !== 'undefined', description: typeof args.options.description !== 'undefined', - title: typeof args.options.title !== 'undefined' + title: typeof args.options.title !== 'undefined', + content: typeof args.options.content !== 'undefined' }); }); } @@ -99,6 +101,9 @@ class SpoPageSetCommand extends SpoCommand { }, { option: '--title [title]' + }, + { + option: '--content [content]' } ); } @@ -138,6 +143,15 @@ class SpoPageSetCommand extends SpoCommand { return 'You can only promote article pages as news article'; } + if (args.options.content) { + try { + JSON.parse(args.options.content); + } + catch (e) { + return `Specified content is not a valid JSON string. Input: ${args.options.content}. Error: ${e}`; + } + } + return true; } ); @@ -174,7 +188,7 @@ class SpoPageSetCommand extends SpoCommand { pageId = page.Id; bannerImageUrl = page.BannerImageUrl; - canvasContent1 = page.CanvasContent1; + canvasContent1 = args.options.content || page.CanvasContent1; layoutWebpartsContent = page.LayoutWebpartsContent; pageDescription = pageDescription || page.Description; topicHeader = page.TopicHeader;