From 0c5087d6d5f737065dc4cd356960f8b0bde4b1df Mon Sep 17 00:00:00 2001 From: mkm17 Date: Tue, 19 Nov 2024 21:41:43 +0100 Subject: [PATCH] Fixes: Allow saving SP page with an existing collapsible section. Closes #6462 --- src/m365/spo/commands/page/canvasContent.ts | 22 +- src/m365/spo/commands/page/clientsidepages.ts | 164 +++++- .../page/page-clientsidewebpart-add.spec.ts | 107 ++++ .../page/page-clientsidewebpart-add.ts | 3 +- .../spo/commands/page/page-section-add.ts | 4 +- .../spo/commands/page/page-text-add.spec.ts | 530 ++++++++++++++++-- src/m365/spo/commands/page/page-text-add.ts | 8 + 7 files changed, 761 insertions(+), 77 deletions(-) diff --git a/src/m365/spo/commands/page/canvasContent.ts b/src/m365/spo/commands/page/canvasContent.ts index d040b794ae6..24b3d853e85 100644 --- a/src/m365/spo/commands/page/canvasContent.ts +++ b/src/m365/spo/commands/page/canvasContent.ts @@ -1,9 +1,11 @@ +import { ClientSideControlPosition, ZoneGroupMetadata } from "./clientsidepages"; + export interface Control { controlType?: number; displayMode: number; emphasis?: { zoneEmphasis?: number }; id?: string; - position: ControlPosition; + position: ClientSideControlPosition; reservedHeight?: number; reservedWidth?: number; webPartData?: any; @@ -44,22 +46,4 @@ export interface BackgroundControl { }, dataVersion: string; } -} - -interface ControlPosition { - controlIndex?: number; - layoutIndex: number; - sectionFactor: number; - sectionIndex: number; - zoneIndex: number; - isLayoutReflowOnTop?: boolean; - zoneId?: string; -} - -interface ZoneGroupMetadata { - type: number; - isExpanded: boolean; - showDividerLine: boolean; - iconAlignment: string; - displayName?: string; } \ No newline at end of file diff --git a/src/m365/spo/commands/page/clientsidepages.ts b/src/m365/spo/commands/page/clientsidepages.ts index 032a448b905..61e3b1a562f 100644 --- a/src/m365/spo/commands/page/clientsidepages.ts +++ b/src/m365/spo/commands/page/clientsidepages.ts @@ -114,7 +114,7 @@ function getGUID(): string { /** * Column size factor. Max value is 12 (= one column), other options are 8,6,4 or 0 */ -type CanvasColumnFactorType = 0 | 2 | 4 | 6 | 8 | 12; +export type CanvasColumnFactorType = 0 | 2 | 4 | 6 | 8 | 12; /** * Gets the next order value 1 based for the provided collection @@ -265,7 +265,8 @@ function reindex(collection?: { order: number, columns?: { order: number }[], co */ export class ClientSidePage { public sections: CanvasSection[] = []; - + public pageSettings?: PageSettings; + public backgroundSettings?: BackgroundSettings; /** * Converts a json object to an escaped string appropriate for use in attributes when storing client-side controls * @@ -324,8 +325,15 @@ export class ClientSidePage { html.push(this.sections[i].toHtml()); } - html.push(""); + if (this.pageSettings) { + html.push(this.pageSettings.toHtml()); + } + if (this.backgroundSettings) { + html.push(this.backgroundSettings.toHtml()); + } + + html.push(""); return html.join(""); } @@ -344,20 +352,26 @@ export class ClientSidePage { getBoundedDivMarkup(html, /]*data-sp-canvascontrol[^>]*?>/i, markup => { // get the control type - const ct = /controlType":(\d*?),/i.exec(markup); + const ct = /controlType":(\d*?)(,|&)/i.exec(markup); // if no control type is present this is a column which we give type 0 to let us process it - const controlType = ct == null || ct.length < 2 ? 0 : parseInt(ct[1], 10); + const controlType = ct == null || ct.length < 0 ? -1 : parseInt(ct[1], 10); let control: CanvasControl | null = null; switch (controlType) { - case 0: + case -1: // empty canvas column control = new CanvasColumn(null, 0); control.fromHtml(markup); page.mergeColumnToTree(control); break; + case 0: + // page settings + control = new PageSettings(); + control.fromHtml(markup); + page.pageSettings = control; + break; case 3: // client side webpart control = new ClientSideWebpart(""); @@ -370,6 +384,12 @@ export class ClientSidePage { control.fromHtml(markup); page.mergePartToTree(control); break; + case 14: + // BackgroundSection + control = new BackgroundSettings(); + control.fromHtml(markup); + page.backgroundSettings = control; + break; } }); @@ -415,6 +435,7 @@ export class ClientSidePage { let sectionFactor: CanvasColumnFactorType = 12; let sectionIndex = 0; let zoneIndex = 0; + let zoneId: string | undefined = ''; if (control.controlData) { // handle case where we don't have position data @@ -428,6 +449,9 @@ export class ClientSidePage { if (hOP(control.controlData.position, "sectionFactor")) { sectionFactor = control.controlData.position.sectionFactor; } + if (hOP(control.controlData.position, "zoneId")) { + zoneId = control.controlData.position.zoneId; + } } } @@ -447,6 +471,11 @@ export class ClientSidePage { column = columns[0]; } + if (!!zoneId) { + section.zoneId = zoneId; + column.zoneId = zoneId; + } + control.column = column; column.addControl(control); } @@ -476,7 +505,7 @@ export class ClientSidePage { } export class CanvasSection { - constructor(public page: ClientSidePage, public order: number, public columns: CanvasColumn[] = []) { + constructor(public page: ClientSidePage, public order: number, public columns: CanvasColumn[] = [], public zoneId?: string) { } /** @@ -496,7 +525,7 @@ export class CanvasSection { */ public addColumn(factor: CanvasColumnFactorType): CanvasColumn { - const column = new CanvasColumn(this, getNextOrder(this.columns), factor); + const column = new CanvasColumn(this, getNextOrder(this.columns), factor, this.zoneId); this.columns.push(column); return column; } @@ -544,12 +573,32 @@ abstract class CanvasControl { protected abstract getControlData(): ClientSideControlData; } +export class PageSettings extends CanvasControl { + constructor() { + super(0, "1.0"); + } + + protected getControlData(): ClientSideControlData { + return this.controlData as any; + } + + public toHtml(): string { + return `
`; + } + + public fromHtml(html: string): void { + super.fromHtml(html); + + } +} + export class CanvasColumn extends CanvasControl { constructor( public section: CanvasSection | null, public order: number, public factor: CanvasColumnFactorType = 12, + public zoneId: string | undefined = undefined, public controls: ClientSidePart[] = [], dataVersion = "1.0") { super(0, dataVersion); @@ -613,8 +662,10 @@ export class CanvasColumn extends CanvasControl { position: { sectionFactor: this.factor, sectionIndex: this.order, - zoneIndex: this.section ? this.section.order : 0 + zoneIndex: this.section ? this.section.order : 0, + zoneId: this.column?.zoneId }, + zoneGroupMetadata: this.controlData?.zoneGroupMetadata || this.column?.controlData?.zoneGroupMetadata, }; } @@ -647,6 +698,73 @@ export abstract class ClientSidePart extends CanvasControl { } } +export class BackgroundSettings extends ClientSidePart { + public propertieJson: TypedHash = {}; + protected serverProcessedContent: ServerProcessedContent | null = null; + + constructor() { + super(0, "1.0"); + } + + protected getControlData(): ClientSideControlData { + return { + controlType: this.controlType + } as any; + } + + public toHtml(): string { + // will form the value of the data-sp-webpartdata attribute + const data = { + dataVersion: this.dataVersion, + instanceId: this.id, + properties: this.propertieJson, + serverProcessedContent: this.serverProcessedContent, + }; + + + const html: string[] = []; + + html.push(`
`); + + html.push(`
`); + + html.push(`
`); + html.push("
"); + + html.push(`
`); + html.push("
"); + + html.push("
"); + html.push("
"); + + return html.join(""); + } + + private setProperties(properties: T): this { + this.propertieJson = extend(this.propertieJson, properties); + return this; + } + + public fromHtml(html: string): void { + super.fromHtml(html); + const webPartData = ClientSidePage.escapedStringToJson(getAttrValueFromString(html, "data-sp-webpartdata")); + + this.setProperties(webPartData.properties); + + if (typeof webPartData.serverProcessedContent !== "undefined") { + this.serverProcessedContent = webPartData.serverProcessedContent; + } + + if (typeof webPartData.dynamicDataPaths !== "undefined") { + this.dynamicDataPaths = webPartData.dynamicDataPaths; + } + + if (typeof webPartData.dynamicDataValues !== "undefined") { + this.dynamicDataValues = webPartData.dynamicDataValues; + } + } +} + export class ClientSideText extends ClientSidePart { private _text: string = ''; @@ -683,8 +801,10 @@ export class ClientSideText extends ClientSidePart { controlIndex: this.order, sectionFactor: this.column ? this.column.factor : 0, sectionIndex: this.column ? this.column.order : 0, - zoneIndex: this.column && this.column.section ? this.column.section.order : 0 + zoneIndex: this.column && this.column.section ? this.column.section.order : 0, + zoneId: this.column?.zoneId }, + zoneGroupMetadata: this.controlData?.zoneGroupMetadata || this.column?.controlData?.zoneGroupMetadata, }; } @@ -834,9 +954,11 @@ export class ClientSideWebpart extends ClientSidePart { controlIndex: this.order, sectionFactor: this.column ? this.column.factor : 0, sectionIndex: this.column ? this.column.order : 0, - zoneIndex: this.column && this.column.section ? this.column.section.order : 0 + zoneIndex: this.column && this.column.section ? this.column.section.order : 0, + zoneId: this.column?.zoneId }, webPartId: this.webPartId, + zoneGroupMetadata: this.controlData?.zoneGroupMetadata || this.column?.controlData?.zoneGroupMetadata, }; } @@ -983,20 +1105,36 @@ interface ServerProcessedContent { links: TypedHash; } -interface ClientSideControlPosition { +export interface ClientSideControlPosition { controlIndex?: number; + layoutIndex?: number; sectionFactor: CanvasColumnFactorType; sectionIndex: number; zoneIndex: number; + isLayoutReflowOnTop?: boolean; + zoneId?: string; +} + +export interface ZoneGroupMetadata { + type: number; + isExpanded: boolean; + showDividerLine: boolean; + iconAlignment: string; + displayName?: string; } -interface ClientSideControlData { +export interface ClientSideControlData { controlType?: number; id?: string; editorType?: string; + emphasis?: { zoneEmphasis?: number }; position: ClientSideControlPosition; + reservedHeight?: number; + reservedWidth?: number; + webPartData?: any; webPartId?: string; displayMode?: number; + zoneGroupMetadata?: ZoneGroupMetadata; } interface ClientSideWebpartData { diff --git a/src/m365/spo/commands/page/page-clientsidewebpart-add.spec.ts b/src/m365/spo/commands/page/page-clientsidewebpart-add.spec.ts index 6268c523af9..78a1d8822a9 100644 --- a/src/m365/spo/commands/page/page-clientsidewebpart-add.spec.ts +++ b/src/m365/spo/commands/page/page-clientsidewebpart-add.spec.ts @@ -2605,6 +2605,113 @@ describe(commands.PAGE_CLIENTSIDEWEBPART_ADD, () => { })); }); + it('adds web part to a column in collapsible section when order 1 specified', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://contoso.sharepoint.com/sites/team-a/_api/sitepages/pages/GetByUrl('sitepages/page.aspx')`) { + return { + "IsPageCheckedOutToCurrentUser": true, + "CanvasContent1": `[{"controlType":4,"editorType":"CKEditor","id":"24cebf73-d376-48e5-9b76-39b967c8dfd9","position":{"controlIndex":1,"sectionFactor":12,"sectionIndex":1,"zoneIndex":1,"zoneId":"e524fc79-e526-4da5-82e6-361018dedc67"},"addedFromPersistedData":true,"innerHTML":"

test

","emphasis":{"zoneEmphasis":0},"zoneGroupMetadata":{"type":1,"isExpanded":true,"showDividerLine":false,"iconAlignment":"left","displayName":"Test"}},{"controlType":0,"pageSettingsSlice":{"isDefaultDescription":true,"isDefaultThumbnail":true,"isSpellCheckEnabled":true,"globalRichTextStylingVersion":1,"rtePageSettings":{"contentVersion":5,"indentationVersion":2},"isEmailReady":false,"webPartsPageSettings":{"isTitleHeadingLevelsEnabled":false}}}]` + }; + } + + if (opts.url === `https://contoso.sharepoint.com/sites/team-a/_api/web/getclientsidewebparts()`) { + return clientSideWebParts; + } + + throw 'Invalid request'; + }); + + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === `https://contoso.sharepoint.com/sites/team-a/_api/sitepages/pages/GetByUrl('sitepages/page.aspx')/SavePageAsDraft`) { + return; + } + + throw 'Invalid request'; + }); + + await command.action(logger, + { + options: { + pageName: 'page.aspx', + webUrl: 'https://contoso.sharepoint.com/sites/team-a', + webPartId: 'e377ea37-9047-43b9-8cdb-a761be2f8e09', + order: 1 + } + }); + assert.strictEqual(replaceId(JSON.stringify(postStub.lastCall.args[0].data)), '{"CanvasContent1":"[{\\"controlType\\":3,\\"displayMode\\":2,\\"id\\":\\"89c644b3-f69c-4e84-85d7-dfa04c6163b5\\",\\"position\\":{\\"controlIndex\\":1,\\"sectionFactor\\":12,\\"sectionIndex\\":1,\\"zoneIndex\\":1,\\"zoneId\\":\\"e524fc79-e526-4da5-82e6-361018dedc67\\"},\\"webPartId\\":\\"e377ea37-9047-43b9-8cdb-a761be2f8e09\\",\\"emphasis\\":{},\\"zoneGroupMetadata\\":{\\"type\\":1,\\"isExpanded\\":true,\\"showDividerLine\\":false,\\"iconAlignment\\":\\"left\\",\\"displayName\\":\\"Test\\"},\\"webPartData\\":{\\"dataVersion\\":\\"1.0\\",\\"description\\":\\"Display a key location on a map\\",\\"id\\":\\"e377ea37-9047-43b9-8cdb-a761be2f8e09\\",\\"instanceId\\":\\"89c644b3-f69c-4e84-85d7-dfa04c6163b5\\",\\"properties\\":{\\"pushPins\\":[],\\"maxNumberOfPushPins\\":1,\\"shouldShowPushPinTitle\\":true,\\"zoomLevel\\":12,\\"mapType\\":\\"road\\"},\\"title\\":\\"Bing maps\\"}},{\\"controlType\\":4,\\"editorType\\":\\"CKEditor\\",\\"id\\":\\"24cebf73-d376-48e5-9b76-39b967c8dfd9\\",\\"position\\":{\\"controlIndex\\":2,\\"sectionFactor\\":12,\\"sectionIndex\\":1,\\"zoneIndex\\":1,\\"zoneId\\":\\"e524fc79-e526-4da5-82e6-361018dedc67\\"},\\"addedFromPersistedData\\":true,\\"innerHTML\\":\\"

test

\\",\\"emphasis\\":{\\"zoneEmphasis\\":0},\\"zoneGroupMetadata\\":{\\"type\\":1,\\"isExpanded\\":true,\\"showDividerLine\\":false,\\"iconAlignment\\":\\"left\\",\\"displayName\\":\\"Test\\"}},{\\"controlType\\":0,\\"pageSettingsSlice\\":{\\"isDefaultDescription\\":true,\\"isDefaultThumbnail\\":true,\\"isSpellCheckEnabled\\":true,\\"globalRichTextStylingVersion\\":1,\\"rtePageSettings\\":{\\"contentVersion\\":5,\\"indentationVersion\\":2},\\"isEmailReady\\":false,\\"webPartsPageSettings\\":{\\"isTitleHeadingLevelsEnabled\\":false}}}]"}'); + }); + + it('adds web part to a column with background settings order 1 specified', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://contoso.sharepoint.com/sites/team-a/_api/sitepages/pages/GetByUrl('sitepages/page.aspx')`) { + return { + "IsPageCheckedOutToCurrentUser": true, + "CanvasContent1": `[{"controlType":4,"editorType":"CKEditor","id":"24cebf73-d376-48e5-9b76-39b967c8dfd9","position":{"controlIndex":1,"sectionFactor":12,"sectionIndex":1,"zoneIndex":1,"zoneId":"e524fc79-e526-4da5-82e6-361018dedc67"},"addedFromPersistedData":true,"zoneGroupMetadata":{"type":0,"isExpanded":true,"showDividerLine":false,"iconAlignment":"left","displayName":"Test"},"innerHTML":"

