Merge pull request #2875 from mmsharepoint/MMSPFx

New feature Expand/Collapse all upgraded SPFx 1.15.0 and PnPJS 3.5.1
This commit is contained in:
Hugo Bernier 2022-07-27 20:42:48 -07:00 committed by GitHub
commit 72458a26af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 5619 additions and 2991 deletions

View File

@ -0,0 +1,10 @@
require('@rushstack/eslint-config/patch/modern-module-resolution');
module.exports = {
extends: ['@microsoft/eslint-config-spfx/lib/profiles/react'],
parserOptions: { tsconfigRootDir: __dirname },
rules: {
"@typescript-eslint/typedef": "off",
"@microsoft/spfx/no-async-await": "off"
}
};

View File

@ -1,4 +1,35 @@
# .CER Certificates
*.cer
# .PEM Certificates
*.pem
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
node_modules
# Build generated files
dist
lib
release
solution
temp
*.sppkg
.heft
# 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

@ -3,7 +3,7 @@
"plusBeta": false,
"isCreatingSolution": true,
"environment": "spo",
"version": "1.13.1",
"version": "1.15.0",
"libraryName": "react-taxonomy-file-explorer",
"libraryId": "5697a573-1bd1-4ff3-96f1-82263c8eb008",
"packageManager": "npm",

View File

@ -22,12 +22,15 @@ 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/)
[UPDATE] Collapse and expand all:
![Collapse and expand all](./assets/ExpandCollapseAll.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)
![SPFx 1.15.0](https://img.shields.io/badge/version-1.15-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")
@ -36,7 +39,6 @@ For further details see the author's [blog post](https://mmsharepoint.wordpress.
![Hosted Workbench Compatible](https://img.shields.io/badge/Hosted%20Workbench-Compatible-green.svg)
![Compatible with Remote Containers](https://img.shields.io/badge/Remote%20Containers-Compatible-green.svg)
## Applies to
- [SharePoint Framework](https://aka.ms/spfx)
@ -60,6 +62,7 @@ react-taxonomy-file-explorer| [Markus Moeller](https://github.com/mmsharepoint)
Version|Date|Comments
-------|----|--------
1.0|December 26, 2021|Initial release
1.1|July 16, 2022|Added expand/collapse all, upgraded to SPFx 1.15.0, upgraded to PnPJS V3.5.1
## Minimal Path to Awesome

View File

@ -9,7 +9,7 @@
"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",
"updateDateTime": "2022-07-16",
"products": [
"SharePoint"
],
@ -20,7 +20,7 @@
},
{
"key": "SPFX-VERSION",
"value": "1.13.0"
"value": "1.15.0"
}
],
"thumbnails": [

View File

@ -0,0 +1,17 @@
{
"$schema": "https://json.schemastore.org/eslintrc.json",
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
]
}

View File

@ -7,12 +7,31 @@
"includeClientSideAssets": true,
"isDomainIsolated": false,
"developer": {
"name": "",
"name": "Markus Moeller",
"websiteUrl": "",
"privacyUrl": "",
"termsOfUseUrl": "",
"mpnId": "Undefined-1.13.1"
}
},
"metadata": {
"shortDescription": {
"default": "react-taxonomy-file-explorer description"
},
"longDescription": {
"default": "react-taxonomy-file-explorer description"
},
"screenshotPaths": [],
"videoUrl": "",
"categories": []
},
"features": [
{
"title": "react-taxonomy-file-explorer Feature",
"description": "The feature that activates elements of the react-taxonomy-file-explorer solution.",
"id": "9804fbff-b443-42e1-86f4-fc261980d916",
"version": "1.0.0.0"
}
]
},
"paths": {
"zippedPackage": "solution/react-taxonomy-file-explorer.sppkg"

View File

@ -13,4 +13,17 @@ build.rig.getTasks = function () {
return result;
};
// disable tslint
build.tslintCmd.enabled = false;
// add eslint
const eslint = require('gulp-eslint');
const eslintSubTask = build.subTask('eslint-subTask', function (gulp, buildOptions, done) {
return gulp.src(['src/**/*.{ts,tsx}'])
.pipe(eslint('./config/eslint.json'))
.pipe(eslint.format())
.pipe(eslint.failAfterError());
});
build.rig.addPreBuildTask(build.task('eslint', eslintSubTask));
build.initialize(require('gulp'));

File diff suppressed because it is too large Load Diff

View File

@ -9,26 +9,36 @@
"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",
"@fluentui/react-file-type-icons": "^8.6.11",
"@microsoft/sp-core-library": "1.15.0",
"@microsoft/sp-lodash-subset": "1.15.0",
"@microsoft/sp-office-ui-fabric-core": "1.15.0",
"@microsoft/sp-property-pane": "1.15.0",
"@microsoft/sp-webpart-base": "1.15.0",
"@pnp/sp": "^3.5.1",
"office-ui-fabric-react": "7.185.7",
"react": "16.13.1",
"react-dom": "16.13.1"
"react-dom": "16.13.1",
"tslib": "2.3.1"
},
"devDependencies": {
"@microsoft/eslint-config-spfx": "1.15.0",
"@microsoft/eslint-plugin-spfx": "1.15.0",
"@microsoft/rush-stack-compiler-3.9": "0.4.47",
"@microsoft/rush-stack-compiler-4.5": "0.2.2",
"@microsoft/sp-build-web": "1.15.0",
"@microsoft/sp-module-interfaces": "1.15.0",
"@rushstack/eslint-config": "2.5.1",
"@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",
"@types/webpack-env": "1.15.2",
"@typescript-eslint/eslint-plugin": "5.31.0",
"@typescript-eslint/parser": "5.31.0",
"ajv": "6.12.5",
"eslint": "8.20.0",
"eslint-plugin-react": "7.30.1",
"eslint-plugin-react-hooks": "4.3.0",
"gulp": "~4.0.2",
"ajv": "~6.12.3",
"@types/webpack-env": "1.13.1"
"gulp-eslint": "6.0.0"
}
}

