Merge pull request #2958 from siddharth-vaghasia/master

added New webpart sample
This commit is contained in:
Hugo Bernier 2022-10-24 09:12:41 -04:00 committed by GitHub
commit 2098cc01a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 23708 additions and 0 deletions

View File

@ -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.15.0",
"image": "docker.io/m365pnp/spfx:1.15.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
],
"portsAttributes": {
"4321": {
"protocol": "https",
"label": "Manifest",
"onAutoForward": "silent",
"requireLocalPort": true
},
// Not needed for SPFx>= 1.12.1
// "5432": {
// "protocol": "https",
// "label": "Workbench",
// "onAutoForward": "silent"
// },
"35729": {
"protocol": "https",
"label": "LiveReload",
"onAutoForward": "silent",
"requireLocalPort": true
}
},
"postCreateCommand": "bash .devcontainer/spfx-startup.sh",
"remoteUser": "node"
}

View File

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

View File

@ -0,0 +1,5 @@
require('@rushstack/eslint-config/patch/modern-module-resolution');
module.exports = {
extends: ['@microsoft/eslint-config-spfx/lib/profiles/react'],
parserOptions: { tsconfigRootDir: __dirname }
};

View File

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

View File

@ -0,0 +1,16 @@
{
"@microsoft/generator-sharepoint": {
"plusBeta": false,
"isCreatingSolution": true,
"version": "1.15.0",
"libraryName": "react-add-formcustomizer-to-list",
"libraryId": "93fb58e4-6db3-4559-b98c-bd42f10a5fc6",
"environment": "spo",
"packageManager": "npm",
"solutionName": "react-add-formcustomizer-to-list",
"solutionShortDescription": "react-add-formcustomizer-to-list description",
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,125 @@
# Add Form Customizer to List
A React based SPFx utility web part which will help admins/user(s) to associate and remove association of the list form customizer extension to a particular list.
With SPFx version 1.15.1, we can now create new type of Extension as Form customizer which allows use to associate custom forms to SharePoint List.
As of writing this web part, this no direct way to associate this form customizer to SP list. We will have to either write PowerShell or Use REST API to associate it with the list.
This web part serves as utility so the developers can use to associate single form customizer with multiple lists with control over option to associate New/Edit/View form separately.
Note - This web part only serve to associate the Form customizer, so it is required that the actual SPFx Form Customizer solution is deployed and installed to targeted Site before association.
## WebPart in Action
![Web part in action](assets/webpartinaction-form.gif "Webpart in action")
### Highlights
* Option to Select Site->List->Content Type
* Option to choose associate either with one or more type of forms(New/Edit/View)
* Option to remove association of form
* Associate single customizer with multiple lists/forms
## Compatibility
![SPFx 1.15.0](https://img.shields.io/badge/SPFx-1.15.0-green.svg)
![Node.js v16 | v14 | v12](https://img.shields.io/badge/Node.js-v16%20%7C%20v14%20%7C%20v12-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://docs.microsoft.com/sharepoint/dev/spfx/sharepoint-framework-overview)
* [Microsoft 365 tenant](https://docs.microsoft.com/sharepoint/dev/spfx/set-up-your-development-environment)
> Get your own free development tenant by subscribing to [Microsoft 365 developer program](http://aka.ms/m365devprogram)
### Package and Deploy
Note - If you don't want to build and package on your own, you can directly download package at this [location](https://github.com/siddharth-vaghasia/public-docs/blob/master/react-add-formcustomizer-to-list.sppkg) and upload to app catalog and install app on required site collection. Skip below steps and directly go to How to use section.
Clone the solution and make sure there is no error before packaging. Try first on local work bench.
Change the `pageURL` property in `/config/serve.json` - This should be a valid modern page on your site collection.
```bash
git clone the repo
npm i
gulp serve
```
- Execute the following gulp task to bundle your solution. This executes a release build of your project by using a dynamic label as the host URL for your assets. This URL is automatically updated based on your tenant CDN settings:
```bash
gulp bundle --ship
```
- Execute the following task to package your solution. This creates an updated `react-add-formcustomizer-to-list.sppkg` package on the `sharepoint/solution` folder.
```bash
gulp package-solution --ship
```
- Upload or drag and drop the newly created client-side solution package to the app catalog in your tenant.
- Based on your tenant settings, if you would not have CDN enabled in your tenant, and the `includeClientSideAssets` setting would be true in the `package-solution.json`, the loading URL for the assets would be dynamically updated and pointing directly to the `ClientSideAssets` folder located in the app catalog site collection.
### How to Use Solution
* Once app is deployed to app catalog successfully
* Install app to required site collection
* Create new modern page. Add **react-add-formcustomizer-to-list** web part to page.
* Publish the page.
To do the association or removing the association , user needs to follow the below steps:
* Select the site from available sites
* Choose a list from the available options
* Choose the content type to which the form customizer needs to associate/remove association if its already associated
* User needs to grab client component id present in form customizer manifest `json` file (Information is added in the client component id section with image)
* Select the required check box option New Form/Edit Form/View Form
* After filling the required values, click on Associate/Remove Association button
* Once it is successful then go to respective list and check the forms
* Users can only associate/remove association of the form customizer to lists of the sites that they have access
You can copy the actual component ID of form customizer with from its `manifest.json` file.
## Solution
Solution|Author(s)
--------|---------
react-add-formcustomizer-to-list | [Siddharth Vaghasia](https://github.com/siddharth-vaghasia) (<https://www.linkedin.com/in/siddharthvaghasia/>)
## Version history
Version|Date|Comments
-------|----|--------
1.0.0|September 04, 2022|Initial release
## 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-add-formcustomizer-to-list%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-add-formcustomizer-to-list) 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-add-formcustomizer-to-list&template=bug-report.yml&sample=react-add-formcustomizer-to-list&authors=@siddharth-vaghasia&title=react-add-formcustomizer-to-list%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-add-formcustomizer-to-list&template=question.yml&sample=react-add-formcustomizer-to-list&authors=@siddharth-vaghasia&title=react-add-formcustomizer-to-list%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-add-formcustomizer-to-list&template=suggestion.yml&sample=react-add-formcustomizer-to-list&authors=@siddharth-vaghasia&title=react-add-formcustomizer-to-list%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.**
For any issue or help, Buzz me on twitter:([siddh_me](https://twitter.com/siddh_me/))
> Sharing is caring!
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-add-formcustomizer-to-list" />

View File

@ -0,0 +1,50 @@
[
{
"name": "pnp-sp-dev-spfx-web-parts-react-add-formcustomizer-to-list",
"source": "pnp",
"title": "Add Form Customizer to List",
"shortDescription": "A React based SPFx utility web part which will help admins/user(s) to associate and remove association of the list form customizer extension to a particular list.",
"url": "https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-add-formcustomizer-to-list",
"downloadUrl": "https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-add-formcustomizer-to-list",
"longDescription": [
"A react based SPFx utility web part which will help admins/user(s) to associate and remove association of the list form customizer extension to a particular list."
],
"creationDateTime": "2022-09-05",
"updateDateTime": "2022-09-05",
"products": [
"SharePoint"
],
"metadata": [
{
"key": "CLIENT-SIDE-DEV",
"value": "React"
},
{
"key": "SPFX-VERSION",
"value": "1.15"
}
],
"thumbnails": [
{
"type": "image",
"order": 100,
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-add-formcustomizer-to-list/assets/webpartinaction-form.gif",
"alt": "Web Part Preview"
}
],
"authors": [
{
"gitHubAccount": "siddharth-vaghasia",
"pictureUrl": "https://github.com/siddharth-vaghasia.png",
"name": "Siddharth Vaghasia"
}
],
"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://learn.microsoft.com/sharepoint/dev/spfx/web-parts/get-started/build-a-hello-world-web-part"
}
]
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

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

View File

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

View File

@ -0,0 +1,40 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-add-formcustomizer-to-list-client-side-solution",
"id": "93fb58e4-6db3-4559-b98c-bd42f10a5fc6",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"developer": {
"name": "",
"websiteUrl": "",
"privacyUrl": "",
"termsOfUseUrl": "",
"mpnId": "Undefined-1.15.0"
},
"metadata": {
"shortDescription": {
"default": "react-add-formcustomizer-to-list description"
},
"longDescription": {
"default": "react-add-formcustomizer-to-list description"
},
"screenshotPaths": [],
"videoUrl": "",
"categories": []
},
"features": [
{
"title": "react-add-formcustomizer-to-list Feature",
"description": "The feature that activates elements of the react-add-formcustomizer-to-list solution.",
"id": "b16b826f-d607-4f4a-a67e-0f42ed12f031",
"version": "1.0.0.0"
}
]
},
"paths": {
"zippedPackage": "solution/react-add-formcustomizer-to-list.sppkg"
}
}

View File

@ -0,0 +1,6 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://xr4vy.sharepoint.com/sites/Retail/_layouts/workbench.aspx"
}

View File

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

View File

@ -0,0 +1,16 @@
'use strict';
const build = require('@microsoft/sp-build-web');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
var getTasks = build.rig.getTasks;
build.rig.getTasks = function () {
var result = getTasks.call(build.rig);
result.set('serve', result.get('serve-deprecated'));
return result;
};
build.initialize(require('gulp'));

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,39 @@
{
"name": "react-add-formcustomizer-to-list",
"version": "0.0.1",
"private": true,
"main": "lib/index.js",
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@microsoft/sp-core-library": "1.15.2",
"@microsoft/sp-lodash-subset": "1.15.2",
"@microsoft/sp-office-ui-fabric-core": "1.15.2",
"@microsoft/sp-property-pane": "1.15.2",
"@microsoft/sp-webpart-base": "1.15.2",
"@pnp/logging": "^3.5.1",
"@pnp/sp": "^3.5.1",
"@pnp/spfx-controls-react": "3.10.0",
"office-ui-fabric-react": "7.185.7",
"react": "16.13.1",
"react-dom": "16.13.1",
"tslib": "2.3.1"
},
"devDependencies": {
"@microsoft/rush-stack-compiler-4.5": "0.2.2",
"@rushstack/eslint-config": "2.5.1",
"@microsoft/eslint-plugin-spfx": "1.15.2",
"@microsoft/eslint-config-spfx": "1.15.2",
"@microsoft/sp-build-web": "1.15.2",
"@types/webpack-env": "~1.15.2",
"ajv": "^6.12.5",
"gulp": "4.0.2",
"@types/react": "16.9.51",
"@types/react-dom": "16.9.8",
"eslint-plugin-react-hooks": "4.3.0",
"@microsoft/sp-module-interfaces": "1.15.2"
}
}

View File

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

View File

@ -0,0 +1,28 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "b07808d4-15db-4988-be7b-73c5777838d7",
"alias": "ReactAddformcustomizertolistWebPart",
"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", // Other
"group": { "default": "Other" },
"title": { "default": "react-addformcustomizertolist" },
"description": { "default": "react-addformcustomizertolist description" },
"officeFabricIconFontName": "Page",
"properties": {
"description": "react-addformcustomizertolist"
}
}]
}

View File

@ -0,0 +1,109 @@
import * as React from "react";
import * as ReactDom from "react-dom";
import { Version } from "@microsoft/sp-core-library";
import {
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 "ReactAddformcustomizertolistWebPartStrings";
import ReactAddformcustomizertolist from "./components/ReactAddformcustomizertolist";
import { IReactAddformcustomizertolistProps } from "./components/IReactAddformcustomizertolistProps";
import { getSP } from "./pnpjsConfig";
export interface IReactAddformcustomizertolistWebPartProps {
description: string;
}
export default class ReactAddformcustomizertolistWebPart extends BaseClientSideWebPart<IReactAddformcustomizertolistWebPartProps> {
private _isDarkTheme = false;
private _environmentMessage = "";
public render(): void {
const element: React.ReactElement<IReactAddformcustomizertolistProps> =
React.createElement(ReactAddformcustomizertolist, {
description: this.properties.description,
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> {
this._environmentMessage = this._getEnvironmentMessage();
getSP(this.context);
return super.onInit();
}
private _getEnvironmentMessage(): string {
if (!this.context.sdks.microsoftTeams) {
// running in Teams
return this.context.isServedFromLocalhost
? strings.AppLocalEnvironmentTeams
: strings.AppTeamsTabEnvironment;
}
return this.context.isServedFromLocalhost
? strings.AppLocalEnvironmentSharePoint
: strings.AppSharePointEnvironment;
}
protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void {
if (!currentTheme) {
return;
}
this._isDarkTheme = !!currentTheme.isInverted;
const { semanticColors } = currentTheme;
if (semanticColors) {
this.domElement.style.setProperty(
"--bodyText",
semanticColors.bodyText || null
);
this.domElement.style.setProperty("--link", semanticColors.link || null);
this.domElement.style.setProperty(
"--linkHovered",
semanticColors.linkHovered || null
);
}
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse("1.0");
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription,
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField("description", {
label: strings.DescriptionFieldLabel,
}),
],
},
],
},
],
};
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

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

View File

@ -0,0 +1,20 @@
import { ISite } from "@pnp/spfx-controls-react/lib/controls/sitePicker/ISitePicker";
import { IDropdownOption } from "office-ui-fabric-react/lib/Dropdown";
export interface IReactAddformcustomizertolistState {
siteUrl: string;
sites: ISite[];
errors: string[];
contentTypes: IDropdownOption[];
NewForm: boolean;
EditForm: boolean;
ViewForm: boolean;
disabled: boolean;
selectedContnetType: string;
selectedList: string;
clientComponentID: string;
isCalloutVisible: boolean;
userMessage: string;
hideDialog: boolean;
chkCustomSiteUrl: boolean;
}

View File

@ -0,0 +1,40 @@
@import '~office-ui-fabric-react/dist/sass/References.scss';
.reactAddformcustomizertolist {
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
}
}
}
.headeClass {
text-align: center;
font-Weight: bold;
}

