New feature Expand/Collapse all upgrd SPFx 1.15.0

This commit is contained in:
mmsharepoint 2022-07-19 11:32:40 +02:00
parent fa3a66492d
commit 91b09cb163
No known key found for this signature in database
GPG Key ID: 00CC809AC5E88082
19 changed files with 5564 additions and 2961 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 # Logs
*.cer logs
# .PEM Certificates *.log
*.pem 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, "plusBeta": false,
"isCreatingSolution": true, "isCreatingSolution": true,
"environment": "spo", "environment": "spo",
"version": "1.13.1", "version": "1.15.0",
"libraryName": "react-taxonomy-file-explorer", "libraryName": "react-taxonomy-file-explorer",
"libraryId": "5697a573-1bd1-4ff3-96f1-82263c8eb008", "libraryId": "5697a573-1bd1-4ff3-96f1-82263c8eb008",
"packageManager": "npm", "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) ![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 ## 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) ![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) ![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 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) ![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) ![Compatible with Remote Containers](https://img.shields.io/badge/Remote%20Containers-Compatible-green.svg)
## Applies to ## Applies to
- [SharePoint Framework](https://aka.ms/spfx) - [SharePoint Framework](https://aka.ms/spfx)
@ -60,6 +62,7 @@ react-taxonomy-file-explorer| [Markus Moeller](https://github.com/mmsharepoint)
Version|Date|Comments Version|Date|Comments
-------|----|-------- -------|----|--------
1.0|December 26, 2021|Initial release 1.0|December 26, 2021|Initial release
1.1|July 16, 2021|Added expand/collapse all, upgraded to SPFx 1.15.0, upgraded to PnPJS V3.5.1
## Minimal Path to Awesome ## Minimal Path to Awesome

View File

@ -7,12 +7,31 @@
"includeClientSideAssets": true, "includeClientSideAssets": true,
"isDomainIsolated": false, "isDomainIsolated": false,
"developer": { "developer": {
"name": "", "name": "Markus Moeller",
"websiteUrl": "", "websiteUrl": "",
"privacyUrl": "", "privacyUrl": "",
"termsOfUseUrl": "", "termsOfUseUrl": "",
"mpnId": "Undefined-1.13.1" "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": { "paths": {
"zippedPackage": "solution/react-taxonomy-file-explorer.sppkg" "zippedPackage": "solution/react-taxonomy-file-explorer.sppkg"

File diff suppressed because it is too large Load Diff

View File

@ -9,26 +9,32 @@
"test": "gulp test" "test": "gulp test"
}, },
"dependencies": { "dependencies": {
"@fluentui/react-file-type-icons": "^8.5.7", "@fluentui/react-file-type-icons": "^8.6.11",
"@microsoft/sp-core-library": "1.13.1", "@microsoft/sp-core-library": "1.15.0",
"@microsoft/sp-lodash-subset": "1.13.1", "@microsoft/sp-lodash-subset": "1.15.0",
"@microsoft/sp-office-ui-fabric-core": "1.13.1", "@microsoft/sp-office-ui-fabric-core": "1.15.0",
"@microsoft/sp-property-pane": "1.13.1", "@microsoft/sp-property-pane": "1.15.0",
"@microsoft/sp-webpart-base": "1.13.1", "@microsoft/sp-webpart-base": "1.15.0",
"@pnp/sp": "^2.11.0", "@pnp/sp": "^3.5.1",
"office-ui-fabric-react": "7.174.1", "office-ui-fabric-react": "7.185.7",
"react": "16.13.1", "react": "16.13.1",
"react-dom": "16.13.1" "react-dom": "16.13.1",
"tslib": "2.3.1"
}, },
"devDependencies": { "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": "16.9.51",
"@types/react-dom": "16.9.8", "@types/react-dom": "16.9.8",
"@microsoft/sp-build-web": "1.13.1", "@types/webpack-env": "1.15.2",
"@microsoft/sp-tslint-rules": "1.13.1", "ajv": "6.12.5",
"@microsoft/sp-module-interfaces": "1.13.1", "eslint": "8.7.0",
"@microsoft/rush-stack-compiler-3.9": "0.4.47", "eslint-plugin-react-hooks": "4.3.0",
"gulp": "~4.0.2", "gulp": "~4.0.2"
"ajv": "~6.12.3",
"@types/webpack-env": "1.13.1"
} }
} }

View File

@ -1,20 +1,35 @@
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 { IFileItem } from "../model/IFileItem";
import { IItem } from "@pnp/sp/items";
export class SPService { export class SPService {
private listName: string; private _listName: string;
private fieldName: string; private _fieldName: string;
private _sp: SPFI;
constructor (listname: string, fieldname: string) { public constructor (serviceScope: ServiceScope, listname: string, fieldname: string) {
this.listName = listname; this._listName = listname;
this.fieldName = fieldname; 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[]> { 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 items: any[] = await this._sp.web.lists.getByTitle(this._listName).items.select('Id', this._fieldName).expand('File').getAll();
const files: IFileItem[] = []; const files: IFileItem[] = [];
items.forEach(i => { items.forEach(i => {
const nameparts = i.File.Name.split('.'); const nameparts: string[] = i.File.Name.split('.');
const file: IFileItem = { const file: IFileItem = {
title: i.File.Name, title: i.File.Name,
extension: nameparts[nameparts.length - 1], extension: nameparts[nameparts.length - 1],
@ -23,10 +38,10 @@ export class SPService {
taxValue: [""], taxValue: [""],
url: i.File.LinkingUrl url: i.File.LinkingUrl
}; };
if (Array.isArray(i[this.fieldName])) { if (Array.isArray(i[this._fieldName])) {
const termguids: string[] = []; const termguids: string[] = [];
const taxvalues: string[] = []; const taxvalues: string[] = [];
i[this.fieldName].forEach(f => { i[this._fieldName].forEach(f => {
termguids.push(f.TermGuid.toLowerCase()); termguids.push(f.TermGuid.toLowerCase());
taxvalues.push(`${f.Label}|${f.TermGuid}`); taxvalues.push(`${f.Label}|${f.TermGuid}`);
}); });
@ -34,21 +49,21 @@ export class SPService {
file.taxValue = taxvalues; file.taxValue = taxvalues;
} }
else { else {
file.termGuid = i[this.fieldName] ? [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()}`]:[""]; file.taxValue = i[this._fieldName] ? [`${i[this._fieldName].Label.toLowerCase()}|${i[this._fieldName].TermGuid.toLowerCase()}`]:[""];
} }
files.push(file); files.push(file);
}); });
return files; 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); const itemID: number = parseInt(file.id);
let fieldValues = file.taxValue.join(';'); let fieldValues = file.taxValue.join(';');
fieldValues += `;${newTaxonomyValue}`; fieldValues += `;${newTaxonomyValue}`;
// https://blog.aterentiev.com/how-to-easily-update-managed-metadata // 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, ErrorMessage: null,
FieldName: fieldName, FieldName: fieldName,
FieldValue: fieldValues, FieldValue: fieldValues,
@ -56,10 +71,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); 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, ErrorMessage: null,
FieldName: fieldName, FieldName: fieldName,
FieldValue: newTaxonomyValue, FieldValue: newTaxonomyValue,
@ -69,16 +84,15 @@ export class SPService {
public async newTaxonomyItemByCopy (file: IFileItem, fieldName: string, newTaxonomyValue: string): Promise<IFileItem> { public async newTaxonomyItemByCopy (file: IFileItem, fieldName: string, newTaxonomyValue: string): Promise<IFileItem> {
const fileUrl: URL = new URL(file.url); const fileUrl: URL = new URL(file.url);
const currentFileNamePart = file.title.replace(`.${file.extension}`, ''); const currentFileNamePart: string = file.title.replace(`.${file.extension}`, '');
const newFilename = `${currentFileNamePart}_Copy.${file.extension}`; const newFilename: string = `${currentFileNamePart}_Copy.${file.extension}`;
const destinationUrl = decodeURI(fileUrl.pathname).replace(file.title, newFilename); const destinationUrl: string = decodeURI(fileUrl.pathname).replace(file.title, newFilename);
await sp.web.getFileByServerRelativePath(decodeURI(fileUrl.pathname)).copyByPath(destinationUrl, false, true); await this._sp.web.getFileByServerRelativePath(decodeURI(fileUrl.pathname)).copyByPath(destinationUrl, false, true);
const newFileItemPromise = await sp.web.getFileByServerRelativePath(destinationUrl).getItem(); const newFileItemPromise: IItem = await this._sp.web.getFileByServerRelativePath(destinationUrl).getItem();
const newFileItem = await newFileItemPromise.get(); const newFileItem = await newFileItemPromise.file.getItem<{ Id: number }>("Id");
console.log(newFileItem); const itemID: number = newFileItem.Id;
const itemID: number = parseInt(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, ErrorMessage: null,
FieldName: fieldName, FieldName: fieldName,
FieldValue: newTaxonomyValue, FieldValue: newTaxonomyValue,
@ -94,4 +108,31 @@ export class SPService {
}; };
return newFile; return newFile;
} }
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,42 @@
import { sp } from "@pnp/sp"; import { ServiceScope } from "@microsoft/sp-core-library";
import { IOrderedTermInfo } from "@pnp/sp/taxonomy"; 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 { IFileItem } from "../model/IFileItem";
import { ITermNode } from "../model/ITermNode"; import { ITermNode } from "../model/ITermNode";
export class TaxonomyService { 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> { 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 parser = new DOMParser();
const xmlField = parser.parseFromString(mmFieldInfo.SchemaXml, "text/xml"); const xmlField = parser.parseFromString(mmFieldInfo.SchemaXml, "text/xml");
const properties = xmlField.getElementsByTagName("ArrayOfProperty")[0].childNodes; const properties = xmlField.getElementsByTagName("ArrayOfProperty")[0].childNodes;
let termsetID: string = ""; let termsetID: string = "";
properties.forEach(prop => { properties.forEach(prop => {
if (prop.childNodes[0].textContent == "TermSetId") { if (prop.childNodes[0].textContent === "TermSetId") {
termsetID = prop.childNodes[1].textContent; termsetID = prop.childNodes[1].textContent;
} }
}); });
return termsetID; 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 // 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[] = []; const termnodes: ITermNode[] = [];
termset.forEach(async ti => { termset.forEach(async ti => {
const tn = this.getTermnode(ti); const tn = this._getTermnode(ti);
termnodes.push(tn); termnodes.push(tn);
}); });
return termnodes; return termnodes;
@ -31,23 +44,23 @@ export class TaxonomyService {
public incorporateFiles (terms: ITermNode[], files: IFileItem[]): ITermNode[] { public incorporateFiles (terms: ITermNode[], files: IFileItem[]): ITermNode[] {
terms.forEach(term => { terms.forEach(term => {
term = this.incorporateFilesIntoTerm(term, files); term = this._incorporateFilesIntoTerm(term, files);
}); });
return terms; return terms;
} }
private getTermnode (term: IOrderedTermInfo): ITermNode { private _getTermnode (term: ITermInfo): ITermNode {
const node: ITermNode = { const node: ITermNode = {
guid: term.id, guid: term.id,
childDocuments: 0, childDocuments: 0,
name: term.defaultLabel, name: term.labels.filter(i => i.isDefault === true)[0].name,
children: [], children: [],
subFiles: [] subFiles: []
}; };
if (term.childrenCount > 0) { if (term.childrenCount > 0) {
const ctnodes: ITermNode[] = []; const ctnodes: ITermNode[] = [];
term.children.forEach(ct => { term.children.forEach(ct => {
const ctnode: ITermNode = this.getTermnode(ct); const ctnode: ITermNode = this._getTermnode(ct);
node.childDocuments += ctnode.childDocuments; node.childDocuments += ctnode.childDocuments;
ctnodes.push(ctnode); ctnodes.push(ctnode);
}); });
@ -56,12 +69,12 @@ export class TaxonomyService {
return node; return node;
} }
private incorporateFilesIntoTerm (term: ITermNode, files: IFileItem[]): ITermNode { private _incorporateFilesIntoTerm (term: ITermNode, files: IFileItem[]): ITermNode {
term.childDocuments = 0; term.childDocuments = 0;
term.subFiles = []; term.subFiles = [];
if (term.children.length > 0) { if (term.children.length > 0) {
term.children.forEach(ct => { term.children.forEach(ct => {
ct = this.incorporateFilesIntoTerm(ct, files); ct = this._incorporateFilesIntoTerm(ct, files);
term.childDocuments += ct.childDocuments; term.childDocuments += ct.childDocuments;
}); });
} }

View File

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

View File

@ -5,7 +5,6 @@ import {
IPropertyPaneConfiguration, IPropertyPaneConfiguration,
PropertyPaneTextField PropertyPaneTextField
} from '@microsoft/sp-property-pane'; } from '@microsoft/sp-property-pane';
import { sp } from "@pnp/sp/presets/all";
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base'; import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import * as strings from 'TaxonomyFileExplorerWebPartStrings'; import * as strings from 'TaxonomyFileExplorerWebPartStrings';
@ -18,18 +17,11 @@ export interface ITaxonomyFileExplorerWebPartProps {
} }
export default class TaxonomyFileExplorerWebPart extends BaseClientSideWebPart<ITaxonomyFileExplorerWebPartProps> { export default class TaxonomyFileExplorerWebPart extends BaseClientSideWebPart<ITaxonomyFileExplorerWebPartProps> {
protected onInit(): Promise<void> {
return super.onInit().then(_ => {
sp.setup({
spfxContext: this.context
});
});
}
public render(): void { public render(): void {
const element: React.ReactElement<ITaxonomyFileExplorerProps> = React.createElement( const element: React.ReactElement<ITaxonomyFileExplorerProps> = React.createElement(
TaxonomyFileExplorer, TaxonomyFileExplorer,
{ {
serviceScope: this.context.serviceScope,
fieldName: this.properties.fieldName, fieldName: this.properties.fieldName,
listName: this.properties.listName listName: this.properties.listName
} }

View File

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

View File

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

View File

@ -4,9 +4,12 @@ import { ITermNode } from "../../../model/ITermNode";
export interface ITermLabelProps { export interface ITermLabelProps {
node: ITermNode; node: ITermNode;
selectedNode: string; selectedNode: string;
collapseAll: boolean;
expandAll: boolean;
renderFiles: (files: IFileItem[]) => void; renderFiles: (files: IFileItem[]) => void;
resetChecked: (s: string) => void; resetChecked: (s: string) => void;
addTerm: (file: IFileItem, newValue: string) => void; addTerm: (file: IFileItem, newValue: string) => void;
replaceTerm: (file: IFileItem, newValue: string) => void; replaceTerm: (file: IFileItem, newValue: string) => void;
copyFile: (file: IFileItem, newValue: string) => void; copyFile: (file: IFileItem, newValue: string) => void;
uploadFile: (file: any, newValue: string) => void;
} }

View File

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

View File

@ -7,6 +7,7 @@ import { TaxonomyService } from '../../../services/TaxonomyService';
import { SPService } from '../../../services/SPService'; import { SPService } from '../../../services/SPService';
import { FileLabel } from './FileLabel'; import { FileLabel } from './FileLabel';
import { TermLabel } from './TermLabel'; import { TermLabel } from './TermLabel';
import { Icon } from 'office-ui-fabric-react';
export const TaxonomyFileExplorer: React.FC<ITaxonomyFileExplorerProps> = (props) => { export const TaxonomyFileExplorer: React.FC<ITaxonomyFileExplorerProps> = (props) => {
const [spSvc, setSpSvc] = React.useState<SPService>(); const [spSvc, setSpSvc] = React.useState<SPService>();
@ -14,12 +15,14 @@ export const TaxonomyFileExplorer: React.FC<ITaxonomyFileExplorerProps> = (props
const [terms, setTerms] = React.useState<ITermNode[]>([]); const [terms, setTerms] = React.useState<ITermNode[]>([]);
const [shownFiles, setShownFiles] = React.useState<IFileItem[]>([]); const [shownFiles, setShownFiles] = React.useState<IFileItem[]>([]);
const [selectedTermnode, setSelectedTermnode] = React.useState<string>(""); const [selectedTermnode, setSelectedTermnode] = React.useState<string>("");
const [collapseAll, setCollapseAll] = React.useState<boolean>(false);
const [expandAll, setExpandAll] = React.useState<boolean>(false);
const buildTree = async () => { const buildTree = async () => {
const taxSvc: TaxonomyService = new TaxonomyService(); const taxSvc: TaxonomyService = new TaxonomyService(props.serviceScope);
const termsetID = await taxSvc.getTermsetInfo(props.fieldName); const termsetID: string = await taxSvc.getTermsetInfo(props.fieldName);
let termnodetree: ITermNode[]; let termnodetree: ITermNode[];
const termnodetreeStr = sessionStorage.getItem(`Termtree_${termsetID}`); const termnodetreeStr: string = sessionStorage.getItem(`Termtree_${termsetID}`);
if (termnodetreeStr === null) { if (termnodetreeStr === null) {
termnodetree = await taxSvc.getTermset(termsetID); termnodetree = await taxSvc.getTermset(termsetID);
sessionStorage.setItem(`Termtree_${termsetID}`, JSON.stringify(termnodetree)); sessionStorage.setItem(`Termtree_${termsetID}`, JSON.stringify(termnodetree));
@ -28,26 +31,26 @@ export const TaxonomyFileExplorer: React.FC<ITaxonomyFileExplorerProps> = (props
termnodetree = JSON.parse(termnodetreeStr); termnodetree = JSON.parse(termnodetreeStr);
} }
const spSrvc: SPService = new SPService(props.listName, props.fieldName); const spSrvc: SPService = new SPService(props.serviceScope, props.listName, props.fieldName);
const files = await spSrvc.getItems(termsetID); const files: IFileItem[] = await spSrvc.getItems(termsetID);
setSpSvc(spSrvc); setSpSvc(spSrvc);
updateFiles(files, termnodetree); updateFiles(files, termnodetree);
}; };
const updateFiles = (files: IFileItem[], termnodetree: ITermNode[]) => { const updateFiles = (files: IFileItem[], termnodetree: ITermNode[]) => {
const taxSvc: TaxonomyService = new TaxonomyService(); const taxSvc: TaxonomyService = new TaxonomyService(props.serviceScope);
termnodetree = taxSvc.incorporateFiles(termnodetree, files); termnodetree = taxSvc.incorporateFiles(termnodetree, files);
setFileItems(files); setFileItems(files);
setTerms(termnodetree); setTerms(termnodetree);
}; };
const renderFiles = (files: IFileItem[]) => { const renderFiles = React.useCallback((files: IFileItem[]) => {
setShownFiles(files); setShownFiles(files);
}; },[setShownFiles]);
const resetChecked = (newNodeID: string) => { const resetChecked = React.useCallback((newNodeID: string) => {
setSelectedTermnode(newNodeID); setSelectedTermnode(newNodeID);
}; },[setSelectedTermnode]);
const reloadFiles = (file: IFileItem) => { const reloadFiles = (file: IFileItem) => {
const newFiles: IFileItem[] = []; const newFiles: IFileItem[] = [];
@ -67,45 +70,66 @@ export const TaxonomyFileExplorer: React.FC<ITaxonomyFileExplorerProps> = (props
updateFiles(newFiles, terms); updateFiles(newFiles, terms);
}; };
const addTerm = (file: IFileItem, newTaxonomyValue: string) => { const addTerm = React.useCallback((file: IFileItem, newTaxonomyValue: string) => {
spSvc.updateTaxonomyItemByAdd(file, props.fieldName, newTaxonomyValue); spSvc.updateTaxonomyItemByAdd(file, props.fieldName, newTaxonomyValue);
reloadFiles(file); 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) => {
spSvc.updateTaxonomyItemByReplace(file, props.fieldName, newTaxonomyValue); spSvc.updateTaxonomyItemByReplace(file, props.fieldName, newTaxonomyValue);
reloadFiles(file); 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); const newFile = await spSvc.newTaxonomyItemByCopy(file, props.fieldName, newTaxonomyValue);
loadNewFiles(newFile); loadNewFiles(newFile);
}; },[spSvc, fileItems, terms]); // eslint-disable-line react-hooks/exhaustive-deps
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(() => { React.useEffect(() => {
buildTree(); buildTree(); // eslint-disable-line @typescript-eslint/no-floating-promises
}, []); }, []); // eslint-disable-line react-hooks/exhaustive-deps
return ( return (
<div className={ styles.taxonomyFileExplorer }> <div className={ styles.taxonomyFileExplorer }>
<div className={ styles.container }> <div className={ styles.container }>
<div className={ styles.row }> <div className={ styles.row }>
<div className={ styles.column }> <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> <ul>
{terms.map(nc => { return <TermLabel node={nc} {terms.map(nc => { return <TermLabel node={nc}
key={nc.guid}
renderFiles={renderFiles} renderFiles={renderFiles}
resetChecked={resetChecked} resetChecked={resetChecked}
selectedNode={selectedTermnode} selectedNode={selectedTermnode}
collapseAll={collapseAll}
expandAll={expandAll}
addTerm={addTerm} addTerm={addTerm}
replaceTerm={replaceTerm} replaceTerm={replaceTerm}
copyFile={copyFile} />; })} copyFile={copyFile}
uploadFile={uploadFile} />; })}
</ul> </ul>
</div> </div>
<div className={ styles.column }> <div className={ styles.column }>
{shownFiles.length > 0 && {shownFiles.length > 0 &&
<ul> <ul>
{shownFiles.map(f => { {shownFiles.map(f => {
return <FileLabel file={f} />; return <FileLabel file={f} key={f.id} />;
})} })}
</ul>} </ul>}
</div> </div>

View File

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

View File

@ -11,47 +11,70 @@ export const TermLabel: React.FC<ITermLabelProps> = (props) => {
const [countDocuments, setCountDocuments] = React.useState<number>(props.node.childDocuments); const [countDocuments, setCountDocuments] = React.useState<number>(props.node.childDocuments);
const [showContextualMenu, setShowContextualMenu] = React.useState<boolean>(false); const [showContextualMenu, setShowContextualMenu] = React.useState<boolean>(false);
const [droppedFile, setDroppedFile] = React.useState<IFileItem>(); const [droppedFile, setDroppedFile] = React.useState<IFileItem>();
const [dragEntered, setDragEntered] = React.useState<boolean>(false);
const toggleIcon = () => { const toggleIcon = React.useCallback(() => {
setShowChildren(!showChildren); setShowChildren(!showChildren);
}; },[setShowChildren,showChildren]);
const nodeSelected = () => { const nodeSelected = React.useCallback(() => {
props.resetChecked(props.node.guid); props.resetChecked(props.node.guid);
props.renderFiles(props.node.subFiles); props.renderFiles(props.node.subFiles);
}; },[]); // eslint-disable-line react-hooks/exhaustive-deps
const hideContextualMenu = () => { const hideContextualMenu = React.useCallback(() => {
setShowContextualMenu(false); setShowContextualMenu(false);
}; },[setShowContextualMenu]);
const drop = (ev) => { const uploadWithNewTerm = React.useCallback((file: any) => {
ev.preventDefault(); const newTaxonomyValue: string = `${props.node.name}|${props.node.guid}`;
var data = ev.dataTransfer.getData("text"); props.uploadFile(file, newTaxonomyValue);
const file: IFileItem = JSON.parse(data); },[]); // eslint-disable-line react-hooks/exhaustive-deps
setDroppedFile(file);
if (ev.ctrlKey) { const addNewTerm = React.useCallback((file: IFileItem) => {
setShowContextualMenu(true); const newTaxonomyValue: string = `${props.node.name}|${props.node.guid}`;
}
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.termGuid.push(props.node.guid);
file.taxValue.push(newTaxonomyValue); file.taxValue.push(newTaxonomyValue);
console.log(file);
props.addTerm(file, newTaxonomyValue); 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]);
const replaceByNewTerm = (file: IFileItem) => { const replaceByNewTerm = (file: IFileItem) => {
const newTaxonomyValue = `${props.node.name}|${props.node.guid}`; const newTaxonomyValue: string = `${props.node.name}|${props.node.guid}`;
file.termGuid = [props.node.guid]; file.termGuid = [props.node.guid];
file.taxValue = [newTaxonomyValue]; file.taxValue = [newTaxonomyValue];
console.log(file); console.log(file);
@ -59,12 +82,11 @@ export const TermLabel: React.FC<ITermLabelProps> = (props) => {
}; };
const copyWithNewTerm = (file: IFileItem) => { const copyWithNewTerm = (file: IFileItem) => {
const newTaxonomyValue = `${props.node.name}|${props.node.guid}`; const newTaxonomyValue: string = `${props.node.name}|${props.node.guid}`;
console.log(file);
props.copyFile(file, newTaxonomyValue); 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[] = [ const menuItems: IContextualMenuItem[] = [
{ {
key: 'copyItem', key: 'copyItem',
@ -81,6 +103,14 @@ export const TermLabel: React.FC<ITermLabelProps> = (props) => {
text: 'Add new term (Link)', text: 'Add new term (Link)',
onClick: () => addNewTerm(droppedFile) onClick: () => addNewTerm(droppedFile)
}]; }];
React.useEffect(() => {
if (props.expandAll) {
setShowChildren(true);
}
if (props.collapseAll) {
setShowChildren(false);
}
}, [props.collapseAll, props.expandAll]);
React.useEffect(() => { React.useEffect(() => {
if (props.selectedNode===props.node.guid) { if (props.selectedNode===props.node.guid) {
props.renderFiles(props.node.subFiles); props.renderFiles(props.node.subFiles);
@ -88,32 +118,42 @@ export const TermLabel: React.FC<ITermLabelProps> = (props) => {
if (props.node.childDocuments !== countDocuments) { if (props.node.childDocuments !== countDocuments) {
setCountDocuments(props.node.childDocuments); setCountDocuments(props.node.childDocuments);
} }
}, [props.node.subFiles]); }, [props.node.subFiles]); // eslint-disable-line react-hooks/exhaustive-deps
return ( return (
<li className={styles.termLabel}> <li className={styles.termLabel}>
<div ref={linkRef} className={`${styles.label} ${props.selectedNode===props.node.guid ? styles.checkedLabel : ""}`} onClick={nodeSelected} onDrop={drop} onDragOver={dragOver}> <div ref={linkRef} className={`${styles.label} ${props.selectedNode===props.node.guid ? styles.checkedLabel : ""}
<label> ${dragEntered ? styles.dragEnter : ""}`}
{props.node.children.length > 0 ? currentExpandIcon : <i className={styles.emptyicon}>&nbsp;</i>} onClick={nodeSelected}
<Icon className={styles.icon} iconName="FabricFolder" /> onDrop={drop}
{props.node.name}{countDocuments>0?<span className={styles.fileCount}>{countDocuments}</span>:""} onDragOver={dragOver}
</label> onDragEnter={dragEnter}
</div> onDragLeave={dragLeave}>
<ContextualMenu <label>
items={menuItems} {props.node.children.length > 0 ? currentExpandIcon : <i className={styles.emptyicon}>&nbsp;</i>}
hidden={!showContextualMenu} <Icon className={styles.icon} iconName="FabricFolder" />
target={linkRef} {props.node.name}{countDocuments>0?<span className={styles.fileCount}>{countDocuments}</span>:""}
onItemClick={hideContextualMenu} </label>
onDismiss={hideContextualMenu} </div>
/> <ContextualMenu
{showChildren && <ul className={`${props.node.children.length > 0 ? styles.liFilled : ""}`}> items={menuItems}
{props.node.children.map(nc => { return <TermLabel node={nc} hidden={!showContextualMenu}
renderFiles={props.renderFiles} target={linkRef}
resetChecked={props.resetChecked} onItemClick={hideContextualMenu}
selectedNode={props.selectedNode} onDismiss={hideContextualMenu}
addTerm={props.addTerm} />
replaceTerm={props.replaceTerm} {showChildren && <ul className={`${props.node.children.length > 0 ? styles.liFilled : ""}`}>
copyFile={props.copyFile} />; })} {props.node.children.map(nc => { return <TermLabel node={nc}
</ul>} key={nc.guid}
</li> 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": { "compilerOptions": {
"target": "es5", "target": "es5",
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,