From 760a28cf0d3ef39e8c045f06b09c284585c31870 Mon Sep 17 00:00:00 2001 From: Nanddeep Nachan Date: Mon, 2 Oct 2023 02:08:54 +0000 Subject: [PATCH 01/23] Adds 'aad group user list' command. Closes #5469 --- docs/docs/cmd/aad/group/group-user-list.mdx | 134 +++++++ docs/docusaurus.config.js | 1 + docs/src/config/sidebars.js | 5 + src/m365/aad/commands.ts | 1 + .../commands/group/group-user-list.spec.ts | 335 ++++++++++++++++++ .../aad/commands/group/group-user-list.ts | 203 +++++++++++ src/utils/odata.ts | 50 +-- 7 files changed, 707 insertions(+), 22 deletions(-) create mode 100644 docs/docs/cmd/aad/group/group-user-list.mdx create mode 100644 src/m365/aad/commands/group/group-user-list.spec.ts create mode 100644 src/m365/aad/commands/group/group-user-list.ts diff --git a/docs/docs/cmd/aad/group/group-user-list.mdx b/docs/docs/cmd/aad/group/group-user-list.mdx new file mode 100644 index 00000000000..26f2152b90f --- /dev/null +++ b/docs/docs/cmd/aad/group/group-user-list.mdx @@ -0,0 +1,134 @@ +import Global from '/docs/cmd/_global.mdx'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# aad group user list + +Lists users of a specific Azure AD group + +## Usage + +```sh +m365 aad group user list [options] +``` + +## Options + +```md definition-list +`-i, --groupId [groupId]` +: The ID of the Azure AD group. Specify `groupId` or `groupDisplayName` but not both. + +`-n, --groupDisplayName [groupDisplayName]` +: The display name of the Azure AD group. Specify `groupId` or `groupDisplayName` but not both. + +`-r, --role [role]` +: Filter the results to only users with the given role: `Owner`, `Member`. + +`-p, --properties [properties]` +: Comma-separated list of properties to retrieve. + +`-f, --filter [filter]` +: OData filter to use to query the list of users with. +``` + + + +## Remarks + +When the `properties` option includes values with a `/`, for example: `memberof/id`, an additional `$expand` query parameter will be included on `memberof`. + +## Examples + +List all group users from a group specified by ID. + +```sh +m365 aad group user list --groupId 03cba9da-3974-46c1-afaf-79caa2e45bbe +``` + +List all owners from a group specified by display name. + +```sh +m365 aad group user list --groupDisplayName Developers --role Owner +``` + +List all group users from a group specified by name. For each one return the display name, e-mail address, and manager display name. + +```sh +m365 aad group user list --groupDisplayName Developers --properties "displayName,mail,manager/displayName" +``` + +List all group users from a group specified by name. For each one return the display name, e-mail address, and manager information. + +```sh +m365 aad group user list --groupDisplayName Developers --properties "displayName,mail,manager/*" + +List all group members that are guest users. + +```sh +m365 aad group user list --groupDisplayName Developers --filter "userType eq 'Guest'" +``` + +## Response + + + + + ```json + [ + { + "id": "da52218e-4822-4ac6-b41d-255e2059655e", + "displayName": "Adele Vance", + "userPrincipalName": "AdeleV@contoso.OnMicrosoft.com", + "givenName": "Adele", + "surname": "Vance", + "roles": [ + "Owner", + "Member" + ] + } + ] + ``` + + + + + ```text + id displayName userPrincipalName roles + ------------------------------------ -------------------- ------------------------------------ -------- + da52218e-4822-4ac6-b41d-255e2059655e Adele Vance AdeleV@contoso.OnMicrosoft.com Owner,Member + ``` + + + + + ```csv + id,displayName,userPrincipalName,givenName,surname + da52218e-4822-4ac6-b41d-255e2059655e,Adele Vance,AdeleV@contoso.OnMicrosoft.com,Adele,Vance + ``` + + + + + ```md + # aad group user list --groupId "1deaa428-8dde-4043-b028-5492226d6114" + + Date: 2023-10-02 + + ## Adele Vance (da52218e-4822-4ac6-b41d-255e2059655e) + + Property | Value + ---------|------- + id | da52218e-4822-4ac6-b41d-255e2059655e + displayName | Adele Vance + userPrincipalName | AdeleV@contoso.OnMicrosoft.com + givenName | Adele + surname | Vance + ``` + + + + + +## More information + +- View the documentation to see what userproperties can be included: [https://pnp.github.io/cli-microsoft365/cmd/aad/user/user-get#more-information](https://pnp.github.io/cli-microsoft365/cmd/aad/user/user-get#more-information) diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index bc0712ce3aa..6f4ddbce8e8 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -13,6 +13,7 @@ const config = { favicon: 'img/favicon.ico', organizationName: 'pnp', projectName: 'cli-microsoft365', + trailingSlash: false, i18n: { defaultLocale: 'en', diff --git a/docs/src/config/sidebars.js b/docs/src/config/sidebars.js index 28f8a409d37..d55c8c52e0e 100644 --- a/docs/src/config/sidebars.js +++ b/docs/src/config/sidebars.js @@ -105,6 +105,11 @@ const sidebars = { type: 'doc', label: 'group remove', id: 'cmd/aad/group/group-remove' + }, + { + type: 'doc', + label: 'group user list', + id: 'cmd/aad/group/group-user-list' } ] }, diff --git a/src/m365/aad/commands.ts b/src/m365/aad/commands.ts index 774e85936d6..b0586fdc7e3 100644 --- a/src/m365/aad/commands.ts +++ b/src/m365/aad/commands.ts @@ -15,6 +15,7 @@ export default { GROUP_GET: `${prefix} group get`, GROUP_LIST: `${prefix} group list`, GROUP_REMOVE: `${prefix} group remove`, + GROUP_USER_LIST: `${prefix} group user list`, GROUPSETTING_ADD: `${prefix} groupsetting add`, GROUPSETTING_GET: `${prefix} groupsetting get`, GROUPSETTING_LIST: `${prefix} groupsetting list`, diff --git a/src/m365/aad/commands/group/group-user-list.spec.ts b/src/m365/aad/commands/group/group-user-list.spec.ts new file mode 100644 index 00000000000..c1f7776f14b --- /dev/null +++ b/src/m365/aad/commands/group/group-user-list.spec.ts @@ -0,0 +1,335 @@ +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 { aadGroup } from '../../../../utils/aadGroup.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 './group-user-list.js'; + +describe(commands.GROUP_USER_LIST, () => { + const groupId = '2c1ba4c4-cd9b-4417-832f-92a34bc34b2a'; + const groupDisplayName = 'CLI Test Group'; + + let log: string[]; + let logger: Logger; + let loggerLogSpy: sinon.SinonSpy; + let commandInfo: CommandInfo; + + before(() => { + sinon.stub(auth, 'restoreAuth').resolves(); + sinon.stub(telemetry, 'trackEvent').returns(); + sinon.stub(pid, 'getProcessName').returns(''); + sinon.stub(session, 'getId').returns(''); + auth.service.connected = true; + commandInfo = Cli.getCommandInfo(command); + }); + + 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'); + (command as any).items = []; + }); + + afterEach(() => { + sinonUtil.restore([ + request.get, + aadGroup.getGroupIdByDisplayName + ]); + }); + + after(() => { + sinon.restore(); + auth.service.connected = false; + }); + + it('has correct name', () => { + assert.strictEqual(command.name, commands.GROUP_USER_LIST); + }); + + it('has a description', () => { + assert.notStrictEqual(command.description, null); + }); + + it('defines correct properties for the default output', () => { + assert.deepStrictEqual(command.defaultProperties(), ['id', 'displayName', 'userPrincipalName', 'roles']); + }); + + it('fails validation if the groupId is not a valid guid.', async () => { + const actual = await command.validate({ + options: { + groupId: 'not-c49b-4fd4-8223-28f0ac3a6402' + } + }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('fails validation when invalid role specified', async () => { + const actual = await command.validate({ + options: { + groupId: '6703ac8a-c49b-4fd4-8223-28f0ac3a6402', + role: 'Invalid' + } + }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('passes validation when valid groupId and no role specified', async () => { + const actual = await command.validate({ + options: { + groupId: groupId + } + }, commandInfo); + assert.strictEqual(actual, true); + }); + + it('correctly lists all users in a Azure AD group by id', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/2c1ba4c4-cd9b-4417-832f-92a34bc34b2a/Owners/microsoft.graph.user?$select=id,displayName,userPrincipalName,givenName,surname`) { + return { + "value": [{ "id": "00000000-0000-0000-0000-000000000000", "displayName": "Anne Matthews", "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", "givenName": "Anne", "surname": "Matthews" }] + }; + } + + if (opts.url === `https://graph.microsoft.com/v1.0/groups/2c1ba4c4-cd9b-4417-832f-92a34bc34b2a/Members/microsoft.graph.user?$select=id,displayName,userPrincipalName,givenName,surname`) { + return { + "value": [ + { "id": "00000000-0000-0000-0000-000000000000", "displayName": "Anne Matthews", "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", "givenName": "Anne", "surname": "Matthews" }, + { "id": "00000000-0000-0000-0000-000000000001", "displayName": "Karl Matteson", "userPrincipalName": "karl.matteson@contoso.onmicrosoft.com", "givenName": "Karl", "surname": "Matteson" } + ] + }; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { verbose: true, groupId: groupId } }); + + assert(loggerLogSpy.calledOnceWithExactly([ + { + "id": "00000000-0000-0000-0000-000000000000", + "displayName": "Anne Matthews", + "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", + "givenName": "Anne", + "surname": "Matthews", + "roles": ["Owner", "Member"] + }, + { + "id": "00000000-0000-0000-0000-000000000001", + "displayName": "Karl Matteson", + "userPrincipalName": "karl.matteson@contoso.onmicrosoft.com", + "givenName": "Karl", + "surname": "Matteson", + "roles": ["Member"] + } + ])); + }); + + it('correctly lists all users in a Azure AD group by name', async () => { + sinon.stub(aadGroup, 'getGroupIdByDisplayName').resolves(groupId); + + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/2c1ba4c4-cd9b-4417-832f-92a34bc34b2a/Owners/microsoft.graph.user?$select=id,displayName,userPrincipalName,givenName,surname`) { + return { + "value": [{ "id": "00000000-0000-0000-0000-000000000000", "displayName": "Anne Matthews", "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", "givenName": "Anne", "surname": "Matthews" }] + }; + } + + if (opts.url === `https://graph.microsoft.com/v1.0/groups/2c1ba4c4-cd9b-4417-832f-92a34bc34b2a/Members/microsoft.graph.user?$select=id,displayName,userPrincipalName,givenName,surname`) { + return { + "value": [ + { "id": "00000000-0000-0000-0000-000000000000", "displayName": "Anne Matthews", "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", "givenName": "Anne", "surname": "Matthews" }, + { "id": "00000000-0000-0000-0000-000000000001", "displayName": "Karl Matteson", "userPrincipalName": "karl.matteson@contoso.onmicrosoft.com", "givenName": "Karl", "surname": "Matteson" } + ] + }; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { verbose: true, groupDisplayName: groupDisplayName } }); + + assert(loggerLogSpy.calledOnceWithExactly([ + { + "id": "00000000-0000-0000-0000-000000000000", + "displayName": "Anne Matthews", + "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", + "givenName": "Anne", + "surname": "Matthews", + "roles": ["Owner", "Member"] + }, + { + "id": "00000000-0000-0000-0000-000000000001", + "displayName": "Karl Matteson", + "userPrincipalName": "karl.matteson@contoso.onmicrosoft.com", + "givenName": "Karl", + "surname": "Matteson", + "roles": ["Member"] + } + ])); + }); + + it('correctly lists all owners in a Azure AD group', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/2c1ba4c4-cd9b-4417-832f-92a34bc34b2a/Owners/microsoft.graph.user?$select=id,displayName,userPrincipalName,givenName,surname`) { + return { + "value": [{ "id": "00000000-0000-0000-0000-000000000000", "displayName": "Anne Matthews", "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", "givenName": "Anne", "surname": "Matthews" }] + }; + } + throw 'Invalid request'; + }); + + await command.action(logger, { options: { groupId: groupId, role: "Owner" } }); + assert(loggerLogSpy.calledOnceWithExactly([ + { + "id": "00000000-0000-0000-0000-000000000000", + "displayName": "Anne Matthews", + "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", + "givenName": "Anne", + "surname": "Matthews", + "roles": ["Owner"] + } + ])); + }); + + it('correctly lists all members in a Azure AD group', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/2c1ba4c4-cd9b-4417-832f-92a34bc34b2a/Members/microsoft.graph.user?$select=id,displayName,userPrincipalName,givenName,surname`) { + return { + "value": [ + { "id": "00000000-0000-0000-0000-000000000000", "displayName": "Anne Matthews", "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", "givenName": "Anne", "surname": "Matthews" }, + { "id": "00000000-0000-0000-0000-000000000001", "displayName": "Karl Matteson", "userPrincipalName": "karl.matteson@contoso.onmicrosoft.com", "givenName": "Karl", "surname": "Matteson" } + ] + }; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { groupId: groupId, role: "Member" } }); + + assert(loggerLogSpy.calledOnceWithExactly([ + { + "id": "00000000-0000-0000-0000-000000000000", + "displayName": "Anne Matthews", + "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", + "givenName": "Anne", + "surname": "Matthews", + "roles": ["Member"] + }, + { + "id": "00000000-0000-0000-0000-000000000001", + "displayName": "Karl Matteson", + "userPrincipalName": "karl.matteson@contoso.onmicrosoft.com", + "givenName": "Karl", + "surname": "Matteson", + "roles": ["Member"] + } + ])); + }); + + it('correctly lists properties for all users in a Azure AD group', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/2c1ba4c4-cd9b-4417-832f-92a34bc34b2a/Owners/microsoft.graph.user?$select=displayName,mail,id&$expand=memberof($select=id),memberof($select=displayName)`) { + return { + "value": [ + { "id": "00000000-0000-0000-0000-000000000000", "displayName": "Karl Matteson", "mail": "karl.matteson@contoso.onmicrosoft.com", "memberOf": [{ "displayName": "Life and Music", "id": "d6c88284-c598-468d-8074-56acaf3c0453" }] } + ] + }; + } + + if (opts.url === `https://graph.microsoft.com/v1.0/groups/2c1ba4c4-cd9b-4417-832f-92a34bc34b2a/Members/microsoft.graph.user?$select=displayName,mail,id&$expand=memberof($select=id),memberof($select=displayName)`) { + return { + "value": [ + { "id": "00000000-0000-0000-0000-000000000001", "displayName": "Anne Matthews", "mail": "anne.matthews@contoso.onmicrosoft.com", "memberOf": [{ "displayName": "Life and Music", "id": "d6c88284-c598-468d-8074-56acaf3c0454" }] } + ] + }; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { groupId: groupId, properties: "displayName,mail,memberof/id,memberof/displayName" } }); + + assert(loggerLogSpy.calledOnceWithExactly([ + { "id": "00000000-0000-0000-0000-000000000000", "displayName": "Karl Matteson", "mail": "karl.matteson@contoso.onmicrosoft.com", "memberOf": [{ "displayName": "Life and Music", "id": "d6c88284-c598-468d-8074-56acaf3c0453" }], "roles": ["Owner"] }, + { "id": "00000000-0000-0000-0000-000000000001", "displayName": "Anne Matthews", "mail": "anne.matthews@contoso.onmicrosoft.com", "memberOf": [{ "displayName": "Life and Music", "id": "d6c88284-c598-468d-8074-56acaf3c0454" }], "roles": ["Member"] } + ])); + }); + + it('correctly lists all guest users in a Azure AD group', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/2c1ba4c4-cd9b-4417-832f-92a34bc34b2a/Owners/microsoft.graph.user?$select=id,displayName,userPrincipalName,givenName,surname&$filter=userType%20eq%20'Guest'&$count=true`) { + return { + "value": [] + }; + } + + if (opts.url === `https://graph.microsoft.com/v1.0/groups/2c1ba4c4-cd9b-4417-832f-92a34bc34b2a/Members/microsoft.graph.user?$select=id,displayName,userPrincipalName,givenName,surname&$filter=userType%20eq%20'Guest'&$count=true`) { + return { + "value": [ + { "id": "00000000-0000-0000-0000-000000000000", "displayName": "Anne Matthews", "userPrincipalName": "annematthews_gmail.com#EXT#@contoso.onmicrosoft.com", "givenName": "Anne", "surname": "Matthews" } + ] + }; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { groupId: groupId, filter: "userType eq 'Guest'" } }); + + assert(loggerLogSpy.calledOnceWithExactly([ + { + "id": "00000000-0000-0000-0000-000000000000", + "displayName": "Anne Matthews", + "userPrincipalName": "annematthews_gmail.com#EXT#@contoso.onmicrosoft.com", + "givenName": "Anne", + "surname": "Matthews", + "roles": ["Member"] + } + ])); + }); + + it('throws an error when group by id cannot be found', async () => { + const error = { + error: { + code: 'Request_ResourceNotFound', + message: `Resource '${groupId}' does not exist or one of its queried reference-property objects are not present.`, + innerError: { + date: '2023-08-30T14:32:41', + 'request-id': 'b7dee9ee-d85b-4e7a-8686-74852cbfd85b', + 'client-request-id': 'b7dee9ee-d85b-4e7a-8686-74852cbfd85b' + } + } + }; + + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/2c1ba4c4-cd9b-4417-832f-92a34bc34b2a/Owners/microsoft.graph.user?$select=id,displayName,userPrincipalName,givenName,surname`) { + throw error; + } + + throw 'Invalid request'; + }); + + await assert.rejects(command.action(logger, { options: { verbose: true, groupId: groupId } }), + new CommandError(error.error.message)); + }); +}); \ No newline at end of file diff --git a/src/m365/aad/commands/group/group-user-list.ts b/src/m365/aad/commands/group/group-user-list.ts new file mode 100644 index 00000000000..ba537c5485b --- /dev/null +++ b/src/m365/aad/commands/group/group-user-list.ts @@ -0,0 +1,203 @@ +import { User } from '@microsoft/microsoft-graph-types'; +import { Logger } from '../../../../cli/Logger.js'; +import GlobalOptions from '../../../../GlobalOptions.js'; +import { CliRequestOptions } from '../../../../request.js'; +import { aadGroup } from '../../../../utils/aadGroup.js'; +import { odata } from '../../../../utils/odata.js'; +import { validation } from '../../../../utils/validation.js'; +import GraphCommand from '../../../base/GraphCommand.js'; +import commands from '../../commands.js'; + +interface CommandArgs { + options: Options; +} + +interface Options extends GlobalOptions { + groupId?: string; + groupDisplayName?: string; + role?: string; + properties?: string; + filter?: string; +} + +interface ExtendedUser extends User { + roles: string[]; +} + +class AadGroupUserListCommand extends GraphCommand { + public get name(): string { + return commands.GROUP_USER_LIST; + } + + public get description(): string { + return 'Lists users of a specific Azure AD group'; + } + + public defaultProperties(): string[] | undefined { + return ['id', 'displayName', 'userPrincipalName', 'roles']; + } + + constructor() { + super(); + + this.#initTelemetry(); + this.#initOptions(); + this.#initOptionSets(); + this.#initValidators(); + } + + #initTelemetry(): void { + this.telemetry.push((args: CommandArgs) => { + Object.assign(this.telemetryProperties, { + groupId: typeof args.options.groupId !== 'undefined', + groupDisplayName: typeof args.options.groupDisplayName !== 'undefined', + role: typeof args.options.role !== 'undefined', + properties: typeof args.options.properties !== 'undefined', + filter: typeof args.options.filter !== 'undefined' + }); + }); + } + + #initOptions(): void { + this.options.unshift( + { + option: "-i, --groupId [groupId]" + }, + { + option: "-n, --groupDisplayName [groupDisplayName]" + }, + { + option: "-r, --role [role]", + autocomplete: ["Owner", "Member"] + }, + { + option: "-p, --properties [properties]" + }, + { + option: "-f, --filter [filter]" + } + ); + } + + #initOptionSets(): void { + this.optionSets.push( + { + options: ['groupId', 'groupDisplayName'] + } + ); + } + + #initValidators(): void { + this.validators.push( + async (args: CommandArgs) => { + if (args.options.groupId && !validation.isValidGuid(args.options.groupId)) { + return `${args.options.groupId} is not a valid GUID`; + } + + if (args.options.role) { + if (['Owner', 'Member'].indexOf(args.options.role) === -1) { + return `${args.options.role} is not a valid role value. Allowed values Owner|Member`; + } + } + + return true; + } + ); + } + + public async commandAction(logger: Logger, args: CommandArgs): Promise { + try { + const groupId = await this.getGroupId(args.options, logger); + + const users: ExtendedUser[] = []; + + if (!args.options.role || args.options.role === 'Owner') { + const owners = await this.getUsers(args.options, 'Owners', groupId, logger); + owners.forEach(owner => users.push({ ...owner, roles: ['Owner'] })); + } + + if (!args.options.role || args.options.role === 'Member') { + const members = await this.getUsers(args.options, 'Members', groupId, logger); + + members.forEach((member: ExtendedUser) => { + const user = users.find((u: ExtendedUser) => u.id === member.id); + + if (user !== undefined) { + user.roles.push('Member'); + } + else { + users.push({ ...member, roles: ['Member'] }); + } + }); + } + + await logger.log(users); + } + catch (err: any) { + this.handleRejectedODataJsonPromise(err); + } + } + + private async getGroupId(options: Options, logger: Logger): Promise { + if (options.groupId) { + return options.groupId; + } + + if (this.verbose) { + await logger.logToStderr('Retrieving Group Id...'); + } + + return await aadGroup.getGroupIdByDisplayName(options.groupDisplayName!); + } + + private async getUsers(options: Options, role: string, groupId: string, logger: Logger): Promise { + const { properties, filter } = options; + + if (this.verbose) { + await logger.logToStderr(`Retrieving ${role} of the group with id ${groupId}`); + } + + const selectProperties: string = properties ? + `${properties.split(',').filter(f => f.toLowerCase() !== 'id').concat('id').map(p => p.trim()).join(',')}` : + 'id,displayName,userPrincipalName,givenName,surname'; + const allSelectProperties: string[] = selectProperties.split(','); + const propertiesWithSlash: string[] = allSelectProperties.filter(item => item.includes('/')); + + let fieldExpand: string = ''; + propertiesWithSlash.forEach(p => { + if (fieldExpand.length > 0) { + fieldExpand += ','; + } + + fieldExpand += `${p.split('/')[0]}($select=${p.split('/')[1]})`; + }); + + const expandParam = fieldExpand.length > 0 ? `&$expand=${fieldExpand}` : ''; + const selectParam = allSelectProperties.filter(item => !item.includes('/')); + const endpoint: string = `${this.resource}/v1.0/groups/${groupId}/${role}/microsoft.graph.user?$select=${selectParam}${expandParam}`; + + let users: ExtendedUser[] = []; + + if (filter) { + // While using the filter, we need to specify the ConsistencyLevel header. + // Can be refactored when the header is no longer necessary. + const requestOptions: CliRequestOptions = { + url: `${endpoint}&$filter=${encodeURIComponent(filter)}&$count=true`, + headers: { + accept: 'application/json;odata.metadata=none', + ConsistencyLevel: 'eventual' + }, + responseType: 'json' + }; + + users = await odata.getAllItems(requestOptions); + } + else { + users = await odata.getAllItems(endpoint); + } + + return users; + } +} + +export default new AadGroupUserListCommand(); \ No newline at end of file diff --git a/src/utils/odata.ts b/src/utils/odata.ts index bcf3c38f8cd..946861c60fa 100644 --- a/src/utils/odata.ts +++ b/src/utils/odata.ts @@ -17,28 +17,34 @@ export interface GraphResponseError { } } -export const odata = { - async getAllItems(url: string, metadata?: 'none' | 'minimal' | 'full'): Promise { - let items: T[] = []; - - const requestOptions: CliRequestOptions = { - url: url, - headers: { - accept: `application/json;odata.metadata=${metadata ?? 'none'}`, - 'odata-version': '4.0' - }, - responseType: 'json' - }; - - const res = await request.get>(requestOptions); - items = res.value; - - const nextLink = res['@odata.nextLink'] ?? res.nextLink; - if (nextLink) { - const nextPageItems = await odata.getAllItems(nextLink, metadata); - items = items.concat(nextPageItems); - } +function getAllItems(url: string): Promise; +function getAllItems(options: CliRequestOptions): Promise; +function getAllItems(url: string, metadata: 'none' | 'minimal' | 'full'): Promise; + +async function getAllItems(param1: unknown, metadata?: 'none' | 'minimal' | 'full'): Promise { + let items: T[] = []; + + const requestOptions: CliRequestOptions = typeof param1 !== 'string' ? param1 as CliRequestOptions : { + url: param1 as string, + headers: { + accept: `application/json;odata.metadata=${metadata ?? 'none'}`, + 'odata-version': '4.0' + }, + responseType: 'json' + }; + + const res = await request.get>(requestOptions); + items = res.value; - return items; + const nextLink = res['@odata.nextLink'] ?? res.nextLink; + if (nextLink) { + const nextPageItems = await odata.getAllItems({ ...requestOptions, url: nextLink }); + items = items.concat(nextPageItems); } + + return items; +} + +export const odata = { + getAllItems }; \ No newline at end of file From b4c0c68b2e089344f88cb75da508d2fe2a34bce9 Mon Sep 17 00:00:00 2001 From: dojcsakj Date: Mon, 25 Sep 2023 09:47:37 +0200 Subject: [PATCH 02/23] Enhances 'cli doctor' to list scopes by resource. Closes #5487 --- src/m365/cli/commands/cli-doctor.spec.ts | 92 ++++++++++++++++++++---- src/m365/cli/commands/cli-doctor.ts | 23 +++--- 2 files changed, 92 insertions(+), 23 deletions(-) diff --git a/src/m365/cli/commands/cli-doctor.spec.ts b/src/m365/cli/commands/cli-doctor.spec.ts index b2a8fc98baf..62f0def167a 100644 --- a/src/m365/cli/commands/cli-doctor.spec.ts +++ b/src/m365/cli/commands/cli-doctor.spec.ts @@ -69,6 +69,7 @@ describe(commands.DOCTOR, () => { it('retrieves scopes in the diagnostic information about the current environment', async () => { const jwt = JSON.stringify({ + aud: 'https://graph.microsoft.com', scp: 'AllSites.FullControl AppCatalog.ReadWrite.All' }); const jwt64 = Buffer.from(jwt).toString('base64'); @@ -98,26 +99,57 @@ describe(commands.DOCTOR, () => { nodeVersion: 'v14.17.0', os: { 'platform': 'win32', 'version': 'Windows 10 Pro', 'release': '10.0.19043' }, roles: [], - scopes: ['AllSites.FullControl', 'AppCatalog.ReadWrite.All'] + scopes: { + 'https://graph.microsoft.com': [ + 'AllSites.FullControl', + 'AppCatalog.ReadWrite.All' + ] + } })); }); it('retrieves scopes from multiple access tokens in the diagnostic information about the current environment', async () => { const jwt1 = JSON.stringify({ + aud: 'https://graph.microsoft.com', scp: 'AllSites.FullControl AppCatalog.ReadWrite.All' }); let jwt64 = Buffer.from(jwt1).toString('base64'); const accessToken1 = `abc.${jwt64}.def`; const jwt2 = JSON.stringify({ + aud: 'https://mydev.sharepoint.com', scp: 'TermStore.Read.All' }); jwt64 = Buffer.from(jwt2).toString('base64'); const accessToken2 = `abc.${jwt64}.def`; + const jwt3 = JSON.stringify({ + aud: 'https://mydev-admin.sharepoint.com', + scp: 'TermStore.Read.All' + }); + jwt64 = Buffer.from(jwt3).toString('base64'); + const accessToken3 = `abc.${jwt64}.def`; + + const jwt4 = JSON.stringify({ + aud: 'https://mydev-my.sharepoint.com', + scp: 'TermStore.Read.All' + }); + jwt64 = Buffer.from(jwt4).toString('base64'); + const accessToken4 = `abc.${jwt64}.def`; + + const jwt5 = JSON.stringify({ + aud: 'https://contoso-admin.sharepoint.com', + scp: 'TermStore.Read.All' + }); + jwt64 = Buffer.from(jwt5).toString('base64'); + const accessToken5 = `abc.${jwt64}.def`; + sinon.stub(auth.service, 'accessTokens').value({ 'https://graph.microsoft.com': { 'expiresOn': '2021-07-04T09:52:18.000Z', 'accessToken': `${accessToken1}` }, - 'https://mydev.sharepoint.com': { 'expiresOn': '2021-07-04T09:52:18.000Z', 'accessToken': `${accessToken2}` } + 'https://mydev.sharepoint.com': { 'expiresOn': '2021-07-04T09:52:18.000Z', 'accessToken': `${accessToken2}` }, + 'https://mydev-admin.sharepoint.com': { 'expiresOn': '2021-07-04T09:52:18.000Z', 'accessToken': `${accessToken3}` }, + 'https://mydev-my.sharepoint.com': { 'expiresOn': '2021-07-04T09:52:18.000Z', 'accessToken': `${accessToken4}` }, + 'https://contoso-admin.sharepoint.com': { 'expiresOn': '2021-07-04T09:52:18.000Z', 'accessToken': `${accessToken5}` } }); sinon.stub(os, 'platform').returns('win32'); sinon.stub(os, 'version').returns('Windows 10 Pro'); @@ -140,12 +172,24 @@ describe(commands.DOCTOR, () => { nodeVersion: 'v14.17.0', os: { 'platform': 'win32', 'version': 'Windows 10 Pro', 'release': '10.0.19043' }, roles: [], - scopes: ['AllSites.FullControl', 'AppCatalog.ReadWrite.All', 'TermStore.Read.All'] + scopes: { + 'https://graph.microsoft.com': [ + 'AllSites.FullControl', + 'AppCatalog.ReadWrite.All' + ], + 'https://mydev.sharepoint.com': [ + 'TermStore.Read.All' + ], + 'https://contoso.sharepoint.com': [ + 'TermStore.Read.All' + ] + } })); }); it('retrieves roles in the diagnostic information about the current environment', async () => { const jwt = JSON.stringify({ + aud: 'https://graph.microsoft.com', roles: ['Sites.Read.All', 'Files.ReadWrite.All'] }); const jwt64 = Buffer.from(jwt).toString('base64'); @@ -175,18 +219,20 @@ describe(commands.DOCTOR, () => { nodeVersion: 'v14.17.0', os: { 'platform': 'win32', 'version': 'Windows 10 Pro', 'release': '10.0.19043' }, roles: ['Sites.Read.All', 'Files.ReadWrite.All'], - scopes: [] + scopes: {} })); }); it('retrieves roles from multiple access tokens in the diagnostic information about the current environment', async () => { const jwt1 = JSON.stringify({ + aud: 'https://graph.microsoft.com', roles: ['Sites.Read.All', 'Files.ReadWrite.All'] }); let jwt64 = Buffer.from(jwt1).toString('base64'); const accessToken1 = `abc.${jwt64}.def`; const jwt2 = JSON.stringify({ + aud: 'https://mydev.sharepoint.com', roles: ['TermStore.Read.All'] }); jwt64 = Buffer.from(jwt2).toString('base64'); @@ -217,12 +263,13 @@ describe(commands.DOCTOR, () => { nodeVersion: 'v14.17.0', os: { 'platform': 'win32', 'version': 'Windows 10 Pro', 'release': '10.0.19043' }, roles: ['Sites.Read.All', 'Files.ReadWrite.All', 'TermStore.Read.All'], - scopes: [] + scopes: {} })); }); it('retrieves roles and scopes in the diagnostic information about the current environment', async () => { const jwt = JSON.stringify({ + aud: 'https://graph.microsoft.com', roles: ['Sites.Read.All', 'Files.ReadWrite.All'], scp: 'Sites.Read.All Files.ReadWrite.All' }); @@ -253,12 +300,19 @@ describe(commands.DOCTOR, () => { nodeVersion: 'v14.17.0', os: { 'platform': 'win32', 'version': 'Windows 10 Pro', 'release': '10.0.19043' }, roles: ['Sites.Read.All', 'Files.ReadWrite.All'], - scopes: ['Sites.Read.All', 'Files.ReadWrite.All'] + scopes: { + 'https://graph.microsoft.com': + [ + 'Sites.Read.All', + 'Files.ReadWrite.All' + ] + } })); }); it('retrieves diagnostic information about the current environment when there are no roles or scopes available', async () => { const jwt = JSON.stringify({ + aud: 'https://graph.microsoft.com', roles: [], scp: '' }); @@ -290,12 +344,13 @@ describe(commands.DOCTOR, () => { nodeVersion: 'v14.17.0', os: { 'platform': 'win32', 'version': 'Windows 10 Pro', 'release': '10.0.19043' }, roles: [], - scopes: [] + scopes: {} })); }); it('retrieves diagnostic information about the current environment with auth type Certificate', async () => { const jwt = JSON.stringify({ + aud: 'https://graph.microsoft.com', roles: ['Sites.Read.All', 'Files.ReadWrite.All'] }); const jwt64 = Buffer.from(jwt).toString('base64'); @@ -325,12 +380,13 @@ describe(commands.DOCTOR, () => { nodeVersion: 'v14.17.0', os: { 'platform': 'win32', 'version': 'Windows 10 Pro', 'release': '10.0.19043' }, roles: ['Sites.Read.All', 'Files.ReadWrite.All'], - scopes: [] + scopes: {} })); }); it('retrieves tenant information as single when TenantID is a GUID', async () => { const jwt = JSON.stringify({ + aud: 'https://graph.microsoft.com', roles: ['Sites.Read.All', 'Files.ReadWrite.All'] }); const jwt64 = Buffer.from(jwt).toString('base64'); @@ -360,12 +416,13 @@ describe(commands.DOCTOR, () => { nodeVersion: 'v14.17.0', os: { 'platform': 'win32', 'version': 'Windows 10 Pro', 'release': '10.0.19043' }, roles: ['Sites.Read.All', 'Files.ReadWrite.All'], - scopes: [] + scopes: {} })); }); it('retrieves diagnostic information about the current environment (debug)', async () => { const jwt = JSON.stringify({ + aud: 'https://graph.microsoft.com', roles: ['Sites.Read.All', 'Files.ReadWrite.All'] }); const jwt64 = Buffer.from(jwt).toString('base64'); @@ -395,12 +452,13 @@ describe(commands.DOCTOR, () => { nodeVersion: 'v14.17.0', os: { 'platform': 'win32', 'version': 'Windows 10 Pro', 'release': '10.0.19043' }, roles: ['Sites.Read.All', 'Files.ReadWrite.All'], - scopes: [] + scopes: {} })); }); it('retrieves diagnostic information of the current environment when executing in docker', async () => { const jwt = JSON.stringify({ + aud: 'https://graph.microsoft.com', roles: ['Sites.Read.All', 'Files.ReadWrite.All'] }); const jwt64 = Buffer.from(jwt).toString('base64'); @@ -430,7 +488,7 @@ describe(commands.DOCTOR, () => { nodeVersion: 'v14.17.0', os: { 'platform': 'win32', 'version': 'Windows 10 Pro', 'release': '10.0.19043' }, roles: ['Sites.Read.All', 'Files.ReadWrite.All'], - scopes: [] + scopes: {} })); }); @@ -459,7 +517,7 @@ describe(commands.DOCTOR, () => { nodeVersion: 'v14.17.0', os: { 'platform': 'win32', 'version': 'Windows 10 Pro', 'release': '10.0.19043' }, roles: [], - scopes: [] + scopes: {} })); }); @@ -489,12 +547,13 @@ describe(commands.DOCTOR, () => { nodeVersion: 'v14.17.0', os: { 'platform': 'win32', 'version': 'Windows 10 Pro', 'release': '10.0.19043' }, roles: [], - scopes: [] + scopes: {} })); }); it('retrieves CLI Configuration in the diagnostic information about the current environment', async () => { const jwt = JSON.stringify({ + aud: 'https://graph.microsoft.com', scp: 'AllSites.FullControl AppCatalog.ReadWrite.All' }); const jwt64 = Buffer.from(jwt).toString('base64'); @@ -528,7 +587,12 @@ describe(commands.DOCTOR, () => { nodeVersion: 'v14.17.0', os: { 'platform': 'win32', 'version': 'Windows 10 Pro', 'release': '10.0.19043' }, roles: [], - scopes: ['AllSites.FullControl', 'AppCatalog.ReadWrite.All'] + scopes: { + 'https://graph.microsoft.com': [ + 'AllSites.FullControl', + 'AppCatalog.ReadWrite.All' + ] + } })); }); }); diff --git a/src/m365/cli/commands/cli-doctor.ts b/src/m365/cli/commands/cli-doctor.ts index ef6d9a27aef..534d9f47736 100644 --- a/src/m365/cli/commands/cli-doctor.ts +++ b/src/m365/cli/commands/cli-doctor.ts @@ -21,7 +21,7 @@ interface CliDiagnosticInfo { cliVersion: string; cliConfig: any; roles: string[]; - scopes: string[]; + scopes: object; } class CliDoctorCommand extends Command { @@ -35,13 +35,16 @@ class CliDoctorCommand extends Command { public async commandAction(logger: Logger): Promise { const roles: string[] = []; - const scopes: string[] = []; + const scopes: Map = new Map(); Object.keys(auth.service.accessTokens).forEach(resource => { const accessToken: string = auth.service.accessTokens[resource].accessToken; this.getRolesFromAccessToken(accessToken).forEach(role => roles.push(role)); - this.getScopesFromAccessToken(accessToken).forEach(scope => scopes.push(scope)); + const [res, scp] = this.getScopesFromAccessToken(accessToken); + if (res !== "") { + scopes.set(res, scp); + } }); const diagnosticInfo: CliDiagnosticInfo = { @@ -58,7 +61,7 @@ class CliDoctorCommand extends Command { cliEnvironment: process.env.CLIMICROSOFT365_ENV ? process.env.CLIMICROSOFT365_ENV : '', cliConfig: Cli.getInstance().config.all, roles: roles, - scopes: scopes + scopes: Object.fromEntries(scopes) }; await logger.log(diagnosticInfo); @@ -85,26 +88,28 @@ class CliDoctorCommand extends Command { return roles; } - private getScopesFromAccessToken(accessToken: string): string[] { + private getScopesFromAccessToken(accessToken: string): [string, string[]] { + let resource: string = ""; let scopes: string[] = []; if (!accessToken || accessToken.length === 0) { - return scopes; + return [resource, scopes]; } const chunks = accessToken.split('.'); if (chunks.length !== 3) { - return scopes; + return [resource, scopes]; } const tokenString: string = Buffer.from(chunks[1], 'base64').toString(); - const token: { scp: string } = JSON.parse(tokenString); + const token: { aud: string, scp: string } = JSON.parse(tokenString); if (token.scp?.length > 0) { + resource = token.aud.replace(/(-my|-admin).sharepoint.com/, '.sharepoint.com'); scopes = token.scp.split(' '); } - return scopes; + return [resource, scopes]; } } From 23a19ebffb243e94cd630158ae191203d458e780 Mon Sep 17 00:00:00 2001 From: unkn0wn-root <54364197+unkn0wn-root@users.noreply.github.com> Date: Tue, 24 Oct 2023 17:12:12 +0200 Subject: [PATCH 03/23] Updates documentation with correct cmder url --- docs/docs/user-guide/completion.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/user-guide/completion.mdx b/docs/docs/user-guide/completion.mdx index 6e70f5c119b..45f9d0c1640 100644 --- a/docs/docs/user-guide/completion.mdx +++ b/docs/docs/user-guide/completion.mdx @@ -9,7 +9,7 @@ To help you use its commands, CLI for Microsoft 365 offers you the ability to au ## Clink (cmder) -On Windows, the CLI for Microsoft 365 offers support for completing commands in [cmder](http://cmder.net) and other shells using [Clink](https://mridgers.github.io/clink/). +On Windows, the CLI for Microsoft 365 offers support for completing commands in [cmder](http://cmder.app) and other shells using [Clink](https://mridgers.github.io/clink/). ### Enable Clink completion From ac53efe394313f87673d2c5fcb639a76bd769b34 Mon Sep 17 00:00:00 2001 From: Ganesh Sanap Date: Tue, 24 Oct 2023 20:26:53 +0530 Subject: [PATCH 04/23] Updates documentation for 'spo field get' --- docs/docs/cmd/spo/field/field-get.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/cmd/spo/field/field-get.mdx b/docs/docs/cmd/spo/field/field-get.mdx index 7d5a06b9264..130e7fc1206 100644 --- a/docs/docs/cmd/spo/field/field-get.mdx +++ b/docs/docs/cmd/spo/field/field-get.mdx @@ -31,7 +31,7 @@ m365 spo field get [options] : The ID of the field to retrieve. Specify `id` or `title` but not both. `-t, --title [title]` -: The display name (case-sensitive) of the field to remove. Specify `id` or `title` but not both. +: The display name (case-sensitive) of the field to retrieve. Specify `id` or `title` but not both. ``` From bcf9db3b827297e63f9e3e156cd834dfe92e2ab7 Mon Sep 17 00:00:00 2001 From: Martin Lingstuyl Date: Wed, 25 Oct 2023 07:56:50 +0200 Subject: [PATCH 05/23] Updates release notes --- docs/docs/about/release-notes.mdx | 9 +++++++++ docs/docs/cmd/aad/group/group-user-list.mdx | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/docs/about/release-notes.mdx b/docs/docs/about/release-notes.mdx index 81593fe6659..bf6a1c778e7 100644 --- a/docs/docs/about/release-notes.mdx +++ b/docs/docs/about/release-notes.mdx @@ -6,6 +6,12 @@ sidebar_position: 4 ## v7.1.0 (beta) +### New commands + +**Azure Active Directory** + +- [aad group user list](../cmd/aad/group/group-user-list.mdx) - lists users of a specific Azure AD group [#5469](https://github.com/pnp/cli-microsoft365/issues/5469) + ### Changes - added new eslint rule: Prevent usage of 'ByServerRelativeUrl' endpoint [#5333](https://github.com/pnp/cli-microsoft365/issues/5333) @@ -24,6 +30,9 @@ sidebar_position: 4 - removed M365 group and connected site. [#5224](https://github.com/pnp/cli-microsoft365/issues/5224) - moved prompt output to stderr. [#5489](https://github.com/pnp/cli-microsoft365/issues/5489) - updated package keywords. [#5433](https://github.com/pnp/cli-microsoft365/issues/5433) +- enhances 'cli doctor' to list scopes by resource. [#5487](https://github.com/pnp/cli-microsoft365/issues/5487) +- updates documentation with correct cmder url +- updates documentation for 'spo field get' ## [v7.0.0](https://github.com/pnp/cli-microsoft365/releases/tag/v7.0.0) diff --git a/docs/docs/cmd/aad/group/group-user-list.mdx b/docs/docs/cmd/aad/group/group-user-list.mdx index 26f2152b90f..210ad8b470f 100644 --- a/docs/docs/cmd/aad/group/group-user-list.mdx +++ b/docs/docs/cmd/aad/group/group-user-list.mdx @@ -35,7 +35,7 @@ m365 aad group user list [options] ## Remarks -When the `properties` option includes values with a `/`, for example: `memberof/id`, an additional `$expand` query parameter will be included on `memberof`. +When the `properties` option includes values with a `/`, for example: `manager/displayName`, an additional `$expand` query parameter will be included on `manager`. ## Examples @@ -61,6 +61,7 @@ List all group users from a group specified by name. For each one return the dis ```sh m365 aad group user list --groupDisplayName Developers --properties "displayName,mail,manager/*" +``` List all group members that are guest users. From 979cc55fbe72233ce32ca92bf37d1fa8e9efb9db Mon Sep 17 00:00:00 2001 From: Martin Lingstuyl Date: Wed, 18 Oct 2023 13:58:17 +0200 Subject: [PATCH 06/23] Fixes bug with authType browser. Closes #5578 --- src/Auth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Auth.ts b/src/Auth.ts index 18e66b9045d..582b81bcfd3 100644 --- a/src/Auth.ts +++ b/src/Auth.ts @@ -361,7 +361,7 @@ export class Auth { // but also stub it for testing /* c8 ignore next 3 */ if (!this._authServer) { - this._authServer = (await import('./AuthServer')).default; + this._authServer = (await import('./AuthServer.js')).default; } (this._authServer as AuthServer).initializeServer(this.service, resource, resolve, reject, logger, debug); From 2c09ab4d7dcb02aeec639451d9cbf2e1726cc9ba Mon Sep 17 00:00:00 2001 From: Waldek Mastykarz Date: Sat, 28 Oct 2023 15:54:19 +0200 Subject: [PATCH 07/23] Stops the spinner for setup prompts. Closes #5598 --- src/m365/commands/setup.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/m365/commands/setup.ts b/src/m365/commands/setup.ts index 95705c895b8..6cffc744786 100644 --- a/src/m365/commands/setup.ts +++ b/src/m365/commands/setup.ts @@ -102,6 +102,9 @@ class SetupCommand extends AnonymousCommand { return; } + // stop the spinner. Fixes #5598 + Cli.getInstance().spinner.stop(); + await logger.logToStderr(`Welcome to the CLI for Microsoft 365 setup!`); await logger.logToStderr(`This command will guide you through the process of configuring the CLI for your needs.`); await logger.logToStderr(`Please, answer the following questions and we'll define a set of settings to best match how you intend to use the CLI.`); @@ -156,6 +159,9 @@ class SetupCommand extends AnonymousCommand { await logger.logToStderr('Configuring settings...'); await logger.logToStderr(''); + // start the spinner. Fixes #5598 + Cli.getInstance().spinner.start(); + await this.configureSettings(settings, false, logger); if (!this.verbose) { From ef9afb2a59c00d605f4fb320234f2a97095c4216 Mon Sep 17 00:00:00 2001 From: Milan Holemans <11723921+milanholemans@users.noreply.github.com> Date: Wed, 18 Oct 2023 13:36:39 +0200 Subject: [PATCH 08/23] Updates contributing script sample docs --- .../script-sample/new-script-sample.mdx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/docs/contribute/script-sample/new-script-sample.mdx b/docs/docs/contribute/script-sample/new-script-sample.mdx index 68865348bc5..bc95f0f2dd2 100644 --- a/docs/docs/contribute/script-sample/new-script-sample.mdx +++ b/docs/docs/contribute/script-sample/new-script-sample.mdx @@ -93,23 +93,31 @@ tags: - libraries --- -# Cool sample to add multiple lists in a sites +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Cool sample to add multiple lists in a site Author: [author name](https://link) Short description of the sample functionality. -=== "PowerShell" + + ```powershell - # powershell script + # Put your PowerShell script here ``` -=== "Bash" + + ```bash - # CLI for Microsoft 365 script + # Put your Bash script here ``` + + + ``` ## Next step From ba486faa247df551f7c261d78c138db9fb67be10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20W=C3=B3jcik?= Date: Sat, 28 Oct 2023 23:52:16 +0200 Subject: [PATCH 09/23] Updates release notes --- docs/docs/about/release-notes.mdx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/docs/about/release-notes.mdx b/docs/docs/about/release-notes.mdx index bf6a1c778e7..b467c17ec61 100644 --- a/docs/docs/about/release-notes.mdx +++ b/docs/docs/about/release-notes.mdx @@ -30,9 +30,12 @@ sidebar_position: 4 - removed M365 group and connected site. [#5224](https://github.com/pnp/cli-microsoft365/issues/5224) - moved prompt output to stderr. [#5489](https://github.com/pnp/cli-microsoft365/issues/5489) - updated package keywords. [#5433](https://github.com/pnp/cli-microsoft365/issues/5433) -- enhances 'cli doctor' to list scopes by resource. [#5487](https://github.com/pnp/cli-microsoft365/issues/5487) -- updates documentation with correct cmder url -- updates documentation for 'spo field get' +- enhanced 'cli doctor' to list scopes by resource. [#5487](https://github.com/pnp/cli-microsoft365/issues/5487) +- updated documentation with correct cmder url +- updated documentation for 'spo field get' +- fixed bug with authType browser. [#5578](https://github.com/pnp/cli-microsoft365/issues/5487) +- updated the spinner for setup prompts. [#5598](https://github.com/pnp/cli-microsoft365/issues/5598) +- updated contributing script sample docs ## [v7.0.0](https://github.com/pnp/cli-microsoft365/releases/tag/v7.0.0) From 54705a958295abd00ae16a395d40bbc644466313 Mon Sep 17 00:00:00 2001 From: Milan Holemans <11723921+milanholemans@users.noreply.github.com> Date: Tue, 31 Oct 2023 19:16:24 +0100 Subject: [PATCH 10/23] Updates version to v7.2.0 --- docs/docs/about/release-notes.mdx | 2 +- npm-shrinkwrap.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/docs/about/release-notes.mdx b/docs/docs/about/release-notes.mdx index b467c17ec61..bfe3a43722a 100644 --- a/docs/docs/about/release-notes.mdx +++ b/docs/docs/about/release-notes.mdx @@ -4,7 +4,7 @@ sidebar_position: 4 # Release notes -## v7.1.0 (beta) +## [v7.1.0](https://github.com/pnp/cli-microsoft365/releases/tag/v7.1.0) ### New commands diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 73e05c5c878..23ed315106e 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "@pnp/cli-microsoft365", - "version": "7.1.0", + "version": "7.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pnp/cli-microsoft365", - "version": "7.1.0", + "version": "7.2.0", "license": "MIT", "dependencies": { "@azure/msal-common": "^13.2.1", diff --git a/package.json b/package.json index e77d60af85a..8b11429a287 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pnp/cli-microsoft365", - "version": "7.1.0", + "version": "7.2.0", "description": "Manage Microsoft 365 and SharePoint Framework projects on any platform", "license": "MIT", "main": "./dist/api.js", From 8fab2342e2556c1c86e95fec5a9938ffd7a74487 Mon Sep 17 00:00:00 2001 From: Nanddeep Nachan Date: Tue, 31 Oct 2023 14:15:43 +0000 Subject: [PATCH 11/23] Adds disambiguation prompt to getGroupIdByDisplayName. Closes #5592 --- .../aad/commands/group/group-remove.spec.ts | 68 +++++++++++++++++ .../commands/group/group-user-list.spec.ts | 72 +++++++++++++++++- .../flow/commands/owner/owner-ensure.spec.ts | 49 ++++++++++++- .../flow/commands/owner/owner-remove.spec.ts | 65 +++++++++++++++++ .../commands/group/group-member-add.spec.ts | 73 ++++++++++++++++++- src/utils/aadGroup.spec.ts | 30 +++++++- src/utils/aadGroup.ts | 4 +- 7 files changed, 356 insertions(+), 5 deletions(-) diff --git a/src/m365/aad/commands/group/group-remove.spec.ts b/src/m365/aad/commands/group/group-remove.spec.ts index a64aecf1e37..4bf8a35f121 100644 --- a/src/m365/aad/commands/group/group-remove.spec.ts +++ b/src/m365/aad/commands/group/group-remove.spec.ts @@ -13,6 +13,8 @@ import { aadGroup } from '../../../../utils/aadGroup.js'; import { Cli } from '../../../../cli/Cli.js'; import { CommandInfo } from '../../../../cli/CommandInfo.js'; import command from './group-remove.js'; +import { settingsNames } from '../../../../settingsNames.js'; +import { formatting } from '../../../../utils/formatting.js'; describe(commands.GROUP_REMOVE, () => { const groupId = '2c1ba4c4-cd9b-4417-832f-92a34bc34b2a'; @@ -22,6 +24,7 @@ describe(commands.GROUP_REMOVE, () => { let logger: Logger; let commandInfo: CommandInfo; let promptOptions: any; + let cli: Cli; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -30,6 +33,7 @@ describe(commands.GROUP_REMOVE, () => { sinon.stub(session, 'getId').returns(''); auth.service.connected = true; commandInfo = Cli.getCommandInfo(command); + cli = Cli.getInstance(); }); beforeEach(() => { @@ -54,8 +58,11 @@ describe(commands.GROUP_REMOVE, () => { afterEach(() => { sinonUtil.restore([ + request.get, request.delete, aadGroup.getGroupIdByDisplayName, + cli.getSettingWithDefaultValue, + Cli.handleMultipleResultsFound, Cli.prompt ]); }); @@ -116,6 +123,7 @@ describe(commands.GROUP_REMOVE, () => { } } }; + sinon.stub(request, 'delete').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/groups/${groupId}`) { throw error; @@ -139,6 +147,66 @@ describe(commands.GROUP_REMOVE, () => { assert(promptIssued); }); + it('handles error when multiple groups with the specified displayName found', async () => { + sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { + if (settingName === settingsNames.prompt) { + return false; + } + + return defaultValue; + }); + + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups?$filter=displayName eq '${formatting.encodeQueryParameter(displayName)}'&$select=id`) { + return { + value: [ + { id: '9b1b1e42-794b-4c71-93ac-5ed92488b67f' }, + { id: '9b1b1e42-794b-4c71-93ac-5ed92488b67g' } + ] + }; + } + + return 'Invalid Request'; + }); + + sinon.stub(request, 'delete').rejects('DELETE request executed'); + + await assert.rejects(command.action(logger, { + options: { + displayName: displayName, + force: true + } + }), new CommandError(`Multiple groups with name 'CLI Test Group' found. Found: 9b1b1e42-794b-4c71-93ac-5ed92488b67f, 9b1b1e42-794b-4c71-93ac-5ed92488b67g.`)); + }); + + it('handles selecting single result when multiple groups with the specified name found and cli is set to prompt', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups?$filter=displayName eq '${formatting.encodeQueryParameter(displayName)}'&$select=id`) { + return { + value: [ + { id: '9b1b1e42-794b-4c71-93ac-5ed92488b67f' }, + { id: '9b1b1e42-794b-4c71-93ac-5ed92488b67g' } + ] + }; + } + + throw 'Invalid request'; + }); + + sinon.stub(Cli, 'handleMultipleResultsFound').resolves({ id: '9b1b1e42-794b-4c71-93ac-5ed92488b67f' }); + + const deleteRequestStub = sinon.stub(request, 'delete').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/9b1b1e42-794b-4c71-93ac-5ed92488b67f`) { + return; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { displayName: displayName, force: true } }); + assert(deleteRequestStub.called); + }); + it('aborts removing group when prompt not confirmed', async () => { const deleteSpy = sinon.stub(request, 'delete').resolves(); diff --git a/src/m365/aad/commands/group/group-user-list.spec.ts b/src/m365/aad/commands/group/group-user-list.spec.ts index c1f7776f14b..62423a424b1 100644 --- a/src/m365/aad/commands/group/group-user-list.spec.ts +++ b/src/m365/aad/commands/group/group-user-list.spec.ts @@ -11,6 +11,8 @@ import { aadGroup } from '../../../../utils/aadGroup.js'; import { pid } from '../../../../utils/pid.js'; import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; +import { settingsNames } from '../../../../settingsNames.js'; +import { formatting } from '../../../../utils/formatting.js'; import commands from '../../commands.js'; import command from './group-user-list.js'; @@ -22,6 +24,7 @@ describe(commands.GROUP_USER_LIST, () => { let logger: Logger; let loggerLogSpy: sinon.SinonSpy; let commandInfo: CommandInfo; + let cli: Cli; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -30,6 +33,7 @@ describe(commands.GROUP_USER_LIST, () => { sinon.stub(session, 'getId').returns(''); auth.service.connected = true; commandInfo = Cli.getCommandInfo(command); + cli = Cli.getInstance(); }); beforeEach(() => { @@ -52,7 +56,9 @@ describe(commands.GROUP_USER_LIST, () => { afterEach(() => { sinonUtil.restore([ request.get, - aadGroup.getGroupIdByDisplayName + aadGroup.getGroupIdByDisplayName, + cli.getSettingWithDefaultValue, + Cli.handleMultipleResultsFound ]); }); @@ -210,6 +216,70 @@ describe(commands.GROUP_USER_LIST, () => { ])); }); + it('handles error when multiple Azure AD groups with the specified displayName found', async () => { + sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { + if (settingName === settingsNames.prompt) { + return false; + } + + return defaultValue; + }); + + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups?$filter=displayName eq '${formatting.encodeQueryParameter(groupDisplayName)}'&$select=id`) { + return { + value: [ + { id: '9b1b1e42-794b-4c71-93ac-5ed92488b67f' }, + { id: '9b1b1e42-794b-4c71-93ac-5ed92488b67g' } + ] + }; + } + + return 'Invalid Request'; + }); + + await assert.rejects(command.action(logger, { + options: { + groupDisplayName: groupDisplayName + } + }), new CommandError(`Multiple groups with name 'CLI Test Group' found. Found: 9b1b1e42-794b-4c71-93ac-5ed92488b67f, 9b1b1e42-794b-4c71-93ac-5ed92488b67g.`)); + }); + + it('handles selecting single result when multiple Azure AD groups with the specified name found and cli is set to prompt', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups?$filter=displayName eq '${formatting.encodeQueryParameter(groupDisplayName)}'&$select=id`) { + return { + value: [ + { id: '9b1b1e42-794b-4c71-93ac-5ed92488b67f' }, + { id: '9b1b1e42-794b-4c71-93ac-5ed92488b67g' } + ] + }; + } + + if (opts.url === `https://graph.microsoft.com/v1.0/groups/9b1b1e42-794b-4c71-93ac-5ed92488b67f/Owners/microsoft.graph.user?$select=id,displayName,userPrincipalName,givenName,surname`) { + return { + "value": [{ "id": "00000000-0000-0000-0000-000000000000", "displayName": "Anne Matthews", "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", "givenName": "Anne", "surname": "Matthews" }] + }; + } + + throw 'Invalid request'; + }); + + sinon.stub(Cli, 'handleMultipleResultsFound').resolves({ id: '9b1b1e42-794b-4c71-93ac-5ed92488b67f' }); + + await command.action(logger, { options: { groupDisplayName: groupDisplayName, role: "Owner" } }); + assert(loggerLogSpy.calledOnceWithExactly([ + { + "id": "00000000-0000-0000-0000-000000000000", + "displayName": "Anne Matthews", + "userPrincipalName": "anne.matthews@contoso.onmicrosoft.com", + "givenName": "Anne", + "surname": "Matthews", + "roles": ["Owner"] + } + ])); + }); + it('correctly lists all members in a Azure AD group', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/groups/2c1ba4c4-cd9b-4417-832f-92a34bc34b2a/Members/microsoft.graph.user?$select=id,displayName,userPrincipalName,givenName,surname`) { diff --git a/src/m365/flow/commands/owner/owner-ensure.spec.ts b/src/m365/flow/commands/owner/owner-ensure.spec.ts index 857f80d1cd6..da2d3b1993f 100644 --- a/src/m365/flow/commands/owner/owner-ensure.spec.ts +++ b/src/m365/flow/commands/owner/owner-ensure.spec.ts @@ -28,6 +28,7 @@ describe(commands.OWNER_ENSURE, () => { let log: string[]; let logger: Logger; let commandInfo: CommandInfo; + let cli: Cli; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -36,6 +37,7 @@ describe(commands.OWNER_ENSURE, () => { sinon.stub(session, 'getId').returns(''); auth.service.connected = true; commandInfo = Cli.getCommandInfo(command); + cli = Cli.getInstance(); }); beforeEach(() => { @@ -55,9 +57,12 @@ describe(commands.OWNER_ENSURE, () => { afterEach(() => { sinonUtil.restore([ + request.get, + request.post, aadGroup.getGroupByDisplayName, aadUser.getUserIdByUpn, - request.post + cli.getSettingWithDefaultValue, + Cli.handleMultipleResultsFound ]); }); @@ -216,6 +221,48 @@ describe(commands.OWNER_ENSURE, () => { assert.deepStrictEqual(postRequestStub.lastCall.args[0].data, requestBody); }); + it('handles selecting single result when multiple groups with the specified name found and cli is set to prompt', async () => { + const requestBody = { + put: [ + { + properties: { + principal: { + id: validGroupId, + type: 'Group' + }, + roleName: 'CanEdit' + } + } + ] + }; + + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups?$filter=displayName eq '${formatting.encodeQueryParameter(validGroupName)}'&$select=id`) { + return { + value: [ + { id: validGroupId }, + { id: '9b1b1e42-794b-4c71-93ac-5ed92488b67g' } + ] + }; + } + + throw 'Invalid request'; + }); + + sinon.stub(Cli, 'handleMultipleResultsFound').resolves({ id: validGroupId }); + + const postRequestStub = sinon.stub(request, 'post').callsFake(async opts => { + if (opts.url === `https://management.azure.com/providers/Microsoft.ProcessSimple/scopes/admin/environments/${formatting.encodeQueryParameter(validEnvironmentName)}/flows/${formatting.encodeQueryParameter(validFlowName)}/modifyPermissions?api-version=2016-11-01`) { + return; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { verbose: true, environmentName: validEnvironmentName, flowName: validFlowName, groupName: validGroupName, roleName: validRoleName, asAdmin: true } }); + assert.deepStrictEqual(postRequestStub.lastCall.args[0].data, requestBody); + }); + it('correctly handles API OData error', async () => { const error = { error: { diff --git a/src/m365/flow/commands/owner/owner-remove.spec.ts b/src/m365/flow/commands/owner/owner-remove.spec.ts index ae8c222e348..ef64132737a 100644 --- a/src/m365/flow/commands/owner/owner-remove.spec.ts +++ b/src/m365/flow/commands/owner/owner-remove.spec.ts @@ -10,6 +10,7 @@ import { telemetry } from '../../../../telemetry.js'; import { aadGroup } from '../../../../utils/aadGroup.js'; import { aadUser } from '../../../../utils/aadUser.js'; import { formatting } from '../../../../utils/formatting.js'; +import { settingsNames } from '../../../../settingsNames.js'; import { pid } from '../../../../utils/pid.js'; import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; @@ -32,6 +33,7 @@ describe(commands.OWNER_REMOVE, () => { let logger: Logger; let commandInfo: CommandInfo; let promptOptions: any; + let cli: Cli; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -40,6 +42,7 @@ describe(commands.OWNER_REMOVE, () => { sinon.stub(session, 'getId').returns(''); auth.service.connected = true; commandInfo = Cli.getCommandInfo(command); + cli = Cli.getInstance(); }); beforeEach(() => { @@ -64,8 +67,11 @@ describe(commands.OWNER_REMOVE, () => { afterEach(() => { sinonUtil.restore([ + request.get, aadGroup.getGroupIdByDisplayName, aadUser.getUserIdByUpn, + cli.getSettingWithDefaultValue, + Cli.handleMultipleResultsFound, Cli.prompt, request.post ]); @@ -140,6 +146,65 @@ describe(commands.OWNER_REMOVE, () => { assert.deepStrictEqual(postStub.lastCall.args[0].data, requestBodyGroup); }); + it('handles error when multiple groups with the specified name found', async () => { + sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { + if (settingName === settingsNames.prompt) { + return false; + } + + return defaultValue; + }); + + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups?$filter=displayName eq '${formatting.encodeQueryParameter(groupName)}'&$select=id`) { + return { + value: [ + { id: '9b1b1e42-794b-4c71-93ac-5ed92488b67f' }, + { id: '9b1b1e42-794b-4c71-93ac-5ed92488b67g' } + ] + }; + } + + return 'Invalid Request'; + }); + + sinon.stub(request, 'post').rejects('POST request executed'); + + await assert.rejects(command.action(logger, { + options: { + verbose: true, environmentName: environmentName, flowName: flowName, groupName: groupName, asAdmin: true, force: true + } + }), new CommandError(`Multiple groups with name 'Test Group' found. Found: 9b1b1e42-794b-4c71-93ac-5ed92488b67f, 9b1b1e42-794b-4c71-93ac-5ed92488b67g.`)); + }); + + it('handles selecting single result when multiple groups with the name found and cli is set to prompt', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups?$filter=displayName eq '${formatting.encodeQueryParameter(groupName)}'&$select=id`) { + return { + value: [ + { id: '9b1b1e42-794b-4c71-93ac-5ed92488b67f' }, + { id: '37a0264d-fea4-4e87-8e5e-e574ff878cf2' } + ] + }; + } + + throw 'Invalid request'; + }); + + sinon.stub(Cli, 'handleMultipleResultsFound').resolves({ id: '37a0264d-fea4-4e87-8e5e-e574ff878cf2' }); + + const postStub = sinon.stub(request, 'post').callsFake(async opts => { + if (opts.url === requestUrlAdmin) { + return; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { verbose: true, environmentName: environmentName, flowName: flowName, groupName: groupName, asAdmin: true, force: true } }); + assert.deepStrictEqual(postStub.lastCall.args[0].data, requestBodyGroup); + }); + it('throws error when no environment found', async () => { const error = { error: { diff --git a/src/m365/spo/commands/group/group-member-add.spec.ts b/src/m365/spo/commands/group/group-member-add.spec.ts index 16e0eb6cfe1..7458e4b6506 100644 --- a/src/m365/spo/commands/group/group-member-add.spec.ts +++ b/src/m365/spo/commands/group/group-member-add.spec.ts @@ -95,7 +95,8 @@ describe(commands.GROUP_MEMBER_ADD, () => { sinonUtil.restore([ request.get, request.post, - cli.getSettingWithDefaultValue + cli.getSettingWithDefaultValue, + Cli.handleMultipleResultsFound ]); }); @@ -565,4 +566,74 @@ describe(commands.GROUP_MEMBER_ADD, () => { } }), new CommandError('The selected permission level is not valid.')); }); + + it('handles error when multiple groups with the specified displayName found', async () => { + sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { + if (settingName === settingsNames.prompt) { + return false; + } + + return defaultValue; + }); + + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups?$filter=displayName eq 'Azure%20AD%20Group%20name'&$select=id`) { + return { + value: [ + { id: '9b1b1e42-794b-4c71-93ac-5ed92488b67f' }, + { id: '9b1b1e42-794b-4c71-93ac-5ed92488b67g' } + ] + }; + } + + return 'Invalid Request'; + }); + + sinon.stub(request, 'post').rejects('POST request executed'); + + await assert.rejects(command.action(logger, { + options: { + webUrl: "https://contoso.sharepoint.com/sites/SiteA", + groupId: 32, + aadGroupNames: "Azure AD Group name" + } + }), new CommandError("Resource 'Azure AD Group name' does not exist.")); + }); + + it('handles selecting single result when multiple groups with the specified name found and cli is set to prompt', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups?$filter=displayName eq 'Azure%20AD%20Group%20name'&$select=id`) { + return { + value: [ + { id: '9b1b1e42-794b-4c71-93ac-5ed92488b67f' }, + { id: '9b1b1e42-794b-4c71-93ac-5ed92488b67g' } + ] + }; + } + + if (opts.url === `https://contoso.sharepoint.com/sites/SiteA/_api/web/sitegroups/GetById('32')?$select=Id`) { + return groupResponse; + } + + throw 'Invalid request'; + }); + + sinon.stub(Cli, 'handleMultipleResultsFound').resolves({ id: '9b1b1e42-794b-4c71-93ac-5ed92488b67f' }); + + sinon.stub(request, 'post').callsFake(async opts => { + if (opts.url === 'https://contoso.sharepoint.com/sites/SiteA/_api/SP.Web.ShareObject' && + opts.data) { + return jsonSingleUser; + } + + throw `Invalid request ${JSON.stringify(opts)}`; + }); + + await command.action(logger, { + options: { + debug: true, webUrl: "https://contoso.sharepoint.com/sites/SiteA", groupId: 32, aadGroupNames: "Azure AD Group name" + } + }); + assert(loggerLogSpy.calledWith(jsonSingleUser.UsersAddedToGroup)); + }); }); diff --git a/src/utils/aadGroup.spec.ts b/src/utils/aadGroup.spec.ts index 3dcf6ace21b..08cca1dca47 100644 --- a/src/utils/aadGroup.spec.ts +++ b/src/utils/aadGroup.spec.ts @@ -146,6 +146,14 @@ describe('utils/aadGroup', () => { }); it('throws error message when multiple groups were found using getGroupIdByDisplayName', async () => { + sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { + if (settingName === settingsNames.prompt) { + return false; + } + + return defaultValue; + }); + sinon.stub(request, 'get').callsFake(async opts => { if (opts.url === `https://graph.microsoft.com/v1.0/groups?$filter=displayName eq '${formatting.encodeQueryParameter(validGroupName)}'&$select=id`) { return { @@ -159,7 +167,27 @@ describe('utils/aadGroup', () => { return 'Invalid Request'; }); - await assert.rejects(aadGroup.getGroupIdByDisplayName(validGroupName), Error(`Multiple groups with name '${validGroupName}' found: ${[validGroupId, validGroupId]}.`)); + await assert.rejects(aadGroup.getGroupIdByDisplayName(validGroupName), Error(`Multiple groups with name 'Group name' found. Found: 00000000-0000-0000-0000-000000000000.`)); + }); + + it('handles selecting single result when multiple groups with the specified name found using getGroupIdByDisplayName and cli is set to prompt', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups?$filter=displayName eq '${formatting.encodeQueryParameter(validGroupName)}'&$select=id`) { + return { + value: [ + { id: validGroupId }, + { id: validGroupId } + ] + }; + } + + return 'Invalid Request'; + }); + + sinon.stub(Cli, 'handleMultipleResultsFound').resolves({ id: validGroupId }); + + const actual = await aadGroup.getGroupIdByDisplayName(validGroupName); + assert.deepStrictEqual(actual, validGroupId); }); it('updates a group to public successfully', async () => { diff --git a/src/utils/aadGroup.ts b/src/utils/aadGroup.ts index 8cedef03367..b2c54a3b464 100644 --- a/src/utils/aadGroup.ts +++ b/src/utils/aadGroup.ts @@ -67,7 +67,9 @@ export const aadGroup = { } if (groups.length > 1) { - throw Error(`Multiple groups with name '${displayName}' found: ${groups.map(x => x.id).join(',')}.`); + const resultAsKeyValuePair = formatting.convertArrayToHashTable('id', groups); + const result = await Cli.handleMultipleResultsFound(`Multiple groups with name '${displayName}' found.`, resultAsKeyValuePair); + return result.id!; } return groups[0].id!; From cf8465a6719c7308f4d6969b70127cad178ad113 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20W=C3=B3jcik?= Date: Sun, 15 Oct 2023 22:09:23 +0200 Subject: [PATCH 12/23] Implement disambiguation prompt in missing places. Closes #5490 --- src/m365/base/AppCommand.spec.ts | 7 +-- src/m365/base/AppCommand.ts | 16 ++---- .../teams/commands/app/app-update.spec.ts | 50 ++++++++++++++++++- src/m365/teams/commands/app/app-update.ts | 15 +++--- 4 files changed, 64 insertions(+), 24 deletions(-) diff --git a/src/m365/base/AppCommand.spec.ts b/src/m365/base/AppCommand.spec.ts index f946f818fad..405af0889f9 100644 --- a/src/m365/base/AppCommand.spec.ts +++ b/src/m365/base/AppCommand.spec.ts @@ -56,7 +56,8 @@ describe('AppCommand', () => { sinonUtil.restore([ fs.existsSync, fs.readFileSync, - Cli.prompt + Cli.prompt, + Cli.handleMultipleResultsFound ]); }); @@ -121,7 +122,7 @@ describe('AppCommand', () => { } ] })); - const cliPromptStub = sinon.stub(Cli, 'prompt').callsFake(async () => ( + const cliPromptStub = sinon.stub(Cli, 'handleMultipleResultsFound').callsFake(async () => ( { appIdIndex: 0 } )); await assert.rejects(cmd.action(logger, { options: {} })); @@ -142,7 +143,7 @@ describe('AppCommand', () => { } ] })); - sinon.stub(Cli, 'prompt').resolves({ appIdIndex: 1 }); + sinon.stub(Cli, 'handleMultipleResultsFound').resolves({ appIdIndex: 1 }); sinon.stub(Command.prototype, 'action').resolves(); try { diff --git a/src/m365/base/AppCommand.ts b/src/m365/base/AppCommand.ts index c84acf9e80b..c278374a996 100644 --- a/src/m365/base/AppCommand.ts +++ b/src/m365/base/AppCommand.ts @@ -5,6 +5,7 @@ import Command, { CommandArgs, CommandError } from '../../Command.js'; import GlobalOptions from '../../GlobalOptions.js'; import { validation } from '../../utils/validation.js'; import { M365RcJson, M365RcJsonApp } from './M365RcJson.js'; +import { formatting } from '../../utils/formatting.js'; export interface AppCommandArgs { options: AppCommandOptions; @@ -89,19 +90,8 @@ export default abstract class AppCommand extends Command { } if (this.m365rcJson.apps.length > 1) { - const result = await Cli.prompt<{ appIdIndex: number }>({ - message: `Multiple Azure AD apps found in ${m365rcJsonPath}. Which app would you like to use?`, - type: 'list', - choices: this.m365rcJson.apps.map((app, i) => { - return { - name: `${app.name} (${app.appId})`, - value: i - }; - }), - default: 0, - name: 'appIdIndex' - }); - + const resultAsKeyValuePair = formatting.convertArrayToHashTable('appIdIndex', this.m365rcJson.apps); + const result = await Cli.handleMultipleResultsFound<{ appIdIndex: number }>(`Multiple Azure AD apps found in ${m365rcJsonPath}.`, resultAsKeyValuePair); this.appId = ((this.m365rcJson as M365RcJson).apps as M365RcJsonApp[])[result.appIdIndex].appId; await super.action(logger, args); } diff --git a/src/m365/teams/commands/app/app-update.spec.ts b/src/m365/teams/commands/app/app-update.spec.ts index 69b1bd2e069..2d85d4b2f9e 100644 --- a/src/m365/teams/commands/app/app-update.spec.ts +++ b/src/m365/teams/commands/app/app-update.spec.ts @@ -53,7 +53,8 @@ describe(commands.APP_UPDATE, () => { request.put, fs.readFileSync, fs.existsSync, - cli.getSettingWithDefaultValue + cli.getSettingWithDefaultValue, + Cli.handleMultipleResultsFound ]); }); @@ -175,6 +176,14 @@ describe(commands.APP_UPDATE, () => { }); it('handles error when multiple Teams apps with the specified name found', async () => { + sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { + if (settingName === settingsNames.prompt) { + return false; + } + + return defaultValue; + }); + sinon.stub(request, 'get').callsFake(async (opts) => { if ((opts.url as string).indexOf(`/v1.0/appCatalogs/teamsApps?$filter=displayName eq '`) > -1) { return { @@ -199,7 +208,44 @@ describe(commands.APP_UPDATE, () => { name: 'Test app', filePath: 'teamsapp.zip' } - } as any), new CommandError('Multiple Teams apps with name Test app found. Please choose one of these ids: e3e29acb-8c79-412b-b746-e6c39ff4cd22, 5b31c38c-2584-42f0-aa47-657fb3a84230')); + } as any), new CommandError('Multiple Teams apps with name Test app found. Found: e3e29acb-8c79-412b-b746-e6c39ff4cd22, 5b31c38c-2584-42f0-aa47-657fb3a84230.')); + }); + + it('handles selecting single result when multiple Teams apps found with the specified name', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if ((opts.url as string).indexOf(`/v1.0/appCatalogs/teamsApps?$filter=displayName eq '`) > -1) { + return { + "value": [ + { + "id": "e3e29acb-8c79-412b-b746-e6c39ff4cd22", + "displayName": "Test app" + }, + { + "id": "5b31c38c-2584-42f0-aa47-657fb3a84230", + "displayName": "Test app" + } + ] + }; + } + throw 'Invalid request'; + }); + + sinon.stub(Cli, 'handleMultipleResultsFound').resolves({ id: '5b31c38c-2584-42f0-aa47-657fb3a84230' }); + + let updateTeamsAppCalled = false; + sinon.stub(request, 'put').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/5b31c38c-2584-42f0-aa47-657fb3a84230`) { + updateTeamsAppCalled = true; + return; + } + + throw 'Invalid request'; + }); + + sinon.stub(fs, 'readFileSync').callsFake(() => '123'); + + await command.action(logger, { options: { filePath: 'teamsapp.zip', name: 'Test app' } }); + assert(updateTeamsAppCalled); }); it('update Teams app in the tenant app catalog by id', async () => { diff --git a/src/m365/teams/commands/app/app-update.ts b/src/m365/teams/commands/app/app-update.ts index d03dff5c737..1f07fd68263 100644 --- a/src/m365/teams/commands/app/app-update.ts +++ b/src/m365/teams/commands/app/app-update.ts @@ -7,6 +7,7 @@ import { formatting } from '../../../../utils/formatting.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; +import { Cli } from '../../../../cli/Cli.js'; interface CommandArgs { options: Options; @@ -89,7 +90,7 @@ class TeamsAppUpdateCommand extends GraphCommand { const { filePath } = args.options; try { - const appId: string = await this.getAppId(args); + const appId: string = await this.getAppId(args.options); const fullPath: string = path.resolve(filePath); if (this.verbose) { await logger.logToStderr(`Updating app with id '${appId}' and file '${fullPath}' in the app catalog...`); @@ -110,13 +111,13 @@ class TeamsAppUpdateCommand extends GraphCommand { } } - private async getAppId(args: CommandArgs): Promise { - if (args.options.id) { - return args.options.id; + private async getAppId(options: Options): Promise { + if (options.id) { + return options.id; } const requestOptions: CliRequestOptions = { - url: `${this.resource}/v1.0/appCatalogs/teamsApps?$filter=displayName eq '${formatting.encodeQueryParameter(args.options.name as string)}'`, + url: `${this.resource}/v1.0/appCatalogs/teamsApps?$filter=displayName eq '${formatting.encodeQueryParameter(options.name as string)}'`, headers: { accept: 'application/json;odata.metadata=none' }, @@ -131,7 +132,9 @@ class TeamsAppUpdateCommand extends GraphCommand { } if (response.value.length > 1) { - throw `Multiple Teams apps with name ${args.options.name} found. Please choose one of these ids: ${response.value.map(x => x.id).join(', ')}`; + const resultAsKeyValuePair = formatting.convertArrayToHashTable('id', response.value); + const result = await Cli.handleMultipleResultsFound<{ id: string; }>(`Multiple Teams apps with name ${options.name} found.`, resultAsKeyValuePair); + return result.id; } return app.id; From b26bab32ccede4835db2c1f7657924eb2be8a592 Mon Sep 17 00:00:00 2001 From: Waldek Mastykarz Date: Sun, 29 Oct 2023 19:17:24 +0100 Subject: [PATCH 13/23] Fixes command sorting. Closes #5606 --- src/cli/Cli.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/cli/Cli.ts b/src/cli/Cli.ts index 763140137c7..bb83e1a8aa9 100644 --- a/src/cli/Cli.ts +++ b/src/cli/Cli.ts @@ -853,9 +853,10 @@ export class Cli { Cli.log(`Commands:`); Cli.log(); - for (const commandName in commandsToPrint) { + const sortedCommandNamesToPrint = Object.getOwnPropertyNames(commandsToPrint).sort(); + sortedCommandNamesToPrint.forEach(commandName => { Cli.log(` ${`${commandName} [options]`.padEnd(maxLength, ' ')} ${commandsToPrint[commandName].command.description}`); - } + }); } const namesOfCommandGroupsToPrint: string[] = Object.keys(commandGroupsToPrint); From 8d3fe74cb3378aa30de132ce16cfa55cde7081f4 Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Mon, 23 Oct 2023 12:20:33 +0200 Subject: [PATCH 14/23] Adds 'aad administrativeunit get' command. Closes #5582 --- .eslintrc.cjs | 2 + .../administrativeunit-get.mdx | 98 ++++++++++ docs/src/config/sidebars.js | 9 + src/m365/aad/commands.ts | 1 + .../administrativeunit-get.spec.ts | 168 ++++++++++++++++++ .../administrativeunit-get.ts | 126 +++++++++++++ 6 files changed, 404 insertions(+) create mode 100644 docs/docs/cmd/aad/administrativeunit/administrativeunit-get.mdx create mode 100644 src/m365/aad/commands/administrativeunit/administrativeunit-get.spec.ts create mode 100644 src/m365/aad/commands/administrativeunit/administrativeunit-get.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 3137e992727..7cda2cdbd03 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -5,6 +5,7 @@ const dictionary = [ 'activation', 'activations', 'adaptive', + 'administrative', 'ai', 'app', 'application', @@ -90,6 +91,7 @@ const dictionary = [ 'threat', 'token', 'type', + 'unit', 'user', 'web', 'webhook' diff --git a/docs/docs/cmd/aad/administrativeunit/administrativeunit-get.mdx b/docs/docs/cmd/aad/administrativeunit/administrativeunit-get.mdx new file mode 100644 index 00000000000..a1880dc3736 --- /dev/null +++ b/docs/docs/cmd/aad/administrativeunit/administrativeunit-get.mdx @@ -0,0 +1,98 @@ +import Global from '/docs/cmd/_global.mdx'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# aad administrativeunit get + +Gets information about a specific administrative unit + +## Usage + +```sh +m365 aad administrativeunit get [options] +``` + +## Options + +```md definition-list +`-i, --id [id]` +: The id of the administrative unit. Specify either `id` or `displayName` but not both. + +`-n, --displayName [displayName]` +: The display name of the administrative unit. Specify either `id` or `displayName` but not both. +``` + + + +## Examples + +Get information about the administrative unit by its id + +```sh +m365 aad administrativeunit get --id 03c4c9dc-6f0c-4c4f-a4e6-0c9ed80f54c7 +``` + +Get information about the administrative unit by its display name + +```sh +m365 aad administrativeunit get --displayName 'Marketing Division' +``` + +## Response + + + + + ```json + { + "id": "0a22c83d-c4ac-43e2-bb5e-87af3015d49f", + "deletedDateTime": null, + "displayName": "Marketing Division", + "description": "Marketing Department Administration", + "membershipRule": null, + "membershipType": null, + "membershipRuleProcessingState": null, + "visibility": "HiddenMembership" + } + ``` + + + + + ```text + deletedDateTime : null + description : Marketing Department Administration + displayName : Marketing Division + id : 0a22c83d-c4ac-43e2-bb5e-87af3015d49f + membershipRule : null + membershipRuleProcessingState: null + membershipType : null + visibility : HiddenMembership + ``` + + + + + ```csv + id,displayName,description,visibility + 0a22c83d-c4ac-43e2-bb5e-87af3015d49f,Marketing Division,Marketing Department Administration,HiddenMembership + ``` + + + + + ```md + Date: 10/23/2023 + + ## Marketing Division (0a22c83d-c4ac-43e2-bb5e-87af3015d49f) + + Property | Value + ---------|------- + id | 0a22c83d-c4ac-43e2-bb5e-87af3015d49f + displayName | Marketing Division + description | Marketing Department Administration + visibility | HiddenMembership + ``` + + + \ No newline at end of file diff --git a/docs/src/config/sidebars.js b/docs/src/config/sidebars.js index d55c8c52e0e..314217ca3ae 100644 --- a/docs/src/config/sidebars.js +++ b/docs/src/config/sidebars.js @@ -26,6 +26,15 @@ const sidebars = { 'cmd/version', { 'Azure Active Directory (aad)': [ + { + administrativeunit: [ + { + type: 'doc', + label: 'administrativeunit get', + id: 'cmd/aad/administrativeunit/administrativeunit-get' + } + ] + }, { app: [ { diff --git a/src/m365/aad/commands.ts b/src/m365/aad/commands.ts index b0586fdc7e3..7dc782826f4 100644 --- a/src/m365/aad/commands.ts +++ b/src/m365/aad/commands.ts @@ -1,6 +1,7 @@ const prefix: string = 'aad'; export default { + ADMINISTRATIVEUNIT_GET: `${prefix} administrativeunit get`, APP_ADD: `${prefix} app add`, APP_GET: `${prefix} app get`, APP_LIST: `${prefix} app list`, diff --git a/src/m365/aad/commands/administrativeunit/administrativeunit-get.spec.ts b/src/m365/aad/commands/administrativeunit/administrativeunit-get.spec.ts new file mode 100644 index 00000000000..dee0c3d6ae7 --- /dev/null +++ b/src/m365/aad/commands/administrativeunit/administrativeunit-get.spec.ts @@ -0,0 +1,168 @@ +import assert from 'assert'; +import sinon from "sinon"; +import auth from '../../../../Auth.js'; +import { CommandInfo } from "../../../../cli/CommandInfo.js"; +import { Logger } from "../../../../cli/Logger.js"; +import commands from "../../commands.js"; +import { telemetry } from '../../../../telemetry.js'; +import { pid } from '../../../../utils/pid.js'; +import { session } from '../../../../utils/session.js'; +import { Cli } from '../../../../cli/Cli.js'; +import command from './administrativeunit-get.js'; +import request from '../../../../request.js'; +import { sinonUtil } from '../../../../utils/sinonUtil.js'; +import { CommandError } from '../../../../Command.js'; +import { formatting } from '../../../../utils/formatting.js'; + +describe(commands.ADMINISTRATIVEUNIT_GET, () => { + let log: string[]; + let logger: Logger; + let loggerLogSpy: sinon.SinonSpy; + let commandInfo: CommandInfo; + const administrativeUnitsReponse = { + value: [ + { + id: 'fc33aa61-cf0e-46b6-9506-f633347202ab', + displayName: 'European Division', + visibility: 'HiddenMembership' + }, + { + id: 'a25b4c5e-e8b7-4f02-a23d-0965b6415098', + displayName: 'Asian Division', + visibility: null + } + ] + }; + const validId = 'fc33aa61-cf0e-46b6-9506-f633347202ab'; + const validDisplayName = 'European Division'; + const invalidDisplayName = 'European'; + + before(() => { + sinon.stub(auth, 'restoreAuth').resolves(); + sinon.stub(telemetry, 'trackEvent').returns(); + sinon.stub(pid, 'getProcessName').returns(''); + sinon.stub(session, 'getId').returns(''); + auth.service.connected = true; + commandInfo = Cli.getCommandInfo(command); + }); + + 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, + Cli.handleMultipleResultsFound + ]); + }); + + after(() => { + sinon.restore(); + auth.service.connected = false; + }); + + it('has correct name', () => { + assert.strictEqual(command.name, commands.ADMINISTRATIVEUNIT_GET); + }); + + it('has a description', () => { + assert.notStrictEqual(command.description, null); + }); + + it('retrieves information about the specified administrative unit by id', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/directory/administrativeUnits/${validId}`) { + return administrativeUnitsReponse.value[0]; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { id: validId } }); + assert(loggerLogSpy.calledOnceWithExactly(administrativeUnitsReponse.value[0])); + }); + + it('retrieves information about the specified administrative unit by displayName', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/directory/administrativeUnits?$filter=displayName eq '${formatting.encodeQueryParameter(validDisplayName)}'`) { + return { + value: [ + administrativeUnitsReponse.value[0] + ] + }; + } + + throw 'Invalid Request'; + }); + + await command.action(logger, { options: { displayName: validDisplayName } }); + assert(loggerLogSpy.calledOnceWithExactly(administrativeUnitsReponse.value[0])); + }); + + it('throws error message when no administrative unit was found by displayName', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/directory/administrativeUnits?$filter=displayName eq '${formatting.encodeQueryParameter(invalidDisplayName)}'`) { + return { value: [] }; + } + + throw 'Invalid Request'; + }); + + await assert.rejects(command.action(logger, { options: { displayName: invalidDisplayName } }), new CommandError(`The specified administrative unit '${invalidDisplayName}' does not exist.`)); + }); + + it('handles selecting single result when multiple administrative units with the specified displayName found and cli is set to prompt', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/directory/administrativeUnits?$filter=displayName eq '${formatting.encodeQueryParameter(validDisplayName)}'`) { + return { + value: [ + administrativeUnitsReponse.value[0], + administrativeUnitsReponse.value[0] + ] + }; + } + + return 'Invalid Request'; + }); + + sinon.stub(Cli, 'handleMultipleResultsFound').resolves({ id: validId, displayName: validDisplayName, visibility: 'HiddenMembership' }); + + await command.action(logger, { options: { displayName: validDisplayName } }); + assert(loggerLogSpy.calledWith(administrativeUnitsReponse.value[0])); + }); + + it('handles random API error', async () => { + const errorMessage = 'Something went wrong'; + sinon.stub(request, 'get').rejects(new Error(errorMessage)); + + await assert.rejects(command.action(logger, { options: { id: validId } }), new CommandError(errorMessage)); + }); + + it('fails validation if the id is not a valid GUID', async () => { + const actual = await command.validate({ options: { id: '123' } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('passes validation if the id is a valid GUID', async () => { + const actual = await command.validate({ options: { id: validId } }, commandInfo); + assert.strictEqual(actual, true); + }); + + it('passes validation if required options specified (displayName)', async () => { + const actual = await command.validate({ options: { displayName: validDisplayName } }, commandInfo); + assert.strictEqual(actual, true); + }); +}); \ No newline at end of file diff --git a/src/m365/aad/commands/administrativeunit/administrativeunit-get.ts b/src/m365/aad/commands/administrativeunit/administrativeunit-get.ts new file mode 100644 index 00000000000..56027c4709b --- /dev/null +++ b/src/m365/aad/commands/administrativeunit/administrativeunit-get.ts @@ -0,0 +1,126 @@ +import { AdministrativeUnit } from "@microsoft/microsoft-graph-types"; +import GlobalOptions from "../../../../GlobalOptions.js"; +import { Logger } from "../../../../cli/Logger.js"; +import { validation } from "../../../../utils/validation.js"; +import request, { CliRequestOptions } from "../../../../request.js"; +import GraphCommand from "../../../base/GraphCommand.js"; +import commands from "../../commands.js"; +import { odata } from "../../../../utils/odata.js"; +import { formatting } from "../../../../utils/formatting.js"; +import { Cli } from "../../../../cli/Cli.js"; + +interface CommandArgs { + options: Options; +} + +export interface Options extends GlobalOptions { + id?: string; + displayName?: string; +} + +class AadAdministrativeUnitGetCommand extends GraphCommand { + public get name(): string { + return commands.ADMINISTRATIVEUNIT_GET; + } + + public get description(): string { + return 'Gets information about a specific administrative unit'; + } + + constructor() { + super(); + + this.#initTelemetry(); + this.#initOptions(); + this.#initValidators(); + this.#initOptionSets(); + this.#initTypes(); + } + + #initTelemetry(): void { + this.telemetry.push((args: CommandArgs) => { + Object.assign(this.telemetryProperties, { + id: typeof args.options.id !== 'undefined', + displayName: typeof args.options.displayName !== 'undefined' + }); + }); + } + + #initOptions(): void { + this.options.unshift( + { + option: '-i, --id [id]' + }, + { + option: '-n, --displayName [displayName]' + } + ); + } + + #initValidators(): void { + this.validators.push( + async (args: CommandArgs) => { + if (args.options.id && !validation.isValidGuid(args.options.id as string)) { + return `${args.options.id} is not a valid GUID`; + } + + return true; + } + ); + } + + #initOptionSets(): void { + this.optionSets.push({ options: ['id', 'displayName'] }); + } + + #initTypes(): void { + this.types.string.push('displayName'); + } + + public async commandAction(logger: Logger, args: CommandArgs): Promise { + let administrativeUnit: AdministrativeUnit; + + try { + if (args.options.id) { + administrativeUnit = await this.getAdministrativeUnitById(args.options.id); + } + else { + administrativeUnit = await this.getAdministrativeUnitByDisplayName(args.options.displayName!); + } + + await logger.log(administrativeUnit); + } + catch (err: any) { + this.handleRejectedODataJsonPromise(err); + } + } + + async getAdministrativeUnitById(id: string): Promise { + const requestOptions: CliRequestOptions = { + url: `${this.resource}/v1.0/directory/administrativeUnits/${id}`, + headers: { + accept: 'application/json;odata.metadata=none' + }, + responseType: 'json' + }; + + return await request.get(requestOptions); + } + + async getAdministrativeUnitByDisplayName(displayName: string): Promise { + const administrativeUnits = await odata.getAllItems(`${this.resource}/v1.0/directory/administrativeUnits?$filter=displayName eq '${formatting.encodeQueryParameter(displayName)}'`); + + if (administrativeUnits.length === 0) { + throw `The specified administrative unit '${displayName}' does not exist.`; + } + + if (administrativeUnits.length > 1) { + const resultAsKeyValuePair = formatting.convertArrayToHashTable('id', administrativeUnits); + return await Cli.handleMultipleResultsFound(`Multiple administrative units with name '${displayName}' found.`, resultAsKeyValuePair); + } + + return administrativeUnits[0]; + } +} + +export default new AadAdministrativeUnitGetCommand(); \ No newline at end of file From b33e06267e8180909f6267dab9e7c27d57e1d091 Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Mon, 23 Oct 2023 12:12:24 +0200 Subject: [PATCH 15/23] Adds 'aad administrativeunit add' command. Closes #5572 --- .../administrativeunit-add.mdx | 115 +++++++++++ docs/src/config/sidebars.js | 5 + src/m365/aad/commands.ts | 1 + .../administrativeunit-add.spec.ts | 192 ++++++++++++++++++ .../administrativeunit-add.ts | 81 ++++++++ 5 files changed, 394 insertions(+) create mode 100644 docs/docs/cmd/aad/administrativeunit/administrativeunit-add.mdx create mode 100644 src/m365/aad/commands/administrativeunit/administrativeunit-add.spec.ts create mode 100644 src/m365/aad/commands/administrativeunit/administrativeunit-add.ts diff --git a/docs/docs/cmd/aad/administrativeunit/administrativeunit-add.mdx b/docs/docs/cmd/aad/administrativeunit/administrativeunit-add.mdx new file mode 100644 index 00000000000..3a87339cda5 --- /dev/null +++ b/docs/docs/cmd/aad/administrativeunit/administrativeunit-add.mdx @@ -0,0 +1,115 @@ +import Global from '/docs/cmd/_global.mdx'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# aad administrativeunit add + +Creates a new administrative unit + +## Usage + +```sh +m365 aad administrativeunit add [options] +``` + +## Options + +```md definition-list +`-n, --displayname ` +: Display name for the administrative unit. + +`-d, --description [description]` +: Description for the administrative unit. + +`--hiddenMembership [hiddenMembership]` +: Indicates whether the administrative unit and its members are hidden. +``` + + + +## Remarks + +:::info + +To use this command you must be either **Global Administrator** or **Privileged Role Administrator**. + +::: + +## Examples + +Create an administrative unit with a specific display name + +```sh +m365 aad administrativeunit add --displayName 'Marketing Division' +``` + +Create an administrative unit with a specific display name and description + +```sh +m365 aad administrativeunit add --displayName 'Marketing Division' --description 'Marketing department administration' +``` + +Create a hidden administrative unit with a specific display name + +```sh +m365 aad administrativeunit add --displayName 'Marketing Division' --hiddenMembership +``` + +## Response + + + + + ```json + { + "id": "00b45a1b-7632-4e94-a3bd-f06aec976d31", + "deletedDateTime": null, + "displayName": "Marketing Division", + "description": "Marketing department administration", + "membershipRule": null, + "membershipType": null, + "membershipRuleProcessingState": null, + "visibility": null + } + ``` + + + + + ```text + deletedDateTime : null + description : Marketing department administration + displayName : Marketing Division + id : 00b45a1b-7632-4e94-a3bd-f06aec976d31 + membershipRule : null + membershipRuleProcessingState: null + membershipType : null + visibility : null + ``` + + + + + ```csv + id,displayName,description,visibility + 00b45a1b-7632-4e94-a3bd-f06aec976d31,Marketing Division,Marketing department administration,HiddenMembership + ``` + + + + + ```md + Date: 10/23/2023 + + ## Marketing Division (00b45a1b-7632-4e94-a3bd-f06aec976d31) + + Property | Value + ---------|------- + id | 00b45a1b-7632-4e94-a3bd-f06aec976d31 + displayName | Marketing Division + description | Marketing department administration + visibility | HiddenMembership + ``` + + + \ No newline at end of file diff --git a/docs/src/config/sidebars.js b/docs/src/config/sidebars.js index 314217ca3ae..025a10c6f97 100644 --- a/docs/src/config/sidebars.js +++ b/docs/src/config/sidebars.js @@ -32,6 +32,11 @@ const sidebars = { type: 'doc', label: 'administrativeunit get', id: 'cmd/aad/administrativeunit/administrativeunit-get' + }, + { + type: 'doc', + label: 'administrativeunit add', + id: 'cmd/aad/administrativeunit/administrativeunit-add' } ] }, diff --git a/src/m365/aad/commands.ts b/src/m365/aad/commands.ts index 7dc782826f4..691dbc04e95 100644 --- a/src/m365/aad/commands.ts +++ b/src/m365/aad/commands.ts @@ -1,6 +1,7 @@ const prefix: string = 'aad'; export default { + ADMINISTRATIVEUNIT_ADD: `${prefix} administrativeunit add`, ADMINISTRATIVEUNIT_GET: `${prefix} administrativeunit get`, APP_ADD: `${prefix} app add`, APP_GET: `${prefix} app get`, diff --git a/src/m365/aad/commands/administrativeunit/administrativeunit-add.spec.ts b/src/m365/aad/commands/administrativeunit/administrativeunit-add.spec.ts new file mode 100644 index 00000000000..e4cbe6c124f --- /dev/null +++ b/src/m365/aad/commands/administrativeunit/administrativeunit-add.spec.ts @@ -0,0 +1,192 @@ +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 commands from '../../commands.js'; +import command from './administrativeunit-add.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 request from '../../../../request.js'; +import { Logger } from '../../../../cli/Logger.js'; +import { CommandError } from '../../../../Command.js'; + +describe(commands.ADMINISTRATIVEUNIT_ADD, () => { + const administrativeUnitReponse: any = { + id: 'fc33aa61-cf0e-46b6-9506-f633347202ab', + displayName: 'European Division', + description: null, + visibility: null + }; + + let log: string[]; + let logger: Logger; + let loggerLogSpy: sinon.SinonSpy; + let commandInfo: CommandInfo; + + before(() => { + sinon.stub(auth, 'restoreAuth').resolves(); + sinon.stub(telemetry, 'trackEvent').returns(); + sinon.stub(pid, 'getProcessName').returns(''); + sinon.stub(session, 'getId').returns(''); + auth.service.connected = true; + commandInfo = Cli.getCommandInfo(command); + }); + + 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'); + (command as any).pollingInterval = 0; + }); + + afterEach(() => { + sinonUtil.restore([ + request.post + ]); + }); + + after(() => { + sinon.restore(); + auth.service.connected = false; + }); + + it('has correct name', () => { + assert.strictEqual(command.name, commands.ADMINISTRATIVEUNIT_ADD); + }); + + it('has a description', () => { + assert.notStrictEqual(command.description, null); + }); + + it('creates an administrative unit with a specific display name', async () => { + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === 'https://graph.microsoft.com/v1.0/directory/administrativeUnits') { + return administrativeUnitReponse; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { displayName: 'European Division' } }); + assert.deepStrictEqual(postStub.lastCall.args[0].data, { + displayName: 'European Division', + description: undefined, + visibility: null + }); + assert(loggerLogSpy.calledOnceWithExactly(administrativeUnitReponse)); + }); + + it('creates an administrative unit with a specific display name and description', async () => { + const privateAdministrativeUnitResponse = { ...administrativeUnitReponse }; + privateAdministrativeUnitResponse.description = 'European Division Administration'; + + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === 'https://graph.microsoft.com/v1.0/directory/administrativeUnits') { + return administrativeUnitReponse; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { displayName: 'European Division', description: 'European Division Administration' } }); + assert.deepStrictEqual(postStub.lastCall.args[0].data, { + displayName: 'European Division', + description: 'European Division Administration', + visibility: null + }); + assert(loggerLogSpy.calledOnceWith(administrativeUnitReponse)); + }); + + it('creates a hidden administrative unit with a specific display name and description', async () => { + const privateAdministrativeUnitResponse = { ...administrativeUnitReponse }; + privateAdministrativeUnitResponse.description = 'European Division Administration'; + privateAdministrativeUnitResponse.visibility = 'HiddenMembership'; + + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === 'https://graph.microsoft.com/v1.0/directory/administrativeUnits') { + return administrativeUnitReponse; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { displayName: 'European Division', description: 'European Division Administration', hiddenMembership: true } }); + assert.deepStrictEqual(postStub.lastCall.args[0].data, { + displayName: 'European Division', + description: 'European Division Administration', + visibility: 'HiddenMembership' + }); + assert(loggerLogSpy.calledOnceWith(administrativeUnitReponse)); + }); + + it('correctly handles API OData error', async () => { + sinon.stub(request, 'post').rejects({ + error: { + 'odata.error': { + code: '-1, InvalidOperationException', + message: { + value: 'Invalid request' + } + } + } + }); + + await assert.rejects(command.action(logger, { options: {} } as any), new CommandError('Invalid request')); + }); + + it('passes validation when only displayName is specified', async () => { + const actual = await command.validate({ options: { displayName: 'European Division' } }, commandInfo); + assert.strictEqual(actual, true); + }); + + it('passes validation when the displayName, description and hiddenMembership are specified', async () => { + const actual = await command.validate({ options: { displayName: 'European Division', description: 'European Division Administration', hiddenMembership: true } }, commandInfo); + assert.strictEqual(actual, true); + }); + + it('supports specifying displayName', () => { + const options = command.options; + let containsOption = false; + options.forEach(o => { + if (o.option.indexOf('--displayName') > -1) { + containsOption = true; + } + }); + assert(containsOption); + }); + + it('supports specifying description', () => { + const options = command.options; + let containsOption = false; + options.forEach(o => { + if (o.option.indexOf('--description') > -1) { + containsOption = true; + } + }); + assert(containsOption); + }); + + it('supports specifying hiddenMembership', () => { + const options = command.options; + let containsOption = false; + options.forEach(o => { + if (o.option.indexOf('--hiddenMembership') > -1) { + containsOption = true; + } + }); + assert(containsOption); + }); +}); \ No newline at end of file diff --git a/src/m365/aad/commands/administrativeunit/administrativeunit-add.ts b/src/m365/aad/commands/administrativeunit/administrativeunit-add.ts new file mode 100644 index 00000000000..ee534f37c89 --- /dev/null +++ b/src/m365/aad/commands/administrativeunit/administrativeunit-add.ts @@ -0,0 +1,81 @@ +import { AdministrativeUnit } from "@microsoft/microsoft-graph-types"; +import GlobalOptions from "../../../../GlobalOptions.js"; +import { Logger } from "../../../../cli/Logger.js"; +import request, { CliRequestOptions } from "../../../../request.js"; +import GraphCommand from "../../../base/GraphCommand.js"; +import commands from "../../commands.js"; + +interface CommandArgs { + options: Options; +} + +interface Options extends GlobalOptions { + displayName: string; + description?: string; + hiddenMembership?: boolean; +} + +class AadAdministrativeUnitAddCommand extends GraphCommand { + public get name(): string { + return commands.ADMINISTRATIVEUNIT_ADD; + } + + public get description(): string { + return 'Creates an administrative unit'; + } + + constructor() { + super(); + + this.#initTelemetry(); + this.#initOptions(); + } + + #initTelemetry(): void { + this.telemetry.push((args: CommandArgs) => { + Object.assign(this.telemetryProperties, { + hiddenMembership: args.options.hiddenMembership + }); + }); + } + + #initOptions(): void { + this.options.unshift( + { + option: '-n, --displayName ' + }, + { + option: '-d, --description [description]' + }, + { + option: '--hiddenMembership [hiddenMembership]' + } + ); + } + + public async commandAction(logger: Logger, args: CommandArgs): Promise { + const requestOptions: CliRequestOptions = { + url: `${this.resource}/v1.0/directory/administrativeUnits`, + headers: { + 'accept': 'application/json;odata.metadata=none' + }, + responseType: 'json', + data: { + description: args.options.description, + displayName: args.options.displayName, + visibility: args.options.hiddenMembership ? 'HiddenMembership' : null + } + }; + + try { + const administrativeUnit = await request.post(requestOptions); + + await logger.log(administrativeUnit); + } + catch (err: any) { + this.handleRejectedODataJsonPromise(err); + } + } +} + +export default new AadAdministrativeUnitAddCommand(); \ No newline at end of file From e5dcdc972963c91dae490058726e33cafef2cbaa Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Fri, 27 Oct 2023 19:50:50 +0200 Subject: [PATCH 16/23] Adds 'aad administrativeunit remove' command. Closes #5595 --- .../administrativeunit-remove.mdx | 52 +++++ docs/src/config/sidebars.js | 25 +- src/m365/aad/commands.ts | 1 + .../administrativeunit-remove.spec.ts | 221 ++++++++++++++++++ .../administrativeunit-remove.ts | 146 ++++++++++++ 5 files changed, 435 insertions(+), 10 deletions(-) create mode 100644 docs/docs/cmd/aad/administrativeunit/administrativeunit-remove.mdx create mode 100644 src/m365/aad/commands/administrativeunit/administrativeunit-remove.spec.ts create mode 100644 src/m365/aad/commands/administrativeunit/administrativeunit-remove.ts diff --git a/docs/docs/cmd/aad/administrativeunit/administrativeunit-remove.mdx b/docs/docs/cmd/aad/administrativeunit/administrativeunit-remove.mdx new file mode 100644 index 00000000000..d40d7ff7bbf --- /dev/null +++ b/docs/docs/cmd/aad/administrativeunit/administrativeunit-remove.mdx @@ -0,0 +1,52 @@ +import Global from '/docs/cmd/_global.mdx'; + +# aad administrativeunit remove + +Removes an administrative unit + +## Usage + +```sh +m365 aad administrativeunit remove [options] +``` + +## options + +```md definition-list +`-i, --id [id]` +: The id of the administrative unit. Specify either `id` or `displayName` but not both. + +`-n, --displayName [displayName]` +: The display name of the administrative unit. Specify either `id` or `displayName` but not both. + +`-f, --force [force]` +: Don't prompt for confirmation. +``` + + + +## Remarks + +:::info + +To use this command you must be either **Global Administrator** or **Privileged Role Administrator**. + +::: + +## Examples + +Remove an administrative unit by its id + +```sh +m365 aad administrativeunit remove --id 03c4c9dc-6f0c-4c4f-a4e6-0c9ed80f54c7 +``` + +Remove an administrative unit by it displayName + +```sh +m365 aad administrativeunit remove --displayName 'Marketing Division' +``` + +## Response + +The command won't return a response on success \ No newline at end of file diff --git a/docs/src/config/sidebars.js b/docs/src/config/sidebars.js index 025a10c6f97..36ba7bc838c 100644 --- a/docs/src/config/sidebars.js +++ b/docs/src/config/sidebars.js @@ -28,16 +28,21 @@ const sidebars = { 'Azure Active Directory (aad)': [ { administrativeunit: [ - { - type: 'doc', - label: 'administrativeunit get', - id: 'cmd/aad/administrativeunit/administrativeunit-get' - }, - { - type: 'doc', - label: 'administrativeunit add', - id: 'cmd/aad/administrativeunit/administrativeunit-add' - } + { + type: 'doc', + label: 'administrativeunit add', + id: 'cmd/aad/administrativeunit/administrativeunit-add' + }, + { + type: 'doc', + label: 'administrativeunit get', + id: 'cmd/aad/administrativeunit/administrativeunit-get' + }, + { + type: 'doc', + label: 'administrativeunit remove', + id: 'cmd/aad/administrativeunit/administrativeunit-remove' + } ] }, { diff --git a/src/m365/aad/commands.ts b/src/m365/aad/commands.ts index 691dbc04e95..96e78cbd286 100644 --- a/src/m365/aad/commands.ts +++ b/src/m365/aad/commands.ts @@ -3,6 +3,7 @@ const prefix: string = 'aad'; export default { ADMINISTRATIVEUNIT_ADD: `${prefix} administrativeunit add`, ADMINISTRATIVEUNIT_GET: `${prefix} administrativeunit get`, + ADMINISTRATIVEUNIT_REMOVE: `${prefix} administrativeunit remove`, APP_ADD: `${prefix} app add`, APP_GET: `${prefix} app get`, APP_LIST: `${prefix} app list`, diff --git a/src/m365/aad/commands/administrativeunit/administrativeunit-remove.spec.ts b/src/m365/aad/commands/administrativeunit/administrativeunit-remove.spec.ts new file mode 100644 index 00000000000..4be8d0d8444 --- /dev/null +++ b/src/m365/aad/commands/administrativeunit/administrativeunit-remove.spec.ts @@ -0,0 +1,221 @@ +import assert from 'assert'; +import sinon from 'sinon'; +import auth from '../../../../Auth.js'; +import commands from '../../commands.js'; +import request from '../../../../request.js'; +import { telemetry } from '../../../../telemetry.js'; +import { Logger } from '../../../../cli/Logger.js'; +import { CommandError } from '../../../../Command.js'; +import { pid } from '../../../../utils/pid.js'; +import { session } from '../../../../utils/session.js'; +import { sinonUtil } from '../../../../utils/sinonUtil.js'; +import { Cli } from '../../../../cli/Cli.js'; +import { CommandInfo } from '../../../../cli/CommandInfo.js'; +import command from './administrativeunit-remove.js'; +import { formatting } from '../../../../utils/formatting.js'; + +describe(commands.ADMINISTRATIVEUNIT_REMOVE, () => { + const administrativeUnitId = 'fc33aa61-cf0e-46b6-9506-f633347202ab'; + const secondAdministrativeUnitId = 'fc33aa61-cf0e-1234-9506-f633347202ab'; + const displayName = 'European Division'; + const invalidDisplayName = 'European'; + + let log: string[]; + let logger: Logger; + let commandInfo: CommandInfo; + let promptOptions: any; + + before(() => { + sinon.stub(auth, 'restoreAuth').resolves(); + sinon.stub(telemetry, 'trackEvent').returns(); + sinon.stub(pid, 'getProcessName').returns(''); + sinon.stub(session, 'getId').returns(''); + auth.service.connected = true; + commandInfo = Cli.getCommandInfo(command); + }); + + beforeEach(() => { + log = []; + logger = { + log: async (msg: string) => { + log.push(msg); + }, + logRaw: async (msg: string) => { + log.push(msg); + }, + logToStderr: async (msg: string) => { + log.push(msg); + } + }; + sinon.stub(Cli, 'prompt').callsFake(async (options: any) => { + promptOptions = options; + return { continue: false }; + }); + promptOptions = undefined; + }); + + afterEach(() => { + sinonUtil.restore([ + request.delete, + request.get, + Cli.handleMultipleResultsFound, + Cli.prompt + ]); + }); + + after(() => { + sinon.restore(); + auth.service.connected = false; + }); + + it('has correct name', () => { + assert.strictEqual(command.name, commands.ADMINISTRATIVEUNIT_REMOVE); + }); + + it('has a description', () => { + assert.notStrictEqual(command.description, null); + }); + + it('removes the specified administrative unit by id without prompting for confirmation', async () => { + const deleteRequestStub = sinon.stub(request, 'delete').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/directory/administrativeUnits/${administrativeUnitId}`) { + return; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { id: administrativeUnitId, force: true } }); + assert(deleteRequestStub.called); + }); + + it('removes the specified administrative unit by displayName while prompting for confirmation', async () => { + const getRequestStub = sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/directory/administrativeUnits?$filter=displayName eq '${formatting.encodeQueryParameter(displayName)}'&$select=id`) { + return { + value: [ + { id: administrativeUnitId } + ] + }; + } + + throw 'Invalid Request'; + }); + + const deleteRequestStub = sinon.stub(request, 'delete').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/directory/administrativeUnits/${administrativeUnitId}`) { + return; + } + + throw 'Invalid request'; + }); + + sinonUtil.restore(Cli.prompt); + sinon.stub(Cli, 'prompt').resolves({ continue: true }); + + await command.action(logger, { options: { displayName: displayName } }); + assert(deleteRequestStub.called); + assert(getRequestStub.called); + }); + + it('removes selected administrative unit when more administrative units with the specified displayName found while prompting for confirmation', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/directory/administrativeUnits?$filter=displayName eq '${formatting.encodeQueryParameter(displayName)}'&$select=id`) { + return { + value: [ + { + id: administrativeUnitId + }, + { + id: secondAdministrativeUnitId + } + ] + }; + } + + throw 'Invalid Request'; + }); + + const deleteRequestStub = sinon.stub(request, 'delete').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/directory/administrativeUnits/${administrativeUnitId}`) { + return; + } + + throw 'Invalid request'; + }); + + sinonUtil.restore(Cli.prompt); + sinon.stub(Cli, 'prompt').resolves({ continue: true }); + sinon.stub(Cli, 'handleMultipleResultsFound').resolves({ id: administrativeUnitId }); + + await command.action(logger, { options: { displayName: displayName } }); + assert(deleteRequestStub.called); + }); + + it('throws an error when administrative unit by id cannot be found', async () => { + const error = { + error: { + code: 'Request_ResourceNotFound', + message: `Resource '${administrativeUnitId}' does not exist or one of its queried reference-property objects are not present.`, + innerError: { + date: '2023-10-27T12:24:36', + 'request-id': 'b7dee9ee-d85b-4e7a-8686-74852cbfd85b', + 'client-request-id': 'b7dee9ee-d85b-4e7a-8686-74852cbfd85b' + } + } + }; + sinon.stub(request, 'delete').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/directory/administrativeUnits/${administrativeUnitId}`) { + throw error; + } + + throw 'Invalid request'; + }); + + await assert.rejects(command.action(logger, { options: { id: administrativeUnitId, force: true } }), + new CommandError(error.error.message)); + }); + + it('prompts before removing the specified administrative unit when confirm option not passed', async () => { + await command.action(logger, { options: { id: administrativeUnitId } }); + let promptIssued = false; + + if (promptOptions && promptOptions.type === 'confirm') { + promptIssued = true; + } + + assert(promptIssued); + }); + + it('aborts removing administrative unit when prompt not confirmed', async () => { + const deleteSpy = sinon.stub(request, 'delete').resolves(); + + await command.action(logger, { options: { id: administrativeUnitId } }); + assert(deleteSpy.notCalled); + }); + + it('fails validation if id is not a valid GUID', async () => { + const actual = await command.validate({ options: { id: 'invalid' } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('passes validation when id is a valid GUID', async () => { + const actual = await command.validate({ options: { id: administrativeUnitId } }, commandInfo); + assert.strictEqual(actual, true); + }); + + it('throws error message when no administrative unit was found by displayName', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/directory/administrativeUnits?$filter=displayName eq '${formatting.encodeQueryParameter(invalidDisplayName)}'&$select=id`) { + return { value: [] }; + } + + throw 'Invalid Request'; + }); + + sinonUtil.restore(Cli.prompt); + sinon.stub(Cli, 'prompt').resolves({ continue: true }); + + await assert.rejects(command.action(logger, { options: { displayName: invalidDisplayName } }), new CommandError(`The specified administrative unit '${invalidDisplayName}' does not exist.`)); + }); +}); \ No newline at end of file diff --git a/src/m365/aad/commands/administrativeunit/administrativeunit-remove.ts b/src/m365/aad/commands/administrativeunit/administrativeunit-remove.ts new file mode 100644 index 00000000000..7d5a4dc47d2 --- /dev/null +++ b/src/m365/aad/commands/administrativeunit/administrativeunit-remove.ts @@ -0,0 +1,146 @@ +import { AdministrativeUnit } from "@microsoft/microsoft-graph-types"; +import GlobalOptions from "../../../../GlobalOptions.js"; +import { Logger } from "../../../../cli/Logger.js"; +import { validation } from "../../../../utils/validation.js"; +import request, { CliRequestOptions } from "../../../../request.js"; +import GraphCommand from "../../../base/GraphCommand.js"; +import commands from "../../commands.js"; +import { odata } from "../../../../utils/odata.js"; +import { formatting } from "../../../../utils/formatting.js"; +import { Cli } from "../../../../cli/Cli.js"; + + +interface CommandArgs { + options: Options; +} + +interface Options extends GlobalOptions { + id?: string; + displayName?: string; + force?: boolean +} + +class AadAdministrativeUnitRemoveCommand extends GraphCommand { + public get name(): string { + return commands.ADMINISTRATIVEUNIT_REMOVE; + } + public get description(): string { + return 'Removes an administrative unit'; + } + + constructor() { + super(); + + this.#initOptions(); + this.#initValidators(); + this.#initOptionSets(); + this.#initTelemetry(); + this.#initTypes(); + } + + #initTelemetry(): void { + this.telemetry.push((args: CommandArgs) => { + Object.assign(this.telemetryProperties, { + id: args.options.id !== 'undefined', + displayName: args.options.displayName !== 'undefined', + force: !!args.options.force + }); + }); + } + + #initOptions(): void { + this.options.unshift( + { + option: '-i, --id [id]' + }, + { + option: '-n, --displayName [displayName]' + }, + { + option: '-f, --force' + } + ); + } + + #initOptionSets(): void { + this.optionSets.push( + { + options: ['id', 'displayName'] + } + ); + } + + #initValidators(): void { + this.validators.push( + async (args: CommandArgs) => { + if (args.options.id && !validation.isValidGuid(args.options.id)) { + return `${args.options.id} is not a valid GUID for option id.`; + } + + return true; + } + ); + } + + #initTypes(): void { + this.types.string.push('id', 'displayName'); + } + + public async commandAction(logger: Logger, args: CommandArgs): Promise { + const removeAdministrativeUnit = async (): Promise => { + try { + let administrativeUnitId = args.options.id; + + if (args.options.displayName) { + administrativeUnitId = await this.getAdministrativeUnitIdByDisplayName(args.options.displayName); + } + + const requestOptions: CliRequestOptions = { + url: `${this.resource}/v1.0/directory/administrativeUnits/${administrativeUnitId}`, + headers: { + accept: 'application/json;odata.metadata=none' + } + }; + + await request.delete(requestOptions); + } + catch (err: any) { + this.handleRejectedODataJsonPromise(err); + } + }; + + if (args.options.force) { + await removeAdministrativeUnit(); + } + else { + const result = await Cli.prompt<{ continue: boolean }>({ + type: 'confirm', + name: 'continue', + default: false, + message: `Are you sure you want to remove administrative unit '${args.options.id || args.options.displayName}'?` + }); + + if (result.continue) { + await removeAdministrativeUnit(); + } + } + } + + async getAdministrativeUnitIdByDisplayName(displayName: string): Promise { + const administrativeUnits = await odata.getAllItems(`${this.resource}/v1.0/directory/administrativeUnits?$filter=displayName eq '${formatting.encodeQueryParameter(displayName)}'&$select=id`); + + if (administrativeUnits.length === 0) { + throw `The specified administrative unit '${displayName}' does not exist.`; + } + + if (administrativeUnits.length > 1) { + const resultAsKeyValuePair = formatting.convertArrayToHashTable('id', administrativeUnits); + const selectedAdministrativeUnit = await Cli.handleMultipleResultsFound(`Multiple administrative units with name '${displayName}' found.`, resultAsKeyValuePair); + return selectedAdministrativeUnit.id!; + } + + return administrativeUnits[0].id!; + } +} + +export default new AadAdministrativeUnitRemoveCommand(); \ No newline at end of file From 09bb8e9047f85b1955ced7a027d2149d97c284bb Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Mon, 16 Oct 2023 13:26:25 +0200 Subject: [PATCH 17/23] Adds 'aad administrativeunit list' command. Closes #5569 --- .../administrativeunit-list.mdx | 83 ++++++++++++ docs/src/config/sidebars.js | 5 + src/m365/aad/commands.ts | 1 + .../administrativeunit-list.spec.ts | 121 ++++++++++++++++++ .../administrativeunit-list.ts | 31 +++++ 5 files changed, 241 insertions(+) create mode 100644 docs/docs/cmd/aad/administrativeunit/administrativeunit-list.mdx create mode 100644 src/m365/aad/commands/administrativeunit/administrativeunit-list.spec.ts create mode 100644 src/m365/aad/commands/administrativeunit/administrativeunit-list.ts diff --git a/docs/docs/cmd/aad/administrativeunit/administrativeunit-list.mdx b/docs/docs/cmd/aad/administrativeunit/administrativeunit-list.mdx new file mode 100644 index 00000000000..fbecf98f286 --- /dev/null +++ b/docs/docs/cmd/aad/administrativeunit/administrativeunit-list.mdx @@ -0,0 +1,83 @@ +import Global from '/docs/cmd/_global.mdx'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# aad administrativeunit list + +Retrieves a list of administrative units + +## Usage + +```sh +m365 aad administrativeunit list [options] +``` + +## Options + + + +## Examples + +Retrieve a list of administrative units + +```sh +m365 aad administrativeunit list +``` + +## Response + + + + + ```json + [ + { + "id": "0a22c83d-c4ac-43e2-bb5e-87af3015d49f", + "deletedDateTime": null, + "displayName": "Marketing Department", + "description": "Marketing Department Administration", + "membershipRule": null, + "membershipType": null, + "membershipRuleProcessingState": null, + "visibility": "HiddenMembership" + } + ] + ``` + + + + + ```text + displayName: 0a22c83d-c4ac-43e2-bb5e-87af3015d49f + id : Marketing Department + visibility : HiddenMembership + ``` + + + + + ```csv + id,displayName,description,visibility + 0a22c83d-c4ac-43e2-bb5e-87af3015d49f,Marketing Department,Marketing Department Administration,HiddenMembership + ``` + + + + + ```md + # aad administrativeunit list + + Date: 2023-10-16 + + ## Marketing Department (0a22c83d-c4ac-43e2-bb5e-87af3015d49f) + + Property | Value + ---------|------- + id | 0a22c83d-c4ac-43e2-bb5e-87af3015d49f + displayName | Marketing Department + description | Marketing Department Administration + visibility | HiddenMembership + ``` + + + \ No newline at end of file diff --git a/docs/src/config/sidebars.js b/docs/src/config/sidebars.js index 36ba7bc838c..0ec3edb862e 100644 --- a/docs/src/config/sidebars.js +++ b/docs/src/config/sidebars.js @@ -38,6 +38,11 @@ const sidebars = { label: 'administrativeunit get', id: 'cmd/aad/administrativeunit/administrativeunit-get' }, + { + type: 'doc', + label: 'administrativeunit list', + id: 'cmd/aad/administrativeunit/administrativeunit-list' + }, { type: 'doc', label: 'administrativeunit remove', diff --git a/src/m365/aad/commands.ts b/src/m365/aad/commands.ts index 96e78cbd286..0274b07dd91 100644 --- a/src/m365/aad/commands.ts +++ b/src/m365/aad/commands.ts @@ -3,6 +3,7 @@ const prefix: string = 'aad'; export default { ADMINISTRATIVEUNIT_ADD: `${prefix} administrativeunit add`, ADMINISTRATIVEUNIT_GET: `${prefix} administrativeunit get`, + ADMINISTRATIVEUNIT_LIST: `${prefix} administrativeunit list`, ADMINISTRATIVEUNIT_REMOVE: `${prefix} administrativeunit remove`, APP_ADD: `${prefix} app add`, APP_GET: `${prefix} app get`, diff --git a/src/m365/aad/commands/administrativeunit/administrativeunit-list.spec.ts b/src/m365/aad/commands/administrativeunit/administrativeunit-list.spec.ts new file mode 100644 index 00000000000..0aadf402be7 --- /dev/null +++ b/src/m365/aad/commands/administrativeunit/administrativeunit-list.spec.ts @@ -0,0 +1,121 @@ +import assert from 'assert'; +import sinon from 'sinon'; +import auth from '../../../../Auth.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 './administrativeunit-list.js'; + +describe(commands.ADMINISTRATIVEUNIT_LIST, () => { + let log: string[]; + let logger: Logger; + let loggerLogSpy: sinon.SinonSpy; + + before(() => { + sinon.stub(auth, 'restoreAuth').resolves(); + sinon.stub(telemetry, 'trackEvent').returns(); + sinon.stub(pid, 'getProcessName').returns(''); + sinon.stub(session, 'getId').returns(''); + auth.service.connected = true; + }); + + 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 + ]); + }); + + after(() => { + sinon.restore(); + auth.service.connected = false; + }); + + it('has correct name', () => { + assert.strictEqual(command.name, commands.ADMINISTRATIVEUNIT_LIST); + }); + + it('has a description', () => { + assert.notStrictEqual(command.description, null); + }); + + it('defines correct properties for the default output', () => { + assert.deepStrictEqual(command.defaultProperties(), ['id', 'displayName', 'visibility']); + }); + + it(`should get a list of administrative units`, async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/directory/administrativeUnits`) { + return { + value: [ + { + id: 'fc33aa61-cf0e-46b6-9506-f633347202ab', + displayName: 'European Division', + visibility: 'HiddenMembership' + }, + { + id: 'a25b4c5e-e8b7-4f02-a23d-0965b6415098', + displayName: 'Asian Division', + visibility: null + } + ] + }; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { + options: {} + }); + + assert( + loggerLogSpy.calledWith([ + { + id: 'fc33aa61-cf0e-46b6-9506-f633347202ab', + displayName: 'European Division', + visibility: 'HiddenMembership' + }, + { + id: 'a25b4c5e-e8b7-4f02-a23d-0965b6415098', + displayName: 'Asian Division', + visibility: null + } + ]) + ); + }); + + it('handles error when retrieving administrative units list failed', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/directory/administrativeUnits`) { + throw { error: { message: 'An error has occurred' } }; + } + throw `Invalid request`; + }); + + await assert.rejects( + command.action(logger, { options: {} } as any), + new CommandError('An error has occurred') + ); + }); +}); \ No newline at end of file diff --git a/src/m365/aad/commands/administrativeunit/administrativeunit-list.ts b/src/m365/aad/commands/administrativeunit/administrativeunit-list.ts new file mode 100644 index 00000000000..fac301097f9 --- /dev/null +++ b/src/m365/aad/commands/administrativeunit/administrativeunit-list.ts @@ -0,0 +1,31 @@ +import { AdministrativeUnit } from '@microsoft/microsoft-graph-types'; +import { Logger } from '../../../../cli/Logger.js'; +import { odata } from '../../../../utils/odata.js'; +import GraphCommand from '../../../base/GraphCommand.js'; +import commands from '../../commands.js'; + +class AadAdministrativeUnitListCommand extends GraphCommand { + public get name(): string { + return commands.ADMINISTRATIVEUNIT_LIST; + } + + public get description(): string { + return 'Retrieves a list of administrative units'; + } + + public defaultProperties(): string[] | undefined { + return ['id', 'displayName', 'visibility']; + } + + public async commandAction(logger: Logger): Promise { + try { + const results = await odata.getAllItems(`${this.resource}/v1.0/directory/administrativeUnits`); + await logger.log(results); + } + catch (err: any) { + this.handleRejectedODataJsonPromise(err); + } + } +} + +export default new AadAdministrativeUnitListCommand(); \ No newline at end of file From 194fd07b489c90ad161f89c2b7e6648d687eef27 Mon Sep 17 00:00:00 2001 From: Martin Lingstuyl Date: Tue, 31 Oct 2023 21:40:57 +0100 Subject: [PATCH 18/23] Updates release notes --- docs/docs/about/release-notes.mdx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/docs/about/release-notes.mdx b/docs/docs/about/release-notes.mdx index bfe3a43722a..4d3fb7fcb2b 100644 --- a/docs/docs/about/release-notes.mdx +++ b/docs/docs/about/release-notes.mdx @@ -4,6 +4,23 @@ sidebar_position: 4 # Release notes +## v7.2.0 (beta) + +### New commands + +**Azure Active Directory** + +- [aad administrativeunit add](../cmd/aad/administrativeunit/administrativeunit-add.mdx) - creates an administrative unit in Azure AD [#5572](https://github.com/pnp/cli-microsoft365/issues/5572) +- [aad administrativeunit get](../cmd/aad/administrativeunit/administrativeunit-get.mdx) - retrieves details for an administrative unit in Azure AD [#5582](https://github.com/pnp/cli-microsoft365/issues/5582) +- [aad administrativeunit list](../cmd/aad/administrativeunit/administrativeunit-list.mdx) - retrieves a list of administrative units in Azure AD [#5569](https://github.com/pnp/cli-microsoft365/issues/5569) +- [aad administrativeunit remove](../cmd/aad/administrativeunit/administrativeunit-remove.mdx) - removes an administrative unit from Azure AD [#5595](https://github.com/pnp/cli-microsoft365/issues/5595) + +### Changes + +- fixes command sorting. [#5606](https://github.com/pnp/cli-microsoft365/issues/5606) +- implements disambiguation prompts in missing places. [#5490](https://github.com/pnp/cli-microsoft365/issues/5490) +- adds a disambiguation prompt to getGroupIdByDisplayName. [#5592](https://github.com/pnp/cli-microsoft365/issues/5592) + ## [v7.1.0](https://github.com/pnp/cli-microsoft365/releases/tag/v7.1.0) ### New commands From ba91bbdf18a8f789152f755430516557eca9b625 Mon Sep 17 00:00:00 2001 From: Martin Lingstuyl Date: Tue, 31 Oct 2023 22:03:41 +0100 Subject: [PATCH 19/23] Adds new contributor --- docs/docs/about/team.mdx | 6 ++++++ package.json | 1 + 2 files changed, 7 insertions(+) diff --git a/docs/docs/about/team.mdx b/docs/docs/about/team.mdx index 381a45ef465..8c59212745b 100644 --- a/docs/docs/about/team.mdx +++ b/docs/docs/about/team.mdx @@ -437,6 +437,12 @@ Do you want to know the curious and bright minds behind the CLI for Microsoft 36 github: 'martinlingstuyl', x: 'martinlingstuyl' }, + { + name: 'Martin Macháček', + company: 'Edhouse', + github: 'MartinM85', + x: '@machym_' + }, { name: 'Mathijs Verbeeck', company: 'Xylos', diff --git a/package.json b/package.json index 8b11429a287..d9fe9b04a42 100644 --- a/package.json +++ b/package.json @@ -173,6 +173,7 @@ "Lengelle, Veronique <25181757+veronicageek@users.noreply.github.com>", "Levert, Sebastien ", "Lingstuyl, Martin ", + "Macháček, Martin ", "Maillot, Michaël ", "Mastykarz, Waldek ", "McDonnell, Kevin ", From d781816e65aada455b901d16e32692034e564559 Mon Sep 17 00:00:00 2001 From: Milan Holemans <11723921+milanholemans@users.noreply.github.com> Date: Tue, 31 Oct 2023 23:08:19 +0100 Subject: [PATCH 20/23] Fixes sample script contribution guide --- .../script-sample/new-script-sample.mdx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/docs/contribute/script-sample/new-script-sample.mdx b/docs/docs/contribute/script-sample/new-script-sample.mdx index bc95f0f2dd2..08de2a3e6f8 100644 --- a/docs/docs/contribute/script-sample/new-script-sample.mdx +++ b/docs/docs/contribute/script-sample/new-script-sample.mdx @@ -86,7 +86,7 @@ All places marked with `<>` are placeholders that should be populated accordingl The sample file should contain a title, a short description, an author, tags, and the script in code blocks. To help you get started you may use the template below. -```md title="index.mdx" +````md title="index.mdx" --- tags: - provisioning @@ -105,20 +105,20 @@ Short description of the sample functionality. - ```powershell - # Put your PowerShell script here - ``` + ```powershell + # Put your PowerShell script here + ``` - ```bash - # Put your Bash script here - ``` + ```bash + # Put your Bash script here + ``` -``` +```` ## Next step From 4b57a7b970f6ab165089278690ae7be270ed0a51 Mon Sep 17 00:00:00 2001 From: Waldek Mastykarz Date: Thu, 26 Oct 2023 20:08:57 +0200 Subject: [PATCH 21/23] Adds support for SPFx v1.18.1.-rc.0. Closes #5593 --- .../docs/cmd/spfx/project/project-upgrade.mdx | 2 +- .../commands/project/project-doctor.spec.ts | 8 + .../spfx/commands/project/project-doctor.ts | 3 +- .../project-doctor/doctor-1.18.1-rc.0.ts | 21 ++ .../project-model/VsCodeSettingsJson.ts | 1 + .../commands/project/project-upgrade.spec.ts | 66 ++++ .../spfx/commands/project/project-upgrade.ts | 29 +- ...10_CODE_settings_filesexclude_jest.spec.ts | 62 +++ ...N014010_CODE_settings_filesexclude_jest.ts | 56 +++ .../project-upgrade/upgrade-1.18.1-rc.0.ts | 57 +++ .../test-projects/spfx-1180-ace/.eslintrc.js | 352 ++++++++++++++++++ .../test-projects/spfx-1180-ace/.gitignore | 34 ++ .../test-projects/spfx-1180-ace/.npmignore | 16 + .../spfx-1180-ace/.vscode/launch.json | 23 ++ .../spfx-1180-ace/.vscode/settings.json | 13 + .../test-projects/spfx-1180-ace/.yo-rc.json | 25 ++ .../test-projects/spfx-1180-ace/README.md | 73 ++++ .../spfx-1180-ace/config/config.json | 18 + .../config/deploy-azure-storage.json | 7 + .../config/package-solution.json | 39 ++ .../spfx-1180-ace/config/sass.json | 3 + .../spfx-1180-ace/config/serve.json | 6 + .../spfx-1180-ace/config/write-manifests.json | 4 + .../test-projects/spfx-1180-ace/gulpfile.js | 16 + .../test-projects/spfx-1180-ace/package.json | 33 ++ ...loWorldAdaptiveCardExtension.manifest.json | 27 ++ .../HelloWorldAdaptiveCardExtension.ts | 53 +++ .../helloWorld/HelloWorldPropertyPane.ts | 23 ++ .../helloWorld/assets/MicrosoftLogo.png | Bin 0 -> 4773 bytes .../helloWorld/cardView/CardView.ts | 49 +++ .../helloWorld/loc/en-us.js | 11 + .../helloWorld/loc/mystring.d.ts | 14 + .../helloWorld/quickView/QuickView.ts | 28 ++ .../quickView/template/QuickViewTemplate.json | 28 ++ .../test-projects/spfx-1180-ace/src/index.ts | 1 + .../test-projects/spfx-1180-ace/tsconfig.json | 35 ++ .../.eslintrc.js | 352 ++++++++++++++++++ .../.gitignore | 34 ++ .../.npmignore | 16 + .../.vscode/launch.json | 23 ++ .../.vscode/settings.json | 13 + .../.yo-rc.json | 25 ++ .../spfx-1180-applicationcustomizer/README.md | 73 ++++ .../config/config.json | 18 + .../config/deploy-azure-storage.json | 7 + .../config/package-solution.json | 45 +++ .../config/sass.json | 3 + .../config/serve.json | 29 ++ .../config/write-manifests.json | 4 + .../gulpfile.js | 16 + .../package.json | 34 ++ .../sharepoint/assets/ClientSideInstance.xml | 9 + .../sharepoint/assets/elements.xml | 9 + ...loWorldApplicationCustomizer.manifest.json | 17 + .../HelloWorldApplicationCustomizer.ts | 39 ++ .../src/extensions/helloWorld/loc/en-us.js | 5 + .../extensions/helloWorld/loc/myStrings.d.ts | 8 + .../src/index.ts | 1 + .../tsconfig.json | 35 ++ .../.eslintrc.js | 352 ++++++++++++++++++ .../.gitignore | 34 ++ .../.npmignore | 16 + .../.vscode/launch.json | 23 ++ .../.vscode/settings.json | 13 + .../.yo-rc.json | 26 ++ .../spfx-1180-fieldcustomizer-react/README.md | 73 ++++ .../config/config.json | 18 + .../config/deploy-azure-storage.json | 7 + .../config/package-solution.json | 44 +++ .../config/sass.json | 3 + .../config/serve.json | 29 ++ .../config/write-manifests.json | 4 + .../gulpfile.js | 16 + .../package.json | 39 ++ .../sharepoint/assets/elements.xml | 12 + .../HelloWorldFieldCustomizer.manifest.json | 17 + .../helloWorld/HelloWorldFieldCustomizer.ts | 54 +++ .../components/HelloWorld.module.scss | 6 + .../helloWorld/components/HelloWorld.tsx | 28 ++ .../src/extensions/helloWorld/loc/en-us.js | 5 + .../extensions/helloWorld/loc/myStrings.d.ts | 8 + .../src/index.ts | 1 + .../tsconfig.json | 35 ++ .../.eslintrc.js | 352 ++++++++++++++++++ .../spfx-1180-formcustomizer-nolib/.gitignore | 34 ++ .../spfx-1180-formcustomizer-nolib/.npmignore | 16 + .../.vscode/launch.json | 23 ++ .../.vscode/settings.json | 13 + .../.yo-rc.json | 26 ++ .../spfx-1180-formcustomizer-nolib/README.md | 73 ++++ .../config/config.json | 18 + .../config/deploy-azure-storage.json | 7 + .../config/package-solution.json | 39 ++ .../config/sass.json | 3 + .../config/serve.json | 53 +++ .../config/write-manifests.json | 4 + .../gulpfile.js | 16 + .../package.json | 35 ++ .../HelloWorldFormCustomizer.manifest.json | 17 + .../HelloWorldFormCustomizer.module.scss | 5 + .../helloWorld/HelloWorldFormCustomizer.ts | 56 +++ .../src/extensions/helloWorld/loc/en-us.js | 7 + .../extensions/helloWorld/loc/myStrings.d.ts | 10 + .../src/index.ts | 1 + .../tsconfig.json | 35 ++ .../.eslintrc.js | 352 ++++++++++++++++++ .../spfx-1180-formcustomizer-react/.gitignore | 34 ++ .../spfx-1180-formcustomizer-react/.npmignore | 16 + .../.vscode/launch.json | 23 ++ .../.vscode/settings.json | 13 + .../.yo-rc.json | 26 ++ .../spfx-1180-formcustomizer-react/README.md | 73 ++++ .../config/config.json | 18 + .../config/deploy-azure-storage.json | 7 + .../config/package-solution.json | 39 ++ .../config/sass.json | 3 + .../config/serve.json | 53 +++ .../config/write-manifests.json | 4 + .../gulpfile.js | 16 + .../package.json | 41 ++ .../HelloWorldFormCustomizer.manifest.json | 17 + .../helloWorld/HelloWorldFormCustomizer.ts | 64 ++++ .../components/HelloWorld.module.scss | 5 + .../helloWorld/components/HelloWorld.tsx | 28 ++ .../src/extensions/helloWorld/loc/en-us.js | 7 + .../extensions/helloWorld/loc/myStrings.d.ts | 10 + .../src/index.ts | 1 + .../tsconfig.json | 35 ++ .../spfx-1180-listviewcommandset/.eslintrc.js | 352 ++++++++++++++++++ .../spfx-1180-listviewcommandset/.gitignore | 34 ++ .../spfx-1180-listviewcommandset/.npmignore | 16 + .../.vscode/launch.json | 23 ++ .../.vscode/settings.json | 13 + .../spfx-1180-listviewcommandset/.yo-rc.json | 25 ++ .../spfx-1180-listviewcommandset/README.md | 73 ++++ .../config/config.json | 18 + .../config/deploy-azure-storage.json | 7 + .../config/package-solution.json | 45 +++ .../config/sass.json | 3 + .../config/serve.json | 31 ++ .../config/write-manifests.json | 4 + .../spfx-1180-listviewcommandset/gulpfile.js | 16 + .../spfx-1180-listviewcommandset/package.json | 34 ++ .../sharepoint/assets/ClientSideInstance.xml | 9 + .../sharepoint/assets/elements.xml | 11 + .../HelloWorldCommandSet.manifest.json | 30 ++ .../helloWorld/HelloWorldCommandSet.ts | 68 ++++ .../src/extensions/helloWorld/loc/en-us.js | 6 + .../extensions/helloWorld/loc/myStrings.d.ts | 9 + .../spfx-1180-listviewcommandset/src/index.ts | 1 + .../tsconfig.json | 35 ++ .../spfx-1180-webpart-nolib/.eslintrc.js | 352 ++++++++++++++++++ .../spfx-1180-webpart-nolib/.gitignore | 34 ++ .../spfx-1180-webpart-nolib/.npmignore | 16 + .../.vscode/launch.json | 23 ++ .../.vscode/settings.json | 13 + .../spfx-1180-webpart-nolib/.yo-rc.json | 25 ++ .../spfx-1180-webpart-nolib/README.md | 73 ++++ .../config/config.json | 18 + .../config/deploy-azure-storage.json | 7 + .../config/package-solution.json | 39 ++ .../spfx-1180-webpart-nolib/config/sass.json | 3 + .../spfx-1180-webpart-nolib/config/serve.json | 6 + .../config/write-manifests.json | 4 + .../spfx-1180-webpart-nolib/gulpfile.js | 16 + .../spfx-1180-webpart-nolib/package.json | 37 ++ .../spfx-1180-webpart-nolib/src/index.ts | 1 + .../HelloWorldWebPart.manifest.json | 28 ++ .../helloWorld/HelloWorldWebPart.module.scss | 34 ++ .../webparts/helloWorld/HelloWorldWebPart.ts | 128 +++++++ .../helloWorld/assets/welcome-dark.png | Bin 0 -> 12545 bytes .../helloWorld/assets/welcome-light.png | Bin 0 -> 12816 bytes .../src/webparts/helloWorld/loc/en-us.js | 16 + .../webparts/helloWorld/loc/mystrings.d.ts | 19 + ...8586-4808-4ff9-8ed2-451c151b74a3_color.png | Bin 0 -> 10248 bytes ...86-4808-4ff9-8ed2-451c151b74a3_outline.png | Bin 0 -> 249 bytes .../spfx-1180-webpart-nolib/tsconfig.json | 35 ++ .../.eslintrc.js | 352 ++++++++++++++++++ .../spfx-1180-webpart-optionaldeps/.gitignore | 34 ++ .../spfx-1180-webpart-optionaldeps/.npmignore | 16 + .../.vscode/launch.json | 23 ++ .../.vscode/settings.json | 13 + .../.yo-rc.json | 25 ++ .../spfx-1180-webpart-optionaldeps/README.md | 73 ++++ .../config/config.json | 18 + .../config/deploy-azure-storage.json | 7 + .../config/package-solution.json | 39 ++ .../config/sass.json | 3 + .../config/serve.json | 6 + .../config/write-manifests.json | 4 + .../gulpfile.js | 16 + .../package.json | 46 +++ .../src/index.ts | 1 + .../HelloWorldWebPart.manifest.json | 28 ++ .../helloWorld/HelloWorldWebPart.module.scss | 34 ++ .../webparts/helloWorld/HelloWorldWebPart.ts | 128 +++++++ .../helloWorld/assets/welcome-dark.png | Bin 0 -> 12545 bytes .../helloWorld/assets/welcome-light.png | Bin 0 -> 12816 bytes .../src/webparts/helloWorld/loc/en-us.js | 16 + .../webparts/helloWorld/loc/mystrings.d.ts | 19 + ...8586-4808-4ff9-8ed2-451c151b74a3_color.png | Bin 0 -> 10248 bytes ...86-4808-4ff9-8ed2-451c151b74a3_outline.png | Bin 0 -> 249 bytes .../tsconfig.json | 35 ++ .../spfx-1181rc0-webpart-react/.eslintrc.js | 352 ++++++++++++++++++ .../spfx-1181rc0-webpart-react/.gitignore | 34 ++ .../spfx-1181rc0-webpart-react/.npmignore | 16 + .../.vscode/launch.json | 23 ++ .../.vscode/settings.json | 14 + .../spfx-1181rc0-webpart-react/.yo-rc.json | 25 ++ .../spfx-1181rc0-webpart-react/README.md | 73 ++++ .../config/config.json | 18 + .../config/deploy-azure-storage.json | 7 + .../config/package-solution.json | 39 ++ .../config/sass.json | 3 + .../config/serve.json | 6 + .../config/write-manifests.json | 4 + .../spfx-1181rc0-webpart-react/gulpfile.js | 16 + .../spfx-1181rc0-webpart-react/package.json | 42 +++ .../spfx-1181rc0-webpart-react/src/index.ts | 1 + .../HelloWorldWebPart.manifest.json | 28 ++ .../webparts/helloWorld/HelloWorldWebPart.ts | 121 ++++++ .../helloWorld/assets/welcome-dark.png | Bin 0 -> 12545 bytes .../helloWorld/assets/welcome-light.png | Bin 0 -> 12816 bytes .../components/HelloWorld.module.scss | 34 ++ .../helloWorld/components/HelloWorld.tsx | 43 +++ .../helloWorld/components/IHelloWorldProps.ts | 7 + .../src/webparts/helloWorld/loc/en-us.js | 16 + .../webparts/helloWorld/loc/mystrings.d.ts | 19 + ...bb8e-5562-4721-8f92-73daad044daf_color.png | Bin 0 -> 10248 bytes ...8e-5562-4721-8f92-73daad044daf_outline.png | Bin 0 -> 249 bytes .../spfx-1181rc0-webpart-react/tsconfig.json | 35 ++ src/m365/spfx/commands/spfx-doctor.ts | 15 + 232 files changed, 8343 insertions(+), 16 deletions(-) create mode 100644 src/m365/spfx/commands/project/project-doctor/doctor-1.18.1-rc.0.ts create mode 100644 src/m365/spfx/commands/project/project-upgrade/rules/FN014010_CODE_settings_filesexclude_jest.spec.ts create mode 100644 src/m365/spfx/commands/project/project-upgrade/rules/FN014010_CODE_settings_filesexclude_jest.ts create mode 100644 src/m365/spfx/commands/project/project-upgrade/upgrade-1.18.1-rc.0.ts create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-ace/.eslintrc.js create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-ace/.gitignore create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-ace/.npmignore create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-ace/.vscode/launch.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-ace/.vscode/settings.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-ace/.yo-rc.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-ace/README.md create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-ace/config/config.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-ace/config/deploy-azure-storage.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-ace/config/package-solution.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-ace/config/sass.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-ace/config/serve.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-ace/config/write-manifests.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-ace/gulpfile.js create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-ace/package.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/HelloWorldAdaptiveCardExtension.manifest.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/HelloWorldAdaptiveCardExtension.ts create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/HelloWorldPropertyPane.ts create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/assets/MicrosoftLogo.png create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/cardView/CardView.ts create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/loc/en-us.js create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/loc/mystring.d.ts create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/quickView/QuickView.ts create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/quickView/template/QuickViewTemplate.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/index.ts create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-ace/tsconfig.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/.eslintrc.js create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/.gitignore create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/.npmignore create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/.vscode/launch.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/.vscode/settings.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/.yo-rc.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/README.md create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/config/config.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/config/deploy-azure-storage.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/config/package-solution.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/config/sass.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/config/serve.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/config/write-manifests.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/gulpfile.js create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/package.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/sharepoint/assets/ClientSideInstance.xml create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/sharepoint/assets/elements.xml create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/src/extensions/helloWorld/HelloWorldApplicationCustomizer.manifest.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/src/extensions/helloWorld/HelloWorldApplicationCustomizer.ts create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/src/extensions/helloWorld/loc/en-us.js create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/src/extensions/helloWorld/loc/myStrings.d.ts create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/src/index.ts create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/tsconfig.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/.eslintrc.js create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/.gitignore create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/.npmignore create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/.vscode/launch.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/.vscode/settings.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/.yo-rc.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/README.md create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/config/config.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/config/deploy-azure-storage.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/config/package-solution.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/config/sass.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/config/serve.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/config/write-manifests.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/gulpfile.js create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/package.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/sharepoint/assets/elements.xml create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/src/extensions/helloWorld/HelloWorldFieldCustomizer.manifest.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/src/extensions/helloWorld/HelloWorldFieldCustomizer.ts create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/src/extensions/helloWorld/components/HelloWorld.module.scss create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/src/extensions/helloWorld/components/HelloWorld.tsx create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/src/extensions/helloWorld/loc/en-us.js create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/src/extensions/helloWorld/loc/myStrings.d.ts create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/src/index.ts create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/tsconfig.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/.eslintrc.js create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/.gitignore create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/.npmignore create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/.vscode/launch.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/.vscode/settings.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/.yo-rc.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/README.md create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/config/config.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/config/deploy-azure-storage.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/config/package-solution.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/config/sass.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/config/serve.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/config/write-manifests.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/gulpfile.js create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/package.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/src/extensions/helloWorld/HelloWorldFormCustomizer.manifest.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/src/extensions/helloWorld/HelloWorldFormCustomizer.module.scss create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/src/extensions/helloWorld/HelloWorldFormCustomizer.ts create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/src/extensions/helloWorld/loc/en-us.js create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/src/extensions/helloWorld/loc/myStrings.d.ts create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/src/index.ts create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/tsconfig.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/.eslintrc.js create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/.gitignore create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/.npmignore create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/.vscode/launch.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/.vscode/settings.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/.yo-rc.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/README.md create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/config/config.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/config/deploy-azure-storage.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/config/package-solution.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/config/sass.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/config/serve.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/config/write-manifests.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/gulpfile.js create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/package.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/src/extensions/helloWorld/HelloWorldFormCustomizer.manifest.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/src/extensions/helloWorld/HelloWorldFormCustomizer.ts create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/src/extensions/helloWorld/components/HelloWorld.module.scss create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/src/extensions/helloWorld/components/HelloWorld.tsx create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/src/extensions/helloWorld/loc/en-us.js create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/src/extensions/helloWorld/loc/myStrings.d.ts create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/src/index.ts create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/tsconfig.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/.eslintrc.js create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/.gitignore create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/.npmignore create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/.vscode/launch.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/.vscode/settings.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/.yo-rc.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/README.md create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/config/config.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/config/deploy-azure-storage.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/config/package-solution.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/config/sass.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/config/serve.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/config/write-manifests.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/gulpfile.js create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/package.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/sharepoint/assets/ClientSideInstance.xml create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/sharepoint/assets/elements.xml create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/src/extensions/helloWorld/HelloWorldCommandSet.manifest.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/src/extensions/helloWorld/HelloWorldCommandSet.ts create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/src/extensions/helloWorld/loc/en-us.js create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/src/extensions/helloWorld/loc/myStrings.d.ts create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/src/index.ts create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/tsconfig.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/.eslintrc.js create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/.gitignore create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/.npmignore create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/.vscode/launch.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/.vscode/settings.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/.yo-rc.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/README.md create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/config/config.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/config/deploy-azure-storage.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/config/package-solution.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/config/sass.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/config/serve.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/config/write-manifests.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/gulpfile.js create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/package.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/src/index.ts create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/src/webparts/helloWorld/HelloWorldWebPart.manifest.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/src/webparts/helloWorld/HelloWorldWebPart.module.scss create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/src/webparts/helloWorld/HelloWorldWebPart.ts create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/src/webparts/helloWorld/assets/welcome-dark.png create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/src/webparts/helloWorld/assets/welcome-light.png create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/src/webparts/helloWorld/loc/en-us.js create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/src/webparts/helloWorld/loc/mystrings.d.ts create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/teams/f4e48586-4808-4ff9-8ed2-451c151b74a3_color.png create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/teams/f4e48586-4808-4ff9-8ed2-451c151b74a3_outline.png create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/tsconfig.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/.eslintrc.js create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/.gitignore create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/.npmignore create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/.vscode/launch.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/.vscode/settings.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/.yo-rc.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/README.md create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/config/config.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/config/deploy-azure-storage.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/config/package-solution.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/config/sass.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/config/serve.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/config/write-manifests.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/gulpfile.js create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/package.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/src/index.ts create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/src/webparts/helloWorld/HelloWorldWebPart.manifest.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/src/webparts/helloWorld/HelloWorldWebPart.module.scss create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/src/webparts/helloWorld/HelloWorldWebPart.ts create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/src/webparts/helloWorld/assets/welcome-dark.png create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/src/webparts/helloWorld/assets/welcome-light.png create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/src/webparts/helloWorld/loc/en-us.js create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/src/webparts/helloWorld/loc/mystrings.d.ts create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/teams/f4e48586-4808-4ff9-8ed2-451c151b74a3_color.png create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/teams/f4e48586-4808-4ff9-8ed2-451c151b74a3_outline.png create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/tsconfig.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/.eslintrc.js create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/.gitignore create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/.npmignore create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/.vscode/launch.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/.vscode/settings.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/.yo-rc.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/README.md create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/config/config.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/config/deploy-azure-storage.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/config/package-solution.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/config/sass.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/config/serve.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/config/write-manifests.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/gulpfile.js create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/package.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/index.ts create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/HelloWorldWebPart.manifest.json create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/HelloWorldWebPart.ts create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/assets/welcome-dark.png create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/assets/welcome-light.png create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/components/HelloWorld.module.scss create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/components/HelloWorld.tsx create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/components/IHelloWorldProps.ts create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/loc/en-us.js create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/loc/mystrings.d.ts create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/teams/21f4bb8e-5562-4721-8f92-73daad044daf_color.png create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/teams/21f4bb8e-5562-4721-8f92-73daad044daf_outline.png create mode 100644 src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/tsconfig.json diff --git a/docs/docs/cmd/spfx/project/project-upgrade.mdx b/docs/docs/cmd/spfx/project/project-upgrade.mdx index d783e1984ae..9e68ca66c1f 100644 --- a/docs/docs/cmd/spfx/project/project-upgrade.mdx +++ b/docs/docs/cmd/spfx/project/project-upgrade.mdx @@ -35,7 +35,7 @@ m365 spfx project upgrade [options] ## Remarks -The `spfx project upgrade` command helps you upgrade your SharePoint Framework project to the specified version. If no version is specified, the command will upgrade to the latest version of the SharePoint Framework it supports (v1.18.0). +The `spfx project upgrade` command helps you upgrade your SharePoint Framework project to the specified version. If no version is specified, the command will upgrade to the latest version of the SharePoint Framework it supports (v1.18.1-rc.0). This command doesn't change your project files. Instead, it gives you a report with all steps necessary to upgrade your project to the specified version of the SharePoint Framework. Changing project files is error-prone, especially when it comes to updating your solution's code. This is why at this moment, this command produces a report that you can use yourself to perform the necessary updates and verify that everything is working as expected. diff --git a/src/m365/spfx/commands/project/project-doctor.spec.ts b/src/m365/spfx/commands/project/project-doctor.spec.ts index 217c043bdad..ccb55b849ad 100644 --- a/src/m365/spfx/commands/project/project-doctor.spec.ts +++ b/src/m365/spfx/commands/project/project-doctor.spec.ts @@ -552,6 +552,14 @@ describe(commands.PROJECT_DOCTOR, () => { assert.strictEqual(findings.length, 0); }); + it('e2e: shows correct number of findings for a valid 1.18.1-rc.0 project', async () => { + sinon.stub(command as any, 'getProjectRoot').callsFake(_ => path.join(process.cwd(), 'src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react')); + + await command.action(logger, { options: {} } as any); + const findings: FindingToReport[] = log[0]; + assert.strictEqual(findings.length, 0); + }); + it('passes validation when package manager not specified', async () => { const actual = await command.validate({ options: {} }, commandInfo); assert.strictEqual(actual, true); diff --git a/src/m365/spfx/commands/project/project-doctor.ts b/src/m365/spfx/commands/project/project-doctor.ts index fa841df3184..1f957604b09 100644 --- a/src/m365/spfx/commands/project/project-doctor.ts +++ b/src/m365/spfx/commands/project/project-doctor.ts @@ -71,7 +71,8 @@ class SpfxProjectDoctorCommand extends BaseProjectCommand { '1.17.2', '1.17.3', '1.17.4', - '1.18.0' + '1.18.0', + '1.18.1-rc.0' ]; protected get allowedOutputs(): string[] { diff --git a/src/m365/spfx/commands/project/project-doctor/doctor-1.18.1-rc.0.ts b/src/m365/spfx/commands/project/project-doctor/doctor-1.18.1-rc.0.ts new file mode 100644 index 00000000000..40691a5087f --- /dev/null +++ b/src/m365/spfx/commands/project/project-doctor/doctor-1.18.1-rc.0.ts @@ -0,0 +1,21 @@ +import { FN001008_DEP_react } from './rules/FN001008_DEP_react.js'; +import { FN001009_DEP_react_dom } from './rules/FN001009_DEP_react_dom.js'; +import { FN001035_DEP_fluentui_react } from './rules/FN001035_DEP_fluentui_react.js'; +import { FN002004_DEVDEP_gulp } from './rules/FN002004_DEVDEP_gulp.js'; +import { FN002007_DEVDEP_ajv } from './rules/FN002007_DEVDEP_ajv.js'; +import { FN002013_DEVDEP_types_webpack_env } from './rules/FN002013_DEVDEP_types_webpack_env.js'; +import { FN002015_DEVDEP_types_react } from './rules/FN002015_DEVDEP_types_react.js'; +import { FN002016_DEVDEP_types_react_dom } from './rules/FN002016_DEVDEP_types_react_dom.js'; +import { FN002019_DEVDEP_microsoft_rush_stack_compiler } from './rules/FN002019_DEVDEP_microsoft_rush_stack_compiler.js'; + +export default [ + new FN001008_DEP_react('17'), + new FN001009_DEP_react_dom('17'), + new FN001035_DEP_fluentui_react('^8.106.4'), + new FN002004_DEVDEP_gulp('4.0.2'), + new FN002007_DEVDEP_ajv('^6.12.5'), + new FN002013_DEVDEP_types_webpack_env('~1.15.2'), + new FN002015_DEVDEP_types_react('17'), + new FN002016_DEVDEP_types_react_dom('17'), + new FN002019_DEVDEP_microsoft_rush_stack_compiler(['4.7']) +]; \ No newline at end of file diff --git a/src/m365/spfx/commands/project/project-model/VsCodeSettingsJson.ts b/src/m365/spfx/commands/project/project-model/VsCodeSettingsJson.ts index 98052cc6eaf..93c1200a445 100644 --- a/src/m365/spfx/commands/project/project-model/VsCodeSettingsJson.ts +++ b/src/m365/spfx/commands/project/project-model/VsCodeSettingsJson.ts @@ -1,6 +1,7 @@ import { JsonFile } from "."; export interface VsCodeSettingsJson extends JsonFile { + "files.exclude"?: { [key: string]: boolean }; "json.schemas"?: VsCodeSettingsJsonJsonSchema[]; } diff --git a/src/m365/spfx/commands/project/project-upgrade.spec.ts b/src/m365/spfx/commands/project/project-upgrade.spec.ts index 645f4f10057..004e5d00b53 100644 --- a/src/m365/spfx/commands/project/project-upgrade.spec.ts +++ b/src/m365/spfx/commands/project/project-upgrade.spec.ts @@ -3121,6 +3121,72 @@ describe(commands.PROJECT_UPGRADE, () => { }); //#endregion + //#region 1.18.0 + it('e2e: shows correct number of findings for upgrading ace 1.18.0 project to 1.18.1-rc.0', async () => { + sinon.stub(command as any, 'getProjectRoot').callsFake(_ => path.join(process.cwd(), 'src/m365/spfx/commands/project/test-projects/spfx-1180-ace')); + + await command.action(logger, { options: { toVersion: '1.18.1-rc.0', preview: true, output: 'json' } } as any); + const findings: FindingToReport[] = log[0]; + assert.strictEqual(findings.length, 10); + }); + + it('e2e: shows correct number of findings for upgrading application customizer 1.18.0 project to 1.18.1-rc.0', async () => { + sinon.stub(command as any, 'getProjectRoot').callsFake(_ => path.join(process.cwd(), 'src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer')); + + await command.action(logger, { options: { toVersion: '1.18.1-rc.0', preview: true, output: 'json' } } as any); + const findings: FindingToReport[] = log[0]; + assert.strictEqual(findings.length, 12); + }); + + it('e2e: shows correct number of findings for upgrading field customizer react 1.18.0 project to 1.18.1-rc.0', async () => { + sinon.stub(command as any, 'getProjectRoot').callsFake(_ => path.join(process.cwd(), 'src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react')); + + await command.action(logger, { options: { toVersion: '1.18.1-rc.0', preview: true, output: 'json' } } as any); + const findings: FindingToReport[] = log[0]; + assert.strictEqual(findings.length, 11); + }); + + it('e2e: shows correct number of findings for upgrading form customizer react 1.18.0 project to 1.18.1-rc.0', async () => { + sinon.stub(command as any, 'getProjectRoot').callsFake(_ => path.join(process.cwd(), 'src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react')); + + await command.action(logger, { options: { toVersion: '1.18.1-rc.0', preview: true, output: 'json' } } as any); + const findings: FindingToReport[] = log[0]; + assert.strictEqual(findings.length, 13); + }); + + it('e2e: shows correct number of findings for upgrading list view command set 1.18.0 project to 1.18.1-rc.0', async () => { + sinon.stub(command as any, 'getProjectRoot').callsFake(_ => path.join(process.cwd(), 'src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset')); + + await command.action(logger, { options: { toVersion: '1.18.1-rc.0', preview: true, output: 'json' } } as any); + const findings: FindingToReport[] = log[0]; + assert.strictEqual(findings.length, 12); + }); + + it('e2e: shows correct number of findings for upgrading no framework web part 1.18.0 project to 1.18.1-rc.0', async () => { + sinon.stub(command as any, 'getProjectRoot').callsFake(_ => path.join(process.cwd(), 'src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib')); + + await command.action(logger, { options: { toVersion: '1.18.1-rc.0', preview: true, output: 'json' } } as any); + const findings: FindingToReport[] = log[0]; + assert.strictEqual(findings.length, 14); + }); + + it('e2e: shows correct number of findings for upgrading react web part 1.18.0 project to 1.18.1-rc.0', async () => { + sinon.stub(command as any, 'getProjectRoot').callsFake(_ => path.join(process.cwd(), 'src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-react')); + + await command.action(logger, { options: { toVersion: '1.18.1-rc.0', preview: true, output: 'json' } } as any); + const findings: FindingToReport[] = log[0]; + assert.strictEqual(findings.length, 14); + }); + + it('e2e: shows correct number of findings for upgrading web part with optional dependencies 1.18.0 project to 1.18.1-rc.0', async () => { + sinon.stub(command as any, 'getProjectRoot').callsFake(_ => path.join(process.cwd(), 'src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps')); + + await command.action(logger, { options: { toVersion: '1.18.1-rc.0', preview: true, output: 'json' } } as any); + const findings: FindingToReport[] = log[0]; + assert.strictEqual(findings.length, 23); + }); + //#endregion + //#region superseded rules it('ignores superseded findings (1.1.0 > 1.2.0)', async () => { sinon.stub(command as any, 'getProjectRoot').returns(path.join(process.cwd(), 'src/m365/spfx/commands/project/test-projects/spfx-110-webpart-react')); diff --git a/src/m365/spfx/commands/project/project-upgrade.ts b/src/m365/spfx/commands/project/project-upgrade.ts index e62583f5a6f..9eaf2dcfeb8 100644 --- a/src/m365/spfx/commands/project/project-upgrade.ts +++ b/src/m365/spfx/commands/project/project-upgrade.ts @@ -2,7 +2,7 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; // uncomment to support upgrading to preview releases -// import { prerelease } from 'semver'; +import { prerelease } from 'semver'; import { Logger } from '../../../../cli/Logger.js'; import Command, { CommandError } from '../../../../Command.js'; import GlobalOptions from '../../../../GlobalOptions.js'; @@ -76,7 +76,8 @@ class SpfxProjectUpgradeCommand extends BaseProjectCommand { '1.17.2', '1.17.3', '1.17.4', - '1.18.0' + '1.18.0', + '1.18.1-rc.0' ]; public static ERROR_NO_PROJECT_ROOT_FOLDER: number = 1; @@ -114,9 +115,9 @@ class SpfxProjectUpgradeCommand extends BaseProjectCommand { preview: args.options.preview }); // uncomment to support upgrading to preview releases - // if (prerelease(this.telemetryProperties.toVersion) && !args.options.preview) { - // this.telemetryProperties.toVersion = this.supportedVersions[this.supportedVersions.length - 2]; - // } + if (prerelease(this.telemetryProperties.toVersion) && !args.options.preview) { + this.telemetryProperties.toVersion = this.supportedVersions[this.supportedVersions.length - 2]; + } }); } @@ -173,15 +174,15 @@ class SpfxProjectUpgradeCommand extends BaseProjectCommand { this.toVersion = args.options.toVersion ? args.options.toVersion : this.supportedVersions[this.supportedVersions.length - 1]; // uncomment to support upgrading to preview releases - // if (!args.options.toVersion && - // !args.options.preview && - // prerelease(this.toVersion)) { - // // no version and no preview specified while the current version to - // // upgrade to is a prerelease so let's grab the first non-preview version - // // since we're supporting only one preview version, it's sufficient for - // // us to take second to last version - // this.toVersion = this.supportedVersions[this.supportedVersions.length - 2]; - // } + if (!args.options.toVersion && + !args.options.preview && + prerelease(this.toVersion)) { + // no version and no preview specified while the current version to + // upgrade to is a prerelease so let's grab the first non-preview version + // since we're supporting only one preview version, it's sufficient for + // us to take second to last version + this.toVersion = this.supportedVersions[this.supportedVersions.length - 2]; + } this.packageManager = args.options.packageManager || 'npm'; this.shell = args.options.shell || 'bash'; diff --git a/src/m365/spfx/commands/project/project-upgrade/rules/FN014010_CODE_settings_filesexclude_jest.spec.ts b/src/m365/spfx/commands/project/project-upgrade/rules/FN014010_CODE_settings_filesexclude_jest.spec.ts new file mode 100644 index 00000000000..3a223d176d0 --- /dev/null +++ b/src/m365/spfx/commands/project/project-upgrade/rules/FN014010_CODE_settings_filesexclude_jest.spec.ts @@ -0,0 +1,62 @@ +import assert from 'assert'; +import fs from 'fs'; +import { sinonUtil } from '../../../../../../utils/sinonUtil.js'; +import { Project } from '../../project-model'; +import { Finding } from '../../report-model/Finding.js'; +import { FN014010_CODE_settings_filesexclude_jest } from './FN014010_CODE_settings_filesexclude_jest.js'; + +describe('FN014010_CODE_settings_filesexclude_jest', () => { + let findings: Finding[]; + let rule: FN014010_CODE_settings_filesexclude_jest; + afterEach(() => { + sinonUtil.restore(fs.existsSync); + }); + + beforeEach(() => { + findings = []; + rule = new FN014010_CODE_settings_filesexclude_jest(); + }); + + it(`doesn't return notifications if vscode folder doesn't exist`, () => { + const project: Project = { + path: '/usr/tmp' + }; + rule.visit(project, findings); + assert.strictEqual(findings.length, 0); + }); + + it(`doesn't return notifications if vscode settings file doesn't exist`, () => { + const project: Project = { + path: '/usr/tmp', + vsCode: {} + }; + rule.visit(project, findings); + assert.strictEqual(findings.length, 0); + }); + + it(`doesn't return notifications if vscode settings file doesn't contain file exclusion rules`, () => { + const project: Project = { + path: '/usr/tmp', + vsCode: { + settingsJson: {} + } + }; + rule.visit(project, findings); + assert.strictEqual(findings.length, 0); + }); + + it(`doesn't return notifications if vscode settings file already excludes jest output files`, () => { + const project: Project = { + path: '/usr/tmp', + vsCode: { + settingsJson: { + "files.exclude": { + "**/jest-output": true + } + } + } + }; + rule.visit(project, findings); + assert.strictEqual(findings.length, 0); + }); +}); \ No newline at end of file diff --git a/src/m365/spfx/commands/project/project-upgrade/rules/FN014010_CODE_settings_filesexclude_jest.ts b/src/m365/spfx/commands/project/project-upgrade/rules/FN014010_CODE_settings_filesexclude_jest.ts new file mode 100644 index 00000000000..c4901a2b85f --- /dev/null +++ b/src/m365/spfx/commands/project/project-upgrade/rules/FN014010_CODE_settings_filesexclude_jest.ts @@ -0,0 +1,56 @@ +import { JsonRule } from '../../JsonRule.js'; +import { Project } from '../../project-model/index.js'; +import { Finding } from '../../report-model/index.js'; + +export class FN014010_CODE_settings_filesexclude_jest extends JsonRule { + constructor() { + super(); + } + + get id(): string { + return 'FN014010'; + } + + get title(): string { + return 'Exclude Jest output files in .vscode/settings.json'; + } + + get description(): string { + return `Add excluding Jest output files in .vscode/settings.json`; + } + + get resolution(): string { + return `{ + "files.exclude": { + "**/jest-output": true + } +}`; + } + + get resolutionType(): string { + return 'json'; + } + + get severity(): string { + return 'Required'; + } + + get file(): string { + return '.vscode/settings.json'; + } + + visit(project: Project, findings: Finding[]): void { + if (!project.vsCode || + !project.vsCode.settingsJson || + !project.vsCode.settingsJson["files.exclude"]) { + return; + } + + if (project.vsCode.settingsJson["files.exclude"]["**/jest-output"] === true) { + return; + } + + const node = this.getAstNodeFromFile(project.vsCode.settingsJson, `files;#exclude`); + this.addFindingWithPosition(findings, node); + } +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/project-upgrade/upgrade-1.18.1-rc.0.ts b/src/m365/spfx/commands/project/project-upgrade/upgrade-1.18.1-rc.0.ts new file mode 100644 index 00000000000..653f05e3bc9 --- /dev/null +++ b/src/m365/spfx/commands/project/project-upgrade/upgrade-1.18.1-rc.0.ts @@ -0,0 +1,57 @@ +import { FN001001_DEP_microsoft_sp_core_library } from './rules/FN001001_DEP_microsoft_sp_core_library.js'; +import { FN001002_DEP_microsoft_sp_lodash_subset } from './rules/FN001002_DEP_microsoft_sp_lodash_subset.js'; +import { FN001003_DEP_microsoft_sp_office_ui_fabric_core } from './rules/FN001003_DEP_microsoft_sp_office_ui_fabric_core.js'; +import { FN001004_DEP_microsoft_sp_webpart_base } from './rules/FN001004_DEP_microsoft_sp_webpart_base.js'; +import { FN001011_DEP_microsoft_sp_dialog } from './rules/FN001011_DEP_microsoft_sp_dialog.js'; +import { FN001012_DEP_microsoft_sp_application_base } from './rules/FN001012_DEP_microsoft_sp_application_base.js'; +import { FN001013_DEP_microsoft_decorators } from './rules/FN001013_DEP_microsoft_decorators.js'; +import { FN001014_DEP_microsoft_sp_listview_extensibility } from './rules/FN001014_DEP_microsoft_sp_listview_extensibility.js'; +import { FN001021_DEP_microsoft_sp_property_pane } from './rules/FN001021_DEP_microsoft_sp_property_pane.js'; +import { FN001022_DEP_office_ui_fabric_react } from './rules/FN001022_DEP_office_ui_fabric_react.js'; +import { FN001023_DEP_microsoft_sp_component_base } from './rules/FN001023_DEP_microsoft_sp_component_base.js'; +import { FN001024_DEP_microsoft_sp_diagnostics } from './rules/FN001024_DEP_microsoft_sp_diagnostics.js'; +import { FN001025_DEP_microsoft_sp_dynamic_data } from './rules/FN001025_DEP_microsoft_sp_dynamic_data.js'; +import { FN001026_DEP_microsoft_sp_extension_base } from './rules/FN001026_DEP_microsoft_sp_extension_base.js'; +import { FN001027_DEP_microsoft_sp_http } from './rules/FN001027_DEP_microsoft_sp_http.js'; +import { FN001028_DEP_microsoft_sp_list_subscription } from './rules/FN001028_DEP_microsoft_sp_list_subscription.js'; +import { FN001029_DEP_microsoft_sp_loader } from './rules/FN001029_DEP_microsoft_sp_loader.js'; +import { FN001030_DEP_microsoft_sp_module_interfaces } from './rules/FN001030_DEP_microsoft_sp_module_interfaces.js'; +import { FN001031_DEP_microsoft_sp_odata_types } from './rules/FN001031_DEP_microsoft_sp_odata_types.js'; +import { FN001032_DEP_microsoft_sp_page_context } from './rules/FN001032_DEP_microsoft_sp_page_context.js'; +import { FN001034_DEP_microsoft_sp_adaptive_card_extension_base } from './rules/FN001034_DEP_microsoft_sp_adaptive_card_extension_base.js'; +import { FN002001_DEVDEP_microsoft_sp_build_web } from './rules/FN002001_DEVDEP_microsoft_sp_build_web.js'; +import { FN002002_DEVDEP_microsoft_sp_module_interfaces } from './rules/FN002002_DEVDEP_microsoft_sp_module_interfaces.js'; +import { FN002022_DEVDEP_microsoft_eslint_plugin_spfx } from './rules/FN002022_DEVDEP_microsoft_eslint_plugin_spfx.js'; +import { FN002023_DEVDEP_microsoft_eslint_config_spfx } from './rules/FN002023_DEVDEP_microsoft_eslint_config_spfx.js'; +import { FN010001_YORC_version } from './rules/FN010001_YORC_version.js'; +import { FN014010_CODE_settings_filesexclude_jest } from './rules/FN014010_CODE_settings_filesexclude_jest.js'; + +export default [ + new FN001001_DEP_microsoft_sp_core_library('1.18.1-rc.0'), + new FN001002_DEP_microsoft_sp_lodash_subset('1.18.1-rc.0'), + new FN001003_DEP_microsoft_sp_office_ui_fabric_core('1.18.1-rc.0'), + new FN001004_DEP_microsoft_sp_webpart_base('1.18.1-rc.0'), + new FN001011_DEP_microsoft_sp_dialog('1.18.1-rc.0'), + new FN001012_DEP_microsoft_sp_application_base('1.18.1-rc.0'), + new FN001014_DEP_microsoft_sp_listview_extensibility('1.18.1-rc.0'), + new FN001021_DEP_microsoft_sp_property_pane('1.18.1-rc.0'), + new FN001022_DEP_office_ui_fabric_react('', false), + new FN001023_DEP_microsoft_sp_component_base('1.18.1-rc.0'), + new FN001024_DEP_microsoft_sp_diagnostics('1.18.1-rc.0'), + new FN001025_DEP_microsoft_sp_dynamic_data('1.18.1-rc.0'), + new FN001026_DEP_microsoft_sp_extension_base('1.18.1-rc.0'), + new FN001027_DEP_microsoft_sp_http('1.18.1-rc.0'), + new FN001028_DEP_microsoft_sp_list_subscription('1.18.1-rc.0'), + new FN001029_DEP_microsoft_sp_loader('1.18.1-rc.0'), + new FN001030_DEP_microsoft_sp_module_interfaces('1.18.1-rc.0'), + new FN001031_DEP_microsoft_sp_odata_types('1.18.1-rc.0'), + new FN001032_DEP_microsoft_sp_page_context('1.18.1-rc.0'), + new FN001013_DEP_microsoft_decorators('1.18.1-rc.0'), + new FN001034_DEP_microsoft_sp_adaptive_card_extension_base('1.18.1-rc.0'), + new FN002001_DEVDEP_microsoft_sp_build_web('1.18.1-rc.0'), + new FN002002_DEVDEP_microsoft_sp_module_interfaces('1.18.1-rc.0'), + new FN002022_DEVDEP_microsoft_eslint_plugin_spfx('1.18.1-rc.0'), + new FN002023_DEVDEP_microsoft_eslint_config_spfx('1.18.1-rc.0'), + new FN010001_YORC_version('1.18.1-rc.0'), + new FN014010_CODE_settings_filesexclude_jest() +]; diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/.eslintrc.js b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/.eslintrc.js new file mode 100644 index 00000000000..ef68d0e95c0 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/.eslintrc.js @@ -0,0 +1,352 @@ +require('@rushstack/eslint-config/patch/modern-module-resolution'); +module.exports = { + extends: ['@microsoft/eslint-config-spfx/lib/profiles/default'], + parserOptions: { tsconfigRootDir: __dirname }, + overrides: [ + { + files: ['*.ts', '*.tsx'], + parser: '@typescript-eslint/parser', + 'parserOptions': { + 'project': './tsconfig.json', + 'ecmaVersion': 2018, + 'sourceType': 'module' + }, + rules: { + // Prevent usage of the JavaScript null value, while allowing code to access existing APIs that may require null. https://www.npmjs.com/package/@rushstack/eslint-plugin + '@rushstack/no-new-null': 1, + // Require Jest module mocking APIs to be called before any other statements in their code block. https://www.npmjs.com/package/@rushstack/eslint-plugin + '@rushstack/hoist-jest-mock': 1, + // Require regular expressions to be constructed from string constants rather than dynamically building strings at runtime. https://www.npmjs.com/package/@rushstack/eslint-plugin-security + '@rushstack/security/no-unsafe-regexp': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/adjacent-overload-signatures': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // + // CONFIGURATION: By default, these are banned: String, Boolean, Number, Object, Symbol + '@typescript-eslint/ban-types': [ + 1, + { + 'extendDefaults': false, + 'types': { + 'String': { + 'message': 'Use \'string\' instead', + 'fixWith': 'string' + }, + 'Boolean': { + 'message': 'Use \'boolean\' instead', + 'fixWith': 'boolean' + }, + 'Number': { + 'message': 'Use \'number\' instead', + 'fixWith': 'number' + }, + 'Object': { + 'message': 'Use \'object\' instead, or else define a proper TypeScript type:' + }, + 'Symbol': { + 'message': 'Use \'symbol\' instead', + 'fixWith': 'symbol' + }, + 'Function': { + 'message': 'The \'Function\' type accepts any function-like value.\nIt provides no type safety when calling the function, which can be a common source of bugs.\nIt also accepts things like class declarations, which will throw at runtime as they will not be called with \'new\'.\nIf you are expecting the function to accept certain arguments, you should explicitly define the function shape.' + } + } + } + ], + // RATIONALE: Code is more readable when the type of every variable is immediately obvious. + // Even if the compiler may be able to infer a type, this inference will be unavailable + // to a person who is reviewing a GitHub diff. This rule makes writing code harder, + // but writing code is a much less important activity than reading it. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/explicit-function-return-type': [ + 1, + { + 'allowExpressions': true, + 'allowTypedFunctionExpressions': true, + 'allowHigherOrderFunctions': false + } + ], + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Rationale to disable: although this is a recommended rule, it is up to dev to select coding style. + // Set to 1 (warning) or 2 (error) to enable. + '@typescript-eslint/explicit-member-accessibility': 0, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-array-constructor': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // + // RATIONALE: The "any" keyword disables static type checking, the main benefit of using TypeScript. + // This rule should be suppressed only in very special cases such as JSON.stringify() + // where the type really can be anything. Even if the type is flexible, another type + // may be more appropriate such as "unknown", "{}", or "Record". + '@typescript-eslint/no-explicit-any': 1, + // RATIONALE: The #1 rule of promises is that every promise chain must be terminated by a catch() + // handler. Thus wherever a Promise arises, the code must either append a catch handler, + // or else return the object to a caller (who assumes this responsibility). Unterminated + // promise chains are a serious issue. Besides causing errors to be silently ignored, + // they can also cause a NodeJS process to terminate unexpectedly. + '@typescript-eslint/no-floating-promises': 2, + // RATIONALE: Catches a common coding mistake. + '@typescript-eslint/no-for-in-array': 2, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-misused-new': 2, + // RATIONALE: The "namespace" keyword is not recommended for organizing code because JavaScript lacks + // a "using" statement to traverse namespaces. Nested namespaces prevent certain bundler + // optimizations. If you are declaring loose functions/variables, it's better to make them + // static members of a class, since classes support property getters and their private + // members are accessible by unit tests. Also, the exercise of choosing a meaningful + // class name tends to produce more discoverable APIs: for example, search+replacing + // the function "reverse()" is likely to return many false matches, whereas if we always + // write "Text.reverse()" is more unique. For large scale organization, it's recommended + // to decompose your code into separate NPM packages, which ensures that component + // dependencies are tracked more conscientiously. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-namespace': [ + 1, + { + 'allowDeclarations': false, + 'allowDefinitionFiles': false + } + ], + // RATIONALE: Parameter properties provide a shorthand such as "constructor(public title: string)" + // that avoids the effort of declaring "title" as a field. This TypeScript feature makes + // code easier to write, but arguably sacrifices readability: In the notes for + // "@typescript-eslint/member-ordering" we pointed out that fields are central to + // a class's design, so we wouldn't want to bury them in a constructor signature + // just to save some typing. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Set to 1 (warning) or 2 (error) to enable the rule + '@typescript-eslint/parameter-properties': 0, + // RATIONALE: When left in shipping code, unused variables often indicate a mistake. Dead code + // may impact performance. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-unused-vars': [ + 1, + { + 'vars': 'all', + // Unused function arguments often indicate a mistake in JavaScript code. However in TypeScript code, + // the compiler catches most of those mistakes, and unused arguments are fairly common for type signatures + // that are overriding a base class method or implementing an interface. + 'args': 'none' + } + ], + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-use-before-define': [ + 2, + { + 'functions': false, + 'classes': true, + 'variables': true, + 'enums': true, + 'typedefs': true + } + ], + // Disallows require statements except in import statements. + // In other words, the use of forms such as var foo = require("foo") are banned. Instead use ES6 style imports or import foo = require("foo") imports. + '@typescript-eslint/no-var-requires': 'error', + // RATIONALE: The "module" keyword is deprecated except when describing legacy libraries. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/prefer-namespace-keyword': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Rationale to disable: it's up to developer to decide if he wants to add type annotations + // Set to 1 (warning) or 2 (error) to enable the rule + '@typescript-eslint/no-inferrable-types': 0, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Rationale to disable: declaration of empty interfaces may be helpful for generic types scenarios + '@typescript-eslint/no-empty-interface': 0, + // RATIONALE: This rule warns if setters are defined without getters, which is probably a mistake. + 'accessor-pairs': 1, + // RATIONALE: In TypeScript, if you write x["y"] instead of x.y, it disables type checking. + 'dot-notation': [ + 1, + { + 'allowPattern': '^_' + } + ], + // RATIONALE: Catches code that is likely to be incorrect + 'eqeqeq': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'for-direction': 1, + // RATIONALE: Catches a common coding mistake. + 'guard-for-in': 2, + // RATIONALE: If you have more than 2,000 lines in a single source file, it's probably time + // to split up your code. + 'max-lines': ['warn', { max: 2000 }], + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-async-promise-executor': 2, + // RATIONALE: Deprecated language feature. + 'no-caller': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-compare-neg-zero': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-cond-assign': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-constant-condition': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-control-regex': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-debugger': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-delete-var': 2, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-duplicate-case': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-empty': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-empty-character-class': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-empty-pattern': 1, + // RATIONALE: Eval is a security concern and a performance concern. + 'no-eval': 1, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-ex-assign': 2, + // RATIONALE: System types are global and should not be tampered with in a scalable code base. + // If two different libraries (or two versions of the same library) both try to modify + // a type, only one of them can win. Polyfills are acceptable because they implement + // a standardized interoperable contract, but polyfills are generally coded in plain + // JavaScript. + 'no-extend-native': 1, + // Disallow unnecessary labels + 'no-extra-label': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-fallthrough': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-func-assign': 1, + // RATIONALE: Catches a common coding mistake. + 'no-implied-eval': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-invalid-regexp': 2, + // RATIONALE: Catches a common coding mistake. + 'no-label-var': 2, + // RATIONALE: Eliminates redundant code. + 'no-lone-blocks': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-misleading-character-class': 2, + // RATIONALE: Catches a common coding mistake. + 'no-multi-str': 2, + // RATIONALE: It's generally a bad practice to call "new Thing()" without assigning the result to + // a variable. Either it's part of an awkward expression like "(new Thing()).doSomething()", + // or else implies that the constructor is doing nontrivial computations, which is often + // a poor class design. + 'no-new': 1, + // RATIONALE: Obsolete language feature that is deprecated. + 'no-new-func': 2, + // RATIONALE: Obsolete language feature that is deprecated. + 'no-new-object': 2, + // RATIONALE: Obsolete notation. + 'no-new-wrappers': 1, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-octal': 2, + // RATIONALE: Catches code that is likely to be incorrect + 'no-octal-escape': 2, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-regex-spaces': 2, + // RATIONALE: Catches a common coding mistake. + 'no-return-assign': 2, + // RATIONALE: Security risk. + 'no-script-url': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-self-assign': 2, + // RATIONALE: Catches a common coding mistake. + 'no-self-compare': 2, + // RATIONALE: This avoids statements such as "while (a = next(), a && a.length);" that use + // commas to create compound expressions. In general code is more readable if each + // step is split onto a separate line. This also makes it easier to set breakpoints + // in the debugger. + 'no-sequences': 1, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-shadow-restricted-names': 2, + // RATIONALE: Obsolete language feature that is deprecated. + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-sparse-arrays': 2, + // RATIONALE: Although in theory JavaScript allows any possible data type to be thrown as an exception, + // such flexibility adds pointless complexity, by requiring every catch block to test + // the type of the object that it receives. Whereas if catch blocks can always assume + // that their object implements the "Error" contract, then the code is simpler, and + // we generally get useful additional information like a call stack. + 'no-throw-literal': 2, + // RATIONALE: Catches a common coding mistake. + 'no-unmodified-loop-condition': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-unsafe-finally': 2, + // RATIONALE: Catches a common coding mistake. + 'no-unused-expressions': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-unused-labels': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-useless-catch': 1, + // RATIONALE: Avoids a potential performance problem. + 'no-useless-concat': 1, + // RATIONALE: The "var" keyword is deprecated because of its confusing "hoisting" behavior. + // Always use "let" or "const" instead. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + 'no-var': 2, + // RATIONALE: Generally not needed in modern code. + 'no-void': 1, + // RATIONALE: Obsolete language feature that is deprecated. + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-with': 2, + // RATIONALE: Makes logic easier to understand, since constants always have a known value + // @typescript-eslint\eslint-plugin\dist\configs\eslint-recommended.js + 'prefer-const': 1, + // RATIONALE: Catches a common coding mistake where "resolve" and "reject" are confused. + 'promise/param-names': 2, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'require-atomic-updates': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'require-yield': 1, + // "Use strict" is redundant when using the TypeScript compiler. + 'strict': [ + 2, + 'never' + ], + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'use-isnan': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + // Set to 1 (warning) or 2 (error) to enable. + // Rationale to disable: !!{} + 'no-extra-boolean-cast': 0, + // ==================================================================== + // @microsoft/eslint-plugin-spfx + // ==================================================================== + '@microsoft/spfx/import-requires-chunk-name': 1, + '@microsoft/spfx/no-require-ensure': 2, + '@microsoft/spfx/pair-react-dom-render-unmount': 1 + } + }, + { + // For unit tests, we can be a little bit less strict. The settings below revise the + // defaults specified in the extended configurations, as well as above. + files: [ + // Test files + '*.test.ts', + '*.test.tsx', + '*.spec.ts', + '*.spec.tsx', + + // Facebook convention + '**/__mocks__/*.ts', + '**/__mocks__/*.tsx', + '**/__tests__/*.ts', + '**/__tests__/*.tsx', + + // Microsoft convention + '**/test/*.ts', + '**/test/*.tsx' + ], + rules: {} + } + ] +}; \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/.gitignore b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/.gitignore new file mode 100644 index 00000000000..51ca7b9e7a7 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/.gitignore @@ -0,0 +1,34 @@ +# Logs +logs +*.log +npm-debug.log* + +# Dependency directories +node_modules + +# Build generated files +dist +lib +release +solution +temp +*.sppkg +.heft + +# Coverage directory used by tools like istanbul +coverage + +# OSX +.DS_Store + +# Visual Studio files +.ntvs_analysis.dat +.vs +bin +obj + +# Resx Generated Code +*.resx.ts + +# Styles Generated Code +*.scss.ts diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/.npmignore b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/.npmignore new file mode 100644 index 00000000000..ae0b487c075 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/.npmignore @@ -0,0 +1,16 @@ +!dist +config + +gulpfile.js + +release +src +temp + +tsconfig.json +tslint.json + +*.log + +.yo-rc.json +.vscode diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/.vscode/launch.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/.vscode/launch.json new file mode 100644 index 00000000000..53ca22cd853 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Hosted workbench", + "type": "msedge", + "request": "launch", + "url": "https://{tenantDomain}/_layouts/workbench.aspx", + "webRoot": "${workspaceRoot}", + "sourceMaps": true, + "sourceMapPathOverrides": { + "webpack:///.././src/*": "${webRoot}/src/*", + "webpack:///../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../../src/*": "${webRoot}/src/*" + }, + "runtimeArgs": [ + "--remote-debugging-port=9222", + "-incognito" + ] + } + ] +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/.vscode/settings.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/.vscode/settings.json new file mode 100644 index 00000000000..a31a2c33223 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/.vscode/settings.json @@ -0,0 +1,13 @@ +// Place your settings in this file to overwrite default and user settings. +{ + // Configure glob patterns for excluding files and folders in the file explorer. + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/bower_components": true, + "**/coverage": true, + "**/lib-amd": true, + "src/**/*.scss.ts": true + }, + "typescript.tsdk": ".\\node_modules\\typescript\\lib" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/.yo-rc.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/.yo-rc.json new file mode 100644 index 00000000000..ec54886ce31 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/.yo-rc.json @@ -0,0 +1,25 @@ +{ + "@microsoft/generator-sharepoint": { + "whichFolder": "subdir", + "solutionName": "spfx", + "environment": "spo", + "skipFeatureDeployment": false, + "isDomainIsolated": false, + "componentType": "adaptiveCardExtension", + "aceTemplateType": "PrimaryText", + "componentName": "HelloWorld", + "componentDescription": "HelloWorld", + "plusBeta": false, + "isCreatingSolution": true, + "nodeVersion": "18.16.0", + "sdksVersions": { + "@microsoft/microsoft-graph-client": "3.0.2", + "@microsoft/teams-js": "2.12.0" + }, + "version": "1.18.0", + "libraryName": "spfx", + "libraryId": "cf43571c-6a6c-4912-93a3-1731b748fd44", + "packageManager": "npm", + "solutionShortDescription": "spfx description" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/README.md b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/README.md new file mode 100644 index 00000000000..4f69a527598 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/README.md @@ -0,0 +1,73 @@ +# spfx + +## Summary + +Short summary on functionality and used technologies. + +[picture of the solution in action, if possible] + +## Used SharePoint Framework Version + +![version](https://img.shields.io/badge/version-1.18.0-green.svg) + +## Applies to + +- [SharePoint Framework](https://aka.ms/spfx) +- [Microsoft 365 tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant) + +> Get your own free development tenant by subscribing to [Microsoft 365 developer program](http://aka.ms/o365devprogram) + +## Prerequisites + +> Any special pre-requisites? + +## Solution + +| Solution | Author(s) | +| ----------- | ------------------------------------------------------- | +| folder name | Author details (name, company, twitter alias with link) | + +## Version history + +| Version | Date | Comments | +| ------- | ---------------- | --------------- | +| 1.1 | March 10, 2021 | Update comment | +| 1.0 | January 29, 2021 | Initial release | + +## Disclaimer + +**THIS CODE IS PROVIDED _AS IS_ WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.** + +--- + +## Minimal Path to Awesome + +- Clone this repository +- Ensure that you are at the solution folder +- in the command-line run: + - **npm install** + - **gulp serve** + +> Include any additional steps as needed. + +## Features + +Description of the extension that expands upon high-level summary above. + +This extension illustrates the following concepts: + +- topic 1 +- topic 2 +- topic 3 + +> Notice that better pictures and documentation will increase the sample usage and the value you are providing for others. Thanks for your submissions advance. + +> Share your web part with others through Microsoft 365 Patterns and Practices program to get visibility and exposure. More details on the community, open-source projects and other activities from http://aka.ms/m365pnp. + +## References + +- [Getting started with SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant) +- [Building for Microsoft teams](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/build-for-teams-overview) +- [Use Microsoft Graph in your solution](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/using-microsoft-graph-apis) +- [Publish SharePoint Framework applications to the Marketplace](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/publish-to-marketplace-overview) +- [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - Guidance, tooling, samples and open-source controls for your Microsoft 365 development diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/config/config.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/config/config.json new file mode 100644 index 00000000000..b841c70c142 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/config/config.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json", + "version": "2.0", + "bundles": { + "hello-world-adaptive-card-extension": { + "components": [ + { + "entrypoint": "./lib/adaptiveCardExtensions/helloWorld/HelloWorldAdaptiveCardExtension.js", + "manifest": "./src/adaptiveCardExtensions/helloWorld/HelloWorldAdaptiveCardExtension.manifest.json" + } + ] + } + }, + "externals": {}, + "localizedResources": { + "HelloWorldAdaptiveCardExtensionStrings": "lib/adaptiveCardExtensions/helloWorld/loc/{locale}.js" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/config/deploy-azure-storage.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/config/deploy-azure-storage.json new file mode 100644 index 00000000000..02290df98af --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/config/deploy-azure-storage.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json", + "workingDir": "./release/assets/", + "account": "", + "container": "spfx", + "accessKey": "" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/config/package-solution.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/config/package-solution.json new file mode 100644 index 00000000000..bf679fa41e1 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/config/package-solution.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json", + "solution": { + "name": "spfx-client-side-solution", + "id": "cf43571c-6a6c-4912-93a3-1731b748fd44", + "version": "1.0.0.0", + "includeClientSideAssets": true, + "isDomainIsolated": false, + "developer": { + "name": "", + "websiteUrl": "", + "privacyUrl": "", + "termsOfUseUrl": "", + "mpnId": "Undefined-1.18.0" + }, + "metadata": { + "shortDescription": { + "default": "spfx description" + }, + "longDescription": { + "default": "spfx description" + }, + "screenshotPaths": [], + "videoUrl": "", + "categories": [] + }, + "features": [ + { + "title": "spfx Feature", + "description": "The feature that activates elements of the spfx solution.", + "id": "38741de6-da4e-4159-a168-d976784ade72", + "version": "1.0.0.0" + } + ] + }, + "paths": { + "zippedPackage": "solution/spfx.sppkg" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/config/sass.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/config/sass.json new file mode 100644 index 00000000000..5e78c982d8d --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/config/sass.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/core-build/sass.schema.json" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/config/serve.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/config/serve.json new file mode 100644 index 00000000000..a4c03e28723 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/config/serve.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/spfx-serve.schema.json", + "port": 4321, + "https": true, + "initialPage": "https://{tenantDomain}/_layouts/workbench.aspx" +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/config/write-manifests.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/config/write-manifests.json new file mode 100644 index 00000000000..bad3526054b --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/config/write-manifests.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json", + "cdnBasePath": "" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/gulpfile.js b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/gulpfile.js new file mode 100644 index 00000000000..be2918708a7 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/gulpfile.js @@ -0,0 +1,16 @@ +'use strict'; + +const build = require('@microsoft/sp-build-web'); + +build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`); + +var getTasks = build.rig.getTasks; +build.rig.getTasks = function () { + var result = getTasks.call(build.rig); + + result.set('serve', result.get('serve-deprecated')); + + return result; +}; + +build.initialize(require('gulp')); diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/package.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/package.json new file mode 100644 index 00000000000..1c6c3a8f8ca --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/package.json @@ -0,0 +1,33 @@ +{ + "name": "spfx", + "version": "0.0.1", + "private": true, + "engines": { + "node": ">=16.13.0 <17.0.0 || >=18.17.1 <19.0.0" + }, + "main": "lib/index.js", + "scripts": { + "build": "gulp bundle", + "clean": "gulp clean", + "test": "gulp test" + }, + "dependencies": { + "tslib": "2.3.1", + "@microsoft/sp-core-library": "1.18.0", + "@microsoft/sp-property-pane": "1.18.0", + "@microsoft/sp-adaptive-card-extension-base": "1.18.0" + }, + "devDependencies": { + "@microsoft/rush-stack-compiler-4.7": "0.1.0", + "@rushstack/eslint-config": "2.5.1", + "@microsoft/eslint-plugin-spfx": "1.18.0", + "@microsoft/eslint-config-spfx": "1.18.0", + "@microsoft/sp-build-web": "1.18.0", + "@types/webpack-env": "~1.15.2", + "ajv": "^6.12.5", + "eslint": "8.7.0", + "gulp": "4.0.2", + "typescript": "4.7.4", + "@microsoft/sp-module-interfaces": "1.18.0" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/HelloWorldAdaptiveCardExtension.manifest.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/HelloWorldAdaptiveCardExtension.manifest.json new file mode 100644 index 00000000000..4a3306db62b --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/HelloWorldAdaptiveCardExtension.manifest.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx/adaptive-card-extension-manifest.schema.json", + "id": "661d8899-a306-4cfb-a558-fe1c42c8b3d5", + "alias": "HelloWorldAdaptiveCardExtension", + "componentType": "AdaptiveCardExtension", + + // The "*" signifies that the version should be taken from the package.json + "version": "*", + "manifestVersion": 2, + + // If true, the component can only be installed on sites where Custom Script is allowed. + // Components that allow authors to embed arbitrary script code should set this to true. + // https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f + "requiresCustomScript": false, + "supportedHosts": ["Dashboard"], + "preconfiguredEntries": [{ + "groupId": "bd067b1e-3ad5-4d5d-a5fe-505f07d7f59c", // Dashboard + "group": { "default": "Dashboard" }, + "title": { "default": "HelloWorld" }, + "description": { "default": "HelloWorld" }, + "iconImageUrl": "https://res.cdn.office.net/files/fabric-cdn-prod_20230308.001/assets/brand-icons/product-monoline/svg/vivaconnections_32x1.svg", + "properties": { + "title": "HelloWorld" + }, + "cardSize": "Medium" + }] +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/HelloWorldAdaptiveCardExtension.ts b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/HelloWorldAdaptiveCardExtension.ts new file mode 100644 index 00000000000..ae4a208b7a5 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/HelloWorldAdaptiveCardExtension.ts @@ -0,0 +1,53 @@ +import type { IPropertyPaneConfiguration } from '@microsoft/sp-property-pane'; +import { BaseAdaptiveCardExtension } from '@microsoft/sp-adaptive-card-extension-base'; +import { CardView } from './cardView/CardView'; +import { QuickView } from './quickView/QuickView'; +import { HelloWorldPropertyPane } from './HelloWorldPropertyPane'; + +export interface IHelloWorldAdaptiveCardExtensionProps { + title: string; +} + +export interface IHelloWorldAdaptiveCardExtensionState { +} + +const CARD_VIEW_REGISTRY_ID: string = 'HelloWorld_CARD_VIEW'; +export const QUICK_VIEW_REGISTRY_ID: string = 'HelloWorld_QUICK_VIEW'; + +export default class HelloWorldAdaptiveCardExtension extends BaseAdaptiveCardExtension< + IHelloWorldAdaptiveCardExtensionProps, + IHelloWorldAdaptiveCardExtensionState +> { + private _deferredPropertyPane: HelloWorldPropertyPane; + + public onInit(): Promise { + this.state = { }; + + // registers the card view to be shown in a dashboard + this.cardNavigator.register(CARD_VIEW_REGISTRY_ID, () => new CardView()); + // registers the quick view to open via QuickView action + this.quickViewNavigator.register(QUICK_VIEW_REGISTRY_ID, () => new QuickView()); + + return Promise.resolve(); + } + + protected loadPropertyPaneResources(): Promise { + return import( + /* webpackChunkName: 'HelloWorld-property-pane'*/ + './HelloWorldPropertyPane' + ) + .then( + (component) => { + this._deferredPropertyPane = new component.HelloWorldPropertyPane(); + } + ); + } + + protected renderCard(): string | undefined { + return CARD_VIEW_REGISTRY_ID; + } + + protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration { + return this._deferredPropertyPane?.getPropertyPaneConfiguration(); + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/HelloWorldPropertyPane.ts b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/HelloWorldPropertyPane.ts new file mode 100644 index 00000000000..4d6d678d317 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/HelloWorldPropertyPane.ts @@ -0,0 +1,23 @@ +import { IPropertyPaneConfiguration, PropertyPaneTextField } from '@microsoft/sp-property-pane'; +import * as strings from 'HelloWorldAdaptiveCardExtensionStrings'; + +export class HelloWorldPropertyPane { + public getPropertyPaneConfiguration(): IPropertyPaneConfiguration { + return { + pages: [ + { + header: { description: strings.PropertyPaneDescription }, + groups: [ + { + groupFields: [ + PropertyPaneTextField('title', { + label: strings.TitleFieldLabel + }) + ] + } + ] + } + ] + }; + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/assets/MicrosoftLogo.png b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/assets/MicrosoftLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..fe9c00a62fae95eb6f5c87243de2ba6d8d891acc GIT binary patch literal 4773 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 SwgYT20S-~ literal 0 HcmV?d00001 diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/cardView/CardView.ts b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/cardView/CardView.ts new file mode 100644 index 00000000000..2fa944d98fa --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/cardView/CardView.ts @@ -0,0 +1,49 @@ +import { + BasePrimaryTextCardView, + IPrimaryTextCardParameters, + IExternalLinkCardAction, + IQuickViewCardAction, + ICardButton +} from '@microsoft/sp-adaptive-card-extension-base'; +import * as strings from 'HelloWorldAdaptiveCardExtensionStrings'; +import { + IHelloWorldAdaptiveCardExtensionProps, + IHelloWorldAdaptiveCardExtensionState, + QUICK_VIEW_REGISTRY_ID +} from '../HelloWorldAdaptiveCardExtension'; + +export class CardView extends BasePrimaryTextCardView< + IHelloWorldAdaptiveCardExtensionProps, + IHelloWorldAdaptiveCardExtensionState +> { + public get cardButtons(): [ICardButton] | [ICardButton, ICardButton] | undefined { + return [ + { + title: strings.QuickViewButton, + action: { + type: 'QuickView', + parameters: { + view: QUICK_VIEW_REGISTRY_ID + } + } + } + ]; + } + + public get data(): IPrimaryTextCardParameters { + return { + primaryText: strings.PrimaryText, + description: strings.Description, + title: this.properties.title + }; + } + + public get onCardSelection(): IQuickViewCardAction | IExternalLinkCardAction | undefined { + return { + type: 'ExternalLink', + parameters: { + target: 'https://www.bing.com' + } + }; + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/loc/en-us.js b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/loc/en-us.js new file mode 100644 index 00000000000..02bf65b41ef --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/loc/en-us.js @@ -0,0 +1,11 @@ +define([], function() { + return { + "PropertyPaneDescription": "Write 1-3 sentences describing the functionality of this component.", + "TitleFieldLabel": "Card title", + "Title": "Adaptive Card Extension", + "SubTitle": "Quick view", + "PrimaryText": "SPFx Adaptive Card Extension", + "Description": "Create your SPFx Adaptive Card Extension solution!", + "QuickViewButton": "Quick view" + } +}); \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/loc/mystring.d.ts b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/loc/mystring.d.ts new file mode 100644 index 00000000000..b0be4c7ea8d --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/loc/mystring.d.ts @@ -0,0 +1,14 @@ +declare interface IHelloWorldAdaptiveCardExtensionStrings { + PropertyPaneDescription: string; + TitleFieldLabel: string; + Title: string; + SubTitle: string; + PrimaryText: string; + Description: string; + QuickViewButton: string; +} + +declare module 'HelloWorldAdaptiveCardExtensionStrings' { + const strings: IHelloWorldAdaptiveCardExtensionStrings; + export = strings; +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/quickView/QuickView.ts b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/quickView/QuickView.ts new file mode 100644 index 00000000000..4b77a439d14 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/quickView/QuickView.ts @@ -0,0 +1,28 @@ +import { ISPFxAdaptiveCard, BaseAdaptiveCardQuickView } from '@microsoft/sp-adaptive-card-extension-base'; +import * as strings from 'HelloWorldAdaptiveCardExtensionStrings'; +import { + IHelloWorldAdaptiveCardExtensionProps, + IHelloWorldAdaptiveCardExtensionState +} from '../HelloWorldAdaptiveCardExtension'; + +export interface IQuickViewData { + subTitle: string; + title: string; +} + +export class QuickView extends BaseAdaptiveCardQuickView< + IHelloWorldAdaptiveCardExtensionProps, + IHelloWorldAdaptiveCardExtensionState, + IQuickViewData +> { + public get data(): IQuickViewData { + return { + subTitle: strings.SubTitle, + title: strings.Title + }; + } + + public get template(): ISPFxAdaptiveCard { + return require('./template/QuickViewTemplate.json'); + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/quickView/template/QuickViewTemplate.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/quickView/template/QuickViewTemplate.json new file mode 100644 index 00000000000..295d637661d --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/adaptiveCardExtensions/helloWorld/quickView/template/QuickViewTemplate.json @@ -0,0 +1,28 @@ +{ + "schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.5", + "body": [ + { + "type": "TextBlock", + "weight": "Bolder", + "text": "${title}" + }, + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "items": [ + { + "type": "TextBlock", + "weight": "Bolder", + "text": "${subTitle}", + "wrap": true + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/index.ts b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/index.ts new file mode 100644 index 00000000000..fb81db1e213 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/src/index.ts @@ -0,0 +1 @@ +// A file is required to be in the root of the /src directory by the TypeScript compiler diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/tsconfig.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/tsconfig.json new file mode 100644 index 00000000000..c4cd392ad1a --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-ace/tsconfig.json @@ -0,0 +1,35 @@ +{ + "extends": "./node_modules/@microsoft/rush-stack-compiler-4.7/includes/tsconfig-web.json", + "compilerOptions": { + "target": "es5", + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "jsx": "react", + "declaration": true, + "sourceMap": true, + "experimentalDecorators": true, + "skipLibCheck": true, + "outDir": "lib", + "inlineSources": false, + "noImplicitAny": true, + + "typeRoots": [ + "./node_modules/@types", + "./node_modules/@microsoft" + ], + "types": [ + "webpack-env" + ], + "lib": [ + "es5", + "dom", + "es2015.collection", + "es2015.promise" + ] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx" + ] +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/.eslintrc.js b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/.eslintrc.js new file mode 100644 index 00000000000..ef68d0e95c0 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/.eslintrc.js @@ -0,0 +1,352 @@ +require('@rushstack/eslint-config/patch/modern-module-resolution'); +module.exports = { + extends: ['@microsoft/eslint-config-spfx/lib/profiles/default'], + parserOptions: { tsconfigRootDir: __dirname }, + overrides: [ + { + files: ['*.ts', '*.tsx'], + parser: '@typescript-eslint/parser', + 'parserOptions': { + 'project': './tsconfig.json', + 'ecmaVersion': 2018, + 'sourceType': 'module' + }, + rules: { + // Prevent usage of the JavaScript null value, while allowing code to access existing APIs that may require null. https://www.npmjs.com/package/@rushstack/eslint-plugin + '@rushstack/no-new-null': 1, + // Require Jest module mocking APIs to be called before any other statements in their code block. https://www.npmjs.com/package/@rushstack/eslint-plugin + '@rushstack/hoist-jest-mock': 1, + // Require regular expressions to be constructed from string constants rather than dynamically building strings at runtime. https://www.npmjs.com/package/@rushstack/eslint-plugin-security + '@rushstack/security/no-unsafe-regexp': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/adjacent-overload-signatures': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // + // CONFIGURATION: By default, these are banned: String, Boolean, Number, Object, Symbol + '@typescript-eslint/ban-types': [ + 1, + { + 'extendDefaults': false, + 'types': { + 'String': { + 'message': 'Use \'string\' instead', + 'fixWith': 'string' + }, + 'Boolean': { + 'message': 'Use \'boolean\' instead', + 'fixWith': 'boolean' + }, + 'Number': { + 'message': 'Use \'number\' instead', + 'fixWith': 'number' + }, + 'Object': { + 'message': 'Use \'object\' instead, or else define a proper TypeScript type:' + }, + 'Symbol': { + 'message': 'Use \'symbol\' instead', + 'fixWith': 'symbol' + }, + 'Function': { + 'message': 'The \'Function\' type accepts any function-like value.\nIt provides no type safety when calling the function, which can be a common source of bugs.\nIt also accepts things like class declarations, which will throw at runtime as they will not be called with \'new\'.\nIf you are expecting the function to accept certain arguments, you should explicitly define the function shape.' + } + } + } + ], + // RATIONALE: Code is more readable when the type of every variable is immediately obvious. + // Even if the compiler may be able to infer a type, this inference will be unavailable + // to a person who is reviewing a GitHub diff. This rule makes writing code harder, + // but writing code is a much less important activity than reading it. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/explicit-function-return-type': [ + 1, + { + 'allowExpressions': true, + 'allowTypedFunctionExpressions': true, + 'allowHigherOrderFunctions': false + } + ], + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Rationale to disable: although this is a recommended rule, it is up to dev to select coding style. + // Set to 1 (warning) or 2 (error) to enable. + '@typescript-eslint/explicit-member-accessibility': 0, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-array-constructor': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // + // RATIONALE: The "any" keyword disables static type checking, the main benefit of using TypeScript. + // This rule should be suppressed only in very special cases such as JSON.stringify() + // where the type really can be anything. Even if the type is flexible, another type + // may be more appropriate such as "unknown", "{}", or "Record". + '@typescript-eslint/no-explicit-any': 1, + // RATIONALE: The #1 rule of promises is that every promise chain must be terminated by a catch() + // handler. Thus wherever a Promise arises, the code must either append a catch handler, + // or else return the object to a caller (who assumes this responsibility). Unterminated + // promise chains are a serious issue. Besides causing errors to be silently ignored, + // they can also cause a NodeJS process to terminate unexpectedly. + '@typescript-eslint/no-floating-promises': 2, + // RATIONALE: Catches a common coding mistake. + '@typescript-eslint/no-for-in-array': 2, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-misused-new': 2, + // RATIONALE: The "namespace" keyword is not recommended for organizing code because JavaScript lacks + // a "using" statement to traverse namespaces. Nested namespaces prevent certain bundler + // optimizations. If you are declaring loose functions/variables, it's better to make them + // static members of a class, since classes support property getters and their private + // members are accessible by unit tests. Also, the exercise of choosing a meaningful + // class name tends to produce more discoverable APIs: for example, search+replacing + // the function "reverse()" is likely to return many false matches, whereas if we always + // write "Text.reverse()" is more unique. For large scale organization, it's recommended + // to decompose your code into separate NPM packages, which ensures that component + // dependencies are tracked more conscientiously. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-namespace': [ + 1, + { + 'allowDeclarations': false, + 'allowDefinitionFiles': false + } + ], + // RATIONALE: Parameter properties provide a shorthand such as "constructor(public title: string)" + // that avoids the effort of declaring "title" as a field. This TypeScript feature makes + // code easier to write, but arguably sacrifices readability: In the notes for + // "@typescript-eslint/member-ordering" we pointed out that fields are central to + // a class's design, so we wouldn't want to bury them in a constructor signature + // just to save some typing. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Set to 1 (warning) or 2 (error) to enable the rule + '@typescript-eslint/parameter-properties': 0, + // RATIONALE: When left in shipping code, unused variables often indicate a mistake. Dead code + // may impact performance. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-unused-vars': [ + 1, + { + 'vars': 'all', + // Unused function arguments often indicate a mistake in JavaScript code. However in TypeScript code, + // the compiler catches most of those mistakes, and unused arguments are fairly common for type signatures + // that are overriding a base class method or implementing an interface. + 'args': 'none' + } + ], + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-use-before-define': [ + 2, + { + 'functions': false, + 'classes': true, + 'variables': true, + 'enums': true, + 'typedefs': true + } + ], + // Disallows require statements except in import statements. + // In other words, the use of forms such as var foo = require("foo") are banned. Instead use ES6 style imports or import foo = require("foo") imports. + '@typescript-eslint/no-var-requires': 'error', + // RATIONALE: The "module" keyword is deprecated except when describing legacy libraries. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/prefer-namespace-keyword': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Rationale to disable: it's up to developer to decide if he wants to add type annotations + // Set to 1 (warning) or 2 (error) to enable the rule + '@typescript-eslint/no-inferrable-types': 0, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Rationale to disable: declaration of empty interfaces may be helpful for generic types scenarios + '@typescript-eslint/no-empty-interface': 0, + // RATIONALE: This rule warns if setters are defined without getters, which is probably a mistake. + 'accessor-pairs': 1, + // RATIONALE: In TypeScript, if you write x["y"] instead of x.y, it disables type checking. + 'dot-notation': [ + 1, + { + 'allowPattern': '^_' + } + ], + // RATIONALE: Catches code that is likely to be incorrect + 'eqeqeq': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'for-direction': 1, + // RATIONALE: Catches a common coding mistake. + 'guard-for-in': 2, + // RATIONALE: If you have more than 2,000 lines in a single source file, it's probably time + // to split up your code. + 'max-lines': ['warn', { max: 2000 }], + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-async-promise-executor': 2, + // RATIONALE: Deprecated language feature. + 'no-caller': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-compare-neg-zero': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-cond-assign': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-constant-condition': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-control-regex': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-debugger': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-delete-var': 2, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-duplicate-case': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-empty': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-empty-character-class': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-empty-pattern': 1, + // RATIONALE: Eval is a security concern and a performance concern. + 'no-eval': 1, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-ex-assign': 2, + // RATIONALE: System types are global and should not be tampered with in a scalable code base. + // If two different libraries (or two versions of the same library) both try to modify + // a type, only one of them can win. Polyfills are acceptable because they implement + // a standardized interoperable contract, but polyfills are generally coded in plain + // JavaScript. + 'no-extend-native': 1, + // Disallow unnecessary labels + 'no-extra-label': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-fallthrough': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-func-assign': 1, + // RATIONALE: Catches a common coding mistake. + 'no-implied-eval': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-invalid-regexp': 2, + // RATIONALE: Catches a common coding mistake. + 'no-label-var': 2, + // RATIONALE: Eliminates redundant code. + 'no-lone-blocks': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-misleading-character-class': 2, + // RATIONALE: Catches a common coding mistake. + 'no-multi-str': 2, + // RATIONALE: It's generally a bad practice to call "new Thing()" without assigning the result to + // a variable. Either it's part of an awkward expression like "(new Thing()).doSomething()", + // or else implies that the constructor is doing nontrivial computations, which is often + // a poor class design. + 'no-new': 1, + // RATIONALE: Obsolete language feature that is deprecated. + 'no-new-func': 2, + // RATIONALE: Obsolete language feature that is deprecated. + 'no-new-object': 2, + // RATIONALE: Obsolete notation. + 'no-new-wrappers': 1, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-octal': 2, + // RATIONALE: Catches code that is likely to be incorrect + 'no-octal-escape': 2, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-regex-spaces': 2, + // RATIONALE: Catches a common coding mistake. + 'no-return-assign': 2, + // RATIONALE: Security risk. + 'no-script-url': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-self-assign': 2, + // RATIONALE: Catches a common coding mistake. + 'no-self-compare': 2, + // RATIONALE: This avoids statements such as "while (a = next(), a && a.length);" that use + // commas to create compound expressions. In general code is more readable if each + // step is split onto a separate line. This also makes it easier to set breakpoints + // in the debugger. + 'no-sequences': 1, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-shadow-restricted-names': 2, + // RATIONALE: Obsolete language feature that is deprecated. + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-sparse-arrays': 2, + // RATIONALE: Although in theory JavaScript allows any possible data type to be thrown as an exception, + // such flexibility adds pointless complexity, by requiring every catch block to test + // the type of the object that it receives. Whereas if catch blocks can always assume + // that their object implements the "Error" contract, then the code is simpler, and + // we generally get useful additional information like a call stack. + 'no-throw-literal': 2, + // RATIONALE: Catches a common coding mistake. + 'no-unmodified-loop-condition': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-unsafe-finally': 2, + // RATIONALE: Catches a common coding mistake. + 'no-unused-expressions': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-unused-labels': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-useless-catch': 1, + // RATIONALE: Avoids a potential performance problem. + 'no-useless-concat': 1, + // RATIONALE: The "var" keyword is deprecated because of its confusing "hoisting" behavior. + // Always use "let" or "const" instead. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + 'no-var': 2, + // RATIONALE: Generally not needed in modern code. + 'no-void': 1, + // RATIONALE: Obsolete language feature that is deprecated. + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-with': 2, + // RATIONALE: Makes logic easier to understand, since constants always have a known value + // @typescript-eslint\eslint-plugin\dist\configs\eslint-recommended.js + 'prefer-const': 1, + // RATIONALE: Catches a common coding mistake where "resolve" and "reject" are confused. + 'promise/param-names': 2, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'require-atomic-updates': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'require-yield': 1, + // "Use strict" is redundant when using the TypeScript compiler. + 'strict': [ + 2, + 'never' + ], + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'use-isnan': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + // Set to 1 (warning) or 2 (error) to enable. + // Rationale to disable: !!{} + 'no-extra-boolean-cast': 0, + // ==================================================================== + // @microsoft/eslint-plugin-spfx + // ==================================================================== + '@microsoft/spfx/import-requires-chunk-name': 1, + '@microsoft/spfx/no-require-ensure': 2, + '@microsoft/spfx/pair-react-dom-render-unmount': 1 + } + }, + { + // For unit tests, we can be a little bit less strict. The settings below revise the + // defaults specified in the extended configurations, as well as above. + files: [ + // Test files + '*.test.ts', + '*.test.tsx', + '*.spec.ts', + '*.spec.tsx', + + // Facebook convention + '**/__mocks__/*.ts', + '**/__mocks__/*.tsx', + '**/__tests__/*.ts', + '**/__tests__/*.tsx', + + // Microsoft convention + '**/test/*.ts', + '**/test/*.tsx' + ], + rules: {} + } + ] +}; \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/.gitignore b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/.gitignore new file mode 100644 index 00000000000..51ca7b9e7a7 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/.gitignore @@ -0,0 +1,34 @@ +# Logs +logs +*.log +npm-debug.log* + +# Dependency directories +node_modules + +# Build generated files +dist +lib +release +solution +temp +*.sppkg +.heft + +# Coverage directory used by tools like istanbul +coverage + +# OSX +.DS_Store + +# Visual Studio files +.ntvs_analysis.dat +.vs +bin +obj + +# Resx Generated Code +*.resx.ts + +# Styles Generated Code +*.scss.ts diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/.npmignore b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/.npmignore new file mode 100644 index 00000000000..ae0b487c075 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/.npmignore @@ -0,0 +1,16 @@ +!dist +config + +gulpfile.js + +release +src +temp + +tsconfig.json +tslint.json + +*.log + +.yo-rc.json +.vscode diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/.vscode/launch.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/.vscode/launch.json new file mode 100644 index 00000000000..53ca22cd853 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Hosted workbench", + "type": "msedge", + "request": "launch", + "url": "https://{tenantDomain}/_layouts/workbench.aspx", + "webRoot": "${workspaceRoot}", + "sourceMaps": true, + "sourceMapPathOverrides": { + "webpack:///.././src/*": "${webRoot}/src/*", + "webpack:///../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../../src/*": "${webRoot}/src/*" + }, + "runtimeArgs": [ + "--remote-debugging-port=9222", + "-incognito" + ] + } + ] +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/.vscode/settings.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/.vscode/settings.json new file mode 100644 index 00000000000..a31a2c33223 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/.vscode/settings.json @@ -0,0 +1,13 @@ +// Place your settings in this file to overwrite default and user settings. +{ + // Configure glob patterns for excluding files and folders in the file explorer. + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/bower_components": true, + "**/coverage": true, + "**/lib-amd": true, + "src/**/*.scss.ts": true + }, + "typescript.tsdk": ".\\node_modules\\typescript\\lib" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/.yo-rc.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/.yo-rc.json new file mode 100644 index 00000000000..a5e0f9f160e --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/.yo-rc.json @@ -0,0 +1,25 @@ +{ + "@microsoft/generator-sharepoint": { + "whichFolder": "subdir", + "solutionName": "spfx", + "environment": "spo", + "skipFeatureDeployment": false, + "isDomainIsolated": false, + "componentType": "extension", + "extensionType": "ApplicationCustomizer", + "componentName": "HelloWorld", + "componentDescription": "HelloWorld", + "plusBeta": false, + "isCreatingSolution": true, + "nodeVersion": "18.16.0", + "sdksVersions": { + "@microsoft/microsoft-graph-client": "3.0.2", + "@microsoft/teams-js": "2.12.0" + }, + "version": "1.18.0", + "libraryName": "spfx", + "libraryId": "38c7411c-7f53-450d-a4e9-b1c679e9db6c", + "packageManager": "npm", + "solutionShortDescription": "spfx description" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/README.md b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/README.md new file mode 100644 index 00000000000..4f69a527598 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/README.md @@ -0,0 +1,73 @@ +# spfx + +## Summary + +Short summary on functionality and used technologies. + +[picture of the solution in action, if possible] + +## Used SharePoint Framework Version + +![version](https://img.shields.io/badge/version-1.18.0-green.svg) + +## Applies to + +- [SharePoint Framework](https://aka.ms/spfx) +- [Microsoft 365 tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant) + +> Get your own free development tenant by subscribing to [Microsoft 365 developer program](http://aka.ms/o365devprogram) + +## Prerequisites + +> Any special pre-requisites? + +## Solution + +| Solution | Author(s) | +| ----------- | ------------------------------------------------------- | +| folder name | Author details (name, company, twitter alias with link) | + +## Version history + +| Version | Date | Comments | +| ------- | ---------------- | --------------- | +| 1.1 | March 10, 2021 | Update comment | +| 1.0 | January 29, 2021 | Initial release | + +## Disclaimer + +**THIS CODE IS PROVIDED _AS IS_ WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.** + +--- + +## Minimal Path to Awesome + +- Clone this repository +- Ensure that you are at the solution folder +- in the command-line run: + - **npm install** + - **gulp serve** + +> Include any additional steps as needed. + +## Features + +Description of the extension that expands upon high-level summary above. + +This extension illustrates the following concepts: + +- topic 1 +- topic 2 +- topic 3 + +> Notice that better pictures and documentation will increase the sample usage and the value you are providing for others. Thanks for your submissions advance. + +> Share your web part with others through Microsoft 365 Patterns and Practices program to get visibility and exposure. More details on the community, open-source projects and other activities from http://aka.ms/m365pnp. + +## References + +- [Getting started with SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant) +- [Building for Microsoft teams](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/build-for-teams-overview) +- [Use Microsoft Graph in your solution](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/using-microsoft-graph-apis) +- [Publish SharePoint Framework applications to the Marketplace](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/publish-to-marketplace-overview) +- [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - Guidance, tooling, samples and open-source controls for your Microsoft 365 development diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/config/config.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/config/config.json new file mode 100644 index 00000000000..41465b9a727 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/config/config.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json", + "version": "2.0", + "bundles": { + "hello-world-application-customizer": { + "components": [ + { + "entrypoint": "./lib/extensions/helloWorld/HelloWorldApplicationCustomizer.js", + "manifest": "./src/extensions/helloWorld/HelloWorldApplicationCustomizer.manifest.json" + } + ] + } + }, + "externals": {}, + "localizedResources": { + "HelloWorldApplicationCustomizerStrings": "lib/extensions/helloWorld/loc/{locale}.js" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/config/deploy-azure-storage.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/config/deploy-azure-storage.json new file mode 100644 index 00000000000..02290df98af --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/config/deploy-azure-storage.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json", + "workingDir": "./release/assets/", + "account": "", + "container": "spfx", + "accessKey": "" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/config/package-solution.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/config/package-solution.json new file mode 100644 index 00000000000..aec64380d20 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/config/package-solution.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json", + "solution": { + "name": "spfx-client-side-solution", + "id": "38c7411c-7f53-450d-a4e9-b1c679e9db6c", + "version": "1.0.0.0", + "includeClientSideAssets": true, + "isDomainIsolated": false, + "developer": { + "name": "", + "websiteUrl": "", + "privacyUrl": "", + "termsOfUseUrl": "", + "mpnId": "Undefined-1.18.0" + }, + "metadata": { + "shortDescription": { + "default": "spfx description" + }, + "longDescription": { + "default": "spfx description" + }, + "screenshotPaths": [], + "videoUrl": "", + "categories": [] + }, + "features": [ + { + "title": "Application Extension - Deployment of custom action", + "description": "Deploys a custom action with ClientSideComponentId association", + "id": "c54def0a-8a31-4a32-8e14-94fb02e551a6", + "version": "1.0.0.0", + "assets": { + "elementManifests": [ + "elements.xml", + "ClientSideInstance.xml" + ] + } + } + ] + }, + "paths": { + "zippedPackage": "solution/spfx.sppkg" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/config/sass.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/config/sass.json new file mode 100644 index 00000000000..5e78c982d8d --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/config/sass.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/core-build/sass.schema.json" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/config/serve.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/config/serve.json new file mode 100644 index 00000000000..f1e1fedcbde --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/config/serve.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/spfx-serve.schema.json", + "port": 4321, + "https": true, + "serveConfigurations": { + "default": { + "pageUrl": "https://{tenantDomain}/SitePages/myPage.aspx", + "customActions": { + "6b5cab24-e6aa-4a7d-b67b-c7999357030d": { + "location": "ClientSideExtension.ApplicationCustomizer", + "properties": { + "testMessage": "Test message" + } + } + } + }, + "helloWorld": { + "pageUrl": "https://{tenantDomain}/SitePages/myPage.aspx", + "customActions": { + "6b5cab24-e6aa-4a7d-b67b-c7999357030d": { + "location": "ClientSideExtension.ApplicationCustomizer", + "properties": { + "testMessage": "Test message" + } + } + } + } + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/config/write-manifests.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/config/write-manifests.json new file mode 100644 index 00000000000..bad3526054b --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/config/write-manifests.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json", + "cdnBasePath": "" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/gulpfile.js b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/gulpfile.js new file mode 100644 index 00000000000..be2918708a7 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/gulpfile.js @@ -0,0 +1,16 @@ +'use strict'; + +const build = require('@microsoft/sp-build-web'); + +build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`); + +var getTasks = build.rig.getTasks; +build.rig.getTasks = function () { + var result = getTasks.call(build.rig); + + result.set('serve', result.get('serve-deprecated')); + + return result; +}; + +build.initialize(require('gulp')); diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/package.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/package.json new file mode 100644 index 00000000000..f579585a512 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/package.json @@ -0,0 +1,34 @@ +{ + "name": "spfx", + "version": "0.0.1", + "private": true, + "engines": { + "node": ">=16.13.0 <17.0.0 || >=18.17.1 <19.0.0" + }, + "main": "lib/index.js", + "scripts": { + "build": "gulp bundle", + "clean": "gulp clean", + "test": "gulp test" + }, + "dependencies": { + "tslib": "2.3.1", + "@microsoft/sp-core-library": "1.18.0", + "@microsoft/decorators": "1.18.0", + "@microsoft/sp-dialog": "1.18.0", + "@microsoft/sp-application-base": "1.18.0" + }, + "devDependencies": { + "@microsoft/rush-stack-compiler-4.7": "0.1.0", + "@rushstack/eslint-config": "2.5.1", + "@microsoft/eslint-plugin-spfx": "1.18.0", + "@microsoft/eslint-config-spfx": "1.18.0", + "@microsoft/sp-build-web": "1.18.0", + "@types/webpack-env": "~1.15.2", + "ajv": "^6.12.5", + "eslint": "8.7.0", + "gulp": "4.0.2", + "typescript": "4.7.4", + "@microsoft/sp-module-interfaces": "1.18.0" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/sharepoint/assets/ClientSideInstance.xml b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/sharepoint/assets/ClientSideInstance.xml new file mode 100644 index 00000000000..fd20925fc0d --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/sharepoint/assets/ClientSideInstance.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/sharepoint/assets/elements.xml b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/sharepoint/assets/elements.xml new file mode 100644 index 00000000000..4be4c7ac3e5 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/sharepoint/assets/elements.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/src/extensions/helloWorld/HelloWorldApplicationCustomizer.manifest.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/src/extensions/helloWorld/HelloWorldApplicationCustomizer.manifest.json new file mode 100644 index 00000000000..419a17c9c55 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/src/extensions/helloWorld/HelloWorldApplicationCustomizer.manifest.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-extension-manifest.schema.json", + + "id": "6b5cab24-e6aa-4a7d-b67b-c7999357030d", + "alias": "HelloWorldApplicationCustomizer", + "componentType": "Extension", + "extensionType": "ApplicationCustomizer", + + // The "*" signifies that the version should be taken from the package.json + "version": "*", + "manifestVersion": 2, + + // If true, the component can only be installed on sites where Custom Script is allowed. + // Components that allow authors to embed arbitrary script code should set this to true. + // https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f + "requiresCustomScript": false +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/src/extensions/helloWorld/HelloWorldApplicationCustomizer.ts b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/src/extensions/helloWorld/HelloWorldApplicationCustomizer.ts new file mode 100644 index 00000000000..366b558d608 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/src/extensions/helloWorld/HelloWorldApplicationCustomizer.ts @@ -0,0 +1,39 @@ +import { Log } from '@microsoft/sp-core-library'; +import { + BaseApplicationCustomizer +} from '@microsoft/sp-application-base'; +import { Dialog } from '@microsoft/sp-dialog'; + +import * as strings from 'HelloWorldApplicationCustomizerStrings'; + +const LOG_SOURCE: string = 'HelloWorldApplicationCustomizer'; + +/** + * If your command set uses the ClientSideComponentProperties JSON input, + * it will be deserialized into the BaseExtension.properties object. + * You can define an interface to describe it. + */ +export interface IHelloWorldApplicationCustomizerProperties { + // This is an example; replace with your own property + testMessage: string; +} + +/** A Custom Action which can be run during execution of a Client Side Application */ +export default class HelloWorldApplicationCustomizer + extends BaseApplicationCustomizer { + + public onInit(): Promise { + Log.info(LOG_SOURCE, `Initialized ${strings.Title}`); + + let message: string = this.properties.testMessage; + if (!message) { + message = '(No properties were provided.)'; + } + + Dialog.alert(`Hello from ${strings.Title}:\n\n${message}`).catch(() => { + /* handle error */ + }); + + return Promise.resolve(); + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/src/extensions/helloWorld/loc/en-us.js b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/src/extensions/helloWorld/loc/en-us.js new file mode 100644 index 00000000000..e1ece91a014 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/src/extensions/helloWorld/loc/en-us.js @@ -0,0 +1,5 @@ +define([], function() { + return { + "Title": "HelloWorldApplicationCustomizer" + } +}); \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/src/extensions/helloWorld/loc/myStrings.d.ts b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/src/extensions/helloWorld/loc/myStrings.d.ts new file mode 100644 index 00000000000..1851df9a8ff --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/src/extensions/helloWorld/loc/myStrings.d.ts @@ -0,0 +1,8 @@ +declare interface IHelloWorldApplicationCustomizerStrings { + Title: string; +} + +declare module 'HelloWorldApplicationCustomizerStrings' { + const strings: IHelloWorldApplicationCustomizerStrings; + export = strings; +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/src/index.ts b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/src/index.ts new file mode 100644 index 00000000000..fb81db1e213 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/src/index.ts @@ -0,0 +1 @@ +// A file is required to be in the root of the /src directory by the TypeScript compiler diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/tsconfig.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/tsconfig.json new file mode 100644 index 00000000000..c4cd392ad1a --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-applicationcustomizer/tsconfig.json @@ -0,0 +1,35 @@ +{ + "extends": "./node_modules/@microsoft/rush-stack-compiler-4.7/includes/tsconfig-web.json", + "compilerOptions": { + "target": "es5", + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "jsx": "react", + "declaration": true, + "sourceMap": true, + "experimentalDecorators": true, + "skipLibCheck": true, + "outDir": "lib", + "inlineSources": false, + "noImplicitAny": true, + + "typeRoots": [ + "./node_modules/@types", + "./node_modules/@microsoft" + ], + "types": [ + "webpack-env" + ], + "lib": [ + "es5", + "dom", + "es2015.collection", + "es2015.promise" + ] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx" + ] +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/.eslintrc.js b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/.eslintrc.js new file mode 100644 index 00000000000..473df80cdf9 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/.eslintrc.js @@ -0,0 +1,352 @@ +require('@rushstack/eslint-config/patch/modern-module-resolution'); +module.exports = { + extends: ['@microsoft/eslint-config-spfx/lib/profiles/react'], + parserOptions: { tsconfigRootDir: __dirname }, + overrides: [ + { + files: ['*.ts', '*.tsx'], + parser: '@typescript-eslint/parser', + 'parserOptions': { + 'project': './tsconfig.json', + 'ecmaVersion': 2018, + 'sourceType': 'module' + }, + rules: { + // Prevent usage of the JavaScript null value, while allowing code to access existing APIs that may require null. https://www.npmjs.com/package/@rushstack/eslint-plugin + '@rushstack/no-new-null': 1, + // Require Jest module mocking APIs to be called before any other statements in their code block. https://www.npmjs.com/package/@rushstack/eslint-plugin + '@rushstack/hoist-jest-mock': 1, + // Require regular expressions to be constructed from string constants rather than dynamically building strings at runtime. https://www.npmjs.com/package/@rushstack/eslint-plugin-security + '@rushstack/security/no-unsafe-regexp': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/adjacent-overload-signatures': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // + // CONFIGURATION: By default, these are banned: String, Boolean, Number, Object, Symbol + '@typescript-eslint/ban-types': [ + 1, + { + 'extendDefaults': false, + 'types': { + 'String': { + 'message': 'Use \'string\' instead', + 'fixWith': 'string' + }, + 'Boolean': { + 'message': 'Use \'boolean\' instead', + 'fixWith': 'boolean' + }, + 'Number': { + 'message': 'Use \'number\' instead', + 'fixWith': 'number' + }, + 'Object': { + 'message': 'Use \'object\' instead, or else define a proper TypeScript type:' + }, + 'Symbol': { + 'message': 'Use \'symbol\' instead', + 'fixWith': 'symbol' + }, + 'Function': { + 'message': 'The \'Function\' type accepts any function-like value.\nIt provides no type safety when calling the function, which can be a common source of bugs.\nIt also accepts things like class declarations, which will throw at runtime as they will not be called with \'new\'.\nIf you are expecting the function to accept certain arguments, you should explicitly define the function shape.' + } + } + } + ], + // RATIONALE: Code is more readable when the type of every variable is immediately obvious. + // Even if the compiler may be able to infer a type, this inference will be unavailable + // to a person who is reviewing a GitHub diff. This rule makes writing code harder, + // but writing code is a much less important activity than reading it. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/explicit-function-return-type': [ + 1, + { + 'allowExpressions': true, + 'allowTypedFunctionExpressions': true, + 'allowHigherOrderFunctions': false + } + ], + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Rationale to disable: although this is a recommended rule, it is up to dev to select coding style. + // Set to 1 (warning) or 2 (error) to enable. + '@typescript-eslint/explicit-member-accessibility': 0, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-array-constructor': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // + // RATIONALE: The "any" keyword disables static type checking, the main benefit of using TypeScript. + // This rule should be suppressed only in very special cases such as JSON.stringify() + // where the type really can be anything. Even if the type is flexible, another type + // may be more appropriate such as "unknown", "{}", or "Record". + '@typescript-eslint/no-explicit-any': 1, + // RATIONALE: The #1 rule of promises is that every promise chain must be terminated by a catch() + // handler. Thus wherever a Promise arises, the code must either append a catch handler, + // or else return the object to a caller (who assumes this responsibility). Unterminated + // promise chains are a serious issue. Besides causing errors to be silently ignored, + // they can also cause a NodeJS process to terminate unexpectedly. + '@typescript-eslint/no-floating-promises': 2, + // RATIONALE: Catches a common coding mistake. + '@typescript-eslint/no-for-in-array': 2, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-misused-new': 2, + // RATIONALE: The "namespace" keyword is not recommended for organizing code because JavaScript lacks + // a "using" statement to traverse namespaces. Nested namespaces prevent certain bundler + // optimizations. If you are declaring loose functions/variables, it's better to make them + // static members of a class, since classes support property getters and their private + // members are accessible by unit tests. Also, the exercise of choosing a meaningful + // class name tends to produce more discoverable APIs: for example, search+replacing + // the function "reverse()" is likely to return many false matches, whereas if we always + // write "Text.reverse()" is more unique. For large scale organization, it's recommended + // to decompose your code into separate NPM packages, which ensures that component + // dependencies are tracked more conscientiously. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-namespace': [ + 1, + { + 'allowDeclarations': false, + 'allowDefinitionFiles': false + } + ], + // RATIONALE: Parameter properties provide a shorthand such as "constructor(public title: string)" + // that avoids the effort of declaring "title" as a field. This TypeScript feature makes + // code easier to write, but arguably sacrifices readability: In the notes for + // "@typescript-eslint/member-ordering" we pointed out that fields are central to + // a class's design, so we wouldn't want to bury them in a constructor signature + // just to save some typing. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Set to 1 (warning) or 2 (error) to enable the rule + '@typescript-eslint/parameter-properties': 0, + // RATIONALE: When left in shipping code, unused variables often indicate a mistake. Dead code + // may impact performance. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-unused-vars': [ + 1, + { + 'vars': 'all', + // Unused function arguments often indicate a mistake in JavaScript code. However in TypeScript code, + // the compiler catches most of those mistakes, and unused arguments are fairly common for type signatures + // that are overriding a base class method or implementing an interface. + 'args': 'none' + } + ], + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-use-before-define': [ + 2, + { + 'functions': false, + 'classes': true, + 'variables': true, + 'enums': true, + 'typedefs': true + } + ], + // Disallows require statements except in import statements. + // In other words, the use of forms such as var foo = require("foo") are banned. Instead use ES6 style imports or import foo = require("foo") imports. + '@typescript-eslint/no-var-requires': 'error', + // RATIONALE: The "module" keyword is deprecated except when describing legacy libraries. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/prefer-namespace-keyword': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Rationale to disable: it's up to developer to decide if he wants to add type annotations + // Set to 1 (warning) or 2 (error) to enable the rule + '@typescript-eslint/no-inferrable-types': 0, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Rationale to disable: declaration of empty interfaces may be helpful for generic types scenarios + '@typescript-eslint/no-empty-interface': 0, + // RATIONALE: This rule warns if setters are defined without getters, which is probably a mistake. + 'accessor-pairs': 1, + // RATIONALE: In TypeScript, if you write x["y"] instead of x.y, it disables type checking. + 'dot-notation': [ + 1, + { + 'allowPattern': '^_' + } + ], + // RATIONALE: Catches code that is likely to be incorrect + 'eqeqeq': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'for-direction': 1, + // RATIONALE: Catches a common coding mistake. + 'guard-for-in': 2, + // RATIONALE: If you have more than 2,000 lines in a single source file, it's probably time + // to split up your code. + 'max-lines': ['warn', { max: 2000 }], + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-async-promise-executor': 2, + // RATIONALE: Deprecated language feature. + 'no-caller': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-compare-neg-zero': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-cond-assign': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-constant-condition': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-control-regex': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-debugger': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-delete-var': 2, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-duplicate-case': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-empty': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-empty-character-class': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-empty-pattern': 1, + // RATIONALE: Eval is a security concern and a performance concern. + 'no-eval': 1, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-ex-assign': 2, + // RATIONALE: System types are global and should not be tampered with in a scalable code base. + // If two different libraries (or two versions of the same library) both try to modify + // a type, only one of them can win. Polyfills are acceptable because they implement + // a standardized interoperable contract, but polyfills are generally coded in plain + // JavaScript. + 'no-extend-native': 1, + // Disallow unnecessary labels + 'no-extra-label': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-fallthrough': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-func-assign': 1, + // RATIONALE: Catches a common coding mistake. + 'no-implied-eval': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-invalid-regexp': 2, + // RATIONALE: Catches a common coding mistake. + 'no-label-var': 2, + // RATIONALE: Eliminates redundant code. + 'no-lone-blocks': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-misleading-character-class': 2, + // RATIONALE: Catches a common coding mistake. + 'no-multi-str': 2, + // RATIONALE: It's generally a bad practice to call "new Thing()" without assigning the result to + // a variable. Either it's part of an awkward expression like "(new Thing()).doSomething()", + // or else implies that the constructor is doing nontrivial computations, which is often + // a poor class design. + 'no-new': 1, + // RATIONALE: Obsolete language feature that is deprecated. + 'no-new-func': 2, + // RATIONALE: Obsolete language feature that is deprecated. + 'no-new-object': 2, + // RATIONALE: Obsolete notation. + 'no-new-wrappers': 1, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-octal': 2, + // RATIONALE: Catches code that is likely to be incorrect + 'no-octal-escape': 2, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-regex-spaces': 2, + // RATIONALE: Catches a common coding mistake. + 'no-return-assign': 2, + // RATIONALE: Security risk. + 'no-script-url': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-self-assign': 2, + // RATIONALE: Catches a common coding mistake. + 'no-self-compare': 2, + // RATIONALE: This avoids statements such as "while (a = next(), a && a.length);" that use + // commas to create compound expressions. In general code is more readable if each + // step is split onto a separate line. This also makes it easier to set breakpoints + // in the debugger. + 'no-sequences': 1, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-shadow-restricted-names': 2, + // RATIONALE: Obsolete language feature that is deprecated. + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-sparse-arrays': 2, + // RATIONALE: Although in theory JavaScript allows any possible data type to be thrown as an exception, + // such flexibility adds pointless complexity, by requiring every catch block to test + // the type of the object that it receives. Whereas if catch blocks can always assume + // that their object implements the "Error" contract, then the code is simpler, and + // we generally get useful additional information like a call stack. + 'no-throw-literal': 2, + // RATIONALE: Catches a common coding mistake. + 'no-unmodified-loop-condition': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-unsafe-finally': 2, + // RATIONALE: Catches a common coding mistake. + 'no-unused-expressions': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-unused-labels': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-useless-catch': 1, + // RATIONALE: Avoids a potential performance problem. + 'no-useless-concat': 1, + // RATIONALE: The "var" keyword is deprecated because of its confusing "hoisting" behavior. + // Always use "let" or "const" instead. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + 'no-var': 2, + // RATIONALE: Generally not needed in modern code. + 'no-void': 1, + // RATIONALE: Obsolete language feature that is deprecated. + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-with': 2, + // RATIONALE: Makes logic easier to understand, since constants always have a known value + // @typescript-eslint\eslint-plugin\dist\configs\eslint-recommended.js + 'prefer-const': 1, + // RATIONALE: Catches a common coding mistake where "resolve" and "reject" are confused. + 'promise/param-names': 2, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'require-atomic-updates': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'require-yield': 1, + // "Use strict" is redundant when using the TypeScript compiler. + 'strict': [ + 2, + 'never' + ], + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'use-isnan': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + // Set to 1 (warning) or 2 (error) to enable. + // Rationale to disable: !!{} + 'no-extra-boolean-cast': 0, + // ==================================================================== + // @microsoft/eslint-plugin-spfx + // ==================================================================== + '@microsoft/spfx/import-requires-chunk-name': 1, + '@microsoft/spfx/no-require-ensure': 2, + '@microsoft/spfx/pair-react-dom-render-unmount': 1 + } + }, + { + // For unit tests, we can be a little bit less strict. The settings below revise the + // defaults specified in the extended configurations, as well as above. + files: [ + // Test files + '*.test.ts', + '*.test.tsx', + '*.spec.ts', + '*.spec.tsx', + + // Facebook convention + '**/__mocks__/*.ts', + '**/__mocks__/*.tsx', + '**/__tests__/*.ts', + '**/__tests__/*.tsx', + + // Microsoft convention + '**/test/*.ts', + '**/test/*.tsx' + ], + rules: {} + } + ] +}; \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/.gitignore b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/.gitignore new file mode 100644 index 00000000000..51ca7b9e7a7 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/.gitignore @@ -0,0 +1,34 @@ +# Logs +logs +*.log +npm-debug.log* + +# Dependency directories +node_modules + +# Build generated files +dist +lib +release +solution +temp +*.sppkg +.heft + +# Coverage directory used by tools like istanbul +coverage + +# OSX +.DS_Store + +# Visual Studio files +.ntvs_analysis.dat +.vs +bin +obj + +# Resx Generated Code +*.resx.ts + +# Styles Generated Code +*.scss.ts diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/.npmignore b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/.npmignore new file mode 100644 index 00000000000..ae0b487c075 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/.npmignore @@ -0,0 +1,16 @@ +!dist +config + +gulpfile.js + +release +src +temp + +tsconfig.json +tslint.json + +*.log + +.yo-rc.json +.vscode diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/.vscode/launch.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/.vscode/launch.json new file mode 100644 index 00000000000..53ca22cd853 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Hosted workbench", + "type": "msedge", + "request": "launch", + "url": "https://{tenantDomain}/_layouts/workbench.aspx", + "webRoot": "${workspaceRoot}", + "sourceMaps": true, + "sourceMapPathOverrides": { + "webpack:///.././src/*": "${webRoot}/src/*", + "webpack:///../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../../src/*": "${webRoot}/src/*" + }, + "runtimeArgs": [ + "--remote-debugging-port=9222", + "-incognito" + ] + } + ] +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/.vscode/settings.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/.vscode/settings.json new file mode 100644 index 00000000000..a31a2c33223 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/.vscode/settings.json @@ -0,0 +1,13 @@ +// Place your settings in this file to overwrite default and user settings. +{ + // Configure glob patterns for excluding files and folders in the file explorer. + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/bower_components": true, + "**/coverage": true, + "**/lib-amd": true, + "src/**/*.scss.ts": true + }, + "typescript.tsdk": ".\\node_modules\\typescript\\lib" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/.yo-rc.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/.yo-rc.json new file mode 100644 index 00000000000..39d38238e1a --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/.yo-rc.json @@ -0,0 +1,26 @@ +{ + "@microsoft/generator-sharepoint": { + "whichFolder": "subdir", + "solutionName": "spfx", + "environment": "spo", + "skipFeatureDeployment": false, + "isDomainIsolated": false, + "componentType": "extension", + "extensionType": "FieldCustomizer", + "template": "react", + "componentName": "HelloWorld", + "componentDescription": "HelloWorld", + "plusBeta": false, + "isCreatingSolution": true, + "nodeVersion": "18.16.0", + "sdksVersions": { + "@microsoft/microsoft-graph-client": "3.0.2", + "@microsoft/teams-js": "2.12.0" + }, + "version": "1.18.0", + "libraryName": "spfx", + "libraryId": "dce7a8d1-2be2-4dfb-a834-ca8a9cdeed5c", + "packageManager": "npm", + "solutionShortDescription": "spfx description" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/README.md b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/README.md new file mode 100644 index 00000000000..4f69a527598 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/README.md @@ -0,0 +1,73 @@ +# spfx + +## Summary + +Short summary on functionality and used technologies. + +[picture of the solution in action, if possible] + +## Used SharePoint Framework Version + +![version](https://img.shields.io/badge/version-1.18.0-green.svg) + +## Applies to + +- [SharePoint Framework](https://aka.ms/spfx) +- [Microsoft 365 tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant) + +> Get your own free development tenant by subscribing to [Microsoft 365 developer program](http://aka.ms/o365devprogram) + +## Prerequisites + +> Any special pre-requisites? + +## Solution + +| Solution | Author(s) | +| ----------- | ------------------------------------------------------- | +| folder name | Author details (name, company, twitter alias with link) | + +## Version history + +| Version | Date | Comments | +| ------- | ---------------- | --------------- | +| 1.1 | March 10, 2021 | Update comment | +| 1.0 | January 29, 2021 | Initial release | + +## Disclaimer + +**THIS CODE IS PROVIDED _AS IS_ WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.** + +--- + +## Minimal Path to Awesome + +- Clone this repository +- Ensure that you are at the solution folder +- in the command-line run: + - **npm install** + - **gulp serve** + +> Include any additional steps as needed. + +## Features + +Description of the extension that expands upon high-level summary above. + +This extension illustrates the following concepts: + +- topic 1 +- topic 2 +- topic 3 + +> Notice that better pictures and documentation will increase the sample usage and the value you are providing for others. Thanks for your submissions advance. + +> Share your web part with others through Microsoft 365 Patterns and Practices program to get visibility and exposure. More details on the community, open-source projects and other activities from http://aka.ms/m365pnp. + +## References + +- [Getting started with SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant) +- [Building for Microsoft teams](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/build-for-teams-overview) +- [Use Microsoft Graph in your solution](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/using-microsoft-graph-apis) +- [Publish SharePoint Framework applications to the Marketplace](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/publish-to-marketplace-overview) +- [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - Guidance, tooling, samples and open-source controls for your Microsoft 365 development diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/config/config.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/config/config.json new file mode 100644 index 00000000000..b49150722de --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/config/config.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json", + "version": "2.0", + "bundles": { + "hello-world-field-customizer": { + "components": [ + { + "entrypoint": "./lib/extensions/helloWorld/HelloWorldFieldCustomizer.js", + "manifest": "./src/extensions/helloWorld/HelloWorldFieldCustomizer.manifest.json" + } + ] + } + }, + "externals": {}, + "localizedResources": { + "HelloWorldFieldCustomizerStrings": "lib/extensions/helloWorld/loc/{locale}.js" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/config/deploy-azure-storage.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/config/deploy-azure-storage.json new file mode 100644 index 00000000000..02290df98af --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/config/deploy-azure-storage.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json", + "workingDir": "./release/assets/", + "account": "", + "container": "spfx", + "accessKey": "" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/config/package-solution.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/config/package-solution.json new file mode 100644 index 00000000000..7a6142fbf92 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/config/package-solution.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json", + "solution": { + "name": "spfx-client-side-solution", + "id": "dce7a8d1-2be2-4dfb-a834-ca8a9cdeed5c", + "version": "1.0.0.0", + "includeClientSideAssets": true, + "isDomainIsolated": false, + "developer": { + "name": "", + "websiteUrl": "", + "privacyUrl": "", + "termsOfUseUrl": "", + "mpnId": "Undefined-1.18.0" + }, + "metadata": { + "shortDescription": { + "default": "spfx description" + }, + "longDescription": { + "default": "spfx description" + }, + "screenshotPaths": [], + "videoUrl": "", + "categories": [] + }, + "features": [ + { + "title": "Application Extension - Deployment of custom action", + "description": "Deploys a custom action with ClientSideComponentId association", + "id": "10827a43-798e-4a7d-9ead-8bfdf47ccddd", + "version": "1.0.0.0", + "assets": { + "elementManifests": [ + "elements.xml" + ] + } + } + ] + }, + "paths": { + "zippedPackage": "solution/spfx.sppkg" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/config/sass.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/config/sass.json new file mode 100644 index 00000000000..5e78c982d8d --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/config/sass.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/core-build/sass.schema.json" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/config/serve.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/config/serve.json new file mode 100644 index 00000000000..b5412c114cb --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/config/serve.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/spfx-serve.schema.json", + "port": 4321, + "https": true, + "serveConfigurations": { + "default": { + "pageUrl": "https://{tenantDomain}/SitePages/myPage.aspx", + "fieldCustomizers": { + "InternalFieldName": { + "id": "e0fc8239-0091-415e-92e0-1fb68a20faab", + "properties": { + "sampleText": "Value" + } + } + } + }, + "helloWorld": { + "pageUrl": "https://{tenantDomain}/SitePages/myPage.aspx", + "fieldCustomizers": { + "InternalFieldName": { + "id": "e0fc8239-0091-415e-92e0-1fb68a20faab", + "properties": { + "sampleText": "Value" + } + } + } + } + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/config/write-manifests.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/config/write-manifests.json new file mode 100644 index 00000000000..bad3526054b --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/config/write-manifests.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json", + "cdnBasePath": "" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/gulpfile.js b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/gulpfile.js new file mode 100644 index 00000000000..be2918708a7 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/gulpfile.js @@ -0,0 +1,16 @@ +'use strict'; + +const build = require('@microsoft/sp-build-web'); + +build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`); + +var getTasks = build.rig.getTasks; +build.rig.getTasks = function () { + var result = getTasks.call(build.rig); + + result.set('serve', result.get('serve-deprecated')); + + return result; +}; + +build.initialize(require('gulp')); diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/package.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/package.json new file mode 100644 index 00000000000..6ddd5604558 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/package.json @@ -0,0 +1,39 @@ +{ + "name": "spfx", + "version": "0.0.1", + "private": true, + "engines": { + "node": ">=16.13.0 <17.0.0 || >=18.17.1 <19.0.0" + }, + "main": "lib/index.js", + "scripts": { + "build": "gulp bundle", + "clean": "gulp clean", + "test": "gulp test" + }, + "dependencies": { + "tslib": "2.3.1", + "react": "17.0.1", + "react-dom": "17.0.1", + "@fluentui/react": "^8.106.4", + "@microsoft/sp-core-library": "1.18.0", + "@microsoft/decorators": "1.18.0", + "@microsoft/sp-listview-extensibility": "1.18.0" + }, + "devDependencies": { + "@microsoft/rush-stack-compiler-4.7": "0.1.0", + "@rushstack/eslint-config": "2.5.1", + "@microsoft/eslint-plugin-spfx": "1.18.0", + "@microsoft/eslint-config-spfx": "1.18.0", + "@microsoft/sp-build-web": "1.18.0", + "@types/webpack-env": "~1.15.2", + "ajv": "^6.12.5", + "eslint": "8.7.0", + "gulp": "4.0.2", + "typescript": "4.7.4", + "@types/react": "17.0.45", + "@types/react-dom": "17.0.17", + "eslint-plugin-react-hooks": "4.3.0", + "@microsoft/sp-module-interfaces": "1.18.0" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/sharepoint/assets/elements.xml b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/sharepoint/assets/elements.xml new file mode 100644 index 00000000000..95ba98024b9 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/sharepoint/assets/elements.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/src/extensions/helloWorld/HelloWorldFieldCustomizer.manifest.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/src/extensions/helloWorld/HelloWorldFieldCustomizer.manifest.json new file mode 100644 index 00000000000..f242e035cec --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/src/extensions/helloWorld/HelloWorldFieldCustomizer.manifest.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-extension-manifest.schema.json", + + "id": "e0fc8239-0091-415e-92e0-1fb68a20faab", + "alias": "HelloWorldFieldCustomizer", + "componentType": "Extension", + "extensionType": "FieldCustomizer", + + // The "*" signifies that the version should be taken from the package.json + "version": "*", + "manifestVersion": 2, + + // If true, the component can only be installed on sites where Custom Script is allowed. + // Components that allow authors to embed arbitrary script code should set this to true. + // https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f + "requiresCustomScript": false +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/src/extensions/helloWorld/HelloWorldFieldCustomizer.ts b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/src/extensions/helloWorld/HelloWorldFieldCustomizer.ts new file mode 100644 index 00000000000..10403a24a38 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/src/extensions/helloWorld/HelloWorldFieldCustomizer.ts @@ -0,0 +1,54 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; + +import { Log } from '@microsoft/sp-core-library'; +import { + BaseFieldCustomizer, + type IFieldCustomizerCellEventParameters +} from '@microsoft/sp-listview-extensibility'; + +import * as strings from 'HelloWorldFieldCustomizerStrings'; +import HelloWorld, { IHelloWorldProps } from './components/HelloWorld'; + +/** + * If your field customizer uses the ClientSideComponentProperties JSON input, + * it will be deserialized into the BaseExtension.properties object. + * You can define an interface to describe it. + */ +export interface IHelloWorldFieldCustomizerProperties { + // This is an example; replace with your own property + sampleText?: string; +} + +const LOG_SOURCE: string = 'HelloWorldFieldCustomizer'; + +export default class HelloWorldFieldCustomizer + extends BaseFieldCustomizer { + + public onInit(): Promise { + // Add your custom initialization to this method. The framework will wait + // for the returned promise to resolve before firing any BaseFieldCustomizer events. + Log.info(LOG_SOURCE, 'Activated HelloWorldFieldCustomizer with properties:'); + Log.info(LOG_SOURCE, JSON.stringify(this.properties, undefined, 2)); + Log.info(LOG_SOURCE, `The following string should be equal: "HelloWorldFieldCustomizer" and "${strings.Title}"`); + return Promise.resolve(); + } + + public onRenderCell(event: IFieldCustomizerCellEventParameters): void { + // Use this method to perform your custom cell rendering. + const text: string = `${this.properties.sampleText}: ${event.fieldValue}`; + + const helloWorld: React.ReactElement<{}> = + React.createElement(HelloWorld, { text } as IHelloWorldProps); + + ReactDOM.render(helloWorld, event.domElement); + } + + public onDisposeCell(event: IFieldCustomizerCellEventParameters): void { + // This method should be used to free any resources that were allocated during rendering. + // For example, if your onRenderCell() called ReactDOM.render(), then you should + // call ReactDOM.unmountComponentAtNode() here. + ReactDOM.unmountComponentAtNode(event.domElement); + super.onDisposeCell(event); + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/src/extensions/helloWorld/components/HelloWorld.module.scss b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/src/extensions/helloWorld/components/HelloWorld.module.scss new file mode 100644 index 00000000000..bd6a48f8d1a --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/src/extensions/helloWorld/components/HelloWorld.module.scss @@ -0,0 +1,6 @@ +.helloWorld { + background-color: "[theme:themePrimary, default:#0078d4]"; + color: "[theme:white, default:#ffffff]"; + text-align: center; + padding: 0.5rem; +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/src/extensions/helloWorld/components/HelloWorld.tsx b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/src/extensions/helloWorld/components/HelloWorld.tsx new file mode 100644 index 00000000000..ac7839d8408 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/src/extensions/helloWorld/components/HelloWorld.tsx @@ -0,0 +1,28 @@ +import { Log } from '@microsoft/sp-core-library'; +import * as React from 'react'; + +import styles from './HelloWorld.module.scss'; + +export interface IHelloWorldProps { + text: string; +} + +const LOG_SOURCE: string = 'HelloWorld'; + +export default class HelloWorld extends React.Component { + public componentDidMount(): void { + Log.info(LOG_SOURCE, 'React Element: HelloWorld mounted'); + } + + public componentWillUnmount(): void { + Log.info(LOG_SOURCE, 'React Element: HelloWorld unmounted'); + } + + public render(): React.ReactElement<{}> { + return ( +
+ { this.props.text } +
+ ); + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/src/extensions/helloWorld/loc/en-us.js b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/src/extensions/helloWorld/loc/en-us.js new file mode 100644 index 00000000000..103d9f12782 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/src/extensions/helloWorld/loc/en-us.js @@ -0,0 +1,5 @@ +define([], function() { + return { + "Title": "HelloWorldFieldCustomizer" + } +}); \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/src/extensions/helloWorld/loc/myStrings.d.ts b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/src/extensions/helloWorld/loc/myStrings.d.ts new file mode 100644 index 00000000000..5733b4be30c --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/src/extensions/helloWorld/loc/myStrings.d.ts @@ -0,0 +1,8 @@ +declare interface IHelloWorldFieldCustomizerStrings { + Title: string; +} + +declare module 'HelloWorldFieldCustomizerStrings' { + const strings: IHelloWorldFieldCustomizerStrings; + export = strings; +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/src/index.ts b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/src/index.ts new file mode 100644 index 00000000000..fb81db1e213 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/src/index.ts @@ -0,0 +1 @@ +// A file is required to be in the root of the /src directory by the TypeScript compiler diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/tsconfig.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/tsconfig.json new file mode 100644 index 00000000000..c4cd392ad1a --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-fieldcustomizer-react/tsconfig.json @@ -0,0 +1,35 @@ +{ + "extends": "./node_modules/@microsoft/rush-stack-compiler-4.7/includes/tsconfig-web.json", + "compilerOptions": { + "target": "es5", + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "jsx": "react", + "declaration": true, + "sourceMap": true, + "experimentalDecorators": true, + "skipLibCheck": true, + "outDir": "lib", + "inlineSources": false, + "noImplicitAny": true, + + "typeRoots": [ + "./node_modules/@types", + "./node_modules/@microsoft" + ], + "types": [ + "webpack-env" + ], + "lib": [ + "es5", + "dom", + "es2015.collection", + "es2015.promise" + ] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx" + ] +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/.eslintrc.js b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/.eslintrc.js new file mode 100644 index 00000000000..ef68d0e95c0 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/.eslintrc.js @@ -0,0 +1,352 @@ +require('@rushstack/eslint-config/patch/modern-module-resolution'); +module.exports = { + extends: ['@microsoft/eslint-config-spfx/lib/profiles/default'], + parserOptions: { tsconfigRootDir: __dirname }, + overrides: [ + { + files: ['*.ts', '*.tsx'], + parser: '@typescript-eslint/parser', + 'parserOptions': { + 'project': './tsconfig.json', + 'ecmaVersion': 2018, + 'sourceType': 'module' + }, + rules: { + // Prevent usage of the JavaScript null value, while allowing code to access existing APIs that may require null. https://www.npmjs.com/package/@rushstack/eslint-plugin + '@rushstack/no-new-null': 1, + // Require Jest module mocking APIs to be called before any other statements in their code block. https://www.npmjs.com/package/@rushstack/eslint-plugin + '@rushstack/hoist-jest-mock': 1, + // Require regular expressions to be constructed from string constants rather than dynamically building strings at runtime. https://www.npmjs.com/package/@rushstack/eslint-plugin-security + '@rushstack/security/no-unsafe-regexp': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/adjacent-overload-signatures': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // + // CONFIGURATION: By default, these are banned: String, Boolean, Number, Object, Symbol + '@typescript-eslint/ban-types': [ + 1, + { + 'extendDefaults': false, + 'types': { + 'String': { + 'message': 'Use \'string\' instead', + 'fixWith': 'string' + }, + 'Boolean': { + 'message': 'Use \'boolean\' instead', + 'fixWith': 'boolean' + }, + 'Number': { + 'message': 'Use \'number\' instead', + 'fixWith': 'number' + }, + 'Object': { + 'message': 'Use \'object\' instead, or else define a proper TypeScript type:' + }, + 'Symbol': { + 'message': 'Use \'symbol\' instead', + 'fixWith': 'symbol' + }, + 'Function': { + 'message': 'The \'Function\' type accepts any function-like value.\nIt provides no type safety when calling the function, which can be a common source of bugs.\nIt also accepts things like class declarations, which will throw at runtime as they will not be called with \'new\'.\nIf you are expecting the function to accept certain arguments, you should explicitly define the function shape.' + } + } + } + ], + // RATIONALE: Code is more readable when the type of every variable is immediately obvious. + // Even if the compiler may be able to infer a type, this inference will be unavailable + // to a person who is reviewing a GitHub diff. This rule makes writing code harder, + // but writing code is a much less important activity than reading it. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/explicit-function-return-type': [ + 1, + { + 'allowExpressions': true, + 'allowTypedFunctionExpressions': true, + 'allowHigherOrderFunctions': false + } + ], + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Rationale to disable: although this is a recommended rule, it is up to dev to select coding style. + // Set to 1 (warning) or 2 (error) to enable. + '@typescript-eslint/explicit-member-accessibility': 0, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-array-constructor': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // + // RATIONALE: The "any" keyword disables static type checking, the main benefit of using TypeScript. + // This rule should be suppressed only in very special cases such as JSON.stringify() + // where the type really can be anything. Even if the type is flexible, another type + // may be more appropriate such as "unknown", "{}", or "Record". + '@typescript-eslint/no-explicit-any': 1, + // RATIONALE: The #1 rule of promises is that every promise chain must be terminated by a catch() + // handler. Thus wherever a Promise arises, the code must either append a catch handler, + // or else return the object to a caller (who assumes this responsibility). Unterminated + // promise chains are a serious issue. Besides causing errors to be silently ignored, + // they can also cause a NodeJS process to terminate unexpectedly. + '@typescript-eslint/no-floating-promises': 2, + // RATIONALE: Catches a common coding mistake. + '@typescript-eslint/no-for-in-array': 2, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-misused-new': 2, + // RATIONALE: The "namespace" keyword is not recommended for organizing code because JavaScript lacks + // a "using" statement to traverse namespaces. Nested namespaces prevent certain bundler + // optimizations. If you are declaring loose functions/variables, it's better to make them + // static members of a class, since classes support property getters and their private + // members are accessible by unit tests. Also, the exercise of choosing a meaningful + // class name tends to produce more discoverable APIs: for example, search+replacing + // the function "reverse()" is likely to return many false matches, whereas if we always + // write "Text.reverse()" is more unique. For large scale organization, it's recommended + // to decompose your code into separate NPM packages, which ensures that component + // dependencies are tracked more conscientiously. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-namespace': [ + 1, + { + 'allowDeclarations': false, + 'allowDefinitionFiles': false + } + ], + // RATIONALE: Parameter properties provide a shorthand such as "constructor(public title: string)" + // that avoids the effort of declaring "title" as a field. This TypeScript feature makes + // code easier to write, but arguably sacrifices readability: In the notes for + // "@typescript-eslint/member-ordering" we pointed out that fields are central to + // a class's design, so we wouldn't want to bury them in a constructor signature + // just to save some typing. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Set to 1 (warning) or 2 (error) to enable the rule + '@typescript-eslint/parameter-properties': 0, + // RATIONALE: When left in shipping code, unused variables often indicate a mistake. Dead code + // may impact performance. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-unused-vars': [ + 1, + { + 'vars': 'all', + // Unused function arguments often indicate a mistake in JavaScript code. However in TypeScript code, + // the compiler catches most of those mistakes, and unused arguments are fairly common for type signatures + // that are overriding a base class method or implementing an interface. + 'args': 'none' + } + ], + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-use-before-define': [ + 2, + { + 'functions': false, + 'classes': true, + 'variables': true, + 'enums': true, + 'typedefs': true + } + ], + // Disallows require statements except in import statements. + // In other words, the use of forms such as var foo = require("foo") are banned. Instead use ES6 style imports or import foo = require("foo") imports. + '@typescript-eslint/no-var-requires': 'error', + // RATIONALE: The "module" keyword is deprecated except when describing legacy libraries. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/prefer-namespace-keyword': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Rationale to disable: it's up to developer to decide if he wants to add type annotations + // Set to 1 (warning) or 2 (error) to enable the rule + '@typescript-eslint/no-inferrable-types': 0, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Rationale to disable: declaration of empty interfaces may be helpful for generic types scenarios + '@typescript-eslint/no-empty-interface': 0, + // RATIONALE: This rule warns if setters are defined without getters, which is probably a mistake. + 'accessor-pairs': 1, + // RATIONALE: In TypeScript, if you write x["y"] instead of x.y, it disables type checking. + 'dot-notation': [ + 1, + { + 'allowPattern': '^_' + } + ], + // RATIONALE: Catches code that is likely to be incorrect + 'eqeqeq': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'for-direction': 1, + // RATIONALE: Catches a common coding mistake. + 'guard-for-in': 2, + // RATIONALE: If you have more than 2,000 lines in a single source file, it's probably time + // to split up your code. + 'max-lines': ['warn', { max: 2000 }], + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-async-promise-executor': 2, + // RATIONALE: Deprecated language feature. + 'no-caller': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-compare-neg-zero': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-cond-assign': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-constant-condition': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-control-regex': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-debugger': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-delete-var': 2, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-duplicate-case': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-empty': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-empty-character-class': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-empty-pattern': 1, + // RATIONALE: Eval is a security concern and a performance concern. + 'no-eval': 1, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-ex-assign': 2, + // RATIONALE: System types are global and should not be tampered with in a scalable code base. + // If two different libraries (or two versions of the same library) both try to modify + // a type, only one of them can win. Polyfills are acceptable because they implement + // a standardized interoperable contract, but polyfills are generally coded in plain + // JavaScript. + 'no-extend-native': 1, + // Disallow unnecessary labels + 'no-extra-label': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-fallthrough': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-func-assign': 1, + // RATIONALE: Catches a common coding mistake. + 'no-implied-eval': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-invalid-regexp': 2, + // RATIONALE: Catches a common coding mistake. + 'no-label-var': 2, + // RATIONALE: Eliminates redundant code. + 'no-lone-blocks': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-misleading-character-class': 2, + // RATIONALE: Catches a common coding mistake. + 'no-multi-str': 2, + // RATIONALE: It's generally a bad practice to call "new Thing()" without assigning the result to + // a variable. Either it's part of an awkward expression like "(new Thing()).doSomething()", + // or else implies that the constructor is doing nontrivial computations, which is often + // a poor class design. + 'no-new': 1, + // RATIONALE: Obsolete language feature that is deprecated. + 'no-new-func': 2, + // RATIONALE: Obsolete language feature that is deprecated. + 'no-new-object': 2, + // RATIONALE: Obsolete notation. + 'no-new-wrappers': 1, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-octal': 2, + // RATIONALE: Catches code that is likely to be incorrect + 'no-octal-escape': 2, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-regex-spaces': 2, + // RATIONALE: Catches a common coding mistake. + 'no-return-assign': 2, + // RATIONALE: Security risk. + 'no-script-url': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-self-assign': 2, + // RATIONALE: Catches a common coding mistake. + 'no-self-compare': 2, + // RATIONALE: This avoids statements such as "while (a = next(), a && a.length);" that use + // commas to create compound expressions. In general code is more readable if each + // step is split onto a separate line. This also makes it easier to set breakpoints + // in the debugger. + 'no-sequences': 1, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-shadow-restricted-names': 2, + // RATIONALE: Obsolete language feature that is deprecated. + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-sparse-arrays': 2, + // RATIONALE: Although in theory JavaScript allows any possible data type to be thrown as an exception, + // such flexibility adds pointless complexity, by requiring every catch block to test + // the type of the object that it receives. Whereas if catch blocks can always assume + // that their object implements the "Error" contract, then the code is simpler, and + // we generally get useful additional information like a call stack. + 'no-throw-literal': 2, + // RATIONALE: Catches a common coding mistake. + 'no-unmodified-loop-condition': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-unsafe-finally': 2, + // RATIONALE: Catches a common coding mistake. + 'no-unused-expressions': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-unused-labels': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-useless-catch': 1, + // RATIONALE: Avoids a potential performance problem. + 'no-useless-concat': 1, + // RATIONALE: The "var" keyword is deprecated because of its confusing "hoisting" behavior. + // Always use "let" or "const" instead. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + 'no-var': 2, + // RATIONALE: Generally not needed in modern code. + 'no-void': 1, + // RATIONALE: Obsolete language feature that is deprecated. + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-with': 2, + // RATIONALE: Makes logic easier to understand, since constants always have a known value + // @typescript-eslint\eslint-plugin\dist\configs\eslint-recommended.js + 'prefer-const': 1, + // RATIONALE: Catches a common coding mistake where "resolve" and "reject" are confused. + 'promise/param-names': 2, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'require-atomic-updates': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'require-yield': 1, + // "Use strict" is redundant when using the TypeScript compiler. + 'strict': [ + 2, + 'never' + ], + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'use-isnan': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + // Set to 1 (warning) or 2 (error) to enable. + // Rationale to disable: !!{} + 'no-extra-boolean-cast': 0, + // ==================================================================== + // @microsoft/eslint-plugin-spfx + // ==================================================================== + '@microsoft/spfx/import-requires-chunk-name': 1, + '@microsoft/spfx/no-require-ensure': 2, + '@microsoft/spfx/pair-react-dom-render-unmount': 1 + } + }, + { + // For unit tests, we can be a little bit less strict. The settings below revise the + // defaults specified in the extended configurations, as well as above. + files: [ + // Test files + '*.test.ts', + '*.test.tsx', + '*.spec.ts', + '*.spec.tsx', + + // Facebook convention + '**/__mocks__/*.ts', + '**/__mocks__/*.tsx', + '**/__tests__/*.ts', + '**/__tests__/*.tsx', + + // Microsoft convention + '**/test/*.ts', + '**/test/*.tsx' + ], + rules: {} + } + ] +}; \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/.gitignore b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/.gitignore new file mode 100644 index 00000000000..51ca7b9e7a7 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/.gitignore @@ -0,0 +1,34 @@ +# Logs +logs +*.log +npm-debug.log* + +# Dependency directories +node_modules + +# Build generated files +dist +lib +release +solution +temp +*.sppkg +.heft + +# Coverage directory used by tools like istanbul +coverage + +# OSX +.DS_Store + +# Visual Studio files +.ntvs_analysis.dat +.vs +bin +obj + +# Resx Generated Code +*.resx.ts + +# Styles Generated Code +*.scss.ts diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/.npmignore b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/.npmignore new file mode 100644 index 00000000000..ae0b487c075 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/.npmignore @@ -0,0 +1,16 @@ +!dist +config + +gulpfile.js + +release +src +temp + +tsconfig.json +tslint.json + +*.log + +.yo-rc.json +.vscode diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/.vscode/launch.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/.vscode/launch.json new file mode 100644 index 00000000000..53ca22cd853 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Hosted workbench", + "type": "msedge", + "request": "launch", + "url": "https://{tenantDomain}/_layouts/workbench.aspx", + "webRoot": "${workspaceRoot}", + "sourceMaps": true, + "sourceMapPathOverrides": { + "webpack:///.././src/*": "${webRoot}/src/*", + "webpack:///../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../../src/*": "${webRoot}/src/*" + }, + "runtimeArgs": [ + "--remote-debugging-port=9222", + "-incognito" + ] + } + ] +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/.vscode/settings.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/.vscode/settings.json new file mode 100644 index 00000000000..a31a2c33223 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/.vscode/settings.json @@ -0,0 +1,13 @@ +// Place your settings in this file to overwrite default and user settings. +{ + // Configure glob patterns for excluding files and folders in the file explorer. + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/bower_components": true, + "**/coverage": true, + "**/lib-amd": true, + "src/**/*.scss.ts": true + }, + "typescript.tsdk": ".\\node_modules\\typescript\\lib" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/.yo-rc.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/.yo-rc.json new file mode 100644 index 00000000000..ea1a0d1485d --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/.yo-rc.json @@ -0,0 +1,26 @@ +{ + "@microsoft/generator-sharepoint": { + "whichFolder": "subdir", + "solutionName": "spfx", + "environment": "spo", + "skipFeatureDeployment": false, + "isDomainIsolated": false, + "componentType": "extension", + "extensionType": "FormCustomizer", + "template": "none", + "componentName": "HelloWorld", + "componentDescription": "HelloWorld", + "plusBeta": false, + "isCreatingSolution": true, + "nodeVersion": "18.16.0", + "sdksVersions": { + "@microsoft/microsoft-graph-client": "3.0.2", + "@microsoft/teams-js": "2.12.0" + }, + "version": "1.18.0", + "libraryName": "spfx", + "libraryId": "dcfe7724-3b8d-4122-b32a-b16dad8223ed", + "packageManager": "npm", + "solutionShortDescription": "spfx description" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/README.md b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/README.md new file mode 100644 index 00000000000..4f69a527598 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/README.md @@ -0,0 +1,73 @@ +# spfx + +## Summary + +Short summary on functionality and used technologies. + +[picture of the solution in action, if possible] + +## Used SharePoint Framework Version + +![version](https://img.shields.io/badge/version-1.18.0-green.svg) + +## Applies to + +- [SharePoint Framework](https://aka.ms/spfx) +- [Microsoft 365 tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant) + +> Get your own free development tenant by subscribing to [Microsoft 365 developer program](http://aka.ms/o365devprogram) + +## Prerequisites + +> Any special pre-requisites? + +## Solution + +| Solution | Author(s) | +| ----------- | ------------------------------------------------------- | +| folder name | Author details (name, company, twitter alias with link) | + +## Version history + +| Version | Date | Comments | +| ------- | ---------------- | --------------- | +| 1.1 | March 10, 2021 | Update comment | +| 1.0 | January 29, 2021 | Initial release | + +## Disclaimer + +**THIS CODE IS PROVIDED _AS IS_ WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.** + +--- + +## Minimal Path to Awesome + +- Clone this repository +- Ensure that you are at the solution folder +- in the command-line run: + - **npm install** + - **gulp serve** + +> Include any additional steps as needed. + +## Features + +Description of the extension that expands upon high-level summary above. + +This extension illustrates the following concepts: + +- topic 1 +- topic 2 +- topic 3 + +> Notice that better pictures and documentation will increase the sample usage and the value you are providing for others. Thanks for your submissions advance. + +> Share your web part with others through Microsoft 365 Patterns and Practices program to get visibility and exposure. More details on the community, open-source projects and other activities from http://aka.ms/m365pnp. + +## References + +- [Getting started with SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant) +- [Building for Microsoft teams](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/build-for-teams-overview) +- [Use Microsoft Graph in your solution](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/using-microsoft-graph-apis) +- [Publish SharePoint Framework applications to the Marketplace](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/publish-to-marketplace-overview) +- [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - Guidance, tooling, samples and open-source controls for your Microsoft 365 development diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/config/config.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/config/config.json new file mode 100644 index 00000000000..3051f6ffc24 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/config/config.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json", + "version": "2.0", + "bundles": { + "hello-world-form-customizer": { + "components": [ + { + "entrypoint": "./lib/extensions/helloWorld/HelloWorldFormCustomizer.js", + "manifest": "./src/extensions/helloWorld/HelloWorldFormCustomizer.manifest.json" + } + ] + } + }, + "externals": {}, + "localizedResources": { + "HelloWorldFormCustomizerStrings": "lib/extensions/helloWorld/loc/{locale}.js" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/config/deploy-azure-storage.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/config/deploy-azure-storage.json new file mode 100644 index 00000000000..02290df98af --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/config/deploy-azure-storage.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json", + "workingDir": "./release/assets/", + "account": "", + "container": "spfx", + "accessKey": "" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/config/package-solution.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/config/package-solution.json new file mode 100644 index 00000000000..97075d3216c --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/config/package-solution.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json", + "solution": { + "name": "spfx-client-side-solution", + "id": "dcfe7724-3b8d-4122-b32a-b16dad8223ed", + "version": "1.0.0.0", + "includeClientSideAssets": true, + "isDomainIsolated": false, + "developer": { + "name": "", + "websiteUrl": "", + "privacyUrl": "", + "termsOfUseUrl": "", + "mpnId": "Undefined-1.18.0" + }, + "metadata": { + "shortDescription": { + "default": "spfx description" + }, + "longDescription": { + "default": "spfx description" + }, + "screenshotPaths": [], + "videoUrl": "", + "categories": [] + }, + "features": [ + { + "title": "spfx Feature", + "description": "The feature that activates elements of the spfx solution.", + "id": "9cfdff8d-74a4-44af-b8fb-adab4bde14f5", + "version": "1.0.0.0" + } + ] + }, + "paths": { + "zippedPackage": "solution/spfx.sppkg" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/config/sass.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/config/sass.json new file mode 100644 index 00000000000..5e78c982d8d --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/config/sass.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/core-build/sass.schema.json" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/config/serve.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/config/serve.json new file mode 100644 index 00000000000..dbcaa2af91a --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/config/serve.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/spfx-serve.schema.json", + "port": 4321, + "https": true, + "serveConfigurations": { + "default": { + "pageUrl": "https://{tenantDomain}/_layouts/15/SPListForm.aspx", + "formCustomizer": { + "componentId": "31ab3a83-1af9-497a-878c-280717a6f862", + "PageType": 8, + "RootFolder": "/sites/mySite/Lists/MyList", + "properties": { + "sampleText": "Value" + } + } + }, + "helloWorld_NewForm": { + "pageUrl": "https://{tenantDomain}/_layouts/15/SPListForm.aspx", + "formCustomizer": { + "componentId": "31ab3a83-1af9-497a-878c-280717a6f862", + "PageType": 8, + "RootFolder": "/sites/mySite/Lists/MyList", + "properties": { + "sampleText": "Value" + } + } + }, + "helloWorld_EditForm": { + "pageUrl": "https://{tenantDomain}/_layouts/15/SPListForm.aspx", + "formCustomizer": { + "componentId": "31ab3a83-1af9-497a-878c-280717a6f862", + "PageType": 6, + "RootFolder": "/sites/mySite/Lists/MyList", + "ID": 1, + "properties": { + "sampleText": "Value" + } + } + }, + "helloWorld_ViewForm": { + "pageUrl": "https://{tenantDomain}/_layouts/15/SPListForm.aspx", + "formCustomizer": { + "componentId": "31ab3a83-1af9-497a-878c-280717a6f862", + "PageType": 4, + "RootFolder": "/sites/mySite/Lists/MyList", + "ID": 1, + "properties": { + "sampleText": "Value" + } + } + } + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/config/write-manifests.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/config/write-manifests.json new file mode 100644 index 00000000000..bad3526054b --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/config/write-manifests.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json", + "cdnBasePath": "" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/gulpfile.js b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/gulpfile.js new file mode 100644 index 00000000000..be2918708a7 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/gulpfile.js @@ -0,0 +1,16 @@ +'use strict'; + +const build = require('@microsoft/sp-build-web'); + +build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`); + +var getTasks = build.rig.getTasks; +build.rig.getTasks = function () { + var result = getTasks.call(build.rig); + + result.set('serve', result.get('serve-deprecated')); + + return result; +}; + +build.initialize(require('gulp')); diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/package.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/package.json new file mode 100644 index 00000000000..9aa99a69d1a --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/package.json @@ -0,0 +1,35 @@ +{ + "name": "spfx", + "version": "0.0.1", + "private": true, + "engines": { + "node": ">=16.13.0 <17.0.0 || >=18.17.1 <19.0.0" + }, + "main": "lib/index.js", + "scripts": { + "build": "gulp bundle", + "clean": "gulp clean", + "test": "gulp test" + }, + "dependencies": { + "tslib": "2.3.1", + "@microsoft/sp-core-library": "1.18.0", + "@microsoft/decorators": "1.18.0", + "@microsoft/sp-listview-extensibility": "1.18.0", + "@microsoft/sp-lodash-subset": "1.18.0", + "@microsoft/sp-office-ui-fabric-core": "1.18.0" + }, + "devDependencies": { + "@microsoft/rush-stack-compiler-4.7": "0.1.0", + "@rushstack/eslint-config": "2.5.1", + "@microsoft/eslint-plugin-spfx": "1.18.0", + "@microsoft/eslint-config-spfx": "1.18.0", + "@microsoft/sp-build-web": "1.18.0", + "@types/webpack-env": "~1.15.2", + "ajv": "^6.12.5", + "eslint": "8.7.0", + "gulp": "4.0.2", + "typescript": "4.7.4", + "@microsoft/sp-module-interfaces": "1.18.0" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/src/extensions/helloWorld/HelloWorldFormCustomizer.manifest.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/src/extensions/helloWorld/HelloWorldFormCustomizer.manifest.json new file mode 100644 index 00000000000..2af2d51fbf2 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/src/extensions/helloWorld/HelloWorldFormCustomizer.manifest.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-extension-manifest.schema.json", + + "id": "31ab3a83-1af9-497a-878c-280717a6f862", + "alias": "HelloWorldFormCustomizer", + "componentType": "Extension", + "extensionType": "FormCustomizer", + + // The "*" signifies that the version should be taken from the package.json + "version": "*", + "manifestVersion": 2, + + // If true, the component can only be installed on sites where Custom Script is allowed. + // Components that allow authors to embed arbitrary script code should set this to true. + // https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f + "requiresCustomScript": false +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/src/extensions/helloWorld/HelloWorldFormCustomizer.module.scss b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/src/extensions/helloWorld/HelloWorldFormCustomizer.module.scss new file mode 100644 index 00000000000..6f9da8dbfc3 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/src/extensions/helloWorld/HelloWorldFormCustomizer.module.scss @@ -0,0 +1,5 @@ +.helloWorld { + background-color: "[theme:white, default:#ffffff]"; + color: "[theme:themePrimary, default:#0078d4]"; + padding: 0.5rem; +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/src/extensions/helloWorld/HelloWorldFormCustomizer.ts b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/src/extensions/helloWorld/HelloWorldFormCustomizer.ts new file mode 100644 index 00000000000..3f745e3c5ca --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/src/extensions/helloWorld/HelloWorldFormCustomizer.ts @@ -0,0 +1,56 @@ +import { Log } from '@microsoft/sp-core-library'; +import { + BaseFormCustomizer +} from '@microsoft/sp-listview-extensibility'; + +import styles from './HelloWorldFormCustomizer.module.scss'; + +/** + * If your form customizer uses the ClientSideComponentProperties JSON input, + * it will be deserialized into the BaseExtension.properties object. + * You can define an interface to describe it. + */ +export interface IHelloWorldFormCustomizerProperties { + // This is an example; replace with your own property + sampleText?: string; +} + +const LOG_SOURCE: string = 'HelloWorldFormCustomizer'; + +export default class HelloWorldFormCustomizer + extends BaseFormCustomizer { + + public onInit(): Promise { + // Add your custom initialization to this method. The framework will wait + // for the returned promise to resolve before rendering the form. + Log.info(LOG_SOURCE, 'Activated HelloWorldFormCustomizer with properties:'); + Log.info(LOG_SOURCE, JSON.stringify(this.properties, undefined, 2)); + return Promise.resolve(); + } + + public render(): void { + // Use this method to perform your custom rendering. + this.domElement.innerHTML = `
`; + } + + public onDispose(): void { + // This method should be used to free any resources that were allocated during rendering. + super.onDispose(); + } + + //@ts-ignore + private _onSave = (): void => { + // TODO: Add your custom save logic here. + + // You MUST call this.formSaved() after you save the form. + this.formSaved(); + } + + //@ts-ignore + private _onClose = (): void => { + // TODO: Add your custom close logic here. + + // You MUST call this.formClosed() after you close the form. + this.formClosed(); + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/src/extensions/helloWorld/loc/en-us.js b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/src/extensions/helloWorld/loc/en-us.js new file mode 100644 index 00000000000..8b989099892 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/src/extensions/helloWorld/loc/en-us.js @@ -0,0 +1,7 @@ +define([], function() { + return { + "Save": "Save", + "Cancel": "Cancel", + "Close": "Close" + } +}); \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/src/extensions/helloWorld/loc/myStrings.d.ts b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/src/extensions/helloWorld/loc/myStrings.d.ts new file mode 100644 index 00000000000..daebd4ab487 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/src/extensions/helloWorld/loc/myStrings.d.ts @@ -0,0 +1,10 @@ +declare interface IHelloWorldFormCustomizerStrings { + Save: string; + Cancel: string; + Close: string; +} + +declare module 'HelloWorldFormCustomizerStrings' { + const strings: IHelloWorldFormCustomizerStrings; + export = strings; +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/src/index.ts b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/src/index.ts new file mode 100644 index 00000000000..fb81db1e213 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/src/index.ts @@ -0,0 +1 @@ +// A file is required to be in the root of the /src directory by the TypeScript compiler diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/tsconfig.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/tsconfig.json new file mode 100644 index 00000000000..c4cd392ad1a --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-nolib/tsconfig.json @@ -0,0 +1,35 @@ +{ + "extends": "./node_modules/@microsoft/rush-stack-compiler-4.7/includes/tsconfig-web.json", + "compilerOptions": { + "target": "es5", + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "jsx": "react", + "declaration": true, + "sourceMap": true, + "experimentalDecorators": true, + "skipLibCheck": true, + "outDir": "lib", + "inlineSources": false, + "noImplicitAny": true, + + "typeRoots": [ + "./node_modules/@types", + "./node_modules/@microsoft" + ], + "types": [ + "webpack-env" + ], + "lib": [ + "es5", + "dom", + "es2015.collection", + "es2015.promise" + ] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx" + ] +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/.eslintrc.js b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/.eslintrc.js new file mode 100644 index 00000000000..473df80cdf9 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/.eslintrc.js @@ -0,0 +1,352 @@ +require('@rushstack/eslint-config/patch/modern-module-resolution'); +module.exports = { + extends: ['@microsoft/eslint-config-spfx/lib/profiles/react'], + parserOptions: { tsconfigRootDir: __dirname }, + overrides: [ + { + files: ['*.ts', '*.tsx'], + parser: '@typescript-eslint/parser', + 'parserOptions': { + 'project': './tsconfig.json', + 'ecmaVersion': 2018, + 'sourceType': 'module' + }, + rules: { + // Prevent usage of the JavaScript null value, while allowing code to access existing APIs that may require null. https://www.npmjs.com/package/@rushstack/eslint-plugin + '@rushstack/no-new-null': 1, + // Require Jest module mocking APIs to be called before any other statements in their code block. https://www.npmjs.com/package/@rushstack/eslint-plugin + '@rushstack/hoist-jest-mock': 1, + // Require regular expressions to be constructed from string constants rather than dynamically building strings at runtime. https://www.npmjs.com/package/@rushstack/eslint-plugin-security + '@rushstack/security/no-unsafe-regexp': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/adjacent-overload-signatures': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // + // CONFIGURATION: By default, these are banned: String, Boolean, Number, Object, Symbol + '@typescript-eslint/ban-types': [ + 1, + { + 'extendDefaults': false, + 'types': { + 'String': { + 'message': 'Use \'string\' instead', + 'fixWith': 'string' + }, + 'Boolean': { + 'message': 'Use \'boolean\' instead', + 'fixWith': 'boolean' + }, + 'Number': { + 'message': 'Use \'number\' instead', + 'fixWith': 'number' + }, + 'Object': { + 'message': 'Use \'object\' instead, or else define a proper TypeScript type:' + }, + 'Symbol': { + 'message': 'Use \'symbol\' instead', + 'fixWith': 'symbol' + }, + 'Function': { + 'message': 'The \'Function\' type accepts any function-like value.\nIt provides no type safety when calling the function, which can be a common source of bugs.\nIt also accepts things like class declarations, which will throw at runtime as they will not be called with \'new\'.\nIf you are expecting the function to accept certain arguments, you should explicitly define the function shape.' + } + } + } + ], + // RATIONALE: Code is more readable when the type of every variable is immediately obvious. + // Even if the compiler may be able to infer a type, this inference will be unavailable + // to a person who is reviewing a GitHub diff. This rule makes writing code harder, + // but writing code is a much less important activity than reading it. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/explicit-function-return-type': [ + 1, + { + 'allowExpressions': true, + 'allowTypedFunctionExpressions': true, + 'allowHigherOrderFunctions': false + } + ], + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Rationale to disable: although this is a recommended rule, it is up to dev to select coding style. + // Set to 1 (warning) or 2 (error) to enable. + '@typescript-eslint/explicit-member-accessibility': 0, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-array-constructor': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // + // RATIONALE: The "any" keyword disables static type checking, the main benefit of using TypeScript. + // This rule should be suppressed only in very special cases such as JSON.stringify() + // where the type really can be anything. Even if the type is flexible, another type + // may be more appropriate such as "unknown", "{}", or "Record". + '@typescript-eslint/no-explicit-any': 1, + // RATIONALE: The #1 rule of promises is that every promise chain must be terminated by a catch() + // handler. Thus wherever a Promise arises, the code must either append a catch handler, + // or else return the object to a caller (who assumes this responsibility). Unterminated + // promise chains are a serious issue. Besides causing errors to be silently ignored, + // they can also cause a NodeJS process to terminate unexpectedly. + '@typescript-eslint/no-floating-promises': 2, + // RATIONALE: Catches a common coding mistake. + '@typescript-eslint/no-for-in-array': 2, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-misused-new': 2, + // RATIONALE: The "namespace" keyword is not recommended for organizing code because JavaScript lacks + // a "using" statement to traverse namespaces. Nested namespaces prevent certain bundler + // optimizations. If you are declaring loose functions/variables, it's better to make them + // static members of a class, since classes support property getters and their private + // members are accessible by unit tests. Also, the exercise of choosing a meaningful + // class name tends to produce more discoverable APIs: for example, search+replacing + // the function "reverse()" is likely to return many false matches, whereas if we always + // write "Text.reverse()" is more unique. For large scale organization, it's recommended + // to decompose your code into separate NPM packages, which ensures that component + // dependencies are tracked more conscientiously. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-namespace': [ + 1, + { + 'allowDeclarations': false, + 'allowDefinitionFiles': false + } + ], + // RATIONALE: Parameter properties provide a shorthand such as "constructor(public title: string)" + // that avoids the effort of declaring "title" as a field. This TypeScript feature makes + // code easier to write, but arguably sacrifices readability: In the notes for + // "@typescript-eslint/member-ordering" we pointed out that fields are central to + // a class's design, so we wouldn't want to bury them in a constructor signature + // just to save some typing. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Set to 1 (warning) or 2 (error) to enable the rule + '@typescript-eslint/parameter-properties': 0, + // RATIONALE: When left in shipping code, unused variables often indicate a mistake. Dead code + // may impact performance. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-unused-vars': [ + 1, + { + 'vars': 'all', + // Unused function arguments often indicate a mistake in JavaScript code. However in TypeScript code, + // the compiler catches most of those mistakes, and unused arguments are fairly common for type signatures + // that are overriding a base class method or implementing an interface. + 'args': 'none' + } + ], + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-use-before-define': [ + 2, + { + 'functions': false, + 'classes': true, + 'variables': true, + 'enums': true, + 'typedefs': true + } + ], + // Disallows require statements except in import statements. + // In other words, the use of forms such as var foo = require("foo") are banned. Instead use ES6 style imports or import foo = require("foo") imports. + '@typescript-eslint/no-var-requires': 'error', + // RATIONALE: The "module" keyword is deprecated except when describing legacy libraries. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/prefer-namespace-keyword': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Rationale to disable: it's up to developer to decide if he wants to add type annotations + // Set to 1 (warning) or 2 (error) to enable the rule + '@typescript-eslint/no-inferrable-types': 0, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Rationale to disable: declaration of empty interfaces may be helpful for generic types scenarios + '@typescript-eslint/no-empty-interface': 0, + // RATIONALE: This rule warns if setters are defined without getters, which is probably a mistake. + 'accessor-pairs': 1, + // RATIONALE: In TypeScript, if you write x["y"] instead of x.y, it disables type checking. + 'dot-notation': [ + 1, + { + 'allowPattern': '^_' + } + ], + // RATIONALE: Catches code that is likely to be incorrect + 'eqeqeq': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'for-direction': 1, + // RATIONALE: Catches a common coding mistake. + 'guard-for-in': 2, + // RATIONALE: If you have more than 2,000 lines in a single source file, it's probably time + // to split up your code. + 'max-lines': ['warn', { max: 2000 }], + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-async-promise-executor': 2, + // RATIONALE: Deprecated language feature. + 'no-caller': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-compare-neg-zero': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-cond-assign': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-constant-condition': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-control-regex': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-debugger': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-delete-var': 2, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-duplicate-case': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-empty': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-empty-character-class': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-empty-pattern': 1, + // RATIONALE: Eval is a security concern and a performance concern. + 'no-eval': 1, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-ex-assign': 2, + // RATIONALE: System types are global and should not be tampered with in a scalable code base. + // If two different libraries (or two versions of the same library) both try to modify + // a type, only one of them can win. Polyfills are acceptable because they implement + // a standardized interoperable contract, but polyfills are generally coded in plain + // JavaScript. + 'no-extend-native': 1, + // Disallow unnecessary labels + 'no-extra-label': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-fallthrough': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-func-assign': 1, + // RATIONALE: Catches a common coding mistake. + 'no-implied-eval': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-invalid-regexp': 2, + // RATIONALE: Catches a common coding mistake. + 'no-label-var': 2, + // RATIONALE: Eliminates redundant code. + 'no-lone-blocks': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-misleading-character-class': 2, + // RATIONALE: Catches a common coding mistake. + 'no-multi-str': 2, + // RATIONALE: It's generally a bad practice to call "new Thing()" without assigning the result to + // a variable. Either it's part of an awkward expression like "(new Thing()).doSomething()", + // or else implies that the constructor is doing nontrivial computations, which is often + // a poor class design. + 'no-new': 1, + // RATIONALE: Obsolete language feature that is deprecated. + 'no-new-func': 2, + // RATIONALE: Obsolete language feature that is deprecated. + 'no-new-object': 2, + // RATIONALE: Obsolete notation. + 'no-new-wrappers': 1, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-octal': 2, + // RATIONALE: Catches code that is likely to be incorrect + 'no-octal-escape': 2, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-regex-spaces': 2, + // RATIONALE: Catches a common coding mistake. + 'no-return-assign': 2, + // RATIONALE: Security risk. + 'no-script-url': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-self-assign': 2, + // RATIONALE: Catches a common coding mistake. + 'no-self-compare': 2, + // RATIONALE: This avoids statements such as "while (a = next(), a && a.length);" that use + // commas to create compound expressions. In general code is more readable if each + // step is split onto a separate line. This also makes it easier to set breakpoints + // in the debugger. + 'no-sequences': 1, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-shadow-restricted-names': 2, + // RATIONALE: Obsolete language feature that is deprecated. + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-sparse-arrays': 2, + // RATIONALE: Although in theory JavaScript allows any possible data type to be thrown as an exception, + // such flexibility adds pointless complexity, by requiring every catch block to test + // the type of the object that it receives. Whereas if catch blocks can always assume + // that their object implements the "Error" contract, then the code is simpler, and + // we generally get useful additional information like a call stack. + 'no-throw-literal': 2, + // RATIONALE: Catches a common coding mistake. + 'no-unmodified-loop-condition': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-unsafe-finally': 2, + // RATIONALE: Catches a common coding mistake. + 'no-unused-expressions': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-unused-labels': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-useless-catch': 1, + // RATIONALE: Avoids a potential performance problem. + 'no-useless-concat': 1, + // RATIONALE: The "var" keyword is deprecated because of its confusing "hoisting" behavior. + // Always use "let" or "const" instead. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + 'no-var': 2, + // RATIONALE: Generally not needed in modern code. + 'no-void': 1, + // RATIONALE: Obsolete language feature that is deprecated. + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-with': 2, + // RATIONALE: Makes logic easier to understand, since constants always have a known value + // @typescript-eslint\eslint-plugin\dist\configs\eslint-recommended.js + 'prefer-const': 1, + // RATIONALE: Catches a common coding mistake where "resolve" and "reject" are confused. + 'promise/param-names': 2, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'require-atomic-updates': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'require-yield': 1, + // "Use strict" is redundant when using the TypeScript compiler. + 'strict': [ + 2, + 'never' + ], + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'use-isnan': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + // Set to 1 (warning) or 2 (error) to enable. + // Rationale to disable: !!{} + 'no-extra-boolean-cast': 0, + // ==================================================================== + // @microsoft/eslint-plugin-spfx + // ==================================================================== + '@microsoft/spfx/import-requires-chunk-name': 1, + '@microsoft/spfx/no-require-ensure': 2, + '@microsoft/spfx/pair-react-dom-render-unmount': 1 + } + }, + { + // For unit tests, we can be a little bit less strict. The settings below revise the + // defaults specified in the extended configurations, as well as above. + files: [ + // Test files + '*.test.ts', + '*.test.tsx', + '*.spec.ts', + '*.spec.tsx', + + // Facebook convention + '**/__mocks__/*.ts', + '**/__mocks__/*.tsx', + '**/__tests__/*.ts', + '**/__tests__/*.tsx', + + // Microsoft convention + '**/test/*.ts', + '**/test/*.tsx' + ], + rules: {} + } + ] +}; \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/.gitignore b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/.gitignore new file mode 100644 index 00000000000..51ca7b9e7a7 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/.gitignore @@ -0,0 +1,34 @@ +# Logs +logs +*.log +npm-debug.log* + +# Dependency directories +node_modules + +# Build generated files +dist +lib +release +solution +temp +*.sppkg +.heft + +# Coverage directory used by tools like istanbul +coverage + +# OSX +.DS_Store + +# Visual Studio files +.ntvs_analysis.dat +.vs +bin +obj + +# Resx Generated Code +*.resx.ts + +# Styles Generated Code +*.scss.ts diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/.npmignore b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/.npmignore new file mode 100644 index 00000000000..ae0b487c075 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/.npmignore @@ -0,0 +1,16 @@ +!dist +config + +gulpfile.js + +release +src +temp + +tsconfig.json +tslint.json + +*.log + +.yo-rc.json +.vscode diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/.vscode/launch.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/.vscode/launch.json new file mode 100644 index 00000000000..53ca22cd853 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Hosted workbench", + "type": "msedge", + "request": "launch", + "url": "https://{tenantDomain}/_layouts/workbench.aspx", + "webRoot": "${workspaceRoot}", + "sourceMaps": true, + "sourceMapPathOverrides": { + "webpack:///.././src/*": "${webRoot}/src/*", + "webpack:///../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../../src/*": "${webRoot}/src/*" + }, + "runtimeArgs": [ + "--remote-debugging-port=9222", + "-incognito" + ] + } + ] +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/.vscode/settings.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/.vscode/settings.json new file mode 100644 index 00000000000..a31a2c33223 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/.vscode/settings.json @@ -0,0 +1,13 @@ +// Place your settings in this file to overwrite default and user settings. +{ + // Configure glob patterns for excluding files and folders in the file explorer. + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/bower_components": true, + "**/coverage": true, + "**/lib-amd": true, + "src/**/*.scss.ts": true + }, + "typescript.tsdk": ".\\node_modules\\typescript\\lib" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/.yo-rc.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/.yo-rc.json new file mode 100644 index 00000000000..09acd5cda48 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/.yo-rc.json @@ -0,0 +1,26 @@ +{ + "@microsoft/generator-sharepoint": { + "whichFolder": "subdir", + "solutionName": "spfx", + "environment": "spo", + "skipFeatureDeployment": false, + "isDomainIsolated": false, + "componentType": "extension", + "extensionType": "FormCustomizer", + "template": "react", + "componentName": "HelloWorld", + "componentDescription": "HelloWorld", + "plusBeta": false, + "isCreatingSolution": true, + "nodeVersion": "18.16.0", + "sdksVersions": { + "@microsoft/microsoft-graph-client": "3.0.2", + "@microsoft/teams-js": "2.12.0" + }, + "version": "1.18.0", + "libraryName": "spfx", + "libraryId": "e7fa4a2a-d9a9-4c63-923c-81fab048b215", + "packageManager": "npm", + "solutionShortDescription": "spfx description" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/README.md b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/README.md new file mode 100644 index 00000000000..4f69a527598 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/README.md @@ -0,0 +1,73 @@ +# spfx + +## Summary + +Short summary on functionality and used technologies. + +[picture of the solution in action, if possible] + +## Used SharePoint Framework Version + +![version](https://img.shields.io/badge/version-1.18.0-green.svg) + +## Applies to + +- [SharePoint Framework](https://aka.ms/spfx) +- [Microsoft 365 tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant) + +> Get your own free development tenant by subscribing to [Microsoft 365 developer program](http://aka.ms/o365devprogram) + +## Prerequisites + +> Any special pre-requisites? + +## Solution + +| Solution | Author(s) | +| ----------- | ------------------------------------------------------- | +| folder name | Author details (name, company, twitter alias with link) | + +## Version history + +| Version | Date | Comments | +| ------- | ---------------- | --------------- | +| 1.1 | March 10, 2021 | Update comment | +| 1.0 | January 29, 2021 | Initial release | + +## Disclaimer + +**THIS CODE IS PROVIDED _AS IS_ WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.** + +--- + +## Minimal Path to Awesome + +- Clone this repository +- Ensure that you are at the solution folder +- in the command-line run: + - **npm install** + - **gulp serve** + +> Include any additional steps as needed. + +## Features + +Description of the extension that expands upon high-level summary above. + +This extension illustrates the following concepts: + +- topic 1 +- topic 2 +- topic 3 + +> Notice that better pictures and documentation will increase the sample usage and the value you are providing for others. Thanks for your submissions advance. + +> Share your web part with others through Microsoft 365 Patterns and Practices program to get visibility and exposure. More details on the community, open-source projects and other activities from http://aka.ms/m365pnp. + +## References + +- [Getting started with SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant) +- [Building for Microsoft teams](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/build-for-teams-overview) +- [Use Microsoft Graph in your solution](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/using-microsoft-graph-apis) +- [Publish SharePoint Framework applications to the Marketplace](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/publish-to-marketplace-overview) +- [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - Guidance, tooling, samples and open-source controls for your Microsoft 365 development diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/config/config.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/config/config.json new file mode 100644 index 00000000000..3051f6ffc24 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/config/config.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json", + "version": "2.0", + "bundles": { + "hello-world-form-customizer": { + "components": [ + { + "entrypoint": "./lib/extensions/helloWorld/HelloWorldFormCustomizer.js", + "manifest": "./src/extensions/helloWorld/HelloWorldFormCustomizer.manifest.json" + } + ] + } + }, + "externals": {}, + "localizedResources": { + "HelloWorldFormCustomizerStrings": "lib/extensions/helloWorld/loc/{locale}.js" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/config/deploy-azure-storage.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/config/deploy-azure-storage.json new file mode 100644 index 00000000000..02290df98af --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/config/deploy-azure-storage.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json", + "workingDir": "./release/assets/", + "account": "", + "container": "spfx", + "accessKey": "" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/config/package-solution.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/config/package-solution.json new file mode 100644 index 00000000000..31f37b854dd --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/config/package-solution.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json", + "solution": { + "name": "spfx-client-side-solution", + "id": "e7fa4a2a-d9a9-4c63-923c-81fab048b215", + "version": "1.0.0.0", + "includeClientSideAssets": true, + "isDomainIsolated": false, + "developer": { + "name": "", + "websiteUrl": "", + "privacyUrl": "", + "termsOfUseUrl": "", + "mpnId": "Undefined-1.18.0" + }, + "metadata": { + "shortDescription": { + "default": "spfx description" + }, + "longDescription": { + "default": "spfx description" + }, + "screenshotPaths": [], + "videoUrl": "", + "categories": [] + }, + "features": [ + { + "title": "spfx Feature", + "description": "The feature that activates elements of the spfx solution.", + "id": "006d304e-ce1d-45ca-a57d-a97272ce71ec", + "version": "1.0.0.0" + } + ] + }, + "paths": { + "zippedPackage": "solution/spfx.sppkg" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/config/sass.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/config/sass.json new file mode 100644 index 00000000000..5e78c982d8d --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/config/sass.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/core-build/sass.schema.json" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/config/serve.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/config/serve.json new file mode 100644 index 00000000000..77a89c49397 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/config/serve.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/spfx-serve.schema.json", + "port": 4321, + "https": true, + "serveConfigurations": { + "default": { + "pageUrl": "https://{tenantDomain}/_layouts/15/SPListForm.aspx", + "formCustomizer": { + "componentId": "5627adf3-310f-4187-b7c3-ec6a602f195d", + "PageType": 8, + "RootFolder": "/sites/mySite/Lists/MyList", + "properties": { + "sampleText": "Value" + } + } + }, + "helloWorld_NewForm": { + "pageUrl": "https://{tenantDomain}/_layouts/15/SPListForm.aspx", + "formCustomizer": { + "componentId": "5627adf3-310f-4187-b7c3-ec6a602f195d", + "PageType": 8, + "RootFolder": "/sites/mySite/Lists/MyList", + "properties": { + "sampleText": "Value" + } + } + }, + "helloWorld_EditForm": { + "pageUrl": "https://{tenantDomain}/_layouts/15/SPListForm.aspx", + "formCustomizer": { + "componentId": "5627adf3-310f-4187-b7c3-ec6a602f195d", + "PageType": 6, + "RootFolder": "/sites/mySite/Lists/MyList", + "ID": 1, + "properties": { + "sampleText": "Value" + } + } + }, + "helloWorld_ViewForm": { + "pageUrl": "https://{tenantDomain}/_layouts/15/SPListForm.aspx", + "formCustomizer": { + "componentId": "5627adf3-310f-4187-b7c3-ec6a602f195d", + "PageType": 4, + "RootFolder": "/sites/mySite/Lists/MyList", + "ID": 1, + "properties": { + "sampleText": "Value" + } + } + } + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/config/write-manifests.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/config/write-manifests.json new file mode 100644 index 00000000000..bad3526054b --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/config/write-manifests.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json", + "cdnBasePath": "" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/gulpfile.js b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/gulpfile.js new file mode 100644 index 00000000000..be2918708a7 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/gulpfile.js @@ -0,0 +1,16 @@ +'use strict'; + +const build = require('@microsoft/sp-build-web'); + +build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`); + +var getTasks = build.rig.getTasks; +build.rig.getTasks = function () { + var result = getTasks.call(build.rig); + + result.set('serve', result.get('serve-deprecated')); + + return result; +}; + +build.initialize(require('gulp')); diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/package.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/package.json new file mode 100644 index 00000000000..e040fd5eddf --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/package.json @@ -0,0 +1,41 @@ +{ + "name": "spfx", + "version": "0.0.1", + "private": true, + "engines": { + "node": ">=16.13.0 <17.0.0 || >=18.17.1 <19.0.0" + }, + "main": "lib/index.js", + "scripts": { + "build": "gulp bundle", + "clean": "gulp clean", + "test": "gulp test" + }, + "dependencies": { + "tslib": "2.3.1", + "react": "17.0.1", + "react-dom": "17.0.1", + "@fluentui/react": "^8.106.4", + "@microsoft/sp-core-library": "1.18.0", + "@microsoft/decorators": "1.18.0", + "@microsoft/sp-listview-extensibility": "1.18.0", + "@microsoft/sp-lodash-subset": "1.18.0", + "@microsoft/sp-office-ui-fabric-core": "1.18.0" + }, + "devDependencies": { + "@microsoft/rush-stack-compiler-4.7": "0.1.0", + "@rushstack/eslint-config": "2.5.1", + "@microsoft/eslint-plugin-spfx": "1.18.0", + "@microsoft/eslint-config-spfx": "1.18.0", + "@microsoft/sp-build-web": "1.18.0", + "@types/webpack-env": "~1.15.2", + "ajv": "^6.12.5", + "eslint": "8.7.0", + "gulp": "4.0.2", + "typescript": "4.7.4", + "@types/react": "17.0.45", + "@types/react-dom": "17.0.17", + "eslint-plugin-react-hooks": "4.3.0", + "@microsoft/sp-module-interfaces": "1.18.0" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/src/extensions/helloWorld/HelloWorldFormCustomizer.manifest.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/src/extensions/helloWorld/HelloWorldFormCustomizer.manifest.json new file mode 100644 index 00000000000..41e6b7e1de3 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/src/extensions/helloWorld/HelloWorldFormCustomizer.manifest.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-extension-manifest.schema.json", + + "id": "5627adf3-310f-4187-b7c3-ec6a602f195d", + "alias": "HelloWorldFormCustomizer", + "componentType": "Extension", + "extensionType": "FormCustomizer", + + // The "*" signifies that the version should be taken from the package.json + "version": "*", + "manifestVersion": 2, + + // If true, the component can only be installed on sites where Custom Script is allowed. + // Components that allow authors to embed arbitrary script code should set this to true. + // https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f + "requiresCustomScript": false +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/src/extensions/helloWorld/HelloWorldFormCustomizer.ts b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/src/extensions/helloWorld/HelloWorldFormCustomizer.ts new file mode 100644 index 00000000000..8cd8c9e602c --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/src/extensions/helloWorld/HelloWorldFormCustomizer.ts @@ -0,0 +1,64 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; + +import { Log } from '@microsoft/sp-core-library'; +import { + BaseFormCustomizer +} from '@microsoft/sp-listview-extensibility'; + +import HelloWorld, { IHelloWorldProps } from './components/HelloWorld'; + +/** + * If your form customizer uses the ClientSideComponentProperties JSON input, + * it will be deserialized into the BaseExtension.properties object. + * You can define an interface to describe it. + */ +export interface IHelloWorldFormCustomizerProperties { + // This is an example; replace with your own property + sampleText?: string; +} + +const LOG_SOURCE: string = 'HelloWorldFormCustomizer'; + +export default class HelloWorldFormCustomizer + extends BaseFormCustomizer { + + public onInit(): Promise { + // Add your custom initialization to this method. The framework will wait + // for the returned promise to resolve before rendering the form. + Log.info(LOG_SOURCE, 'Activated HelloWorldFormCustomizer with properties:'); + Log.info(LOG_SOURCE, JSON.stringify(this.properties, undefined, 2)); + return Promise.resolve(); + } + + public render(): void { + // Use this method to perform your custom rendering. + + const helloWorld: React.ReactElement<{}> = + React.createElement(HelloWorld, { + context: this.context, + displayMode: this.displayMode, + onSave: this._onSave, + onClose: this._onClose + } as IHelloWorldProps); + + ReactDOM.render(helloWorld, this.domElement); + } + + public onDispose(): void { + // This method should be used to free any resources that were allocated during rendering. + ReactDOM.unmountComponentAtNode(this.domElement); + super.onDispose(); + } + + private _onSave = (): void => { + + // You MUST call this.formSaved() after you save the form. + this.formSaved(); + } + + private _onClose = (): void => { + // You MUST call this.formClosed() after you close the form. + this.formClosed(); + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/src/extensions/helloWorld/components/HelloWorld.module.scss b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/src/extensions/helloWorld/components/HelloWorld.module.scss new file mode 100644 index 00000000000..6f9da8dbfc3 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/src/extensions/helloWorld/components/HelloWorld.module.scss @@ -0,0 +1,5 @@ +.helloWorld { + background-color: "[theme:white, default:#ffffff]"; + color: "[theme:themePrimary, default:#0078d4]"; + padding: 0.5rem; +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/src/extensions/helloWorld/components/HelloWorld.tsx b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/src/extensions/helloWorld/components/HelloWorld.tsx new file mode 100644 index 00000000000..2eb7dc1496b --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/src/extensions/helloWorld/components/HelloWorld.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { Log, FormDisplayMode } from '@microsoft/sp-core-library'; +import { FormCustomizerContext } from '@microsoft/sp-listview-extensibility'; + +import styles from './HelloWorld.module.scss'; + +export interface IHelloWorldProps { + context: FormCustomizerContext; + displayMode: FormDisplayMode; + onSave: () => void; + onClose: () => void; +} + +const LOG_SOURCE: string = 'HelloWorld'; + +export default class HelloWorld extends React.Component { + public componentDidMount(): void { + Log.info(LOG_SOURCE, 'React Element: HelloWorld mounted'); + } + + public componentWillUnmount(): void { + Log.info(LOG_SOURCE, 'React Element: HelloWorld unmounted'); + } + + public render(): React.ReactElement<{}> { + return
; + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/src/extensions/helloWorld/loc/en-us.js b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/src/extensions/helloWorld/loc/en-us.js new file mode 100644 index 00000000000..8b989099892 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/src/extensions/helloWorld/loc/en-us.js @@ -0,0 +1,7 @@ +define([], function() { + return { + "Save": "Save", + "Cancel": "Cancel", + "Close": "Close" + } +}); \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/src/extensions/helloWorld/loc/myStrings.d.ts b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/src/extensions/helloWorld/loc/myStrings.d.ts new file mode 100644 index 00000000000..daebd4ab487 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/src/extensions/helloWorld/loc/myStrings.d.ts @@ -0,0 +1,10 @@ +declare interface IHelloWorldFormCustomizerStrings { + Save: string; + Cancel: string; + Close: string; +} + +declare module 'HelloWorldFormCustomizerStrings' { + const strings: IHelloWorldFormCustomizerStrings; + export = strings; +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/src/index.ts b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/src/index.ts new file mode 100644 index 00000000000..fb81db1e213 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/src/index.ts @@ -0,0 +1 @@ +// A file is required to be in the root of the /src directory by the TypeScript compiler diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/tsconfig.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/tsconfig.json new file mode 100644 index 00000000000..c4cd392ad1a --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-formcustomizer-react/tsconfig.json @@ -0,0 +1,35 @@ +{ + "extends": "./node_modules/@microsoft/rush-stack-compiler-4.7/includes/tsconfig-web.json", + "compilerOptions": { + "target": "es5", + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "jsx": "react", + "declaration": true, + "sourceMap": true, + "experimentalDecorators": true, + "skipLibCheck": true, + "outDir": "lib", + "inlineSources": false, + "noImplicitAny": true, + + "typeRoots": [ + "./node_modules/@types", + "./node_modules/@microsoft" + ], + "types": [ + "webpack-env" + ], + "lib": [ + "es5", + "dom", + "es2015.collection", + "es2015.promise" + ] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx" + ] +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/.eslintrc.js b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/.eslintrc.js new file mode 100644 index 00000000000..ef68d0e95c0 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/.eslintrc.js @@ -0,0 +1,352 @@ +require('@rushstack/eslint-config/patch/modern-module-resolution'); +module.exports = { + extends: ['@microsoft/eslint-config-spfx/lib/profiles/default'], + parserOptions: { tsconfigRootDir: __dirname }, + overrides: [ + { + files: ['*.ts', '*.tsx'], + parser: '@typescript-eslint/parser', + 'parserOptions': { + 'project': './tsconfig.json', + 'ecmaVersion': 2018, + 'sourceType': 'module' + }, + rules: { + // Prevent usage of the JavaScript null value, while allowing code to access existing APIs that may require null. https://www.npmjs.com/package/@rushstack/eslint-plugin + '@rushstack/no-new-null': 1, + // Require Jest module mocking APIs to be called before any other statements in their code block. https://www.npmjs.com/package/@rushstack/eslint-plugin + '@rushstack/hoist-jest-mock': 1, + // Require regular expressions to be constructed from string constants rather than dynamically building strings at runtime. https://www.npmjs.com/package/@rushstack/eslint-plugin-security + '@rushstack/security/no-unsafe-regexp': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/adjacent-overload-signatures': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // + // CONFIGURATION: By default, these are banned: String, Boolean, Number, Object, Symbol + '@typescript-eslint/ban-types': [ + 1, + { + 'extendDefaults': false, + 'types': { + 'String': { + 'message': 'Use \'string\' instead', + 'fixWith': 'string' + }, + 'Boolean': { + 'message': 'Use \'boolean\' instead', + 'fixWith': 'boolean' + }, + 'Number': { + 'message': 'Use \'number\' instead', + 'fixWith': 'number' + }, + 'Object': { + 'message': 'Use \'object\' instead, or else define a proper TypeScript type:' + }, + 'Symbol': { + 'message': 'Use \'symbol\' instead', + 'fixWith': 'symbol' + }, + 'Function': { + 'message': 'The \'Function\' type accepts any function-like value.\nIt provides no type safety when calling the function, which can be a common source of bugs.\nIt also accepts things like class declarations, which will throw at runtime as they will not be called with \'new\'.\nIf you are expecting the function to accept certain arguments, you should explicitly define the function shape.' + } + } + } + ], + // RATIONALE: Code is more readable when the type of every variable is immediately obvious. + // Even if the compiler may be able to infer a type, this inference will be unavailable + // to a person who is reviewing a GitHub diff. This rule makes writing code harder, + // but writing code is a much less important activity than reading it. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/explicit-function-return-type': [ + 1, + { + 'allowExpressions': true, + 'allowTypedFunctionExpressions': true, + 'allowHigherOrderFunctions': false + } + ], + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Rationale to disable: although this is a recommended rule, it is up to dev to select coding style. + // Set to 1 (warning) or 2 (error) to enable. + '@typescript-eslint/explicit-member-accessibility': 0, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-array-constructor': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // + // RATIONALE: The "any" keyword disables static type checking, the main benefit of using TypeScript. + // This rule should be suppressed only in very special cases such as JSON.stringify() + // where the type really can be anything. Even if the type is flexible, another type + // may be more appropriate such as "unknown", "{}", or "Record". + '@typescript-eslint/no-explicit-any': 1, + // RATIONALE: The #1 rule of promises is that every promise chain must be terminated by a catch() + // handler. Thus wherever a Promise arises, the code must either append a catch handler, + // or else return the object to a caller (who assumes this responsibility). Unterminated + // promise chains are a serious issue. Besides causing errors to be silently ignored, + // they can also cause a NodeJS process to terminate unexpectedly. + '@typescript-eslint/no-floating-promises': 2, + // RATIONALE: Catches a common coding mistake. + '@typescript-eslint/no-for-in-array': 2, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-misused-new': 2, + // RATIONALE: The "namespace" keyword is not recommended for organizing code because JavaScript lacks + // a "using" statement to traverse namespaces. Nested namespaces prevent certain bundler + // optimizations. If you are declaring loose functions/variables, it's better to make them + // static members of a class, since classes support property getters and their private + // members are accessible by unit tests. Also, the exercise of choosing a meaningful + // class name tends to produce more discoverable APIs: for example, search+replacing + // the function "reverse()" is likely to return many false matches, whereas if we always + // write "Text.reverse()" is more unique. For large scale organization, it's recommended + // to decompose your code into separate NPM packages, which ensures that component + // dependencies are tracked more conscientiously. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-namespace': [ + 1, + { + 'allowDeclarations': false, + 'allowDefinitionFiles': false + } + ], + // RATIONALE: Parameter properties provide a shorthand such as "constructor(public title: string)" + // that avoids the effort of declaring "title" as a field. This TypeScript feature makes + // code easier to write, but arguably sacrifices readability: In the notes for + // "@typescript-eslint/member-ordering" we pointed out that fields are central to + // a class's design, so we wouldn't want to bury them in a constructor signature + // just to save some typing. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Set to 1 (warning) or 2 (error) to enable the rule + '@typescript-eslint/parameter-properties': 0, + // RATIONALE: When left in shipping code, unused variables often indicate a mistake. Dead code + // may impact performance. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-unused-vars': [ + 1, + { + 'vars': 'all', + // Unused function arguments often indicate a mistake in JavaScript code. However in TypeScript code, + // the compiler catches most of those mistakes, and unused arguments are fairly common for type signatures + // that are overriding a base class method or implementing an interface. + 'args': 'none' + } + ], + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-use-before-define': [ + 2, + { + 'functions': false, + 'classes': true, + 'variables': true, + 'enums': true, + 'typedefs': true + } + ], + // Disallows require statements except in import statements. + // In other words, the use of forms such as var foo = require("foo") are banned. Instead use ES6 style imports or import foo = require("foo") imports. + '@typescript-eslint/no-var-requires': 'error', + // RATIONALE: The "module" keyword is deprecated except when describing legacy libraries. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/prefer-namespace-keyword': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Rationale to disable: it's up to developer to decide if he wants to add type annotations + // Set to 1 (warning) or 2 (error) to enable the rule + '@typescript-eslint/no-inferrable-types': 0, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Rationale to disable: declaration of empty interfaces may be helpful for generic types scenarios + '@typescript-eslint/no-empty-interface': 0, + // RATIONALE: This rule warns if setters are defined without getters, which is probably a mistake. + 'accessor-pairs': 1, + // RATIONALE: In TypeScript, if you write x["y"] instead of x.y, it disables type checking. + 'dot-notation': [ + 1, + { + 'allowPattern': '^_' + } + ], + // RATIONALE: Catches code that is likely to be incorrect + 'eqeqeq': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'for-direction': 1, + // RATIONALE: Catches a common coding mistake. + 'guard-for-in': 2, + // RATIONALE: If you have more than 2,000 lines in a single source file, it's probably time + // to split up your code. + 'max-lines': ['warn', { max: 2000 }], + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-async-promise-executor': 2, + // RATIONALE: Deprecated language feature. + 'no-caller': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-compare-neg-zero': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-cond-assign': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-constant-condition': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-control-regex': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-debugger': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-delete-var': 2, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-duplicate-case': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-empty': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-empty-character-class': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-empty-pattern': 1, + // RATIONALE: Eval is a security concern and a performance concern. + 'no-eval': 1, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-ex-assign': 2, + // RATIONALE: System types are global and should not be tampered with in a scalable code base. + // If two different libraries (or two versions of the same library) both try to modify + // a type, only one of them can win. Polyfills are acceptable because they implement + // a standardized interoperable contract, but polyfills are generally coded in plain + // JavaScript. + 'no-extend-native': 1, + // Disallow unnecessary labels + 'no-extra-label': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-fallthrough': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-func-assign': 1, + // RATIONALE: Catches a common coding mistake. + 'no-implied-eval': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-invalid-regexp': 2, + // RATIONALE: Catches a common coding mistake. + 'no-label-var': 2, + // RATIONALE: Eliminates redundant code. + 'no-lone-blocks': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-misleading-character-class': 2, + // RATIONALE: Catches a common coding mistake. + 'no-multi-str': 2, + // RATIONALE: It's generally a bad practice to call "new Thing()" without assigning the result to + // a variable. Either it's part of an awkward expression like "(new Thing()).doSomething()", + // or else implies that the constructor is doing nontrivial computations, which is often + // a poor class design. + 'no-new': 1, + // RATIONALE: Obsolete language feature that is deprecated. + 'no-new-func': 2, + // RATIONALE: Obsolete language feature that is deprecated. + 'no-new-object': 2, + // RATIONALE: Obsolete notation. + 'no-new-wrappers': 1, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-octal': 2, + // RATIONALE: Catches code that is likely to be incorrect + 'no-octal-escape': 2, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-regex-spaces': 2, + // RATIONALE: Catches a common coding mistake. + 'no-return-assign': 2, + // RATIONALE: Security risk. + 'no-script-url': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-self-assign': 2, + // RATIONALE: Catches a common coding mistake. + 'no-self-compare': 2, + // RATIONALE: This avoids statements such as "while (a = next(), a && a.length);" that use + // commas to create compound expressions. In general code is more readable if each + // step is split onto a separate line. This also makes it easier to set breakpoints + // in the debugger. + 'no-sequences': 1, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-shadow-restricted-names': 2, + // RATIONALE: Obsolete language feature that is deprecated. + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-sparse-arrays': 2, + // RATIONALE: Although in theory JavaScript allows any possible data type to be thrown as an exception, + // such flexibility adds pointless complexity, by requiring every catch block to test + // the type of the object that it receives. Whereas if catch blocks can always assume + // that their object implements the "Error" contract, then the code is simpler, and + // we generally get useful additional information like a call stack. + 'no-throw-literal': 2, + // RATIONALE: Catches a common coding mistake. + 'no-unmodified-loop-condition': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-unsafe-finally': 2, + // RATIONALE: Catches a common coding mistake. + 'no-unused-expressions': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-unused-labels': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-useless-catch': 1, + // RATIONALE: Avoids a potential performance problem. + 'no-useless-concat': 1, + // RATIONALE: The "var" keyword is deprecated because of its confusing "hoisting" behavior. + // Always use "let" or "const" instead. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + 'no-var': 2, + // RATIONALE: Generally not needed in modern code. + 'no-void': 1, + // RATIONALE: Obsolete language feature that is deprecated. + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-with': 2, + // RATIONALE: Makes logic easier to understand, since constants always have a known value + // @typescript-eslint\eslint-plugin\dist\configs\eslint-recommended.js + 'prefer-const': 1, + // RATIONALE: Catches a common coding mistake where "resolve" and "reject" are confused. + 'promise/param-names': 2, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'require-atomic-updates': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'require-yield': 1, + // "Use strict" is redundant when using the TypeScript compiler. + 'strict': [ + 2, + 'never' + ], + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'use-isnan': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + // Set to 1 (warning) or 2 (error) to enable. + // Rationale to disable: !!{} + 'no-extra-boolean-cast': 0, + // ==================================================================== + // @microsoft/eslint-plugin-spfx + // ==================================================================== + '@microsoft/spfx/import-requires-chunk-name': 1, + '@microsoft/spfx/no-require-ensure': 2, + '@microsoft/spfx/pair-react-dom-render-unmount': 1 + } + }, + { + // For unit tests, we can be a little bit less strict. The settings below revise the + // defaults specified in the extended configurations, as well as above. + files: [ + // Test files + '*.test.ts', + '*.test.tsx', + '*.spec.ts', + '*.spec.tsx', + + // Facebook convention + '**/__mocks__/*.ts', + '**/__mocks__/*.tsx', + '**/__tests__/*.ts', + '**/__tests__/*.tsx', + + // Microsoft convention + '**/test/*.ts', + '**/test/*.tsx' + ], + rules: {} + } + ] +}; \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/.gitignore b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/.gitignore new file mode 100644 index 00000000000..51ca7b9e7a7 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/.gitignore @@ -0,0 +1,34 @@ +# Logs +logs +*.log +npm-debug.log* + +# Dependency directories +node_modules + +# Build generated files +dist +lib +release +solution +temp +*.sppkg +.heft + +# Coverage directory used by tools like istanbul +coverage + +# OSX +.DS_Store + +# Visual Studio files +.ntvs_analysis.dat +.vs +bin +obj + +# Resx Generated Code +*.resx.ts + +# Styles Generated Code +*.scss.ts diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/.npmignore b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/.npmignore new file mode 100644 index 00000000000..ae0b487c075 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/.npmignore @@ -0,0 +1,16 @@ +!dist +config + +gulpfile.js + +release +src +temp + +tsconfig.json +tslint.json + +*.log + +.yo-rc.json +.vscode diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/.vscode/launch.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/.vscode/launch.json new file mode 100644 index 00000000000..53ca22cd853 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Hosted workbench", + "type": "msedge", + "request": "launch", + "url": "https://{tenantDomain}/_layouts/workbench.aspx", + "webRoot": "${workspaceRoot}", + "sourceMaps": true, + "sourceMapPathOverrides": { + "webpack:///.././src/*": "${webRoot}/src/*", + "webpack:///../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../../src/*": "${webRoot}/src/*" + }, + "runtimeArgs": [ + "--remote-debugging-port=9222", + "-incognito" + ] + } + ] +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/.vscode/settings.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/.vscode/settings.json new file mode 100644 index 00000000000..a31a2c33223 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/.vscode/settings.json @@ -0,0 +1,13 @@ +// Place your settings in this file to overwrite default and user settings. +{ + // Configure glob patterns for excluding files and folders in the file explorer. + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/bower_components": true, + "**/coverage": true, + "**/lib-amd": true, + "src/**/*.scss.ts": true + }, + "typescript.tsdk": ".\\node_modules\\typescript\\lib" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/.yo-rc.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/.yo-rc.json new file mode 100644 index 00000000000..59b28d90f24 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/.yo-rc.json @@ -0,0 +1,25 @@ +{ + "@microsoft/generator-sharepoint": { + "whichFolder": "subdir", + "solutionName": "spfx", + "environment": "spo", + "skipFeatureDeployment": false, + "isDomainIsolated": false, + "componentType": "extension", + "extensionType": "ListViewCommandSet", + "componentName": "HelloWorld", + "componentDescription": "HelloWorld", + "plusBeta": false, + "isCreatingSolution": true, + "nodeVersion": "18.16.0", + "sdksVersions": { + "@microsoft/microsoft-graph-client": "3.0.2", + "@microsoft/teams-js": "2.12.0" + }, + "version": "1.18.0", + "libraryName": "spfx", + "libraryId": "3fa3d57f-6a75-4178-b775-352d336d863c", + "packageManager": "npm", + "solutionShortDescription": "spfx description" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/README.md b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/README.md new file mode 100644 index 00000000000..4f69a527598 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/README.md @@ -0,0 +1,73 @@ +# spfx + +## Summary + +Short summary on functionality and used technologies. + +[picture of the solution in action, if possible] + +## Used SharePoint Framework Version + +![version](https://img.shields.io/badge/version-1.18.0-green.svg) + +## Applies to + +- [SharePoint Framework](https://aka.ms/spfx) +- [Microsoft 365 tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant) + +> Get your own free development tenant by subscribing to [Microsoft 365 developer program](http://aka.ms/o365devprogram) + +## Prerequisites + +> Any special pre-requisites? + +## Solution + +| Solution | Author(s) | +| ----------- | ------------------------------------------------------- | +| folder name | Author details (name, company, twitter alias with link) | + +## Version history + +| Version | Date | Comments | +| ------- | ---------------- | --------------- | +| 1.1 | March 10, 2021 | Update comment | +| 1.0 | January 29, 2021 | Initial release | + +## Disclaimer + +**THIS CODE IS PROVIDED _AS IS_ WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.** + +--- + +## Minimal Path to Awesome + +- Clone this repository +- Ensure that you are at the solution folder +- in the command-line run: + - **npm install** + - **gulp serve** + +> Include any additional steps as needed. + +## Features + +Description of the extension that expands upon high-level summary above. + +This extension illustrates the following concepts: + +- topic 1 +- topic 2 +- topic 3 + +> Notice that better pictures and documentation will increase the sample usage and the value you are providing for others. Thanks for your submissions advance. + +> Share your web part with others through Microsoft 365 Patterns and Practices program to get visibility and exposure. More details on the community, open-source projects and other activities from http://aka.ms/m365pnp. + +## References + +- [Getting started with SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant) +- [Building for Microsoft teams](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/build-for-teams-overview) +- [Use Microsoft Graph in your solution](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/using-microsoft-graph-apis) +- [Publish SharePoint Framework applications to the Marketplace](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/publish-to-marketplace-overview) +- [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - Guidance, tooling, samples and open-source controls for your Microsoft 365 development diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/config/config.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/config/config.json new file mode 100644 index 00000000000..ae77f0fffb6 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/config/config.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json", + "version": "2.0", + "bundles": { + "hello-world-command-set": { + "components": [ + { + "entrypoint": "./lib/extensions/helloWorld/HelloWorldCommandSet.js", + "manifest": "./src/extensions/helloWorld/HelloWorldCommandSet.manifest.json" + } + ] + } + }, + "externals": {}, + "localizedResources": { + "HelloWorldCommandSetStrings": "lib/extensions/helloWorld/loc/{locale}.js" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/config/deploy-azure-storage.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/config/deploy-azure-storage.json new file mode 100644 index 00000000000..02290df98af --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/config/deploy-azure-storage.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json", + "workingDir": "./release/assets/", + "account": "", + "container": "spfx", + "accessKey": "" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/config/package-solution.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/config/package-solution.json new file mode 100644 index 00000000000..df6a5555e8d --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/config/package-solution.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json", + "solution": { + "name": "spfx-client-side-solution", + "id": "3fa3d57f-6a75-4178-b775-352d336d863c", + "version": "1.0.0.0", + "includeClientSideAssets": true, + "isDomainIsolated": false, + "developer": { + "name": "", + "websiteUrl": "", + "privacyUrl": "", + "termsOfUseUrl": "", + "mpnId": "Undefined-1.18.0" + }, + "metadata": { + "shortDescription": { + "default": "spfx description" + }, + "longDescription": { + "default": "spfx description" + }, + "screenshotPaths": [], + "videoUrl": "", + "categories": [] + }, + "features": [ + { + "title": "Application Extension - Deployment of custom action", + "description": "Deploys a custom action with ClientSideComponentId association", + "id": "2743545e-d974-41b7-97c4-324d38a8a58d", + "version": "1.0.0.0", + "assets": { + "elementManifests": [ + "elements.xml", + "ClientSideInstance.xml" + ] + } + } + ] + }, + "paths": { + "zippedPackage": "solution/spfx.sppkg" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/config/sass.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/config/sass.json new file mode 100644 index 00000000000..5e78c982d8d --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/config/sass.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/core-build/sass.schema.json" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/config/serve.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/config/serve.json new file mode 100644 index 00000000000..468b3847b94 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/config/serve.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/spfx-serve.schema.json", + "port": 4321, + "https": true, + "serveConfigurations": { + "default": { + "pageUrl": "https://{tenantDomain}/SitePages/myPage.aspx", + "customActions": { + "327f916d-e546-461e-befb-3c4f35611e65": { + "location": "ClientSideExtension.ListViewCommandSet.CommandBar", + "properties": { + "sampleTextOne": "One item is selected in the list", + "sampleTextTwo": "This command is always visible." + } + } + } + }, + "helloWorld": { + "pageUrl": "https://{tenantDomain}/SitePages/myPage.aspx", + "customActions": { + "327f916d-e546-461e-befb-3c4f35611e65": { + "location": "ClientSideExtension.ListViewCommandSet.CommandBar", + "properties": { + "sampleTextOne": "One item is selected in the list", + "sampleTextTwo": "This command is always visible." + } + } + } + } + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/config/write-manifests.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/config/write-manifests.json new file mode 100644 index 00000000000..bad3526054b --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/config/write-manifests.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json", + "cdnBasePath": "" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/gulpfile.js b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/gulpfile.js new file mode 100644 index 00000000000..be2918708a7 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/gulpfile.js @@ -0,0 +1,16 @@ +'use strict'; + +const build = require('@microsoft/sp-build-web'); + +build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`); + +var getTasks = build.rig.getTasks; +build.rig.getTasks = function () { + var result = getTasks.call(build.rig); + + result.set('serve', result.get('serve-deprecated')); + + return result; +}; + +build.initialize(require('gulp')); diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/package.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/package.json new file mode 100644 index 00000000000..002037c4365 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/package.json @@ -0,0 +1,34 @@ +{ + "name": "spfx", + "version": "0.0.1", + "private": true, + "engines": { + "node": ">=16.13.0 <17.0.0 || >=18.17.1 <19.0.0" + }, + "main": "lib/index.js", + "scripts": { + "build": "gulp bundle", + "clean": "gulp clean", + "test": "gulp test" + }, + "dependencies": { + "tslib": "2.3.1", + "@microsoft/sp-core-library": "1.18.0", + "@microsoft/decorators": "1.18.0", + "@microsoft/sp-listview-extensibility": "1.18.0", + "@microsoft/sp-dialog": "1.18.0" + }, + "devDependencies": { + "@microsoft/rush-stack-compiler-4.7": "0.1.0", + "@rushstack/eslint-config": "2.5.1", + "@microsoft/eslint-plugin-spfx": "1.18.0", + "@microsoft/eslint-config-spfx": "1.18.0", + "@microsoft/sp-build-web": "1.18.0", + "@types/webpack-env": "~1.15.2", + "ajv": "^6.12.5", + "eslint": "8.7.0", + "gulp": "4.0.2", + "typescript": "4.7.4", + "@microsoft/sp-module-interfaces": "1.18.0" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/sharepoint/assets/ClientSideInstance.xml b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/sharepoint/assets/ClientSideInstance.xml new file mode 100644 index 00000000000..55c7f7f10d6 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/sharepoint/assets/ClientSideInstance.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/sharepoint/assets/elements.xml b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/sharepoint/assets/elements.xml new file mode 100644 index 00000000000..3343d6b2f7a --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/sharepoint/assets/elements.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/src/extensions/helloWorld/HelloWorldCommandSet.manifest.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/src/extensions/helloWorld/HelloWorldCommandSet.manifest.json new file mode 100644 index 00000000000..a175a15bf20 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/src/extensions/helloWorld/HelloWorldCommandSet.manifest.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx/command-set-extension-manifest.schema.json", + + "id": "327f916d-e546-461e-befb-3c4f35611e65", + "alias": "HelloWorldCommandSet", + "componentType": "Extension", + "extensionType": "ListViewCommandSet", + + // The "*" signifies that the version should be taken from the package.json + "version": "*", + "manifestVersion": 2, + + // If true, the component can only be installed on sites where Custom Script is allowed. + // Components that allow authors to embed arbitrary script code should set this to true. + // https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f + "requiresCustomScript": false, + + "items": { + "COMMAND_1": { + "title": { "default": "Command One" }, + "iconImageUrl": "icons/request.png", + "type": "command" + }, + "COMMAND_2": { + "title": { "default": "Command Two" }, + "iconImageUrl": "icons/cancel.png", + "type": "command" + } + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/src/extensions/helloWorld/HelloWorldCommandSet.ts b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/src/extensions/helloWorld/HelloWorldCommandSet.ts new file mode 100644 index 00000000000..bcff625be3f --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/src/extensions/helloWorld/HelloWorldCommandSet.ts @@ -0,0 +1,68 @@ +import { Log } from '@microsoft/sp-core-library'; +import { + BaseListViewCommandSet, + type Command, + type IListViewCommandSetExecuteEventParameters, + type ListViewStateChangedEventArgs +} from '@microsoft/sp-listview-extensibility'; +import { Dialog } from '@microsoft/sp-dialog'; + +/** + * If your command set uses the ClientSideComponentProperties JSON input, + * it will be deserialized into the BaseExtension.properties object. + * You can define an interface to describe it. + */ +export interface IHelloWorldCommandSetProperties { + // This is an example; replace with your own properties + sampleTextOne: string; + sampleTextTwo: string; +} + +const LOG_SOURCE: string = 'HelloWorldCommandSet'; + +export default class HelloWorldCommandSet extends BaseListViewCommandSet { + + public onInit(): Promise { + Log.info(LOG_SOURCE, 'Initialized HelloWorldCommandSet'); + + // initial state of the command's visibility + const compareOneCommand: Command = this.tryGetCommand('COMMAND_1'); + compareOneCommand.visible = false; + + this.context.listView.listViewStateChangedEvent.add(this, this._onListViewStateChanged); + + return Promise.resolve(); + } + + public onExecute(event: IListViewCommandSetExecuteEventParameters): void { + switch (event.itemId) { + case 'COMMAND_1': + Dialog.alert(`${this.properties.sampleTextOne}`).catch(() => { + /* handle error */ + }); + break; + case 'COMMAND_2': + Dialog.alert(`${this.properties.sampleTextTwo}`).catch(() => { + /* handle error */ + }); + break; + default: + throw new Error('Unknown command'); + } + } + + private _onListViewStateChanged = (args: ListViewStateChangedEventArgs): void => { + Log.info(LOG_SOURCE, 'List view state changed'); + + const compareOneCommand: Command = this.tryGetCommand('COMMAND_1'); + if (compareOneCommand) { + // This command should be hidden unless exactly one row is selected. + compareOneCommand.visible = this.context.listView.selectedRows?.length === 1; + } + + // TODO: Add your logic here + + // You should call this.raiseOnChage() to update the command bar + this.raiseOnChange(); + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/src/extensions/helloWorld/loc/en-us.js b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/src/extensions/helloWorld/loc/en-us.js new file mode 100644 index 00000000000..236e9f3da6d --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/src/extensions/helloWorld/loc/en-us.js @@ -0,0 +1,6 @@ +define([], function() { + return { + "Command1": "Command 1", + "Command2": "Command 2" + } +}); \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/src/extensions/helloWorld/loc/myStrings.d.ts b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/src/extensions/helloWorld/loc/myStrings.d.ts new file mode 100644 index 00000000000..86be120b34a --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/src/extensions/helloWorld/loc/myStrings.d.ts @@ -0,0 +1,9 @@ +declare interface IHelloWorldCommandSetStrings { + Command1: string; + Command2: string; +} + +declare module 'HelloWorldCommandSetStrings' { + const strings: IHelloWorldCommandSetStrings; + export = strings; +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/src/index.ts b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/src/index.ts new file mode 100644 index 00000000000..fb81db1e213 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/src/index.ts @@ -0,0 +1 @@ +// A file is required to be in the root of the /src directory by the TypeScript compiler diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/tsconfig.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/tsconfig.json new file mode 100644 index 00000000000..c4cd392ad1a --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-listviewcommandset/tsconfig.json @@ -0,0 +1,35 @@ +{ + "extends": "./node_modules/@microsoft/rush-stack-compiler-4.7/includes/tsconfig-web.json", + "compilerOptions": { + "target": "es5", + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "jsx": "react", + "declaration": true, + "sourceMap": true, + "experimentalDecorators": true, + "skipLibCheck": true, + "outDir": "lib", + "inlineSources": false, + "noImplicitAny": true, + + "typeRoots": [ + "./node_modules/@types", + "./node_modules/@microsoft" + ], + "types": [ + "webpack-env" + ], + "lib": [ + "es5", + "dom", + "es2015.collection", + "es2015.promise" + ] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx" + ] +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/.eslintrc.js b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/.eslintrc.js new file mode 100644 index 00000000000..ef68d0e95c0 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/.eslintrc.js @@ -0,0 +1,352 @@ +require('@rushstack/eslint-config/patch/modern-module-resolution'); +module.exports = { + extends: ['@microsoft/eslint-config-spfx/lib/profiles/default'], + parserOptions: { tsconfigRootDir: __dirname }, + overrides: [ + { + files: ['*.ts', '*.tsx'], + parser: '@typescript-eslint/parser', + 'parserOptions': { + 'project': './tsconfig.json', + 'ecmaVersion': 2018, + 'sourceType': 'module' + }, + rules: { + // Prevent usage of the JavaScript null value, while allowing code to access existing APIs that may require null. https://www.npmjs.com/package/@rushstack/eslint-plugin + '@rushstack/no-new-null': 1, + // Require Jest module mocking APIs to be called before any other statements in their code block. https://www.npmjs.com/package/@rushstack/eslint-plugin + '@rushstack/hoist-jest-mock': 1, + // Require regular expressions to be constructed from string constants rather than dynamically building strings at runtime. https://www.npmjs.com/package/@rushstack/eslint-plugin-security + '@rushstack/security/no-unsafe-regexp': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/adjacent-overload-signatures': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // + // CONFIGURATION: By default, these are banned: String, Boolean, Number, Object, Symbol + '@typescript-eslint/ban-types': [ + 1, + { + 'extendDefaults': false, + 'types': { + 'String': { + 'message': 'Use \'string\' instead', + 'fixWith': 'string' + }, + 'Boolean': { + 'message': 'Use \'boolean\' instead', + 'fixWith': 'boolean' + }, + 'Number': { + 'message': 'Use \'number\' instead', + 'fixWith': 'number' + }, + 'Object': { + 'message': 'Use \'object\' instead, or else define a proper TypeScript type:' + }, + 'Symbol': { + 'message': 'Use \'symbol\' instead', + 'fixWith': 'symbol' + }, + 'Function': { + 'message': 'The \'Function\' type accepts any function-like value.\nIt provides no type safety when calling the function, which can be a common source of bugs.\nIt also accepts things like class declarations, which will throw at runtime as they will not be called with \'new\'.\nIf you are expecting the function to accept certain arguments, you should explicitly define the function shape.' + } + } + } + ], + // RATIONALE: Code is more readable when the type of every variable is immediately obvious. + // Even if the compiler may be able to infer a type, this inference will be unavailable + // to a person who is reviewing a GitHub diff. This rule makes writing code harder, + // but writing code is a much less important activity than reading it. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/explicit-function-return-type': [ + 1, + { + 'allowExpressions': true, + 'allowTypedFunctionExpressions': true, + 'allowHigherOrderFunctions': false + } + ], + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Rationale to disable: although this is a recommended rule, it is up to dev to select coding style. + // Set to 1 (warning) or 2 (error) to enable. + '@typescript-eslint/explicit-member-accessibility': 0, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-array-constructor': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // + // RATIONALE: The "any" keyword disables static type checking, the main benefit of using TypeScript. + // This rule should be suppressed only in very special cases such as JSON.stringify() + // where the type really can be anything. Even if the type is flexible, another type + // may be more appropriate such as "unknown", "{}", or "Record". + '@typescript-eslint/no-explicit-any': 1, + // RATIONALE: The #1 rule of promises is that every promise chain must be terminated by a catch() + // handler. Thus wherever a Promise arises, the code must either append a catch handler, + // or else return the object to a caller (who assumes this responsibility). Unterminated + // promise chains are a serious issue. Besides causing errors to be silently ignored, + // they can also cause a NodeJS process to terminate unexpectedly. + '@typescript-eslint/no-floating-promises': 2, + // RATIONALE: Catches a common coding mistake. + '@typescript-eslint/no-for-in-array': 2, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-misused-new': 2, + // RATIONALE: The "namespace" keyword is not recommended for organizing code because JavaScript lacks + // a "using" statement to traverse namespaces. Nested namespaces prevent certain bundler + // optimizations. If you are declaring loose functions/variables, it's better to make them + // static members of a class, since classes support property getters and their private + // members are accessible by unit tests. Also, the exercise of choosing a meaningful + // class name tends to produce more discoverable APIs: for example, search+replacing + // the function "reverse()" is likely to return many false matches, whereas if we always + // write "Text.reverse()" is more unique. For large scale organization, it's recommended + // to decompose your code into separate NPM packages, which ensures that component + // dependencies are tracked more conscientiously. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-namespace': [ + 1, + { + 'allowDeclarations': false, + 'allowDefinitionFiles': false + } + ], + // RATIONALE: Parameter properties provide a shorthand such as "constructor(public title: string)" + // that avoids the effort of declaring "title" as a field. This TypeScript feature makes + // code easier to write, but arguably sacrifices readability: In the notes for + // "@typescript-eslint/member-ordering" we pointed out that fields are central to + // a class's design, so we wouldn't want to bury them in a constructor signature + // just to save some typing. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Set to 1 (warning) or 2 (error) to enable the rule + '@typescript-eslint/parameter-properties': 0, + // RATIONALE: When left in shipping code, unused variables often indicate a mistake. Dead code + // may impact performance. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-unused-vars': [ + 1, + { + 'vars': 'all', + // Unused function arguments often indicate a mistake in JavaScript code. However in TypeScript code, + // the compiler catches most of those mistakes, and unused arguments are fairly common for type signatures + // that are overriding a base class method or implementing an interface. + 'args': 'none' + } + ], + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-use-before-define': [ + 2, + { + 'functions': false, + 'classes': true, + 'variables': true, + 'enums': true, + 'typedefs': true + } + ], + // Disallows require statements except in import statements. + // In other words, the use of forms such as var foo = require("foo") are banned. Instead use ES6 style imports or import foo = require("foo") imports. + '@typescript-eslint/no-var-requires': 'error', + // RATIONALE: The "module" keyword is deprecated except when describing legacy libraries. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/prefer-namespace-keyword': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Rationale to disable: it's up to developer to decide if he wants to add type annotations + // Set to 1 (warning) or 2 (error) to enable the rule + '@typescript-eslint/no-inferrable-types': 0, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Rationale to disable: declaration of empty interfaces may be helpful for generic types scenarios + '@typescript-eslint/no-empty-interface': 0, + // RATIONALE: This rule warns if setters are defined without getters, which is probably a mistake. + 'accessor-pairs': 1, + // RATIONALE: In TypeScript, if you write x["y"] instead of x.y, it disables type checking. + 'dot-notation': [ + 1, + { + 'allowPattern': '^_' + } + ], + // RATIONALE: Catches code that is likely to be incorrect + 'eqeqeq': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'for-direction': 1, + // RATIONALE: Catches a common coding mistake. + 'guard-for-in': 2, + // RATIONALE: If you have more than 2,000 lines in a single source file, it's probably time + // to split up your code. + 'max-lines': ['warn', { max: 2000 }], + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-async-promise-executor': 2, + // RATIONALE: Deprecated language feature. + 'no-caller': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-compare-neg-zero': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-cond-assign': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-constant-condition': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-control-regex': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-debugger': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-delete-var': 2, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-duplicate-case': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-empty': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-empty-character-class': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-empty-pattern': 1, + // RATIONALE: Eval is a security concern and a performance concern. + 'no-eval': 1, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-ex-assign': 2, + // RATIONALE: System types are global and should not be tampered with in a scalable code base. + // If two different libraries (or two versions of the same library) both try to modify + // a type, only one of them can win. Polyfills are acceptable because they implement + // a standardized interoperable contract, but polyfills are generally coded in plain + // JavaScript. + 'no-extend-native': 1, + // Disallow unnecessary labels + 'no-extra-label': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-fallthrough': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-func-assign': 1, + // RATIONALE: Catches a common coding mistake. + 'no-implied-eval': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-invalid-regexp': 2, + // RATIONALE: Catches a common coding mistake. + 'no-label-var': 2, + // RATIONALE: Eliminates redundant code. + 'no-lone-blocks': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-misleading-character-class': 2, + // RATIONALE: Catches a common coding mistake. + 'no-multi-str': 2, + // RATIONALE: It's generally a bad practice to call "new Thing()" without assigning the result to + // a variable. Either it's part of an awkward expression like "(new Thing()).doSomething()", + // or else implies that the constructor is doing nontrivial computations, which is often + // a poor class design. + 'no-new': 1, + // RATIONALE: Obsolete language feature that is deprecated. + 'no-new-func': 2, + // RATIONALE: Obsolete language feature that is deprecated. + 'no-new-object': 2, + // RATIONALE: Obsolete notation. + 'no-new-wrappers': 1, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-octal': 2, + // RATIONALE: Catches code that is likely to be incorrect + 'no-octal-escape': 2, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-regex-spaces': 2, + // RATIONALE: Catches a common coding mistake. + 'no-return-assign': 2, + // RATIONALE: Security risk. + 'no-script-url': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-self-assign': 2, + // RATIONALE: Catches a common coding mistake. + 'no-self-compare': 2, + // RATIONALE: This avoids statements such as "while (a = next(), a && a.length);" that use + // commas to create compound expressions. In general code is more readable if each + // step is split onto a separate line. This also makes it easier to set breakpoints + // in the debugger. + 'no-sequences': 1, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-shadow-restricted-names': 2, + // RATIONALE: Obsolete language feature that is deprecated. + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-sparse-arrays': 2, + // RATIONALE: Although in theory JavaScript allows any possible data type to be thrown as an exception, + // such flexibility adds pointless complexity, by requiring every catch block to test + // the type of the object that it receives. Whereas if catch blocks can always assume + // that their object implements the "Error" contract, then the code is simpler, and + // we generally get useful additional information like a call stack. + 'no-throw-literal': 2, + // RATIONALE: Catches a common coding mistake. + 'no-unmodified-loop-condition': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-unsafe-finally': 2, + // RATIONALE: Catches a common coding mistake. + 'no-unused-expressions': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-unused-labels': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-useless-catch': 1, + // RATIONALE: Avoids a potential performance problem. + 'no-useless-concat': 1, + // RATIONALE: The "var" keyword is deprecated because of its confusing "hoisting" behavior. + // Always use "let" or "const" instead. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + 'no-var': 2, + // RATIONALE: Generally not needed in modern code. + 'no-void': 1, + // RATIONALE: Obsolete language feature that is deprecated. + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-with': 2, + // RATIONALE: Makes logic easier to understand, since constants always have a known value + // @typescript-eslint\eslint-plugin\dist\configs\eslint-recommended.js + 'prefer-const': 1, + // RATIONALE: Catches a common coding mistake where "resolve" and "reject" are confused. + 'promise/param-names': 2, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'require-atomic-updates': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'require-yield': 1, + // "Use strict" is redundant when using the TypeScript compiler. + 'strict': [ + 2, + 'never' + ], + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'use-isnan': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + // Set to 1 (warning) or 2 (error) to enable. + // Rationale to disable: !!{} + 'no-extra-boolean-cast': 0, + // ==================================================================== + // @microsoft/eslint-plugin-spfx + // ==================================================================== + '@microsoft/spfx/import-requires-chunk-name': 1, + '@microsoft/spfx/no-require-ensure': 2, + '@microsoft/spfx/pair-react-dom-render-unmount': 1 + } + }, + { + // For unit tests, we can be a little bit less strict. The settings below revise the + // defaults specified in the extended configurations, as well as above. + files: [ + // Test files + '*.test.ts', + '*.test.tsx', + '*.spec.ts', + '*.spec.tsx', + + // Facebook convention + '**/__mocks__/*.ts', + '**/__mocks__/*.tsx', + '**/__tests__/*.ts', + '**/__tests__/*.tsx', + + // Microsoft convention + '**/test/*.ts', + '**/test/*.tsx' + ], + rules: {} + } + ] +}; \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/.gitignore b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/.gitignore new file mode 100644 index 00000000000..51ca7b9e7a7 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/.gitignore @@ -0,0 +1,34 @@ +# Logs +logs +*.log +npm-debug.log* + +# Dependency directories +node_modules + +# Build generated files +dist +lib +release +solution +temp +*.sppkg +.heft + +# Coverage directory used by tools like istanbul +coverage + +# OSX +.DS_Store + +# Visual Studio files +.ntvs_analysis.dat +.vs +bin +obj + +# Resx Generated Code +*.resx.ts + +# Styles Generated Code +*.scss.ts diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/.npmignore b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/.npmignore new file mode 100644 index 00000000000..ae0b487c075 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/.npmignore @@ -0,0 +1,16 @@ +!dist +config + +gulpfile.js + +release +src +temp + +tsconfig.json +tslint.json + +*.log + +.yo-rc.json +.vscode diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/.vscode/launch.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/.vscode/launch.json new file mode 100644 index 00000000000..53ca22cd853 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Hosted workbench", + "type": "msedge", + "request": "launch", + "url": "https://{tenantDomain}/_layouts/workbench.aspx", + "webRoot": "${workspaceRoot}", + "sourceMaps": true, + "sourceMapPathOverrides": { + "webpack:///.././src/*": "${webRoot}/src/*", + "webpack:///../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../../src/*": "${webRoot}/src/*" + }, + "runtimeArgs": [ + "--remote-debugging-port=9222", + "-incognito" + ] + } + ] +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/.vscode/settings.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/.vscode/settings.json new file mode 100644 index 00000000000..a31a2c33223 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/.vscode/settings.json @@ -0,0 +1,13 @@ +// Place your settings in this file to overwrite default and user settings. +{ + // Configure glob patterns for excluding files and folders in the file explorer. + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/bower_components": true, + "**/coverage": true, + "**/lib-amd": true, + "src/**/*.scss.ts": true + }, + "typescript.tsdk": ".\\node_modules\\typescript\\lib" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/.yo-rc.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/.yo-rc.json new file mode 100644 index 00000000000..43476a60502 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/.yo-rc.json @@ -0,0 +1,25 @@ +{ + "@microsoft/generator-sharepoint": { + "whichFolder": "subdir", + "solutionName": "spfx", + "environment": "spo", + "skipFeatureDeployment": false, + "isDomainIsolated": false, + "componentType": "webpart", + "template": "none", + "componentName": "HelloWorld", + "componentDescription": "HelloWorld", + "plusBeta": false, + "isCreatingSolution": true, + "nodeVersion": "18.16.0", + "sdksVersions": { + "@microsoft/microsoft-graph-client": "3.0.2", + "@microsoft/teams-js": "2.12.0" + }, + "version": "1.18.0", + "libraryName": "spfx", + "libraryId": "1cd14766-14af-406b-87ad-ce978b0f10ff", + "packageManager": "npm", + "solutionShortDescription": "spfx description" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/README.md b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/README.md new file mode 100644 index 00000000000..4f69a527598 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/README.md @@ -0,0 +1,73 @@ +# spfx + +## Summary + +Short summary on functionality and used technologies. + +[picture of the solution in action, if possible] + +## Used SharePoint Framework Version + +![version](https://img.shields.io/badge/version-1.18.0-green.svg) + +## Applies to + +- [SharePoint Framework](https://aka.ms/spfx) +- [Microsoft 365 tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant) + +> Get your own free development tenant by subscribing to [Microsoft 365 developer program](http://aka.ms/o365devprogram) + +## Prerequisites + +> Any special pre-requisites? + +## Solution + +| Solution | Author(s) | +| ----------- | ------------------------------------------------------- | +| folder name | Author details (name, company, twitter alias with link) | + +## Version history + +| Version | Date | Comments | +| ------- | ---------------- | --------------- | +| 1.1 | March 10, 2021 | Update comment | +| 1.0 | January 29, 2021 | Initial release | + +## Disclaimer + +**THIS CODE IS PROVIDED _AS IS_ WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.** + +--- + +## Minimal Path to Awesome + +- Clone this repository +- Ensure that you are at the solution folder +- in the command-line run: + - **npm install** + - **gulp serve** + +> Include any additional steps as needed. + +## Features + +Description of the extension that expands upon high-level summary above. + +This extension illustrates the following concepts: + +- topic 1 +- topic 2 +- topic 3 + +> Notice that better pictures and documentation will increase the sample usage and the value you are providing for others. Thanks for your submissions advance. + +> Share your web part with others through Microsoft 365 Patterns and Practices program to get visibility and exposure. More details on the community, open-source projects and other activities from http://aka.ms/m365pnp. + +## References + +- [Getting started with SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant) +- [Building for Microsoft teams](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/build-for-teams-overview) +- [Use Microsoft Graph in your solution](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/using-microsoft-graph-apis) +- [Publish SharePoint Framework applications to the Marketplace](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/publish-to-marketplace-overview) +- [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - Guidance, tooling, samples and open-source controls for your Microsoft 365 development diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/config/config.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/config/config.json new file mode 100644 index 00000000000..5712fed7b1d --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/config/config.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json", + "version": "2.0", + "bundles": { + "hello-world-web-part": { + "components": [ + { + "entrypoint": "./lib/webparts/helloWorld/HelloWorldWebPart.js", + "manifest": "./src/webparts/helloWorld/HelloWorldWebPart.manifest.json" + } + ] + } + }, + "externals": {}, + "localizedResources": { + "HelloWorldWebPartStrings": "lib/webparts/helloWorld/loc/{locale}.js" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/config/deploy-azure-storage.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/config/deploy-azure-storage.json new file mode 100644 index 00000000000..02290df98af --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/config/deploy-azure-storage.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json", + "workingDir": "./release/assets/", + "account": "", + "container": "spfx", + "accessKey": "" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/config/package-solution.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/config/package-solution.json new file mode 100644 index 00000000000..80b9c845948 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/config/package-solution.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json", + "solution": { + "name": "spfx-client-side-solution", + "id": "1cd14766-14af-406b-87ad-ce978b0f10ff", + "version": "1.0.0.0", + "includeClientSideAssets": true, + "isDomainIsolated": false, + "developer": { + "name": "", + "websiteUrl": "", + "privacyUrl": "", + "termsOfUseUrl": "", + "mpnId": "Undefined-1.18.0" + }, + "metadata": { + "shortDescription": { + "default": "spfx description" + }, + "longDescription": { + "default": "spfx description" + }, + "screenshotPaths": [], + "videoUrl": "", + "categories": [] + }, + "features": [ + { + "title": "spfx Feature", + "description": "The feature that activates elements of the spfx solution.", + "id": "a04c7c39-eb50-431b-8967-b319d028da74", + "version": "1.0.0.0" + } + ] + }, + "paths": { + "zippedPackage": "solution/spfx.sppkg" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/config/sass.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/config/sass.json new file mode 100644 index 00000000000..5e78c982d8d --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/config/sass.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/core-build/sass.schema.json" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/config/serve.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/config/serve.json new file mode 100644 index 00000000000..a4c03e28723 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/config/serve.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/spfx-serve.schema.json", + "port": 4321, + "https": true, + "initialPage": "https://{tenantDomain}/_layouts/workbench.aspx" +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/config/write-manifests.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/config/write-manifests.json new file mode 100644 index 00000000000..bad3526054b --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/config/write-manifests.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json", + "cdnBasePath": "" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/gulpfile.js b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/gulpfile.js new file mode 100644 index 00000000000..be2918708a7 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/gulpfile.js @@ -0,0 +1,16 @@ +'use strict'; + +const build = require('@microsoft/sp-build-web'); + +build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`); + +var getTasks = build.rig.getTasks; +build.rig.getTasks = function () { + var result = getTasks.call(build.rig); + + result.set('serve', result.get('serve-deprecated')); + + return result; +}; + +build.initialize(require('gulp')); diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/package.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/package.json new file mode 100644 index 00000000000..839f4d46e1e --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/package.json @@ -0,0 +1,37 @@ +{ + "name": "spfx", + "version": "0.0.1", + "private": true, + "engines": { + "node": ">=16.13.0 <17.0.0 || >=18.17.1 <19.0.0" + }, + "main": "lib/index.js", + "scripts": { + "build": "gulp bundle", + "clean": "gulp clean", + "test": "gulp test" + }, + "dependencies": { + "tslib": "2.3.1", + "@microsoft/sp-core-library": "1.18.0", + "@microsoft/sp-component-base": "1.18.0", + "@microsoft/sp-property-pane": "1.18.0", + "@microsoft/sp-webpart-base": "1.18.0", + "@microsoft/sp-lodash-subset": "1.18.0", + "@microsoft/sp-office-ui-fabric-core": "1.18.0" + }, + "devDependencies": { + "@microsoft/rush-stack-compiler-4.7": "0.1.0", + "@rushstack/eslint-config": "2.5.1", + "@microsoft/eslint-plugin-spfx": "1.18.0", + "@microsoft/eslint-config-spfx": "1.18.0", + "@microsoft/sp-build-web": "1.18.0", + "@types/webpack-env": "~1.15.2", + "ajv": "^6.12.5", + "eslint": "8.7.0", + "gulp": "4.0.2", + "typescript": "4.7.4", + "@microsoft/sp-module-interfaces": "1.18.0", + "@fluentui/react": "^8.106.4" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/src/index.ts b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/src/index.ts new file mode 100644 index 00000000000..fb81db1e213 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/src/index.ts @@ -0,0 +1 @@ +// A file is required to be in the root of the /src directory by the TypeScript compiler diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/src/webparts/helloWorld/HelloWorldWebPart.manifest.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/src/webparts/helloWorld/HelloWorldWebPart.manifest.json new file mode 100644 index 00000000000..596007e91a5 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/src/webparts/helloWorld/HelloWorldWebPart.manifest.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json", + "id": "f4e48586-4808-4ff9-8ed2-451c151b74a3", + "alias": "HelloWorldWebPart", + "componentType": "WebPart", + + // The "*" signifies that the version should be taken from the package.json + "version": "*", + "manifestVersion": 2, + + // If true, the component can only be installed on sites where Custom Script is allowed. + // Components that allow authors to embed arbitrary script code should set this to true. + // https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f + "requiresCustomScript": false, + "supportedHosts": ["SharePointWebPart", "TeamsPersonalApp", "TeamsTab", "SharePointFullPage"], + "supportsThemeVariants": true, + + "preconfiguredEntries": [{ + "groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Advanced + "group": { "default": "Advanced" }, + "title": { "default": "HelloWorld" }, + "description": { "default": "HelloWorld" }, + "officeFabricIconFontName": "Page", + "properties": { + "description": "HelloWorld" + } + }] +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/src/webparts/helloWorld/HelloWorldWebPart.module.scss b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/src/webparts/helloWorld/HelloWorldWebPart.module.scss new file mode 100644 index 00000000000..2f9eb7ab6ee --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/src/webparts/helloWorld/HelloWorldWebPart.module.scss @@ -0,0 +1,34 @@ +@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss'; + +.helloWorld { + overflow: hidden; + padding: 1em; + color: "[theme:bodyText, default: #323130]"; + color: var(--bodyText); + &.teams { + font-family: $ms-font-family-fallbacks; + } +} + +.welcome { + text-align: center; +} + +.welcomeImage { + width: 100%; + max-width: 420px; +} + +.links { + a { + text-decoration: none; + color: "[theme:link, default:#03787c]"; + color: var(--link); // note: CSS Custom Properties support is limited to modern browsers only + + &:hover { + text-decoration: underline; + color: "[theme:linkHovered, default: #014446]"; + color: var(--linkHovered); // note: CSS Custom Properties support is limited to modern browsers only + } + } +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/src/webparts/helloWorld/HelloWorldWebPart.ts b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/src/webparts/helloWorld/HelloWorldWebPart.ts new file mode 100644 index 00000000000..c7a03dbd58f --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/src/webparts/helloWorld/HelloWorldWebPart.ts @@ -0,0 +1,128 @@ +import { Version } from '@microsoft/sp-core-library'; +import { + type IPropertyPaneConfiguration, + PropertyPaneTextField +} from '@microsoft/sp-property-pane'; +import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base'; +import type { IReadonlyTheme } from '@microsoft/sp-component-base'; +import { escape } from '@microsoft/sp-lodash-subset'; + +import styles from './HelloWorldWebPart.module.scss'; +import * as strings from 'HelloWorldWebPartStrings'; + +export interface IHelloWorldWebPartProps { + description: string; +} + +export default class HelloWorldWebPart extends BaseClientSideWebPart { + + private _isDarkTheme: boolean = false; + private _environmentMessage: string = ''; + + public render(): void { + this.domElement.innerHTML = ` +
+
+ +

Well done, ${escape(this.context.pageContext.user.displayName)}!

+
${this._environmentMessage}
+
Web part property value: ${escape(this.properties.description)}
+
+
+

Welcome to SharePoint Framework!

+

+ The SharePoint Framework (SPFx) is a extensibility model for Microsoft Viva, Microsoft Teams and SharePoint. It's the easiest way to extend Microsoft 365 with automatic Single Sign On, automatic hosting and industry standard tooling. +

+

Learn more about SPFx development:

+ +
+
`; + } + + protected onInit(): Promise { + return this._getEnvironmentMessage().then(message => { + this._environmentMessage = message; + }); + } + + + + private _getEnvironmentMessage(): Promise { + if (!!this.context.sdks.microsoftTeams) { // running in Teams, office.com or Outlook + return this.context.sdks.microsoftTeams.teamsJs.app.getContext() + .then(context => { + let environmentMessage: string = ''; + switch (context.app.host.name) { + case 'Office': // running in Office + environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentOffice : strings.AppOfficeEnvironment; + break; + case 'Outlook': // running in Outlook + environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentOutlook : strings.AppOutlookEnvironment; + break; + case 'Teams': // running in Teams + case 'TeamsModern': + environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentTeams : strings.AppTeamsTabEnvironment; + break; + default: + environmentMessage = strings.UnknownEnvironment; + } + + return environmentMessage; + }); + } + + return Promise.resolve(this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentSharePoint : strings.AppSharePointEnvironment); + } + + protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void { + if (!currentTheme) { + return; + } + + this._isDarkTheme = !!currentTheme.isInverted; + const { + semanticColors + } = currentTheme; + + if (semanticColors) { + this.domElement.style.setProperty('--bodyText', semanticColors.bodyText || null); + this.domElement.style.setProperty('--link', semanticColors.link || null); + this.domElement.style.setProperty('--linkHovered', semanticColors.linkHovered || null); + } + + } + + protected get dataVersion(): Version { + return Version.parse('1.0'); + } + + protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration { + return { + pages: [ + { + header: { + description: strings.PropertyPaneDescription + }, + groups: [ + { + groupName: strings.BasicGroupName, + groupFields: [ + PropertyPaneTextField('description', { + label: strings.DescriptionFieldLabel + }) + ] + } + ] + } + ] + }; + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/src/webparts/helloWorld/assets/welcome-dark.png b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/src/webparts/helloWorld/assets/welcome-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..42f0b8d24a9aa964a2be4885fe5700c1c54191a9 GIT binary patch literal 12545 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�Gp~kbx)oU28ew4C2+WtKA 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 G`2PSf1J4)$ literal 0 HcmV?d00001 diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/src/webparts/helloWorld/assets/welcome-light.png b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/src/webparts/helloWorld/assets/welcome-light.png new file mode 100644 index 0000000000000000000000000000000000000000..69eb3b48cd83031f106df4b4df127c749657e319 GIT binary patch literal 12816 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 literal 0 HcmV?d00001 diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/src/webparts/helloWorld/loc/en-us.js b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/src/webparts/helloWorld/loc/en-us.js new file mode 100644 index 00000000000..3b25e74a7d6 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/src/webparts/helloWorld/loc/en-us.js @@ -0,0 +1,16 @@ +define([], function() { + return { + "PropertyPaneDescription": "Description", + "BasicGroupName": "Group Name", + "DescriptionFieldLabel": "Description Field", + "AppLocalEnvironmentSharePoint": "The app is running on your local environment as SharePoint web part", + "AppLocalEnvironmentTeams": "The app is running on your local environment as Microsoft Teams app", + "AppLocalEnvironmentOffice": "The app is running on your local environment in office.com", + "AppLocalEnvironmentOutlook": "The app is running on your local environment in Outlook", + "AppSharePointEnvironment": "The app is running on SharePoint page", + "AppTeamsTabEnvironment": "The app is running in Microsoft Teams", + "AppOfficeEnvironment": "The app is running in office.com", + "AppOutlookEnvironment": "The app is running in Outlook", + "UnknownEnvironment": "The app is running in an unknown environment" + } +}); \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/src/webparts/helloWorld/loc/mystrings.d.ts b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/src/webparts/helloWorld/loc/mystrings.d.ts new file mode 100644 index 00000000000..21046bcd8bc --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/src/webparts/helloWorld/loc/mystrings.d.ts @@ -0,0 +1,19 @@ +declare interface IHelloWorldWebPartStrings { + PropertyPaneDescription: string; + BasicGroupName: string; + DescriptionFieldLabel: string; + AppLocalEnvironmentSharePoint: string; + AppLocalEnvironmentTeams: string; + AppLocalEnvironmentOffice: string; + AppLocalEnvironmentOutlook: string; + AppSharePointEnvironment: string; + AppTeamsTabEnvironment: string; + AppOfficeEnvironment: string; + AppOutlookEnvironment: string; + UnknownEnvironment: string; +} + +declare module 'HelloWorldWebPartStrings' { + const strings: IHelloWorldWebPartStrings; + export = strings; +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/teams/f4e48586-4808-4ff9-8ed2-451c151b74a3_color.png b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/teams/f4e48586-4808-4ff9-8ed2-451c151b74a3_color.png new file mode 100644 index 0000000000000000000000000000000000000000..0e1f764fa8df4791a61c71b4f011c26f9083ee52 GIT binary patch literal 10248 zcmeHs2T)UM*Y2hl>0MEYh=BCai=jwWAoLiXZ}l(vc=b zI!cjVEr>`H2yl1Q)4ub~+6|q6PIX{3=ffYI)PdeQ zR`~Mm(+6(VOiWV08f{~!kKt+Um1}JSomPzC1=>>nm(xf=7gse0=?;PVduy zEDHV*&HDD2WGpQJ(3PMyHI30OB--?qyKctjm$(S4S2NfdCk)|+aJDnD5GA1w6Z7nv zj*32{u?f{@Jid#KNH>B4a?fF-GSP0UZ^ zMsLlx<6Re>>Aw1q_ZaBIDS)jRnWQehYrSG*x*>C7WbMIo*Pq=3)M!-sC;d9v=3xMSbN^<7GMT#nQNXnK z&ii+XSf%-~PXO~P0(E{kxiQ&2b#5Lq87DfAaCR@6NeB=XAx@xk)?k~CFep9oiTamp zo?pW?v#Ez71^dXynlAM*N)SY+m7gX)NW4_8GcU22mPntoLR03GtAz zBaVqN7d7unQB+34rhx#N;}ivrN?v5Od?gXQGlO2-!Bq8(DNX0Tan?owwB+Z>bE5jR zrC69~v06O*$C|W1X>ptqVNHtl(~@*%@#bsN6oaVp_v?vVPae{?;kQo>*AtYcdcl4v z;;}~NdB0OM*S1s5UTM!;anPNK8PK?t{m%GQA&BNmIe%R0WfB{^-zGKJgjvNq(! z;qsa_xtjfJ%XWCWUD|4jkfX4OOdUyt6c0L)sf~UvDXt|SK@T0iGk8b;j!8GeBE>C& zMawZn##4kflRTeooM-D6LicI;2xq!5oY5_Oz3r*j=yOv$_7ZM?ZiPtOri2-}FP7hc z;B%o0_4HH84bfo{Vfql3U+ESp9nT*}aOcE(MSDd`X@AqpXL%OK)Z*o=?5(yYuqM66 zx+Wf{Whyj!{iPl{e?OmVz+ymVfQI!^Y-Vg4t-gR!zGc2X(tKX-sIKMd@zknoKCf=h z`^?wOQ<m-s&7qo5>{EfKY)>tpraAj?FlcAYY9UPMh1<_s zogFl%wD{$NI?8n0IIZw)p^U7oVdSDtLILMsXg=d0SL@l?wpoqYOS8{rxmyF-Xz6_E z6ymhvOykPg=Gd|X7S5~)j9%Xp7(b(X=JtK$eUJMirrM?_OkGT~^4|}@2j~W9@@wvA zbwW@Ts9=;dD!J3{#-(fbJ8U|oQ3=T2PSsnH3l$5*dE7#B7h{Kbhv=)CS3+G@U5+;$ zjjtJ(TEtov8ZVpOvC0>*5p6qbDePbxe1W@Qyr2EO#Rl1K?)CKRMW)rJkpraz@nzAt zY+OAq=BpWJExqO$oBQYQJEBZ1OosJ+Yc8jKaFI(trYUZ@QaYABR#qJyRJ!Z1iP={q;Hf5N)mTnN4t+w)3! zNO|2i-8bo@AU>|zuli`UXSG=nTTo`uPS8ZqlTC}w<_+d$<_}?VQcyivcj-H_`8?IU zi@aOM4$%3YEwYc~f=V+hCIdqzcQZG!iN1-WZMsQ(_lyl|4YuxWqyp9a1Y(MuyUIK(GoB5WB~+<+ZjVhiW?q>>H+*uMDQ%u9 zJN>f1_310`k+@loW%Id>?n|ReZ7*I2_P4g2dAm?1+Bw|3N%$C*R?9nB-0Ims(H#=A zN1*9%p2};Rdr?p+^tSx%kuBV|^71A!Z%n7B4TGItSq$mhdzcb!6yGaZ@9xMqWi!Pc z8XY>mPrd)}z;@que~M;==2vP1>dVv*XtrolBVR_^MBIuz5!n?Pcxv~Q%6UvoXH?rS zTTzIY5ijpG-;!7IRIWe^z7wdG3JxxBTYUUlq3V^#g?Bh_(ZDP7Z+ut_W2a-^#P+66 zrY5E8n_x_QO%e=f77>f0x5SGgYcEj)Uuj!~kAw(KK^o2-PGnBQ>Zra_)zCj^@YyLT zHqO6htN8RS4!ghoYIUxjZ?Y=XakKu3_TyaJR_fN9ty%ha&Qyg;5pzDo4Da{v#poOA zr`*cvQbf(scrc}laEtby%~DzcR zu#&jqqeai2tQ6iT8n>znj`Brr6t47d1?}w4?i#hvCc`|u(94(2v5(Ist7du=wattN zr|H__Zn@rFoL!U@QW6R*Ai^0d?mVlg=zMmovdQzk=Xyc2(bQi5T6?D7#8xy@T=J~U zw88G(+iKcG+0Zw09*1CwZDkFImVC_jQS; zd8+$Io^iKI-rAbkqu;fZT(GmLP7e;8x!8U4{Xy~m4J*wORh};=cOKcatQInaS$pPaGvDq>Bd};29O@rST zgFxM5cz2~~wd%bD^Yg8{2`nF-deB)Edz~0S;T;>5jcnC81IFW;h z_B*pKv*;(`oNtRtr4Q#ptuV&Dn9?Ld~xbbGZ5LZg$ge4y(y`l-dEUX2h2 z$Z4^kX&LX@005>;G`OL)HZ)Ln!Foy{kXUDwM1ZFkxOW7Ab7}!z2$w4;Jj5C0hQ`47 zS8E#hA!sCw-%8F<%Fs&_<&M@1!lBH9;N~ttS6q~k{A#N7=K_>L0#6hk0SWN*!1yQ! z!1%xODue$?Vo83;cL@FpjNjVO7@~>Ap&+snvJz6_+5u=kX?|6D$T=L+RoPTa=O+dD z1mkzdk?XhLiUCzX$UdI7#z2 zI1=rK)<<~$Zb1s4B)fv}{N4tP&)-bI^!*P1i-Q1!*B@X~aLN}v5pF0eG!pL)W^X1igK+coop$k3VXHZg>^J(*YVPB&7Qf zv;PGDfc}mobsY)){ru(Y3l`)LfUAIVo8Mq`ab%tCF0I zqJomM5)>sR=ZbJqazVllR%sn+Q-WS5%?## z>EA(?e=V555NLuea3j^%4=D}>p2m^BE@)>k!teD3aR&Xn{FXU9&iAMMFY*7@gfs}iAVCk z1pQt9qbGiE%y-$(VfDN4`&j?Wc(uTwy?s#^w7{ua5dn2^l~a;;afQmt$)S{xQu6Y$ zvPfw;Sp}4gyyE|^3cquMGnld_soJnOb1c^5Kc^Z=(hM*8bBYI749VY@4AQ*)|Ni>d z$v?{Rzv=onUH>Qp{|NkVb^V*Jf0TiL1pc?W{{KoB{U3);6b3wL`h!PG^!zD*@Hk58 zbN+%kEqDdfB4feli=>wuznx|PfcCC_he7WKyxT(Rt;~>&ai8(44g--_`c}gF;vzlW zy=GEFGJ`VmR(%#;V#1x>CQ`EfCQ{N;73O`Ez_Conbf3dX#8*DDQLu?2rT% z%y@CJ=Sr@a>5((uH-)crwK_Ak4wzQh_=&|M>w0Ov{F47pOrc@b$k}AmELmohEjFr* z2Mf6#MRv`dfuQ9A2ppq;^z#65ZGfIEwR$cTU`zU+RJ}pghJVO1Zs1Uo7s;;K6P}c}j}Qf<01E0pb@&k~4tgrF;;7P! zvhdA5t~Z(pf$L}iGxYpApRq897d~Ah$5?&l69Y{Q^b=o8>B+H|%YA@O3{BtA5M^4@ z;YTv+K4bb{$je5LyDo9B#i$(|eHuXiG+^MH6bJp$<9u#jUOY6w!PTfeTHj^u$4%0Gwp@scyRMj@i=%nUDb8-M#uG9h$Ww)7t z=WGPBp=W7=4Y|Ul)?&-d__-&c`*PGYK~N3`r$A43(JN^IYdGq06%$}gMR(NhB>0i> zS!GH7L_U2HvR?S{T><um585(!p@r=%(no0?x3k_m^iIC0l{{fM;?|RsjT2wh_uL~2s8pZusy?Zi zl~JmK?Ib;Cv+I4QYgNe&Yx(q&&9ZYfxrGf(+y#yV5pxSURbs+ur)2tMCY&;;n&s1~ zMri|gb7aVupbGAl1pssPE3fyN=b2|y;8`ABMWV{7Pv$0Y{}xapNS0e0r8LM zIeJ(UgiM6fPo)|lU>t2*7>>Od?~P5r9JN0C%?@nK&Ft$+DUUn?<)YMxYrV9(eyo#k z1jp@-Ej&?itv6y@Z;Xr#VW!j)LHp)hVX(KXLP%~4k)3&C<7zDq8!VvfsM9rid}#J3 zZ*upb-3U5I*z**cZTpeY@%En2p~FHAtX2_67M1Unxo@zPuP0bL9GQ4O-_UUozSTbu z;-0VKW>*G|O^O#=eCK}dRFZdAB~j>UMYYMA$xO1mn)2BG@&^xHe>dL8mPf?6KHhA9 z7w!Bs*4acG>BgLzQTO$T;-WrBtAF0rifxnHA@HUPvcQ3Xe~DULXa?gxvB1|JY|q_&La`FI1X^c z@$mV5E)i3<``tp14`;M#$ydQ0pm=&(Q~tLLpz7+`z>ZhRx0%& zs!0z7MLi6MRq#c_^hLw*>R9fvla&Xv34G7@IuUDIH%$d|1}Njb5SJrh_Ltqm zVtbNfmK>)@Q2@VQgo#+GjoLw^UojZHp{T~9KtlHgt}8j> z6_R2aCLGl2hhgC-R}#P6=vcc44?PWwh_oiZd&6T#Yqphn1)NDg=7@vkl`-|EkVl=7 zDoA-PtsywMqy4m822p4C;x(m?^A^ui63wYu+6Eu$-mZTH4I!qPRkMS`LbFLV0EU^f z_noO;y4}0fwInGqiMnELM|SzuM_(8BQf8*B~ZXrB1W`RVoM(a%dxfE))Xu3S4Ahuo+B@)XpL z&v;igBB~r;)UmGFW*-}i|1_)SSgT`VF@sl{5kFkCE`HB9muYYPl3A?>;{0tS`t4vu zQ}|?cK#5~K*-)P)k|s3+_S}xCqG~VvU^l>|b@syJ$DYm@1gjcR@#;c4eKrkCVZzex zeJ~d{FOH#ltlmyh0QG_Lbby>K-A2dZTpCsFF8buhQ!8SnCmTaW&8r<*ja;&SU=r+|x((Sx>~OOWal z&m8D5_lWSA6Yv6x!5eb{2YB++niLNL)+iXVC5D2|JV$?3$zZD3Jr^J?7d>_OSw->F zYX(AnA~+!{OhryiCfYfSkAEz&iZIF`8Wr%(jBcDZNK9!&V08vhgl3nHb^8b~Y8~!V z#xyhecUh?iUd^jK9F<)$dJwCE=u;OlFjR@V=qy61o%Aa@!YN4!t>1Z ze8WkK!qZiCplZsXCsm#>?f^Q*wgDgdmP#lG*w*q(|j7R zXUlJ|8#Yt7i_V$dG741vqn$>rnFuss80eq89dtZ{*K400ft>fXN%Prr}8R>Y! z&I;H+;~Z@b+y6qX+b*QX=f<-9UMss`ov0afT?z<)nQ9NtfQrgJkdX8y$HUz#8-D(P zJFzPxOoAF>jaQ>~qAPY#P#z;%NhFI>QLq?Hl|Rl~IYkb$2iIYS#>9*M+%p_JBPSa7aOh15i4ZLUg zx?`4~B5j4aP2g_loUev;#XpLFIeOH5bGLlJwUtx-+Q%(1GYd|)^Aywc|My!*8G9fe z=e;18;df%6ExwhX-px&zp16Poz+KSV%mzoR^IGT1Sla}bH+LJA0TlL8RtvPjS55|LjhwdSxcwxo8Vj?Zm;U(vken}t2dcFdD0V{Zj}5nMt|ggHb6 zb1rcvXoCi&oRdwUcw{ok;ljk%IiI-bkSK<&h~vQiun0d1M3^H}|K>Za=X zi_?vJh7ZmV2Hz7JYPm;7 z^jJVk2D*1Vnr@xIhJPez3Y8v8$^7j1&9=_kVWOpTOwc(Fxf69wgdf-sIiO*-TgXbG zmXc>{nV(}dP(3N9YR6$l?KITC7$f~KyP${yoE=kNFI?+d$ay4f2_$H%oL$B%PvhLN zfxEUa_p2k9g9{Zqy#=ovSONWV2L6I~mVKoDhfgd2@X$1NNETJWa+|p#ZG`l`^MW>9 KtK_^>#Qy@$+e&%> literal 0 HcmV?d00001 diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/teams/f4e48586-4808-4ff9-8ed2-451c151b74a3_outline.png b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/teams/f4e48586-4808-4ff9-8ed2-451c151b74a3_outline.png new file mode 100644 index 0000000000000000000000000000000000000000..e8cb4b6ba4f726d47a2e274f16b6069b9a8041cc GIT binary patch literal 249 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz&H|6fVg?3oVGw3ym^DWND9BhG z%|CHbgX^da?eHs6`1kLARhjOac zf3;y1Iq~M{LLS3g_M2bU{+PBvomV=FH7$YTy5I%1<5B$=?>3fqI5P%5iajq7)W9SX p;gpazd1JnvZNlx8HB0WjVJ`J~Q+P@%pA+aZ22WQ%mvv4FO#n^cR9FB2 literal 0 HcmV?d00001 diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/tsconfig.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/tsconfig.json new file mode 100644 index 00000000000..c4cd392ad1a --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-nolib/tsconfig.json @@ -0,0 +1,35 @@ +{ + "extends": "./node_modules/@microsoft/rush-stack-compiler-4.7/includes/tsconfig-web.json", + "compilerOptions": { + "target": "es5", + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "jsx": "react", + "declaration": true, + "sourceMap": true, + "experimentalDecorators": true, + "skipLibCheck": true, + "outDir": "lib", + "inlineSources": false, + "noImplicitAny": true, + + "typeRoots": [ + "./node_modules/@types", + "./node_modules/@microsoft" + ], + "types": [ + "webpack-env" + ], + "lib": [ + "es5", + "dom", + "es2015.collection", + "es2015.promise" + ] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx" + ] +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/.eslintrc.js b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/.eslintrc.js new file mode 100644 index 00000000000..ef68d0e95c0 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/.eslintrc.js @@ -0,0 +1,352 @@ +require('@rushstack/eslint-config/patch/modern-module-resolution'); +module.exports = { + extends: ['@microsoft/eslint-config-spfx/lib/profiles/default'], + parserOptions: { tsconfigRootDir: __dirname }, + overrides: [ + { + files: ['*.ts', '*.tsx'], + parser: '@typescript-eslint/parser', + 'parserOptions': { + 'project': './tsconfig.json', + 'ecmaVersion': 2018, + 'sourceType': 'module' + }, + rules: { + // Prevent usage of the JavaScript null value, while allowing code to access existing APIs that may require null. https://www.npmjs.com/package/@rushstack/eslint-plugin + '@rushstack/no-new-null': 1, + // Require Jest module mocking APIs to be called before any other statements in their code block. https://www.npmjs.com/package/@rushstack/eslint-plugin + '@rushstack/hoist-jest-mock': 1, + // Require regular expressions to be constructed from string constants rather than dynamically building strings at runtime. https://www.npmjs.com/package/@rushstack/eslint-plugin-security + '@rushstack/security/no-unsafe-regexp': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/adjacent-overload-signatures': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // + // CONFIGURATION: By default, these are banned: String, Boolean, Number, Object, Symbol + '@typescript-eslint/ban-types': [ + 1, + { + 'extendDefaults': false, + 'types': { + 'String': { + 'message': 'Use \'string\' instead', + 'fixWith': 'string' + }, + 'Boolean': { + 'message': 'Use \'boolean\' instead', + 'fixWith': 'boolean' + }, + 'Number': { + 'message': 'Use \'number\' instead', + 'fixWith': 'number' + }, + 'Object': { + 'message': 'Use \'object\' instead, or else define a proper TypeScript type:' + }, + 'Symbol': { + 'message': 'Use \'symbol\' instead', + 'fixWith': 'symbol' + }, + 'Function': { + 'message': 'The \'Function\' type accepts any function-like value.\nIt provides no type safety when calling the function, which can be a common source of bugs.\nIt also accepts things like class declarations, which will throw at runtime as they will not be called with \'new\'.\nIf you are expecting the function to accept certain arguments, you should explicitly define the function shape.' + } + } + } + ], + // RATIONALE: Code is more readable when the type of every variable is immediately obvious. + // Even if the compiler may be able to infer a type, this inference will be unavailable + // to a person who is reviewing a GitHub diff. This rule makes writing code harder, + // but writing code is a much less important activity than reading it. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/explicit-function-return-type': [ + 1, + { + 'allowExpressions': true, + 'allowTypedFunctionExpressions': true, + 'allowHigherOrderFunctions': false + } + ], + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Rationale to disable: although this is a recommended rule, it is up to dev to select coding style. + // Set to 1 (warning) or 2 (error) to enable. + '@typescript-eslint/explicit-member-accessibility': 0, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-array-constructor': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // + // RATIONALE: The "any" keyword disables static type checking, the main benefit of using TypeScript. + // This rule should be suppressed only in very special cases such as JSON.stringify() + // where the type really can be anything. Even if the type is flexible, another type + // may be more appropriate such as "unknown", "{}", or "Record". + '@typescript-eslint/no-explicit-any': 1, + // RATIONALE: The #1 rule of promises is that every promise chain must be terminated by a catch() + // handler. Thus wherever a Promise arises, the code must either append a catch handler, + // or else return the object to a caller (who assumes this responsibility). Unterminated + // promise chains are a serious issue. Besides causing errors to be silently ignored, + // they can also cause a NodeJS process to terminate unexpectedly. + '@typescript-eslint/no-floating-promises': 2, + // RATIONALE: Catches a common coding mistake. + '@typescript-eslint/no-for-in-array': 2, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-misused-new': 2, + // RATIONALE: The "namespace" keyword is not recommended for organizing code because JavaScript lacks + // a "using" statement to traverse namespaces. Nested namespaces prevent certain bundler + // optimizations. If you are declaring loose functions/variables, it's better to make them + // static members of a class, since classes support property getters and their private + // members are accessible by unit tests. Also, the exercise of choosing a meaningful + // class name tends to produce more discoverable APIs: for example, search+replacing + // the function "reverse()" is likely to return many false matches, whereas if we always + // write "Text.reverse()" is more unique. For large scale organization, it's recommended + // to decompose your code into separate NPM packages, which ensures that component + // dependencies are tracked more conscientiously. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-namespace': [ + 1, + { + 'allowDeclarations': false, + 'allowDefinitionFiles': false + } + ], + // RATIONALE: Parameter properties provide a shorthand such as "constructor(public title: string)" + // that avoids the effort of declaring "title" as a field. This TypeScript feature makes + // code easier to write, but arguably sacrifices readability: In the notes for + // "@typescript-eslint/member-ordering" we pointed out that fields are central to + // a class's design, so we wouldn't want to bury them in a constructor signature + // just to save some typing. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Set to 1 (warning) or 2 (error) to enable the rule + '@typescript-eslint/parameter-properties': 0, + // RATIONALE: When left in shipping code, unused variables often indicate a mistake. Dead code + // may impact performance. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-unused-vars': [ + 1, + { + 'vars': 'all', + // Unused function arguments often indicate a mistake in JavaScript code. However in TypeScript code, + // the compiler catches most of those mistakes, and unused arguments are fairly common for type signatures + // that are overriding a base class method or implementing an interface. + 'args': 'none' + } + ], + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-use-before-define': [ + 2, + { + 'functions': false, + 'classes': true, + 'variables': true, + 'enums': true, + 'typedefs': true + } + ], + // Disallows require statements except in import statements. + // In other words, the use of forms such as var foo = require("foo") are banned. Instead use ES6 style imports or import foo = require("foo") imports. + '@typescript-eslint/no-var-requires': 'error', + // RATIONALE: The "module" keyword is deprecated except when describing legacy libraries. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/prefer-namespace-keyword': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Rationale to disable: it's up to developer to decide if he wants to add type annotations + // Set to 1 (warning) or 2 (error) to enable the rule + '@typescript-eslint/no-inferrable-types': 0, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Rationale to disable: declaration of empty interfaces may be helpful for generic types scenarios + '@typescript-eslint/no-empty-interface': 0, + // RATIONALE: This rule warns if setters are defined without getters, which is probably a mistake. + 'accessor-pairs': 1, + // RATIONALE: In TypeScript, if you write x["y"] instead of x.y, it disables type checking. + 'dot-notation': [ + 1, + { + 'allowPattern': '^_' + } + ], + // RATIONALE: Catches code that is likely to be incorrect + 'eqeqeq': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'for-direction': 1, + // RATIONALE: Catches a common coding mistake. + 'guard-for-in': 2, + // RATIONALE: If you have more than 2,000 lines in a single source file, it's probably time + // to split up your code. + 'max-lines': ['warn', { max: 2000 }], + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-async-promise-executor': 2, + // RATIONALE: Deprecated language feature. + 'no-caller': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-compare-neg-zero': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-cond-assign': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-constant-condition': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-control-regex': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-debugger': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-delete-var': 2, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-duplicate-case': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-empty': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-empty-character-class': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-empty-pattern': 1, + // RATIONALE: Eval is a security concern and a performance concern. + 'no-eval': 1, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-ex-assign': 2, + // RATIONALE: System types are global and should not be tampered with in a scalable code base. + // If two different libraries (or two versions of the same library) both try to modify + // a type, only one of them can win. Polyfills are acceptable because they implement + // a standardized interoperable contract, but polyfills are generally coded in plain + // JavaScript. + 'no-extend-native': 1, + // Disallow unnecessary labels + 'no-extra-label': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-fallthrough': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-func-assign': 1, + // RATIONALE: Catches a common coding mistake. + 'no-implied-eval': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-invalid-regexp': 2, + // RATIONALE: Catches a common coding mistake. + 'no-label-var': 2, + // RATIONALE: Eliminates redundant code. + 'no-lone-blocks': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-misleading-character-class': 2, + // RATIONALE: Catches a common coding mistake. + 'no-multi-str': 2, + // RATIONALE: It's generally a bad practice to call "new Thing()" without assigning the result to + // a variable. Either it's part of an awkward expression like "(new Thing()).doSomething()", + // or else implies that the constructor is doing nontrivial computations, which is often + // a poor class design. + 'no-new': 1, + // RATIONALE: Obsolete language feature that is deprecated. + 'no-new-func': 2, + // RATIONALE: Obsolete language feature that is deprecated. + 'no-new-object': 2, + // RATIONALE: Obsolete notation. + 'no-new-wrappers': 1, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-octal': 2, + // RATIONALE: Catches code that is likely to be incorrect + 'no-octal-escape': 2, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-regex-spaces': 2, + // RATIONALE: Catches a common coding mistake. + 'no-return-assign': 2, + // RATIONALE: Security risk. + 'no-script-url': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-self-assign': 2, + // RATIONALE: Catches a common coding mistake. + 'no-self-compare': 2, + // RATIONALE: This avoids statements such as "while (a = next(), a && a.length);" that use + // commas to create compound expressions. In general code is more readable if each + // step is split onto a separate line. This also makes it easier to set breakpoints + // in the debugger. + 'no-sequences': 1, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-shadow-restricted-names': 2, + // RATIONALE: Obsolete language feature that is deprecated. + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-sparse-arrays': 2, + // RATIONALE: Although in theory JavaScript allows any possible data type to be thrown as an exception, + // such flexibility adds pointless complexity, by requiring every catch block to test + // the type of the object that it receives. Whereas if catch blocks can always assume + // that their object implements the "Error" contract, then the code is simpler, and + // we generally get useful additional information like a call stack. + 'no-throw-literal': 2, + // RATIONALE: Catches a common coding mistake. + 'no-unmodified-loop-condition': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-unsafe-finally': 2, + // RATIONALE: Catches a common coding mistake. + 'no-unused-expressions': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-unused-labels': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-useless-catch': 1, + // RATIONALE: Avoids a potential performance problem. + 'no-useless-concat': 1, + // RATIONALE: The "var" keyword is deprecated because of its confusing "hoisting" behavior. + // Always use "let" or "const" instead. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + 'no-var': 2, + // RATIONALE: Generally not needed in modern code. + 'no-void': 1, + // RATIONALE: Obsolete language feature that is deprecated. + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-with': 2, + // RATIONALE: Makes logic easier to understand, since constants always have a known value + // @typescript-eslint\eslint-plugin\dist\configs\eslint-recommended.js + 'prefer-const': 1, + // RATIONALE: Catches a common coding mistake where "resolve" and "reject" are confused. + 'promise/param-names': 2, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'require-atomic-updates': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'require-yield': 1, + // "Use strict" is redundant when using the TypeScript compiler. + 'strict': [ + 2, + 'never' + ], + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'use-isnan': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + // Set to 1 (warning) or 2 (error) to enable. + // Rationale to disable: !!{} + 'no-extra-boolean-cast': 0, + // ==================================================================== + // @microsoft/eslint-plugin-spfx + // ==================================================================== + '@microsoft/spfx/import-requires-chunk-name': 1, + '@microsoft/spfx/no-require-ensure': 2, + '@microsoft/spfx/pair-react-dom-render-unmount': 1 + } + }, + { + // For unit tests, we can be a little bit less strict. The settings below revise the + // defaults specified in the extended configurations, as well as above. + files: [ + // Test files + '*.test.ts', + '*.test.tsx', + '*.spec.ts', + '*.spec.tsx', + + // Facebook convention + '**/__mocks__/*.ts', + '**/__mocks__/*.tsx', + '**/__tests__/*.ts', + '**/__tests__/*.tsx', + + // Microsoft convention + '**/test/*.ts', + '**/test/*.tsx' + ], + rules: {} + } + ] +}; \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/.gitignore b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/.gitignore new file mode 100644 index 00000000000..51ca7b9e7a7 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/.gitignore @@ -0,0 +1,34 @@ +# Logs +logs +*.log +npm-debug.log* + +# Dependency directories +node_modules + +# Build generated files +dist +lib +release +solution +temp +*.sppkg +.heft + +# Coverage directory used by tools like istanbul +coverage + +# OSX +.DS_Store + +# Visual Studio files +.ntvs_analysis.dat +.vs +bin +obj + +# Resx Generated Code +*.resx.ts + +# Styles Generated Code +*.scss.ts diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/.npmignore b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/.npmignore new file mode 100644 index 00000000000..ae0b487c075 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/.npmignore @@ -0,0 +1,16 @@ +!dist +config + +gulpfile.js + +release +src +temp + +tsconfig.json +tslint.json + +*.log + +.yo-rc.json +.vscode diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/.vscode/launch.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/.vscode/launch.json new file mode 100644 index 00000000000..53ca22cd853 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Hosted workbench", + "type": "msedge", + "request": "launch", + "url": "https://{tenantDomain}/_layouts/workbench.aspx", + "webRoot": "${workspaceRoot}", + "sourceMaps": true, + "sourceMapPathOverrides": { + "webpack:///.././src/*": "${webRoot}/src/*", + "webpack:///../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../../src/*": "${webRoot}/src/*" + }, + "runtimeArgs": [ + "--remote-debugging-port=9222", + "-incognito" + ] + } + ] +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/.vscode/settings.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/.vscode/settings.json new file mode 100644 index 00000000000..a31a2c33223 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/.vscode/settings.json @@ -0,0 +1,13 @@ +// Place your settings in this file to overwrite default and user settings. +{ + // Configure glob patterns for excluding files and folders in the file explorer. + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/bower_components": true, + "**/coverage": true, + "**/lib-amd": true, + "src/**/*.scss.ts": true + }, + "typescript.tsdk": ".\\node_modules\\typescript\\lib" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/.yo-rc.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/.yo-rc.json new file mode 100644 index 00000000000..43476a60502 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/.yo-rc.json @@ -0,0 +1,25 @@ +{ + "@microsoft/generator-sharepoint": { + "whichFolder": "subdir", + "solutionName": "spfx", + "environment": "spo", + "skipFeatureDeployment": false, + "isDomainIsolated": false, + "componentType": "webpart", + "template": "none", + "componentName": "HelloWorld", + "componentDescription": "HelloWorld", + "plusBeta": false, + "isCreatingSolution": true, + "nodeVersion": "18.16.0", + "sdksVersions": { + "@microsoft/microsoft-graph-client": "3.0.2", + "@microsoft/teams-js": "2.12.0" + }, + "version": "1.18.0", + "libraryName": "spfx", + "libraryId": "1cd14766-14af-406b-87ad-ce978b0f10ff", + "packageManager": "npm", + "solutionShortDescription": "spfx description" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/README.md b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/README.md new file mode 100644 index 00000000000..4f69a527598 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/README.md @@ -0,0 +1,73 @@ +# spfx + +## Summary + +Short summary on functionality and used technologies. + +[picture of the solution in action, if possible] + +## Used SharePoint Framework Version + +![version](https://img.shields.io/badge/version-1.18.0-green.svg) + +## Applies to + +- [SharePoint Framework](https://aka.ms/spfx) +- [Microsoft 365 tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant) + +> Get your own free development tenant by subscribing to [Microsoft 365 developer program](http://aka.ms/o365devprogram) + +## Prerequisites + +> Any special pre-requisites? + +## Solution + +| Solution | Author(s) | +| ----------- | ------------------------------------------------------- | +| folder name | Author details (name, company, twitter alias with link) | + +## Version history + +| Version | Date | Comments | +| ------- | ---------------- | --------------- | +| 1.1 | March 10, 2021 | Update comment | +| 1.0 | January 29, 2021 | Initial release | + +## Disclaimer + +**THIS CODE IS PROVIDED _AS IS_ WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.** + +--- + +## Minimal Path to Awesome + +- Clone this repository +- Ensure that you are at the solution folder +- in the command-line run: + - **npm install** + - **gulp serve** + +> Include any additional steps as needed. + +## Features + +Description of the extension that expands upon high-level summary above. + +This extension illustrates the following concepts: + +- topic 1 +- topic 2 +- topic 3 + +> Notice that better pictures and documentation will increase the sample usage and the value you are providing for others. Thanks for your submissions advance. + +> Share your web part with others through Microsoft 365 Patterns and Practices program to get visibility and exposure. More details on the community, open-source projects and other activities from http://aka.ms/m365pnp. + +## References + +- [Getting started with SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant) +- [Building for Microsoft teams](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/build-for-teams-overview) +- [Use Microsoft Graph in your solution](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/using-microsoft-graph-apis) +- [Publish SharePoint Framework applications to the Marketplace](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/publish-to-marketplace-overview) +- [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - Guidance, tooling, samples and open-source controls for your Microsoft 365 development diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/config/config.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/config/config.json new file mode 100644 index 00000000000..5712fed7b1d --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/config/config.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json", + "version": "2.0", + "bundles": { + "hello-world-web-part": { + "components": [ + { + "entrypoint": "./lib/webparts/helloWorld/HelloWorldWebPart.js", + "manifest": "./src/webparts/helloWorld/HelloWorldWebPart.manifest.json" + } + ] + } + }, + "externals": {}, + "localizedResources": { + "HelloWorldWebPartStrings": "lib/webparts/helloWorld/loc/{locale}.js" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/config/deploy-azure-storage.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/config/deploy-azure-storage.json new file mode 100644 index 00000000000..02290df98af --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/config/deploy-azure-storage.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json", + "workingDir": "./release/assets/", + "account": "", + "container": "spfx", + "accessKey": "" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/config/package-solution.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/config/package-solution.json new file mode 100644 index 00000000000..80b9c845948 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/config/package-solution.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json", + "solution": { + "name": "spfx-client-side-solution", + "id": "1cd14766-14af-406b-87ad-ce978b0f10ff", + "version": "1.0.0.0", + "includeClientSideAssets": true, + "isDomainIsolated": false, + "developer": { + "name": "", + "websiteUrl": "", + "privacyUrl": "", + "termsOfUseUrl": "", + "mpnId": "Undefined-1.18.0" + }, + "metadata": { + "shortDescription": { + "default": "spfx description" + }, + "longDescription": { + "default": "spfx description" + }, + "screenshotPaths": [], + "videoUrl": "", + "categories": [] + }, + "features": [ + { + "title": "spfx Feature", + "description": "The feature that activates elements of the spfx solution.", + "id": "a04c7c39-eb50-431b-8967-b319d028da74", + "version": "1.0.0.0" + } + ] + }, + "paths": { + "zippedPackage": "solution/spfx.sppkg" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/config/sass.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/config/sass.json new file mode 100644 index 00000000000..5e78c982d8d --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/config/sass.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/core-build/sass.schema.json" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/config/serve.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/config/serve.json new file mode 100644 index 00000000000..a4c03e28723 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/config/serve.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/spfx-serve.schema.json", + "port": 4321, + "https": true, + "initialPage": "https://{tenantDomain}/_layouts/workbench.aspx" +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/config/write-manifests.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/config/write-manifests.json new file mode 100644 index 00000000000..bad3526054b --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/config/write-manifests.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json", + "cdnBasePath": "" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/gulpfile.js b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/gulpfile.js new file mode 100644 index 00000000000..be2918708a7 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/gulpfile.js @@ -0,0 +1,16 @@ +'use strict'; + +const build = require('@microsoft/sp-build-web'); + +build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`); + +var getTasks = build.rig.getTasks; +build.rig.getTasks = function () { + var result = getTasks.call(build.rig); + + result.set('serve', result.get('serve-deprecated')); + + return result; +}; + +build.initialize(require('gulp')); diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/package.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/package.json new file mode 100644 index 00000000000..61134b279a6 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/package.json @@ -0,0 +1,46 @@ +{ + "name": "spfx", + "version": "0.0.1", + "private": true, + "engines": { + "node": ">=16.13.0 <17.0.0 || >=18.17.1 <19.0.0" + }, + "main": "lib/index.js", + "scripts": { + "build": "gulp bundle", + "clean": "gulp clean", + "test": "gulp test" + }, + "dependencies": { + "tslib": "2.3.1", + "@microsoft/sp-core-library": "1.18.0", + "@microsoft/sp-property-pane": "1.18.0", + "@microsoft/sp-webpart-base": "1.18.0", + "@microsoft/sp-lodash-subset": "1.18.0", + "@microsoft/sp-office-ui-fabric-core": "1.18.0", + "@microsoft/sp-component-base": "1.18.0", + "@microsoft/sp-diagnostics": "1.18.0", + "@microsoft/sp-dynamic-data": "1.18.0", + "@microsoft/sp-extension-base": "1.18.0", + "@microsoft/sp-http": "1.18.0", + "@microsoft/sp-list-subscription": "1.18.0", + "@microsoft/sp-loader": "1.18.0", + "@microsoft/sp-module-interfaces": "1.18.0", + "@microsoft/sp-odata-types": "1.18.0", + "@microsoft/sp-page-context": "1.18.0" + }, + "devDependencies": { + "@microsoft/rush-stack-compiler-4.7": "0.1.0", + "@rushstack/eslint-config": "2.5.1", + "@microsoft/eslint-plugin-spfx": "1.18.0", + "@microsoft/eslint-config-spfx": "1.18.0", + "@microsoft/sp-build-web": "1.18.0", + "@types/webpack-env": "~1.15.2", + "ajv": "^6.12.5", + "eslint": "8.7.0", + "gulp": "4.0.2", + "typescript": "4.7.4", + "@microsoft/sp-module-interfaces": "1.18.0", + "@fluentui/react": "^8.106.4" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/src/index.ts b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/src/index.ts new file mode 100644 index 00000000000..fb81db1e213 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/src/index.ts @@ -0,0 +1 @@ +// A file is required to be in the root of the /src directory by the TypeScript compiler diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/src/webparts/helloWorld/HelloWorldWebPart.manifest.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/src/webparts/helloWorld/HelloWorldWebPart.manifest.json new file mode 100644 index 00000000000..596007e91a5 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/src/webparts/helloWorld/HelloWorldWebPart.manifest.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json", + "id": "f4e48586-4808-4ff9-8ed2-451c151b74a3", + "alias": "HelloWorldWebPart", + "componentType": "WebPart", + + // The "*" signifies that the version should be taken from the package.json + "version": "*", + "manifestVersion": 2, + + // If true, the component can only be installed on sites where Custom Script is allowed. + // Components that allow authors to embed arbitrary script code should set this to true. + // https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f + "requiresCustomScript": false, + "supportedHosts": ["SharePointWebPart", "TeamsPersonalApp", "TeamsTab", "SharePointFullPage"], + "supportsThemeVariants": true, + + "preconfiguredEntries": [{ + "groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Advanced + "group": { "default": "Advanced" }, + "title": { "default": "HelloWorld" }, + "description": { "default": "HelloWorld" }, + "officeFabricIconFontName": "Page", + "properties": { + "description": "HelloWorld" + } + }] +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/src/webparts/helloWorld/HelloWorldWebPart.module.scss b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/src/webparts/helloWorld/HelloWorldWebPart.module.scss new file mode 100644 index 00000000000..2f9eb7ab6ee --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/src/webparts/helloWorld/HelloWorldWebPart.module.scss @@ -0,0 +1,34 @@ +@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss'; + +.helloWorld { + overflow: hidden; + padding: 1em; + color: "[theme:bodyText, default: #323130]"; + color: var(--bodyText); + &.teams { + font-family: $ms-font-family-fallbacks; + } +} + +.welcome { + text-align: center; +} + +.welcomeImage { + width: 100%; + max-width: 420px; +} + +.links { + a { + text-decoration: none; + color: "[theme:link, default:#03787c]"; + color: var(--link); // note: CSS Custom Properties support is limited to modern browsers only + + &:hover { + text-decoration: underline; + color: "[theme:linkHovered, default: #014446]"; + color: var(--linkHovered); // note: CSS Custom Properties support is limited to modern browsers only + } + } +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/src/webparts/helloWorld/HelloWorldWebPart.ts b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/src/webparts/helloWorld/HelloWorldWebPart.ts new file mode 100644 index 00000000000..c7a03dbd58f --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/src/webparts/helloWorld/HelloWorldWebPart.ts @@ -0,0 +1,128 @@ +import { Version } from '@microsoft/sp-core-library'; +import { + type IPropertyPaneConfiguration, + PropertyPaneTextField +} from '@microsoft/sp-property-pane'; +import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base'; +import type { IReadonlyTheme } from '@microsoft/sp-component-base'; +import { escape } from '@microsoft/sp-lodash-subset'; + +import styles from './HelloWorldWebPart.module.scss'; +import * as strings from 'HelloWorldWebPartStrings'; + +export interface IHelloWorldWebPartProps { + description: string; +} + +export default class HelloWorldWebPart extends BaseClientSideWebPart { + + private _isDarkTheme: boolean = false; + private _environmentMessage: string = ''; + + public render(): void { + this.domElement.innerHTML = ` +
+
+ +

Well done, ${escape(this.context.pageContext.user.displayName)}!

+
${this._environmentMessage}
+
Web part property value: ${escape(this.properties.description)}
+
+
+

Welcome to SharePoint Framework!

+

+ The SharePoint Framework (SPFx) is a extensibility model for Microsoft Viva, Microsoft Teams and SharePoint. It's the easiest way to extend Microsoft 365 with automatic Single Sign On, automatic hosting and industry standard tooling. +

+

Learn more about SPFx development:

+ +
+
`; + } + + protected onInit(): Promise { + return this._getEnvironmentMessage().then(message => { + this._environmentMessage = message; + }); + } + + + + private _getEnvironmentMessage(): Promise { + if (!!this.context.sdks.microsoftTeams) { // running in Teams, office.com or Outlook + return this.context.sdks.microsoftTeams.teamsJs.app.getContext() + .then(context => { + let environmentMessage: string = ''; + switch (context.app.host.name) { + case 'Office': // running in Office + environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentOffice : strings.AppOfficeEnvironment; + break; + case 'Outlook': // running in Outlook + environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentOutlook : strings.AppOutlookEnvironment; + break; + case 'Teams': // running in Teams + case 'TeamsModern': + environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentTeams : strings.AppTeamsTabEnvironment; + break; + default: + environmentMessage = strings.UnknownEnvironment; + } + + return environmentMessage; + }); + } + + return Promise.resolve(this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentSharePoint : strings.AppSharePointEnvironment); + } + + protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void { + if (!currentTheme) { + return; + } + + this._isDarkTheme = !!currentTheme.isInverted; + const { + semanticColors + } = currentTheme; + + if (semanticColors) { + this.domElement.style.setProperty('--bodyText', semanticColors.bodyText || null); + this.domElement.style.setProperty('--link', semanticColors.link || null); + this.domElement.style.setProperty('--linkHovered', semanticColors.linkHovered || null); + } + + } + + protected get dataVersion(): Version { + return Version.parse('1.0'); + } + + protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration { + return { + pages: [ + { + header: { + description: strings.PropertyPaneDescription + }, + groups: [ + { + groupName: strings.BasicGroupName, + groupFields: [ + PropertyPaneTextField('description', { + label: strings.DescriptionFieldLabel + }) + ] + } + ] + } + ] + }; + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/src/webparts/helloWorld/assets/welcome-dark.png b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/src/webparts/helloWorld/assets/welcome-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..42f0b8d24a9aa964a2be4885fe5700c1c54191a9 GIT binary patch literal 12545 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�Gp~kbx)oU28ew4C2+WtKA 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 G`2PSf1J4)$ literal 0 HcmV?d00001 diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/src/webparts/helloWorld/assets/welcome-light.png b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/src/webparts/helloWorld/assets/welcome-light.png new file mode 100644 index 0000000000000000000000000000000000000000..69eb3b48cd83031f106df4b4df127c749657e319 GIT binary patch literal 12816 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 literal 0 HcmV?d00001 diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/src/webparts/helloWorld/loc/en-us.js b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/src/webparts/helloWorld/loc/en-us.js new file mode 100644 index 00000000000..3b25e74a7d6 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/src/webparts/helloWorld/loc/en-us.js @@ -0,0 +1,16 @@ +define([], function() { + return { + "PropertyPaneDescription": "Description", + "BasicGroupName": "Group Name", + "DescriptionFieldLabel": "Description Field", + "AppLocalEnvironmentSharePoint": "The app is running on your local environment as SharePoint web part", + "AppLocalEnvironmentTeams": "The app is running on your local environment as Microsoft Teams app", + "AppLocalEnvironmentOffice": "The app is running on your local environment in office.com", + "AppLocalEnvironmentOutlook": "The app is running on your local environment in Outlook", + "AppSharePointEnvironment": "The app is running on SharePoint page", + "AppTeamsTabEnvironment": "The app is running in Microsoft Teams", + "AppOfficeEnvironment": "The app is running in office.com", + "AppOutlookEnvironment": "The app is running in Outlook", + "UnknownEnvironment": "The app is running in an unknown environment" + } +}); \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/src/webparts/helloWorld/loc/mystrings.d.ts b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/src/webparts/helloWorld/loc/mystrings.d.ts new file mode 100644 index 00000000000..21046bcd8bc --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/src/webparts/helloWorld/loc/mystrings.d.ts @@ -0,0 +1,19 @@ +declare interface IHelloWorldWebPartStrings { + PropertyPaneDescription: string; + BasicGroupName: string; + DescriptionFieldLabel: string; + AppLocalEnvironmentSharePoint: string; + AppLocalEnvironmentTeams: string; + AppLocalEnvironmentOffice: string; + AppLocalEnvironmentOutlook: string; + AppSharePointEnvironment: string; + AppTeamsTabEnvironment: string; + AppOfficeEnvironment: string; + AppOutlookEnvironment: string; + UnknownEnvironment: string; +} + +declare module 'HelloWorldWebPartStrings' { + const strings: IHelloWorldWebPartStrings; + export = strings; +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/teams/f4e48586-4808-4ff9-8ed2-451c151b74a3_color.png b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/teams/f4e48586-4808-4ff9-8ed2-451c151b74a3_color.png new file mode 100644 index 0000000000000000000000000000000000000000..0e1f764fa8df4791a61c71b4f011c26f9083ee52 GIT binary patch literal 10248 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 KtK_^>#Qy@$+e&%> literal 0 HcmV?d00001 diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/teams/f4e48586-4808-4ff9-8ed2-451c151b74a3_outline.png b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/teams/f4e48586-4808-4ff9-8ed2-451c151b74a3_outline.png new file mode 100644 index 0000000000000000000000000000000000000000..e8cb4b6ba4f726d47a2e274f16b6069b9a8041cc GIT binary patch literal 249 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz&H|6fVg?3oVGw3ym^DWND9BhG z%|CHbgX^da?eHs6`1kLARhjOac zf3;y1Iq~M{LLS3g_M2bU{+PBvomV=FH7$YTy5I%1<5B$=?>3fqI5P%5iajq7)W9SX p;gpazd1JnvZNlx8HB0WjVJ`J~Q+P@%pA+aZ22WQ%mvv4FO#n^cR9FB2 literal 0 HcmV?d00001 diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/tsconfig.json b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/tsconfig.json new file mode 100644 index 00000000000..c4cd392ad1a --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1180-webpart-optionaldeps/tsconfig.json @@ -0,0 +1,35 @@ +{ + "extends": "./node_modules/@microsoft/rush-stack-compiler-4.7/includes/tsconfig-web.json", + "compilerOptions": { + "target": "es5", + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "jsx": "react", + "declaration": true, + "sourceMap": true, + "experimentalDecorators": true, + "skipLibCheck": true, + "outDir": "lib", + "inlineSources": false, + "noImplicitAny": true, + + "typeRoots": [ + "./node_modules/@types", + "./node_modules/@microsoft" + ], + "types": [ + "webpack-env" + ], + "lib": [ + "es5", + "dom", + "es2015.collection", + "es2015.promise" + ] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx" + ] +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/.eslintrc.js b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/.eslintrc.js new file mode 100644 index 00000000000..473df80cdf9 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/.eslintrc.js @@ -0,0 +1,352 @@ +require('@rushstack/eslint-config/patch/modern-module-resolution'); +module.exports = { + extends: ['@microsoft/eslint-config-spfx/lib/profiles/react'], + parserOptions: { tsconfigRootDir: __dirname }, + overrides: [ + { + files: ['*.ts', '*.tsx'], + parser: '@typescript-eslint/parser', + 'parserOptions': { + 'project': './tsconfig.json', + 'ecmaVersion': 2018, + 'sourceType': 'module' + }, + rules: { + // Prevent usage of the JavaScript null value, while allowing code to access existing APIs that may require null. https://www.npmjs.com/package/@rushstack/eslint-plugin + '@rushstack/no-new-null': 1, + // Require Jest module mocking APIs to be called before any other statements in their code block. https://www.npmjs.com/package/@rushstack/eslint-plugin + '@rushstack/hoist-jest-mock': 1, + // Require regular expressions to be constructed from string constants rather than dynamically building strings at runtime. https://www.npmjs.com/package/@rushstack/eslint-plugin-security + '@rushstack/security/no-unsafe-regexp': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/adjacent-overload-signatures': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // + // CONFIGURATION: By default, these are banned: String, Boolean, Number, Object, Symbol + '@typescript-eslint/ban-types': [ + 1, + { + 'extendDefaults': false, + 'types': { + 'String': { + 'message': 'Use \'string\' instead', + 'fixWith': 'string' + }, + 'Boolean': { + 'message': 'Use \'boolean\' instead', + 'fixWith': 'boolean' + }, + 'Number': { + 'message': 'Use \'number\' instead', + 'fixWith': 'number' + }, + 'Object': { + 'message': 'Use \'object\' instead, or else define a proper TypeScript type:' + }, + 'Symbol': { + 'message': 'Use \'symbol\' instead', + 'fixWith': 'symbol' + }, + 'Function': { + 'message': 'The \'Function\' type accepts any function-like value.\nIt provides no type safety when calling the function, which can be a common source of bugs.\nIt also accepts things like class declarations, which will throw at runtime as they will not be called with \'new\'.\nIf you are expecting the function to accept certain arguments, you should explicitly define the function shape.' + } + } + } + ], + // RATIONALE: Code is more readable when the type of every variable is immediately obvious. + // Even if the compiler may be able to infer a type, this inference will be unavailable + // to a person who is reviewing a GitHub diff. This rule makes writing code harder, + // but writing code is a much less important activity than reading it. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/explicit-function-return-type': [ + 1, + { + 'allowExpressions': true, + 'allowTypedFunctionExpressions': true, + 'allowHigherOrderFunctions': false + } + ], + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Rationale to disable: although this is a recommended rule, it is up to dev to select coding style. + // Set to 1 (warning) or 2 (error) to enable. + '@typescript-eslint/explicit-member-accessibility': 0, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-array-constructor': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // + // RATIONALE: The "any" keyword disables static type checking, the main benefit of using TypeScript. + // This rule should be suppressed only in very special cases such as JSON.stringify() + // where the type really can be anything. Even if the type is flexible, another type + // may be more appropriate such as "unknown", "{}", or "Record". + '@typescript-eslint/no-explicit-any': 1, + // RATIONALE: The #1 rule of promises is that every promise chain must be terminated by a catch() + // handler. Thus wherever a Promise arises, the code must either append a catch handler, + // or else return the object to a caller (who assumes this responsibility). Unterminated + // promise chains are a serious issue. Besides causing errors to be silently ignored, + // they can also cause a NodeJS process to terminate unexpectedly. + '@typescript-eslint/no-floating-promises': 2, + // RATIONALE: Catches a common coding mistake. + '@typescript-eslint/no-for-in-array': 2, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-misused-new': 2, + // RATIONALE: The "namespace" keyword is not recommended for organizing code because JavaScript lacks + // a "using" statement to traverse namespaces. Nested namespaces prevent certain bundler + // optimizations. If you are declaring loose functions/variables, it's better to make them + // static members of a class, since classes support property getters and their private + // members are accessible by unit tests. Also, the exercise of choosing a meaningful + // class name tends to produce more discoverable APIs: for example, search+replacing + // the function "reverse()" is likely to return many false matches, whereas if we always + // write "Text.reverse()" is more unique. For large scale organization, it's recommended + // to decompose your code into separate NPM packages, which ensures that component + // dependencies are tracked more conscientiously. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-namespace': [ + 1, + { + 'allowDeclarations': false, + 'allowDefinitionFiles': false + } + ], + // RATIONALE: Parameter properties provide a shorthand such as "constructor(public title: string)" + // that avoids the effort of declaring "title" as a field. This TypeScript feature makes + // code easier to write, but arguably sacrifices readability: In the notes for + // "@typescript-eslint/member-ordering" we pointed out that fields are central to + // a class's design, so we wouldn't want to bury them in a constructor signature + // just to save some typing. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Set to 1 (warning) or 2 (error) to enable the rule + '@typescript-eslint/parameter-properties': 0, + // RATIONALE: When left in shipping code, unused variables often indicate a mistake. Dead code + // may impact performance. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-unused-vars': [ + 1, + { + 'vars': 'all', + // Unused function arguments often indicate a mistake in JavaScript code. However in TypeScript code, + // the compiler catches most of those mistakes, and unused arguments are fairly common for type signatures + // that are overriding a base class method or implementing an interface. + 'args': 'none' + } + ], + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/no-use-before-define': [ + 2, + { + 'functions': false, + 'classes': true, + 'variables': true, + 'enums': true, + 'typedefs': true + } + ], + // Disallows require statements except in import statements. + // In other words, the use of forms such as var foo = require("foo") are banned. Instead use ES6 style imports or import foo = require("foo") imports. + '@typescript-eslint/no-var-requires': 'error', + // RATIONALE: The "module" keyword is deprecated except when describing legacy libraries. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + '@typescript-eslint/prefer-namespace-keyword': 1, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Rationale to disable: it's up to developer to decide if he wants to add type annotations + // Set to 1 (warning) or 2 (error) to enable the rule + '@typescript-eslint/no-inferrable-types': 0, + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + // Rationale to disable: declaration of empty interfaces may be helpful for generic types scenarios + '@typescript-eslint/no-empty-interface': 0, + // RATIONALE: This rule warns if setters are defined without getters, which is probably a mistake. + 'accessor-pairs': 1, + // RATIONALE: In TypeScript, if you write x["y"] instead of x.y, it disables type checking. + 'dot-notation': [ + 1, + { + 'allowPattern': '^_' + } + ], + // RATIONALE: Catches code that is likely to be incorrect + 'eqeqeq': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'for-direction': 1, + // RATIONALE: Catches a common coding mistake. + 'guard-for-in': 2, + // RATIONALE: If you have more than 2,000 lines in a single source file, it's probably time + // to split up your code. + 'max-lines': ['warn', { max: 2000 }], + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-async-promise-executor': 2, + // RATIONALE: Deprecated language feature. + 'no-caller': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-compare-neg-zero': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-cond-assign': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-constant-condition': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-control-regex': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-debugger': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-delete-var': 2, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-duplicate-case': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-empty': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-empty-character-class': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-empty-pattern': 1, + // RATIONALE: Eval is a security concern and a performance concern. + 'no-eval': 1, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-ex-assign': 2, + // RATIONALE: System types are global and should not be tampered with in a scalable code base. + // If two different libraries (or two versions of the same library) both try to modify + // a type, only one of them can win. Polyfills are acceptable because they implement + // a standardized interoperable contract, but polyfills are generally coded in plain + // JavaScript. + 'no-extend-native': 1, + // Disallow unnecessary labels + 'no-extra-label': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-fallthrough': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-func-assign': 1, + // RATIONALE: Catches a common coding mistake. + 'no-implied-eval': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-invalid-regexp': 2, + // RATIONALE: Catches a common coding mistake. + 'no-label-var': 2, + // RATIONALE: Eliminates redundant code. + 'no-lone-blocks': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-misleading-character-class': 2, + // RATIONALE: Catches a common coding mistake. + 'no-multi-str': 2, + // RATIONALE: It's generally a bad practice to call "new Thing()" without assigning the result to + // a variable. Either it's part of an awkward expression like "(new Thing()).doSomething()", + // or else implies that the constructor is doing nontrivial computations, which is often + // a poor class design. + 'no-new': 1, + // RATIONALE: Obsolete language feature that is deprecated. + 'no-new-func': 2, + // RATIONALE: Obsolete language feature that is deprecated. + 'no-new-object': 2, + // RATIONALE: Obsolete notation. + 'no-new-wrappers': 1, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-octal': 2, + // RATIONALE: Catches code that is likely to be incorrect + 'no-octal-escape': 2, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-regex-spaces': 2, + // RATIONALE: Catches a common coding mistake. + 'no-return-assign': 2, + // RATIONALE: Security risk. + 'no-script-url': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-self-assign': 2, + // RATIONALE: Catches a common coding mistake. + 'no-self-compare': 2, + // RATIONALE: This avoids statements such as "while (a = next(), a && a.length);" that use + // commas to create compound expressions. In general code is more readable if each + // step is split onto a separate line. This also makes it easier to set breakpoints + // in the debugger. + 'no-sequences': 1, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-shadow-restricted-names': 2, + // RATIONALE: Obsolete language feature that is deprecated. + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-sparse-arrays': 2, + // RATIONALE: Although in theory JavaScript allows any possible data type to be thrown as an exception, + // such flexibility adds pointless complexity, by requiring every catch block to test + // the type of the object that it receives. Whereas if catch blocks can always assume + // that their object implements the "Error" contract, then the code is simpler, and + // we generally get useful additional information like a call stack. + 'no-throw-literal': 2, + // RATIONALE: Catches a common coding mistake. + 'no-unmodified-loop-condition': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-unsafe-finally': 2, + // RATIONALE: Catches a common coding mistake. + 'no-unused-expressions': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-unused-labels': 1, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-useless-catch': 1, + // RATIONALE: Avoids a potential performance problem. + 'no-useless-concat': 1, + // RATIONALE: The "var" keyword is deprecated because of its confusing "hoisting" behavior. + // Always use "let" or "const" instead. + // + // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json + 'no-var': 2, + // RATIONALE: Generally not needed in modern code. + 'no-void': 1, + // RATIONALE: Obsolete language feature that is deprecated. + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'no-with': 2, + // RATIONALE: Makes logic easier to understand, since constants always have a known value + // @typescript-eslint\eslint-plugin\dist\configs\eslint-recommended.js + 'prefer-const': 1, + // RATIONALE: Catches a common coding mistake where "resolve" and "reject" are confused. + 'promise/param-names': 2, + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'require-atomic-updates': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'require-yield': 1, + // "Use strict" is redundant when using the TypeScript compiler. + 'strict': [ + 2, + 'never' + ], + // RATIONALE: Catches code that is likely to be incorrect + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + 'use-isnan': 2, + // STANDARDIZED BY: eslint\conf\eslint-recommended.js + // Set to 1 (warning) or 2 (error) to enable. + // Rationale to disable: !!{} + 'no-extra-boolean-cast': 0, + // ==================================================================== + // @microsoft/eslint-plugin-spfx + // ==================================================================== + '@microsoft/spfx/import-requires-chunk-name': 1, + '@microsoft/spfx/no-require-ensure': 2, + '@microsoft/spfx/pair-react-dom-render-unmount': 1 + } + }, + { + // For unit tests, we can be a little bit less strict. The settings below revise the + // defaults specified in the extended configurations, as well as above. + files: [ + // Test files + '*.test.ts', + '*.test.tsx', + '*.spec.ts', + '*.spec.tsx', + + // Facebook convention + '**/__mocks__/*.ts', + '**/__mocks__/*.tsx', + '**/__tests__/*.ts', + '**/__tests__/*.tsx', + + // Microsoft convention + '**/test/*.ts', + '**/test/*.tsx' + ], + rules: {} + } + ] +}; \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/.gitignore b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/.gitignore new file mode 100644 index 00000000000..51ca7b9e7a7 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/.gitignore @@ -0,0 +1,34 @@ +# Logs +logs +*.log +npm-debug.log* + +# Dependency directories +node_modules + +# Build generated files +dist +lib +release +solution +temp +*.sppkg +.heft + +# Coverage directory used by tools like istanbul +coverage + +# OSX +.DS_Store + +# Visual Studio files +.ntvs_analysis.dat +.vs +bin +obj + +# Resx Generated Code +*.resx.ts + +# Styles Generated Code +*.scss.ts diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/.npmignore b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/.npmignore new file mode 100644 index 00000000000..ae0b487c075 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/.npmignore @@ -0,0 +1,16 @@ +!dist +config + +gulpfile.js + +release +src +temp + +tsconfig.json +tslint.json + +*.log + +.yo-rc.json +.vscode diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/.vscode/launch.json b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/.vscode/launch.json new file mode 100644 index 00000000000..53ca22cd853 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Hosted workbench", + "type": "msedge", + "request": "launch", + "url": "https://{tenantDomain}/_layouts/workbench.aspx", + "webRoot": "${workspaceRoot}", + "sourceMaps": true, + "sourceMapPathOverrides": { + "webpack:///.././src/*": "${webRoot}/src/*", + "webpack:///../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../src/*": "${webRoot}/src/*", + "webpack:///../../../../../src/*": "${webRoot}/src/*" + }, + "runtimeArgs": [ + "--remote-debugging-port=9222", + "-incognito" + ] + } + ] +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/.vscode/settings.json b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/.vscode/settings.json new file mode 100644 index 00000000000..161416626c7 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/.vscode/settings.json @@ -0,0 +1,14 @@ +// Place your settings in this file to overwrite default and user settings. +{ + // Configure glob patterns for excluding files and folders in the file explorer. + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/bower_components": true, + "**/coverage": true, + "**/jest-output": true, + "**/lib-amd": true, + "src/**/*.scss.ts": true + }, + "typescript.tsdk": ".\\node_modules\\typescript\\lib" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/.yo-rc.json b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/.yo-rc.json new file mode 100644 index 00000000000..23926f9cdd1 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/.yo-rc.json @@ -0,0 +1,25 @@ +{ + "@microsoft/generator-sharepoint": { + "whichFolder": "subdir", + "solutionName": "spfx", + "environment": "spo", + "skipFeatureDeployment": false, + "isDomainIsolated": false, + "componentType": "webpart", + "template": "react", + "componentName": "HelloWorld", + "componentDescription": "HelloWorld", + "plusBeta": false, + "isCreatingSolution": true, + "nodeVersion": "18.18.2", + "sdksVersions": { + "@microsoft/microsoft-graph-client": "3.0.2", + "@microsoft/teams-js": "2.12.0" + }, + "version": "1.18.1-rc.0", + "libraryName": "spfx", + "libraryId": "9201c0a3-2f43-4bf3-8620-ce64b82af222", + "packageManager": "npm", + "solutionShortDescription": "spfx description" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/README.md b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/README.md new file mode 100644 index 00000000000..6c1d70cb3ea --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/README.md @@ -0,0 +1,73 @@ +# spfx + +## Summary + +Short summary on functionality and used technologies. + +[picture of the solution in action, if possible] + +## Used SharePoint Framework Version + +![version](https://img.shields.io/badge/version-1.18.1--rc.0-yellow.svg) + +## Applies to + +- [SharePoint Framework](https://aka.ms/spfx) +- [Microsoft 365 tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant) + +> Get your own free development tenant by subscribing to [Microsoft 365 developer program](http://aka.ms/o365devprogram) + +## Prerequisites + +> Any special pre-requisites? + +## Solution + +| Solution | Author(s) | +| ----------- | ------------------------------------------------------- | +| folder name | Author details (name, company, twitter alias with link) | + +## Version history + +| Version | Date | Comments | +| ------- | ---------------- | --------------- | +| 1.1 | March 10, 2021 | Update comment | +| 1.0 | January 29, 2021 | Initial release | + +## Disclaimer + +**THIS CODE IS PROVIDED _AS IS_ WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.** + +--- + +## Minimal Path to Awesome + +- Clone this repository +- Ensure that you are at the solution folder +- in the command-line run: + - **npm install** + - **gulp serve** + +> Include any additional steps as needed. + +## Features + +Description of the extension that expands upon high-level summary above. + +This extension illustrates the following concepts: + +- topic 1 +- topic 2 +- topic 3 + +> Notice that better pictures and documentation will increase the sample usage and the value you are providing for others. Thanks for your submissions advance. + +> Share your web part with others through Microsoft 365 Patterns and Practices program to get visibility and exposure. More details on the community, open-source projects and other activities from http://aka.ms/m365pnp. + +## References + +- [Getting started with SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant) +- [Building for Microsoft teams](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/build-for-teams-overview) +- [Use Microsoft Graph in your solution](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/using-microsoft-graph-apis) +- [Publish SharePoint Framework applications to the Marketplace](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/publish-to-marketplace-overview) +- [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - Guidance, tooling, samples and open-source controls for your Microsoft 365 development diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/config/config.json b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/config/config.json new file mode 100644 index 00000000000..5712fed7b1d --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/config/config.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json", + "version": "2.0", + "bundles": { + "hello-world-web-part": { + "components": [ + { + "entrypoint": "./lib/webparts/helloWorld/HelloWorldWebPart.js", + "manifest": "./src/webparts/helloWorld/HelloWorldWebPart.manifest.json" + } + ] + } + }, + "externals": {}, + "localizedResources": { + "HelloWorldWebPartStrings": "lib/webparts/helloWorld/loc/{locale}.js" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/config/deploy-azure-storage.json b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/config/deploy-azure-storage.json new file mode 100644 index 00000000000..02290df98af --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/config/deploy-azure-storage.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json", + "workingDir": "./release/assets/", + "account": "", + "container": "spfx", + "accessKey": "" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/config/package-solution.json b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/config/package-solution.json new file mode 100644 index 00000000000..2676c4949a9 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/config/package-solution.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json", + "solution": { + "name": "spfx-client-side-solution", + "id": "9201c0a3-2f43-4bf3-8620-ce64b82af222", + "version": "1.0.0.0", + "includeClientSideAssets": true, + "isDomainIsolated": false, + "developer": { + "name": "", + "websiteUrl": "", + "privacyUrl": "", + "termsOfUseUrl": "", + "mpnId": "Undefined-1.18.1-rc.0" + }, + "metadata": { + "shortDescription": { + "default": "spfx description" + }, + "longDescription": { + "default": "spfx description" + }, + "screenshotPaths": [], + "videoUrl": "", + "categories": [] + }, + "features": [ + { + "title": "spfx Feature", + "description": "The feature that activates elements of the spfx solution.", + "id": "b1596d37-4363-4995-93d8-2da7ce88b47c", + "version": "1.0.0.0" + } + ] + }, + "paths": { + "zippedPackage": "solution/spfx.sppkg" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/config/sass.json b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/config/sass.json new file mode 100644 index 00000000000..5e78c982d8d --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/config/sass.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/core-build/sass.schema.json" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/config/serve.json b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/config/serve.json new file mode 100644 index 00000000000..a4c03e28723 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/config/serve.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/spfx-serve.schema.json", + "port": 4321, + "https": true, + "initialPage": "https://{tenantDomain}/_layouts/workbench.aspx" +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/config/write-manifests.json b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/config/write-manifests.json new file mode 100644 index 00000000000..bad3526054b --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/config/write-manifests.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json", + "cdnBasePath": "" +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/gulpfile.js b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/gulpfile.js new file mode 100644 index 00000000000..be2918708a7 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/gulpfile.js @@ -0,0 +1,16 @@ +'use strict'; + +const build = require('@microsoft/sp-build-web'); + +build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`); + +var getTasks = build.rig.getTasks; +build.rig.getTasks = function () { + var result = getTasks.call(build.rig); + + result.set('serve', result.get('serve-deprecated')); + + return result; +}; + +build.initialize(require('gulp')); diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/package.json b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/package.json new file mode 100644 index 00000000000..8f005fa10cf --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/package.json @@ -0,0 +1,42 @@ +{ + "name": "spfx", + "version": "0.0.1", + "private": true, + "engines": { + "node": ">=16.13.0 <17.0.0 || >=18.17.1 <19.0.0" + }, + "main": "lib/index.js", + "scripts": { + "build": "gulp bundle", + "clean": "gulp clean", + "test": "gulp test" + }, + "dependencies": { + "tslib": "2.3.1", + "react": "17.0.1", + "react-dom": "17.0.1", + "@fluentui/react": "^8.106.4", + "@microsoft/sp-core-library": "1.18.1-rc.0", + "@microsoft/sp-component-base": "1.18.1-rc.0", + "@microsoft/sp-property-pane": "1.18.1-rc.0", + "@microsoft/sp-webpart-base": "1.18.1-rc.0", + "@microsoft/sp-lodash-subset": "1.18.1-rc.0", + "@microsoft/sp-office-ui-fabric-core": "1.18.1-rc.0" + }, + "devDependencies": { + "@microsoft/rush-stack-compiler-4.7": "0.1.0", + "@rushstack/eslint-config": "2.5.1", + "@microsoft/eslint-plugin-spfx": "1.18.1-rc.0", + "@microsoft/eslint-config-spfx": "1.18.1-rc.0", + "@microsoft/sp-build-web": "1.18.1-rc.0", + "@types/webpack-env": "~1.15.2", + "ajv": "^6.12.5", + "eslint": "8.7.0", + "gulp": "4.0.2", + "typescript": "4.7.4", + "@types/react": "17.0.45", + "@types/react-dom": "17.0.17", + "eslint-plugin-react-hooks": "4.3.0", + "@microsoft/sp-module-interfaces": "1.18.1-rc.0" + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/index.ts b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/index.ts new file mode 100644 index 00000000000..fb81db1e213 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/index.ts @@ -0,0 +1 @@ +// A file is required to be in the root of the /src directory by the TypeScript compiler diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/HelloWorldWebPart.manifest.json b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/HelloWorldWebPart.manifest.json new file mode 100644 index 00000000000..44c19d4a2de --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/HelloWorldWebPart.manifest.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json", + "id": "21f4bb8e-5562-4721-8f92-73daad044daf", + "alias": "HelloWorldWebPart", + "componentType": "WebPart", + + // The "*" signifies that the version should be taken from the package.json + "version": "*", + "manifestVersion": 2, + + // If true, the component can only be installed on sites where Custom Script is allowed. + // Components that allow authors to embed arbitrary script code should set this to true. + // https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f + "requiresCustomScript": false, + "supportedHosts": ["SharePointWebPart", "TeamsPersonalApp", "TeamsTab", "SharePointFullPage"], + "supportsThemeVariants": true, + + "preconfiguredEntries": [{ + "groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Advanced + "group": { "default": "Advanced" }, + "title": { "default": "HelloWorld" }, + "description": { "default": "HelloWorld" }, + "officeFabricIconFontName": "Page", + "properties": { + "description": "HelloWorld" + } + }] +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/HelloWorldWebPart.ts b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/HelloWorldWebPart.ts new file mode 100644 index 00000000000..e0c367c855a --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/HelloWorldWebPart.ts @@ -0,0 +1,121 @@ +import * as React from 'react'; +import * as ReactDom from 'react-dom'; +import { Version } from '@microsoft/sp-core-library'; +import { + type IPropertyPaneConfiguration, + PropertyPaneTextField +} from '@microsoft/sp-property-pane'; +import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base'; +import { IReadonlyTheme } from '@microsoft/sp-component-base'; + +import * as strings from 'HelloWorldWebPartStrings'; +import HelloWorld from './components/HelloWorld'; +import { IHelloWorldProps } from './components/IHelloWorldProps'; + +export interface IHelloWorldWebPartProps { + description: string; +} + +export default class HelloWorldWebPart extends BaseClientSideWebPart { + + private _isDarkTheme: boolean = false; + private _environmentMessage: string = ''; + + public render(): void { + const element: React.ReactElement = React.createElement( + HelloWorld, + { + description: this.properties.description, + isDarkTheme: this._isDarkTheme, + environmentMessage: this._environmentMessage, + hasTeamsContext: !!this.context.sdks.microsoftTeams, + userDisplayName: this.context.pageContext.user.displayName + } + ); + + ReactDom.render(element, this.domElement); + } + + protected onInit(): Promise { + return this._getEnvironmentMessage().then(message => { + this._environmentMessage = message; + }); + } + + + + private _getEnvironmentMessage(): Promise { + if (!!this.context.sdks.microsoftTeams) { // running in Teams, office.com or Outlook + return this.context.sdks.microsoftTeams.teamsJs.app.getContext() + .then(context => { + let environmentMessage: string = ''; + switch (context.app.host.name) { + case 'Office': // running in Office + environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentOffice : strings.AppOfficeEnvironment; + break; + case 'Outlook': // running in Outlook + environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentOutlook : strings.AppOutlookEnvironment; + break; + case 'Teams': // running in Teams + case 'TeamsModern': + environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentTeams : strings.AppTeamsTabEnvironment; + break; + default: + environmentMessage = strings.UnknownEnvironment; + } + + return environmentMessage; + }); + } + + return Promise.resolve(this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentSharePoint : strings.AppSharePointEnvironment); + } + + protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void { + if (!currentTheme) { + return; + } + + this._isDarkTheme = !!currentTheme.isInverted; + const { + semanticColors + } = currentTheme; + + if (semanticColors) { + this.domElement.style.setProperty('--bodyText', semanticColors.bodyText || null); + this.domElement.style.setProperty('--link', semanticColors.link || null); + this.domElement.style.setProperty('--linkHovered', semanticColors.linkHovered || null); + } + + } + + protected onDispose(): void { + ReactDom.unmountComponentAtNode(this.domElement); + } + + protected get dataVersion(): Version { + return Version.parse('1.0'); + } + + protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration { + return { + pages: [ + { + header: { + description: strings.PropertyPaneDescription + }, + groups: [ + { + groupName: strings.BasicGroupName, + groupFields: [ + PropertyPaneTextField('description', { + label: strings.DescriptionFieldLabel + }) + ] + } + ] + } + ] + }; + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/assets/welcome-dark.png b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/assets/welcome-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..42f0b8d24a9aa964a2be4885fe5700c1c54191a9 GIT binary patch literal 12545 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�Gp~kbx)oU28ew4C2+WtKA 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 G`2PSf1J4)$ literal 0 HcmV?d00001 diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/assets/welcome-light.png b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/assets/welcome-light.png new file mode 100644 index 0000000000000000000000000000000000000000..69eb3b48cd83031f106df4b4df127c749657e319 GIT binary patch literal 12816 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 literal 0 HcmV?d00001 diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/components/HelloWorld.module.scss b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/components/HelloWorld.module.scss new file mode 100644 index 00000000000..9d9cf7feb07 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/components/HelloWorld.module.scss @@ -0,0 +1,34 @@ +@import '~@fluentui/react/dist/sass/References.scss'; + +.helloWorld { + overflow: hidden; + padding: 1em; + color: "[theme:bodyText, default: #323130]"; + color: var(--bodyText); + &.teams { + font-family: $ms-font-family-fallbacks; + } +} + +.welcome { + text-align: center; +} + +.welcomeImage { + width: 100%; + max-width: 420px; +} + +.links { + a { + text-decoration: none; + color: "[theme:link, default:#03787c]"; + color: var(--link); // note: CSS Custom Properties support is limited to modern browsers only + + &:hover { + text-decoration: underline; + color: "[theme:linkHovered, default: #014446]"; + color: var(--linkHovered); // note: CSS Custom Properties support is limited to modern browsers only + } + } +} \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/components/HelloWorld.tsx b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/components/HelloWorld.tsx new file mode 100644 index 00000000000..7900f29251d --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/components/HelloWorld.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; +import styles from './HelloWorld.module.scss'; +import type { IHelloWorldProps } from './IHelloWorldProps'; +import { escape } from '@microsoft/sp-lodash-subset'; + +export default class HelloWorld extends React.Component { + public render(): React.ReactElement { + const { + description, + isDarkTheme, + environmentMessage, + hasTeamsContext, + userDisplayName + } = this.props; + + return ( +
+
+ +

Well done, {escape(userDisplayName)}!

+
{environmentMessage}
+
Web part property value: {escape(description)}
+
+
+

Welcome to SharePoint Framework!

+

+ The SharePoint Framework (SPFx) is a extensibility model for Microsoft Viva, Microsoft Teams and SharePoint. It's the easiest way to extend Microsoft 365 with automatic Single Sign On, automatic hosting and industry standard tooling. +

+

Learn more about SPFx development:

+ +
+
+ ); + } +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/components/IHelloWorldProps.ts b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/components/IHelloWorldProps.ts new file mode 100644 index 00000000000..f7fdb822c61 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/components/IHelloWorldProps.ts @@ -0,0 +1,7 @@ +export interface IHelloWorldProps { + description: string; + isDarkTheme: boolean; + environmentMessage: string; + hasTeamsContext: boolean; + userDisplayName: string; +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/loc/en-us.js b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/loc/en-us.js new file mode 100644 index 00000000000..3b25e74a7d6 --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/loc/en-us.js @@ -0,0 +1,16 @@ +define([], function() { + return { + "PropertyPaneDescription": "Description", + "BasicGroupName": "Group Name", + "DescriptionFieldLabel": "Description Field", + "AppLocalEnvironmentSharePoint": "The app is running on your local environment as SharePoint web part", + "AppLocalEnvironmentTeams": "The app is running on your local environment as Microsoft Teams app", + "AppLocalEnvironmentOffice": "The app is running on your local environment in office.com", + "AppLocalEnvironmentOutlook": "The app is running on your local environment in Outlook", + "AppSharePointEnvironment": "The app is running on SharePoint page", + "AppTeamsTabEnvironment": "The app is running in Microsoft Teams", + "AppOfficeEnvironment": "The app is running in office.com", + "AppOutlookEnvironment": "The app is running in Outlook", + "UnknownEnvironment": "The app is running in an unknown environment" + } +}); \ No newline at end of file diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/loc/mystrings.d.ts b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/loc/mystrings.d.ts new file mode 100644 index 00000000000..21046bcd8bc --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/src/webparts/helloWorld/loc/mystrings.d.ts @@ -0,0 +1,19 @@ +declare interface IHelloWorldWebPartStrings { + PropertyPaneDescription: string; + BasicGroupName: string; + DescriptionFieldLabel: string; + AppLocalEnvironmentSharePoint: string; + AppLocalEnvironmentTeams: string; + AppLocalEnvironmentOffice: string; + AppLocalEnvironmentOutlook: string; + AppSharePointEnvironment: string; + AppTeamsTabEnvironment: string; + AppOfficeEnvironment: string; + AppOutlookEnvironment: string; + UnknownEnvironment: string; +} + +declare module 'HelloWorldWebPartStrings' { + const strings: IHelloWorldWebPartStrings; + export = strings; +} diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/teams/21f4bb8e-5562-4721-8f92-73daad044daf_color.png b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/teams/21f4bb8e-5562-4721-8f92-73daad044daf_color.png new file mode 100644 index 0000000000000000000000000000000000000000..0e1f764fa8df4791a61c71b4f011c26f9083ee52 GIT binary patch literal 10248 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 KtK_^>#Qy@$+e&%> literal 0 HcmV?d00001 diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/teams/21f4bb8e-5562-4721-8f92-73daad044daf_outline.png b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/teams/21f4bb8e-5562-4721-8f92-73daad044daf_outline.png new file mode 100644 index 0000000000000000000000000000000000000000..e8cb4b6ba4f726d47a2e274f16b6069b9a8041cc GIT binary patch literal 249 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz&H|6fVg?3oVGw3ym^DWND9BhG z%|CHbgX^da?eHs6`1kLARhjOac zf3;y1Iq~M{LLS3g_M2bU{+PBvomV=FH7$YTy5I%1<5B$=?>3fqI5P%5iajq7)W9SX p;gpazd1JnvZNlx8HB0WjVJ`J~Q+P@%pA+aZ22WQ%mvv4FO#n^cR9FB2 literal 0 HcmV?d00001 diff --git a/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/tsconfig.json b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/tsconfig.json new file mode 100644 index 00000000000..c4cd392ad1a --- /dev/null +++ b/src/m365/spfx/commands/project/test-projects/spfx-1181rc0-webpart-react/tsconfig.json @@ -0,0 +1,35 @@ +{ + "extends": "./node_modules/@microsoft/rush-stack-compiler-4.7/includes/tsconfig-web.json", + "compilerOptions": { + "target": "es5", + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "jsx": "react", + "declaration": true, + "sourceMap": true, + "experimentalDecorators": true, + "skipLibCheck": true, + "outDir": "lib", + "inlineSources": false, + "noImplicitAny": true, + + "typeRoots": [ + "./node_modules/@types", + "./node_modules/@microsoft" + ], + "types": [ + "webpack-env" + ], + "lib": [ + "es5", + "dom", + "es2015.collection", + "es2015.promise" + ] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx" + ] +} diff --git a/src/m365/spfx/commands/spfx-doctor.ts b/src/m365/spfx/commands/spfx-doctor.ts index e9dbd482676..fec10a03345 100644 --- a/src/m365/spfx/commands/spfx-doctor.ts +++ b/src/m365/spfx/commands/spfx-doctor.ts @@ -550,6 +550,21 @@ class SpfxDoctorCommand extends BaseProjectCommand { range: '^4', fix: 'npm i -g yo@4' } + }, + '1.18.1-rc.0': { + gulpCli: { + range: '^1 || ^2', + fix: 'npm i -g gulp-cli@2' + }, + node: { + range: '>=16.13.0 <17.0.0 || >=18.17.1 <19.0.0', + fix: 'Install Node.js >=16.13.0 <17.0.0 || >=18.17.1 <19.0.0' + }, + sp: SharePointVersion.SPO, + yo: { + range: '^4', + fix: 'npm i -g yo@4' + } } }; From f440e15607ba007c71c6de1b019ae4d5dd861b2c Mon Sep 17 00:00:00 2001 From: Milan Holemans <11723921+milanholemans@users.noreply.github.com> Date: Tue, 31 Oct 2023 23:59:57 +0100 Subject: [PATCH 22/23] Updates release notes --- docs/docs/about/release-notes.mdx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/docs/about/release-notes.mdx b/docs/docs/about/release-notes.mdx index 4d3fb7fcb2b..95aa23a49ef 100644 --- a/docs/docs/about/release-notes.mdx +++ b/docs/docs/about/release-notes.mdx @@ -17,9 +17,12 @@ sidebar_position: 4 ### Changes -- fixes command sorting. [#5606](https://github.com/pnp/cli-microsoft365/issues/5606) -- implements disambiguation prompts in missing places. [#5490](https://github.com/pnp/cli-microsoft365/issues/5490) -- adds a disambiguation prompt to getGroupIdByDisplayName. [#5592](https://github.com/pnp/cli-microsoft365/issues/5592) +- fixes command sorting [#5606](https://github.com/pnp/cli-microsoft365/issues/5606) +- implements disambiguation prompts in missing places [#5490](https://github.com/pnp/cli-microsoft365/issues/5490) +- adds a disambiguation prompt to getGroupIdByDisplayName [#5592](https://github.com/pnp/cli-microsoft365/issues/5592) +- extended 'spfx project upgrade' with support for v1.18.1.-rc.0 [#5593](https://github.com/pnp/cli-microsoft365/issues/5593) +- extended 'spfx doctor' with support for v1.18.1.-rc.0 [#5593](https://github.com/pnp/cli-microsoft365/issues/5593) +- extended 'spfx project doctor' with support for v1.18.1.-rc.0 [#5593](https://github.com/pnp/cli-microsoft365/issues/5593) ## [v7.1.0](https://github.com/pnp/cli-microsoft365/releases/tag/v7.1.0) From d9292ff7c402639a37bd87ee4bde6bbd3ab68f27 Mon Sep 17 00:00:00 2001 From: Milan Holemans <11723921+milanholemans@users.noreply.github.com> Date: Wed, 1 Nov 2023 08:16:00 +0100 Subject: [PATCH 23/23] Fixes 'aad administrativeunit' flags --- .../cmd/aad/administrativeunit/administrativeunit-add.mdx | 8 ++++++-- .../cmd/aad/administrativeunit/administrativeunit-get.mdx | 6 +++++- .../aad/administrativeunit/administrativeunit-list.mdx | 6 +++++- .../aad/administrativeunit/administrativeunit-remove.mdx | 8 ++++++-- .../commands/administrativeunit/administrativeunit-add.ts | 6 +++--- 5 files changed, 25 insertions(+), 9 deletions(-) diff --git a/docs/docs/cmd/aad/administrativeunit/administrativeunit-add.mdx b/docs/docs/cmd/aad/administrativeunit/administrativeunit-add.mdx index 3a87339cda5..cec7e4dea1e 100644 --- a/docs/docs/cmd/aad/administrativeunit/administrativeunit-add.mdx +++ b/docs/docs/cmd/aad/administrativeunit/administrativeunit-add.mdx @@ -21,7 +21,7 @@ m365 aad administrativeunit add [options] `-d, --description [description]` : Description for the administrative unit. -`--hiddenMembership [hiddenMembership]` +`--hiddenMembership` : Indicates whether the administrative unit and its members are hidden. ``` @@ -112,4 +112,8 @@ m365 aad administrativeunit add --displayName 'Marketing Division' --hiddenMembe ``` - \ No newline at end of file + + +## More information + +- Administrative units: https://learn.microsoft.com/entra/identity/role-based-access-control/administrative-units diff --git a/docs/docs/cmd/aad/administrativeunit/administrativeunit-get.mdx b/docs/docs/cmd/aad/administrativeunit/administrativeunit-get.mdx index a1880dc3736..f9377476ce7 100644 --- a/docs/docs/cmd/aad/administrativeunit/administrativeunit-get.mdx +++ b/docs/docs/cmd/aad/administrativeunit/administrativeunit-get.mdx @@ -95,4 +95,8 @@ m365 aad administrativeunit get --displayName 'Marketing Division' ``` - \ No newline at end of file + + +## More information + +- Administrative units: https://learn.microsoft.com/entra/identity/role-based-access-control/administrative-units diff --git a/docs/docs/cmd/aad/administrativeunit/administrativeunit-list.mdx b/docs/docs/cmd/aad/administrativeunit/administrativeunit-list.mdx index fbecf98f286..fae45d6590d 100644 --- a/docs/docs/cmd/aad/administrativeunit/administrativeunit-list.mdx +++ b/docs/docs/cmd/aad/administrativeunit/administrativeunit-list.mdx @@ -80,4 +80,8 @@ m365 aad administrativeunit list ``` - \ No newline at end of file + + +## More information + +- Administrative units: https://learn.microsoft.com/entra/identity/role-based-access-control/administrative-units diff --git a/docs/docs/cmd/aad/administrativeunit/administrativeunit-remove.mdx b/docs/docs/cmd/aad/administrativeunit/administrativeunit-remove.mdx index d40d7ff7bbf..8760edd0417 100644 --- a/docs/docs/cmd/aad/administrativeunit/administrativeunit-remove.mdx +++ b/docs/docs/cmd/aad/administrativeunit/administrativeunit-remove.mdx @@ -19,7 +19,7 @@ m365 aad administrativeunit remove [options] `-n, --displayName [displayName]` : The display name of the administrative unit. Specify either `id` or `displayName` but not both. -`-f, --force [force]` +`-f, --force` : Don't prompt for confirmation. ``` @@ -49,4 +49,8 @@ m365 aad administrativeunit remove --displayName 'Marketing Division' ## Response -The command won't return a response on success \ No newline at end of file +The command won't return a response on success + +## More information + +- Administrative units: https://learn.microsoft.com/entra/identity/role-based-access-control/administrative-units diff --git a/src/m365/aad/commands/administrativeunit/administrativeunit-add.ts b/src/m365/aad/commands/administrativeunit/administrativeunit-add.ts index ee534f37c89..02f64bdd8bf 100644 --- a/src/m365/aad/commands/administrativeunit/administrativeunit-add.ts +++ b/src/m365/aad/commands/administrativeunit/administrativeunit-add.ts @@ -34,7 +34,7 @@ class AadAdministrativeUnitAddCommand extends GraphCommand { #initTelemetry(): void { this.telemetry.push((args: CommandArgs) => { Object.assign(this.telemetryProperties, { - hiddenMembership: args.options.hiddenMembership + hiddenMembership: !!args.options.hiddenMembership }); }); } @@ -48,7 +48,7 @@ class AadAdministrativeUnitAddCommand extends GraphCommand { option: '-d, --description [description]' }, { - option: '--hiddenMembership [hiddenMembership]' + option: '--hiddenMembership' } ); } @@ -57,7 +57,7 @@ class AadAdministrativeUnitAddCommand extends GraphCommand { const requestOptions: CliRequestOptions = { url: `${this.resource}/v1.0/directory/administrativeUnits`, headers: { - 'accept': 'application/json;odata.metadata=none' + accept: 'application/json;odata.metadata=none' }, responseType: 'json', data: {