View File

@ -0,0 +1,552 @@
import * as React from "react";
import styles from "./ReactAddformcustomizertolist.module.scss";
import { IReactAddformcustomizertolistProps } from "./IReactAddformcustomizertolistProps";
import { SPFI } from "@pnp/sp";
import { getSP } from "../pnpjsConfig";
import { Logger, LogLevel } from "@pnp/logging";
import { Label } from "@microsoft/office-ui-fabric-react-bundle";
import "@pnp/sp/content-types/list";
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import { SPHttpClient, SPHttpClientResponse } from "@microsoft/sp-http";
import { ISite, SitePicker } from "@pnp/spfx-controls-react/lib/SitePicker";
import { ListPicker } from "@pnp/spfx-controls-react/lib/ListPicker";
import { IReactAddformcustomizertolistState } from "./IReactAddformcustomizertolistState";
import { Dropdown, IDropdownOption } from "office-ui-fabric-react/lib/Dropdown";
import { IContentTypeInfo } from "@pnp/sp/content-types/types";
import { IWeb, Web } from "@pnp/sp/webs";
import { TextField } from "office-ui-fabric-react/lib/TextField";
import {
IStackStyles,
IStackTokens,
Stack,
} from "office-ui-fabric-react/lib/Stack";
import {
DefaultButton,
IButtonStyles,
IconButton,
} from "office-ui-fabric-react/lib/Button";
import { Callout } from "office-ui-fabric-react/lib/Callout";
import { Checkbox } from "office-ui-fabric-react/lib/Checkbox";
import { Image } from "office-ui-fabric-react/lib/Image";
import Dialog, {
DialogFooter,
DialogType,
IDialogContentProps,
} from "office-ui-fabric-react/lib/Dialog";
import { IIconProps, IModalProps } from "office-ui-fabric-react";
import { IList } from "@pnp/sp/lists";
// const iconClass = mergeStyles({
// fontSize: 20,
// height: 12,
// width: 12,
// margin: "5px 25px",
// });
const stackTokens: IStackTokens = {
childrenGap: 10,
};
const chkstackTokens: IStackTokens = {
childrenGap: 6,
};
const labelCalloutStackStyles: Partial<IStackStyles> = {
root: { padding: 20 },
};
const iconButtonStyles: Partial<IButtonStyles> = { root: { marginBottom: -3 } };
const iconProps: IIconProps = { iconName: "Info" };
const modelProps: IModalProps = {
isBlocking: false,
styles: { main: { maxWidth: 450 } },
};
export interface IBodyObject {
NewFormClientSideComponentId?: string;
EditFormClientSideComponentId?: string;
DisplayFormClientSideComponentId?: string;
}
export default class ReactAddformcustomizertolist extends React.Component<
IReactAddformcustomizertolistProps,
IReactAddformcustomizertolistState
> {
private _LOG_SOURCE = "ReactAddformcustomizertolist";
private _sp: SPFI;
private _dialogContentProps: IDialogContentProps = {
type: DialogType.largeHeader,
title: "Information!",
subText: "",
};
public constructor(props: IReactAddformcustomizertolistProps) {
super(props);
this.state = {
sites: [],
siteUrl: "",
errors: [],
contentTypes: [],
NewForm: false,
EditForm: false,
ViewForm: false,
disabled: false,
selectedContnetType: "",
selectedList: "",
clientComponentID: "",
isCalloutVisible: false,
userMessage: "",
hideDialog: true,
chkCustomSiteUrl: false,
};
this._sp = getSP();
this.onListPickerChange = this.onListPickerChange.bind(this);
this.CTTypeChanged = this.CTTypeChanged.bind(this);
this.onNewFormChange = this.onNewFormChange.bind(this);
this.onEditFormChange = this.onEditFormChange.bind(this);
this.onViewFormChange = this.onViewFormChange.bind(this);
this.addFormCustomizer = this.addFormCustomizer.bind(this);
this.removeFormCustomizer = this.removeFormCustomizer.bind(this);
this.handleCCIDChange = this.handleCCIDChange.bind(this);
this.toggleIsCalloutVisible = this.toggleIsCalloutVisible.bind(this);
this.toggleHideDialog = this.toggleHideDialog.bind(this);
this.onCustomSiteUrlChange = this.onCustomSiteUrlChange.bind(this);
this.handleCustomSiteUrlChange = this.handleCustomSiteUrlChange.bind(this);
this.SiteSelectionChange = this.SiteSelectionChange.bind(this);
}
public render(): React.ReactElement<IReactAddformcustomizertolistProps> {
try {
const { hasTeamsContext } = this.props;
return (
<section
className={`${styles.reactAddformcustomizertolist} ${
hasTeamsContext ? styles.teams : ""
}`}
>
<h1 className={styles.headeClass}>Add form customizer to list</h1>
<Stack tokens={stackTokens}>
{!this.state.chkCustomSiteUrl && (
<SitePicker
context={this.props.context}
label={"Select the site"}
mode={"site"}
allowSearch={true}
multiSelect={false}
onChange={this.SiteSelectionChange}
// onChange={(sites) => {
// console.log(sites);
// this.setState({ siteUrl: sites[0].url });
// this.setState({ sites: sites });
// this.setState({ contentTypes: [] });
// }}
placeholder={"Select the site"}
searchPlaceholder={"Choose the site"}
selectedSites={this.state.sites}
initialSites={this.state.sites}
/>
)}
{this.state.siteUrl && (
<Label>{`Selected site url: ${this.state.siteUrl}`}</Label>
)}
<Checkbox
label="Custom Site"
value={"Custom Site"}
checked={this.state.chkCustomSiteUrl}
onChange={this.onCustomSiteUrlChange}
/>
{this.state.chkCustomSiteUrl && (
<>
<Label>Enter site url</Label>
<TextField
value={this.state.siteUrl}
onChange={this.handleCustomSiteUrlChange}
// onChange={(e) => {
// this.handleCustomSiteUrlChange(e);
// }}
/>
</>
)}
<ListPicker
context={this.props.context}
label="Select the list"
placeHolder="Select the list"
baseTemplate={100}
includeHidden={false}
multiSelect={false}
webAbsoluteUrl={this.state.siteUrl}
onSelectionChanged={this.onListPickerChange}
selectedList={this.state.selectedList}
disabled={!this.state.siteUrl}
/>
<Dropdown
label="Select a content type"
placeholder="Select a content type..."
onChange={this.CTTypeChanged}
options={this.state.contentTypes}
required={true}
selectedKey={this.state.selectedContnetType}
/>
<Stack horizontal tokens={chkstackTokens}>
<Label required={true}>Client Component ID </Label>
<IconButton
id={"iconButtonId"}
iconProps={iconProps}
title="Info"
ariaLabel="Info"
onClick={this.toggleIsCalloutVisible}
styles={iconButtonStyles}
/>
{this.state.isCalloutVisible && (
<Callout
target={"#iconButtonId"}
setInitialFocus
onDismiss={this.toggleIsCalloutVisible}
ariaDescribedBy={"description"}
role="alertdialog"
>
<Stack
tokens={stackTokens}
horizontalAlign="start"
styles={labelCalloutStackStyles}
>
<Image
src={require("../assets/ClientComponentID.png")}
alt="Client Component ID"
height={400}
width={500}
/>
<span id={"description"}>
{`
Enter the 'Client Component ID' present in form customizer
manifest json file.`}
</span>
<DefaultButton onClick={this.toggleIsCalloutVisible}>
Close
</DefaultButton>
</Stack>
</Callout>
)}
</Stack>
<TextField
value={this.state.clientComponentID}
onChange={this.handleCCIDChange}
// onChange={(e) => {
// this.handleCCIDChange(e);
// }}
/>
<Label>Select the required form to associate the customizer</Label>
<Stack horizontal tokens={chkstackTokens}>
<Checkbox
label="New Form"
value={"New Form"}
checked={this.state.NewForm}
onChange={this.onNewFormChange}
/>
<Checkbox
label="Edit Form"
value={"Edit Form"}
checked={this.state.EditForm}
onChange={this.onEditFormChange}
/>
<Checkbox
label="View Form"
value={"View Form"}
checked={this.state.ViewForm}
onChange={this.onViewFormChange}
/>
</Stack>
<Stack horizontal tokens={stackTokens}>
<DefaultButton
text="Associate"
onClick={this.addFormCustomizer}
allowDisabledFocus
disabled={this.state.disabled}
/>
<DefaultButton
text="Remove Association"
onClick={this.removeFormCustomizer}
allowDisabledFocus
disabled={this.state.disabled}
/>
</Stack>
<Stack>
<Dialog
hidden={this.state.hideDialog}
onDismiss={this.toggleHideDialog}
dialogContentProps={this._dialogContentProps}
modalProps={modelProps}
>
<Label>{this.state.userMessage}</Label>
<DialogFooter>
<DefaultButton onClick={this.toggleHideDialog} text="Close" />
</DialogFooter>
</Dialog>
</Stack>
</Stack>
</section>
);
} catch (err) {
Logger.write(
`${this._LOG_SOURCE} (render) - ${JSON.stringify(err)} - `,
LogLevel.Error
);
}
return null;
}
public SiteSelectionChange(sites: ISite[]): void {
console.log(sites);
this.setState({ siteUrl: sites[0].url });
this.setState({ sites: sites });
this.setState({ contentTypes: [] });
}
public toggleHideDialog(): void {
//this.dialogContentProps.subText = this.state.userMessage;
this.setState({ hideDialog: !this.state.hideDialog });
}
public toggleIsCalloutVisible(): void {
this.setState({ isCalloutVisible: !this.state.isCalloutVisible });
}
public handleCustomSiteUrlChange(
e: React.ChangeEvent<HTMLInputElement>
): void {
this.setState({ siteUrl: e.target.value });
}
public handleCCIDChange(e: React.ChangeEvent<HTMLInputElement>): void {
this.setState({ clientComponentID: e.target.value });
}
public async onListPickerChange(list: string): Promise<void> {
try {
this.setState({ selectedList: list });
this.setState({ contentTypes: [] });
await this._getContentTypes(list);
} catch (err) {
Logger.write(
`${this._LOG_SOURCE} (onListPickerChange) - ${JSON.stringify(err)} - `,
LogLevel.Error
);
}
}
private _getContentTypes = async (
listNameorListId: string
): Promise<void> => {
try {
const ctTypes: { key: string; text: string }[] = [];
ctTypes.push({ key: "", text: "" });
const web: IWeb = Web([this._sp.web, this.state.siteUrl]);
const list: IList = web.lists.getById(listNameorListId);
const listCTTypes: IContentTypeInfo[] = await list.contentTypes();
for await (const currentCTType of listCTTypes) {
const id: string = currentCTType.Id.StringValue.toString();
ctTypes.push({ key: id, text: currentCTType.Name });
}
this.setState({ contentTypes: ctTypes });
} catch (err) {
Logger.write(
`${this._LOG_SOURCE} (_getContentTypes) - ${JSON.stringify(err)} - `,
LogLevel.Error
);
}
};
public CTTypeChanged(
ev: React.FormEvent<HTMLDivElement>,
item: IDropdownOption
): void {
this.setState({ selectedContnetType: item.key ? item.key.toString() : "" });
}
public reloadWebpart = (): void => {
this.setState({
sites: [],
siteUrl: "",
errors: [],
contentTypes: [],
NewForm: false,
EditForm: false,
ViewForm: false,
disabled: false,
selectedContnetType: null,
selectedList: null,
clientComponentID: "",
isCalloutVisible: false,
chkCustomSiteUrl: false,
//userMessage: "",
//hideDialog: true,
});
};
public async addFormCustomizer(): Promise<boolean> {
const isValid: boolean = this.validedFormFields();
try {
if (isValid) {
const result: SPHttpClientResponse = await this.addremoveFormCustomizer(
"add"
);
if (!result.ok) {
Logger.write(
`Could not update content type - ${this._LOG_SOURCE}`,
LogLevel.Error
);
return false;
} else {
//alert("Associated the form customiser with the selected list");
//this.dialogContentProps.subText = "Associated the form customiser with the selected list.";
this.setState({
hideDialog: false,
userMessage:
"Associated the form customiser with the selected list.",
});
//Modal dialog
this.reloadWebpart();
}
} else {
//alert("Enter all the required fields");
// this.dialogContentProps.subText = "Enter all the required fields.";
this.setState({
hideDialog: false,
userMessage: "Enter all the required fields.",
});
}
} catch (err) {
Logger.write(
`${this._LOG_SOURCE} (addFormCustomizer) - ${JSON.stringify(err)} - `,
LogLevel.Error
);
}
}
public validedFormFields(): boolean {
let isFormValid = false;
if (
this.state.siteUrl &&
this.state.selectedList &&
this.state.clientComponentID &&
this.state.selectedContnetType &&
(this.state.NewForm || this.state.EditForm || this.state.ViewForm)
) {
isFormValid = true;
}
return isFormValid;
}
public async removeFormCustomizer(): Promise<boolean> {
const isValid: boolean = this.validedFormFields();
try {
if (isValid) {
const result: SPHttpClientResponse = await this.addremoveFormCustomizer(
"remove"
);
if (!result.ok) {
Logger.write(
`Could not update content type - ${this._LOG_SOURCE}`,
LogLevel.Error
);
return false;
} else {
//alert("Removed the associated form customiser from the selected list");
// this.dialogContentProps.subText = "Removed the associated form customiser from the selected list.";
this.setState({
hideDialog: false,
userMessage:
"Removed the associated form customiser from the selected list.",
});
this.reloadWebpart();
}
} else {
// alert("Enter all the required fields");
//this.dialogContentProps.subText = "Enter all the required fields.";
this.setState({
hideDialog: false,
userMessage: "Enter all the required fields.",
});
}
} catch (err) {
Logger.write(
`${this._LOG_SOURCE} (addFormCustomizer) - ${JSON.stringify(err)} - `,
LogLevel.Error
);
}
}
public async addremoveFormCustomizer(
addorremove: string
): Promise<SPHttpClientResponse> {
const web: IWeb = Web([this._sp.web, this.state.siteUrl]);
//conext
const ctUrl: string = await web.lists
.getById(this.state.selectedList)
.contentTypes.getById(this.state.selectedContnetType)
.toUrl();
const bodyObj: IBodyObject = {};
if (this.state.NewForm) {
bodyObj.NewFormClientSideComponentId =
addorremove === "add" ? this.state.clientComponentID : "";
}
if (this.state.EditForm) {
bodyObj.EditFormClientSideComponentId =
addorremove === "add" ? this.state.clientComponentID : "";
}
if (this.state.ViewForm) {
bodyObj.DisplayFormClientSideComponentId =
addorremove === "add" ? this.state.clientComponentID : "";
}
let result: SPHttpClientResponse = null;
result = await this.props.context.spHttpClient.fetch(
`${ctUrl}`,
SPHttpClient.configurations.v1,
{
method: "PATCH",
body: JSON.stringify(bodyObj),
}
);
return result;
}
public onCustomSiteUrlChange(
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
isChecked?: boolean
): void {
this.setState({ chkCustomSiteUrl: isChecked });
}
public onNewFormChange(
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
isChecked?: boolean
): void {
this.setState({ NewForm: isChecked });
}
public onEditFormChange(
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
isChecked?: boolean
): void {
this.setState({ EditForm: isChecked });
}
public onViewFormChange(
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
isChecked?: boolean
): void {
this.setState({ ViewForm: isChecked });
}
}