View File

@ -1,20 +1,36 @@
import { sp } from "@pnp/sp";
import { ServiceScope } from "@microsoft/sp-core-library";
import { PageContext } from "@microsoft/sp-page-context";
import { SPFI, spfi, SPFx } from "@pnp/sp";
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";
import "@pnp/sp/items/get-all";
import "@pnp/sp/files";
import "@pnp/sp/folders";
import { IFileItem } from "../model/IFileItem";
import { IItem } from "@pnp/sp/items";
export class SPService {
private listName: string;
private fieldName: string;
private _listName: string;
private _fieldName: string;
private _sp: SPFI;
constructor (listname: string, fieldname: string) {
this.listName = listname;
this.fieldName = fieldname;
public constructor (serviceScope: ServiceScope, listname: string, fieldname: string) {
this._listName = listname;
this._fieldName = fieldname;
serviceScope.whenFinished(() => {
const pageContext: PageContext = serviceScope.consume(PageContext.serviceKey);
this._sp = spfi().using(SPFx({ pageContext }));
});
}
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();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const items: any[] = await this._sp.web.lists.getByTitle(this._listName).items.select('Id', this._fieldName).expand('File').getAll();
const files: IFileItem[] = [];
items.forEach(i => {
const nameparts = i.File.Name.split('.');
const nameparts: string[] = i.File.Name.split('.');
const file: IFileItem = {
title: i.File.Name,
extension: nameparts[nameparts.length - 1],
@ -23,10 +39,10 @@ export class SPService {
taxValue: [""],
url: i.File.LinkingUrl
};
if (Array.isArray(i[this.fieldName])) {
if (Array.isArray(i[this._fieldName])) {
const termguids: string[] = [];
const taxvalues: string[] = [];
i[this.fieldName].forEach(f => {
i[this._fieldName].forEach(f => {
termguids.push(f.TermGuid.toLowerCase());
taxvalues.push(`${f.Label}|${f.TermGuid}`);
});
@ -34,21 +50,21 @@ export class SPService {
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()}`]:[""];
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) {
public async updateTaxonomyItemByAdd(file: IFileItem, fieldName: string, newTaxonomyValue: string): Promise<void> {
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([{
await this._sp.web.lists.getByTitle(this._listName).items.getById(itemID).validateUpdateListItem([{
ErrorMessage: null,
FieldName: fieldName,
FieldValue: fieldValues,
@ -56,10 +72,10 @@ export class SPService {
}]);
}
public async updateTaxonomyItemByReplace (file: IFileItem, fieldName: string, newTaxonomyValue: string) {
public async updateTaxonomyItemByReplace (file: IFileItem, fieldName: string, newTaxonomyValue: string): Promise<void> {
const itemID: number = parseInt(file.id);
await sp.web.lists.getByTitle(this.listName).items.getById(itemID).validateUpdateListItem([{
await this._sp.web.lists.getByTitle(this._listName).items.getById(itemID).validateUpdateListItem([{
ErrorMessage: null,
FieldName: fieldName,
FieldValue: newTaxonomyValue,
@ -69,16 +85,16 @@ export class SPService {
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);
const currentFileNamePart: string = file.title.replace(`.${file.extension}`, '');
// eslint-disable-next-line @typescript-eslint/no-inferrable-types
const newFilename: string = `${currentFileNamePart}_Copy.${file.extension}`;
const destinationUrl: string = decodeURI(fileUrl.pathname).replace(file.title, newFilename);
await this._sp.web.getFileByServerRelativePath(decodeURI(fileUrl.pathname)).copyByPath(destinationUrl, false, true);
const newFileItemPromise: IItem = await this._sp.web.getFileByServerRelativePath(destinationUrl).getItem();
const newFileItem = await newFileItemPromise.file.getItem<{ Id: number }>("Id");
const itemID: number = newFileItem.Id;
await sp.web.lists.getByTitle(this.listName).items.getById(itemID).validateUpdateListItem([{
await this._sp.web.lists.getByTitle(this._listName).items.getById(itemID).validateUpdateListItem([{
ErrorMessage: null,
FieldName: fieldName,
FieldValue: newTaxonomyValue,
@ -94,4 +110,32 @@ export class SPService {
};
return newFile;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public async newTaxonomyItemByUpload (file: any, fieldName: string, newTaxonomyValue: string): Promise<IFileItem> {
const libraryRoot = await this._sp.web.lists.getByTitle(this._listName).rootFolder();
// Assuming small file size, otherwise use chunks
const result = await this._sp.web.getFolderByServerRelativePath(libraryRoot.ServerRelativeUrl).files.addUsingPath(file.name, file, { Overwrite: true });
const fileNameParts: string[] = result.data.Name.split('.');
const newFileItemPromise = await this._sp.web.getFileByServerRelativePath(result.data.ServerRelativeUrl).getItem();
const newFileItem = await newFileItemPromise.file.getItem<{ Id: number }>("Id");
const itemID: number = newFileItem.Id;
await this._sp.web.lists.getByTitle(this._listName).items.getById(itemID).validateUpdateListItem([{
ErrorMessage: null,
FieldName: fieldName,
FieldValue: newTaxonomyValue,
HasException: false
}]);
const newFile: IFileItem = {
extension: fileNameParts[fileNameParts.length - 1],
id: itemID.toString(),
taxValue: [newTaxonomyValue],
termGuid: [newTaxonomyValue.split('|')[1]],
title: file.name,
url: result.data.ServerRelativeUrl
};
return newFile;
}
}

View File

@ -1,29 +1,43 @@
import { sp } from "@pnp/sp";
import { IOrderedTermInfo } from "@pnp/sp/taxonomy";
import { ServiceScope } from "@microsoft/sp-core-library";
import { PageContext } from "@microsoft/sp-page-context";
import { SPFI, spfi, SPFx } from "@pnp/sp";
import "@pnp/sp/taxonomy";
import { IOrderedTermInfo, ITermInfo } from "@pnp/sp/taxonomy";
import "@pnp/sp/fields";
import { IFileItem } from "../model/IFileItem";
import { ITermNode } from "../model/ITermNode";
export class TaxonomyService {
private _sp: SPFI;
public constructor (serviceScope: ServiceScope) {
serviceScope.whenFinished(() => {
const pageContext: PageContext = serviceScope.consume(PageContext.serviceKey);
this._sp = spfi().using(SPFx({ pageContext }));
});
}
public async getTermsetInfo (fieldName: string): Promise<string> {
const mmFieldInfo = await sp.web.fields.getByInternalNameOrTitle(fieldName).get();
const mmFieldInfo = await this._sp.web.fields.getByInternalNameOrTitle(fieldName)();
const parser = new DOMParser();
const xmlField = parser.parseFromString(mmFieldInfo.SchemaXml, "text/xml");
const properties = xmlField.getElementsByTagName("ArrayOfProperty")[0].childNodes;
// eslint-disable-next-line @typescript-eslint/no-inferrable-types
let termsetID: string = "";
properties.forEach(prop => {
if (prop.childNodes[0].textContent == "TermSetId") {
if (prop.childNodes[0].textContent === "TermSetId") {
termsetID = prop.childNodes[1].textContent;
}
});
return termsetID;
}
public async getTermset (termsetID: string) {
public async getTermset(termsetID: string): Promise<ITermNode[]> {
// list all the terms available in this term set by term set id
const termset: IOrderedTermInfo[] = await sp.termStore.sets.getById(termsetID).getAllChildrenAsOrderedTree();
const termset: IOrderedTermInfo[] = await this._sp.termStore.sets.getById(termsetID).getAllChildrenAsOrderedTree();
const termnodes: ITermNode[] = [];
termset.forEach(async ti => {
const tn = this.getTermnode(ti);
const tn = this._getTermnode(ti);
termnodes.push(tn);
});
return termnodes;
@ -31,23 +45,23 @@ export class TaxonomyService {
public incorporateFiles (terms: ITermNode[], files: IFileItem[]): ITermNode[] {
terms.forEach(term => {
term = this.incorporateFilesIntoTerm(term, files);
term = this._incorporateFilesIntoTerm(term, files);
});
return terms;
}
private getTermnode (term: IOrderedTermInfo): ITermNode {
private _getTermnode (term: ITermInfo): ITermNode {
const node: ITermNode = {
guid: term.id,
childDocuments: 0,
name: term.defaultLabel,
name: term.labels.filter(i => i.isDefault === true)[0].name,
children: [],
subFiles: []
};
if (term.childrenCount > 0) {
const ctnodes: ITermNode[] = [];
term.children.forEach(ct => {
const ctnode: ITermNode = this.getTermnode(ct);
const ctnode: ITermNode = this._getTermnode(ct);
node.childDocuments += ctnode.childDocuments;
ctnodes.push(ctnode);
});
@ -56,12 +70,12 @@ export class TaxonomyService {
return node;
}
private incorporateFilesIntoTerm (term: ITermNode, files: IFileItem[]): ITermNode {
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);
ct = this._incorporateFilesIntoTerm(ct, files);
term.childDocuments += ct.childDocuments;
});
}

View File

@ -14,7 +14,7 @@
"requiresCustomScript": false,
"supportedHosts": ["SharePointWebPart", "TeamsPersonalApp", "TeamsTab", "SharePointFullPage"],
"supportsThemeVariants": true,
"hiddenFromToolbox": false,
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },

View File

@ -5,7 +5,6 @@ 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';
@ -18,18 +17,11 @@ export interface ITaxonomyFileExplorerWebPartProps {
}
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,
{
serviceScope: this.context.serviceScope,
fieldName: this.properties.fieldName,
listName: this.properties.listName
}

View File

@ -7,9 +7,9 @@ import { IFileLabelProps } from './IFileLabelProps';
initializeFileTypeIcons(undefined);
export const FileLabel: React.FC<IFileLabelProps> = (props) => {
const drag = (ev) => {
const drag = React.useCallback((ev)=> {
ev.dataTransfer.setData("text/plain", JSON.stringify(props.file));
};
}, [props.file]);
return (
<li className={styles.fileLabel} draggable={true} onDragStart={drag}>

View File

@ -1,4 +1,7 @@
import { ServiceScope } from "@microsoft/sp-core-library";
export interface ITaxonomyFileExplorerProps {
serviceScope: ServiceScope;
fieldName: string;
listName: string;
}

View File

@ -4,9 +4,13 @@ import { ITermNode } from "../../../model/ITermNode";
export interface ITermLabelProps {
node: ITermNode;
selectedNode: string;
collapseAll: boolean;
expandAll: boolean;
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;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
uploadFile: (file: any, newValue: string) => void;
}

View File

@ -10,14 +10,17 @@
.row {
@include ms-Grid-row;
color: "[theme: themePrimary, default: #0078d7]";
padding: 20px;
padding: 8px 20px;
}
.column {
@include ms-Grid-col;
@include ms-lg6;
}
.icon {
margin-right: 6px;
cursor: pointer;
}
ul {
list-style: none;
padding-inline-start: 0px;

View File

@ -7,6 +7,7 @@ import { TaxonomyService } from '../../../services/TaxonomyService';
import { SPService } from '../../../services/SPService';
import { FileLabel } from './FileLabel';
import { TermLabel } from './TermLabel';
import { Icon } from 'office-ui-fabric-react';
export const TaxonomyFileExplorer: React.FC<ITaxonomyFileExplorerProps> = (props) => {
const [spSvc, setSpSvc] = React.useState<SPService>();
@ -14,12 +15,15 @@ export const TaxonomyFileExplorer: React.FC<ITaxonomyFileExplorerProps> = (props
const [terms, setTerms] = React.useState<ITermNode[]>([]);
const [shownFiles, setShownFiles] = React.useState<IFileItem[]>([]);
const [selectedTermnode, setSelectedTermnode] = React.useState<string>("");
const [collapseAll, setCollapseAll] = React.useState<boolean>(false);
const [expandAll, setExpandAll] = React.useState<boolean>(false);
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const buildTree = async () => {
const taxSvc: TaxonomyService = new TaxonomyService();
const termsetID = await taxSvc.getTermsetInfo(props.fieldName);
const taxSvc: TaxonomyService = new TaxonomyService(props.serviceScope);
const termsetID: string = await taxSvc.getTermsetInfo(props.fieldName);
let termnodetree: ITermNode[];
const termnodetreeStr = sessionStorage.getItem(`Termtree_${termsetID}`);
const termnodetreeStr: string = sessionStorage.getItem(`Termtree_${termsetID}`);
if (termnodetreeStr === null) {
termnodetree = await taxSvc.getTermset(termsetID);
sessionStorage.setItem(`Termtree_${termsetID}`, JSON.stringify(termnodetree));
@ -28,27 +32,30 @@ export const TaxonomyFileExplorer: React.FC<ITaxonomyFileExplorerProps> = (props
termnodetree = JSON.parse(termnodetreeStr);
}
const spSrvc: SPService = new SPService(props.listName, props.fieldName);
const files = await spSrvc.getItems(termsetID);
const spSrvc: SPService = new SPService(props.serviceScope, props.listName, props.fieldName);
const files: IFileItem[] = await spSrvc.getItems(termsetID);
setSpSvc(spSrvc);
// eslint-disable-next-line @typescript-eslint/no-use-before-define
updateFiles(files, termnodetree);
};
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const updateFiles = (files: IFileItem[], termnodetree: ITermNode[]) => {
const taxSvc: TaxonomyService = new TaxonomyService();
const taxSvc: TaxonomyService = new TaxonomyService(props.serviceScope);
termnodetree = taxSvc.incorporateFiles(termnodetree, files);
setFileItems(files);
setTerms(termnodetree);
};
const renderFiles = (files: IFileItem[]) => {
const renderFiles = React.useCallback((files: IFileItem[]) => {
setShownFiles(files);
};
},[setShownFiles]);
const resetChecked = (newNodeID: string) => {
const resetChecked = React.useCallback((newNodeID: string) => {
setSelectedTermnode(newNodeID);
};
},[setSelectedTermnode]);
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const reloadFiles = (file: IFileItem) => {
const newFiles: IFileItem[] = [];
fileItems.forEach(fi => {
@ -62,50 +69,75 @@ export const TaxonomyFileExplorer: React.FC<ITaxonomyFileExplorerProps> = (props
updateFiles(newFiles, terms);
};
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const loadNewFiles = (file: IFileItem) => {
const newFiles: IFileItem[] = [file].concat(fileItems);
updateFiles(newFiles, terms);
};
const addTerm = (file: IFileItem, newTaxonomyValue: string) => {
const addTerm = React.useCallback((file: IFileItem, newTaxonomyValue: string) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
spSvc.updateTaxonomyItemByAdd(file, props.fieldName, newTaxonomyValue);
reloadFiles(file);
};
},[spSvc, fileItems, terms]); // eslint-disable-line react-hooks/exhaustive-deps
const replaceTerm = (file: IFileItem, newTaxonomyValue: string) => {
const replaceTerm = React.useCallback((file: IFileItem, newTaxonomyValue: string) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
spSvc.updateTaxonomyItemByReplace(file, props.fieldName, newTaxonomyValue);
reloadFiles(file);
};
},[spSvc, fileItems, terms]); // eslint-disable-line react-hooks/exhaustive-deps
const copyFile = async (file: IFileItem, newTaxonomyValue: string) => {
const copyFile = React.useCallback(async (file: IFileItem, newTaxonomyValue: string) => {
const newFile = await spSvc.newTaxonomyItemByCopy(file, props.fieldName, newTaxonomyValue);
loadNewFiles(newFile);
};
},[spSvc, fileItems, terms]); // eslint-disable-line react-hooks/exhaustive-deps
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const uploadFile = React.useCallback(async (file: any, newTaxonomyValue: string) => {
const newFile = await spSvc.newTaxonomyItemByUpload(file, props.fieldName, newTaxonomyValue)
loadNewFiles(newFile);
},[spSvc, fileItems, terms]); // eslint-disable-line react-hooks/exhaustive-deps
const expandAllTerms = React.useCallback(() => {
setExpandAll(true);
setCollapseAll(false);
},[setExpandAll, setCollapseAll]);
const collapseAllTerms = React.useCallback(() => {
setCollapseAll(true);
setExpandAll(false);
},[setExpandAll, setCollapseAll]);
React.useEffect(() => {
buildTree();
}, []);
buildTree(); // eslint-disable-line @typescript-eslint/no-floating-promises
}, []); // eslint-disable-line react-hooks/exhaustive-deps
return (
<div className={ styles.taxonomyFileExplorer }>
<div className={ styles.container }>
<div className={ styles.container }>
<div className={ styles.row }>
<div className={ styles.column }>
<Icon className={styles.icon} iconName="ExploreContent" onClick={expandAllTerms} /> {/* Alt: DoubleChevronRight */}
<Icon className={styles.icon} iconName="CollapseContent" onClick={collapseAllTerms} /> {/* Alt: DoubleChevronDown */}
<ul>
{terms.map(nc => { return <TermLabel node={nc}
{terms.map(nc => { return <TermLabel node={nc}
key={nc.guid}
renderFiles={renderFiles}
resetChecked={resetChecked}
selectedNode={selectedTermnode}
collapseAll={collapseAll}
expandAll={expandAll}
addTerm={addTerm}
replaceTerm={replaceTerm}
copyFile={copyFile} />; })}
copyFile={copyFile}
uploadFile={uploadFile} />; })}
</ul>
</div>
<div className={ styles.column }>
{shownFiles.length > 0 &&
<ul>
{shownFiles.map(f => {
return <FileLabel file={f} />;
return <FileLabel file={f} key={f.id} />;
})}
</ul>}
</div>

View File

@ -17,6 +17,9 @@
.checkedLabel {
border: 1px dotted "[theme: themePrimary, default: #0078d7]";
}
.dragEnter {
background-color: "[theme: themeLight, default: #c7e0f4]";
}
.icon {
margin-right: 6px;
cursor: pointer;

View File

@ -11,60 +11,89 @@ export const TermLabel: React.FC<ITermLabelProps> = (props) => {
const [countDocuments, setCountDocuments] = React.useState<number>(props.node.childDocuments);
const [showContextualMenu, setShowContextualMenu] = React.useState<boolean>(false);
const [droppedFile, setDroppedFile] = React.useState<IFileItem>();
const [dragEntered, setDragEntered] = React.useState<boolean>(false);
const toggleIcon = () => {
const toggleIcon = React.useCallback(() => {
setShowChildren(!showChildren);
};
},[setShowChildren,showChildren]);
const nodeSelected = () => {
const nodeSelected = React.useCallback(() => {
props.resetChecked(props.node.guid);
props.renderFiles(props.node.subFiles);
};
},[]); // eslint-disable-line react-hooks/exhaustive-deps
const hideContextualMenu = () => {
const hideContextualMenu = React.useCallback(() => {
setShowContextualMenu(false);
};
},[setShowContextualMenu]);
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}`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const uploadWithNewTerm = React.useCallback((file: any) => {
// eslint-disable-next-line @typescript-eslint/no-inferrable-types
const newTaxonomyValue: string = `${props.node.name}|${props.node.guid}`;
props.uploadFile(file, newTaxonomyValue);
},[]); // eslint-disable-line react-hooks/exhaustive-deps
const addNewTerm = React.useCallback((file: IFileItem) => {
// eslint-disable-next-line @typescript-eslint/no-inferrable-types
const newTaxonomyValue: string = `${props.node.name}|${props.node.guid}`;
file.termGuid.push(props.node.guid);
file.taxValue.push(newTaxonomyValue);
console.log(file);
props.addTerm(file, newTaxonomyValue);
};
},[]); // eslint-disable-line react-hooks/exhaustive-deps
const drop = React.useCallback((ev) => {
ev.preventDefault();
// Drop is a file or a FileLabel
if (ev.dataTransfer.types.indexOf('Files') > -1) {
const dt = ev.dataTransfer;
const files = Array.prototype.slice.call(dt.files);
files.forEach(fileToUpload => {
uploadWithNewTerm(fileToUpload);
});
}
else {
const data: string = 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
}
}
},[uploadWithNewTerm, addNewTerm]);
const dragOver = React.useCallback((ev) => {
ev.preventDefault();
},[]);
const dragEnter = React.useCallback((ev) => {
setDragEntered(true);
},[setDragEntered]);
const dragLeave = React.useCallback((ev) => {
setDragEntered(false);
},[setDragEntered]);
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const replaceByNewTerm = (file: IFileItem) => {
const newTaxonomyValue = `${props.node.name}|${props.node.guid}`;
// eslint-disable-next-line @typescript-eslint/no-inferrable-types
const newTaxonomyValue: string = `${props.node.name}|${props.node.guid}`;
file.termGuid = [props.node.guid];
file.taxValue = [newTaxonomyValue];
console.log(file);
props.addTerm(file, newTaxonomyValue);
};
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const copyWithNewTerm = (file: IFileItem) => {
const newTaxonomyValue = `${props.node.name}|${props.node.guid}`;
console.log(file);
// eslint-disable-next-line @typescript-eslint/no-inferrable-types
const newTaxonomyValue: string = `${props.node.name}|${props.node.guid}`;
props.copyFile(file, newTaxonomyValue);
};
const currentExpandIcon = showChildren? <Icon className={styles.icon} iconName="ChevronDown" onClick={toggleIcon} />:<Icon className={styles.icon} iconName="ChevronRight" onClick={toggleIcon} />;
const currentExpandIcon: JSX.Element = showChildren? <Icon className={styles.icon} iconName="ChevronDown" onClick={toggleIcon} />:<Icon className={styles.icon} iconName="ChevronRight" onClick={toggleIcon} />;
const menuItems: IContextualMenuItem[] = [
{
key: 'copyItem',
@ -81,6 +110,14 @@ export const TermLabel: React.FC<ITermLabelProps> = (props) => {
text: 'Add new term (Link)',
onClick: () => addNewTerm(droppedFile)
}];
React.useEffect(() => {
if (props.expandAll) {
setShowChildren(true);
}
if (props.collapseAll) {
setShowChildren(false);
}
}, [props.collapseAll, props.expandAll]);
React.useEffect(() => {
if (props.selectedNode===props.node.guid) {
props.renderFiles(props.node.subFiles);
@ -88,32 +125,42 @@ export const TermLabel: React.FC<ITermLabelProps> = (props) => {
if (props.node.childDocuments !== countDocuments) {
setCountDocuments(props.node.childDocuments);
}
}, [props.node.subFiles]);
}, [props.node.subFiles]); // eslint-disable-line react-hooks/exhaustive-deps
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>
<li className={styles.termLabel}>
<div ref={linkRef} className={`${styles.label} ${props.selectedNode===props.node.guid ? styles.checkedLabel : ""}
${dragEntered ? styles.dragEnter : ""}`}
onClick={nodeSelected}
onDrop={drop}
onDragOver={dragOver}
onDragEnter={dragEnter}
onDragLeave={dragLeave}>
<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}
key={nc.guid}
renderFiles={props.renderFiles}
resetChecked={props.resetChecked}
selectedNode={props.selectedNode}
collapseAll={props.collapseAll}
expandAll={props.expandAll}
addTerm={props.addTerm}
replaceTerm={props.replaceTerm}
copyFile={props.copyFile}
uploadFile={props.uploadFile} />; })}
</ul>}
</li>
);
};

View File

@ -1,5 +1,5 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.9/includes/tsconfig-web.json",
"extends": "./node_modules/@microsoft/rush-stack-compiler-4.5/includes/tsconfig-web.json",
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,

View File

@ -1,29 +0,0 @@
{
"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
}
}