Added new sample react-taxonomy-file-explorer
This commit is contained in:
parent
fb46fbcbb2
commit
5d7ea6e32a
|
@ -0,0 +1,16 @@
|
|||
!dist
|
||||
config
|
||||
|
||||
gulpfile.js
|
||||
|
||||
release
|
||||
src
|
||||
temp
|
||||
|
||||
tsconfig.json
|
||||
tslint.json
|
||||
|
||||
*.log
|
||||
|
||||
.yo-rc.json
|
||||
.vscode
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"@microsoft/generator-sharepoint": {
|
||||
"plusBeta": false,
|
||||
"isCreatingSolution": true,
|
||||
"environment": "spo",
|
||||
"version": "1.13.1",
|
||||
"libraryName": "react-taxonomy-file-explorer",
|
||||
"libraryId": "5697a573-1bd1-4ff3-96f1-82263c8eb008",
|
||||
"packageManager": "npm",
|
||||
"isDomainIsolated": false,
|
||||
"componentType": "webpart"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
# React Taxonomy File Explorer
|
||||
|
||||
## Summary
|
||||
|
||||
This solution renders a given Termset as a Tree and incorporates files similar than a folder structure in file explorer. Benefit: Due to multiple selection in the managed metadata column the same file can occur more than once.
|
||||
Additionally with drag and drop options the file can be changed:
|
||||
- By replacing the managed metadata column with the target term (Move)
|
||||
- By adding the target term to the managed metadata column (Link)
|
||||
- By copying the file to a new one with (only) the target term in the managed metadata column (Copy)
|
||||
|
||||
In action this looks like:
|
||||
|
||||
Link:
|
||||
|
||||
![Adding the target term to the managed metadata column (Link)](./assets/03Link.gif)
|
||||
|
||||
Move:
|
||||
|
||||
![Replacing the managed metadata column with the target term (Move)](./assets/04Move.gif)
|
||||
|
||||
Copy:
|
||||
|
||||
![Copying the file to a new one with (only) the target term in the managed metadata column (Copy)](./assets/05Copy.gif)
|
||||
|
||||
For further details see the author's [blog post](https://mmsharepoint.wordpress.com/2021/12/23/a-sharepoint-file-explorer-based-on-managed-metadata-and-spfx/)
|
||||
|
||||
## Used SharePoint Framework Version
|
||||
|
||||
![version](https://img.shields.io/badge/version-1.13-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)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A hierarchical Termset bound to a managed metadata column
|
||||
- A document library using that managed metadata column and several documents with selected terms
|
||||
|
||||
## Solution
|
||||
|
||||
Solution|Author(s)
|
||||
--------|---------
|
||||
react-taxonomy-file-explorer| [Markus Moeller](https://github.com/mmsharepoint) ([@moeller2_0](http://www.twitter.com/moeller2_0))
|
||||
|
||||
## Version history
|
||||
|
||||
Version|Date|Comments
|
||||
-------|----|--------
|
||||
1.0|December 26, 2021|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**
|
||||
- Instantiate the webpart in the online workbench on a site where prerequisites are met:
|
||||
- A hierarchical Termset bound to a managed metadata column
|
||||
- A document library using that managed metadata column and several documents with selected terms
|
||||
- In the webpart properties set the library name and the internal field name of the managed metadata column
|
||||
- Done!
|
||||
|
||||
## Features
|
||||
|
||||
This web part illustrates the following concepts:
|
||||
|
||||
- Use HTML5 drag and drop event handling
|
||||
- Use options by modifier key pressed
|
||||
- Update Managed Metadata columns with [PnPJS](https://pnp.github.io/pnpjs/)
|
||||
- Build trees with recursive components
|
||||
- Traverse trees with callback functions
|
||||
- [FluentUI React File Type Icons](https://www.npmjs.com/package/@fluentui/react-file-type-icons)
|
||||
- [FluentUI Contextual Menu](https://developer.microsoft.com/en-us/fluentui#/controls/web/contextualmenu)
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
## 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-teams-graph-upload-as-pdf") 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-teams-graph-upload-as-pdf) and see what the community is saying.
|
||||
|
||||
If you encounter any issues while 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-teams-graph-upload-as-pdf&template=bug-report.yml&sample=react-teams-graph-upload-as-pdf&authors=@mmsharepoint&title=react-teams-graph-upload-as-pdf%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-teams-graph-upload-as-pdf&template=question.yml&sample=react-teams-graph-upload-as-pdf&authors=@mmsharepoint&title=react-teams-graph-upload-as-pdf%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-teams-graph-upload-as-pdf&template=suggestion.yml&sample=react-teams-graph-upload-as-pdf&authors=@mmsharepoint&title=react-teams-graph-upload-as-pdf%20-%20).
|
||||
|
||||
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-taxonomy-file-explorer" />
|
Binary file not shown.
After Width: | Height: | Size: 253 KiB |
Binary file not shown.
After Width: | Height: | Size: 162 KiB |
Binary file not shown.
After Width: | Height: | Size: 180 KiB |
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
|
||||
"version": "2.0",
|
||||
"bundles": {
|
||||
"taxonomy-file-explorer-web-part": {
|
||||
"components": [
|
||||
{
|
||||
"entrypoint": "./lib/webparts/taxonomyFileExplorer/TaxonomyFileExplorerWebPart.js",
|
||||
"manifest": "./src/webparts/taxonomyFileExplorer/TaxonomyFileExplorerWebPart.manifest.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"externals": {},
|
||||
"localizedResources": {
|
||||
"TaxonomyFileExplorerWebPartStrings": "lib/webparts/taxonomyFileExplorer/loc/{locale}.js"
|
||||
}
|
||||
}
|
|
@ -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-taxonomy-file-explorer",
|
||||
"accessKey": "<!-- ACCESS KEY -->"
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
|
||||
"solution": {
|
||||
"name": "react-taxonomy-file-explorer-client-side-solution",
|
||||
"id": "5697a573-1bd1-4ff3-96f1-82263c8eb008",
|
||||
"version": "1.0.0.0",
|
||||
"includeClientSideAssets": true,
|
||||
"isDomainIsolated": false,
|
||||
"developer": {
|
||||
"name": "",
|
||||
"websiteUrl": "",
|
||||
"privacyUrl": "",
|
||||
"termsOfUseUrl": "",
|
||||
"mpnId": "Undefined-1.13.1"
|
||||
}
|
||||
},
|
||||
"paths": {
|
||||
"zippedPackage": "solution/react-taxonomy-file-explorer.sppkg"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
|
||||
"port": 4321,
|
||||
"https": true,
|
||||
"initialPage": "https://enter-your-SharePoint-site/_layouts/workbench.aspx"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
|
@ -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
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"name": "react-taxonomy-file-explorer",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
"build": "gulp bundle",
|
||||
"clean": "gulp clean",
|
||||
"test": "gulp test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluentui/react-file-type-icons": "^8.5.7",
|
||||
"@microsoft/sp-core-library": "1.13.1",
|
||||
"@microsoft/sp-lodash-subset": "1.13.1",
|
||||
"@microsoft/sp-office-ui-fabric-core": "1.13.1",
|
||||
"@microsoft/sp-property-pane": "1.13.1",
|
||||
"@microsoft/sp-webpart-base": "1.13.1",
|
||||
"@pnp/sp": "^2.11.0",
|
||||
"office-ui-fabric-react": "7.174.1",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.13.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "16.9.51",
|
||||
"@types/react-dom": "16.9.8",
|
||||
"@microsoft/sp-build-web": "1.13.1",
|
||||
"@microsoft/sp-tslint-rules": "1.13.1",
|
||||
"@microsoft/sp-module-interfaces": "1.13.1",
|
||||
"@microsoft/rush-stack-compiler-3.9": "0.4.47",
|
||||
"gulp": "~4.0.2",
|
||||
"ajv": "~5.2.2",
|
||||
"@types/webpack-env": "1.13.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
|
@ -0,0 +1,8 @@
|
|||
export interface IFileItem {
|
||||
id: string;
|
||||
title: string;
|
||||
extension: string;
|
||||
url: string;
|
||||
termGuid: string[];
|
||||
taxValue: string[];
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { IFileItem } from "./IFileItem";
|
||||
|
||||
export interface ITermNode {
|
||||
name: string;
|
||||
guid: string;
|
||||
childDocuments: number;
|
||||
children: ITermNode[];
|
||||
subFiles: IFileItem[];
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
import { sp } from "@pnp/sp";
|
||||
import { IFileItem } from "../model/IFileItem";
|
||||
|
||||
export class SPService {
|
||||
private listName: string;
|
||||
private fieldName: string;
|
||||
|
||||
constructor (listname: string, fieldname: string) {
|
||||
this.listName = listname;
|
||||
this.fieldName = fieldname;
|
||||
}
|
||||
|
||||
public async getItems (termsetID: string): Promise<IFileItem[]> {
|
||||
const items: any[] = await sp.web.lists.getByTitle(this.listName).items.select('Id', this.fieldName).expand('File').get();
|
||||
const files: IFileItem[] = [];
|
||||
items.forEach(i => {
|
||||
const nameparts = i.File.Name.split('.');
|
||||
const file: IFileItem = {
|
||||
title: i.File.Name,
|
||||
extension: nameparts[nameparts.length - 1],
|
||||
id: i.Id,
|
||||
termGuid: [""],
|
||||
taxValue: [""],
|
||||
url: i.File.LinkingUrl
|
||||
};
|
||||
if (Array.isArray(i[this.fieldName])) {
|
||||
const termguids: string[] = [];
|
||||
const taxvalues: string[] = [];
|
||||
i[this.fieldName].forEach(f => {
|
||||
termguids.push(f.TermGuid.toLowerCase());
|
||||
taxvalues.push(`${f.Label}|${f.TermGuid}`);
|
||||
});
|
||||
file.termGuid = termguids;
|
||||
file.taxValue = taxvalues;
|
||||
}
|
||||
else {
|
||||
file.termGuid = i[this.fieldName] ? [i[this.fieldName].TermGuid.toLowerCase()]:[""];
|
||||
file.taxValue = i[this.fieldName] ? [`${i[this.fieldName].Label.toLowerCase()}|${i[this.fieldName].TermGuid.toLowerCase()}`]:[""];
|
||||
}
|
||||
files.push(file);
|
||||
});
|
||||
return files;
|
||||
}
|
||||
|
||||
public async updateTaxonomyItemByAdd (file: IFileItem, fieldName: string, newTaxonomyValue: string) {
|
||||
const itemID: number = parseInt(file.id);
|
||||
let fieldValues = file.taxValue.join(';');
|
||||
fieldValues += `;${newTaxonomyValue}`;
|
||||
|
||||
// https://blog.aterentiev.com/how-to-easily-update-managed-metadata
|
||||
await sp.web.lists.getByTitle(this.listName).items.getById(itemID).validateUpdateListItem([{
|
||||
ErrorMessage: null,
|
||||
FieldName: fieldName,
|
||||
FieldValue: fieldValues,
|
||||
HasException: false
|
||||
}]);
|
||||
}
|
||||
|
||||
public async updateTaxonomyItemByReplace (file: IFileItem, fieldName: string, newTaxonomyValue: string) {
|
||||
const itemID: number = parseInt(file.id);
|
||||
|
||||
await sp.web.lists.getByTitle(this.listName).items.getById(itemID).validateUpdateListItem([{
|
||||
ErrorMessage: null,
|
||||
FieldName: fieldName,
|
||||
FieldValue: newTaxonomyValue,
|
||||
HasException: false
|
||||
}]);
|
||||
}
|
||||
|
||||
public async newTaxonomyItemByCopy (file: IFileItem, fieldName: string, newTaxonomyValue: string): Promise<IFileItem> {
|
||||
const fileUrl: URL = new URL(file.url);
|
||||
const currentFileNamePart = file.title.replace(`.${file.extension}`, '');
|
||||
const newFilename = `${currentFileNamePart}_Copy.${file.extension}`;
|
||||
const destinationUrl = decodeURI(fileUrl.pathname).replace(file.title, newFilename);
|
||||
await sp.web.getFileByServerRelativePath(decodeURI(fileUrl.pathname)).copyByPath(destinationUrl, false, true);
|
||||
const newFileItemPromise = await sp.web.getFileByServerRelativePath(destinationUrl).getItem();
|
||||
const newFileItem = await newFileItemPromise.get();
|
||||
console.log(newFileItem);
|
||||
const itemID: number = parseInt(newFileItem.Id);
|
||||
|
||||
await sp.web.lists.getByTitle(this.listName).items.getById(itemID).validateUpdateListItem([{
|
||||
ErrorMessage: null,
|
||||
FieldName: fieldName,
|
||||
FieldValue: newTaxonomyValue,
|
||||
HasException: false
|
||||
}]);
|
||||
const newFile: IFileItem = {
|
||||
extension: file.extension,
|
||||
id: itemID.toString(),
|
||||
taxValue: [newTaxonomyValue],
|
||||
termGuid: [newTaxonomyValue.split('|')[1]],
|
||||
title: newFilename,
|
||||
url: fileUrl.host + '/' + destinationUrl
|
||||
};
|
||||
return newFile;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
import { sp } from "@pnp/sp";
|
||||
import { IOrderedTermInfo } from "@pnp/sp/taxonomy";
|
||||
import { IFileItem } from "../model/IFileItem";
|
||||
import { ITermNode } from "../model/ITermNode";
|
||||
|
||||
export class TaxonomyService {
|
||||
public async getTermsetInfo (fieldName: string): Promise<string> {
|
||||
const mmFieldInfo = await sp.web.fields.getByInternalNameOrTitle(fieldName).get();
|
||||
const parser = new DOMParser();
|
||||
const xmlField = parser.parseFromString(mmFieldInfo.SchemaXml, "text/xml");
|
||||
const properties = xmlField.getElementsByTagName("ArrayOfProperty")[0].childNodes;
|
||||
let termsetID: string = "";
|
||||
properties.forEach(prop => {
|
||||
if (prop.childNodes[0].textContent == "TermSetId") {
|
||||
termsetID = prop.childNodes[1].textContent;
|
||||
}
|
||||
});
|
||||
return termsetID;
|
||||
}
|
||||
|
||||
public async getTermset (termsetID: string) {
|
||||
// list all the terms available in this term set by term set id
|
||||
const termset: IOrderedTermInfo[] = await sp.termStore.sets.getById(termsetID).getAllChildrenAsOrderedTree();
|
||||
const termnodes: ITermNode[] = [];
|
||||
termset.forEach(async ti => {
|
||||
const tn = this.getTermnode(ti);
|
||||
termnodes.push(tn);
|
||||
});
|
||||
return termnodes;
|
||||
}
|
||||
|
||||
public incorporateFiles (terms: ITermNode[], files: IFileItem[]): ITermNode[] {
|
||||
terms.forEach(term => {
|
||||
term = this.incorporateFilesIntoTerm(term, files);
|
||||
});
|
||||
return terms;
|
||||
}
|
||||
|
||||
private getTermnode (term: IOrderedTermInfo): ITermNode {
|
||||
const node: ITermNode = {
|
||||
guid: term.id,
|
||||
childDocuments: 0,
|
||||
name: term.defaultLabel,
|
||||
children: [],
|
||||
subFiles: []
|
||||
};
|
||||
if (term.childrenCount > 0) {
|
||||
const ctnodes: ITermNode[] = [];
|
||||
term.children.forEach(ct => {
|
||||
const ctnode: ITermNode = this.getTermnode(ct);
|
||||
node.childDocuments += ctnode.childDocuments;
|
||||
ctnodes.push(ctnode);
|
||||
});
|
||||
node.children = ctnodes;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
private incorporateFilesIntoTerm (term: ITermNode, files: IFileItem[]): ITermNode {
|
||||
term.childDocuments = 0;
|
||||
term.subFiles = [];
|
||||
if (term.children.length > 0) {
|
||||
term.children.forEach(ct => {
|
||||
ct = this.incorporateFilesIntoTerm(ct, files);
|
||||
term.childDocuments += ct.childDocuments;
|
||||
});
|
||||
}
|
||||
files.forEach(fi => {
|
||||
if (fi.termGuid.indexOf(term.guid.toLowerCase()) > -1) {
|
||||
term.childDocuments++;
|
||||
term.subFiles.push(fi);
|
||||
}
|
||||
});
|
||||
return term;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
|
||||
"id": "e086d23e-079e-42c1-aecf-d042a264ddac",
|
||||
"alias": "TaxonomyFileExplorerWebPart",
|
||||
"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": "Taxonomy File Explorer" },
|
||||
"description": { "default": "A structural view on files, based on Managed Metadata column" },
|
||||
"officeFabricIconFontName": "BulletedTreeList",
|
||||
"properties": {
|
||||
"listName": "Documents",
|
||||
"fieldName": "FolderStructure"
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
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 { sp } from "@pnp/sp/presets/all";
|
||||
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
|
||||
|
||||
import * as strings from 'TaxonomyFileExplorerWebPartStrings';
|
||||
import { TaxonomyFileExplorer } from './components/TaxonomyFileExplorer';
|
||||
import { ITaxonomyFileExplorerProps } from './components/ITaxonomyFileExplorerProps';
|
||||
|
||||
export interface ITaxonomyFileExplorerWebPartProps {
|
||||
fieldName: string;
|
||||
listName: string;
|
||||
}
|
||||
|
||||
export default class TaxonomyFileExplorerWebPart extends BaseClientSideWebPart<ITaxonomyFileExplorerWebPartProps> {
|
||||
protected onInit(): Promise<void> {
|
||||
return super.onInit().then(_ => {
|
||||
sp.setup({
|
||||
spfxContext: this.context
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public render(): void {
|
||||
const element: React.ReactElement<ITaxonomyFileExplorerProps> = React.createElement(
|
||||
TaxonomyFileExplorer,
|
||||
{
|
||||
fieldName: this.properties.fieldName,
|
||||
listName: this.properties.listName
|
||||
}
|
||||
);
|
||||
|
||||
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('listName', {
|
||||
label: strings.ListnameFieldLabel
|
||||
}),
|
||||
PropertyPaneTextField('fieldName', {
|
||||
label: strings.FieldnameFieldLabel
|
||||
})
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
.fileLabel {
|
||||
padding-left: 8px;
|
||||
li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.filelink {
|
||||
margin-left: 6px;
|
||||
text-decoration: none;
|
||||
font-style: italic;
|
||||
font-size: 0.9em;
|
||||
color: "[theme: themePrimary, default: #0078d7]";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import * as React from 'react';
|
||||
import styles from './FileLabel.module.scss';
|
||||
import { getFileTypeIconProps, initializeFileTypeIcons } from '@fluentui/react-file-type-icons';
|
||||
import { Icon } from 'office-ui-fabric-react/lib/Icon';
|
||||
import { IFileLabelProps } from './IFileLabelProps';
|
||||
|
||||
initializeFileTypeIcons(undefined);
|
||||
|
||||
export const FileLabel: React.FC<IFileLabelProps> = (props) => {
|
||||
const drag = (ev) => {
|
||||
ev.dataTransfer.setData("text/plain", JSON.stringify(props.file));
|
||||
};
|
||||
|
||||
return (
|
||||
<li className={styles.fileLabel} draggable={true} onDragStart={drag}>
|
||||
<Icon {...getFileTypeIconProps({ extension: props.file.extension, size: 16 })} />
|
||||
<a className={styles.filelink} draggable={false} href={props.file.url}>{props.file.title}</a>
|
||||
</li>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
import { IFileItem } from "../../../model/IFileItem";
|
||||
|
||||
export interface IFileLabelProps {
|
||||
file: IFileItem;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export interface ITaxonomyFileExplorerProps {
|
||||
fieldName: string;
|
||||
listName: string;
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import { IFileItem } from "../../../model/IFileItem";
|
||||
import { ITermNode } from "../../../model/ITermNode";
|
||||
|
||||
export interface ITermLabelProps {
|
||||
node: ITermNode;
|
||||
selectedNode: string;
|
||||
renderFiles: (files: IFileItem[]) => void;
|
||||
resetChecked: (s: string) => void;
|
||||
addTerm: (file: IFileItem, newValue: string) => void;
|
||||
replaceTerm: (file: IFileItem, newValue: string) => void;
|
||||
copyFile: (file: IFileItem, newValue: string) => void;
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
@import '~office-ui-fabric-react/dist/sass/References.scss';
|
||||
|
||||
.taxonomyFileExplorer {
|
||||
.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;
|
||||
color: "[theme: themePrimary, default: #0078d7]";
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.column {
|
||||
@include ms-Grid-col;
|
||||
@include ms-lg6;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding-inline-start: 0px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
import * as React from 'react';
|
||||
import styles from './TaxonomyFileExplorer.module.scss';
|
||||
import { ITaxonomyFileExplorerProps } from './ITaxonomyFileExplorerProps';
|
||||
import { IFileItem } from '../../../model/IFileItem';
|
||||
import { ITermNode } from '../../../model/ITermNode';
|
||||
import { TaxonomyService } from '../../../services/TaxonomyService';
|
||||
import { SPService } from '../../../services/SPService';
|
||||
import { FileLabel } from './FileLabel';
|
||||
import { TermLabel } from './TermLabel';
|
||||
|
||||
export const TaxonomyFileExplorer: React.FC<ITaxonomyFileExplorerProps> = (props) => {
|
||||
const [spSvc, setSpSvc] = React.useState<SPService>();
|
||||
const [fileItems, setFileItems] = React.useState<IFileItem[]>([]);
|
||||
const [terms, setTerms] = React.useState<ITermNode[]>([]);
|
||||
const [shownFiles, setShownFiles] = React.useState<IFileItem[]>([]);
|
||||
const [selectedTermnode, setSelectedTermnode] = React.useState<string>("");
|
||||
|
||||
const buildTree = async () => {
|
||||
const taxSvc: TaxonomyService = new TaxonomyService();
|
||||
const termsetID = await taxSvc.getTermsetInfo(props.fieldName);
|
||||
let termnodetree: ITermNode[];
|
||||
const termnodetreeStr = sessionStorage.getItem(`Termtree_${termsetID}`);
|
||||
if (termnodetreeStr === null) {
|
||||
termnodetree = await taxSvc.getTermset(termsetID);
|
||||
sessionStorage.setItem(`Termtree_${termsetID}`, JSON.stringify(termnodetree));
|
||||
}
|
||||
else {
|
||||
termnodetree = JSON.parse(termnodetreeStr);
|
||||
}
|
||||
|
||||
const spSrvc: SPService = new SPService(props.listName, props.fieldName);
|
||||
const files = await spSrvc.getItems(termsetID);
|
||||
setSpSvc(spSrvc);
|
||||
updateFiles(files, termnodetree);
|
||||
};
|
||||
|
||||
const updateFiles = (files: IFileItem[], termnodetree: ITermNode[]) => {
|
||||
const taxSvc: TaxonomyService = new TaxonomyService();
|
||||
termnodetree = taxSvc.incorporateFiles(termnodetree, files);
|
||||
setFileItems(files);
|
||||
setTerms(termnodetree);
|
||||
};
|
||||
|
||||
const renderFiles = (files: IFileItem[]) => {
|
||||
setShownFiles(files);
|
||||
};
|
||||
|
||||
const resetChecked = (newNodeID: string) => {
|
||||
setSelectedTermnode(newNodeID);
|
||||
};
|
||||
|
||||
const reloadFiles = (file: IFileItem) => {
|
||||
const newFiles: IFileItem[] = [];
|
||||
fileItems.forEach(fi => {
|
||||
if (fi.id === file.id && fi.url === file.url) {
|
||||
newFiles.push(file);
|
||||
}
|
||||
else {
|
||||
newFiles.push(fi);
|
||||
}
|
||||
});
|
||||
updateFiles(newFiles, terms);
|
||||
};
|
||||
|
||||
const loadNewFiles = (file: IFileItem) => {
|
||||
const newFiles: IFileItem[] = [file].concat(fileItems);
|
||||
updateFiles(newFiles, terms);
|
||||
};
|
||||
|
||||
const addTerm = (file: IFileItem, newTaxonomyValue: string) => {
|
||||
spSvc.updateTaxonomyItemByAdd(file, props.fieldName, newTaxonomyValue);
|
||||
reloadFiles(file);
|
||||
};
|
||||
|
||||
const replaceTerm = (file: IFileItem, newTaxonomyValue: string) => {
|
||||
spSvc.updateTaxonomyItemByReplace(file, props.fieldName, newTaxonomyValue);
|
||||
reloadFiles(file);
|
||||
};
|
||||
|
||||
const copyFile = async (file: IFileItem, newTaxonomyValue: string) => {
|
||||
const newFile = await spSvc.newTaxonomyItemByCopy(file, props.fieldName, newTaxonomyValue);
|
||||
loadNewFiles(newFile);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
buildTree();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={ styles.taxonomyFileExplorer }>
|
||||
<div className={ styles.container }>
|
||||
<div className={ styles.row }>
|
||||
<div className={ styles.column }>
|
||||
<ul>
|
||||
{terms.map(nc => { return <TermLabel node={nc}
|
||||
renderFiles={renderFiles}
|
||||
resetChecked={resetChecked}
|
||||
selectedNode={selectedTermnode}
|
||||
addTerm={addTerm}
|
||||
replaceTerm={replaceTerm}
|
||||
copyFile={copyFile} />; })}
|
||||
</ul>
|
||||
</div>
|
||||
<div className={ styles.column }>
|
||||
{shownFiles.length > 0 &&
|
||||
<ul>
|
||||
{shownFiles.map(f => {
|
||||
return <FileLabel file={f} />;
|
||||
})}
|
||||
</ul>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
.termLabel {
|
||||
li {
|
||||
list-style: none;
|
||||
}
|
||||
.liFilled {
|
||||
border-left-color: "[theme: themePrimary, default: #0078d7]";
|
||||
border-left-style: dotted;
|
||||
border-left-width: 1px;
|
||||
}
|
||||
ul {
|
||||
padding-inline-start: 16px;
|
||||
}
|
||||
.label {
|
||||
margin-bottom: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.checkedLabel {
|
||||
border: 1px dotted "[theme: themePrimary, default: #0078d7]";
|
||||
}
|
||||
.icon {
|
||||
margin-right: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.emptyicon {
|
||||
margin-right: 15px;
|
||||
}
|
||||
.fileCount {
|
||||
font-weight: 600;
|
||||
padding-left: 18px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
import * as React from 'react';
|
||||
import styles from './TermLabel.module.scss';
|
||||
import { ContextualMenu, IContextualMenuItem } from "office-ui-fabric-react/lib/ContextualMenu";
|
||||
import { Icon } from 'office-ui-fabric-react/lib/Icon';
|
||||
import { ITermLabelProps } from "./ITermLabelProps";
|
||||
import { IFileItem } from '../../../model/IFileItem';
|
||||
|
||||
export const TermLabel: React.FC<ITermLabelProps> = (props) => {
|
||||
const linkRef = React.useRef(null);
|
||||
const [showChildren, setShowChildren] = React.useState<boolean>(true);
|
||||
const [countDocuments, setCountDocuments] = React.useState<number>(props.node.childDocuments);
|
||||
const [showContextualMenu, setShowContextualMenu] = React.useState<boolean>(false);
|
||||
const [droppedFile, setDroppedFile] = React.useState<IFileItem>();
|
||||
|
||||
const toggleIcon = () => {
|
||||
setShowChildren(!showChildren);
|
||||
};
|
||||
|
||||
const nodeSelected = () => {
|
||||
props.resetChecked(props.node.guid);
|
||||
props.renderFiles(props.node.subFiles);
|
||||
};
|
||||
|
||||
const hideContextualMenu = () => {
|
||||
setShowContextualMenu(false);
|
||||
};
|
||||
|
||||
const drop = (ev) => {
|
||||
ev.preventDefault();
|
||||
var data = ev.dataTransfer.getData("text");
|
||||
const file: IFileItem = JSON.parse(data);
|
||||
setDroppedFile(file);
|
||||
if (ev.ctrlKey) {
|
||||
setShowContextualMenu(true);
|
||||
}
|
||||
else {
|
||||
addNewTerm(file); // Default option: Simply add the new (target) term to existing ones
|
||||
}
|
||||
};
|
||||
|
||||
const dragOver = (ev) => {
|
||||
ev.preventDefault();
|
||||
};
|
||||
|
||||
const addNewTerm = (file: IFileItem) => {
|
||||
const newTaxonomyValue = `${props.node.name}|${props.node.guid}`;
|
||||
file.termGuid.push(props.node.guid);
|
||||
file.taxValue.push(newTaxonomyValue);
|
||||
console.log(file);
|
||||
props.addTerm(file, newTaxonomyValue);
|
||||
};
|
||||
|
||||
const replaceByNewTerm = (file: IFileItem) => {
|
||||
const newTaxonomyValue = `${props.node.name}|${props.node.guid}`;
|
||||
file.termGuid = [props.node.guid];
|
||||
file.taxValue = [newTaxonomyValue];
|
||||
console.log(file);
|
||||
props.addTerm(file, newTaxonomyValue);
|
||||
};
|
||||
|
||||
const copyWithNewTerm = (file: IFileItem) => {
|
||||
const newTaxonomyValue = `${props.node.name}|${props.node.guid}`;
|
||||
console.log(file);
|
||||
props.copyFile(file, newTaxonomyValue);
|
||||
};
|
||||
|
||||
const currentExpandIcon = showChildren? <Icon className={styles.icon} iconName="ChevronDown" onClick={toggleIcon} />:<Icon className={styles.icon} iconName="ChevronRight" onClick={toggleIcon} />;
|
||||
const menuItems: IContextualMenuItem[] = [
|
||||
{
|
||||
key: 'copyItem',
|
||||
text: 'Create new file with term (Copy)',
|
||||
onClick: () => copyWithNewTerm(droppedFile)
|
||||
},
|
||||
{
|
||||
key: 'moveItem',
|
||||
text: 'Replace with new term (Move)',
|
||||
onClick: () => replaceByNewTerm(droppedFile)
|
||||
},
|
||||
{
|
||||
key: 'linkItem',
|
||||
text: 'Add new term (Link)',
|
||||
onClick: () => addNewTerm(droppedFile)
|
||||
}];
|
||||
React.useEffect(() => {
|
||||
if (props.selectedNode===props.node.guid) {
|
||||
props.renderFiles(props.node.subFiles);
|
||||
}
|
||||
if (props.node.childDocuments !== countDocuments) {
|
||||
setCountDocuments(props.node.childDocuments);
|
||||
}
|
||||
}, [props.node.subFiles]);
|
||||
return (
|
||||
<li className={styles.termLabel}>
|
||||
<div ref={linkRef} className={`${styles.label} ${props.selectedNode===props.node.guid ? styles.checkedLabel : ""}`} onClick={nodeSelected} onDrop={drop} onDragOver={dragOver}>
|
||||
<label>
|
||||
{props.node.children.length > 0 ? currentExpandIcon : <i className={styles.emptyicon}> </i>}
|
||||
<Icon className={styles.icon} iconName="FabricFolder" />
|
||||
{props.node.name}{countDocuments>0?<span className={styles.fileCount}>{countDocuments}</span>:""}
|
||||
</label>
|
||||
</div>
|
||||
<ContextualMenu
|
||||
items={menuItems}
|
||||
hidden={!showContextualMenu}
|
||||
target={linkRef}
|
||||
onItemClick={hideContextualMenu}
|
||||
onDismiss={hideContextualMenu}
|
||||
/>
|
||||
{showChildren && <ul className={`${props.node.children.length > 0 ? styles.liFilled : ""}`}>
|
||||
{props.node.children.map(nc => { return <TermLabel node={nc}
|
||||
renderFiles={props.renderFiles}
|
||||
resetChecked={props.resetChecked}
|
||||
selectedNode={props.selectedNode}
|
||||
addTerm={props.addTerm}
|
||||
replaceTerm={props.replaceTerm}
|
||||
copyFile={props.copyFile} />; })}
|
||||
</ul>}
|
||||
</li>
|
||||
);
|
||||
};
|
8
samples/react-taxonomy-file-explorer/src/webparts/taxonomyFileExplorer/loc/en-us.js
vendored
Normal file
8
samples/react-taxonomy-file-explorer/src/webparts/taxonomyFileExplorer/loc/en-us.js
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
define([], function() {
|
||||
return {
|
||||
"PropertyPaneDescription": "Description",
|
||||
"BasicGroupName": "Group Name",
|
||||
"ListnameFieldLabel": "Listname",
|
||||
"FieldnameFieldLabel": "(Internal) Fieldname"
|
||||
}
|
||||
});
|
11
samples/react-taxonomy-file-explorer/src/webparts/taxonomyFileExplorer/loc/mystrings.d.ts
vendored
Normal file
11
samples/react-taxonomy-file-explorer/src/webparts/taxonomyFileExplorer/loc/mystrings.d.ts
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
declare interface ITaxonomyFileExplorerWebPartStrings {
|
||||
PropertyPaneDescription: string;
|
||||
BasicGroupName: string;
|
||||
ListnameFieldLabel: string;
|
||||
FieldnameFieldLabel: string;
|
||||
}
|
||||
|
||||
declare module 'TaxonomyFileExplorerWebPartStrings' {
|
||||
const strings: ITaxonomyFileExplorerWebPartStrings;
|
||||
export = strings;
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
After Width: | Height: | Size: 542 B |
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.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": [
|
||||
"webpack-env"
|
||||
],
|
||||
"lib": [
|
||||
"es5",
|
||||
"dom",
|
||||
"es2015.collection",
|
||||
"es2015.promise"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"extends": "./node_modules/@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-with-statement": true,
|
||||
"semicolon": true,
|
||||
"trailing-comma": false,
|
||||
"typedef": false,
|
||||
"typedef-whitespace": false,
|
||||
"use-named-parameter": true,
|
||||
"variable-name": false,
|
||||
"whitespace": false
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue