diff --git a/packages/file-handling/README.md b/packages/file-handling/README.md index 0d5510f319c6ddc0e8b68d1298fe6c4a2bb17cbf..69e46997818fc6783cddc973c7ece423e2c3b619 100644 --- a/packages/file-handling/README.md +++ b/packages/file-handling/README.md @@ -27,10 +27,16 @@ Files will be uploaded sequentially (not parallel) to prevent overburdening the - example `<vpu-fileupload allowed-mime-types='image/png,text/plain'></vpu-fileupload>` ... PNGs or TXTs only - example `<vpu-fileupload allowed-mime-types='*/*'></vpu-fileupload>` ... all file types (default) - `disabled` (optional): disable input control - - example `<vpu-fileupload disabled>` + - example `<vpu-fileupload disabled></vpu-fileupload>` - `decompress-zip` (optional): decompress zip file and queue the contained files (including files in folders) - - example `<vpu-fileupload decompress-zip>` + - example `<vpu-fileupload decompress-zip></vpu-fileupload>` - mime types of `allowed-mime-types` will also be checked for the files in the zip file +- `nextcloud-auth-url` (optional): Nextcloud Auth Url to use with the Nextcloud file picker + - example `<vpu-fileupload nextcloud-auth-url="http://localhost:8081/index.php/apps/webapppassword"></vpu-fileupload>` + - `nextcloud-web-dav-url` also needs to be set for the Nextcloud file picker to be active +- `nextcloud-web-dav-url` (optional): Nextcloud WebDav Url to use with the Nextcloud file picker + - example `<vpu-fileupload nextcloud-web-dav-url="http://localhost:8081/remote.php/dav/files"></vpu-fileupload>` + - `nextcloud-auth-url` also needs to be set for the Nextcloud file picker to be active ## Local development diff --git a/packages/file-handling/package.json b/packages/file-handling/package.json index e18aa0c4f1fd8713230360667aff501760355adf..bfbc93924c3ecfb2efd750697eecbaa3efb9eade 100644 --- a/packages/file-handling/package.json +++ b/packages/file-handling/package.json @@ -29,7 +29,9 @@ "i18next": "^19.4.2", "lit-element": "^2.1.0", "lit-html": "^1.1.1", - "material-design-icons-svg": "^3.0.0" + "material-design-icons-svg": "^3.0.0", + "tabulator-tables": "^4.7.0", + "webdav": "^3.3.0" }, "scripts": { "clean": "rm dist/*", diff --git a/packages/file-handling/rollup.config.js b/packages/file-handling/rollup.config.js index 8fbc8e44a727ed10ab6ec1f6859cca7c11acb20f..644db66e82b8408da8c446cf6b679d01b2330de3 100644 --- a/packages/file-handling/rollup.config.js +++ b/packages/file-handling/rollup.config.js @@ -44,6 +44,7 @@ export default { {src: 'assets/index.html', dest: 'dist'}, {src: 'assets/favicon.ico', dest: 'dist'}, {src: 'node_modules/material-design-icons-svg/paths/*.json', dest: 'dist/local/vpu-common/icons'}, + {src: 'node_modules/tabulator-tables/dist/css', dest: 'dist/local/fileupload/tabulator-tables'}, ], }), (process.env.ROLLUP_WATCH === 'true') ? serve({contentBase: 'dist', host: '127.0.0.1', port: 8002}) : false diff --git a/packages/file-handling/src/fileupload.js b/packages/file-handling/src/fileupload.js index 849e7ed4c45d459bd6f762f605c955310ab860de..b5a993ecf0026ca41f5634302c78cd2d9d31aa73 100644 --- a/packages/file-handling/src/fileupload.js +++ b/packages/file-handling/src/fileupload.js @@ -6,6 +6,8 @@ import VPULitElement from 'vpu-common/vpu-lit-element'; import * as commonUtils from "vpu-common/utils"; import {Icon, MiniSpinner} from 'vpu-common'; import * as commonStyles from 'vpu-common/styles'; +import {NextcloudFilePicker} from "./vpu-nextcloud-file-picker"; +import {classMap} from 'lit-html/directives/class-map.js'; function mimeTypesToAccept(mimeTypes) { @@ -34,6 +36,8 @@ export class FileUpload extends ScopedElementsMixin(VPULitElement) { super(); this.lang = 'de'; this.url = ''; + this.nextcloudAuthUrl = ''; + this.nextcloudWebDavUrl = ''; this.dropArea = null; this.allowedMimeTypes = '*/*'; this.text = ''; @@ -53,6 +57,7 @@ export class FileUpload extends ScopedElementsMixin(VPULitElement) { return { 'vpu-icon': Icon, 'vpu-mini-spinner': MiniSpinner, + 'vpu-nextcloud-file-picker': NextcloudFilePicker, }; } @@ -64,6 +69,8 @@ export class FileUpload extends ScopedElementsMixin(VPULitElement) { lang: { type: String }, url: { type: String }, allowedMimeTypes: { type: String, attribute: 'allowed-mime-types' }, + nextcloudAuthUrl: { type: String, attribute: 'nextcloud-auth-url' }, + nextcloudWebDavUrl: { type: String, attribute: 'nextcloud-web-dav-url' }, text: { type: String }, buttonLabel: { type: String, attribute: 'button-label' }, uploadInProgress: { type: Boolean, attribute: false }, @@ -451,6 +458,14 @@ export class FileUpload extends ScopedElementsMixin(VPULitElement) { multiple accept="${mimeTypesToAccept(allowedMimeTypes)}" name='file'> + <vpu-nextcloud-file-picker class="${classMap({hidden: this.nextcloudWebDavUrl === "" || this.nextcloudAuthUrl === ""})}" + ?disabled="${this.uploadInProgress || this.disabled}" + lang="${this.lang}" + auth-url="${this.nextcloudAuthUrl}" + web-dav-url="${this.nextcloudWebDavUrl}" + @vpu-nextcloud-file-picker-file-downloaded="${(event) => { + this.queueFile(event.detail.file); + }}"></vpu-nextcloud-file-picker> <label class="button is-primary" for="fileElem" ?disabled="${this.disabled}"> <vpu-icon style="display: ${this.uploadInProgress ? "inline-block" : "none"}" name="lock"></vpu-icon> ${this.buttonLabel || i18n.t('upload-label')} diff --git a/packages/file-handling/src/vpu-nextcloud-file-picker.js b/packages/file-handling/src/vpu-nextcloud-file-picker.js new file mode 100644 index 0000000000000000000000000000000000000000..fed15c0f427244d4e21719cbf47a1226b730f8dd --- /dev/null +++ b/packages/file-handling/src/vpu-nextcloud-file-picker.js @@ -0,0 +1,254 @@ +import {i18n} from './i18n'; +import {css, html} from 'lit-element'; +import {ScopedElementsMixin} from '@open-wc/scoped-elements'; +import VPULitElement from 'vpu-common/vpu-lit-element'; +import {MiniSpinner} from 'vpu-common'; +import * as commonUtils from 'vpu-common/utils'; +import * as commonStyles from 'vpu-common/styles'; +// `import {createClient} from 'webdav/web';` didn't seem to work if fileupload demo page is built +// `import createClient from 'webdav/web';` didn't seem to work if Signature project is built +import * as webDavWeb from 'webdav/web'; +import {classMap} from 'lit-html/directives/class-map.js'; +import {humanFileSize} from 'vpu-common/i18next'; +import Tabulator from 'tabulator-tables'; + +/** + * NextcloudFilePicker web component + */ +export class NextcloudFilePicker extends ScopedElementsMixin(VPULitElement) { + constructor() { + super(); + this.lang = 'de'; + this.authUrl = ''; + this.webDavUrl = ''; + this.loginWindow = null; + this.isPickerActive = false; + this.statusText = ""; + this.lastDirectoryPath = "/"; + this.directoryPath = "/"; + this.webDavClient = null; + this.tabulatorTable = null; + + this._onReceiveWindowMessage = this.onReceiveWindowMessage.bind(this); + } + + static get scopedElements() { + return { + 'vpu-mini-spinner': MiniSpinner, + }; + } + + /** + * See: https://lit-element.polymer-project.org/guide/properties#initialize + */ + static get properties() { + return { + lang: { type: String }, + authUrl: { type: String, attribute: "auth-url" }, + webDavUrl: { type: String, attribute: "web-dav-url" }, + isPickerActive: { type: Boolean, attribute: false }, + statusText: { type: String, attribute: false }, + directoryPath: { type: String, attribute: false }, + }; + } + + update(changedProperties) { + changedProperties.forEach((oldValue, propName) => { + switch (propName) { + case "lang": + i18n.changeLanguage(this.lang); + break; + } + }); + + super.update(changedProperties); + } + + disconnectedCallback() { + window.removeEventListener('message', this._onReceiveWindowMessage); + super.disconnectedCallback(); + } + + connectedCallback() { + super.connectedCallback(); + + this.updateComplete.then(() => { + // see: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage + window.addEventListener('message', this._onReceiveWindowMessage); + + // http://tabulator.info/docs/4.7 + // TODO: format size and lastmod + // TODO: translation of column headers + // TODO: mime type icon + this.tabulatorTable = new Tabulator(this._("#directory-content-table"), { + layout: "fitDataStretch", + selectable: true, + columns: [ + {title: "Filename", field: "basename"}, + {title: "Size", field: "size", formatter: (cell, formatterParams, onRendered) => { + return cell.getRow().getData().type === "directory" ? "" : humanFileSize(cell.getValue());}}, + {title: "Type", field: "type"}, + {title: "Mime", field: "mime"}, + {title: "Last modified", field: "lastmod", sorter: "date"}, + ], + rowClick: (e, row) => { + const data = row.getData(); + + switch(data.type) { + case "directory": + this.directoryClicked(e, data); + break; + case "file": + console.log("file selected", data); + break; + } + }, + }); + }); + } + + openFilePicker() { + // TODO: translation + this.statusText = "Auth in progress"; + this.loginWindow = window.open(this.authUrl, "Nextcloud Login", + "width=400,height=400,menubar=no,scrollbars=no,status=no,titlebar=no,toolbar=no"); + } + + onReceiveWindowMessage(event) { + const data = event.data; + console.log("data", data); + + if (data.type === "webapppassword") { + this.loginWindow.close(); + // alert("Login name: " + data.loginName + "\nApp password: " + data.token); + + const apiUrl = this.webDavUrl + "/" + data.loginName; + + // https://github.com/perry-mitchell/webdav-client/blob/master/API.md#module_WebDAV.createClient + this.webDavClient = webDavWeb.createClient( + apiUrl, + { + username: data.loginName, + password: data.token + } + ); + + this.loadDirectory("/"); + } + } + + /** + * Loads the directory from WebDAV + * + * @param path + */ + loadDirectory(path) { + // TODO: translation + this.statusText = "Loading directory from Nextcloud: " + path; + this.lastDirectoryPath = this.directoryPath; + this.directoryPath = path; + + // https://github.com/perry-mitchell/webdav-client#getdirectorycontents + this.webDavClient + .getDirectoryContents(path, {details: true}) + .then(contents => { + console.log("contents", contents); + this.statusText = ""; + this.tabulatorTable.setData(contents.data); + this.isPickerActive = true; + }).catch(error => { + console.error(error.message); + this.statusText = error.message; + this.isPickerActive = false; + }); + } + + directoryClicked(event, file) { + this.loadDirectory(file.filename); + event.preventDefault(); + } + + downloadFiles(files) { + files.forEach((fileData) => this.downloadFile(fileData)); + } + + downloadFile(fileData) { + this.statusText = "Loading " + fileData.filename + "..."; + + // https://github.com/perry-mitchell/webdav-client#getfilecontents + this.webDavClient + .getFileContents(fileData.filename) + .then(contents => { + // create file to send via event + const file = new File([contents], fileData.basename, { type: fileData.mime }); + console.log("binaryFile", file); + + // send event + const data = {"file": file, "data": fileData}; + const event = new CustomEvent("vpu-nextcloud-file-picker-file-downloaded", + { "detail": data, bubbles: true, composed: true }); + this.dispatchEvent(event); + + this.statusText = ""; + }).catch(error => { + console.error(error.message); + this.statusText = error.message; + }); + } + + /** + * Returns the parent directory path + * + * @returns {string} parent directory path + */ + getParentDirectoryPath() { + let path = this.directoryPath.replace(/\/$/, ""); + path = path.replace(path.split("/").pop(), "").replace(/\/$/, ""); + + return (path === "") ? "/" : path; + } + + static get styles() { + // language=css + return css` + ${commonStyles.getGeneralCSS()} + ${commonStyles.getButtonCSS()} + + .block { + margin-bottom: 10px; + } + `; + } + + render() { + commonUtils.initAssetBaseURL('vpu-tabulator-table'); + const tabulatorCss = commonUtils.getAssetURL('local/vpu-fileupload/tabulator-tables/css/tabulator.min.css'); + console.log("tabulatorCss", tabulatorCss); + + return html` + <link rel="stylesheet" href="${tabulatorCss}"> + <div class="block"> + <button class="button" + title="${i18n.t('nextcloud-file-picker.open-nextcloud-file-picker')}" + @click="${async () => { this.openFilePicker(); } }">${i18n.t('nextcloud-file-picker.open')}</button> + </div> + <div class="block ${classMap({hidden: this.statusText === ""})}"> + <vpu-mini-spinner style="font-size: 0.7em"></vpu-mini-spinner> + ${this.statusText} + </div> + <div class="block ${classMap({hidden: !this.isPickerActive})}"> + <h2>${this.directoryPath}</h2> + <button class="button is-small" + title="${i18n.t('nextcloud-file-picker.folder-last')}" + @click="${() => { this.loadDirectory(this.lastDirectoryPath); }}">⇦</button> + <button class="button is-small" + title="${i18n.t('nextcloud-file-picker.folder-up')}" + @click="${() => { this.loadDirectory(this.getParentDirectoryPath()); }}">⇧</button> + <table id="directory-content-table"></table> + <button class="button" + title="${i18n.t('nextcloud-file-picker.folder-up')}" + @click="${() => { this.downloadFiles(this.tabulatorTable.getSelectedData()); }}">${i18n.t('nextcloud-file-picker.select-files')}</button> + </div> + `; + } +}