New feature Expand/Collapse all upgrd SPFx 1.15.0
This commit is contained in:
parent
fa3a66492d
commit
91b09cb163
|
@ -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"
|
||||
}
|
||||
};
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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, 2021|Added expand/collapse all, upgraded to SPFx 1.15.0, upgraded to PnPJS V3.5.1
|
||||
|
||||
|
||||
## Minimal Path to Awesome
|
||||
|
|
|
@ -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"
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -9,26 +9,32 @@
|
|||
"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",
|
||||
"gulp": "~4.0.2",
|
||||
"ajv": "~6.12.3",
|
||||
"@types/webpack-env": "1.13.1"
|
||||
"@types/webpack-env": "1.15.2",
|
||||
"ajv": "6.12.5",
|
||||
"eslint": "8.7.0",
|
||||
"eslint-plugin-react-hooks": "4.3.0",
|
||||
"gulp": "~4.0.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 { 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();
|
||||
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 +38,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 +49,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 +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);
|
||||
|
||||
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 +84,15 @@ 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}`, '');
|
||||
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 +108,31 @@ export class SPService {
|
|||
};
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -1,29 +1,42 @@
|
|||
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;
|
||||
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 +44,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 +69,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;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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" },
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import { ServiceScope } from "@microsoft/sp-core-library";
|
||||
|
||||
export interface ITaxonomyFileExplorerProps {
|
||||
serviceScope: ServiceScope;
|
||||
fieldName: string;
|
||||
listName: string;
|
||||
}
|
||||
|
|
|
@ -4,9 +4,12 @@ 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;
|
||||
uploadFile: (file: any, newValue: string) => void;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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,14 @@ 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);
|
||||
|
||||
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,26 +31,26 @@ 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);
|
||||
updateFiles(files, termnodetree);
|
||||
};
|
||||
|
||||
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]);
|
||||
|
||||
const reloadFiles = (file: IFileItem) => {
|
||||
const newFiles: IFileItem[] = [];
|
||||
|
@ -67,45 +70,66 @@ export const TaxonomyFileExplorer: React.FC<ITaxonomyFileExplorerProps> = (props
|
|||
updateFiles(newFiles, terms);
|
||||
};
|
||||
|
||||
const addTerm = (file: IFileItem, newTaxonomyValue: string) => {
|
||||
const addTerm = React.useCallback((file: IFileItem, newTaxonomyValue: string) => {
|
||||
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) => {
|
||||
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
|
||||
|
||||
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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -11,47 +11,70 @@ 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}`;
|
||||
const uploadWithNewTerm = React.useCallback((file: any) => {
|
||||
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) => {
|
||||
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]);
|
||||
|
||||
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.taxValue = [newTaxonomyValue];
|
||||
console.log(file);
|
||||
|
@ -59,12 +82,11 @@ export const TermLabel: React.FC<ITermLabelProps> = (props) => {
|
|||
};
|
||||
|
||||
const copyWithNewTerm = (file: IFileItem) => {
|
||||
const newTaxonomyValue = `${props.node.name}|${props.node.guid}`;
|
||||
console.log(file);
|
||||
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 +103,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 +118,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}> </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}> </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>
|
||||
);
|
||||
};
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue