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

New command: spo page publish. #6436

Closed
wants to merge 1 commit into from
Closed
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
41 changes: 41 additions & 0 deletions docs/docs/cmd/spo/page/page-publish.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import Global from '/docs/cmd/_global.mdx';

# spo page publish

Publishes a modern page

## Usage

```sh
m365 spo page publish [options]
```

## Options

```md definition-list
`-u, --webUrl <webUrl>`
: URL of the site where the page is located.

`-n, --name <name>`
: Name of the page.
```

<Global />

## Examples

Publish a modern page

```sh
m365 spo page publish --webUrl https://contoso.sharepoint.com/sites/Marketing --name "Style guide.aspx"
```

Publish a modern page in a subfolder

```sh
m365 spo page publish --webUrl https://contoso.sharepoint.com/sites/Marketing --name "/Styles/Guide.aspx"
```

## Response

The command won't return a response on success.
5 changes: 5 additions & 0 deletions docs/src/config/sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3157,6 +3157,11 @@ const sidebars: SidebarsConfig = {
label: 'page list',
id: 'cmd/spo/page/page-list'
},
{
type: 'doc',
label: 'page publish',
id: 'cmd/spo/page/page-publish'
},
{
type: 'doc',
label: 'page remove',
Expand Down
1 change: 1 addition & 0 deletions src/m365/spo/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ export default {
PAGE_COPY: `${prefix} page copy`,
PAGE_GET: `${prefix} page get`,
PAGE_LIST: `${prefix} page list`,
PAGE_PUBLISH: `${prefix} page publish`,
PAGE_REMOVE: `${prefix} page remove`,
PAGE_SET: `${prefix} page set`,
PAGE_CLIENTSIDEWEBPART_ADD: `${prefix} page clientsidewebpart add`,
Expand Down
178 changes: 178 additions & 0 deletions src/m365/spo/commands/page/page-publish.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import assert from 'assert';
import sinon from 'sinon';
import { z } from 'zod';
import auth from '../../../../Auth.js';
import { cli } from '../../../../cli/cli.js';
import { CommandInfo } from '../../../../cli/CommandInfo.js';
import { Logger } from '../../../../cli/Logger.js';
import { CommandError } from '../../../../Command.js';
import request from '../../../../request.js';
import { telemetry } from '../../../../telemetry.js';
import { pid } from '../../../../utils/pid.js';
import { session } from '../../../../utils/session.js';
import { sinonUtil } from '../../../../utils/sinonUtil.js';
import commands from '../../commands.js';
import command from './page-publish.js';
import { urlUtil } from '../../../../utils/urlUtil.js';
import { formatting } from '../../../../utils/formatting.js';

describe(commands.PAGE_PUBLISH, () => {
let log: string[];
let logger: Logger;
let loggerLogSpy: sinon.SinonSpy;
let commandInfo: CommandInfo;
let postStub: sinon.SinonStub;
let commandOptionsSchema: z.ZodTypeAny;

const webUrl = 'https://contoso.sharepoint.com/sites/Marketing';
const serverRelativeUrl = urlUtil.getServerRelativeSiteUrl(webUrl);
const pageName = 'HR.aspx';

before(() => {
sinon.stub(auth, 'restoreAuth').resolves();
sinon.stub(telemetry, 'trackEvent').returns();
sinon.stub(pid, 'getProcessName').returns('');
sinon.stub(session, 'getId').returns('');
auth.connection.active = true;
commandInfo = cli.getCommandInfo(command);
commandOptionsSchema = commandInfo.command.getSchemaToParse()!;
});

beforeEach(() => {
log = [];
logger = {
log: async (msg: string) => {
log.push(msg);
},
logRaw: async (msg: string) => {
log.push(msg);
},
logToStderr: async (msg: string) => {
log.push(msg);
}
};
loggerLogSpy = sinon.spy(logger, 'log');

const serverRelativePageUrl = `${serverRelativeUrl}/SitePages/${pageName}`;
postStub = sinon.stub(request, 'post').callsFake(async (opts) => {
if (opts.url === `${webUrl}/_api/web/GetFileByServerRelativePath(DecodedUrl='${formatting.encodeQueryParameter(serverRelativePageUrl)}')/Publish()`) {
return;
}

throw 'Invalid request: ' + opts.url;
});
});

afterEach(() => {
sinonUtil.restore([
request.post
]);
});

after(() => {
sinon.restore();
auth.connection.active = false;
});

it('has correct name', () => {
assert.strictEqual(command.name, commands.PAGE_PUBLISH);
});

it('has a description', () => {
assert.notStrictEqual(command.description, null);
});

it('logs no command output', async () => {
await command.action(logger,
{
options: {
webUrl: webUrl,
name: pageName
}
});

assert(loggerLogSpy.notCalled);
});

it('correctly publishes page', async () => {
await command.action(logger,
{
options: {
webUrl: webUrl,
name: pageName
}
});

assert(postStub.calledOnce);
});

it('correctly publishes a page when extension is not specified', async () => {
await command.action(logger,
{
options: {
webUrl: webUrl,
name: pageName.substring(0, pageName.lastIndexOf('.')),
verbose: true
}
});

assert(postStub.calledOnce);
});

it('correctly publishes a nested page', async () => {
const pageUrl = '/folder1/folder2/' + pageName;
postStub.restore();

postStub = sinon.stub(request, 'post').callsFake(async (opts) => {
if (opts.url === `${webUrl}/_api/web/GetFileByServerRelativePath(DecodedUrl='${formatting.encodeQueryParameter(serverRelativeUrl + '/SitePages' + pageUrl)}')/Publish()`) {
return;
}

throw 'Invalid request: ' + opts.url;
});

await command.action(logger,
{
options: {
webUrl: webUrl,
name: pageUrl
}
});

assert(postStub.calledOnce);
});

it('correctly handles API error', async () => {
postStub.restore();
const errorMessage = 'The file /sites/Marketing/SitePages/My-new-page.aspx does not exist.';

sinon.stub(request, 'post').rejects({
error: {
'odata.error': {
message: {
lang: 'en-US',
value: errorMessage
}
}
}
});

await assert.rejects(command.action(logger, { options: { webUrl: webUrl, name: pageName } }),
new CommandError(errorMessage));
});

it('fails validation if webUrl is not a valid SharePoint URL', async () => {
const actual = commandOptionsSchema.safeParse({ webUrl: 'foo' });
assert.strictEqual(actual.success, false);
});

it('passes validation when the webUrl is a valid SharePoint URL and name is specified', async () => {
const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, name: pageName });
assert.strictEqual(actual.success, true);
});

it('passes validation when name has no extension', async () => {
const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, name: 'page' });
assert.strictEqual(actual.success, true);
});
});
69 changes: 69 additions & 0 deletions src/m365/spo/commands/page/page-publish.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { z } from 'zod';
import { zod } from '../../../../utils/zod.js';
import { Logger } from '../../../../cli/Logger.js';
import { globalOptionsZod } from '../../../../Command.js';
import request, { CliRequestOptions } from '../../../../request.js';
import { formatting } from '../../../../utils/formatting.js';
import { urlUtil } from '../../../../utils/urlUtil.js';
import { validation } from '../../../../utils/validation.js';
import SpoCommand from '../../../base/SpoCommand.js';
import commands from '../../commands.js';

