From b659d7b47fea245d8570dcd51b2a4993832b46dc Mon Sep 17 00:00:00 2001 From: Shantha Kumar T Date: Mon, 23 Sep 2024 06:27:32 +0530 Subject: [PATCH] Added internalName option for spo field commands --- docs/docs/cmd/spo/field/field-get.mdx | 14 +- docs/docs/cmd/spo/field/field-remove.mdx | 15 +- docs/docs/cmd/spo/field/field-set.mdx | 20 +- src/m365/spo/commands/field/field-get.spec.ts | 45 +++ src/m365/spo/commands/field/field-get.ts | 12 +- .../spo/commands/field/field-remove.spec.ts | 59 +++- src/m365/spo/commands/field/field-remove.ts | 12 +- src/m365/spo/commands/field/field-set.spec.ts | 260 +++++++++++++++++- src/m365/spo/commands/field/field-set.ts | 10 +- 9 files changed, 430 insertions(+), 17 deletions(-) diff --git a/docs/docs/cmd/spo/field/field-get.mdx b/docs/docs/cmd/spo/field/field-get.mdx index 130e7fc1206..fa77496a0a6 100644 --- a/docs/docs/cmd/spo/field/field-get.mdx +++ b/docs/docs/cmd/spo/field/field-get.mdx @@ -28,10 +28,14 @@ m365 spo field get [options] : Server- or web-relative URL of the list where the field is located. Specify either `listTitle`, `listId` or `listUrl`. `-i, --id [id]` -: The ID of the field to retrieve. Specify `id` or `title` but not both. +: The ID of the field to retrieve. Specify either `id`, `title` or `internalName`. `-t, --title [title]` -: The display name (case-sensitive) of the field to retrieve. Specify `id` or `title` but not both. +: The display name (case-sensitive) of the field to retrieve. Specify either `id`, `title` or `internalName`. +``` + +`--internalName [internalName]` +: The internal name (case-sensitive) of the field to retrieve. Specify either `id`, `title` or `internalName`. ``` @@ -56,6 +60,12 @@ Retrieves list column by display name located in the specified site. Retrieves t m365 spo field get --webUrl https://contoso.sharepoint.com/sites/contoso-sales --listUrl "Lists/Events" --title "Title" ``` +Retrieves list column by internal name located in the specified site. Retrieves the list by its url. + +```sh +m365 spo field get --webUrl https://contoso.sharepoint.com/sites/contoso-sales --listUrl "Lists/Events" --internalName "Title" +``` + ## Response diff --git a/docs/docs/cmd/spo/field/field-remove.mdx b/docs/docs/cmd/spo/field/field-remove.mdx index f1e3b39fbd9..193c8ab6fa2 100644 --- a/docs/docs/cmd/spo/field/field-remove.mdx +++ b/docs/docs/cmd/spo/field/field-remove.mdx @@ -26,13 +26,16 @@ m365 spo field remove [options] : Server- or web-relative URL of the list where the field is located. Specify either `listTitle`, `listId` or `listUrl`. `-i, --id [id]` -: The ID of the field to remove. Specify either `id`, `title`, or `group`. +: The ID of the field to remove. Specify either `id`, `title`, `internalName`, or `group`. `-t, --title [title]` -: The display name (case-sensitive) of the field to remove. Specify either `id`, `title`, or `group`. +: The display name (case-sensitive) of the field to remove. Specify either `id`, `title`, `internalName`, or `group`. + +`--internalName [internalName]` +: The internal name (case-sensitive) of the field to remove. Specify either `id`, `title`, `internalName`, or `group`. `-g, --group [group]` -: Delete all fields from this group (case-sensitive). Specify either `id`, `title`, or `group`. +: Delete all fields from this group (case-sensitive). Specify either `id`, `title`, `internalName`, or `group`. `-f, --force` : Don't prompt for confirming removing the field. @@ -60,6 +63,12 @@ Remove the list column with the specified display name, located in the specified m365 spo field remove --webUrl https://contoso.sharepoint.com/sites/contoso-sales --listUrl "Lists/Events" --title "Title" ``` +Remove the list column with the specified display name, located in the specified site. Retrieves the list by its url. + +```sh +m365 spo field remove --webUrl https://contoso.sharepoint.com/sites/contoso-sales --listUrl "Lists/Events" --internalName "Title" +``` + Remove all site columns from the specified group. ```sh diff --git a/docs/docs/cmd/spo/field/field-set.mdx b/docs/docs/cmd/spo/field/field-set.mdx index 14d532bfb64..cb449bc81a0 100644 --- a/docs/docs/cmd/spo/field/field-set.mdx +++ b/docs/docs/cmd/spo/field/field-set.mdx @@ -26,10 +26,13 @@ m365 spo field set [options] : Server- or site-relative URL of the list where the field is located (if list column). Specify either `listTitle`, `listId` or `listUrl`. `-i, --id [id]` -: ID of the field to update. Specify either `id` or `title` but not both. +: ID of the field to update. Specify either `id`, `title`, or 'internalName' but not all. `-t, --title [title]` -: Title or internal name of the field to update. Specify either `id` or `title` but not both. +: Title of the field to update. Specify either `id`, `title`, or 'internalName' but not all. + +`--internalName [internalName]` +: Internal name of the field to update. Specify either `id`, `title`, or 'internalName' but not all. `--updateExistingLists` : Set, to push the update to existing lists. Otherwise, the changes will apply to new lists only. @@ -51,6 +54,12 @@ When updating column formatting for a field with the `--CustomFormatter` option, Update the title of the site column specified by its internal name and push changes to existing lists. +```sh +m365 spo field set --webUrl https://contoso.sharepoint.com/sites/project-x --internalName 'MyColumn' --updateExistingLists --Title 'My column' +``` + +Update the title of the site column specified by its title and push changes to existing lists. + ```sh m365 spo field set --webUrl https://contoso.sharepoint.com/sites/project-x --title 'MyColumn' --updateExistingLists --Title 'My column' ``` @@ -67,11 +76,16 @@ Update the description of a column specified by the ID on a list retrieved by th m365 spo field set --webUrl https://contoso.sharepoint.com/sites/project-x --listUrl '/sites/project-x/Lists/My List' --id 330f29c5-5c4c-465f-9f4b-7903020ae1ce --Description 'My column Description' ``` -Update column formatting of the specified list column. +Update column formatting of the specified list column based on title. ```sh m365 spo field set --webUrl https://contoso.sharepoint.com/sites/project-x --listTitle 'My List' --title 'MyColumn' --CustomFormatter '{"schema":"https://developer.microsoft.com/json-schemas/sp/column-formatting.schema.json", "elmType": "div", "txtContent": "@currentField"}' ``` +Update column formatting of the specified list column based on internalName. + +```sh +m365 spo field set --webUrl https://contoso.sharepoint.com/sites/project-x --listTitle 'My List' --internalName 'MyColumn' --CustomFormatter '{"schema":"https://developer.microsoft.com/json-schemas/sp/column-formatting.schema.json", "elmType": "div", "txtContent": "@currentField"}' +``` ## Response diff --git a/src/m365/spo/commands/field/field-get.spec.ts b/src/m365/spo/commands/field/field-get.spec.ts index 5f7fbc806d8..6226a37f65f 100644 --- a/src/m365/spo/commands/field/field-get.spec.ts +++ b/src/m365/spo/commands/field/field-get.spec.ts @@ -320,6 +320,51 @@ describe(commands.FIELD_GET, () => { assert.strictEqual(getStub.lastCall.args[0].url, 'https://contoso.sharepoint.com/sites/portal/_api/web/lists(guid\'03e45e84-1992-4d42-9116-26f756012634\')/fields/getbyinternalnameortitle(\'Title\')'); }); + it('should call the correct GET url when field internalName and list title specified (verbose)', async () => { + const getStub = sinon.stub(request, 'get').callsFake(async (opts) => { + if ((opts.url as string).indexOf(`/_api/web/lists`) > -1) { + return { + "Id": "03e45e84-1992-4d42-9116-26f756012634" + }; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { debug: true, verbose: true, webUrl: 'https://contoso.sharepoint.com/sites/portal', internalName: 'Title', listTitle: 'Documents' } }); + assert.strictEqual(getStub.lastCall.args[0].url, 'https://contoso.sharepoint.com/sites/portal/_api/web/lists/getByTitle(\'Documents\')/fields/getbyinternalnameortitle(\'Title\')'); + }); + + it('should call the correct GET url when field internalName and list title specified', async () => { + const getStub = sinon.stub(request, 'get').callsFake(async (opts) => { + if ((opts.url as string).indexOf(`/_api/web/lists`) > -1) { + return { + "Id": "03e45e84-1992-4d42-9116-26f756012634" + }; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { webUrl: 'https://contoso.sharepoint.com/sites/portal', internalName: 'Title', listTitle: 'Documents' } }); + assert.strictEqual(getStub.lastCall.args[0].url, 'https://contoso.sharepoint.com/sites/portal/_api/web/lists/getByTitle(\'Documents\')/fields/getbyinternalnameortitle(\'Title\')'); + }); + + it('should call the correct GET url when field internalName and list url specified', async () => { + const getStub = sinon.stub(request, 'get').callsFake(async (opts) => { + if ((opts.url as string).indexOf(`/_api/web/lists`) > -1) { + return { + "Id": "03e45e84-1992-4d42-9116-26f756012634" + }; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { debug: true, webUrl: 'https://contoso.sharepoint.com/sites/portal', internalName: 'Title', listId: '03e45e84-1992-4d42-9116-26f756012634' } }); + assert.strictEqual(getStub.lastCall.args[0].url, 'https://contoso.sharepoint.com/sites/portal/_api/web/lists(guid\'03e45e84-1992-4d42-9116-26f756012634\')/fields/getbyinternalnameortitle(\'Title\')'); + }); + it('correctly handles site column not found', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { if ((opts.url as string).indexOf(`/_api/web/fields/getbyid('03e45e84-1992-4d42-9116-26f756012634')`) > -1) { diff --git a/src/m365/spo/commands/field/field-get.ts b/src/m365/spo/commands/field/field-get.ts index 9fd81b4458c..2329b642e55 100644 --- a/src/m365/spo/commands/field/field-get.ts +++ b/src/m365/spo/commands/field/field-get.ts @@ -18,6 +18,7 @@ interface Options extends GlobalOptions { listUrl?: string; id?: string; title?: string; + internalName?: string; } class SpoFieldGetCommand extends SpoCommand { @@ -45,7 +46,8 @@ class SpoFieldGetCommand extends SpoCommand { listTitle: typeof args.options.listTitle !== 'undefined', listUrl: typeof args.options.listUrl !== 'undefined', id: typeof args.options.id !== 'undefined', - title: typeof args.options.title !== 'undefined' + title: typeof args.options.title !== 'undefined', + internalName: typeof args.options.internalName !== 'undefined' }); }); } @@ -69,6 +71,9 @@ class SpoFieldGetCommand extends SpoCommand { }, { option: '-t, --title [title]' + }, + { + option: '--internalName [internalName]' } ); } @@ -95,7 +100,7 @@ class SpoFieldGetCommand extends SpoCommand { } #initOptionSets(): void { - this.optionSets.push({ options: ['id', 'title'] }); + this.optionSets.push({ options: ['id', 'title', 'internalName'] }); } public async commandAction(logger: Logger, args: CommandArgs): Promise { @@ -117,6 +122,9 @@ class SpoFieldGetCommand extends SpoCommand { if (args.options.id) { fieldRestUrl = `/getbyid('${formatting.encodeQueryParameter(args.options.id)}')`; } + else if (args.options.internalName) { + fieldRestUrl = `/getbyinternalnameortitle('${formatting.encodeQueryParameter(args.options.internalName as string)}')`; + } else { fieldRestUrl = `/getbyinternalnameortitle('${formatting.encodeQueryParameter(args.options.title as string)}')`; } diff --git a/src/m365/spo/commands/field/field-remove.spec.ts b/src/m365/spo/commands/field/field-remove.spec.ts index de7e5156484..f8eab248ed2 100644 --- a/src/m365/spo/commands/field/field-remove.spec.ts +++ b/src/m365/spo/commands/field/field-remove.spec.ts @@ -94,6 +94,18 @@ describe(commands.FIELD_REMOVE, () => { assert(promptIssued); }); + it('prompts before removing field when confirmation argument not passed (internalName)', async () => { + await command.action(logger, { options: { internalName: 'myfield1', webUrl: 'https://contoso.sharepoint.com' } }); + + assert(promptIssued); + }); + + it('prompts before removing list column when confirmation argument not passed (internalName)', async () => { + await command.action(logger, { options: { internalName: 'myfield1', webUrl: 'https://contoso.sharepoint.com', listTitle: 'My List' } }); + + assert(promptIssued); + }); + it('aborts removing field when prompt not confirmed', async () => { sinonUtil.restore(cli.promptForConfirmation); sinon.stub(cli, 'promptForConfirmation').resolves(false); @@ -454,6 +466,51 @@ describe(commands.FIELD_REMOVE, () => { assert.strictEqual(getStub.lastCall.args[0].url, 'https://contoso.sharepoint.com/sites/portal/_api/web/lists(guid\'03e45e84-1992-4d42-9116-26f756012634\')/fields/getbyinternalnameortitle(\'Title\')'); }); + it('calls the correct get url when field internalName and list title specified (verbose)', async () => { + const getStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if ((opts.url as string).indexOf(`/_api/web/lists`) > -1) { + return { + "Id": "03e45e84-1992-4d42-9116-26f756012634" + }; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { debug: true, verbose: true, webUrl: 'https://contoso.sharepoint.com/sites/portal', internalName: 'Title', listTitle: 'Documents', force: true } }); + assert.strictEqual(getStub.lastCall.args[0].url, 'https://contoso.sharepoint.com/sites/portal/_api/web/lists/getByTitle(\'Documents\')/fields/getbyinternalnameortitle(\'Title\')'); + }); + + it('calls the correct get url when field internalName and list title specified', async () => { + const getStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if ((opts.url as string).indexOf(`/_api/web/lists`) > -1) { + return { + "Id": "03e45e84-1992-4d42-9116-26f756012634" + }; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { webUrl: 'https://contoso.sharepoint.com/sites/portal', internalName: 'Title', listTitle: 'Documents', force: true } }); + assert.strictEqual(getStub.lastCall.args[0].url, 'https://contoso.sharepoint.com/sites/portal/_api/web/lists/getByTitle(\'Documents\')/fields/getbyinternalnameortitle(\'Title\')'); + }); + + it('calls the correct get url when field internalName and list url specified', async () => { + const getStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if ((opts.url as string).indexOf(`/_api/web/lists`) > -1) { + return { + "Id": "03e45e84-1992-4d42-9116-26f756012634" + }; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { debug: true, webUrl: 'https://contoso.sharepoint.com/sites/portal', internalName: 'Title', listId: '03e45e84-1992-4d42-9116-26f756012634', force: true } }); + assert.strictEqual(getStub.lastCall.args[0].url, 'https://contoso.sharepoint.com/sites/portal/_api/web/lists(guid\'03e45e84-1992-4d42-9116-26f756012634\')/fields/getbyinternalnameortitle(\'Title\')'); + }); + it('correctly handles site column not found', async () => { const error = { error: { @@ -534,7 +591,7 @@ describe(commands.FIELD_REMOVE, () => { assert(containsTypeOption); }); - it('fails validation if both id and title options are not passed', async () => { + it('fails validation if either of id, title and internalName options are not passed', async () => { sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { if (settingName === settingsNames.prompt) { return false; diff --git a/src/m365/spo/commands/field/field-remove.ts b/src/m365/spo/commands/field/field-remove.ts index fb8efabb529..1fb8a700d2d 100644 --- a/src/m365/spo/commands/field/field-remove.ts +++ b/src/m365/spo/commands/field/field-remove.ts @@ -19,6 +19,7 @@ interface Options extends GlobalOptions { group?: string; listTitle?: string; title?: string; + internalName?: string; listUrl?: string; webUrl: string; } @@ -50,6 +51,7 @@ class SpoFieldRemoveCommand extends SpoCommand { id: typeof args.options.id !== 'undefined', group: typeof args.options.group !== 'undefined', title: typeof args.options.title !== 'undefined', + internalName: typeof args.options.internalName !== 'undefined', force: (!(!args.options.force)).toString() }); }); @@ -75,6 +77,9 @@ class SpoFieldRemoveCommand extends SpoCommand { { option: '-t, --title [title]' }, + { + option: '--internalName [internalName]' + }, { option: '-g, --group [group]' }, @@ -106,7 +111,7 @@ class SpoFieldRemoveCommand extends SpoCommand { } #initOptionSets(): void { - this.optionSets.push({ options: ['id', 'title', 'group'] }); + this.optionSets.push({ options: ['id', 'title', 'internalName', 'group'] }); } public async commandAction(logger: Logger, args: CommandArgs): Promise { @@ -191,7 +196,8 @@ class SpoFieldRemoveCommand extends SpoCommand { } else { try { - await removeField(listRestUrl, args.options.id, args.options.title); + const columnName: string | undefined = args.options.title ? args.options.title : args.options.internalName; + await removeField(listRestUrl, args.options.id, columnName); // REST post call doesn't return anything } catch (err: any) { @@ -204,7 +210,7 @@ class SpoFieldRemoveCommand extends SpoCommand { await prepareRemoval(); } else { - const confirmMessage: string = `Are you sure you want to remove the ${args.options.group ? 'fields' : 'field'} ${args.options.id || args.options.title || 'from group ' + args.options.group} ${messageEnd}?`; + const confirmMessage: string = `Are you sure you want to remove the ${args.options.group ? 'fields' : 'field'} ${args.options.id || args.options.title || args.options.internalName || 'from group ' + args.options.group} ${messageEnd}?`; const result = await cli.promptForConfirmation({ message: confirmMessage }); diff --git a/src/m365/spo/commands/field/field-set.spec.ts b/src/m365/spo/commands/field/field-set.spec.ts index adeb18a8efd..95ee86908da 100644 --- a/src/m365/spo/commands/field/field-set.spec.ts +++ b/src/m365/spo/commands/field/field-set.spec.ts @@ -112,6 +112,44 @@ describe(commands.FIELD_SET, () => { assert(loggerLogSpy.notCalled); }); + it('updates site column specified by internalName', async () => { + sinon.stub(request, 'post').callsFake(async (opts) => { + if ((opts.url as string).indexOf(`/_vti_bin/client.svc/ProcessQuery`) > -1) { + if (opts.headers && + opts.headers['X-RequestDigest'] !== 'ABC') { + throw 'Invalid request'; + } + + if (opts.data === `MyColumn`) { + return JSON.stringify([{ + "SchemaVersion": "15.0.0.0", + "LibraryVersion": "16.0.8231.1213", + "ErrorInfo": null, + "TraceCorrelationId": "7c0aa19e-1058-0000-37ae-14170affbedb" + }, 664, { + "IsNull": false + }, 665, { + "_ObjectType_": "SP.FieldText", + "_ObjectIdentity_": "7c0aa19e-1058-0000-37ae-14170affbedb|740c6a0b-85e2-48a0-a494-e0f1759d4aa7:site:ff7a8065-9120-4c0a-982a-163ab9014179:web:e781d3dc-238d-44f7-8724-5e3e9eabcd6e:field:5d021339-4d62-4fe9-9d2a-c99bc56a157a" + }]); + } + + if (opts.data === `My columnfalse`) { + return JSON.stringify([ + { + "SchemaVersion": "15.0.0.0", "LibraryVersion": "16.0.8231.1213", "ErrorInfo": null, "TraceCorrelationId": "b909a19e-5020-0000-37ae-17f800b4ea4c" + } + ]); + } + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { webUrl: 'https://contoso.sharepoint.com', internalName: 'MyColumn', Description: 'My column' } }); + assert(loggerLogSpy.notCalled); + }); + it('updates site column specified by id, pushing the changes to existing lists', async () => { sinon.stub(request, 'post').callsFake(async (opts) => { if ((opts.url as string).indexOf(`/_vti_bin/client.svc/ProcessQuery`) > -1) { @@ -254,6 +292,58 @@ describe(commands.FIELD_SET, () => { await command.action(logger, { options: { debug: true, webUrl: 'https://contoso.sharepoint.com', listTitle: 'My List', title: 'MyColumn', Description: 'My column' } }); }); + it('updates list column specified by internalName, list specified by title', async () => { + sinon.stub(request, 'post').callsFake(async (opts) => { + if ((opts.url as string).indexOf(`/_vti_bin/client.svc/ProcessQuery`) > -1) { + if (opts.headers && + opts.headers['X-RequestDigest'] !== 'ABC') { + throw 'Invalid request'; + } + + if (opts.data === `My List`) { + return JSON.stringify([{ + "SchemaVersion": "15.0.0.0", + "LibraryVersion": "16.0.8231.1213", + "ErrorInfo": null, + "TraceCorrelationId": "270fa19e-f0f7-0000-37ae-1733ad1b6703" + }, 664, { + "IsNull": false + }, 665, { + "_ObjectType_": "SP.List", + "_ObjectIdentity_": "270fa19e-f0f7-0000-37ae-1733ad1b6703|740c6a0b-85e2-48a0-a494-e0f1759d4aa7:site:ff7a8065-9120-4c0a-982a-163ab9014179:web:e781d3dc-238d-44f7-8724-5e3e9eabcd6e:list:03cef05c-ba50-4dcf-a876-304f0626085c", + "_ObjectVersion_": "6" + }]); + } + + if (opts.data === `MyColumn`) { + return JSON.stringify([{ + "SchemaVersion": "15.0.0.0", + "LibraryVersion": "16.0.8231.1213", + "ErrorInfo": null, + "TraceCorrelationId": "7c0aa19e-1058-0000-37ae-14170affbedb" + }, 664, { + "IsNull": false + }, 665, { + "_ObjectType_": "SP.FieldText", + "_ObjectIdentity_": "fe0ea19e-7022-0000-37ae-1357e77e046c|740c6a0b-85e2-48a0-a494-e0f1759d4aa7:site:ff7a8065-9120-4c0a-982a-163ab9014179:web:e781d3dc-238d-44f7-8724-5e3e9eabcd6e:list:03cef05c-ba50-4dcf-a876-304f0626085c:field:5d021339-4d62-4fe9-9d2a-c99bc56a157a" + }]); + } + + if (opts.data === `My columnfalse`) { + return JSON.stringify([ + { + "SchemaVersion": "15.0.0.0", "LibraryVersion": "16.0.8231.1213", "ErrorInfo": null, "TraceCorrelationId": "b909a19e-5020-0000-37ae-17f800b4ea4c" + } + ]); + } + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { debug: true, webUrl: 'https://contoso.sharepoint.com', listTitle: 'My List', internalName: 'MyColumn', Description: 'My column' } }); + }); + it('updates list column specified by name, list specified by url', async () => { const webUrl = 'https://contoso.sharepoint.com'; const listUrl = '/lists/TestList'; @@ -309,6 +399,61 @@ describe(commands.FIELD_SET, () => { await command.action(logger, { options: { debug: true, webUrl: webUrl, listUrl: listUrl, title: 'MyColumn', Description: 'My column Description' } }); }); + it('updates list column specified by internalName, list specified by url', async () => { + const webUrl = 'https://contoso.sharepoint.com'; + const listUrl = '/lists/TestList'; + const listServerRelativeUrl: string = urlUtil.getServerRelativePath(webUrl, listUrl); + sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === 'https://contoso.sharepoint.com/_vti_bin/client.svc/ProcessQuery') { + if (opts.headers && + opts.headers['X-RequestDigest'] !== 'ABC') { + throw 'Invalid request'; + } + + if (opts.data === `${listServerRelativeUrl}`) { + return JSON.stringify([{ + "SchemaVersion": "15.0.0.0", + "LibraryVersion": "16.0.8231.1213", + "ErrorInfo": null, + "TraceCorrelationId": "270fa19e-f0f7-0000-37ae-1733ad1b6703" + }, 664, { + "IsNull": false + }, 665, { + "_ObjectType_": "SP.List", + "_ObjectIdentity_": "270fa19e-f0f7-0000-37ae-1733ad1b6703|740c6a0b-85e2-48a0-a494-e0f1759d4aa7:site:ff7a8065-9120-4c0a-982a-163ab9014179:web:e781d3dc-238d-44f7-8724-5e3e9eabcd6e:list:03cef05c-ba50-4dcf-a876-304f0626085c", + "_ObjectVersion_": "6" + }]); + } + + if (opts.data === `MyColumn`) { + return JSON.stringify([{ + "SchemaVersion": "15.0.0.0", + "LibraryVersion": "16.0.8231.1213", + "ErrorInfo": null, + "TraceCorrelationId": "7c0aa19e-1058-0000-37ae-14170affbedb" + }, 664, { + "IsNull": false + }, 665, { + "_ObjectType_": "SP.FieldText", + "_ObjectIdentity_": "fe0ea19e-7022-0000-37ae-1357e77e046c|740c6a0b-85e2-48a0-a494-e0f1759d4aa7:site:ff7a8065-9120-4c0a-982a-163ab9014179:web:e781d3dc-238d-44f7-8724-5e3e9eabcd6e:list:03cef05c-ba50-4dcf-a876-304f0626085c:field:5d021339-4d62-4fe9-9d2a-c99bc56a157a" + }]); + } + + if (opts.data === `My column Descriptionfalse`) { + return JSON.stringify([ + { + "SchemaVersion": "15.0.0.0", "LibraryVersion": "16.0.8231.1213", "ErrorInfo": null, "TraceCorrelationId": "b909a19e-5020-0000-37ae-17f800b4ea4c" + } + ]); + } + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { debug: true, webUrl: webUrl, listUrl: listUrl, internalName: 'MyColumn', Description: 'My column Description' } }); + }); + it('correctly escapes XML in list title', async () => { sinon.stub(request, 'post').callsFake(async (opts) => { if ((opts.url as string).indexOf(`/_vti_bin/client.svc/ProcessQuery`) > -1) { @@ -452,6 +597,44 @@ describe(commands.FIELD_SET, () => { assert(loggerLogSpy.notCalled); }); + it('correctly escapes XML in field internalName', async () => { + sinon.stub(request, 'post').callsFake(async (opts) => { + if ((opts.url as string).indexOf(`/_vti_bin/client.svc/ProcessQuery`) > -1) { + if (opts.headers && + opts.headers['X-RequestDigest'] !== 'abc') { + throw 'Invalid request'; + } + + if (opts.data === `MyColumn>`) { + return JSON.stringify([{ + "SchemaVersion": "15.0.0.0", + "LibraryVersion": "16.0.8231.1213", + "ErrorInfo": null, + "TraceCorrelationId": "7c0aa19e-1058-0000-37ae-14170affbedb" + }, 664, { + "IsNull": false + }, 665, { + "_ObjectType_": "SP.FieldText", + "_ObjectIdentity_": "7c0aa19e-1058-0000-37ae-14170affbedb|740c6a0b-85e2-48a0-a494-e0f1759d4aa7:site:ff7a8065-9120-4c0a-982a-163ab9014179:web:e781d3dc-238d-44f7-8724-5e3e9eabcd6e:field:5d021339-4d62-4fe9-9d2a-c99bc56a157a" + }]); + } + + if (opts.data === `My columnfalse`) { + return JSON.stringify([ + { + "SchemaVersion": "15.0.0.0", "LibraryVersion": "16.0.8231.1213", "ErrorInfo": null, "TraceCorrelationId": "b909a19e-5020-0000-37ae-17f800b4ea4c" + } + ]); + } + } + + throw 'Invalid request'; + }); + + await assert.rejects(command.action(logger, { options: { webUrl: 'https://contoso.sharepoint.com', internalName: 'MyColumn>', Description: 'My column' } })); + assert(loggerLogSpy.notCalled); + }); + it('correctly escapes XML in field properties', async () => { sinon.stub(request, 'post').callsFake(async (opts) => { if ((opts.url as string).indexOf(`/_vti_bin/client.svc/ProcessQuery`) > -1) { @@ -552,6 +735,37 @@ describe(commands.FIELD_SET, () => { new CommandError(`Column 'MyColumn' does not exist. It may have been deleted by another user.`)); }); + it('correctly handles an error when the field specified by internalName doesn\'t exist', async () => { + sinon.stub(request, 'post').callsFake(async (opts) => { + if ((opts.url as string).indexOf(`/_vti_bin/client.svc/ProcessQuery`) > -1) { + if (opts.headers && + opts.headers['X-RequestDigest'] !== 'ABC') { + throw 'Invalid request'; + } + + if (opts.data === `MyColumn`) { + return JSON.stringify([{ + "SchemaVersion": "15.0.0.0", + "LibraryVersion": "16.0.8231.1213", + "ErrorInfo": { + "ErrorMessage": "Column 'MyColumn' does not exist. It may have been deleted by another user.", + "ErrorValue": null, + "TraceCorrelationId": "4c0fa19e-b007-0000-37ae-1d177693b378", + "ErrorCode": -2147024809, + "ErrorTypeName": "System.ArgumentException" + }, + "TraceCorrelationId": "4c0fa19e-b007-0000-37ae-1d177693b378" + }]); + } + } + + throw 'Invalid request'; + }); + + await assert.rejects(command.action(logger, { options: { webUrl: 'https://contoso.sharepoint.com', internalName: 'MyColumn', Description: 'My column' } } as any), + new CommandError(`Column 'MyColumn' does not exist. It may have been deleted by another user.`)); + }); + it('correctly handles an error when the list specified by id doesn\'t exist', async () => { sinon.stub(request, 'post').callsFake(async (opts) => { if ((opts.url as string).indexOf(`/_vti_bin/client.svc/ProcessQuery`) > -1) { @@ -669,7 +883,7 @@ describe(commands.FIELD_SET, () => { assert.notStrictEqual(actual, true); }); - it('fails validation if neither id nor title are specified', async () => { + it('fails validation if either id, title, or internalName are not specified', async () => { sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { if (settingName === settingsNames.prompt) { return false; @@ -695,6 +909,45 @@ describe(commands.FIELD_SET, () => { assert.notStrictEqual(actual, true); }); + it('fails validation if both id and internalName are specified', async () => { + sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { + if (settingName === settingsNames.prompt) { + return false; + } + + return defaultValue; + }); + + const actual = await command.validate({ options: { webUrl: 'https://contoso.sharepoint.com', id: '330f29c5-5c4c-465f-9f4b-7903020ae1ce', internalName: 'MyColumn' } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('fails validation if both title and internalName are specified', async () => { + sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { + if (settingName === settingsNames.prompt) { + return false; + } + + return defaultValue; + }); + + const actual = await command.validate({ options: { webUrl: 'https://contoso.sharepoint.com', title: 'MyColumn', internalName: 'MyColumn' } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('fails validation if id, title and internalName are specified', async () => { + sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { + if (settingName === settingsNames.prompt) { + return false; + } + + return defaultValue; + }); + + const actual = await command.validate({ options: { webUrl: 'https://contoso.sharepoint.com', id: '330f29c5-5c4c-465f-9f4b-7903020ae1ce', title: 'MyColumn', internalName: 'MyColumn' } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + it('fails validation if id is specified and is not a valid GUID', async () => { const actual = await command.validate({ options: { webUrl: 'https://contoso.sharepoint.com', id: 'invalid' } }, commandInfo); assert.notStrictEqual(actual, true); @@ -720,6 +973,11 @@ describe(commands.FIELD_SET, () => { assert.strictEqual(actual, true); }); + it('passes validation when webUrl, listId and internalName are specified', async () => { + const actual = await command.validate({ options: { webUrl: 'https://contoso.sharepoint.com', listId: '330f29c5-5c4c-465f-9f4b-7903020ae1ce', internalName: 'MyColumn' } }, commandInfo); + assert.strictEqual(actual, true); + }); + it('passes validation when webUrl, listTitle and id are specified', async () => { const actual = await command.validate({ options: { webUrl: 'https://contoso.sharepoint.com', listTitle: 'My List', id: '330f29c5-5c4c-465f-9f4b-7903020ae1ce' } }, commandInfo); assert.strictEqual(actual, true); diff --git a/src/m365/spo/commands/field/field-set.ts b/src/m365/spo/commands/field/field-set.ts index f95eeb0f7f1..64fd67f10e8 100644 --- a/src/m365/spo/commands/field/field-set.ts +++ b/src/m365/spo/commands/field/field-set.ts @@ -16,6 +16,7 @@ interface CommandArgs { interface Options extends GlobalOptions { id?: string; title?: string; + internalName?: string; listId?: string; listTitle?: string; listUrl?: string; @@ -46,6 +47,7 @@ class SpoFieldSetCommand extends SpoCommand { Object.assign(this.telemetryProperties, { id: typeof args.options.id !== 'undefined', title: typeof args.options.title !== 'undefined', + internalName: typeof args.options.internalName !== 'undefined', listId: typeof args.options.listId !== 'undefined', listTitle: typeof args.options.listTitle !== 'undefined', listUrl: typeof args.options.listUrl !== 'undefined', @@ -74,6 +76,9 @@ class SpoFieldSetCommand extends SpoCommand { { option: '-t, --title [title]' }, + { + option: '--internalName [internalName]' + }, { option: '--updateExistingLists' } @@ -109,7 +114,7 @@ class SpoFieldSetCommand extends SpoCommand { } #initOptionSets(): void { - this.optionSets.push({ options: ['id', 'title'] }); + this.optionSets.push({ options: ['id', 'title', 'internalName'] }); } public allowUnknownOptions(): boolean | undefined { @@ -158,7 +163,7 @@ class SpoFieldSetCommand extends SpoCommand { // retrieve column CSOM object id const fieldQuery: string = args.options.id ? `${formatting.escapeXml(args.options.id)}` : - `${formatting.escapeXml(args.options.name || args.options.title)}`; + `${formatting.escapeXml(args.options.name || args.options.title || args.options.internalName)}`; let requestOptions: CliRequestOptions = { url: `${args.options.webUrl}/_vti_bin/client.svc/ProcessQuery`, @@ -206,6 +211,7 @@ class SpoFieldSetCommand extends SpoCommand { 'listUrl', 'id', 'title', + 'internalName', 'updateExistingLists', 'debug', 'verbose',