Merge pull request #1593 from kunj-sangani/master

This commit is contained in:
Hugo Bernier 2020-11-07 16:41:01 -05:00 committed by GitHub
commit d927ec41da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 18310 additions and 0 deletions

View File

@ -0,0 +1,75 @@
# react-edit-application-customizers
## Summary
This web part will allow users to view/update application customizers properties across any web where the current user has access to. This web part can be helpful when we require to update the properties for application customizer without using any PowerShell script or cli tool.
![Web part in action](assets/react-all-applicationcustomizers.gif?raw=true "Webpart in action")
## Idea behind this web part
- SPFx Application customizer can be used to add scripts, and add custom html to well known placeholder(header and footer)
- We can use properties to pass data to Application customizers to make solution customizable.
- To update properties of application customizer there is no UI based solution.
- To update the title, details and other information of application customizer we use either PowerShell script or cli tool.
- This webpart can be used at a central location where all the users have access and if they require to update title, description and properties.
## Features
- Webpart to view/update Application Customizers registered for a selected web
- Provides two different UI Accordion or List based(configurable)
- Provides a dropdown to select the web from where we would require to fetch application customizers
- Allows to update application customizer properties which makes it easy to make re-useable application customizers
## Used SharePoint Framework Version
![SPFx 1.11](https://img.shields.io/badge/version-1.11-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)
## Solution
Solution|Author(s)
--------|---------
react-edit-applicationcustomizers | [Kunj Sangani](https://www.linkedin.com/in/kunj-sangani/) and [Siddharth Vaghasia](https://www.linkedin.com/in/siddharthvaghasia)
## Version history
Version|Date|Comments
-------|----|--------
1.0|October 16, 2020|Initial release
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Minimal Path to Awesome
- Clone this repository
- Ensure that you are at the solution folder
- in the command-line run:
- **npm install**
- **gulp serve**
For any issue or help, Buzz us on twitter:([sanganikunj](https://twitter.com/sanganikunj)) or ([siddh_me](https://twitter.com/siddh_me/))
> Sharing is caring!
## References
- [Getting started with SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
- [Building for Microsoft teams](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/build-for-teams-overview)
- [Use Microsoft Graph in your solution](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/using-microsoft-graph-apis)
- [Publish SharePoint Framework applications to the Marketplace](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/publish-to-marketplace-overview)
- [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - Guidance, tooling, samples and open-source controls for your Microsoft 365 development
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-edit-applicationcustomizers" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

View File

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

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
"deployCdnPath": "temp/deploy"
}

View File

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

View File

@ -0,0 +1,20 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-all-application-customizers-client-side-solution",
"id": "f560c809-b5a8-4320-abef-224f27d2d0f0",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"isDomainIsolated": false,
"developer": {
"name": "",
"websiteUrl": "",
"privacyUrl": "",
"termsOfUseUrl": "",
"mpnId": ""
}
},
"paths": {
"zippedPackage": "solution/react-all-application-customizers.sppkg"
}
}

View File

@ -0,0 +1,10 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://localhost:5432/workbench",
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

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,7 @@
'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.`);
build.initialize(require('gulp'));

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,43 @@
{
"name": "react-all-application-customizers",
"version": "0.0.1",
"private": true,
"main": "lib/index.js",
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@material-ui/core": "^4.11.0",
"@microsoft/sp-core-library": "1.11.0",
"@microsoft/sp-lodash-subset": "1.11.0",
"@microsoft/sp-office-ui-fabric-core": "1.11.0",
"@microsoft/sp-property-pane": "1.11.0",
"@microsoft/sp-webpart-base": "1.11.0",
"@pnp/sp": "^2.0.10",
"ace-builds": "^1.4.12",
"office-ui-fabric-react": "6.214.0",
"react": "16.8.5",
"react-ace": "^9.1.3",
"react-dom": "16.8.5"
},
"devDependencies": {
"@types/react": "16.8.8",
"@types/react-dom": "16.8.3",
"@microsoft/sp-build-web": "1.11.0",
"@microsoft/sp-tslint-rules": "1.11.0",
"@microsoft/sp-module-interfaces": "1.11.0",
"@microsoft/sp-webpart-workbench": "1.11.0",
"@microsoft/rush-stack-compiler-3.3": "0.3.5",
"gulp": "~3.9.1",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"ajv": "~5.2.2",
"@types/webpack-env": "1.13.1",
"@types/es6-promise": "0.0.33"
}
}

View File

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

View File

@ -0,0 +1,35 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "05567ead-69c5-4c36-958c-613a21cce18c",
"alias": "ApplicationCustomizersWebPart",
"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"
],
"preconfiguredEntries": [
{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": {
"default": "Other"
},
"title": {
"default": "ApplicationCustomizers"
},
"description": {
"default": "ApplicationCustomizers description"
},
"officeFabricIconFontName": "CustomizeToolbar",
"properties": {
"description": "ApplicationCustomizers",
"designType": "List"
}
}
]
}

