react-admin-sc-catalog-pnpjs - Site Collection App Catalogs Summary View (#1058)

* Initial commit

* Implemented business logic and UI

* UI, Readme.md, bugfixing

* Update README.md

* removed unuseful comments

* Code Review
This commit is contained in:
Federico 2019-12-09 09:10:51 +00:00 committed by Vesa Juvonen
parent e204ea6e44
commit 43cbbe2d71
30 changed files with 19267 additions and 0 deletions

View File

@ -0,0 +1,25 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# change these settings to your own preference
indent_style = space
indent_size = 2
# we recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[{package,bower}.json]
indent_style = space
indent_size = 2

View File

@ -0,0 +1,32 @@
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
node_modules
# Build generated files
dist
lib
solution
temp
*.sppkg
# Coverage directory used by tools like istanbul
coverage
# OSX
.DS_Store
# Visual Studio files
.ntvs_analysis.dat
.vs
bin
obj
# Resx Generated Code
*.resx.ts
# Styles Generated Code
*.scss.ts

View File

@ -0,0 +1,5 @@
{
"recommendations": [
"msjsdiag.debugger-for-chrome"
]
}

View File

@ -0,0 +1,43 @@
{
/**
* Install Chrome Debugger Extension for Visual Studio Code to debug your components with the
* Chrome browser: https://aka.ms/spfx-debugger-extensions
*/
"version": "0.2.0",
"configurations": [{
"name": "Local workbench",
"type": "chrome",
"request": "launch",
"url": "https://localhost:4321/temp/workbench.html",
"webRoot": "${workspaceRoot}",
"sourceMaps": true,
"sourceMapPathOverrides": {
"webpack:///.././src/*": "${webRoot}/src/*",
"webpack:///../../../src/*": "${webRoot}/src/*",
"webpack:///../../../../src/*": "${webRoot}/src/*",
"webpack:///../../../../../src/*": "${webRoot}/src/*"
},
"runtimeArgs": [
"--remote-debugging-port=9222"
]
},
{
"name": "Hosted workbench",
"type": "chrome",
"request": "launch",
"url": "https://enter-your-SharePoint-site/_layouts/workbench.aspx",
"webRoot": "${workspaceRoot}",
"sourceMaps": true,
"sourceMapPathOverrides": {
"webpack:///.././src/*": "${webRoot}/src/*",
"webpack:///../../../src/*": "${webRoot}/src/*",
"webpack:///../../../../src/*": "${webRoot}/src/*",
"webpack:///../../../../../src/*": "${webRoot}/src/*"
},
"runtimeArgs": [
"--remote-debugging-port=9222",
"-incognito"
]
}
]
}

View File

@ -0,0 +1,13 @@
// Place your settings in this file to overwrite default and user settings.
{
// Configure glob patterns for excluding files and folders in the file explorer.
"files.exclude": {
"**/.git": true,
"**/.DS_Store": true,
"**/bower_components": true,
"**/coverage": true,
"**/lib-amd": true,
"src/**/*.scss.ts": true
},
"typescript.tsdk": ".\\node_modules\\typescript\\lib"
}

View File

@ -0,0 +1,12 @@
{
"@microsoft/generator-sharepoint": {
"isCreatingSolution": true,
"environment": "spo",
"version": "1.9.1",
"libraryName": "react-admin-sc-catalog-pnpjs",
"libraryId": "59954bf3-e8a7-4def-933b-c1b618e0ab19",
"packageManager": "npm",
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,61 @@
# react-admin-sc-catalog-pnpjs - Site Collection App Catalogs Summary View.
## Summary
A SPFx WebPart using [@pnp/sp/appcatalog](https://pnp.github.io/pnpjs/sp/docs/alm/) and [@pnp/spfx-controls-react](https://sharepoint.github.io/sp-dev-fx-controls-react/). It allows to see in a single view all the SiteCollection catalogs and the Apps installed with some useful metadata.
It needs Globlal Administrator or SharePoint Online Administrator permissions in order to access Site collection App Catalogs hidden list (https://yourtenant.sharepoint.com/sites/appcatalog/Lists/SiteCollectionAppCatalogs) and each Site Collection App Catalog. This WebPart use [@pnp/spfx-controls-react WebPartTitle](https://sharepoint.github.io/sp-dev-fx-controls-react/controls/WebPartTitle/) and [@pnp/spfx-controls-react ListView](https://sharepoint.github.io/sp-dev-fx-controls-react/controls/ListView/) components.
## react-admin-sc-catalog-pnpjs in action
![WebPartInAction](./assets/react-admin-sc-catalog-pnpjs-webpart-animated.gif)
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/version-1.9.1-green.svg)
## Applies to
* [SharePoint Framework](https:/dev.office.com/sharepoint)
* [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment)
## Solution
Solution|Author(s)
--------|---------
react-admin-sc-catalog-pnpjs | [Federico Porceddu](https://www.federicoporceddu.com)
## Version history
Version|Date|Comments
-------|----|--------
1.0|November 07, 2019|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
* in the command line run:
* restore dependencies: `npm install`
* build solution `gulp build --ship`
* bundle solution: `gulp bundle --ship`
* package solution: `gulp package-solution --ship`
* locate solution at `.\sharepoint\solution\react-admin-sc-catalog-pnpjs.sppkg`
* upload it to your tenant app catalog
* add `react-admin-sc-catalog-pnpjs` app to your site
* add `react-admin-sc-catalog-pnpjs` webpart to your page to see it in action
## Features
This Web Part illustrates the following concepts on top of the SharePoint Framework:
* How to use [@pnp/sp/appcatalog](https://pnp.github.io/pnpjs/sp/docs/alm/).
* How to use [@pnp/spfx-controls-react WebPartTitle](https://sharepoint.github.io/sp-dev-fx-controls-react/controls/WebPartTitle/)
* How to use [@pnp/spfx-controls-react ListView](https://sharepoint.github.io/sp-dev-fx-controls-react/controls/ListView/)
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/react-admin-sc-catalog-pnpjs" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 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": {
"site-collection-catalog-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/siteCollectionCatalog/SiteCollectionCatalogWebPart.js",
"manifest": "./src/webparts/siteCollectionCatalog/SiteCollectionCatalogWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"SiteCollectionCatalogWebPartStrings": "lib/webparts/siteCollectionCatalog/loc/{locale}.js",
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/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-admin-sc-catalog-pnpjs",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,13 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-admin-sc-catalog-pnpjs-client-side-solution",
"id": "59954bf3-e8a7-4def-933b-c1b618e0ab19",
"version": "2.0.0.0",
"includeClientSideAssets": true,
"isDomainIsolated": false
},
"paths": {
"zippedPackage": "solution/react-admin-sc-catalog-pnpjs.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 gulp = require('gulp');
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(gulp);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,47 @@
{
"name": "react-admin-sc-catalog-pnpjs",
"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": {
"@microsoft/sp-core-library": "1.9.1",
"@microsoft/sp-lodash-subset": "1.9.1",
"@microsoft/sp-office-ui-fabric-core": "1.9.1",
"@microsoft/sp-webpart-base": "1.9.1",
"@pnp/common": "^1.3.6",
"@pnp/graph": "^1.3.6",
"@pnp/logging": "^1.3.6",
"@pnp/odata": "^1.3.6",
"@pnp/sp": "^1.3.6",
"@pnp/spfx-controls-react": "1.15.0",
"@types/es6-promise": "0.0.33",
"@types/react": "16.8.8",
"@types/react-dom": "16.8.3",
"@types/webpack-env": "1.13.1",
"office-ui-fabric-react": "6.189.2",
"react": "16.8.5",
"react-dom": "16.8.5"
},
"resolutions": {
"@types/react": "16.8.8"
},
"devDependencies": {
"@microsoft/sp-build-web": "1.9.1",
"@microsoft/sp-tslint-rules": "1.9.1",
"@microsoft/sp-module-interfaces": "1.9.1",
"@microsoft/sp-webpart-workbench": "1.9.1",
"@microsoft/rush-stack-compiler-2.9": "0.7.16",
"gulp": "~3.9.1",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"ajv": "~5.2.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,27 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "87cc523d-1f83-4486-a11d-cf78a7c8c749",
"alias": "SiteCollectionCatalogWebPart",
"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": "SiteCollectionCatalog" },
"description": { "default": "SiteCollectionCatalog description" },
"officeFabricIconFontName": "Page",
"properties": {
"description": "SiteCollectionCatalog"
}
}]
}

View File

@ -0,0 +1,80 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version, DisplayMode } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-webpart-base';
import * as strings from 'SiteCollectionCatalogWebPartStrings';
import SiteCollectionCatalog from './components/SiteCollectionCatalog';
import { sp } from "@pnp/sp";
export interface ISiteCollectionCatalogWebPartProps {
description: string;
title: string;
displayMode: DisplayMode;
updateProperty: (value: string) => void;
}
export default class SiteCollectionCatalogWebPart extends BaseClientSideWebPart<ISiteCollectionCatalogWebPartProps> {
public 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<ISiteCollectionCatalogWebPartProps> = React.createElement(
SiteCollectionCatalog,
{
description: this.properties.description,
title: this.properties.title,
displayMode: this.displayMode,
updateProperty: (value: string) => {
this.properties.title = value;
}
}
);
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
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,5 @@
export interface ISiteCollectionCatalogState {
siteAppCatalogs: any[];
}

View File

@ -0,0 +1,74 @@
@import '~office-ui-fabric-react/dist/sass/References.scss';
.siteCollectionCatalog {
.container {
max-width: 700px;
margin: 0px auto;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.row {
@include ms-Grid-row;
@include ms-fontColor-white;
background-color: $ms-color-themeDark;
padding: 20px;
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl8;
@include ms-xlPush2;
@include ms-lgPush1;
}
.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;
// 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;
}
}
}

View File

@ -0,0 +1,118 @@
import * as React from 'react';
import styles from './SiteCollectionCatalog.module.scss';
import { ISiteCollectionCatalogState } from './ISiteCollectionCatalogProps';
import { SiteCollectionCatalogHelper } from './SiteCollectionCatalogHelper';
import { ListView, IViewField, SelectionMode } from "@pnp/spfx-controls-react/lib/ListView";
import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
import { ISiteCollectionCatalogWebPartProps } from '../SiteCollectionCatalogWebPart';
import { IColumn } from 'office-ui-fabric-react/lib/components/DetailsList';
const _viewFields: IViewField[] = [
{
name: "SiteTitle",
linkPropertyName: "SiteURL",
displayName: "Site Collection",
sorting: true,
minWidth: 120,
isResizable: true
},
{
name: "AppTitle",
displayName: "App Title",
sorting: true,
minWidth: 200,
isResizable: true,
render: (item: any) => {
var _color = "Red";
if (!item.NoApps) {
_color = "Green";
}
const element: any = React.createElement("span", { style: { color: _color } }, item.AppTitle);
return element;
}
},
{
name: "AppCatalogVersion",
displayName: "AppCatalogVersion",
sorting: true,
minWidth: 100
},
{
name: "Deployed",
displayName: "Deployed",
minWidth: 70,
render: (item: any) => {
var _color = "Red";
if (String(item.Deployed).toLowerCase() == "true") {
_color = "Green";
}
const element: any = React.createElement("span", { style: { color: _color } }, item.Deployed);
return element;
}
},
{
name: "InstalledVersion",
displayName: "Installed Version",
sorting: true,
minWidth: 100
},
{
name: "IsClientSideSolution",
displayName: "Is Client Side Solution",
minWidth: 150,
render: (item: any) => {
var _color = "Red";
if (String(item.IsClientSideSolution).toLowerCase() == "true") {
_color = "Green";
}
const element: any = React.createElement("span", { style: { color: _color } }, item.IsClientSideSolution);
return element;
}
}
];
export default class SiteCollectionCatalog extends React.Component<ISiteCollectionCatalogWebPartProps, ISiteCollectionCatalogState> {
constructor(props: ISiteCollectionCatalogWebPartProps) {
super(props);
this.state = {
siteAppCatalogs: []
};
}
public componentDidMount() {
var tmpRes: Promise<any[]> = SiteCollectionCatalogHelper.getSiteCollectionCatalogList();
tmpRes.then(res => {
this.setState({ siteAppCatalogs: res });
});
}
private _getSelection(items: any[]) {
console.log('Selected items:', items);
}
public render(): React.ReactElement<ISiteCollectionCatalogWebPartProps> {
return (
<div className={styles.siteCollectionCatalog}>
<WebPartTitle displayMode={this.props.displayMode}
title={this.props.title}
updateProperty={this.props.updateProperty} />
<ListView
items={this.state.siteAppCatalogs}
viewFields={_viewFields}
compact={true}
selectionMode={SelectionMode.none}
selection={this._getSelection}
showFilter={true}
filterPlaceHolder="Search..."
/>
</div>
);
}
}

View File

@ -0,0 +1,84 @@
import { sp, Site } from "@pnp/sp";
import { combine } from "@pnp/common";
export class SiteCollectionCatalogHelper {
public static async getSiteCollectionCatalogList(): Promise<SiteCatalogApps[]> {
let resultData = new Array<SiteCatalogApps>();
var tenantAppCatalogWeb = await sp.getTenantAppCatalogWeb();
let siteCollectionAppCatalogs = await tenantAppCatalogWeb.lists.getByTitle("Site Collection App Catalogs").items.get();
let data = await Promise.all(siteCollectionAppCatalogs.map(async siteCollCatalog => {
let siteUrl = siteCollCatalog["SiteCollectionUrl"];
let tmpSite = new Site(siteUrl);
let tmpCatalog = await tmpSite.rootWeb.getSiteCollectionAppCatalog(siteUrl);
let apps = await tmpCatalog.get();
let tmpWeb = (await (await tmpSite.getRootWeb()).select("Title").get());
if (apps.length == 0) {
let tmpApp = new SiteCatalogApps();
tmpApp = this.MapAppProps(tmpWeb, siteUrl, null, true);
resultData.push(tmpApp);
} else {
apps.forEach(app => {
let tmpApp = new SiteCatalogApps();
tmpApp = this.MapAppProps(tmpWeb, siteUrl, app, false);
resultData.push(tmpApp);
});
}
}));
return await resultData;
}
public static MapAppProps(tmpWeb: any, siteUrl: string, app: any, IsEmpty: boolean): SiteCatalogApps {
var tmpApp: SiteCatalogApps = new SiteCatalogApps();
if (IsEmpty) {
tmpApp.SiteTitle = tmpWeb["Title"];
tmpApp.SiteURL = combine(siteUrl, "/AppCatalog/");
tmpApp.AppTitle = "There are no apps Available ";
tmpApp.AppCatalogVersion = "-";
tmpApp.Deployed = "-";
tmpApp.InstalledVersion = "-";
tmpApp.IsEnabled = "-";
tmpApp.IsClientSideSolution = "-";
tmpApp.NoApps = true;
} else {
tmpApp.SiteTitle = tmpWeb["Title"];
tmpApp.SiteURL = combine(siteUrl, "/AppCatalog/");
tmpApp.AppTitle = app["Title"];
tmpApp.AppCatalogVersion = app["AppCatalogVersion"];
tmpApp.Deployed = String(app["Deployed"]);
tmpApp.InstalledVersion = app["InstalledVersion"];
tmpApp.IsEnabled = String(app["IsEnabled"]);
tmpApp.IsClientSideSolution = String(app["IsClientSideSolution"]);
tmpApp.NoApps = false;
}
return tmpApp;
}
}
export class SiteCatalogApps {
public SiteTitle: string;
public SiteURL: string;
public AppTitle: string;
public AppCatalogVersion: string;
public Deployed: string;
public InstalledVersion: string;
public IsEnabled: string;
public IsClientSideSolution: string;
public NoApps: boolean;
}

View File

@ -0,0 +1,7 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field"
}
});

View File

@ -0,0 +1,10 @@
declare interface ISiteCollectionCatalogWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
}
declare module 'SiteCollectionCatalogWebPartStrings' {
const strings: ISiteCollectionCatalogWebPartStrings;
export = strings;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,38 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-2.9/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"
],
"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
}
}