diff --git a/docs/docs/cmd/spo/brandcenter/brandcenter-colors-list.mdx b/docs/docs/cmd/spo/brandcenter/brandcenter-colors-list.mdx new file mode 100644 index 00000000000..59986eb5dfd --- /dev/null +++ b/docs/docs/cmd/spo/brandcenter/brandcenter-colors-list.mdx @@ -0,0 +1,115 @@ +import Global from '../../_global.mdx'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# spo brandcenter colors list + +Lists the brand center colors + +## Usage + +```sh +m365 spo brandcenter colors list [options] +``` + +## Options + + + +## Permissions + + + + + | Resource | Permissions | + |------------|---------------| + | SharePoint | AllSites.Read | + + + + + | Resource | Permissions | + |------------|----------------| + | SharePoint | Sites.Read.All | + + + + +## Remarks + +If the brand colors list has not been created yet in the Brand Center, the command will return an empty array. + +## Examples + +List all brand center colors + +```sh +m365 spo brandcenter colors list +``` + +## Response + + + + + ```json + [ + { + "Title": "Primary", + "ColorCode": "#0078D4", + "IsVisible": true + }, + { + "Title": "Secondary", + "ColorCode": "#FF4500", + "IsVisible": false + } + ] + ``` + + + + + ```txt + Title ColorCode IsVisible + --------- --------- --------- + Primary #0078D4 true + Secondary #FF4500 false + ``` + + + + + ```csv + Title,ColorCode,IsVisible + Primary,#0078D4,true + Secondary,#FF4500,false + ``` + + + + + ```md + # spo brandcenter colors list + + Date: 4/4/2026 + + ## Primary + + Property | Value + ---------|------- + Title | Primary + ColorCode | #0078D4 + IsVisible | true + + ## Secondary + + Property | Value + ---------|------- + Title | Secondary + ColorCode | #FF4500 + IsVisible | false + ``` + + + diff --git a/docs/src/config/sidebars.ts b/docs/src/config/sidebars.ts index 55e13dca27c..32b0bc230c9 100644 --- a/docs/src/config/sidebars.ts +++ b/docs/src/config/sidebars.ts @@ -2398,6 +2398,11 @@ const sidebars: SidebarsConfig = { }, { brandcenter: [ + { + type: 'doc', + label: 'brandcenter colors list', + id: 'cmd/spo/brandcenter/brandcenter-colors-list' + }, { type: 'doc', label: 'brandcenter settings list', diff --git a/eslint.config.mjs b/eslint.config.mjs index 8f2002d59ac..925703e197b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -35,6 +35,7 @@ const dictionary = [ 'center', 'checklist', 'client', + 'colors', 'comm', 'command', 'community', diff --git a/src/m365/spo/commands.ts b/src/m365/spo/commands.ts index 238ce27cb9e..de91de5a149 100644 --- a/src/m365/spo/commands.ts +++ b/src/m365/spo/commands.ts @@ -20,6 +20,7 @@ export default { APPLICATIONCUSTOMIZER_SET: `${prefix} applicationcustomizer set`, APPPAGE_ADD: `${prefix} apppage add`, APPPAGE_SET: `${prefix} apppage set`, + BRANDCENTER_COLORS_LIST: `${prefix} brandcenter colors list`, BRANDCENTER_SETTINGS_LIST: `${prefix} brandcenter settings list`, CDN_GET: `${prefix} cdn get`, CDN_ORIGIN_ADD: `${prefix} cdn origin add`, diff --git a/src/m365/spo/commands/brandcenter/brandcenter-colors-list.spec.ts b/src/m365/spo/commands/brandcenter/brandcenter-colors-list.spec.ts new file mode 100644 index 00000000000..10dc09ac513 --- /dev/null +++ b/src/m365/spo/commands/brandcenter/brandcenter-colors-list.spec.ts @@ -0,0 +1,241 @@ +import assert from 'assert'; +import sinon from 'sinon'; +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 { odata } from '../../../../utils/odata.js'; +import { pid } from '../../../../utils/pid.js'; +import { session } from '../../../../utils/session.js'; +import { sinonUtil } from '../../../../utils/sinonUtil.js'; +import { z } from 'zod'; +import commands from '../../commands.js'; +import command from './brandcenter-colors-list.js'; + +describe(commands.BRANDCENTER_COLORS_LIST, () => { + let log: any[]; + let logger: Logger; + let loggerLogSpy: sinon.SinonSpy; + let commandInfo: CommandInfo; + let commandOptionsSchema: z.ZodTypeAny; + + const configurationResponseWithColors = { + "BrandColorsListId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "BrandColorsListUrl": { + "DecodedUrl": "https://contoso.sharepoint.com/sites/BrandGuide/_catalogs/brandcolors" + }, + "BrandFontLibraryId": "23af51de-856c-4d00-aa11-0d03af0e46e3", + "BrandFontLibraryUrl": { + "DecodedUrl": "https://contoso.sharepoint.com/sites/BrandGuide/Fonts" + }, + "IsBrandCenterSiteFeatureEnabled": true, + "IsPublicCdnEnabled": true, + "SiteId": "52b46e48-9c0c-40cb-a955-13eb6c717ff3", + "SiteUrl": "https://contoso.sharepoint.com/sites/BrandGuide" + }; + + const configurationResponseWithoutColors = { + "BrandColorsListId": "00000000-0000-0000-0000-000000000000", + "BrandColorsListUrl": null, + "BrandFontLibraryId": "23af51de-856c-4d00-aa11-0d03af0e46e3", + "BrandFontLibraryUrl": { + "DecodedUrl": "https://contoso.sharepoint.com/sites/BrandGuide/Fonts" + }, + "IsBrandCenterSiteFeatureEnabled": true, + "IsPublicCdnEnabled": true, + "SiteId": "52b46e48-9c0c-40cb-a955-13eb6c717ff3", + "SiteUrl": "https://contoso.sharepoint.com/sites/BrandGuide" + }; + + const brandColorsListItems = [ + { + "OData__SPColorTitle": "Primary", + "OData__SPColorCode": "#0078D4", + "OData__SPColorVisible": true + }, + { + "OData__SPColorTitle": "Secondary", + "OData__SPColorCode": "#FF4500", + "OData__SPColorVisible": false + } + ]; + + const mappedBrandColors = [ + { + "Title": "Primary", + "ColorCode": "#0078D4", + "IsVisible": true + }, + { + "Title": "Secondary", + "ColorCode": "#FF4500", + "IsVisible": false + } + ]; + + before(() => { + sinon.stub(auth, 'restoreAuth').resolves(); + sinon.stub(telemetry, 'trackEvent').resolves(); + sinon.stub(pid, 'getProcessName').returns(''); + sinon.stub(session, 'getId').returns(''); + + auth.connection.active = true; + auth.connection.spoUrl = 'https://contoso.sharepoint.com'; + 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'); + }); + + afterEach(() => { + sinonUtil.restore([ + request.get, + odata.getAllItems + ]); + }); + + after(() => { + sinon.restore(); + auth.connection.active = false; + auth.connection.spoUrl = undefined; + }); + + it('has correct name', () => { + assert.strictEqual(command.name, commands.BRANDCENTER_COLORS_LIST); + }); + + it('has a description', () => { + assert.notStrictEqual(command.description, null); + }); + + it('defines correct default properties', () => { + assert.deepStrictEqual(command.defaultProperties(), ['ColorCode', 'Title', 'IsVisible']); + }); + + it('passes validation with no options', () => { + const actual = commandOptionsSchema.safeParse({}); + assert.strictEqual(actual.success, true); + }); + + it('fails validation with unknown options', () => { + const actual = commandOptionsSchema.safeParse({ option: "value" }); + assert.strictEqual(actual.success, false); + }); + + it('returns empty array when brand colors list does not exist', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === 'https://contoso.sharepoint.com/_api/Brandcenter/Configuration') { + return configurationResponseWithoutColors; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: {} }); + assert(loggerLogSpy.calledOnceWithExactly([])); + }); + + it('returns empty array when brand colors list does not exist (verbose)', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === 'https://contoso.sharepoint.com/_api/Brandcenter/Configuration') { + return configurationResponseWithoutColors; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { verbose: true } }); + assert(loggerLogSpy.calledOnceWithExactly([])); + }); + + it('successfully lists brand center colors', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === 'https://contoso.sharepoint.com/_api/Brandcenter/Configuration') { + return configurationResponseWithColors; + } + + throw 'Invalid request'; + }); + + sinon.stub(odata, 'getAllItems').callsFake(async (url) => { + if (url === `https://contoso.sharepoint.com/sites/BrandGuide/_api/web/lists(guid'a1b2c3d4-e5f6-7890-abcd-ef1234567890')/items?$select=OData__SPColorTitle,OData__SPColorCode,OData__SPColorVisible`) { + return brandColorsListItems; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: {} }); + assert(loggerLogSpy.calledOnceWithExactly(mappedBrandColors)); + }); + + it('successfully lists brand center colors (verbose)', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === 'https://contoso.sharepoint.com/_api/Brandcenter/Configuration') { + return configurationResponseWithColors; + } + + throw 'Invalid request'; + }); + + sinon.stub(odata, 'getAllItems').callsFake(async (url) => { + if (url === `https://contoso.sharepoint.com/sites/BrandGuide/_api/web/lists(guid'a1b2c3d4-e5f6-7890-abcd-ef1234567890')/items?$select=OData__SPColorTitle,OData__SPColorCode,OData__SPColorVisible`) { + return brandColorsListItems; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { verbose: true } }); + assert(loggerLogSpy.calledOnceWithExactly(mappedBrandColors)); + }); + + it('correctly handles error when retrieving configuration', async () => { + sinon.stub(request, 'get').rejects({ + "error": { + "code": "accessDenied", + "message": "Access denied" + } + }); + + await assert.rejects(command.action(logger, { options: {} }), + new CommandError('Access denied')); + }); + + it('correctly handles error when retrieving list items', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === 'https://contoso.sharepoint.com/_api/Brandcenter/Configuration') { + return configurationResponseWithColors; + } + + throw 'Invalid request'; + }); + + sinon.stub(odata, 'getAllItems').rejects({ + "error": { + "code": "itemNotFound", + "message": "The specified list was not found" + } + }); + + await assert.rejects(command.action(logger, { options: {} }), + new CommandError('The specified list was not found')); + }); +}); diff --git a/src/m365/spo/commands/brandcenter/brandcenter-colors-list.ts b/src/m365/spo/commands/brandcenter/brandcenter-colors-list.ts new file mode 100644 index 00000000000..fe0ed207ad7 --- /dev/null +++ b/src/m365/spo/commands/brandcenter/brandcenter-colors-list.ts @@ -0,0 +1,88 @@ +import commands from '../../commands.js'; +import { globalOptionsZod } from '../../../../Command.js'; +import { Logger } from '../../../../cli/Logger.js'; +import request, { CliRequestOptions } from '../../../../request.js'; +import { odata } from '../../../../utils/odata.js'; +import { spo } from '../../../../utils/spo.js'; +import SpoCommand from '../../../base/SpoCommand.js'; +import { z } from 'zod'; + +const options = globalOptionsZod.strict(); + +interface BrandCenterConfiguration { + BrandColorsListId: string; + BrandColorsListUrl: { DecodedUrl: string } | null; + SiteUrl: string; +} + +interface BrandColorListItem { + OData__SPColorTitle: string; + OData__SPColorCode: string; + OData__SPColorVisible: boolean; +} + +class SpoBrandCenterColorsListCommand extends SpoCommand { + public get name(): string { + return commands.BRANDCENTER_COLORS_LIST; + } + + public get description(): string { + return 'Lists the brand center colors'; + } + + public defaultProperties(): string[] | undefined { + return ['ColorCode', 'Title', 'IsVisible']; + } + + public get schema(): z.ZodTypeAny | undefined { + return options; + } + + public async commandAction(logger: Logger): Promise { + if (this.verbose) { + await logger.logToStderr(`Retrieving brand center colors...`); + } + + try { + const spoUrl: string = await spo.getSpoUrl(logger, this.verbose); + + const configRequestOptions: CliRequestOptions = { + url: `${spoUrl}/_api/Brandcenter/Configuration`, + headers: { + accept: 'application/json;odata=nometadata' + }, + responseType: 'json' + }; + + const config = await request.get(configRequestOptions); + + if (!config.BrandColorsListUrl) { + if (this.verbose) { + await logger.logToStderr('Brand colors list not found.'); + } + + await logger.log([]); + return; + } + + if (this.verbose) { + await logger.logToStderr(`Brand colors list found at '${config.BrandColorsListUrl.DecodedUrl}'.`); + } + + const items = await odata.getAllItems(`${config.SiteUrl}/_api/web/lists(guid'${config.BrandColorsListId}')/items?$select=OData__SPColorTitle,OData__SPColorCode,OData__SPColorVisible`); + + const result = items.map(item => ({ + Title: item.OData__SPColorTitle, + ColorCode: item.OData__SPColorCode, + IsVisible: item.OData__SPColorVisible + })); + + await logger.log(result); + } + catch (err: any) { + this.handleRejectedODataJsonPromise(err); + } + } +} + +export default new SpoBrandCenterColorsListCommand();