View File

@ -0,0 +1,85 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
IPropertyPaneConfiguration,
PropertyPaneTextField,
PropertyPaneChoiceGroup
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import * as strings from 'ApplicationCustomizersWebPartStrings';
import ApplicationCustomizers from './components/ApplicationCustomizers';
import { IApplicationCustomizersProps } from './components/IApplicationCustomizersProps';
import { sp } from "@pnp/sp/presets/all";
export interface IApplicationCustomizersWebPartProps {
description: string;
designType: string;
}
export default class ApplicationCustomizersWebPart extends BaseClientSideWebPart<IApplicationCustomizersWebPartProps> {
protected onInit(): Promise<void> {
return super.onInit().then(_ => {
// other init code may be present
sp.setup({
spfxContext: this.context
});
});
}
public render(): void {
const element: React.ReactElement<IApplicationCustomizersProps> = React.createElement(
ApplicationCustomizers,
{
description: this.properties.description,
context: this.context,
designType: this.properties.designType
}
);
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: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('description', {
label: strings.DescriptionFieldLabel
}),
PropertyPaneChoiceGroup('designType', {
label: strings.DesignFieldLabel,
options: [
{ key: 'Accordion', checked: true, text: 'Accordion', iconProps: { officeFabricIconFontName: 'AutoFillTemplate' } },
{ key: 'List', text: 'List', iconProps: { officeFabricIconFontName: 'GroupedList' } }
]
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,84 @@
@import '~office-ui-fabric-react/dist/sass/References.scss';
.applicationCustomizers {
.container {
max-width: 1200px;
margin: 0px auto;
}
.row {
@include ms-Grid-row;
padding: 20px;
}
.column2 {
@include ms-Grid-col;
@include ms-lg2;
@include ms-xl2;
margin-top: 5px;
padding: 10px;
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl10;
margin-top: 5px;
padding: 10px;
}
.title {
@include ms-font-xl;
@include ms-fontColor-white;
}
.subTitle {
@include ms-font-l;
@include ms-fontColor-white;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.button {
// Our button
text-decoration: none;
height: 32px;
margin-top: 10px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
border-color: $ms-color-themePrimary;
color: $ms-color-white;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: $ms-font-size-m;
font-weight: $ms-font-weight-regular;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: $ms-font-weight-semibold;
font-size: $ms-font-size-m;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}
:global{
#workbenchPageContent{
max-width: 1200px;
}
}

View File

@ -0,0 +1,446 @@
import * as React from 'react';
import styles from './ApplicationCustomizers.module.scss';
import { IApplicationCustomizersProps } from './IApplicationCustomizersProps';
import { assign } from '@microsoft/sp-lodash-subset';
import ApplicationCustomizersService from "../service/ApplicationCustomizersService";
import { Dropdown, DropdownMenuItemType, IDropdownOption, IDropdownStyles } from 'office-ui-fabric-react/lib/Dropdown';
import MuiAccordion from '@material-ui/core/Accordion';
import MuiAccordionSummary from '@material-ui/core/AccordionSummary';
import MuiAccordionDetails from '@material-ui/core/AccordionDetails';
import { withStyles } from '@material-ui/core/styles';
import { DefaultButton, TextField, thProperties, Dialog, DialogFooter, DialogType, List, mergeStyleSets, getFocusStyle, ITheme, getTheme, IconButton, Panel, PanelType } from 'office-ui-fabric-react';
import AceEditor from "react-ace";
import "ace-builds/src-noconflict/mode-json";
import "ace-builds/src-noconflict/theme-github";
const Accordion = withStyles({
root: {
border: '1px solid rgba(0, 0, 0, .125)',
boxShadow: 'none',
'&:not(:last-child)': {
borderBottom: 0,
},
'&:before': {
display: 'none',
},
'&$expanded': {
margin: 'auto',
},
},
expanded: {},
})(MuiAccordion);
const AccordionSummary = withStyles({
root: {
backgroundColor: 'rgba(0, 0, 0, .03)',
borderBottom: '1px solid rgba(0, 0, 0, .125)',
marginBottom: -1,
minHeight: 56,
'&$expanded': {
minHeight: 56,
},
},
content: {
'&$expanded': {
margin: '12px 0',
},
},
expanded: {},
})(MuiAccordionSummary);
const AccordionDetails = withStyles((themes) => ({
root: {
padding: themes.spacing(2),
display: 'block'
},
}))(MuiAccordionDetails);
const theme: ITheme = getTheme();
const { palette, semanticColors, fonts } = theme;
const classNames = mergeStyleSets({
container: {
overflow: 'auto',
maxHeight: 500,
},
itemCell: [
getFocusStyle(theme, { inset: -1 }),
{
minHeight: 54,
padding: 10,
boxSizing: 'border-box',
borderBottom: `1px solid ${semanticColors.bodyDivider}`,
display: 'flex',
selectors: {
'&:hover': { background: palette.neutralLight },
},
},
],
itemImage: {
flexShrink: 0,
},
itemContent: {
marginLeft: 10,
overflow: 'hidden',
flexGrow: 1,
},
itemName: [
fonts.xLarge,
{
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
color: 'black'
},
],
itemIndex: {
fontSize: fonts.small.fontSize,
marginBottom: 10,
color: 'black'
},
chevron: {
alignSelf: 'center',
marginLeft: 10,
color: palette.neutralTertiary,
fontSize: fonts.large.fontSize,
flexShrink: 0,
},
});
const applicationCustomizersService = new ApplicationCustomizersService();
export interface IApplicationCustomizersState {
selectedItem: IDropdownOption;
dropdownSites: IDropdownOption[];
expanded: string | false;
allCustomizers: any;
previousEditIndex?: number;
editJSON?: { Title: string; Description: string; ClientSideComponentProperties: any };
hideDialog: boolean;
dialogContentProps?: any;
isPanelOpen: boolean;
itemInEdit?: number;
isViewPanelOpen: boolean;
viewJSON?: {
Title: string; Description: string; ClientSideComponentId: any;
ClientSideComponentProperties: any; Id: any;
};
}
export default class ApplicationCustomizers extends React.Component<IApplicationCustomizersProps, IApplicationCustomizersState> {
constructor(props: IApplicationCustomizersProps) {
super(props);
this.state = {
selectedItem: undefined,
dropdownSites: undefined,
expanded: 'panel1',
allCustomizers: [],
previousEditIndex: undefined,
hideDialog: true,
isPanelOpen: false,
isViewPanelOpen: false
};
}
public componentDidMount() {
applicationCustomizersService.fetchAllApplictionCustomizers(this.props.context.pageContext.web.absoluteUrl)
.then((allCustomizers) => {
allCustomizers = allCustomizers.map((cus) => { return assign(cus, { inEdit: false }); });
this.setState({ allCustomizers: allCustomizers });
}).catch((err) => {
console.log(err);
});
applicationCustomizersService.getAllSiteCollection()
.then((allSites) => {
let dropdownSites = allSites.PrimarySearchResults.map((val) => {
val['key'] = val['SPSiteUrl'];
val['text'] = `${val['Title']} - ${val['SPSiteUrl']}`;
return val;
});
this.setState({ dropdownSites: dropdownSites });
});
}
private onChange = (event: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => {
this.setState({ selectedItem: item });
applicationCustomizersService.fetchAllApplictionCustomizers(item.key as string)
.then((allCustomizers) => {
allCustomizers = allCustomizers.map((cus) => { return assign(cus, { inEdit: false }); });
this.setState({ allCustomizers: allCustomizers });
}).catch((err) => {
console.log(err);
});
}
public handleChange = (panel: string) => (event: React.ChangeEvent<{}>, newExpanded: boolean) => {
this.setState({ expanded: newExpanded ? panel : false });
}
public editCustomApplication = (index: number, inEdit: boolean) => {
let allCustomizers = this.state.allCustomizers;
allCustomizers[index].inEdit = inEdit;
if (this.state.previousEditIndex !== undefined && inEdit) {
allCustomizers[this.state.previousEditIndex].inEdit = false;
}
if (inEdit) {
this.setState({
isPanelOpen: this.props.designType === "List" ? true : false,
itemInEdit: index,
editJSON: {
Title: allCustomizers[index].Title,
Description: allCustomizers[index].Description,
ClientSideComponentProperties: allCustomizers[index].ClientSideComponentProperties
}
});
}
if (!inEdit) {
this.setState({ isPanelOpen: false });
}
this.setState({ allCustomizers: allCustomizers, previousEditIndex: index });
}
public onChangeJSON = (obj: string, newValue: string) => {
let { editJSON } = this.state;
editJSON[obj] = newValue;
this.setState({ editJSON });
}
public updateCustomizer = (index: number) => {
let webURL = this.state.selectedItem ? this.state.selectedItem.key : this.props.context.pageContext.web.absoluteUrl;
let { allCustomizers } = this.state;
applicationCustomizersService.updateApplicationCustomizer(webURL, this.state.allCustomizers[index].Id, this.state.editJSON)
.then(() => {
allCustomizers[index].inEdit = false;
this.setState({
allCustomizers: allCustomizers,
hideDialog: false,
isPanelOpen: false,
dialogContentProps: {
type: DialogType.normal,
title: 'Updated Successfully',
closeButtonAriaLabel: 'Close',
subText: 'Your Customizer is updated. Please refresh the page to look at the changes?'
}
});
}).catch((err) => {
this.setState({
hideDialog: false,
dialogContentProps: {
type: DialogType.normal,
title: 'Updat Error',
closeButtonAriaLabel: 'Close',
subText: 'There was some error while updating you customizer. Please try again'
}
});
});
}
private toggleHideDialog = () => {
applicationCustomizersService.fetchAllApplictionCustomizers(this.state.selectedItem ?
this.state.selectedItem.key as string : this.props.context.pageContext.web.absoluteUrl)
.then((allCustomizers) => {
allCustomizers = allCustomizers.map((cus) => { return assign(cus, { inEdit: false }); });
this.setState({ allCustomizers: allCustomizers, hideDialog: true });
}).catch((err) => {
console.log(err);
});
}
private viewCustomApplication = (index: number) => {
let { allCustomizers } = this.state;
this.setState({
isViewPanelOpen: true,
viewJSON: {
Title: allCustomizers[index].Title,
ClientSideComponentId: allCustomizers[index].ClientSideComponentId,
ClientSideComponentProperties: allCustomizers[index].ClientSideComponentProperties,
Description: allCustomizers[index].Description,
Id: allCustomizers[index].Id
}
});
}
private onRenderCell = (item: any, index: number, isScrolling: boolean): JSX.Element => {
return (
<div className={classNames.itemCell} data-is-focusable={true}>
<div className={classNames.itemContent}>
<div className={classNames.itemName}>{item.Title}</div>
<div className={classNames.itemIndex}>{item.Description}</div>
</div>
<IconButton iconProps={{ iconName: 'View' }} onClick={() => { this.viewCustomApplication(index); }} title="View" ariaLabel="View"></IconButton>
<IconButton iconProps={{ iconName: 'Edit' }} onClick={() => { this.editCustomApplication(index, true); }} title="Edit" ariaLabel="Edit"></IconButton>
</div>
);
}
public render(): React.ReactElement<IApplicationCustomizersProps> {
return (
<div className={styles.applicationCustomizers}>
<div className={styles.container}>
<div className={styles.row}>
<h1>{this.props.description}</h1>
</div>
<div className={styles.row}>
<Dropdown
label="Select Web"
selectedKey={this.state.selectedItem ? this.state.selectedItem.key : undefined}
onChange={this.onChange}
placeholder="Select an option"
options={this.state.dropdownSites}
/>
</div>
{this.props.designType === "Accordion" && this.state.allCustomizers.length !== 0 &&
<div className={styles.row}>
{this.state.allCustomizers.map((customizer, index) => {
return (
<Accordion square expanded={this.state.expanded === `panel${index + 1}`} onChange={this.handleChange(`panel${index + 1}`)}>
<AccordionSummary aria-controls="panel1d-content" id="panel1d-header">
<div>{customizer.Title}{customizer.Description && ` - ${customizer.Description}`}</div>
</AccordionSummary>
<AccordionDetails>
{!customizer.inEdit ?
<div>
<div className={styles.column2}>Component ID</div>
<div className={styles.column}>{customizer.ClientSideComponentId}</div>
<div className={styles.column2}>ID</div>
<div className={styles.column}>{customizer.Id}</div>
<div className={styles.column2}>Properties</div>
<div className={styles.column}>{customizer.ClientSideComponentProperties}</div>
</div> :
<div>
<div className={styles.column2}>Title</div>
<div className={styles.column}><TextField value={this.state.editJSON.Title}
onChange={(ev, newVal) => {
this.onChangeJSON("Title", newVal);
}} />
</div>
<div className={styles.column2}>Description</div>
<div className={styles.column}><TextField value={this.state.editJSON.Description} multiline rows={3}
onChange={(ev, newVal) => {
this.onChangeJSON("Description", newVal);
}} /></div>
<div className={styles.column2}>Properties</div>
<div className={styles.column}>
<AceEditor
placeholder="Placeholder Text"
mode="json"
theme="github"
onChange={(val) => { this.onChangeJSON("ClientSideComponentProperties", val); }}
fontSize={14}
style={{ height: 200, width: 790 }}
showPrintMargin={true}
showGutter={true}
highlightActiveLine={false}
value={this.state.editJSON.ClientSideComponentProperties}
setOptions={{
enableBasicAutocompletion: true,
enableLiveAutocompletion: false,
enableSnippets: false,
showLineNumbers: true,
tabSize: 2,
}} />
</div>
</div>
}
{!customizer.inEdit ?
<DefaultButton className={styles.button} text="Edit" onClick={() => { this.editCustomApplication(index, true); }} /> :
[<DefaultButton className={styles.button} text="Update" onClick={() => { this.updateCustomizer(index); }} />,
<DefaultButton style={{ marginLeft: 10, marginTop: 10 }} text="Cancel" onClick={() => { this.editCustomApplication(index, false); }} />]
}
</AccordionDetails>
</Accordion>
);
})}
</div>
}
{this.props.designType === "List" && this.state.allCustomizers.length !== 0 &&
<div className={styles.row}>
<List items={this.state.allCustomizers} onRenderCell={this.onRenderCell} />
</div>
}
{this.state.allCustomizers.length === 0 &&
<div className={styles.row}>We did not find any Application Customizers for the selected web</div>
}
<Dialog
hidden={this.state.hideDialog}
onDismiss={() => this.toggleHideDialog()}
dialogContentProps={this.state.dialogContentProps}
>
<DialogFooter>
<DefaultButton onClick={() => this.toggleHideDialog()} text="Cancel" />
</DialogFooter>
</Dialog>
<Panel
headerText="Edit Application Customizer"
isOpen={this.state.isPanelOpen}
onDismiss={() => this.setState({ isPanelOpen: false })}
closeButtonAriaLabel="Close"
type={PanelType.large}
>
{this.state.editJSON &&
<div className={styles.applicationCustomizers}>
<div className={styles.column2}>Title</div>
<div className={styles.column}><TextField value={this.state.editJSON.Title}
onChange={(ev, newVal) => {
this.onChangeJSON("Title", newVal);
}} />
</div>
<div className={styles.column2}>Description</div>
<div className={styles.column}><TextField value={this.state.editJSON.Description} multiline rows={3}
onChange={(ev, newVal) => {
this.onChangeJSON("Description", newVal);
}} /></div>
<div className={styles.column2}>Properties</div>
<div className={styles.column}>
<AceEditor
placeholder="Placeholder Text"
mode="json"
theme="github"
onChange={(val) => { this.onChangeJSON("ClientSideComponentProperties", val); }}
fontSize={14}
style={{ height: 200, width: 800 }}
showPrintMargin={true}
showGutter={true}
highlightActiveLine={false}
value={this.state.editJSON.ClientSideComponentProperties}
setOptions={{
enableBasicAutocompletion: true,
enableLiveAutocompletion: false,
enableSnippets: false,
showLineNumbers: true,
tabSize: 2,
}} />
</div>
<DefaultButton style={{ marginLeft: 10, marginTop: 10 }} className={styles.button} text="Update" onClick={() => { this.updateCustomizer(this.state.itemInEdit); }} />
<DefaultButton style={{ marginLeft: 10, marginTop: 10 }} text="Cancel" onClick={() => { this.editCustomApplication(this.state.itemInEdit, false); }} />
</div>}
</Panel>
<Panel
headerText="View Application Customizer"
isOpen={this.state.isViewPanelOpen}
onDismiss={() => this.setState({ isViewPanelOpen: false })}
closeButtonAriaLabel="Close"
type={PanelType.medium}
>{this.state.viewJSON &&
<div className={styles.applicationCustomizers}>
<div className={styles.column2}>Title</div>
<div className={styles.column}>{this.state.viewJSON.Title}</div>
<div className={styles.column2}>Description</div>
<div className={styles.column}>{this.state.viewJSON.Description ? this.state.viewJSON.Description : 'null'}</div>
<div className={styles.column2}>ComponentID</div>
<div className={styles.column}>{this.state.viewJSON.ClientSideComponentId}</div>
<div className={styles.column2}>ID</div>
<div className={styles.column}>{this.state.viewJSON.Id}</div>
<div className={styles.column2}>Properties</div>
<div className={styles.column}>{this.state.viewJSON.ClientSideComponentProperties}</div>
</div>}</Panel>
</div>
</div>
);
}
}

View File

@ -0,0 +1,7 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
export interface IApplicationCustomizersProps {
description: string;
context: WebPartContext;
designType: string;
}

View File

@ -0,0 +1,8 @@
define([], function () {
return {
"PropertyPaneDescription": "",
"BasicGroupName": "",
"DescriptionFieldLabel": "Title",
"DesignFieldLabel": "Choose design"
}
});

View File

@ -0,0 +1,11 @@
declare interface IApplicationCustomizersWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
DesignFieldLabel: string;
}
declare module 'ApplicationCustomizersWebPartStrings' {
const strings: IApplicationCustomizersWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,61 @@
import { sp } from "@pnp/sp";
import "@pnp/sp/webs";
import "@pnp/sp/user-custom-actions";
import { ISearchQuery } from "@pnp/sp/search";
import { Web } from "@pnp/sp/webs";
export default class ApplicationCustomizersService {
/**
* fetchAllApplictionCustomizers
*/
public fetchAllApplictionCustomizers = async (webURL: string) => {
let web = Web(webURL);
let response;
try {
response = await web.userCustomActions();
console.log(response);
//let temp = await sp.site.userCustomActions();
//console.log(temp);
} catch (error) {
console.log(error);
response = error;
}
return response;
}
/**
* getAllSiteCollection
*/
public getAllSiteCollection = async () => {
let response;
try {
response = await sp.search(<ISearchQuery>{
Querytext: "contentclass:STS_Site",
SelectProperties: ["Title", "SPSiteUrl", "WebTemplate"],
RowLimit: 1000,
TrimDuplicates: true
});
console.log(response.PrimarySearchResults);
} catch (error) {
console.log(error);
response = error;
}
return response;
}
/**
* updateApplicationCustomizer
*/
public updateApplicationCustomizer = async (webURL: string | number, selectedID: string, updateJSON: any) => {
let web = Web(webURL as string);
let response;
try {
response = await web.userCustomActions.getById(selectedID).update(updateJSON);
} catch (error) {
console.log(error);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

View File

@ -0,0 +1,39 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.3/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": [
"es6-promise",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx"
],
"exclude": [
"node_modules",
"lib"
]
}

View File

@ -0,0 +1,30 @@
{
"extends": "@microsoft/sp-tslint-rules/base-tslint.json",
"rules": {
"class-name": false,
"export-name": false,
"forin": false,
"label-position": false,
"member-access": true,
"no-arg": false,
"no-console": false,
"no-construct": false,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"variable-name": false,
"whitespace": false
}
}