Merge pull request #2182 from mmsharepoint/MMDev

Added new sample react-taxonomy-file-explorer
This commit is contained in:
Hugo Bernier 2022-01-03 01:20:00 -05:00 committed by GitHub
commit dcce40fded
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 22555 additions and 0 deletions

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,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"
}
}

View File

@ -0,0 +1,114 @@
# 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/)
## Compatibility
![SPFx 1.13.0](https://img.shields.io/badge/SPFx-1.13.0-green.svg)
![Node.js v14 | v12 | v10](https://img.shields.io/badge/Node.js-v14%20%7C%20v12%20%7C%20v10-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)
## 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
## 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 web part 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 web part 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-taxonomy-file-explorer") 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-taxonomy-file-explorer) 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-taxonomy-file-explorer&template=bug-report.yml&sample=react-taxonomy-file-explorer&authors=@mmsharepoint&title=react-taxonomy-file-explorer%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-taxonomy-file-explorer&template=question.yml&sample=react-taxonomy-file-explorer&authors=@mmsharepoint&title=react-taxonomy-file-explorer%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-taxonomy-file-explorer&template=suggestion.yml&sample=react-taxonomy-file-explorer&authors=@mmsharepoint&title=react-taxonomy-file-explorer%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

View File

@ -0,0 +1,62 @@
[
{
"name": "pnp-sp-dev-spfx-web-parts-react-taxonomy-file-explorer",
"source": "pnp",
"title": "Taxonomy File Explorer",
"shortDescription": "This solution renders a given Termset as a Tree and incorporates files similar than a folder structure in file explorer.",
"url": "https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-taxonomy-file-explorer",
"longDescription": [
"This solution renders a given Termset as a Tree and incorporates files similar than a folder structure in file explorer."
],
"creationDateTime": "2021-12-26",
"updateDateTime": "2021-12-26",
"products": [
"SharePoint",
"Office"
],
"metadata": [
{
"key": "CLIENT-SIDE-DEV",
"value": "React"
},
{
"key": "SPFX-VERSION",
"value": "1.13.0"
}
],
"thumbnails": [
{
"type": "image",
"order": 100,
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-taxonomy-file-explorer/assets/03Link.gif",
"alt": "Linking to a file"
},
{
"type": "image",
"order": 101,
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-taxonomy-file-explorer/assets/04Move.gif",
"alt": "Moving a file"
},
{
"type": "image",
"order": 102,
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-taxonomy-file-explorer/assets/04Move.gif",
"alt": "Copying a file"
}
],
"authors": [
{
"gitHubAccount": "mmsharepoint",
"pictureUrl": "https://github.com/mmsharepoint.png",
"name": "Markus Moeller"
}
],
"references": [
{
"name": "Build your first SharePoint client-side web part",
"description": "Client-side web parts are client-side components that run in the context of a SharePoint page. Client-side web parts can be deployed to SharePoint environments that support the SharePoint Framework. You can also use modern JavaScript web frameworks, tools, and libraries to build them.",
"url": "https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/build-a-hello-world-web-part"
}
]
}
]

View File

@ -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"
}
}

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-taxonomy-file-explorer",
"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-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"
}
}

View File

@ -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"
}

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,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"
}
}

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,8 @@
export interface IFileItem {
id: string;
title: string;
extension: string;
url: string;
termGuid: string[];
taxValue: string[];
}

View File

@ -0,0 +1,9 @@
import { IFileItem } from "./IFileItem";
export interface ITermNode {
name: string;
guid: string;
childDocuments: number;
children: ITermNode[];
subFiles: IFileItem[];
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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"
}
}]
}

View File

@ -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
})
]
}
]
}
]
};
}
}

View File

@ -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]";
}
}

View File

@ -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>
);
};

View File

@ -0,0 +1,5 @@
import { IFileItem } from "../../../model/IFileItem";
export interface IFileLabelProps {
file: IFileItem;
}

View File

@ -0,0 +1,4 @@
export interface ITaxonomyFileExplorerProps {
fieldName: string;
listName: string;
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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>
);
};

View File

@ -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;
}
}

View File

@ -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}>&nbsp;</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>
);
};

View File

@ -0,0 +1,8 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"ListnameFieldLabel": "Listname",
"FieldnameFieldLabel": "(Internal) Fieldname"
}
});

View 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

View File

@ -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"
]
}

View File

@ -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
}
}