Compare commits
No commits in common. "34c060c2fc69f2d8c7b27b7516078e7354411d71" and "c82668fb5ce0ead7b704641abc9520c5fc651700" have entirely different histories.
34c060c2fc
...
c82668fb5c
|
@ -7237,7 +7237,7 @@
|
||||||
"2. Copy Name allows to copy the name of the document without using the currently available rename functionality."
|
"2. Copy Name allows to copy the name of the document without using the currently available rename functionality."
|
||||||
],
|
],
|
||||||
"creationDateTime": "2023-10-04",
|
"creationDateTime": "2023-10-04",
|
||||||
"updateDateTime": "2024-08-28",
|
"updateDateTime": "2023-10-04",
|
||||||
"products": [
|
"products": [
|
||||||
"SharePoint"
|
"SharePoint"
|
||||||
],
|
],
|
||||||
|
@ -7248,7 +7248,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "SPFX-VERSION",
|
"key": "SPFX-VERSION",
|
||||||
"value": "1.19.0"
|
"value": "1.18.0"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tags": [],
|
"tags": [],
|
||||||
|
|
|
@ -7237,7 +7237,7 @@
|
||||||
"2. Copy Name allows to copy the name of the document without using the currently available rename functionality."
|
"2. Copy Name allows to copy the name of the document without using the currently available rename functionality."
|
||||||
],
|
],
|
||||||
"creationDateTime": "2023-10-04",
|
"creationDateTime": "2023-10-04",
|
||||||
"updateDateTime": "2024-08-28",
|
"updateDateTime": "2023-10-04",
|
||||||
"products": [
|
"products": [
|
||||||
"SharePoint"
|
"SharePoint"
|
||||||
],
|
],
|
||||||
|
@ -7248,7 +7248,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "SPFX-VERSION",
|
"key": "SPFX-VERSION",
|
||||||
"value": "1.19.0"
|
"value": "1.18.0"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tags": [],
|
"tags": [],
|
||||||
|
@ -14912,124 +14912,6 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"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",
|
"name": "pnp-sp-dev-spfx-web-parts-react-enhanced-list-formatting",
|
||||||
"source": "pnp",
|
"source": "pnp",
|
||||||
|
@ -17210,7 +17092,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.)."
|
"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",
|
"creationDateTime": "2020-11-06",
|
||||||
"updateDateTime": "2024-09-12",
|
"updateDateTime": "2020-11-06",
|
||||||
"products": [
|
"products": [
|
||||||
"SharePoint"
|
"SharePoint"
|
||||||
],
|
],
|
||||||
|
@ -17221,7 +17103,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "SPFX-VERSION",
|
"key": "SPFX-VERSION",
|
||||||
"value": "1.19.0"
|
"value": "1.10.0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "SPFX-TEAMSTAB",
|
"key": "SPFX-TEAMSTAB",
|
||||||
|
@ -17255,12 +17137,14 @@
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
"gitHubAccount": "sebastienlevert",
|
"gitHubAccount": "sebastienlevert",
|
||||||
|
"company": "Microsoft",
|
||||||
"pictureUrl": "https://github.com/sebastienlevert.png",
|
"pictureUrl": "https://github.com/sebastienlevert.png",
|
||||||
"name": "Sébastien Levert",
|
"name": "Sébastien Levert",
|
||||||
"twitter": "sebastienlevert"
|
"twitter": "sebastienlevert"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"gitHubAccount": "Abderahman88",
|
"gitHubAccount": "Abderahman88",
|
||||||
|
"company": "",
|
||||||
"pictureUrl": "https://avatars.githubusercontent.com/u/36161889?s=460&u=afdd5f6681bc375ee3811482dec79824c12d8170&v=4",
|
"pictureUrl": "https://avatars.githubusercontent.com/u/36161889?s=460&u=afdd5f6681bc375ee3811482dec79824c12d8170&v=4",
|
||||||
"name": "Abderahman Moujahid"
|
"name": "Abderahman Moujahid"
|
||||||
}
|
}
|
||||||
|
@ -19758,7 +19642,7 @@
|
||||||
"Service Health for Microsoft 365 solution show the health status for all the M365 services"
|
"Service Health for Microsoft 365 solution show the health status for all the M365 services"
|
||||||
],
|
],
|
||||||
"creationDateTime": "2023-02-03",
|
"creationDateTime": "2023-02-03",
|
||||||
"updateDateTime": "2024-09-05",
|
"updateDateTime": "2024-02-10",
|
||||||
"products": [
|
"products": [
|
||||||
"SharePoint"
|
"SharePoint"
|
||||||
],
|
],
|
||||||
|
@ -19769,7 +19653,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "SPFX-VERSION",
|
"key": "SPFX-VERSION",
|
||||||
"value": "1.19.0"
|
"value": "1.16.1"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"thumbnails": [
|
"thumbnails": [
|
||||||
|
@ -29416,55 +29300,6 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"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",
|
"name": "pnp-sp-dev-spfx-web-parts-react-tree-orgchart",
|
||||||
"source": "pnp",
|
"source": "pnp",
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
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**********"
|
|
|
@ -1,352 +0,0 @@
|
||||||
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: {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
|
@ -1,34 +0,0 @@
|
||||||
# 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
|
|
|
@ -1,16 +0,0 @@
|
||||||
!dist
|
|
||||||
config
|
|
||||||
|
|
||||||
gulpfile.js
|
|
||||||
|
|
||||||
release
|
|
||||||
src
|
|
||||||
temp
|
|
||||||
|
|
||||||
tsconfig.json
|
|
||||||
tslint.json
|
|
||||||
|
|
||||||
*.log
|
|
||||||
|
|
||||||
.yo-rc.json
|
|
||||||
.vscode
|
|
|
@ -1,21 +0,0 @@
|
||||||
{
|
|
||||||
"@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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,239 +0,0 @@
|
||||||
# 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" />
|
|
|
@ -1,25 +0,0 @@
|
||||||
|
|
||||||
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
|
|
|
@ -1,264 +0,0 @@
|
||||||
## 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
|
|
|
@ -1,60 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
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; }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
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; }
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
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; }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
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; }
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,60 +0,0 @@
|
||||||
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!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,103 +0,0 @@
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"profiles": {
|
|
||||||
"O365C.FuncApp.Induction": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"commandLineArgs": "--port 7032",
|
|
||||||
"launchBrowser": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"dependencies": {
|
|
||||||
"appInsights1": {
|
|
||||||
"type": "appInsights"
|
|
||||||
},
|
|
||||||
"storage1": {
|
|
||||||
"type": "storage",
|
|
||||||
"connectionId": "AzureWebJobsStorage"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"dependencies": {
|
|
||||||
"appInsights1": {
|
|
||||||
"type": "appInsights.sdk"
|
|
||||||
},
|
|
||||||
"storage1": {
|
|
||||||
"type": "storage.emulator",
|
|
||||||
"connectionId": "AzureWebJobsStorage"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,270 +0,0 @@
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,111 +0,0 @@
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"version": "2.0",
|
|
||||||
"logging": {
|
|
||||||
"applicationInsights": {
|
|
||||||
"samplingSettings": {
|
|
||||||
"isEnabled": true,
|
|
||||||
"excludedTypes": "Request"
|
|
||||||
},
|
|
||||||
"enableLiveMetricsFilters": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
{
|
|
||||||
"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": "*"
|
|
||||||
}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 79 KiB |
Before Width: | Height: | Size: 105 KiB |
Before Width: | Height: | Size: 94 KiB |
|
@ -1,72 +0,0 @@
|
||||||
[
|
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
|
@ -1,19 +0,0 @@
|
||||||
{
|
|
||||||
"$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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"$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 -->"
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
{
|
|
||||||
"$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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://developer.microsoft.com/json-schemas/core-build/sass.schema.json"
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/spfx-serve.schema.json",
|
|
||||||
"port": 4321,
|
|
||||||
"https": true,
|
|
||||||
"initialPage": "https://{tenantDomain}/_layouts/workbench.aspx"
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
|
|
||||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
'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'));
|
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
|
|
@ -1,14 +0,0 @@
|
||||||
export type EmployeeInfo = {
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
department: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type EmployeeOnboarding = EmployeeInfo & {
|
|
||||||
teamMembership: boolean;
|
|
||||||
notification: boolean;
|
|
||||||
processedOn: string;
|
|
||||||
completedOn: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
{
|
|
||||||
"$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."
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}
|
|
|
@ -1,143 +0,0 @@
|
||||||
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
|
|
||||||
})
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 12 KiB |
|
@ -1,62 +0,0 @@
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,32 +0,0 @@
|
||||||
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;
|
|
|
@ -1,54 +0,0 @@
|
||||||
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;
|
|
|
@ -1,13 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
@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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,131 +0,0 @@
|
||||||
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;
|
|
|
@ -1,25 +0,0 @@
|
||||||
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;
|
|
|
@ -1,36 +0,0 @@
|
||||||
@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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
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 <></>;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export * from "./WebPartTitle";
|
|
|
@ -1,17 +0,0 @@
|
||||||
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"
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,20 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 249 B |
|
@ -1,35 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
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**********"
|
|
|
@ -1,352 +0,0 @@
|
||||||
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: {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
|
@ -1,34 +0,0 @@
|
||||||
# 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
|
|
|
@ -1,16 +0,0 @@
|
||||||
!dist
|
|
||||||
config
|
|
||||||
|
|
||||||
gulpfile.js
|
|
||||||
|
|
||||||
release
|
|
||||||
src
|
|
||||||
temp
|
|
||||||
|
|
||||||
tsconfig.json
|
|
||||||
tslint.json
|
|
||||||
|
|
||||||
*.log
|
|
||||||
|
|
||||||
.yo-rc.json
|
|
||||||
.vscode
|
|
|
@ -1 +0,0 @@
|
||||||
v18.18.0
|
|
|
@ -1,21 +0,0 @@
|
||||||
{
|
|
||||||
"@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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,103 +0,0 @@
|
||||||
# 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" />
|
|
Before Width: | Height: | Size: 277 KiB |
|
@ -1,50 +0,0 @@
|
||||||
[
|
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
|
@ -1,18 +0,0 @@
|
||||||
{
|
|
||||||
"$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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"$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 -->"
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
{
|
|
||||||
"$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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://developer.microsoft.com/json-schemas/core-build/sass.schema.json"
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/spfx-serve.schema.json",
|
|
||||||
"port": 4321,
|
|
||||||
"https": true,
|
|
||||||
"initialPage": "https://{tenantDomain}/_layouts/workbench.aspx"
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
|
|
||||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
'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'));
|
|
||||||
|
|
|
@ -1,44 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
|
|
@ -1,35 +0,0 @@
|
||||||
{
|
|
||||||
"$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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,119 +0,0 @@
|
||||||
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,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 12 KiB |
|
@ -1,21 +0,0 @@
|
||||||
.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]";
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
define([], function () {
|
|
||||||
return {};
|
|
||||||
});
|
|
|
@ -1,6 +0,0 @@
|
||||||
declare interface IEnhancedButtonWebPartStrings {}
|
|
||||||
|
|
||||||
declare module "EnhancedButtonWebPartStrings" {
|
|
||||||
const strings: IEnhancedButtonWebPartStrings;
|
|
||||||
export = strings;
|
|
||||||
}
|
|
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 249 B |
|
@ -1,35 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
// 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"
|
||||||
|
}
|
|
@ -7,11 +7,9 @@ echo
|
||||||
echo -e "\e[1;94mGenerating dev certificate\e[0m"
|
echo -e "\e[1;94mGenerating dev certificate\e[0m"
|
||||||
gulp trust-dev-cert
|
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 ~/.gcb-serve-data/gcb-serve.cer ./spfx-dev-cert.cer
|
||||||
cp ~/.rushstack/rushstack-serve.pem ./spfx-dev-cert.pem
|
cp ~/.gcb-serve-data/gcb-serve.cer ./spfx-dev-cert.pem
|
||||||
|
|
||||||
## add *.cer to .gitignore to prevent certificates from being saved in repo
|
## add *.cer to .gitignore to prevent certificates from being saved in repo
|
||||||
if ! grep -Fxq '*.cer' ./.gitignore
|
if ! grep -Fxq '*.cer' ./.gitignore
|
|
@ -1,352 +0,0 @@
|
||||||
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: {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
|
@ -9,11 +9,9 @@ node_modules
|
||||||
# Build generated files
|
# Build generated files
|
||||||
dist
|
dist
|
||||||
lib
|
lib
|
||||||
release
|
|
||||||
solution
|
solution
|
||||||
temp
|
temp
|
||||||
*.sppkg
|
*.sppkg
|
||||||
.heft
|
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
# Coverage directory used by tools like istanbul
|
||||||
coverage
|
coverage
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
!dist
|
|
||||||
config
|
|
||||||
|
|
||||||
gulpfile.js
|
|
||||||
|
|
||||||
release
|
|
||||||
src
|
|
||||||
temp
|
|
||||||
|
|
||||||
tsconfig.json
|
|
||||||
tslint.json
|
|
||||||
|
|
||||||
*.log
|
|
||||||
|
|
||||||
.yo-rc.json
|
|
||||||
.vscode
|
|