Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes: Allow saving SP page with an existing collapsible section. Closes #6462 #6540

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,20 @@
// 'm365 spo site get --url /', you'd use:
// "args": ["spo", "site", "get", "--url", "/"]
// after debugging, revert changes so that they won't end up in your PR
"args": [],
"args": [
"spo",
"page",
"text",
"add",
"--webUrl",
"https://contoso.sharepoint.com/sites/audienceTest",
"--pageName",
"Test5.aspx",
"--text",
"Divider",
"--section",
"1"
],
"console": "integratedTerminal",
"env": {
"NODE_OPTIONS": "--enable-source-maps"
Expand Down
132 changes: 126 additions & 6 deletions src/m365/spo/commands/page/clientsidepages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -324,9 +325,17 @@ export class ClientSidePage {
html.push(this.sections[i].toHtml());
}

html.push("</div>");
if (this.backgroundSettings) {
html.push(this.backgroundSettings.toHtml());
}

return html.join("");
if (this.pageSettings) {
html.push(this.pageSettings.toHtml());
}

html.push("</div>");
const result = html.join("");
return result;
}

/**
Expand All @@ -344,20 +353,27 @@ export class ClientSidePage {
getBoundedDivMarkup(html, /<div\b[^>]*data-sp-canvascontrol[^>]*?>/i, markup => {

// get the control type
const ct = /controlType&quot;&#58;(\d*?),/i.exec(markup);
const ct = /controlType&quot;&#58;(\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(<CanvasColumn>control);
break;
case 0:
// page settings
control = new PageSettings();
control.fromHtml(markup);
page.pageSettings = <PageSettings>control;
// page.mergeColumnToTree(<CanvasColumn>control);
break;
case 3:
// client side webpart
control = new ClientSideWebpart("");
Expand All @@ -370,6 +386,12 @@ export class ClientSidePage {
control.fromHtml(markup);
page.mergePartToTree(<ClientSidePart>control);
break;
case 14:
// BackgroundSection
control = new BackgroundSettings();
control.fromHtml(markup);
page.backgroundSettings = <BackgroundSettings>control;
break;
}
});

Expand Down Expand Up @@ -544,6 +566,25 @@ 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 `<div data-sp-canvascontrol="" data-sp-canvasdataversion="${this.dataVersion}" data-sp-controldata="${this.jsonData}"></div>`;
}

public fromHtml(html: string): void {
super.fromHtml(html);

}
}

export class CanvasColumn extends CanvasControl {

constructor(
Expand Down Expand Up @@ -615,6 +656,7 @@ export class CanvasColumn extends CanvasControl {
sectionIndex: this.order,
zoneIndex: this.section ? this.section.order : 0
},
zoneGroupMetadata: this.controlData?.zoneGroupMetadata || this.column?.controlData?.zoneGroupMetadata,
};
}

Expand Down Expand Up @@ -647,6 +689,73 @@ export abstract class ClientSidePart extends CanvasControl {
}
}

export class BackgroundSettings extends ClientSidePart {
public propertieJson: TypedHash<any> = {};
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(`<div data-sp-canvascontrol="" data-sp-canvasdataversion="${this.dataVersion}" data-sp-controldata="${this.jsonData}">`);

html.push(`<div data-sp-webpart="" data-sp-webpartdataversion="${this.dataVersion}" data-sp-webpartdata="${ClientSidePage.jsonToEscapedString(data)}">`);

html.push(`<div data-sp-componentid="">`);
html.push("</div>");

html.push(`<div data-sp-htmlproperties="">`);
html.push("</div>");

html.push("</div>");
html.push("</div>");

return html.join("");
}

private setProperties<T = any>(properties: T): this {
this.propertieJson = extend(this.propertieJson, properties);
return this;
}

public fromHtml(html: string): void {
super.fromHtml(html);
const webPartData = ClientSidePage.escapedStringToJson<ClientSideWebpartData>(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 = '';
Expand Down Expand Up @@ -685,6 +794,7 @@ export class ClientSideText extends ClientSidePart {
sectionIndex: this.column ? this.column.order : 0,
zoneIndex: this.column && this.column.section ? this.column.section.order : 0
},
zoneGroupMetadata: this.controlData?.zoneGroupMetadata || this.column?.controlData?.zoneGroupMetadata,
};
}

Expand Down Expand Up @@ -837,6 +947,7 @@ export class ClientSideWebpart extends ClientSidePart {
zoneIndex: this.column && this.column.section ? this.column.section.order : 0
},
webPartId: this.webPartId,
zoneGroupMetadata: this.controlData?.zoneGroupMetadata || this.column?.controlData?.zoneGroupMetadata,
};

}
Expand Down Expand Up @@ -990,13 +1101,22 @@ interface ClientSideControlPosition {
zoneIndex: number;
}

interface ZoneGroupMetadata {
type: number;
isExpanded: boolean;
showDividerLine: boolean;
iconAlignment: string;
displayName?: string;
}

interface ClientSideControlData {
controlType?: number;
id?: string;
editorType?: string;
position: ClientSideControlPosition;
webPartId?: string;
displayMode?: number;
zoneGroupMetadata?: ZoneGroupMetadata;
}

interface ClientSideWebpartData {
Expand Down
36 changes: 36 additions & 0 deletions src/m365/spo/commands/page/page-clientsidewebpart-add.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2605,6 +2605,42 @@ describe(commands.PAGE_CLIENTSIDEWEBPART_ADD, () => {
}));
});

it('adds web part to an empty 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": `[{"id":"emptySection","position":{"controlIndex":1,"sectionFactor":12,"sectionIndex":1,"zoneIndex":1},"zoneGroupMetadata":{"type":1,"isExpanded":true,"showDividerLine":true,"iconAlignment":"left","displayName":"Test1"},"addedFromPersistedData":true},{"controlType":0,"pageSettingsSlice":{"isDefaultDescription":true,"isDefaultThumbnail":true,"isSpellCheckEnabled":true,"globalRichTextStylingVersion":0,"rtePageSettings":{"contentVersion":4,"indentationVersion":1},"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},\\"webPartId\\":\\"e377ea37-9047-43b9-8cdb-a761be2f8e09\\",\\"emphasis\\":{},\\"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\\"},\\"zoneGroupMetadata\\":{\\"type\\":1,\\"isExpanded\\":true,\\"showDividerLine\\":true,\\"iconAlignment\\":\\"left\\",\\"displayName\\":\\"Test1\\"}},{\\"controlType\\":0,\\"pageSettingsSlice\\":{\\"isDefaultDescription\\":true,\\"isDefaultThumbnail\\":true,\\"isSpellCheckEnabled\\":true,\\"globalRichTextStylingVersion\\":0,\\"rtePageSettings\\":{\\"contentVersion\\":4,\\"indentationVersion\\":1},\\"isEmailReady\\":false,\\"webPartsPageSettings\\":{\\"isTitleHeadingLevelsEnabled\\":false}}}]"}');
});

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) {
Expand Down
3 changes: 2 additions & 1 deletion src/m365/spo/commands/page/page-clientsidewebpart-add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading
Loading