Merge branch 'main' into dependabot/npm_and_yarn/samples/react-my-dashboard/multi-9423f4c335

This commit is contained in:
Hugo Bernier 2024-09-28 18:35:19 -07:00 committed by GitHub
commit 89e1e34dbb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
165 changed files with 142116 additions and 42116 deletions

View File

@ -14912,6 +14912,124 @@
}
]
},
{
"name": "pnp-sp-dev-spfx-web-parts-react-employees-onboarding",
"source": "pnp",
"title": "Employee Onboarding",
"shortDescription": "This project is an SPFx (SharePoint Framework) application designed for employee onboarding.",
"url": "https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-employees-onboarding",
"downloadUrl": "https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-employees-onboarding",
"longDescription": [
"This project is an SPFx (SharePoint Framework) application designed for employee onboarding. It automates various tasks for each employee, such as updating their department, joining the team, and sending notification emails. The application utilizes the Microsoft Graph SDK's batch requests approach to efficiently manage these operations within a .NET-based Azure function. Additionally, the system logs information into a SharePoint list for auditing purposes."
],
"creationDateTime": "2024-09-01",
"updateDateTime": "2024-09-01",
"products": [
"SharePoint"
],
"metadata": [
{
"key": "CLIENT-SIDE-DEV",
"value": "React"
},
{
"key": "SPFX-VERSION",
"value": "1.19.0"
}
],
"thumbnails": [
{
"name": "demo1.png",
"type": "image",
"order": 100,
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-employees-onboarding/assets/D:\\GitHub\\pnp\\sp-dev-fx-webparts\\samples\\react-employees-onboarding\\assets\\demo1.png",
"alt": "Web Part Preview"
},
{
"name": "demo2.png",
"type": "image",
"order": 101,
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-employees-onboarding/assets/D:\\GitHub\\pnp\\sp-dev-fx-webparts\\samples\\react-employees-onboarding\\assets\\demo2.png",
"alt": "Web Part Preview"
},
{
"name": "demo3.png",
"type": "image",
"order": 102,
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-employees-onboarding/assets/D:\\GitHub\\pnp\\sp-dev-fx-webparts\\samples\\react-employees-onboarding\\assets\\demo3.png",
"alt": "Web Part Preview"
},
{
"name": "demo4.png",
"type": "image",
"order": 103,
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-employees-onboarding/assets/D:\\GitHub\\pnp\\sp-dev-fx-webparts\\samples\\react-employees-onboarding\\assets\\demo4.png",
"alt": "Web Part Preview"
}
],
"authors": [
{
"gitHubAccount": "ejazhussain",
"pictureUrl": "https://github.com/ejazhussain.png",
"name": "Ejaz Hussain"
}
],
"references": [
{
"name": "Build your first SharePoint client-side web part",
"description": "Client-side web parts are client-side components that run in the context of a SharePoint page. Client-side web parts can be deployed to SharePoint environments that support the SharePoint Framework. You can also use modern JavaScript web frameworks, tools, and libraries to build them.",
"url": "https://docs.microsoft.com/sharepoint/dev/spfx/web-parts/get-started/build-a-hello-world-web-part"
}
]
},
{
"name": "pnp-sp-dev-spfx-web-parts-react-enhanced-button",
"source": "pnp",
"title": "Enhanced Button",
"shortDescription": "Extends the functionality of the native button web part.",
"url": "https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-enhanced-button",
"downloadUrl": "https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-enhanced-button",
"longDescription": [
"The Enhanced Button Web Part is a custom SharePoint web part that extends the functionality of the native button web part. It provides additional configuration options to create more customizable and flexible buttons within your SharePoint pages."
],
"creationDateTime": "2024-09-01",
"updateDateTime": "2024-09-01",
"products": [
"SharePoint"
],
"metadata": [
{
"key": "CLIENT-SIDE-DEV",
"value": "React"
},
{
"key": "SPFX-VERSION",
"value": "1.19.0"
}
],
"thumbnails": [
{
"type": "image",
"order": 100,
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-enhanced-button/assets/app.jpeg",
"alt": "Web Part Preview"
}
],
"authors": [
{
"gitHubAccount": "AriGunawan",
"pictureUrl": "https://github.com/AriGunawan.png",
"name": "Ari Gunawan"
}
],
"references": [
{
"name": "Build your first SharePoint client-side web part",
"description": "Client-side web parts are client-side components that run in the context of a SharePoint page. Client-side web parts can be deployed to SharePoint environments that support the SharePoint Framework. You can also use modern JavaScript web frameworks, tools, and libraries to build them.",
"url": "https://docs.microsoft.com/sharepoint/dev/spfx/web-parts/get-started/build-a-hello-world-web-part"
}
]
},
{
"name": "pnp-sp-dev-spfx-web-parts-react-enhanced-list-formatting",
"source": "pnp",
@ -17092,7 +17210,7 @@
"This is a sample web part developed using React Framework to gather events from the underlying group calendar of a Team site. This sample also demonstrates the utilization of web parts as Teams tabs and Personal tab and offering a visualization context to change behaviors based on the platform used (Getting the proper information from the team vs. SharePoint site, understanding the context of the theme on Teams, etc.)."
],
"creationDateTime": "2020-11-06",
"updateDateTime": "2020-11-06",
"updateDateTime": "2024-09-12",
"products": [
"SharePoint"
],
@ -17103,7 +17221,7 @@
},
{
"key": "SPFX-VERSION",
"value": "1.10.0"
"value": "1.19.0"
},
{
"key": "SPFX-TEAMSTAB",
@ -17137,14 +17255,12 @@
"authors": [
{
"gitHubAccount": "sebastienlevert",
"company": "Microsoft",
"pictureUrl": "https://github.com/sebastienlevert.png",
"name": "Sébastien Levert",
"twitter": "sebastienlevert"
},
{
"gitHubAccount": "Abderahman88",
"company": "",
"pictureUrl": "https://avatars.githubusercontent.com/u/36161889?s=460&u=afdd5f6681bc375ee3811482dec79824c12d8170&v=4",
"name": "Abderahman Moujahid"
}
@ -19642,7 +19758,7 @@
"Service Health for Microsoft 365 solution show the health status for all the M365 services"
],
"creationDateTime": "2023-02-03",
"updateDateTime": "2024-02-10",
"updateDateTime": "2024-09-05",
"products": [
"SharePoint"
],
@ -19653,7 +19769,7 @@
},
{
"key": "SPFX-VERSION",
"value": "1.16.1"
"value": "1.19.0"
}
],
"thumbnails": [
@ -29300,6 +29416,55 @@
}
]
},
{
"name": "pnp-sp-dev-spfx-web-parts-react-training-checklist",
"source": "pnp",
"title": "Training Checklist",
"shortDescription": "YSample web part to display a training checklist in a SharePoint page.",
"url": "https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-training-checklist",
"downloadUrl": "https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-training-checklist",
"longDescription": [
"YSample web part to display a training checklist in a SharePoint page."
],
"creationDateTime": "2024-09-12",
"updateDateTime": "2024-09-12",
"products": [
"SharePoint"
],
"metadata": [
{
"key": "CLIENT-SIDE-DEV",
"value": "React"
},
{
"key": "SPFX-VERSION",
"value": "1.20.0"
}
],
"thumbnails": [
{
"name": "trainingchecklist.png",
"type": "image",
"order": 100,
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-training-checklist/assets/trainingchecklist.png",
"alt": "Web Part Preview"
}
],
"authors": [
{
"gitHubAccount": "ValerasNarbutas",
"pictureUrl": "https://github.com/ValerasNarbutas.png",
"name": "Valeras Narbutas"
}
],
"references": [
{
"name": "Build your first SharePoint client-side web part",
"description": "Client-side web parts are client-side components that run in the context of a SharePoint page. Client-side web parts can be deployed to SharePoint environments that support the SharePoint Framework. You can also use modern JavaScript web frameworks, tools, and libraries to build them.",
"url": "https://docs.microsoft.com/sharepoint/dev/spfx/web-parts/get-started/build-a-hello-world-web-part"
}
]
},
{
"name": "pnp-sp-dev-spfx-web-parts-react-tree-orgchart",
"source": "pnp",

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,38 @@
{
"name": "SPFx 1.19.0",
"image": "docker.io/m365pnp/spfx:1.19.0",
"customizations": {
"vscode": {
"extensions": [
"editorconfig.editorconfig",
"dbaeumer.vscode-eslint"
]
}
},
"forwardPorts": [
4321,
35729,
5432
],
"portsAttributes": {
"4321": {
"protocol": "https",
"label": "Manifest",
"onAutoForward": "silent",
"requireLocalPort": true
},
"5432": {
"protocol": "https",
"label": "Workbench",
"onAutoForward": "silent"
},
"35729": {
"protocol": "https",
"label": "LiveReload",
"onAutoForward": "silent",
"requireLocalPort": true
}
},
"postCreateCommand": "bash .devcontainer/spfx-startup.sh",
"remoteUser": "node"
}

View File

@ -7,9 +7,11 @@ echo
echo -e "\e[1;94mGenerating dev certificate\e[0m"
gulp trust-dev-cert
# Convert the generated PEM certificate to a CER certificate
openssl x509 -inform PEM -in ~/.rushstack/rushstack-serve.pem -outform DER -out ./spfx-dev-cert.cer
cp ~/.gcb-serve-data/gcb-serve.cer ./spfx-dev-cert.cer
cp ~/.gcb-serve-data/gcb-serve.cer ./spfx-dev-cert.pem
# Copy the PEM ecrtificate for non-Windows hosts
cp ~/.rushstack/rushstack-serve.pem ./spfx-dev-cert.pem
## add *.cer to .gitignore to prevent certificates from being saved in repo
if ! grep -Fxq '*.cer' ./.gitignore

View File

@ -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<k,V>".
'@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: {}
}
]
};

View File

@ -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

View File

@ -0,0 +1,16 @@
!dist
config
gulpfile.js
release
src
temp
tsconfig.json
tslint.json
*.log
.yo-rc.json
.vscode

View File