const options = globalOptionsZod
.extend({
webUrl: zod.alias('u', z.string()
.refine(url => validation.isValidSharePointUrl(url) === true, url => ({
message: `'${url}' is not a valid SharePoint Online site URL.`
}))
),
name: zod.alias('n', z.string())
})
.strict();
declare type Options = z.infer<typeof options>;

interface CommandArgs {
options: Options;
}

class SpoPagePublishCommand extends SpoCommand {
public get name(): string {
return commands.PAGE_PUBLISH;
}

public get description(): string {
return 'Publishes a modern page';
}

public get schema(): z.ZodTypeAny {
return options;
}

public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
try {
// Remove leading slashes from the page name (page can be nested in folders)
let pageName: string = urlUtil.removeLeadingSlashes(args.options.name);
if (!pageName.toLowerCase().endsWith('.aspx')) {
pageName += '.aspx';
}

if (this.verbose) {
await logger.logToStderr(`Publishing page ${pageName}...`);
}

const filePath = `${urlUtil.getServerRelativeSiteUrl(args.options.webUrl)}/SitePages/${pageName}`;
const requestOptions: CliRequestOptions = {
url: `${args.options.webUrl}/_api/web/GetFileByServerRelativePath(DecodedUrl='${formatting.encodeQueryParameter(filePath)}')/Publish()`,
headers: {
accept: 'application/json;odata=nometadata'
}
};

await request.post(requestOptions);
}
catch (err: any) {
this.handleRejectedODataJsonPromise(err);
}
}
}

export default new SpoPagePublishCommand();