View File

@ -0,0 +1,20 @@
// create File item to work with it internally
export interface IFile {
Id: number;
Title: string;
Name: string;
Size: number;
}
// create PnP JS response interface for File
export interface IResponseFile {
Length: number;
}
// create PnP JS response interface for Item
export interface IResponseItem {
Id: number;
File: IResponseFile;
FileLeafRef: string;
Title: string;
}

View File

@ -0,0 +1,11 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field",
"AppLocalEnvironmentSharePoint": "The app is running on your local environment as SharePoint web part",
"AppLocalEnvironmentTeams": "The app is running on your local environment as Microsoft Teams app",
"AppSharePointEnvironment": "The app is running on SharePoint page",
"AppTeamsTabEnvironment": "The app is running in Microsoft Teams"
}
});

View File

@ -0,0 +1,14 @@
declare interface IReactAddformcustomizertolistWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
AppLocalEnvironmentSharePoint: string;
AppLocalEnvironmentTeams: string;
AppSharePointEnvironment: string;
AppTeamsTabEnvironment: string;
}
declare module 'ReactAddformcustomizertolistWebPartStrings' {
const strings: IReactAddformcustomizertolistWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,20 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
// import pnp and pnp logging system
import { spfi, SPFI, SPFx } from "@pnp/sp";
import { LogLevel, PnPLogging } from "@pnp/logging";
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";
import "@pnp/sp/batching";
let _sp: SPFI = null;
export const getSP = (context?: WebPartContext): SPFI => {
if (_sp === null && context !== null) {
//You must add the @pnp/logging package to include the PnPLogging behavior it is no longer a peer dependency
// The LogLevel set's at what level a message will be written to the console
_sp = spfi().using(SPFx(context)).using(PnPLogging(LogLevel.Warning));
}
return _sp;
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 B

View File

@ -0,0 +1,35 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-4.5/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,
"strictNullChecks": false,
"noUnusedLocals": false,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection",
"es2015.promise"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx"
]
}