diff --git a/api/etemplate.php b/api/etemplate.php index b7e204c804..63bdbd2b18 100644 --- a/api/etemplate.php +++ b/api/etemplate.php @@ -13,11 +13,11 @@ use EGroupware\Api; // add et2- prefix to following widgets/tags, if NO 0) + { + // @ts-ignore This is the typecheck, no need to warn about it + value = (typeof this.value[0].path != "undefined") ? this.value[0].path : this.value[0]; + } + + // Send to server this.processingPromise = this.egw().request( this.method, - [this.methodId, this.value, button/*, savemode*/] + [this.methodId, value, button/*, savemode*/] ).then((data) => { this.processingPromise = null; diff --git a/api/js/etemplate/Et2Vfs/Et2VfsSelectDialog.ts b/api/js/etemplate/Et2Vfs/Et2VfsSelectDialog.ts index 3c19c4cfb5..f9329106b9 100644 --- a/api/js/etemplate/Et2Vfs/Et2VfsSelectDialog.ts +++ b/api/js/etemplate/Et2Vfs/Et2VfsSelectDialog.ts @@ -20,7 +20,7 @@ import {SearchMixinInterface} from "../Et2Select/SearchMixin"; import {SelectOption} from "../Et2Select/FindSelectOptions"; import {DialogButton, Et2Dialog} from "../Et2Dialog/Et2Dialog"; import {HasSlotController} from "../Et2Widget/slot"; -import {IegwAppLocal} from "../../jsapi/egw_global"; +import {egw, IegwAppLocal} from "../../jsapi/egw_global"; import {Et2Select} from "../Et2Select/Et2Select"; import {Et2VfsSelectRow} from "./Et2VfsSelectRow"; import {Et2VfsPath} from "./Et2VfsPath"; @@ -131,7 +131,7 @@ export class Et2VfsSelectDialog extends Et2InputWidget(LitElement) implements Se // Still need some server-side info protected _serverContent : Promise = Promise.resolve({}); - private static SERVER_URL = "EGroupware\\Api\\Etemplate\\Widget\\Vfs::ajax_vfsSelectContent"; + private static SERVER_URL = "EGroupware\\Api\\Etemplate\\Widget\\Vfs::ajax_vfsSelect_content"; protected readonly hasSlotController = new HasSlotController(this, 'help-text', 'toolbar', 'footer'); @@ -179,6 +179,7 @@ export class Et2VfsSelectDialog extends Et2InputWidget(LitElement) implements Se // Use filemanager translations this.egw().langRequireApp(this.egw().window, "filemanager", () => {this.requestUpdate()}); + this.handleClose = this.handleClose.bind(this); this.handleCreateDirectory = this.handleCreateDirectory.bind(this); this.handleSearchKeyDown = this.handleSearchKeyDown.bind(this); } @@ -312,14 +313,12 @@ export class Et2VfsSelectDialog extends Et2InputWidget(LitElement) implements Se return this._dialog.hide(); } - getComplete() : Promise<[number, Object]> + async getComplete() : Promise<[number, Object]> { - return this._dialog.getComplete().then((value) => - { - // Overwrite dialog's value with what we say - value[1] = this.value; - return value - }); + const value = await this._dialog.getComplete(); + await this.handleClose(); + value[1] = this.value; + return value; } startSearch() : Promise @@ -409,6 +408,85 @@ export class Et2VfsSelectDialog extends Et2InputWidget(LitElement) implements Se } } + private async handleClose() + { + // Should already be complete, we want the button + let dialogValue = await this._dialog.getComplete(); + switch(this.mode) + { + case "select-dir": + // If they didn't pick a specific directory and didn't cancel, use the current directory + this.value = this.value.length ? this.value : [this.path]; + break; + case "saveas": + // Saveas wants a full path, including filename + this.value = [this.path + "/" + this.filename]; + + // Check for existing file, ask what to do + if(this.fileInfo(this.value[0])) + { + let result = await this.overwritePrompt(this.filename); + if(result == null) + { + return; + } + this.value = [this.path + "/" + result]; + } + break; + } + this.dispatchEvent(new Event("change", {bubbles: true})); + } + + /** + * User tried to saveas when we can see that file already exists. Prompt to overwrite or rename. + * + * We offer a suggested new name by appending "(#)", and give back either the original filename, their + * modified filename, or null if they cancel. + * + * @param filename + * @returns {Promise<[number|string, Object]|null>} [Button,filename] or null if they cancel + * @private + */ + private overwritePrompt(filename) : Promise<[number | string, object] | null> + { + // Make a filename suggestion + const parts = filename.split("."); + const extension = parts.pop(); + const newName = parts.join("."); + let counter = 0; + let suggestion; + do + { + counter++; + suggestion = `${newName} (${counter}).${extension}`; + } + while(this.fileInfo(suggestion)) + + // Ask about it + const saveModeDialogButtons = [ + { + label: self.egw().lang("Yes"), + id: "overwrite", + class: "ui-priority-primary", + "default": true, + image: 'check' + }, + {label: self.egw().lang("Rename"), id: "rename", image: 'edit'}, + {label: self.egw().lang("Cancel"), id: "cancel"} + ]; + return Et2Dialog.show_prompt(null, + self.egw().lang('Do you want to overwrite existing file %1 in directory %2?', filename, this.path), + self.egw().lang('File %1 already exists', filename), + suggestion, saveModeDialogButtons, null).getComplete().then(([button, value]) => + { + if(button == "cancel") + { + return null; + } + return button == "rename" ? value.value : filename; + }); + } + /** * Sets the selected files * @param {Et2VfsSelectRow | Et2VfsSelectRow[]} file @@ -551,8 +629,8 @@ export class Et2VfsSelectDialog extends Et2InputWidget(LitElement) implements Se { this.currentFile = file; - // Can't select a directory normally - if(file.value.isDir && this.mode != "select-dir") + // Can't select a directory normally, can't select anything in "saveas" + if(file.value.isDir && this.mode != "select-dir" || this.mode == "saveas") { return; } @@ -567,15 +645,6 @@ export class Et2VfsSelectDialog extends Et2InputWidget(LitElement) implements Se // Set focus after updating so the value is announced by screen readers //this.updateComplete.then(() => this.displayInput.focus({ preventScroll: true })); - - if(this.value !== oldValue) - { - // Emit after updating - this.updateComplete.then(() => - { - this.dispatchEvent(new Event('change', {bubbles: true})); - }); - } } } @@ -838,8 +907,7 @@ export class Et2VfsSelectDialog extends Et2InputWidget(LitElement) implements Se const hasToolbar = !!hasToolbarSlot; const hasFilename = this.mode == "saveas"; - const mime = this.mimeList.length == 1 ? this.mimeList[0].value : - (typeof this.mime == "string" ? this.mime : ""); + const mime = typeof this.mime == "string" ? this.mime : (this.mimeList.length == 1 ? this.mimeList[0].value : ""); return html` - ${hasFilename ? html`` : nothing} + ${hasFilename ? html` + {this.filename = e.target.value;}} + > + ` : nothing}
1 ? {ids: ids, action: 'attachment'} : {ids: ids[0], action: 'attachment'}, - name: action === 'saveOneToVfs' ? attachments[0]['filename'] : null - }); - vfs_select.click(); + buttonLabel: this.egw.lang(action === 'saveOneToVfs' ? 'Save' : 'Save all'), + title: this.egw.lang(action === 'saveOneToVfs' ? 'Save attachment' : 'Save attachments'), + filename: action === 'saveOneToVfs' ? attachments[0]['filename'] : null + }, this.et2); + // Serious violation of type - methodId is a string + // Set it to an array here bypassing normal checking + vfs_select.methodId = ids.length > 1 ? {ids: ids, action: 'attachment'} : {ids: ids[0], action: 'attachment'}, + vfs_select.updateComplete.then(() => vfs_select.click()); + // Single use only, remove when done + vfs_select.addEventListener("change", () => vfs_select.remove()); break; case 'collabora': attachment = attachments[row_id]; @@ -3388,16 +3394,20 @@ app.classes.mail = AppJS.extend( ids.push(_id); names.push(filename+'.eml'); } - var vfs_select = et2_createWidget('vfs-select', { + let vfs_select = loadWebComponent('et2-vfs-select', { mode: _elems.length > 1 ? 'select-dir' : 'saveas', mime: 'message/rfc822', method: 'mail.mail_ui.ajax_vfsSave', - button_label: _elems.length>1 ? egw.lang('Save all') : egw.lang('save'), - dialog_title: this.egw.lang("Save email"), - method_id: _elems.length > 1 ? {ids:ids, action:'message'}: {ids: ids[0], action: 'message'}, - name: _elems.length > 1 ? names : names[0], - }); - vfs_select.click(); + buttonLabel: _elems.length > 1 ? egw.lang('Save all') : egw.lang('save'), + title: this.egw.lang("Save email"), + filename: _elems.length > 1 ? names : names[0], + }, this.et2); + // Serious violation of type - methodId is a string + // Set it to an array here bypassing normal checking + vfs_select.methodId = _elems.length > 1 ? {ids: ids, action: 'message'} : {ids: ids[0], action: 'message'}; + vfs_select.updateComplete.then(() => vfs_select.click()); + // Single use only, remove when done + vfs_select.addEventListener("change", () => vfs_select.remove()); }, /** @@ -5513,6 +5523,9 @@ app.classes.mail = AppJS.extend( case 'uploadForCompose': document.getElementById('mail-compose_uploadForCompose').click(); break; + case 'selectFromVFSForCompose': + widget.show(); + break; default: widget.click(); } diff --git a/mail/templates/default/app.css b/mail/templates/default/app.css index b127b6ebfe..6fd803b373 100644 --- a/mail/templates/default/app.css +++ b/mail/templates/default/app.css @@ -954,11 +954,7 @@ and (orientation : landscape) { } #mail-compose_mimeType{margin-left:1em;} /*Make file uploads in compose dialog invisible*/ -.mail-compose_toolbar_assist div.mail-compose_fileselector, #mail-compose_selectFromVFSForCompose, .mail-compose_toolbar_assist { - display:none; -} -/*Make file uploads in compose dialog invisible*/ -.mail-compose_toolbar_assist div.mail-compose_fileselector, #mail-compose_selectFromVFSForCompose, .mail-compose_toolbar_assist { +.mail-compose_toolbar_assist div.mail-compose_fileselector, .mail-compose_toolbar_assist { display:none; } diff --git a/mail/templates/default/compose.xet b/mail/templates/default/compose.xet index c5a63b75b0..0749bb2494 100644 --- a/mail/templates/default/compose.xet +++ b/mail/templates/default/compose.xet @@ -4,9 +4,9 @@