@ -0,0 +1,21 @@
{
"@microsoft/generator-sharepoint": {
"plusBeta": false,
"isCreatingSolution": true,
"nodeVersion": "18.17.1",
"sdksVersions": {
"@microsoft/microsoft-graph-client": "3.0.2",
"@microsoft/teams-js": "2.12.0"
},
"version": "1.19.0",
"libraryName": "on-boarding",
"libraryId": "2f1e9f0d-4c50-49fa-90c3-ed2a73555b89",
"environment": "spo",
"packageManager": "npm",
"solutionName": "OnBoarding",
"solutionShortDescription": "OnBoarding description",
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,239 @@
# Employee Onboarding
## Summary
This project is an SPFx (SharePoint Framework) application designed for employee onboarding. It automates various tasks for each employee, such as updating their department, joining the team, and sending notification emails. The application utilizes the Microsoft Graph SDK's batch requests approach to efficiently manage these operations within a .NET-based Azure function. Additionally, the system logs information into a SharePoint list for auditing purposes.
For more information on batch requests with Microsoft Graph SDK, refer to the [official documentation](https://learn.microsoft.com/graph/sdks/batch-requests?tabs=csharp).
## Demo
* Users can import a CSV file containing user information.
![](./assets/demo1.png)
* List of users to be processed
![](./assets/demo2.png)
* Completion of onboarding tasks
![](./assets/demo3.png)
* Summary
![](./assets/demo4.png)
## Compatibility
| :warning: Important |
|:---------------------------|
| Every SPFx version is optimally compatible with specific versions of Node.js. In order to be able to build this sample, you need to ensure that the version of Node on your workstation matches one of the versions listed in this section. This sample will not work on a different version of Node.|
|Refer to <https://aka.ms/spfx-matrix> for more information on SPFx compatibility. |
This sample is optimally compatible with the following environment configuration:
![SPFx 1.19.0](https://img.shields.io/badge/SPFx-1.19.0-green.svg)
![Node.js v18](https://img.shields.io/badge/Node.js-v18-green.svg)
![Compatible with SharePoint Online](https://img.shields.io/badge/SharePoint%20Online-Compatible-green.svg)
![Does not work with SharePoint 2019](https://img.shields.io/badge/SharePoint%20Server%202019-Incompatible-red.svg "SharePoint Server 2019 requires SPFx 1.4.1 or lower")
![Does not work with SharePoint 2016 (Feature Pack 2)](https://img.shields.io/badge/SharePoint%20Server%202016%20(Feature%20Pack%202)-Incompatible-red.svg "SharePoint Server 2016 Feature Pack 2 requires SPFx 1.1")
![Local Workbench Unsupported](https://img.shields.io/badge/Local%20Workbench-Unsupported-red.svg "Local workbench is no longer available as of SPFx 1.13 and above")
![Hosted Workbench Compatible](https://img.shields.io/badge/Hosted%20Workbench-Compatible-green.svg)
![Compatible with Remote Containers](https://img.shields.io/badge/Remote%20Containers-Compatible-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)
>
## Contributors
* [Ejaz Hussain](https://github.com/ejazhussain)
## Version history
| Version | Date | Comments |
| ------- | --------------- | --------------- |
| 1.0 | September 01, 2024 | Initial release |
## Prerequisites
### NodeJS - v18.17.1
### Setting up backend
### 1. Create and configure the Azure AD application
* Run the following command to Create an Azure AD app
```PowerShell
$app = Register-PnPAzureADApp -ApplicationName "sp-graph-auth" -Tenant contoso.onmicrosoft.com -OutPath c:\temp -CertificatePassword (ConvertTo-SecureString -String "password" -AsPlainText -Force) -Store CurrentUser -Interactive
```
* Keep note of the EncodedBased64String and thumbprint value of the certificate
* Upload the generate certificate to Azure AD app
* Generate a client secret and keep note of this value
* Configured permissions as below
| API / Permissions name | Type | Description |
|-----------------------------------|--------------|----------------------------------------------------------|
| **Microsoft Graph (4)** | | |
| Mail.Send | Application | Send mail as any user |
| Sites.FullControl.All | Application | Have full control of all site collections |
| TeamMember.ReadWrite.All | Application | Add and remove members from all teams |
| User.ReadWrite.All | Application | Read and write all users' full profiles |
| **SharePoint (3)** | | |
| Sites.FullControl.All | Application | Have full control of all site collections |
| Sites.ReadWrite.All | Application | Read and write items in all site collections |
### 2. Deploy Azure function app
1. Create an Azure function app. [Creating Azure Function App](https://learn.microsoft.com/en-us/azure/azure-functions/functions-create-function-app-portal?pivots=programming-language-csharp)
2. Navigate to `/api/O365C.FuncApp.Induction` folder
3. Run the following commands to publish the azure function app to azure
```ps
#publish the code
dotnet publish -c Release
$publishFolder = "O365C.FuncApp.Induction/bin/Release/net8.0/publish"
```
```ps
# create the zip
$publishZip = "publish.zip"
if(Test-path $publishZip) {Remove-item $publishZip}
Add-Type -assembly "system.io.compression.filesystem"
[io.compression.zipfile]::CreateFromDirectory($publishFolder, $publishZip)
```
```ps
# deploy the zipped package
az functionapp deployment source config-zip `
-g $resourceGroup -n $functionAppName --src $publishZip
```
### 3. Configure an Azure function app
1. **Open Function App:** Locate and select your Function App from the "Function Apps" section.
2. **Access Configuration:** Navigate to the "Configuration" section under "Settings".
3. **Add Application Settings:** Use the "+ New application setting" button to add new environment variables.
```JSON
[
{
"name": "Base64EncodedCert",
"value": "Certificate base64 encoded value",
},
{
"name": "CertificateThumbprint",
"value": "Certificate thumbprint",
},
{
"name": "ClientId",
"value": "xxxxxx-xxxxx-4b47-b143-db04e3b5586f",
},
{
"name": "ClientSecret",
"value": "xxxxxx-xxxxx-4b47-b143-db04e3b5586f",
},
{
"name": "TenantId",
"value": "xxxx-xxxxx-xxx-8304-0f0f2f840b5d",
},
{
"name": "SiteUrl",
"value": "https://mytenant.sharepoint.com/sites/dev"
}
]
```
5. **Save Changes:** After adding your variables, save the changes.
6. **Restart Function App:** Optionally, restart your Function App to ensure the new settings are applied.
### 4. Get function app endpoint
Retrieve the function URL for the `Onboarding` function from the previously deployed function app and save it for later use in the web part properties.
### 5. Creating SharePoint list (Onboarding)
Create the SharePoint list called `Onboarding` with the following columns
| Column | Type |
|------------------------|--------------------|
| Title | Single line of text |
| Email | Single line of text |
| Department | Yes/No |
| Team Membership | Yes/No |
| Notification | Yes/No |
| Completed On | Date and Time |
| Processed On | Date and Time |
## Minimal Path to Awesome
* Clone this repository
* Ensure that you are at the solution folder
* in the command-line run:
* `npm install`
* `npm run serve`
* Add the web part to the page and configure the following properties
* Title
* Description
* ListUrl
* Azure Function Url
## Features
1. **CSV Import**:
* Users can import a CSV file containing user information for onboarding.
* The application parses the CSV file and displays the list of users to be processed.
2. **Automated Onboarding Tasks**:
* Updates the department information for each employee.
* Adds employees to the appropriate teams.
* Sends notification emails to the employees.
3. **Batch Processing with Microsoft Graph SDK**:
* Utilizes the Microsoft Graph SDK's batch requests approach to efficiently manage multiple operations.
* Ensures efficient and scalable processing of onboarding tasks.
4. **Azure Function Integration**:
* Offloads processing to a .NET-based Azure function for better performance and scalability.
* Logs information into a SharePoint list for auditing purposes.
5. **Progress Tracking**:
* Displays the progress of onboarding tasks.
* Provides a summary of completed tasks.
## Help
We do not support samples, but this community is always willing to help, and we want to improve these samples. We use GitHub to track issues, which makes it easy for community members to volunteer their time and help resolve issues.
If you're having issues building the solution, please run [spfx doctor](https://pnp.github.io/cli-microsoft365/cmd/spfx/spfx-doctor/) from within the solution folder to diagnose incompatibility issues with your environment.
You can try looking at [issues related to this sample](https://github.com/pnp/sp-dev-fx-webparts/issues?q=label%3A%22sample%3A%20react-employees-onboarding%22) to see if anybody else is having the same issues.
You can also try looking at [discussions related to this sample](https://github.com/pnp/sp-dev-fx-webparts/discussions?discussions_q=react-employees-onboarding) and see what the community is saying.
If you encounter any issues using this sample, [create a new issue](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Abug-suspected%2Csample%3A%20react-employees-onboarding&template=bug-report.yml&sample=react-employees-onboarding&authors=@ejazhussain&title=react-employees-onboarding%20-%20).
For questions regarding this sample, [create a new question](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Aquestion%2Csample%3A%20react-employees-onboarding&template=question.yml&sample=react-employees-onboarding&authors=@ejazhussain&title=react-employees-onboarding%20-%20).
Finally, if you have an idea for improvement, [make a suggestion](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Aenhancement%2Csample%3A%20react-employees-onboarding&template=suggestion.yml&sample=react-employees-onboarding&authors=@ejazhussain&title=react-employees-onboarding%20-%20).
## 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.**
<img src="https://m365-visitor-stats.azurewebsites.net/sp-dev-fx-webparts/samples/react-employees-onboarding" />

View File

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.11.35222.181
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "O365C.FuncApp.Induction", "O365C.FuncApp.Induction\O365C.FuncApp.Induction.csproj", "{67E5EDBB-0B30-40AD-8F02-BFEF23604835}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{67E5EDBB-0B30-40AD-8F02-BFEF23604835}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{67E5EDBB-0B30-40AD-8F02-BFEF23604835}.Debug|Any CPU.Build.0 = Debug|Any CPU
{67E5EDBB-0B30-40AD-8F02-BFEF23604835}.Release|Any CPU.ActiveCfg = Release|Any CPU
{67E5EDBB-0B30-40AD-8F02-BFEF23604835}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3EBE7D36-76DF-4F13-8EE9-595DBB9DF115}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,264 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# Azure Functions localsettings file
#local.settings.json
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# DNX
project.lock.json
project.fragment.lock.json
artifacts/
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
#*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# NuGet v3's project.json files produces more ignoreable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
node_modules/
orleans.codegen.cs
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# CodeRush
.cr/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc

View File

@ -0,0 +1,60 @@
using Azure.Identity;
using Microsoft.Graph;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace O365C.FuncApp.Induction.Helpers
{
public class GraphAuthenticationManager
{
private static GraphServiceClient? _appGraphClient;
public static GraphServiceClient GetAuthenticatedGraphClient(AzureFunctionSettings config)
{
try
{
if (_appGraphClient == null)
{
// The client credentials flow requires that you request the
// /.default scope, and pre-configure your permissions on the
// app registration in Azure. An administrator must grant consent
// to those permissions beforehand.
var scopes = new[] { "https://graph.microsoft.com/.default" };
// Values from app registration
var clientId = config.ClientId;
var tenantId = config.TenantId;
var clientSecret = config.ClientSecret;
// using Azure.Identity;
var options = new ClientSecretCredentialOptions
{
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud,
};
// https://learn.microsoft.com/dotnet/api/azure.identity.clientsecretcredential
var clientSecretCredential = new ClientSecretCredential(
tenantId, clientId, clientSecret, options);
_appGraphClient = new GraphServiceClient(clientSecretCredential);
return _appGraphClient;
}
else
{
return _appGraphClient;
}
}
catch (Exception)
{
throw;
}
}
}
}

View File

@ -0,0 +1,14 @@
using System.Security.Cryptography.X509Certificates;
namespace O365C.FuncApp.Induction
{
public class AzureFunctionSettings
{
public string TenantId { get; set; }
public string ClientId { get; set; }
public string ClientSecret { get; set; }
public string SiteUrl { get; set; }
public string Base64EncodedCert { get; set; }
public string CertificateThumbprint { get; set; }
}
}

View File

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace O365C.FuncApp.Induction.Models
{
public class LogInfo
{
public string Name { get; set; } = string.Empty;
public string Email { get; set; }
public bool Department { get; set; }
public bool TeamMembership { get; set; }
public bool Notification { get; set; }
public DateTime ProcessedOn { get; set; }
public DateTime CompletedOn { get; set; }
}
}

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace O365C.FuncApp.Induction.Models
{
public class RequestDetail
{
public string Name { get; set; }
public string Email { get; set; }
public string Department { get; set; }
}
}

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace O365C.FuncApp.OnBoarding.Models
{
public class UserDetail
{
public string Id { get; set; }
public string DisplayName { get; set; }
public string GivenName { get; set; }
public string Surname { get; set; }
public string UserPrincipalName { get; set; }
public string JobTitle { get; set; }
public string Department { get; set; }
public string MobilePhone { get; set; }
public string OfficeLocation { get; set; }
public string Mail { get; set; }
}
}

View File

@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Azure.Identity" Version="1.12.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.21.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.1.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="1.2.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.17.0" />
<PackageReference Include="Microsoft.ApplicationInsights.WorkerService" Version="2.22.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="1.2.0" />
<PackageReference Include="Microsoft.Graph" Version="5.56.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="PnP.Core" Version="1.14.0" />
<PackageReference Include="PnP.Core.Auth" Version="1.14.0" />
</ItemGroup>
<ItemGroup>
<None Update="host.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="local.settings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
</ItemGroup>
<ItemGroup>
<Using Include="System.Threading.ExecutionContext" Alias="ExecutionContext" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,60 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using O365C.FuncApp.Induction.Models;
using O365C.FuncApp.Induction.Services;
using O365C.FuncApp.OnBoarding.Models;
namespace O365C.FuncApp.Induction
{
public class Onborading
{
private readonly ILogger<Onborading> _logger;
private readonly AzureFunctionSettings _azureFunctionSettings;
private readonly ISharePointService _sharePointService;
private readonly IGraphService _graphService;
public Onborading(ILogger<Onborading> logger, AzureFunctionSettings azureFunctionSettings, ISharePointService sharePointService, IGraphService graphService)
{
_logger = logger;
_azureFunctionSettings = azureFunctionSettings;
_sharePointService = sharePointService;
_graphService = graphService;
}
[Function("Onborading")]
public async Task<IActionResult> RunAsync([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequest req)
{
_logger.LogInformation("C# HTTP trigger function processed a request.");
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
if (string.IsNullOrEmpty(requestBody))
{
return new BadRequestObjectResult("Please pass a valid request body");
}
try
{
List<RequestDetail> requestDetail = JsonConvert.DeserializeObject<List<RequestDetail>>(requestBody);
if(requestDetail == null)
{
return new BadRequestObjectResult("Invalid JSON in request body");
}
var result = await _graphService.UserOnboarding(requestDetail);
return new OkObjectResult(result);
}
catch (JsonReaderException)
{
return new BadRequestObjectResult("Invalid JSON in request body");
}
catch (Exception ex)
{
// Log the exception here
return new StatusCodeResult(StatusCodes.Status500InternalServerError);
}
//return new OkObjectResult("Welcome to Azure Functions!");
}
}
}

View File

@ -0,0 +1,103 @@
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using O365C.FuncApp.Induction;
using O365C.FuncApp.Induction.Services;
using PnP.Core.Auth.Services.Builder.Configuration;
using PnP.Core.Services.Builder.Configuration;
using System.Security.Cryptography.X509Certificates;
AzureFunctionSettings azureFunctionSettings = null;
var host = new HostBuilder()
.ConfigureFunctionsWebApplication()
.ConfigureServices((context, services) =>
{
services.AddApplicationInsightsTelemetryWorkerService();
services.ConfigureFunctionsApplicationInsights();
// Add our global configuration instance
services.AddSingleton(options =>
{
var configuration = context.Configuration;
azureFunctionSettings = new AzureFunctionSettings();
configuration.Bind(azureFunctionSettings);
return configuration;
});
// Add our configuration class
services.AddSingleton(options => { return azureFunctionSettings; });
// Add and configure PnP Core SDK
services.AddPnPCore(options =>
{
// Add the base site url
options.Sites.Add("Default", new PnPCoreSiteOptions
{
SiteUrl = azureFunctionSettings.SiteUrl
});
});
services.AddPnPCoreAuthentication(options =>
{
// Load the certificate to use
X509Certificate2 cert = LoadCertificate(azureFunctionSettings);
// Configure certificate based auth
options.Credentials.Configurations.Add("CertAuth", new PnPCoreAuthenticationCredentialConfigurationOptions
{
ClientId = azureFunctionSettings.ClientId,
TenantId = azureFunctionSettings.TenantId,
X509Certificate = new PnPCoreAuthenticationX509CertificateOptions
{
Certificate = cert,
}
});
// Connect this auth method to the configured site
options.Sites.Add("Default", new PnPCoreAuthenticationSiteOptions
{
AuthenticationProviderName = "CertAuth",
});
options.Credentials.DefaultConfiguration = "CertAuth";
});
// Add services
services.AddSingleton<IGraphService, GraphService>();
services.AddSingleton<ITokenService, TokenService>();
services.AddSingleton<ISharePointService, SharePointService>();
})
.Build();
host.Run();
X509Certificate2 LoadCertificate(AzureFunctionSettings? azureFunctionSettings)
{
// Will only be populated correctly when running in the Azure Function host
string certBase64Encoded = Environment.GetEnvironmentVariable("Base64EncodedCert")?.ToString() ?? string.Empty;
if (!string.IsNullOrEmpty(certBase64Encoded))
{
// Azure Function flow
return new X509Certificate2(Convert.FromBase64String(certBase64Encoded),
"pBGO8A1tQLBdPsI",
X509KeyStorageFlags.Exportable |
X509KeyStorageFlags.MachineKeySet |
X509KeyStorageFlags.EphemeralKeySet);
}
else
{
// Local flow
var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
var thumbprint = azureFunctionSettings?.CertificateThumbprint ?? "defaultThumbprint";
var certificateCollection = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false);
store.Close();
return certificateCollection.First();
}
}

View File

@ -0,0 +1,9 @@
{
"profiles": {
"O365C.FuncApp.Induction": {
"commandName": "Project",
"commandLineArgs": "--port 7032",
"launchBrowser": false
}
}
}

View File

@ -0,0 +1,11 @@
{
"dependencies": {
"appInsights1": {
"type": "appInsights"
},
"storage1": {
"type": "storage",
"connectionId": "AzureWebJobsStorage"
}
}
}

View File

@ -0,0 +1,11 @@
{
"dependencies": {
"appInsights1": {
"type": "appInsights.sdk"
},
"storage1": {
"type": "storage.emulator",
"connectionId": "AzureWebJobsStorage"
}
}
}

View File

@ -0,0 +1,270 @@

using Microsoft.Extensions.Logging;
using Microsoft.Graph;
using Microsoft.Graph.Models;
using Microsoft.Graph.Users.Item.SendMail;
using O365C.FuncApp.Induction.Helpers;
using O365C.FuncApp.OnBoarding.Models;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using O365C.FuncApp.Induction.Models;
using O365C.FuncApp.Induction.Services;
using O365C.FuncApp.Induction;
namespace O365C.FuncApp.Induction.Services
{
public interface IGraphService
{
Task<UserDetail> GetUserInfo(string email);
Task<List<LogInfo>> UserOnboarding(List<RequestDetail> requestDetails);
}
public class GraphService : IGraphService
{
private readonly ISharePointService _sharePointService;
private readonly AzureFunctionSettings _azureFunctionSettings;
private readonly ILogger<GraphService> _logger;
public GraphService(ILogger<GraphService> logger, AzureFunctionSettings azureFunctionSettings, ISharePointService sharePointService)
{
_azureFunctionSettings = azureFunctionSettings;
_logger = logger;
_sharePointService = sharePointService;
}
public async Task<UserDetail> GetUserInfo(string email)
{
try
{
var graphClient = GraphAuthenticationManager.GetAuthenticatedGraphClient(_azureFunctionSettings);
// Get the user by email
var user = await graphClient.Users[email]
.GetAsync(requestConfiguration =>
{
requestConfiguration.QueryParameters.Select = new[] { "id", "displayName", "givenName", "surname", "userPrincipalName", "jobTitle", "department", "mobilePhone", "officeLocation", "mail" };
});
// Create a new user detail object
var result = new UserDetail
{
Id = user.Id,
DisplayName = user.DisplayName,
GivenName = user.GivenName,
Surname = user.Surname,
UserPrincipalName = user.UserPrincipalName,
JobTitle = user.JobTitle,
Department = user.Department,
MobilePhone = user.MobilePhone,
OfficeLocation = user.OfficeLocation,
Mail = user.Mail
};
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting user information");
return null;
}
}
public async Task<List<LogInfo>> UserOnboarding(List<RequestDetail> requestDetails)
{
try
{
List<LogInfo> result = new List<LogInfo>();
var graphClient = GraphAuthenticationManager.GetAuthenticatedGraphClient(_azureFunctionSettings);
// Loop through the request details
foreach (var requestDetail in requestDetails)
{
_logger.LogInformation($"Processing {requestDetail.Name} ...............");
_logger.LogInformation($"************************************************************************************");
var email = requestDetail.Email;
var department = requestDetail.Department;
var name = requestDetail.Name;
var logInfo = new LogInfo();
logInfo.Name = name;
logInfo.Email = email;
var user = await GetUserInfo(email);
//if user is not found, skip to the next user
if (user == null)
{
Console.WriteLine($"User {email} not found");
continue;
}
#region Update User Department
var userToUpdate = new User
{
Department = department
};
var updateDeptRequest = graphClient.Users[email].ToPatchRequestInformation(userToUpdate);
#endregion
#region Assign User to Department Team
var devTeamId = "a82e7c4b-e9f1-440f-ba87-5ba9a992ef15";
var membershipRequestBody = new AadUserConversationMember
{
OdataType = "#microsoft.graph.aadUserConversationMember",
Roles = new List<string>
{
"member",
},
AdditionalData = new Dictionary<string, object>
{
{
"user@odata.bind", $"https://graph.microsoft.com/v1.0/users('{user.Id}')"
},
},
};
var teamMembershipRequest = graphClient.Teams[devTeamId].Members.ToPostRequestInformation(membershipRequestBody);
#endregion
#region Send Email to User
var emailRequestBody = new SendMailPostRequestBody
{
Message = new Message
{
Subject = "Onboarding completion",
Body = new ItemBody
{
ContentType = BodyType.Html,
Content = $@"
<h1>Welcome to the {user.Department} team</h1>
<p>You have been successfully onboarded to the team</p>
<h3>Completed tasks</h3>
<ul style='font-family: Arial, sans-serif;'>
<li>Department has been updated</li>
<li>Joined the Development Team Microsoft Team - see below link</li>
<li>Notification sent to the user</li>
</ul>
<table style='border-collapse: collapse; width: 100%; font-family: Arial, sans-serif;'>
<thead style='background-color: #f2f2f2;'>
<tr>
<th style='border: 1px solid #ddd; padding: 8px; text-align: left;'>Name</th>
<th style='border: 1px solid #ddd; padding: 8px; text-align: left;'>Department</th>
<th style='border: 1px solid #ddd; padding: 8px; text-align: left;'>Joined Team</th>
</tr>
</thead>
<tbody>
<tr>
<td style='border: 1px solid #ddd; padding: 8px;'>{user.DisplayName}</td>
<td style='border: 1px solid #ddd; padding: 8px;'>{user.Department}</td>
<td style='border: 1px solid #ddd; padding: 8px;'><a href='https://teams.microsoft.com/l/team/19%3a9J2Sn_khW6HgW6fIv1kTcvbGrGndWMKt0MHDZoqIwtw1%40thread.tacv2/conversations?groupId=a82e7c4b-e9f1-440f-ba87-5ba9a992ef15&tenantId=3f4d536c-9ebc-4eb1-8304-0f0f2f840b5d'>Dev Team</a></td>
</tr>
</tbody>
</table>
",
},
ToRecipients = new List<Recipient>
{
new Recipient
{
EmailAddress = new EmailAddress
{
Address = user.Mail.ToString(),
},
},
},
},
SaveToSentItems = false,
};
var sendEmailRequest = graphClient.Users[user.Id].SendMail.ToPostRequestInformation(emailRequestBody);
#endregion
#region Building batch request
// Build the batch
var batchRequestContent = new BatchRequestContentCollection(graphClient);
// Using AddBatchRequestStepAsync adds each request as a step
// with no specified order of execution
var updateDeptStep = await batchRequestContent.AddBatchRequestStepAsync(updateDeptRequest);
var teamMembershipStep = await batchRequestContent.AddBatchRequestStepAsync(teamMembershipRequest);
var sendEmailStep = await batchRequestContent.AddBatchRequestStepAsync(sendEmailRequest);
// Execute the batch
var returnedResponse = await graphClient.Batch.PostAsync(batchRequestContent);
#endregion
// De-serialize response based on known return type
try
{
await returnedResponse.GetResponseByIdAsync(updateDeptStep);
Console.WriteLine($"User department updated to {department}");
logInfo.Department = true;
//Add wait time for 2 seconds
await Task.Delay(2000);
}
catch (Exception ex)
{
Console.WriteLine($"Get user failed: {ex.Message}");
logInfo.Department = false;
}
try
{
await returnedResponse.GetResponseByIdAsync(teamMembershipStep);
Console.WriteLine($"User added to team");
logInfo.TeamMembership = true;
await Task.Delay(2000);
}
catch (Exception ex)
{
Console.WriteLine($"Get team membership failed: {ex.Message}");
logInfo.TeamMembership = false;
}
try
{
await returnedResponse.GetResponseByIdAsync(sendEmailStep);
Console.WriteLine($"Email sent to user");
logInfo.Notification = true;
await Task.Delay(2000);
}
catch (Exception ex)
{
Console.WriteLine($"Send email failed: {ex.Message}");
logInfo.Notification = false;
}
//Log item to SharePoint list
if(logInfo.Department && logInfo.TeamMembership && logInfo.Notification)
{
logInfo.CompletedOn = DateTime.Now;
}
logInfo.ProcessedOn = DateTime.Now;
_logger.LogInformation("AddItemsToList to SharePoint List");
await _sharePointService.AddListItemAsync(logInfo);
_logger.LogInformation("Done");
result.Add(logInfo);
}
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error Onboarding the user");
return null;
}
}
}
}

View File

@ -0,0 +1,111 @@
using Google.Protobuf.Collections;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using O365C.FuncApp.Induction;
using O365C.FuncApp.Induction.Models;
using PnP.Core.Model;
using PnP.Core.Model.SharePoint;
using PnP.Core.QueryModel;
using PnP.Core.Services;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace O365C.FuncApp.Induction.Services
{
public interface ISharePointService
{
Task AddListItemAsync(LogInfo logInfo);
}
public class SharePointService : ISharePointService
{
private readonly AzureFunctionSettings _azureFunctionSettings;
private readonly IPnPContextFactory _pnpContextFactory;
private readonly ILogger logger;
public SharePointService(
IPnPContextFactory pnpContextFactory,
ILoggerFactory loggerFactory,
AzureFunctionSettings azureFunctionSettings
)
{
logger = loggerFactory.CreateLogger<SharePointService>();
_pnpContextFactory = pnpContextFactory;
_azureFunctionSettings = azureFunctionSettings;
}
public async Task AddListItemAsync(LogInfo logInfo)
{
try
{
using (var context = await _pnpContextFactory.CreateAsync(new Uri(_azureFunctionSettings.SiteUrl)))
{
// Get the list
var list = context.Web.Lists.GetByTitle("Onboarding");
//// Check if the item already exists (use the caml query ) if so update it else create a new one.
//StringBuilder camlQuery = new StringBuilder();
//camlQuery.Append("<View>");
//camlQuery.Append("<Query><Where>");
//camlQuery.Append("<Eq><FieldRef Name='Email' /><Value Type='Text'>" + logInfo.Email + "</Value></Eq>");
//camlQuery.Append("</Where></Query>");
//camlQuery.Append("<RowLimit>1</RowLimit>");
//camlQuery.Append("</View>");
//// Load the list items
//list.LoadItemsByCamlQuery(new CamlQueryOptions()
//{
// ViewXml = camlQuery.ToString(),
// DatesInUtc = true
//});
//await context.ExecuteAsync();
//var listItem = list.Items.AsRequested().FirstOrDefault();
//if (listItem != null && listItem.Count > 0)
//{
// // Update the item
// listItem.Values["Title"] = logInfo.Name;
// listItem.Values["Department"] = logInfo.Department;
// listItem.Values["TeamMembership"] = logInfo.TeamMembership;
// listItem.Values["Notification"] = logInfo.Notification;
// listItem.Values["CompletedOn"] = logInfo.CompletedOn;
// await listItem.UpdateAsync();
// await context.ExecuteAsync();
//}
// Create a new item
await list.Items.AddAsync(new Dictionary<string, object>()
{
{ "Title", logInfo.Name },
{ "Email", logInfo.Email },
{ "Department", logInfo.Department },
{ "TeamMembership", logInfo.TeamMembership },
{ "Notification", logInfo.Notification },
{ "ProcessedOn", logInfo.ProcessedOn },
{ "CompletedOn", logInfo.CompletedOn }
});
await context.ExecuteAsync();
}
}
catch (Exception ex)
{
logger.LogError(ex, "Error in AddItemsToList in Onboarding List");
}
}
}
}

View File

@ -0,0 +1,40 @@
using Azure.Core;
using Azure.Identity;
using Microsoft.Extensions.Configuration;
using O365C.FuncApp.Induction;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace O365C.FuncApp.Induction.Services
{
public interface ITokenService
{
Task<string> GetAccessTokenAsync();
}
public class TokenService: ITokenService
{
private readonly AzureFunctionSettings _azureFunctionSettings;
private readonly TokenCredential _tokenCredential;
public TokenService(AzureFunctionSettings azureFunctionSettings)
{
_azureFunctionSettings = azureFunctionSettings;
// Create TokenCredential using client secret
_tokenCredential = new ClientSecretCredential(_azureFunctionSettings.TenantId, _azureFunctionSettings.ClientId, _azureFunctionSettings.ClientSecret);
}
public async Task<string> GetAccessTokenAsync()
{
// Use _tokenCredential to get access token
var tokenRequestContext = new TokenRequestContext(new[] { "https://graph.microsoft.com/.default" });
var accessToken = await _tokenCredential.GetTokenAsync(tokenRequestContext, default);
return accessToken.Token;
}
}
}

View File

@ -0,0 +1,12 @@
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"excludedTypes": "Request"
},
"enableLiveMetricsFilters": true
}
}
}

View File

@ -0,0 +1,17 @@
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"TenantId": "",
"ClientId": "",
"ClientSecret": "",
"SiteUrl": "https://tenat.sharepoint.com/sites/dev",
"CertificateThumbprint": "",
"Base64EncodedCert": ""
},
"Host": {
"LocalHttpPort": 7015,
"CORS": "*"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

@ -0,0 +1,72 @@
[
{
"name": "pnp-sp-dev-spfx-web-parts-react-employees-onboarding",
"source": "pnp",
"title": "Employee Onboarding",
"shortDescription": "This project is an SPFx (SharePoint Framework) application designed for employee onboarding.",
"url": "https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-employees-onboarding",
"downloadUrl": "https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-employees-onboarding",
"longDescription": [
"This project is an SPFx (SharePoint Framework) application designed for employee onboarding. It automates various tasks for each employee, such as updating their department, joining the team, and sending notification emails. The application utilizes the Microsoft Graph SDK's batch requests approach to efficiently manage these operations within a .NET-based Azure function. Additionally, the system logs information into a SharePoint list for auditing purposes."
],
"creationDateTime": "2024-09-01",
"updateDateTime": "2024-09-01",
"products": [
"SharePoint"
],
"metadata": [
{
"key": "CLIENT-SIDE-DEV",
"value": "React"
},
{
"key": "SPFX-VERSION",
"value": "1.19.0"
}
],
"thumbnails": [
{
"name": "demo1.png",
"type": "image",
"order": 100,
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-employees-onboarding/assets/D:\\GitHub\\pnp\\sp-dev-fx-webparts\\samples\\react-employees-onboarding\\assets\\demo1.png",
"alt": "Web Part Preview"
},
{
"name": "demo2.png",
"type": "image",
"order": 101,
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-employees-onboarding/assets/D:\\GitHub\\pnp\\sp-dev-fx-webparts\\samples\\react-employees-onboarding\\assets\\demo2.png",
"alt": "Web Part Preview"
},
{
"name": "demo3.png",
"type": "image",
"order": 102,
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-employees-onboarding/assets/D:\\GitHub\\pnp\\sp-dev-fx-webparts\\samples\\react-employees-onboarding\\assets\\demo3.png",
"alt": "Web Part Preview"
},
{
"name": "demo4.png",
"type": "image",
"order": 103,
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-employees-onboarding/assets/D:\\GitHub\\pnp\\sp-dev-fx-webparts\\samples\\react-employees-onboarding\\assets\\demo4.png",
"alt": "Web Part Preview"
}
],
"authors": [
{
"gitHubAccount": "ejazhussain",
"pictureUrl": "https://github.com/ejazhussain.png",
"name": "Ejaz Hussain"
}
],
"references": [
{
"name": "Build your first SharePoint client-side web part",
"description": "Client-side web parts are client-side components that run in the context of a SharePoint page. Client-side web parts can be deployed to SharePoint environments that support the SharePoint Framework. You can also use modern JavaScript web frameworks, tools, and libraries to build them.",
"url": "https://docs.microsoft.com/sharepoint/dev/spfx/web-parts/get-started/build-a-hello-world-web-part"
}
]
}
]

View File

@ -0,0 +1,19 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"induction-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/induction/InductionWebPart.js",
"manifest": "./src/webparts/induction/InductionWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"InductionWebPartStrings": "lib/webparts/induction/loc/{locale}.js",
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js"
}
}

View File

@ -0,0 +1,7 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./release/assets/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "on-boarding",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,38 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "on-boarding-client-side-solution",
"id": "2f1e9f0d-4c50-49fa-90c3-ed2a73555b89",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"developer": {
"name": "",
"websiteUrl": "",
"privacyUrl": "",
"termsOfUseUrl": "",
"mpnId": "Undefined-1.19.0"
},
"metadata": {
"shortDescription": {
"default": "OnBoarding description"
},
"longDescription": {
"default": "OnBoarding description"
},
"screenshotPaths": [],
"videoUrl": "",
"categories": []
},
"features": [{
"title": "on-boarding Feature",
"description": "The feature that activates elements of the on-boarding solution.",
"id": "f105bfaa-b923-4872-96ac-2f5f5989a2b1",
"version": "1.0.0.0"
}]
},
"paths": {
"zippedPackage": "solution/on-boarding.sppkg"
}
}

View File

@ -0,0 +1,3 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/sass.schema.json"
}

View File

@ -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"
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}

View File

@ -0,0 +1,22 @@
'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;
};
/* fast-serve */
const { addFastServe } = require("spfx-fast-serve-helpers");
addFastServe(build);
/* end of fast-serve */
build.initialize(require('gulp'));

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,55 @@
{
"name": "on-boarding",
"version": "0.0.1",
"private": true,
"engines": {
"node": ">=18.17.1 <19.0.0"
},
"main": "lib/index.js",
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test",
"serve": "fast-serve"
},
"dependencies": {
"@fluentui/react": "^8.106.4",
"@mantine/core": "^6.0.22",
"@mantine/dropzone": "^6.0.22",
"@mantine/hooks": "^6.0.22",
"@mantine/notifications": "^6.0.22",
"@microsoft/sp-component-base": "1.19.0",
"@microsoft/sp-core-library": "1.19.0",
"@microsoft/sp-lodash-subset": "1.19.0",
"@microsoft/sp-office-ui-fabric-core": "1.19.0",
"@microsoft/sp-property-pane": "1.19.0",
"@microsoft/sp-webpart-base": "1.19.0",
"@pnp/spfx-controls-react": "3.19.0",
"@tabler/icons-react": "^3.11.0",
"@types/uuid": "^10.0.0",
"axios": "^1.7.5",
"react": "17.0.1",
"react-dom": "17.0.1",
"react-papaparse": "^4.4.0",
"recoil": "^0.7.7",
"tslib": "2.3.1",
"uuid": "^10.0.0"
},
"devDependencies": {
"@microsoft/eslint-config-spfx": "1.20.1",
"@microsoft/eslint-plugin-spfx": "1.20.1",
"@microsoft/rush-stack-compiler-4.7": "0.1.0",
"@microsoft/sp-build-web": "1.20.1",
"@microsoft/sp-module-interfaces": "1.20.1",
"@rushstack/eslint-config": "2.5.1",
"@types/react": "17.0.45",
"@types/react-dom": "17.0.17",
"@types/webpack-env": "~1.15.2",
"ajv": "^6.12.5",
"eslint": "8.7.0",
"eslint-plugin-react-hooks": "4.3.0",
"gulp": "4.0.2",
"spfx-fast-serve-helpers": "~1.19.0",
"typescript": "4.7.4"
}
}

View File

@ -0,0 +1 @@
// A file is required to be in the root of the /src directory by the TypeScript compiler

View File

@ -0,0 +1,14 @@
export type EmployeeInfo = {
name: string;
email: string;
department: string;
};
export type EmployeeOnboarding = EmployeeInfo & {
teamMembership: boolean;
notification: boolean;
processedOn: string;
completedOn: string;
};

View File

@ -0,0 +1,29 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "672a8bfb-d69a-48d5-9b09-97f2847c74b7",
"alias": "InductionWebPart",
"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": "Induction" },
"description": { "default": "Induction description" },
"officeFabricIconFontName": "SchoolDataSyncLogo",
"properties": {
"title": "Employee Onboarding",
"description": "This employee onboarding application automates tasks like department updates, team assignments, and notification emails using Microsoft Graph SDK's batch request feature for efficiency."
}
}]
}

View File

@ -0,0 +1,143 @@
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 'InductionWebPartStrings';
import { IInductionProps } from './components/IInductionProps';
import { Container } from './components/Container';
export interface IInductionWebPartProps {
title: string;
description: string;
listUrl: string;
azureFunctionUrl:string
}
export default class InductionWebPart extends BaseClientSideWebPart<IInductionWebPartProps> {
private _isDarkTheme: boolean = false;
private _environmentMessage: string = '';
public render(): void {
const element: React.ReactElement<IInductionProps> = React.createElement(
Container,
{
title: this.properties.title,
description: this.properties.description,
listUrl: this.properties.listUrl,
azureFunctionUrl:this.properties.azureFunctionUrl,
isDarkTheme: this._isDarkTheme,
environmentMessage: this._environmentMessage,
hasTeamsContext: !!this.context.sdks.microsoftTeams,
userDisplayName: this.context.pageContext.user.displayName,
context: this.context
}
);
ReactDom.render(element, this.domElement);
}
protected onInit(): Promise<void> {
return this._getEnvironmentMessage().then(message => {
this._environmentMessage = message;
});
}
private _getEnvironmentMessage(): Promise<string> {
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);
this.domElement.style.setProperty('--primaryButtonBackground', semanticColors.primaryButtonBackground || null);
this.domElement.style.setProperty('--primaryButtonBackgroundHovered', semanticColors.primaryButtonBackgroundHovered || null);
this.domElement.style.setProperty('--primaryButtonBackgroundPressed', semanticColors.primaryButtonBackgroundPressed || null);
this.domElement.style.setProperty('--primaryButtonText', semanticColors.primaryButtonText || null);
this.domElement.style.setProperty('--primaryButtonBorder', semanticColors.primaryButtonBorder || null);
this.domElement.style.setProperty('--themePrimrary', "#cd1409" || 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("title", {
label: "Title",
}),
PropertyPaneTextField("description", {
label: "Description",
}),
PropertyPaneTextField('listUrl', {
label: strings.ListUrlFieldLabel
}),
PropertyPaneTextField('azureFunctionUrl', {
label: strings.AzureFunctionUrlFieldLabel
})
]
}
]
}
]
};
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,62 @@
import * as React from "react";
//import { WebPartContext } from "@microsoft/sp-webpart-base";
import { RecoilRoot } from "recoil";
import { MantineProvider } from "@mantine/core";
import Induction from "./Induction";
import { WebPartContext } from "@microsoft/sp-webpart-base";
export interface IContainerProps {
title: string;
description: string;
listUrl: string;
azureFunctionUrl: string;
isDarkTheme: boolean;
environmentMessage: string;
hasTeamsContext: boolean;
userDisplayName: string;
context: WebPartContext
}
const brandColors: any = [
'#ffebe8',
'#fdd4d2',
'#f9a6a1',
'#f6756d',
'#f44c41',
'#f23425',
'#f32617',
'#d81a0d',
'#c11308',
'#a90403'
];
export const Container: React.FC<IContainerProps> = (props) => {
return (
<MantineProvider
theme={{
colorScheme: 'light',
primaryColor: 'red',
colors: {
red: brandColors,
// blue: brandColors,
},
shadows: {
md: '1px 1px 3px rgba(0, 0, 0, .25)',
xl: '5px 5px 3px rgba(0, 0, 0, .25)',
},
headings: { fontFamily: "Segoe UI" , sizes: {
h1: { fontSize: '2rem' },
}},
}}
withGlobalStyles
withNormalizeCSS
>
<RecoilRoot>
{/* <Divider my="xs" label="Preference" labelPosition="center" /> */}
<Induction {...props} />
</RecoilRoot>
</MantineProvider>
);
};

View File

@ -0,0 +1,32 @@
import * as React from "react";
import { Group, Text, rem } from '@mantine/core';
import { IconFileTypeCsv } from '@tabler/icons-react';
import { Dropzone, DropzoneProps, FileWithPath, MIME_TYPES } from "@mantine/dropzone";
const DropzoneComponent: React.FC<Partial<DropzoneProps> & { handleDrop: (files: FileWithPath[]) => void }> = (props) => {
return (
<Dropzone
onDrop={(files) => props.handleDrop(files)}
onReject={(files) => console.log('rejected files', files)}
maxSize={3 * 1024 ** 2}
accept={[MIME_TYPES.csv]}
{...props}
>
<Group position="center" spacing="xl" style={{ minHeight: rem(220), pointerEvents: 'none' }}>
<Dropzone.Idle>
<IconFileTypeCsv size="3.2rem" stroke={1.5} />
</Dropzone.Idle>
<div>
<Text size="xl" inline>
Drag or click to select files
</Text>
<Text size="sm" color="dimmed" inline mt={7}>
Please note that only CSV files are supported, and each file should not exceed 5 MB.
</Text>
</div>
</Group>
</Dropzone>
);
};
export default DropzoneComponent;

View File

@ -0,0 +1,54 @@
import * as React from 'react';
import { Table } from '@mantine/core';
import { EmployeeOnboarding } from '../../../types/Components.Types';
import { IconSquareCheck, IconSquareLetterX } from '@tabler/icons-react';
interface EmployeeTableProps {
data: EmployeeOnboarding[];
fileName?: string;
isCompleted?: boolean;
}
const EmployeeTable: React.FC<EmployeeTableProps> = ({ data, fileName, isCompleted }) => {
return (
<Table striped highlightOnHover withBorder withColumnBorders>
{fileName && <caption>{fileName}</caption>}
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Department</th>
{isCompleted && (
<>
<th>Team Membership</th>
<th>Notification</th>
</>
)}
</tr>
</thead>
<tbody>
{data.map((element: EmployeeOnboarding, index) => (
<tr key={index}>
<td>{element.name}</td>
<td>{element.email}</td>
<td>
{isCompleted ? (
element.department ? <IconSquareCheck color='#006600' /> : <IconSquareLetterX color="#ED2939" />
) : (
element.department
)}
</td>
{isCompleted && (
<>
<td>{element.teamMembership ? <IconSquareCheck color='#006600' /> : <IconSquareLetterX color="#ED2939" />}</td>
<td>{element.notification ? <IconSquareCheck color='#006600' /> : <IconSquareLetterX color="#ED2939" />}</td>
</>
)}
</tr>
))}
</tbody>
</Table>
);
};
export default EmployeeTable;

View File

@ -0,0 +1,13 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
export interface IInductionProps {
title: string;
description: string;
listUrl: string;
azureFunctionUrl: string;
isDarkTheme: boolean;
environmentMessage: string;
hasTeamsContext: boolean;
userDisplayName: string;
context: WebPartContext
}

View File

@ -0,0 +1,34 @@
@import '~@fluentui/react/dist/sass/References.scss';
.induction {
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
}
}
}

View File

@ -0,0 +1,131 @@
import * as React from 'react';
import styles from './Induction.module.scss';
import type { IInductionProps } from './IInductionProps';
import { Stepper, Button, Group, Text, Flex } from '@mantine/core';
import { usePapaParse } from "react-papaparse";
import axios from 'axios';
import { FileWithPath } from '@mantine/dropzone';
import { EmployeeOnboarding } from '../../../types/Components.Types';
import DropzoneComponent from './DropzoneComponent';
import EmployeeTable from './EmployeeTable';
import OnboardingTimeline from './OnboardingTimeline';
import { IconExternalLink } from '@tabler/icons-react';
import { Placeholder } from "@pnp/spfx-controls-react/lib/Placeholder";
import { WebPartTitle } from './webPartTitle';
const Induction: React.FC<IInductionProps> = (props) => {
const { listUrl, azureFunctionUrl, context, description, title } = props;
const { readString } = usePapaParse();
const [active, setActive] = React.useState(0);
const [acceptedFiles, setAcceptedFiles] = React.useState<FileWithPath>({} as FileWithPath);
const [importedData, setImportedData] = React.useState<EmployeeOnboarding[]>([]);
const [data, setData] = React.useState<EmployeeOnboarding[]>([]);
const [loading, setLoading] = React.useState<boolean>(false);
const [completed, setCompleted] = React.useState<boolean>(false);
const parseCSVFile = (file: File): void => {
const reader = new FileReader();
reader.onload = (event) => {
const csvString = event.target?.result;
if (typeof csvString === 'string') {
readString(csvString, {
header: true,
worker: true,
complete: (results) => {
setImportedData(results.data.map((item: any) => ({
name: item.Name,
email: item.Email,
department: item.Department
})) as EmployeeOnboarding[]);
if (results.data.length > 0) {
setActive(1);
}
},
});
}
};
reader.readAsText(file);
};
const handleDrop = (files: FileWithPath[]): void => {
if (files.length > 1) {
alert('Only one file is allowed');
return;
}
setAcceptedFiles(files[0]);
parseCSVFile(files[0]);
};
const handleStepClick = (stepIndex: number): void => {
setActive(stepIndex);
};
const handleOnboarding = async (): Promise<void> => {
try {
setLoading(true);
const headers = { 'Content-Type': 'application/json' };
const response = await axios.post(azureFunctionUrl, importedData, { headers });
setData(response.data);
setCompleted(true);
setTimeout(() => {
setActive(2);
}, 3000);
} catch (err) {
console.error(err);
setCompleted(false);
} finally {
setLoading(false);
}
};
const handleConfigure = ():void => {
// Context of the web part
context.propertyPane.open();
}
if (!listUrl || !azureFunctionUrl) {
return (
<Placeholder
iconName='Edit'
iconText='Configure your web part'
description='Please configure the web part to start using it.'
buttonLabel='Configure'
onConfigure={handleConfigure}
/>
);
}
return (
<section className={`${styles.induction}`}>
<WebPartTitle title={title} description={description} />
<Stepper active={active} breakpoint="sm" onStepClick={handleStepClick} radius={"md"}>
<Stepper.Step label="Step 1" description="Import users detail">
<DropzoneComponent handleDrop={handleDrop} {...props} />
{importedData.length > 0 && (
<Flex style={{ marginTop: '20px', padding: '10px' }} mih={50} gap="md" justify="flex-start" align="flex-start" direction="column" wrap="wrap">
<EmployeeTable data={importedData} fileName={acceptedFiles.name} />
</Flex>
)}
</Stepper.Step>
<Stepper.Step label="Step 2" description="Onboarding">
<Flex style={{ marginTop: '20px', padding: '10px' }} mih={50} gap="md" justify="flex-start" align="flex-start" direction="column" wrap="wrap">
<EmployeeTable data={importedData} />
<Text fs="italic" fz="md" fw={500} c="#333333" > The following changes will be made for each user:</Text>
<OnboardingTimeline data={data} />
{!completed && <Button loading={loading} loaderProps={{ type: 'dots' }} onClick={handleOnboarding} style={{ marginTop: 40 }} gradient={{ from: 'blue', to: 'cyan', deg: 90 }}>Proceed</Button>}
</Flex>
</Stepper.Step>
<Stepper.Completed>
<EmployeeTable data={data} isCompleted />
<Group position="center" mt="xl">
<Button component="a" href={`${listUrl}`} variant="outline" leftIcon={<IconExternalLink size="0.9rem" />}>
View more detail
</Button>
</Group>
</Stepper.Completed>
</Stepper>
</section>
);
};
export default Induction;

View File

@ -0,0 +1,25 @@
import * as React from 'react';
import { Timeline } from '@mantine/core';
import { EmployeeOnboarding } from '../../../types/Components.Types';
interface OnboardingTimelineProps {
data: EmployeeOnboarding[];
}
const OnboardingTimeline: React.FC<OnboardingTimelineProps> = ({ data }) => {
return (
<Timeline color="lime" lineWidth={2} bulletSize={26}>
<Timeline.Item title="User Department" color="blue" bullet={data.length > 0 ? "✅" : ""}>
Updating user department
</Timeline.Item>
<Timeline.Item title="Join Microsoft Team" color="blue" bullet={data.length > 0 ? "✅" : ""}>
Assigning user to user department Team
</Timeline.Item>
<Timeline.Item title="Notification" color="blue" bullet={data.length > 0 ? "✅" : ""}>
Notify user via email
</Timeline.Item>
</Timeline>
);
};
export default OnboardingTimeline;

View File

@ -0,0 +1,36 @@
@import '~@fluentui/react/dist/sass/References.scss';
.webPartHeader {
.webPartTitle {
margin-bottom: 10px;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
flex-grow: 1;
display: flex;
align-items: flex-start;
color: "[theme: themePrimary, default: #0078d7]";
}
}
.webpartDescription {
margin-bottom: 20px;
margin-top: 5px;
font-size: 14px;
color: "[theme:bodyText, default: #323130]";
color: var(--bodyText);
}
// View mode
span {
// Nothing at the moment
a:link {
text-decoration: none;
}
}
.moreLink {
margin-bottom: 11px;
}
}

View File

@ -0,0 +1,62 @@
import * as React from "react";
import type { IReadonlyTheme } from "@microsoft/sp-component-base";
import styles from "./WebPartTitle.module.scss";
export interface IWebPartTitleProps {
title?: string;
titleIcon?: string;
description?: string;
className?: string;
placeholder?: string;
moreLink?: JSX.Element | (() => React.ReactNode);
themeVariant?: IReadonlyTheme;
}
/**
* Web Part Title component
*/
export class WebPartTitle extends React.Component<IWebPartTitleProps, {}> {
/**
* Constructor
*/
constructor(props: IWebPartTitleProps) {
super(props);
}
/**
* Default React component render method
*/
public render(): React.ReactElement<IWebPartTitleProps> {
const color: string =
(!!this.props.themeVariant &&
this.props.themeVariant?.semanticColors?.bodyText) ||
"";
if (this.props.title || this.props.titleIcon) {
return (
<div
className={`${styles.webPartHeader} ${
this.props.className ? this.props.className : ""
}`}
>
<div className={styles.webPartTitle} style={{ color: color }}>
<h2>{this.props.title && this.props.title}</h2>
</div>
{this.props.description && (
<div className={styles.webpartDescription}>
{this.props.description}
</div>
)}
{this.props.moreLink && (
<span className={styles.moreLink}>
{typeof this.props.moreLink === "function"
? this.props.moreLink()
: this.props.moreLink}
</span>
)}
</div>
);
}
return <></>;
}
}

View File

@ -0,0 +1 @@
export * from "./WebPartTitle";

View File

@ -0,0 +1,17 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"ListUrlFieldLabel": "List Url",
"AzureFunctionUrlFieldLabel": "Azure Function Url",
"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"
}
});

View File

@ -0,0 +1,20 @@
declare interface IInductionWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
ListUrlFieldLabel: string;
AzureFunctionUrlFieldLabel: string;
AppLocalEnvironmentSharePoint: string;
AppLocalEnvironmentTeams: string;
AppLocalEnvironmentOffice: string;
AppLocalEnvironmentOutlook: string;
AppSharePointEnvironment: string;
AppTeamsTabEnvironment: string;
AppOfficeEnvironment: string;
AppOutlookEnvironment: string;
UnknownEnvironment: string;
}
declare module 'InductionWebPartStrings' {
const strings: IInductionWebPartStrings;
export = strings;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 B

View File

@ -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"
]
}

View File

@ -0,0 +1,38 @@
{
"name": "SPFx 1.19.0",
"image": "docker.io/m365pnp/spfx:1.19.0",
"customizations": {
"vscode": {
"extensions": [
"editorconfig.editorconfig",
"dbaeumer.vscode-eslint"
]
}
},
"forwardPorts": [
4321,
35729,
5432
],
"portsAttributes": {
"4321": {
"protocol": "https",
"label": "Manifest",
"onAutoForward": "silent",
"requireLocalPort": true
},
"5432": {
"protocol": "https",
"label": "Workbench",
"onAutoForward": "silent"
},
"35729": {
"protocol": "https",
"label": "LiveReload",
"onAutoForward": "silent",
"requireLocalPort": true
}
},
"postCreateCommand": "bash .devcontainer/spfx-startup.sh",
"remoteUser": "node"
}

View File

@ -0,0 +1,33 @@
echo
echo -e "\e[1;94mInstalling Node dependencies\e[0m"
npm install
## commands to create dev certificate and copy it to the root folder of the project
echo
echo -e "\e[1;94mGenerating dev certificate\e[0m"
gulp trust-dev-cert
# Convert the generated PEM certificate to a CER certificate
openssl x509 -inform PEM -in ~/.rushstack/rushstack-serve.pem -outform DER -out ./spfx-dev-cert.cer
# Copy the PEM ecrtificate for non-Windows hosts
cp ~/.rushstack/rushstack-serve.pem ./spfx-dev-cert.pem
## add *.cer to .gitignore to prevent certificates from being saved in repo
if ! grep -Fxq '*.cer' ./.gitignore
then
echo "# .CER Certificates" >> .gitignore
echo "*.cer" >> .gitignore
fi
## add *.pem to .gitignore to prevent certificates from being saved in repo
if ! grep -Fxq '*.pem' ./.gitignore
then
echo "# .PEM Certificates" >> .gitignore
echo "*.pem" >> .gitignore
fi
echo
echo -e "\e[1;92mReady!\e[0m"
echo -e "\n\e[1;94m**********\nOptional: if you plan on using gulp serve, don't forget to add the container certificate to your local machine. Please visit https://aka.ms/spfx-devcontainer for more information\n**********"

View File

@ -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<k,V>".
'@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: {}
}
]
};

View File

@ -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

View File

@ -0,0 +1,16 @@
!dist
config
gulpfile.js
release
src
temp
tsconfig.json
tslint.json
*.log
.yo-rc.json
.vscode

View File

@ -0,0 +1 @@
v18.18.0

View File

@ -0,0 +1,21 @@
{
"@microsoft/generator-sharepoint": {
"plusBeta": false,
"isCreatingSolution": true,
"nodeVersion": "18.18.0",
"sdksVersions": {
"@microsoft/microsoft-graph-client": "3.0.2",
"@microsoft/teams-js": "2.12.0"
},
"version": "1.19.0",
"libraryName": "react-enhanced-button",
"libraryId": "84804705-0fca-4c5b-8b6f-e9b01c02d820",
"environment": "spo",
"packageManager": "npm",
"solutionName": "react-enhanced-button",
"solutionShortDescription": "react-enhanced-button description",
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,103 @@
# Enhanced Button
## Summary
The Enhanced Button Web Part is a custom SharePoint web part that extends the functionality of the native button web part. It provides additional configuration options to create more customizable and flexible buttons within your SharePoint pages.
![App](assets/app.jpeg)
## Compatibility
| :warning: Important |
|:---------------------------|
| Every SPFx version is optimally compatible with specific versions of Node.js. In order to be able to build this sample, you need to ensure that the version of Node on your workstation matches one of the versions listed in this section. This sample will not work on a different version of Node.|
|Refer to <https://aka.ms/spfx-matrix> for more information on SPFx compatibility. |
This sample is optimally compatible with the following environment configuration:
![SPFx 1.19.0](https://img.shields.io/badge/SPFx-1.19.0-green.svg)
![Node.js v18](https://img.shields.io/badge/Node.js-v18-green.svg)
![Compatible with SharePoint Online](https://img.shields.io/badge/SharePoint%20Online-Compatible-green.svg)
![Does not work with SharePoint 2019](https://img.shields.io/badge/SharePoint%20Server%202019-Incompatible-red.svg "SharePoint Server 2019 requires SPFx 1.4.1 or lower")
![Does not work with SharePoint 2016 (Feature Pack 2)](https://img.shields.io/badge/SharePoint%20Server%202016%20(Feature%20Pack%202)-Incompatible-red.svg "SharePoint Server 2016 Feature Pack 2 requires SPFx 1.1")
![Local Workbench Unsupported](https://img.shields.io/badge/Local%20Workbench-Unsupported-red.svg "Local workbench is no longer available as of SPFx 1.13 and above")
![Hosted Workbench Compatible](https://img.shields.io/badge/Hosted%20Workbench-Compatible-green.svg)
![Compatible with Remote Containers](https://img.shields.io/badge/Remote%20Containers-Compatible-green.svg)
## Applies to
* [SharePoint Framework](https://learn.microsoft.com/sharepoint/dev/spfx/sharepoint-framework-overview)
* [Microsoft 365 tenant](https://learn.microsoft.com/sharepoint/dev/spfx/set-up-your-development-environment)
> Get your own free development tenant by subscribing to [Microsoft 365 developer program](https://aka.ms/m365/devprogram)
## Contributors
<!--
We use this section to recognize and promote your contributions. Please provide one author per line -- even if you worked together on it.
We'll only use the info you provided here. Make sure to include your full name, not just your GitHub username.
Provide a link to your GitHub profile to help others find more cool things you have done. The only link we'll accept is a link to your GitHub profile.
If you want to provide links to your social media, blog, and employer name, make sure to update your GitHub profile.
-->
* [Ari Gunawan](https://github.com/AriGunawan)
## Version history
Version|Date|Comments
-------|----|--------
1.0|September 01, 2024|Initial release
## Minimal path to awesome
* Clone this repository (or [download this solution as a .ZIP file](https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-enhanced-button) then unzip it)
* From your command line, change your current directory to the directory containing this sample (`react-enhanced-button`, located under `samples`)
* in the command line run:
* `npm install`
* `gulp serve` or `npm run serve`
> This sample can also be opened with [VS Code Remote Development](https://code.visualstudio.com/docs/remote/remote-overview). Visit <https://aka.ms/spfx-devcontainer> for further instructions.
## Features
This web part offers the following enhanced configuration options:
Basic configuration options:
* **Label**: Customize the button text
* **Link**: Specify the URL to link to
* **Link Behaviour**: Configure how the button link opens (e.g., same tab, new tab)
* **Button Alignment**: Align the button horizontally (left, center, right)
* **Width**: Customize the button width
* **Height**: Adjust the button height
* **Border Radius**: Set the corner roundness of the button
Advanced configuration options:
* **Container Styles**: Apply custom styles to the button container using raw CSS value
* **Button Styles**: Customize the button's appearance using raw CSS value
* **Button Hover Styles**: Define styles for when users hover over the button using raw CSS value
## Help
We do not support samples, but this community is always willing to help, and we want to improve these samples. We use GitHub to track issues, which makes it easy for community members to volunteer their time and help resolve issues.
If you're having issues building the solution, please run [spfx doctor](https://pnp.github.io/cli-microsoft365/cmd/spfx/spfx-doctor/) from within the solution folder to diagnose incompatibility issues with your environment.
You can try looking at [issues related to this sample](https://github.com/pnp/sp-dev-fx-webparts/issues?q=label%3A%22sample%3A%20react-enhanced-button%22) to see if anybody else is having the same issues.
You can also try looking at [discussions related to this sample](https://github.com/pnp/sp-dev-fx-webparts/discussions?discussions_q=react-enhanced-button) and see what the community is saying.
If you encounter any issues using this sample, [create a new issue](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Abug-suspected%2Csample%3A%20react-enhanced-button&template=bug-report.yml&sample=react-enhanced-button&authors=@AriGunawan&title=react-enhanced-button%20-%20).
For questions regarding this sample, [create a new question](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Aquestion%2Csample%3A%20react-enhanced-button&template=question.yml&sample=react-enhanced-button&authors=@AriGunawan&title=react-enhanced-button%20-%20).
Finally, if you have an idea for improvement, [make a suggestion](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Aenhancement%2Csample%3A%20react-enhanced-button&template=suggestion.yml&sample=react-enhanced-button&authors=@AriGunawan&title=react-enhanced-button%20-%20).
## 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.**
<img src="https://m365-visitor-stats.azurewebsites.net/sp-dev-fx-webparts/samples/react-enhanced-button" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

View File

@ -0,0 +1,50 @@
[
{
"name": "pnp-sp-dev-spfx-web-parts-react-enhanced-button",
"source": "pnp",
"title": "Enhanced Button",
"shortDescription": "Extends the functionality of the native button web part.",
"url": "https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-enhanced-button",
"downloadUrl": "https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-enhanced-button",
"longDescription": [
"The Enhanced Button Web Part is a custom SharePoint web part that extends the functionality of the native button web part. It provides additional configuration options to create more customizable and flexible buttons within your SharePoint pages."
],
"creationDateTime": "2024-09-01",
"updateDateTime": "2024-09-01",
"products": [
"SharePoint"
],
"metadata": [
{
"key": "CLIENT-SIDE-DEV",
"value": "React"
},
{
"key": "SPFX-VERSION",
"value": "1.19.0"
}
],
"thumbnails": [
{
"type": "image",
"order": 100,
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-enhanced-button/assets/app.jpeg",
"alt": "Web Part Preview"
}
],
"authors": [
{
"gitHubAccount": "AriGunawan",
"pictureUrl": "https://github.com/AriGunawan.png",
"name": "Ari Gunawan"
}
],
"references": [
{
"name": "Build your first SharePoint client-side web part",
"description": "Client-side web parts are client-side components that run in the context of a SharePoint page. Client-side web parts can be deployed to SharePoint environments that support the SharePoint Framework. You can also use modern JavaScript web frameworks, tools, and libraries to build them.",
"url": "https://docs.microsoft.com/sharepoint/dev/spfx/web-parts/get-started/build-a-hello-world-web-part"
}
]
}
]

View File

@ -0,0 +1,18 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"enhanced-button-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/enhancedButton/EnhancedButtonWebPart.js",
"manifest": "./src/webparts/enhancedButton/EnhancedButtonWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"EnhancedButtonWebPartStrings": "lib/webparts/enhancedButton/loc/{locale}.js"
}
}

View File

@ -0,0 +1,7 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./release/assets/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "react-enhanced-button",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,40 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "React Enhanced Button",
"id": "84804705-0fca-4c5b-8b6f-e9b01c02d820",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"developer": {
"name": "",
"websiteUrl": "",
"privacyUrl": "",
"termsOfUseUrl": "",
"mpnId": "Undefined-1.19.0"
},
"metadata": {
"shortDescription": {
"default": "react-enhanced-button description"
},
"longDescription": {
"default": "react-enhanced-button description"
},
"screenshotPaths": [],
"videoUrl": "",
"categories": []
},
"features": [
{
"title": "react-enhanced-button Feature",
"description": "The feature that activates elements of the react-enhanced-button solution.",
"id": "219523c6-69fa-4dfb-8824-5a0f695b69f6",
"version": "1.0.0.0"
}
]
},
"paths": {
"zippedPackage": "solution/React Enhanced Button.sppkg"
}
}

View File

@ -0,0 +1,3 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/sass.schema.json"
}

View File

@ -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"
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}

View File

@ -0,0 +1,22 @@
'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;
};
/* fast-serve */
const { addFastServe } = require("spfx-fast-serve-helpers");
addFastServe(build);
/* end of fast-serve */
build.initialize(require('gulp'));

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,44 @@
{
"name": "react-enhanced-button",
"version": "0.0.1",
"private": true,
"engines": {
"node": ">=18.17.1 <19.0.0"
},
"main": "lib/index.js",
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test",
"serve": "fast-serve"
},
"dependencies": {
"tslib": "2.3.1",
"react": "17.0.1",
"react-dom": "17.0.1",
"@fluentui/react": "^8.106.4",
"@microsoft/sp-core-library": "1.19.0",
"@microsoft/sp-component-base": "1.19.0",
"@microsoft/sp-property-pane": "1.19.0",
"@microsoft/sp-webpart-base": "1.19.0",
"@microsoft/sp-lodash-subset": "1.19.0",
"@microsoft/sp-office-ui-fabric-core": "1.19.0"
},
"devDependencies": {
"@microsoft/rush-stack-compiler-4.7": "0.1.0",
"@rushstack/eslint-config": "2.5.1",
"@microsoft/eslint-plugin-spfx": "1.20.1",
"@microsoft/eslint-config-spfx": "1.20.1",
"@microsoft/sp-build-web": "1.20.1",
"@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.20.1",
"spfx-fast-serve-helpers": "~1.19.0"
}
}

View File

@ -0,0 +1 @@
// A file is required to be in the root of the /src directory by the TypeScript compiler

View File

@ -0,0 +1,35 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "5e8d2262-3bfb-45d0-b896-2c52ca69e87b",
"alias": "EnhancedButtonWebPart",
"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": "Enhanced Button" },
"description": { "default": "EnhancedButton description" },
"officeFabricIconFontName": "ButtonControl",
"properties": {
"buttonAlignment": "left"
}
}
]
}

View File

@ -0,0 +1,119 @@
import * as React from "react";
import * as ReactDom from "react-dom";
import { Version } from "@microsoft/sp-core-library";
import {
PropertyPaneDropdown,
PropertyPaneTextField,
type IPropertyPaneConfiguration,
} from "@microsoft/sp-property-pane";
import { BaseClientSideWebPart } from "@microsoft/sp-webpart-base";
import EnhancedButton from "./components/EnhancedButton";
export interface IEnhancedButtonWebPartProps {
label: string;
link: string;
buttonAlignment: string;
width: string;
height: string;
borderRadius: string;
linkBehaviour: string;
containerStyles: string;
buttonStyles: string;
buttonOnHoverStyles: string;
}
export default class EnhancedButtonWebPart extends BaseClientSideWebPart<IEnhancedButtonWebPartProps> {
public render(): void {
const element: React.ReactElement = React.createElement(
EnhancedButton,
this.properties
);
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse("1.0");
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
groups: [
{
groupName: "Basic",
groupFields: [
PropertyPaneTextField("label", {
label: "Label",
}),
PropertyPaneTextField("link", {
label: "Link",
}),
PropertyPaneDropdown("linkBehaviour", {
label: "Link Behaviour",
options: [
{ key: "_blank", text: "Open in New Tab" },
{ key: "_self", text: "Open in Same Tab" },
],
}),
PropertyPaneDropdown("buttonAlignment", {
label: "Button Alignment",
options: [
{ key: "flex-start", text: "Left" },
{ key: "center", text: "Center" },
{ key: "flex-end", text: "Right" },
],
}),
PropertyPaneTextField("width", {
label: "Width",
}),
PropertyPaneTextField("height", {
label: "Height",
}),
PropertyPaneDropdown("borderRadius", {
label: "Border Radius",
options: [
{ key: "0", text: "None" },
{ key: "4px", text: "Small" },
{ key: "9999px", text: "Medium" },
{ key: "100%", text: "Full" },
],
}),
],
},
{
groupName: "Advanced",
groupFields: [
PropertyPaneTextField("containerStyles", {
label: "Container Styles",
description: "CSS styles to apply to the container",
multiline: true,
rows: 5,
}),
PropertyPaneTextField("buttonStyles", {
label: "Button Styles",
description: "CSS styles to apply to the button",
multiline: true,
rows: 5,
}),
PropertyPaneTextField("buttonOnHoverStyles", {
label: "Button On Hover Styles",
description: "CSS styles to apply to the button on hover",
multiline: true,
rows: 5,
}),
],
},
],
},
],
};
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,21 @@
.enhancedButton {
display: flex;
justify-content: var(--justify-content);
}
.link {
width: var(--width);
height: var(--height);
border-radius: var(--border-radius);
font-size: 16px;
font-weight: 600;
text-decoration: none;
padding: 8px 16px;
background-color: "[theme:themePrimary, default:#0078d7]";
color: "[theme:white, default:#ffffff]";
cursor: pointer;
&:hover {
background-color: "[theme:themeDark, default:#005a9e]";
}
}

View File

@ -0,0 +1,46 @@
import { css } from "@fluentui/react";
import { useId } from "@fluentui/react-hooks";
import * as React from "react";
import { IEnhancedButtonWebPartProps } from "../EnhancedButtonWebPart";
import styles from "./EnhancedButton.module.scss";
export default function EnhancedButton(
props: IEnhancedButtonWebPartProps
): React.ReactElement {
const id = useId();
return (
<>
<style>
{`
#${id}.${styles.enhancedButton} {
--justify-content: ${props.buttonAlignment};
--width: ${props.width || "auto"};
--height: ${props.height || "auto"};
--border-radius: ${props.borderRadius || "4px"};
}
#${id}.${styles.enhancedButton}.customCss {
${props.containerStyles}
}
#${id}.customCss .${styles.link} {
${props.buttonStyles}
}
#${id}.customCss .${styles.link}:hover {
${props.buttonOnHoverStyles}
}
`}
</style>
<div id={id} className={css(styles.enhancedButton, "customCss")}>
<a
rel="noopener noreferrer"
href={props.link}
className={styles.link}
target={props.linkBehaviour}
>
{props.label}
</a>
</div>
</>
);
}

View File

@ -0,0 +1,3 @@
define([], function () {
return {};
});

View File

@ -0,0 +1,6 @@
declare interface IEnhancedButtonWebPartStrings {}
declare module "EnhancedButtonWebPartStrings" {
const strings: IEnhancedButtonWebPartStrings;
export = strings;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 B

View File

@ -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"
]
}

View File

@ -1,39 +0,0 @@
// For more information on how to run this SPFx project in a VS Code Remote Container, please visit https://aka.ms/spfx-devcontainer
{
"name": "SPFx 1.10.0",
"image": "docker.io/m365pnp/spfx:1.10.0",
// Set *default* container specific settings.json values on container create.
"settings": {},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"editorconfig.editorconfig",
"dbaeumer.vscode-eslint"
],
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [
4321,
35729,
5432
],
"portsAttributes": {
"4321": {
"protocol": "https",
"label": "Manifest",
"onAutoForward": "silent",
"requireLocalPort": true
},
"5432": {
"protocol": "https",
"label": "Workbench",
"onAutoForward": "silent"
},
"35729": {
"protocol": "https",
"label": "LiveReload",
"onAutoForward": "silent",
"requireLocalPort": true
}
},
"postCreateCommand": "bash .devcontainer/spfx-startup.sh",
"remoteUser": "node"
}

View File

@ -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<k,V>".
'@typescript-eslint/no-explicit-any': 0,
// 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': 0,
// 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: {}
}
]
};

View File

@ -9,9 +9,11 @@ node_modules
# Build generated files
dist
lib
release
solution
temp
*.sppkg
.heft
# Coverage directory used by tools like istanbul
coverage

View File

@ -0,0 +1,16 @@
!dist
config
gulpfile.js
release
src
temp
tsconfig.json
tslint.json
*.log
.yo-rc.json
.vscode

View File

@ -0,0 +1 @@
v18.17.1

View File

@ -1,11 +1,20 @@
{
"@microsoft/generator-sharepoint": {
"plusBeta": false,
"isCreatingSolution": true,
"environment": "spo",
"version": "1.9.1",
"nodeVersion": "18.17.1",
"sdksVersions": {
"@microsoft/microsoft-graph-client": "3.0.2",
"@microsoft/teams-js": "2.12.0"
},
"version": "1.19.0",
"libraryName": "react-graph-calendar",
"libraryId": "42fe0a0f-c4d9-4b05-806c-3857decb3d71",
"environment": "spo",
"packageManager": "npm",
"solutionName": "react-graph-calendar",
"solutionShortDescription": "react-graph-calendar description",
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"componentType": "webpart"
}

Some files were not shown because too many files have changed in this diff Show More