test

"},{"controlType":0,"pageSettingsSlice":{"isDefaultDescription":true,"isDefaultThumbnail":true,"isSpellCheckEnabled":true,"globalRichTextStylingVersion":1,"rtePageSettings":{"contentVersion":5,"indentationVersion":2},"isEmailReady":false,"webPartsPageSettings":{"isTitleHeadingLevelsEnabled":false}}},{"controlType":14,"webPartData":{"properties":{"zoneBackground":{"e524fc79-e526-4da5-82e6-361018dedc67":{"type":"gradient","gradient":"radial-gradient(55.05% 96.28% at -5.05% -8.89%, #585984 0%, rgba(88, 89, 132, 0) 100%),linear-gradient(72.98deg, #AD8D8E 0.02%, #2A2A56 102.53%)","useLightText":true,"overlay":{"color":"#000000","opacity":60}}}},"serverProcessedContent":{"htmlStrings":{},"searchablePlainTexts":{},"imageSources":{},"links":{}},"dataVersion":"1.0"}}]` + }; + } + + if (opts.url === `https://contoso.sharepoint.com/sites/team-a/_api/web/getclientsidewebparts()`) { + return clientSideWebParts; + } + + throw 'Invalid request'; + }); + + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === `https://contoso.sharepoint.com/sites/team-a/_api/sitepages/pages/GetByUrl('sitepages/page.aspx')/SavePageAsDraft`) { + return; + } + + throw 'Invalid request'; + }); + + await command.action(logger, + { + options: { + pageName: 'page.aspx', + webUrl: 'https://contoso.sharepoint.com/sites/team-a', + webPartId: 'e377ea37-9047-43b9-8cdb-a761be2f8e09', + order: 1 + } + }); + assert.strictEqual(replaceId(JSON.stringify(postStub.lastCall.args[0].data)), '{"CanvasContent1":"[{\\"controlType\\":3,\\"displayMode\\":2,\\"id\\":\\"89c644b3-f69c-4e84-85d7-dfa04c6163b5\\",\\"position\\":{\\"controlIndex\\":1,\\"sectionFactor\\":12,\\"sectionIndex\\":1,\\"zoneIndex\\":1,\\"zoneId\\":\\"e524fc79-e526-4da5-82e6-361018dedc67\\"},\\"webPartId\\":\\"e377ea37-9047-43b9-8cdb-a761be2f8e09\\",\\"emphasis\\":{},\\"zoneGroupMetadata\\":{\\"type\\":0,\\"isExpanded\\":true,\\"showDividerLine\\":false,\\"iconAlignment\\":\\"left\\",\\"displayName\\":\\"Test\\"},\\"webPartData\\":{\\"dataVersion\\":\\"1.0\\",\\"description\\":\\"Display a key location on a map\\",\\"id\\":\\"e377ea37-9047-43b9-8cdb-a761be2f8e09\\",\\"instanceId\\":\\"89c644b3-f69c-4e84-85d7-dfa04c6163b5\\",\\"properties\\":{\\"pushPins\\":[],\\"maxNumberOfPushPins\\":1,\\"shouldShowPushPinTitle\\":true,\\"zoomLevel\\":12,\\"mapType\\":\\"road\\"},\\"title\\":\\"Bing maps\\"}},{\\"controlType\\":4,\\"editorType\\":\\"CKEditor\\",\\"id\\":\\"24cebf73-d376-48e5-9b76-39b967c8dfd9\\",\\"position\\":{\\"controlIndex\\":2,\\"sectionFactor\\":12,\\"sectionIndex\\":1,\\"zoneIndex\\":1,\\"zoneId\\":\\"e524fc79-e526-4da5-82e6-361018dedc67\\"},\\"addedFromPersistedData\\":true,\\"zoneGroupMetadata\\":{\\"type\\":0,\\"isExpanded\\":true,\\"showDividerLine\\":false,\\"iconAlignment\\":\\"left\\",\\"displayName\\":\\"Test\\"},\\"innerHTML\\":\\"

test

\\"},{\\"controlType\\":0,\\"pageSettingsSlice\\":{\\"isDefaultDescription\\":true,\\"isDefaultThumbnail\\":true,\\"isSpellCheckEnabled\\":true,\\"globalRichTextStylingVersion\\":1,\\"rtePageSettings\\":{\\"contentVersion\\":5,\\"indentationVersion\\":2},\\"isEmailReady\\":false,\\"webPartsPageSettings\\":{\\"isTitleHeadingLevelsEnabled\\":false}}},{\\"controlType\\":14,\\"webPartData\\":{\\"properties\\":{\\"zoneBackground\\":{\\"e524fc79-e526-4da5-82e6-361018dedc67\\":{\\"type\\":\\"gradient\\",\\"gradient\\":\\"radial-gradient(55.05% 96.28% at -5.05% -8.89%, #585984 0%, rgba(88, 89, 132, 0) 100%),linear-gradient(72.98deg, #AD8D8E 0.02%, #2A2A56 102.53%)\\",\\"useLightText\\":true,\\"overlay\\":{\\"color\\":\\"#000000\\",\\"opacity\\":60}}}},\\"serverProcessedContent\\":{\\"htmlStrings\\":{},\\"searchablePlainTexts\\":{},\\"imageSources\\":{},\\"links\\":{}},\\"dataVersion\\":\\"1.0\\"}}]"}'); + }); + + it('adds web part to a column with background settings and collapsible section', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://contoso.sharepoint.com/sites/team-a/_api/sitepages/pages/GetByUrl('sitepages/page.aspx')`) { + return { + "IsPageCheckedOutToCurrentUser": true, + "CanvasContent1": `[{"controlType":4,"editorType":"CKEditor","id":"24cebf73-d376-48e5-9b76-39b967c8dfd9","position":{"controlIndex":1,"sectionFactor":12,"sectionIndex":1,"zoneIndex":1,"zoneId":"e524fc79-e526-4da5-82e6-361018dedc67"},"addedFromPersistedData":true,"zoneGroupMetadata":{"type":1,"isExpanded":true,"showDividerLine":false,"iconAlignment":"left","displayName":"Test"},"innerHTML":"

test

"},{"controlType":0,"pageSettingsSlice":{"isDefaultDescription":true,"isDefaultThumbnail":true,"isSpellCheckEnabled":true,"globalRichTextStylingVersion":1,"rtePageSettings":{"contentVersion":5,"indentationVersion":2},"isEmailReady":false,"webPartsPageSettings":{"isTitleHeadingLevelsEnabled":false}}},{"controlType":14,"webPartData":{"properties":{"zoneBackground":{"e524fc79-e526-4da5-82e6-361018dedc67":{"type":"gradient","gradient":"radial-gradient(55.05% 96.28% at -5.05% -8.89%, #585984 0%, rgba(88, 89, 132, 0) 100%), linear-gradient(72.98deg, #AD8D8E 0.02%, #2A2A56 102.53%)","useLightText":true,"overlay":{"color":"#000000","opacity":60}}}},"serverProcessedContent":{"htmlStrings":{},"searchablePlainTexts":{},"imageSources":{},"links":{}},"dataVersion":"1.0"}}]` + }; + } + + if (opts.url === `https://contoso.sharepoint.com/sites/team-a/_api/web/getclientsidewebparts()`) { + return clientSideWebParts; + } + + throw 'Invalid request'; + }); + + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === `https://contoso.sharepoint.com/sites/team-a/_api/sitepages/pages/GetByUrl('sitepages/page.aspx')/SavePageAsDraft`) { + return; + } + + throw 'Invalid request'; + }); + + await command.action(logger, + { + options: { + pageName: 'page.aspx', + webUrl: 'https://contoso.sharepoint.com/sites/team-a', + webPartId: 'e377ea37-9047-43b9-8cdb-a761be2f8e09' + } + }); + assert.strictEqual(replaceId(JSON.stringify(postStub.lastCall.args[0].data)), '{"CanvasContent1":"[{\\"controlType\\":4,\\"editorType\\":\\"CKEditor\\",\\"id\\":\\"24cebf73-d376-48e5-9b76-39b967c8dfd9\\",\\"position\\":{\\"controlIndex\\":1,\\"sectionFactor\\":12,\\"sectionIndex\\":1,\\"zoneIndex\\":1,\\"zoneId\\":\\"e524fc79-e526-4da5-82e6-361018dedc67\\"},\\"addedFromPersistedData\\":true,\\"zoneGroupMetadata\\":{\\"type\\":1,\\"isExpanded\\":true,\\"showDividerLine\\":false,\\"iconAlignment\\":\\"left\\",\\"displayName\\":\\"Test\\"},\\"innerHTML\\":\\"

test

\\"},{\\"controlType\\":3,\\"displayMode\\":2,\\"id\\":\\"89c644b3-f69c-4e84-85d7-dfa04c6163b5\\",\\"position\\":{\\"controlIndex\\":2,\\"sectionFactor\\":12,\\"sectionIndex\\":1,\\"zoneIndex\\":1,\\"zoneId\\":\\"e524fc79-e526-4da5-82e6-361018dedc67\\"},\\"webPartId\\":\\"e377ea37-9047-43b9-8cdb-a761be2f8e09\\",\\"emphasis\\":{},\\"zoneGroupMetadata\\":{\\"type\\":1,\\"isExpanded\\":true,\\"showDividerLine\\":false,\\"iconAlignment\\":\\"left\\",\\"displayName\\":\\"Test\\"},\\"webPartData\\":{\\"dataVersion\\":\\"1.0\\",\\"description\\":\\"Display a key location on a map\\",\\"id\\":\\"e377ea37-9047-43b9-8cdb-a761be2f8e09\\",\\"instanceId\\":\\"89c644b3-f69c-4e84-85d7-dfa04c6163b5\\",\\"properties\\":{\\"pushPins\\":[],\\"maxNumberOfPushPins\\":1,\\"shouldShowPushPinTitle\\":true,\\"zoomLevel\\":12,\\"mapType\\":\\"road\\"},\\"title\\":\\"Bing maps\\"}},{\\"controlType\\":0,\\"pageSettingsSlice\\":{\\"isDefaultDescription\\":true,\\"isDefaultThumbnail\\":true,\\"isSpellCheckEnabled\\":true,\\"globalRichTextStylingVersion\\":1,\\"rtePageSettings\\":{\\"contentVersion\\":5,\\"indentationVersion\\":2},\\"isEmailReady\\":false,\\"webPartsPageSettings\\":{\\"isTitleHeadingLevelsEnabled\\":false}}},{\\"controlType\\":14,\\"webPartData\\":{\\"properties\\":{\\"zoneBackground\\":{\\"e524fc79-e526-4da5-82e6-361018dedc67\\":{\\"type\\":\\"gradient\\",\\"gradient\\":\\"radial-gradient(55.05% 96.28% at -5.05% -8.89%, #585984 0%, rgba(88, 89, 132, 0) 100%), linear-gradient(72.98deg, #AD8D8E 0.02%, #2A2A56 102.53%)\\",\\"useLightText\\":true,\\"overlay\\":{\\"color\\":\\"#000000\\",\\"opacity\\":60}}}},\\"serverProcessedContent\\":{\\"htmlStrings\\":{},\\"searchablePlainTexts\\":{},\\"imageSources\\":{},\\"links\\":{}},\\"dataVersion\\":\\"1.0\\"}}]"}'); + }); + it('correctly handles sections in reverse order', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { if ((opts.url as string).indexOf(`/_api/sitepages/pages/GetByUrl('sitepages/page.aspx')`) > -1) { diff --git a/src/m365/spo/commands/page/page-clientsidewebpart-add.ts b/src/m365/spo/commands/page/page-clientsidewebpart-add.ts index 33273588927..5c6b94e7aa3 100644 --- a/src/m365/spo/commands/page/page-clientsidewebpart-add.ts +++ b/src/m365/spo/commands/page/page-clientsidewebpart-add.ts @@ -270,7 +270,8 @@ class SpoPageClientSideWebPartAddCommand extends SpoCommand { id: webPart.id, position: Object.assign({}, control.position), webPartId: webPart.webPartId, - emphasis: {} + emphasis: {}, + zoneGroupMetadata: control.zoneGroupMetadata }, webPart); if (!control.controlType) { diff --git a/src/m365/spo/commands/page/page-section-add.ts b/src/m365/spo/commands/page/page-section-add.ts index 8aa29096158..de4eb61771f 100644 --- a/src/m365/spo/commands/page/page-section-add.ts +++ b/src/m365/spo/commands/page/page-section-add.ts @@ -7,7 +7,7 @@ import { validation } from '../../../../utils/validation.js'; import SpoCommand from '../../../base/SpoCommand.js'; import commands from '../../commands.js'; import { BackgroundControl, Control } from './canvasContent.js'; -import { CanvasSectionTemplate } from './clientsidepages.js'; +import { CanvasColumnFactorType, CanvasSectionTemplate } from './clientsidepages.js'; interface CommandArgs { options: Options; @@ -372,7 +372,7 @@ class SpoPageSectionAddCommand extends SpoCommand { return columns; } - private getColumn(zoneIndex: number, sectionIndex: number, sectionFactor: number, args: CommandArgs, zoneId?: string): Control { + private getColumn(zoneIndex: number, sectionIndex: number, sectionFactor: CanvasColumnFactorType, args: CommandArgs, zoneId?: string): Control { const { zoneEmphasis, isCollapsibleSection, isExpanded, showDivider, iconAlignment, collapsibleTitle } = args.options; const columnValue: Control = { displayMode: 2, diff --git a/src/m365/spo/commands/page/page-text-add.spec.ts b/src/m365/spo/commands/page/page-text-add.spec.ts index a1d3192fa41..e4bb6583fcb 100644 --- a/src/m365/spo/commands/page/page-text-add.spec.ts +++ b/src/m365/spo/commands/page/page-text-add.spec.ts @@ -17,7 +17,6 @@ import command from './page-text-add.js'; describe(commands.PAGE_TEXT_ADD, () => { let log: string[]; let logger: Logger; - let loggerLogSpy: sinon.SinonSpy; let commandInfo: CommandInfo; let loggerLogToStderrSpy: sinon.SinonSpy; @@ -49,7 +48,6 @@ describe(commands.PAGE_TEXT_ADD, () => { log.push(msg); } }; - loggerLogSpy = sinon.spy(logger, 'log'); loggerLogToStderrSpy = sinon.spy(logger, 'logToStderr'); }); @@ -75,7 +73,7 @@ describe(commands.PAGE_TEXT_ADD, () => { it('adds text to an empty modern page', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { - if ((opts.url as string).indexOf(`/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/SitePages/page.aspx')?$expand=ListItemAllFields/ClientSideApplicationId`) > -1) { + if (opts.url === `https://contoso.sharepoint.com/sites/team-a/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/SitePages/page.aspx')?$expand=ListItemAllFields/ClientSideApplicationId`) { return { ListItemAllFields: { CommentsDisabled: false, @@ -140,9 +138,9 @@ describe(commands.PAGE_TEXT_ADD, () => { throw 'Invalid request'; }); - sinon.stub(request, 'post').callsFake(async (opts) => { + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { if (opts.url === "https://contoso.sharepoint.com/sites/team-a/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/sitepages/page.aspx')/ListItemAllFields") { - return {}; + return; } throw 'Invalid request'; @@ -156,12 +154,15 @@ describe(commands.PAGE_TEXT_ADD, () => { text: 'Hello world' } }); - assert(loggerLogSpy.notCalled); + + const regex = /

Hello world<\/p><\/div><\/div>

<\/div><\/div>/; + + assert.match(postStub.lastCall.args[0].data.CanvasContent1, regex); }); it('adds text to an empty modern page (debug)', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { - if ((opts.url as string).indexOf(`https://contoso.sharepoint.com/sites/team-a/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/SitePages/page.aspx')?$expand=ListItemAllFields/ClientSideApplicationId`) > -1) { + if (opts.url === `https://contoso.sharepoint.com/sites/team-a/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/SitePages/page.aspx')?$expand=ListItemAllFields/ClientSideApplicationId`) { return { ListItemAllFields: { CommentsDisabled: false, @@ -227,9 +228,8 @@ describe(commands.PAGE_TEXT_ADD, () => { }); sinon.stub(request, 'post').callsFake(async (opts) => { - if ((opts.url as string).indexOf(`https://contoso.sharepoint.com/sites/team-a/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/sitepages/page.aspx')/ListItemAllFields`) > -1 && - JSON.stringify(opts.data).indexOf(`","position":{"controlIndex":1,"sectionFactor":12,"sectionIndex":1,"zoneIndex":1}}\\">

Hello world

"}`) > -1) { - return {}; + if (opts.url === `https://contoso.sharepoint.com/sites/team-a/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/sitepages/page.aspx')/ListItemAllFields`) { + return; } throw 'Invalid request'; @@ -249,7 +249,7 @@ describe(commands.PAGE_TEXT_ADD, () => { it('adds text to an empty modern page on root of tenant (debug)', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { - if ((opts.url as string).indexOf(`https://contoso.sharepoint.com/_api/web/GetFileByServerRelativePath(DecodedUrl='/SitePages/page.aspx')?$expand=ListItemAllFields/ClientSideApplicationId`) > -1) { + if (opts.url === `https://contoso.sharepoint.com/_api/web/GetFileByServerRelativePath(DecodedUrl='/SitePages/page.aspx')?$expand=ListItemAllFields/ClientSideApplicationId`) { return { ListItemAllFields: { CommentsDisabled: false, @@ -315,9 +315,8 @@ describe(commands.PAGE_TEXT_ADD, () => { }); sinon.stub(request, 'post').callsFake(async (opts) => { - if ((opts.url as string).indexOf(`https://contoso.sharepoint.com/_api/web/GetFileByServerRelativePath(DecodedUrl='/sitepages/page.aspx')/ListItemAllFields`) > -1 && - JSON.stringify(opts.data).indexOf(`","position":{"controlIndex":1,"sectionFactor":12,"sectionIndex":1,"zoneIndex":1}}\\">

Hello world

"}`) > -1) { - return {}; + if (opts.url === `https://contoso.sharepoint.com/_api/web/GetFileByServerRelativePath(DecodedUrl='/sitepages/page.aspx')/ListItemAllFields`) { + return; } throw 'Invalid request'; @@ -337,10 +336,8 @@ describe(commands.PAGE_TEXT_ADD, () => { it('appends text to a modern page which already had some text', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { - if ( - (opts.url as string).indexOf( - `/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/SitePages/page.aspx')?$expand=ListItemAllFields/ClientSideApplicationId` - ) > -1 + if (opts.url === + `https://contoso.sharepoint.com/sites/team-a/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/SitePages/page.aspx')?$expand=ListItemAllFields/ClientSideApplicationId` ) { return { ListItemAllFields: { @@ -406,9 +403,9 @@ describe(commands.PAGE_TEXT_ADD, () => { throw 'Invalid request'; }); - sinon.stub(request, 'post').callsFake(async (opts) => { + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { if (opts.url === "https://contoso.sharepoint.com/sites/team-a/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/sitepages/page.aspx')/ListItemAllFields") { - return {}; + return; } throw 'Invalid request'; @@ -422,15 +419,16 @@ describe(commands.PAGE_TEXT_ADD, () => { text: 'Hello world' } }); - assert(loggerLogSpy.notCalled); + + const regex = /

Hello world<\/p><\/div><\/div>

Hello world<\/p><\/div><\/div><\/div>/; + + assert.match(postStub.lastCall.args[0].data.CanvasContent1, regex); }); it('adds text in the specified order to a modern page which already had some text', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { - if ( - (opts.url as string).indexOf( - `/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/SitePages/page.aspx')?$expand=ListItemAllFields/ClientSideApplicationId` - ) > -1 + if (opts.url === + `https://contoso.sharepoint.com/sites/team-a/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/SitePages/page.aspx')?$expand=ListItemAllFields/ClientSideApplicationId` ) { return { ListItemAllFields: { @@ -496,9 +494,9 @@ describe(commands.PAGE_TEXT_ADD, () => { throw 'Invalid request'; }); - sinon.stub(request, 'post').callsFake(async (opts) => { + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { if (opts.url === "https://contoso.sharepoint.com/sites/team-a/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/sitepages/page.aspx')/ListItemAllFields") { - return {}; + return; } throw 'Invalid request'; @@ -513,16 +511,16 @@ describe(commands.PAGE_TEXT_ADD, () => { order: 2 } }); - assert(loggerLogSpy.notCalled); + + const regex = /

Hello world<\/p><\/div><\/div>

Hello world 1.1<\/p><\/div><\/div>

Hello world 2<\/p><\/div><\/div><\/div>/; + + assert.match(postStub.lastCall.args[0].data.CanvasContent1, regex); }); it('adds text to a modern page without specifying the page file extension', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { - if ( - (opts.url as string).indexOf( - `/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/SitePages/page.aspx')?$expand=ListItemAllFields/ClientSideApplicationId` - ) > -1 - ) { + if (opts.url === + `https://contoso.sharepoint.com/sites/team-a/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/SitePages/page.aspx')?$expand=ListItemAllFields/ClientSideApplicationId`) { return { ListItemAllFields: { CommentsDisabled: false, @@ -587,9 +585,9 @@ describe(commands.PAGE_TEXT_ADD, () => { throw 'Invalid request'; }); - sinon.stub(request, 'post').callsFake(async (opts) => { + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { if (opts.url === "https://contoso.sharepoint.com/sites/team-a/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/sitepages/page.aspx')/ListItemAllFields") { - return {}; + return; } throw 'Invalid request'; @@ -603,12 +601,460 @@ describe(commands.PAGE_TEXT_ADD, () => { text: 'Hello world' } }); - assert(loggerLogSpy.notCalled); + + const regex = /

Hello world<\/p><\/div><\/div>

<\/div><\/div>/; + + assert.match(postStub.lastCall.args[0].data.CanvasContent1, regex); + }); + + it('adds text to a modern page with existing empty collapsable section and page settings control', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === + `https://contoso.sharepoint.com/sites/team-a/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/SitePages/page.aspx')?$expand=ListItemAllFields/ClientSideApplicationId`) { + return { + ListItemAllFields: { + CommentsDisabled: false, + FileSystemObjectType: 0, + Id: 1, + ServerRedirectedEmbedUri: null, + ServerRedirectedEmbedUrl: '', + ContentTypeId: '0x0101009D1CB255DA76424F860D91F20E6C41180062FDF2882AB3F745ACB63105A3C623C9', + FileLeafRef: 'Home.aspx', + ComplianceAssetId: null, + WikiField: null, + Title: 'Home', + ClientSideApplicationId: 'b6917cb1-93a0-4b97-a84d-7cf49975d4ec', + PageLayoutType: 'Home', + CanvasContent1: '
', + BannerImageUrl: { + Description: '/_layouts/15/images/sitepagethumbnail.png', + Url: 'https://contoso.sharepoint.com/_layouts/15/images/sitepagethumbnail.png' + }, + Description: 'Lorem ipsum Dolor samet Lorem ipsum', + PromotedState: null, + FirstPublishedDate: null, + LayoutWebpartsContent: null, + AuthorsId: null, + AuthorsStringId: null, + OriginalSourceUrl: null, + ID: 1, + Created: '2018-01-20T09:54:41', + AuthorId: 1073741823, + Modified: '2018-04-12T12:42:47', + EditorId: 12, + OData__CopySource: null, + CheckoutUserId: null, + OData__UIVersionString: '7.0', + GUID: 'edaab907-e729-48dd-9e73-26487c0cf592' + }, + CheckInComment: '', + CheckOutType: 2, + ContentTag: '{E82A21D1-CA2C-4854-98F2-012AC0E7FA09},25,1', + CustomizedPageStatus: 1, + ETag: '"{E82A21D1-CA2C-4854-98F2-012AC0E7FA09},25"', + Exists: true, + IrmEnabled: false, + Length: '805', + Level: 1, + LinkingUri: null, + LinkingUrl: '', + MajorVersion: 7, + MinorVersion: 0, + Name: 'home.aspx', + ServerRelativeUrl: '/sites/team-a/SitePages/home.aspx', + TimeCreated: '2018-01-20T08:54:41Z', + TimeLastModified: '2018-04-12T10:42:46Z', + Title: 'Home', + UIVersion: 3584, + UIVersionLabel: '7.0', + UniqueId: 'e82a21d1-ca2c-4854-98f2-012ac0e7fa09' + }; + } + + throw 'Invalid request'; + }); + + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === "https://contoso.sharepoint.com/sites/team-a/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/sitepages/page.aspx')/ListItemAllFields") { + return; + } + + throw 'Invalid request'; + }); + + await command.action(logger, + { + options: { + pageName: 'page.aspx', + webUrl: 'https://contoso.sharepoint.com/sites/team-a', + text: 'Hello world' + } + }); + + const regex = /

Hello world<\/p><\/div><\/div>

<\/div><\/div>/; + + assert.match(postStub.lastCall.args[0].data.CanvasContent1, regex); + }); + + it('adds text to a modern page with existing collapsable section with existing text webpart', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://contoso.sharepoint.com/sites/team-a/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/SitePages/page.aspx')?$expand=ListItemAllFields/ClientSideApplicationId`) { + return { + ListItemAllFields: { + CommentsDisabled: false, + FileSystemObjectType: 0, + Id: 1, + ServerRedirectedEmbedUri: null, + ServerRedirectedEmbedUrl: '', + ContentTypeId: '0x0101009D1CB255DA76424F860D91F20E6C41180062FDF2882AB3F745ACB63105A3C623C9', + FileLeafRef: 'Home.aspx', + ComplianceAssetId: null, + WikiField: null, + Title: 'Home', + ClientSideApplicationId: 'b6917cb1-93a0-4b97-a84d-7cf49975d4ec', + PageLayoutType: 'Home', + CanvasContent1: '

text

', + BannerImageUrl: { + Description: '/_layouts/15/images/sitepagethumbnail.png', + Url: 'https://contoso.sharepoint.com/_layouts/15/images/sitepagethumbnail.png' + }, + Description: 'Lorem ipsum Dolor samet Lorem ipsum', + PromotedState: null, + FirstPublishedDate: null, + LayoutWebpartsContent: null, + AuthorsId: null, + AuthorsStringId: null, + OriginalSourceUrl: null, + ID: 1, + Created: '2018-01-20T09:54:41', + AuthorId: 1073741823, + Modified: '2018-04-12T12:42:47', + EditorId: 12, + OData__CopySource: null, + CheckoutUserId: null, + OData__UIVersionString: '7.0', + GUID: 'edaab907-e729-48dd-9e73-26487c0cf592' + }, + CheckInComment: '', + CheckOutType: 2, + ContentTag: '{E82A21D1-CA2C-4854-98F2-012AC0E7FA09},25,1', + CustomizedPageStatus: 1, + ETag: '"{E82A21D1-CA2C-4854-98F2-012AC0E7FA09},25"', + Exists: true, + IrmEnabled: false, + Length: '805', + Level: 1, + LinkingUri: null, + LinkingUrl: '', + MajorVersion: 7, + MinorVersion: 0, + Name: 'home.aspx', + ServerRelativeUrl: '/sites/team-a/SitePages/home.aspx', + TimeCreated: '2018-01-20T08:54:41Z', + TimeLastModified: '2018-04-12T10:42:46Z', + Title: 'Home', + UIVersion: 3584, + UIVersionLabel: '7.0', + UniqueId: 'e82a21d1-ca2c-4854-98f2-012ac0e7fa09' + }; + } + + throw 'Invalid request'; + }); + + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === "https://contoso.sharepoint.com/sites/team-a/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/sitepages/page.aspx')/ListItemAllFields") { + return; + } + + throw 'Invalid request'; + }); + + await command.action(logger, + { + options: { + pageName: 'page.aspx', + webUrl: 'https://contoso.sharepoint.com/sites/team-a', + text: 'Hello world' + } + }); + + const regex = /

text<\/p><\/div><\/div>

Hello world<\/p><\/div><\/div>

<\/div><\/div>/; + + assert.match(postStub.lastCall.args[0].data.CanvasContent1, regex); + }); + + it('adds text to a modern page with background setting and existing text webpart', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://contoso.sharepoint.com/sites/team-a/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/SitePages/page.aspx')?$expand=ListItemAllFields/ClientSideApplicationId`) { + return { + ListItemAllFields: { + CommentsDisabled: false, + FileSystemObjectType: 0, + Id: 1, + ServerRedirectedEmbedUri: null, + ServerRedirectedEmbedUrl: '', + ContentTypeId: '0x0101009D1CB255DA76424F860D91F20E6C41180062FDF2882AB3F745ACB63105A3C623C9', + FileLeafRef: 'Home.aspx', + ComplianceAssetId: null, + WikiField: null, + Title: 'Home', + ClientSideApplicationId: 'b6917cb1-93a0-4b97-a84d-7cf49975d4ec', + PageLayoutType: 'Home', + CanvasContent1: '

test

', + BannerImageUrl: { + Description: '/_layouts/15/images/sitepagethumbnail.png', + Url: 'https://contoso.sharepoint.com/_layouts/15/images/sitepagethumbnail.png' + }, + Description: 'Lorem ipsum Dolor samet Lorem ipsum', + PromotedState: null, + FirstPublishedDate: null, + LayoutWebpartsContent: null, + AuthorsId: null, + AuthorsStringId: null, + OriginalSourceUrl: null, + ID: 1, + Created: '2018-01-20T09:54:41', + AuthorId: 1073741823, + Modified: '2018-04-12T12:42:47', + EditorId: 12, + OData__CopySource: null, + CheckoutUserId: null, + OData__UIVersionString: '7.0', + GUID: 'edaab907-e729-48dd-9e73-26487c0cf592' + }, + CheckInComment: '', + CheckOutType: 2, + ContentTag: '{E82A21D1-CA2C-4854-98F2-012AC0E7FA09},25,1', + CustomizedPageStatus: 1, + ETag: '"{E82A21D1-CA2C-4854-98F2-012AC0E7FA09},25"', + Exists: true, + IrmEnabled: false, + Length: '805', + Level: 1, + LinkingUri: null, + LinkingUrl: '', + MajorVersion: 7, + MinorVersion: 0, + Name: 'home.aspx', + ServerRelativeUrl: '/sites/team-a/SitePages/home.aspx', + TimeCreated: '2018-01-20T08:54:41Z', + TimeLastModified: '2018-04-12T10:42:46Z', + Title: 'Home', + UIVersion: 3584, + UIVersionLabel: '7.0', + UniqueId: 'e82a21d1-ca2c-4854-98f2-012ac0e7fa09' + }; + } + + throw 'Invalid request'; + }); + + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === "https://contoso.sharepoint.com/sites/team-a/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/sitepages/page.aspx')/ListItemAllFields") { + return; + } + + throw 'Invalid request'; + }); + + await command.action(logger, + { + options: { + pageName: 'page.aspx', + webUrl: 'https://contoso.sharepoint.com/sites/team-a', + text: 'Hello world' + } + }); + + const regex = /

test<\/p><\/div><\/div>

Hello world<\/p><\/div><\/div>

<\/div>
<\/div>
<\/div><\/div><\/div><\/div>/; + + assert.match(postStub.lastCall.args[0].data.CanvasContent1, regex); + }); + + it('adds text to a modern page on specific section and column with background setting and existing text webpart', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://contoso.sharepoint.com/sites/team-a/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/SitePages/page.aspx')?$expand=ListItemAllFields/ClientSideApplicationId`) { + return { + ListItemAllFields: { + CommentsDisabled: false, + FileSystemObjectType: 0, + Id: 1, + ServerRedirectedEmbedUri: null, + ServerRedirectedEmbedUrl: '', + ContentTypeId: '0x0101009D1CB255DA76424F860D91F20E6C41180062FDF2882AB3F745ACB63105A3C623C9', + FileLeafRef: 'Home.aspx', + ComplianceAssetId: null, + WikiField: null, + Title: 'Home', + ClientSideApplicationId: 'b6917cb1-93a0-4b97-a84d-7cf49975d4ec', + PageLayoutType: 'Home', + CanvasContent1: '

test

', + BannerImageUrl: { + Description: '/_layouts/15/images/sitepagethumbnail.png', + Url: 'https://contoso.sharepoint.com/_layouts/15/images/sitepagethumbnail.png' + }, + Description: 'Lorem ipsum Dolor samet Lorem ipsum', + PromotedState: null, + FirstPublishedDate: null, + LayoutWebpartsContent: null, + AuthorsId: null, + AuthorsStringId: null, + OriginalSourceUrl: null, + ID: 1, + Created: '2018-01-20T09:54:41', + AuthorId: 1073741823, + Modified: '2018-04-12T12:42:47', + EditorId: 12, + OData__CopySource: null, + CheckoutUserId: null, + OData__UIVersionString: '7.0', + GUID: 'edaab907-e729-48dd-9e73-26487c0cf592' + }, + CheckInComment: '', + CheckOutType: 2, + ContentTag: '{E82A21D1-CA2C-4854-98F2-012AC0E7FA09},25,1', + CustomizedPageStatus: 1, + ETag: '"{E82A21D1-CA2C-4854-98F2-012AC0E7FA09},25"', + Exists: true, + IrmEnabled: false, + Length: '805', + Level: 1, + LinkingUri: null, + LinkingUrl: '', + MajorVersion: 7, + MinorVersion: 0, + Name: 'home.aspx', + ServerRelativeUrl: '/sites/team-a/SitePages/home.aspx', + TimeCreated: '2018-01-20T08:54:41Z', + TimeLastModified: '2018-04-12T10:42:46Z', + Title: 'Home', + UIVersion: 3584, + UIVersionLabel: '7.0', + UniqueId: 'e82a21d1-ca2c-4854-98f2-012ac0e7fa09' + }; + } + + throw 'Invalid request'; + }); + + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === "https://contoso.sharepoint.com/sites/team-a/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/sitepages/page.aspx')/ListItemAllFields") { + return; + } + + throw 'Invalid request'; + }); + + await command.action(logger, + { + options: { + pageName: 'page.aspx', + webUrl: 'https://contoso.sharepoint.com/sites/team-a', + text: 'Hello world', + section: 1, + column: 2 + } + }); + + const regex = /

test<\/p><\/div><\/div>

Hello world<\/p><\/div><\/div>

<\/div>
<\/div>
<\/div><\/div><\/div><\/div>/; + + assert.match(postStub.lastCall.args[0].data.CanvasContent1, regex); + }); + + it('adds text to a modern page on specific section and column with background setting, collapsible section and existing text webpart', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://contoso.sharepoint.com/sites/team-a/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/SitePages/page.aspx')?$expand=ListItemAllFields/ClientSideApplicationId`) { + return { + ListItemAllFields: { + CommentsDisabled: false, + FileSystemObjectType: 0, + Id: 1, + ServerRedirectedEmbedUri: null, + ServerRedirectedEmbedUrl: '', + ContentTypeId: '0x0101009D1CB255DA76424F860D91F20E6C41180062FDF2882AB3F745ACB63105A3C623C9', + FileLeafRef: 'Home.aspx', + ComplianceAssetId: null, + WikiField: null, + Title: 'Home', + ClientSideApplicationId: 'b6917cb1-93a0-4b97-a84d-7cf49975d4ec', + PageLayoutType: 'Home', + CanvasContent1: '

test

', + BannerImageUrl: { + Description: '/_layouts/15/images/sitepagethumbnail.png', + Url: 'https://contoso.sharepoint.com/_layouts/15/images/sitepagethumbnail.png' + }, + Description: 'Lorem ipsum Dolor samet Lorem ipsum', + PromotedState: null, + FirstPublishedDate: null, + LayoutWebpartsContent: null, + AuthorsId: null, + AuthorsStringId: null, + OriginalSourceUrl: null, + ID: 1, + Created: '2018-01-20T09:54:41', + AuthorId: 1073741823, + Modified: '2018-04-12T12:42:47', + EditorId: 12, + OData__CopySource: null, + CheckoutUserId: null, + OData__UIVersionString: '7.0', + GUID: 'edaab907-e729-48dd-9e73-26487c0cf592' + }, + CheckInComment: '', + CheckOutType: 2, + ContentTag: '{E82A21D1-CA2C-4854-98F2-012AC0E7FA09},25,1', + CustomizedPageStatus: 1, + ETag: '"{E82A21D1-CA2C-4854-98F2-012AC0E7FA09},25"', + Exists: true, + IrmEnabled: false, + Length: '805', + Level: 1, + LinkingUri: null, + LinkingUrl: '', + MajorVersion: 7, + MinorVersion: 0, + Name: 'home.aspx', + ServerRelativeUrl: '/sites/team-a/SitePages/home.aspx', + TimeCreated: '2018-01-20T08:54:41Z', + TimeLastModified: '2018-04-12T10:42:46Z', + Title: 'Home', + UIVersion: 3584, + UIVersionLabel: '7.0', + UniqueId: 'e82a21d1-ca2c-4854-98f2-012ac0e7fa09' + }; + } + + throw 'Invalid request'; + }); + + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === "https://contoso.sharepoint.com/sites/team-a/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/sitepages/page.aspx')/ListItemAllFields") { + return; + } + + throw 'Invalid request'; + }); + + await command.action(logger, + { + options: { + pageName: 'page.aspx', + webUrl: 'https://contoso.sharepoint.com/sites/team-a', + text: 'Hello world', + section: 1, + column: 2 + } + }); + + const regex = /

test<\/p><\/div><\/div>

Hello world<\/p><\/div><\/div>

<\/div>
<\/div>
<\/div><\/div><\/div><\/div>/; + + assert.match(postStub.lastCall.args[0].data.CanvasContent1, regex); }); it('correctly handles OData error when adding text to a non-existing page', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { - if ((opts.url as string).indexOf(`/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/SitePages/foo.aspx')?$expand=ListItemAllFields/ClientSideApplicationId`) > -1) { + if (opts.url === `https://contoso.sharepoint.com/sites/team-a/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/SitePages/foo.aspx')?$expand=ListItemAllFields/ClientSideApplicationId`) { throw { error: { 'odata.error': { message: { value: 'The file /sites/team-a/SitePages/foo.aspx does not exist' } } } }; } @@ -627,7 +1073,7 @@ describe(commands.PAGE_TEXT_ADD, () => { it('correctly handles OData error when adding text to a page', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { - if ((opts.url as string).indexOf(`/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/SitePages/page.aspx')?$expand=ListItemAllFields/ClientSideApplicationId`) > -1) { + if (opts.url === `https://contoso.sharepoint.com/sites/team-a/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/SitePages/page.aspx')?$expand=ListItemAllFields/ClientSideApplicationId`) { return { ListItemAllFields: { CommentsDisabled: false, @@ -708,7 +1154,7 @@ describe(commands.PAGE_TEXT_ADD, () => { it('correctly handles error if target page is not a modern page', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { - if ((opts.url as string).indexOf(`/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/SitePages/page.aspx')?$expand=ListItemAllFields/ClientSideApplicationId`) > -1) { + if (opts.url === `https://contoso.sharepoint.com/sites/team-a/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/SitePages/page.aspx')?$expand=ListItemAllFields/ClientSideApplicationId`) { return { ListItemAllFields: { CommentsDisabled: false, @@ -777,7 +1223,7 @@ describe(commands.PAGE_TEXT_ADD, () => { it('correctly handles invalid section error when adding text to modern page', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { - if ((opts.url as string).indexOf(`/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/SitePages/page.aspx')?$expand=ListItemAllFields/ClientSideApplicationId`) > -1) { + if (opts.url === `https://contoso.sharepoint.com/sites/team-a/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/SitePages/page.aspx')?$expand=ListItemAllFields/ClientSideApplicationId`) { return { ListItemAllFields: { CommentsDisabled: false, @@ -855,7 +1301,7 @@ describe(commands.PAGE_TEXT_ADD, () => { it('correctly handles invalid column error when adding text to modern page', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { - if ((opts.url as string).indexOf(`/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/SitePages/page.aspx')?$expand=ListItemAllFields/ClientSideApplicationId`) > -1) { + if (opts.url === `https://contoso.sharepoint.com/sites/team-a/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/SitePages/page.aspx')?$expand=ListItemAllFields/ClientSideApplicationId`) { return { ListItemAllFields: { CommentsDisabled: false, @@ -934,7 +1380,7 @@ describe(commands.PAGE_TEXT_ADD, () => { it('correctly handles error when parsing modern page contents', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { - if ((opts.url as string).indexOf(`/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/SitePages/page.aspx')?$expand=ListItemAllFields/ClientSideApplicationId`) > -1) { + if (opts.url === `https://contoso.sharepoint.com/sites/team-a/_api/web/GetFileByServerRelativePath(DecodedUrl='/sites/team-a/SitePages/page.aspx')?$expand=ListItemAllFields/ClientSideApplicationId`) { return { ListItemAllFields: { CommentsDisabled: false, diff --git a/src/m365/spo/commands/page/page-text-add.ts b/src/m365/spo/commands/page/page-text-add.ts index ba89b6e0133..a7b04e800c5 100644 --- a/src/m365/spo/commands/page/page-text-add.ts +++ b/src/m365/spo/commands/page/page-text-add.ts @@ -7,6 +7,7 @@ import { validation } from '../../../../utils/validation.js'; import SpoCommand from '../../../base/SpoCommand.js'; import commands from '../../commands.js'; import { + CanvasSection, ClientSidePage, ClientSideText } from './clientsidepages.js'; @@ -118,6 +119,13 @@ class SpoPageTextAddCommand extends SpoCommand { const section: number = (args.options.section || 1) - 1; const column: number = (args.options.column || 1) - 1; + // Add a new section when page does not contain any sections + if (page.sections.length < 1) { + const newSection = new CanvasSection(page, 1); + newSection.defaultColumn; + page.sections.push(newSection); + } + // Make sure the section is in range if (section >= page.sections.length) { throw new Error(`Invalid section '${section + 1}'`);