mirror of
https://github.com/EGroupware/egroupware.git
synced 2025-03-04 10:11:26 +01:00
Et2VfsUpload: WIP mostly working
This commit is contained in:
parent
9a7475cf5b
commit
e3faa3d97f
@ -23,11 +23,11 @@ Use `image` to specify the icon
|
||||
|
||||
### Limit files allowed
|
||||
|
||||
Use the `multiple`, `allow`, `maxFiles` and `maxFileSize` attributes to place restrictions on the files to be uploaded.
|
||||
Use the `multiple`, `accept`, `maxFiles` and `maxFileSize` attributes to place restrictions on the files to be uploaded.
|
||||
|
||||
```html:preview
|
||||
<et2-file image="image" allow="image/*" label="Choose an image"></et2-file>
|
||||
<et2-file image="images" allow="image/*" multiple label="Choose images"></et2-file>
|
||||
<et2-file image="image" accept="image/*" label="Choose an image"></et2-file>
|
||||
<et2-file image="images" accept="image/*" multiple label="Choose images"></et2-file>
|
||||
<et2-file maxFiles="3" label="Max. 3 files"></et2-file>
|
||||
<et2-file maxFileSize="10000" label="Small files only"></et2-file>
|
||||
```
|
||||
@ -45,6 +45,9 @@ not do that
|
||||
|
||||
Use the `display` attribute for different ways of showing results
|
||||
```html:preview
|
||||
<et2-file display="large" label="Large">
|
||||
<et2-file-item slot="list" size="654321000" display="large" closable>Large file (default)</et2-file-item>
|
||||
</et2-file>
|
||||
<et2-file display="small" label="Small">
|
||||
<et2-file-item slot="list" size="654321000" display="small" closable>Small file</et2-file-item>
|
||||
</et2-file>
|
||||
|
@ -2,6 +2,8 @@ import {css} from 'lit';
|
||||
|
||||
export default css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
flex: 1 1 fit-content;
|
||||
}
|
||||
|
||||
:host([loading]) .file__button et2-image {
|
||||
@ -16,6 +18,15 @@ export default css`
|
||||
min-width: 25em;
|
||||
background-color: var(--sl-panel-background-color);
|
||||
overflow-y: auto;
|
||||
z-index: var(--sl-z-index-toast);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Single display (multiple=false) match height
|
||||
*/
|
||||
|
||||
.file--single et2-file-item[display="small"]::part(base) {
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
@ -17,6 +17,10 @@ export interface FileInfo extends ResumableFile
|
||||
loading? : boolean;
|
||||
accepted? : boolean;
|
||||
warning? : string;
|
||||
|
||||
// Existing values
|
||||
path? : string;
|
||||
|
||||
// ResumableFile
|
||||
uniqueIdentifier : string;
|
||||
file : File;
|
||||
@ -61,7 +65,7 @@ export class Et2File extends Et2InputWidget(LitElement)
|
||||
];
|
||||
}
|
||||
|
||||
/** A string that defines the file types the file dropzone should accept. Defaults to all. */
|
||||
/** A string that defines the file types the file should accept. Defaults to all. */
|
||||
@property({type: String, reflect: true}) accept = "";
|
||||
|
||||
/** An optional maximum size of a file that will be considered valid. */
|
||||
@ -124,6 +128,13 @@ export class Et2File extends Et2InputWidget(LitElement)
|
||||
// We use an object, not an Array
|
||||
newValue = {...newValue};
|
||||
}
|
||||
Object.keys(newValue).forEach((key) =>
|
||||
{
|
||||
if(typeof newValue[key].uniqueIdentifier == "undefined")
|
||||
{
|
||||
newValue[key].uniqueIdentifier = (newValue[key]['ino'] ?? key) + newValue[key].path;
|
||||
}
|
||||
});
|
||||
this.__value = newValue;
|
||||
this.requestUpdate("value", oldValue);
|
||||
}
|
||||
@ -147,6 +158,12 @@ export class Et2File extends Et2InputWidget(LitElement)
|
||||
return Array.from(this.list?.querySelectorAll("et2-file-item")) ?? [];
|
||||
}
|
||||
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
this.resumableQuery = this.resumableQuery.bind(this);
|
||||
}
|
||||
|
||||
disconnectedCallback()
|
||||
{
|
||||
super.disconnectedCallback();
|
||||
@ -170,6 +187,17 @@ export class Et2File extends Et2InputWidget(LitElement)
|
||||
}
|
||||
}
|
||||
|
||||
loadFromXML(node : Node)
|
||||
{
|
||||
super.loadFromXML(node);
|
||||
|
||||
// Set display to "small" for multiple=false && nothing else set
|
||||
if(!node.hasAttribute("display") && !this.multiple)
|
||||
{
|
||||
this.display = "small";
|
||||
}
|
||||
}
|
||||
|
||||
protected createResumable()
|
||||
{
|
||||
const resumable = new Resumable(this.resumableOptions);
|
||||
@ -195,10 +223,7 @@ export class Et2File extends Et2InputWidget(LitElement)
|
||||
{
|
||||
const options = {
|
||||
target: this.egw().ajaxUrl(this.uploadTarget),
|
||||
query: {
|
||||
request_id: this.getInstanceManager()?.etemplate_exec_id,
|
||||
widget_id: this.id,
|
||||
},
|
||||
query: this.resumableQuery,
|
||||
chunkSize: this.chunkSize,
|
||||
|
||||
// Checking for already uploaded chunks - resumable uploads
|
||||
@ -226,12 +251,26 @@ export class Et2File extends Et2InputWidget(LitElement)
|
||||
return options;
|
||||
}
|
||||
|
||||
protected resumableQuery(file /*: ResumableFile*/, chunk /*: ResumableChunk */)
|
||||
{
|
||||
return {
|
||||
request_id: this.getInstanceManager()?.etemplate_exec_id,
|
||||
widget_id: this.id,
|
||||
};
|
||||
}
|
||||
|
||||
public findFileItem(file)
|
||||
{
|
||||
const fileInfo = this.files.find((i) => i.file.uniqueIdentifier == file.uniqueIdentifier);
|
||||
const searchIdentifier = file.uniqueIdentifier;
|
||||
let fileInfo = this.files.find((i) => i.file.uniqueIdentifier == searchIdentifier);
|
||||
if(!fileInfo)
|
||||
{
|
||||
const source = <FileInfo[]>Object.values(this.value);
|
||||
fileInfo = source.find(e => e.uniqueIdentifier == searchIdentifier);
|
||||
}
|
||||
|
||||
file = fileInfo;
|
||||
const fileIndex = this.files.indexOf(fileInfo) ?? null;
|
||||
const fileItem : Et2FileItem = fileIndex !== -1 ? this.fileItemList[fileIndex] : null;
|
||||
const fileItem : Et2FileItem = this.fileItemList.find(i => i.dataset.fileId == searchIdentifier);
|
||||
return fileItem;
|
||||
}
|
||||
|
||||
@ -298,6 +337,7 @@ export class Et2File extends Et2InputWidget(LitElement)
|
||||
const response = ((JSON.parse(jsonResponse)['response'] ?? {}).find(i => i['type'] == "data") ?? {})['data'] ?? {};
|
||||
const fileItem = this.findFileItem(file);
|
||||
file.loading = false;
|
||||
file.progress = () => 100;
|
||||
if(fileItem)
|
||||
{
|
||||
fileItem.progress = 100;
|
||||
@ -332,6 +372,7 @@ export class Et2File extends Et2InputWidget(LitElement)
|
||||
}
|
||||
this.value[tempName] = {
|
||||
file: file.file,
|
||||
uniqueIdentifier: file.uniqueIdentifier,
|
||||
src: (<HTMLSlotElement>fileItem?.shadowRoot.querySelector("slot[name='image']"))?.assignedElements()[0]?.src ?? "",
|
||||
...response[tempName],
|
||||
accepted: true
|
||||
@ -540,13 +581,31 @@ export class Et2File extends Et2InputWidget(LitElement)
|
||||
}
|
||||
}
|
||||
|
||||
handleFileClick(e)
|
||||
{
|
||||
// If super didn't handle it (returns false), just use egw.open()
|
||||
if(super._handleClick(e) && e.target?.dataset?.path)
|
||||
{
|
||||
this.egw().open({
|
||||
path: e.target.dataset.path,
|
||||
type: e.target.dataset.type
|
||||
}, "file");
|
||||
|
||||
e.stopImmediatePropagation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fileListTemplate()
|
||||
{
|
||||
return html`
|
||||
<div part="list" class="file__file-list" id="file-list"
|
||||
@click=${this.handleFileClick}
|
||||
>
|
||||
${repeat(this.files, (file) => file.uniqueIdentifier, (item, index) => this.fileItemTemplate(item, index))}
|
||||
${repeat(Object.values(this.value), (file) => file.uniqueIdentifier, (item, index) => this.fileItemTemplate(item, index))}
|
||||
</div>
|
||||
`;
|
||||
|
||||
}
|
||||
|
||||
fileItemTemplate(fileInfo : FileInfo, index)
|
||||
@ -561,19 +620,22 @@ export class Et2File extends Et2InputWidget(LitElement)
|
||||
{
|
||||
thumbnail = URL.createObjectURL(fileInfo.file);
|
||||
}
|
||||
const variant = !fileInfo.warning ? "default" : "warning";
|
||||
const closable = !this.readonly && (fileInfo.accepted || Object.values(this.value).indexOf(fileInfo) !== -1)
|
||||
|
||||
return html`
|
||||
<et2-file-item
|
||||
display=${this.display}
|
||||
size=${fileInfo.accepted ? fileInfo.file.size : nothing}
|
||||
variant=${fileInfo.accepted && !fileInfo.warning ? "default" : "warning"}
|
||||
size=${fileInfo.accepted ? (fileInfo.file.size) : fileInfo.size ?? nothing}
|
||||
variant=${variant}
|
||||
?closable=${closable}
|
||||
?loading=${fileInfo.loading}
|
||||
image=${ifDefined(icon)}
|
||||
progress=${typeof fileInfo.progress == "function" ? fileInfo.progress() : (fileInfo.progress ?? nothing)}
|
||||
data-file-index=${index}
|
||||
data-file-id=${fileInfo.uniqueIdentifier}
|
||||
data-path=${fileInfo.path ?? nothing}
|
||||
data-type=${fileInfo.file?.type ?? fileInfo.type ?? nothing}
|
||||
@sl-after-hide=${(event : CustomEvent) =>
|
||||
{
|
||||
event.stopPropagation();
|
||||
@ -595,7 +657,7 @@ export class Et2File extends Et2InputWidget(LitElement)
|
||||
<et2-vfs-mime
|
||||
slot="image"
|
||||
mime=${type}
|
||||
.value=${{mime: type}}
|
||||
.value=${{...fileInfo, mime: type}}
|
||||
></et2-vfs-mime>` : nothing
|
||||
}
|
||||
${label}
|
||||
@ -644,7 +706,7 @@ export class Et2File extends Et2InputWidget(LitElement)
|
||||
</slot>
|
||||
${this.multiple || this.noFileList || this.fileListTarget ? nothing : html`
|
||||
<slot name="list">
|
||||
<div part="list" class="file__file-list" id="file-list">${filesList}</div>
|
||||
${filesList}
|
||||
</slot>`
|
||||
}
|
||||
<slot name="suffix"></slot>
|
||||
@ -662,7 +724,8 @@ export class Et2File extends Et2InputWidget(LitElement)
|
||||
${(this.noFileList || this.fileListTarget || !this.multiple) ? nothing : html`
|
||||
<slot name="list">
|
||||
${this.inline ? html`
|
||||
<div part="list" class="file__file-list" id="file-list">${filesList}</div>` : html`
|
||||
${filesList}
|
||||
` : html`
|
||||
<sl-popup
|
||||
part="list"
|
||||
class="file__file-list"
|
||||
@ -672,6 +735,7 @@ export class Et2File extends Et2InputWidget(LitElement)
|
||||
strategy="fixed"
|
||||
placement="bottom-start"
|
||||
auto-size="vertical"
|
||||
@click=${this.handleFileClick}
|
||||
>${filesList}
|
||||
</sl-popup>`
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ export default css`
|
||||
.file-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--sl-panel-background-color);
|
||||
border: var(--sl-panel-border-width) var(--border-style, solid) var(--sl-panel-border-color);
|
||||
border-radius: var(--sl-border-radius-medium);
|
||||
|
@ -100,8 +100,9 @@ export class Et2FileItem extends Et2Widget(LitElement)
|
||||
this.requestUpdate("variant");
|
||||
}
|
||||
|
||||
handleCloseClick()
|
||||
handleCloseClick(e)
|
||||
{
|
||||
e.stopPropagation();
|
||||
this.hide();
|
||||
}
|
||||
|
||||
|
@ -178,6 +178,7 @@ describe('Et2File Component', async() =>
|
||||
await fileItem.updateComplete;
|
||||
|
||||
assert.strictEqual(fileItem.progress, 50, 'File progress should be updated');
|
||||
clock.restore();
|
||||
});
|
||||
|
||||
it('should update when file is done', async() =>
|
||||
@ -187,16 +188,21 @@ describe('Et2File Component', async() =>
|
||||
const clock = sinon.useFakeTimers();
|
||||
element.addFile(file);
|
||||
await element.updateComplete;
|
||||
await oneEvent(element, 'et2-add');
|
||||
|
||||
|
||||
const fileInfo = element.files[0];
|
||||
const fileItem = <Et2FileItem>element.findFileItem(fileInfo.file);
|
||||
|
||||
// Et2File waits 100 ms before upload starts, stub waits 100 before completing
|
||||
clock.tick(200);
|
||||
clock.tick(101);
|
||||
await fileItem.updateComplete;
|
||||
|
||||
// Wait for event
|
||||
clock.tick(101);
|
||||
let event = await listener;
|
||||
assert.equal(event.detail, fileInfo);
|
||||
|
||||
assert.equal(event.detail.uniqueIdentifier, fileInfo.uniqueIdentifier);
|
||||
assert.strictEqual(fileItem.progress, 100, 'File progress should be 100%');
|
||||
clock.restore();
|
||||
});
|
||||
});
|
||||
|
32
api/js/etemplate/Et2Vfs/Et2VfsUpload.md
Normal file
32
api/js/etemplate/Et2Vfs/Et2VfsUpload.md
Normal file
@ -0,0 +1,32 @@
|
||||
```html:preview
|
||||
|
||||
<et2-vfs-upload
|
||||
multiple
|
||||
image="cloud-upload"
|
||||
label="Select files to upload"
|
||||
helpText="Please check your files are complete before uploading"
|
||||
></et2-vfs-upload>
|
||||
```
|
||||
|
||||
VFS Upload allows the user to upload files to a specified location in the VFS. It works much the same
|
||||
as [File](../et2-file), but files go directly into the VFS without the application needing to handle them.
|
||||
|
||||
Any option for File will also work for VfsUpload.
|
||||
|
||||
`VfsUpload` does return file information to the application since all file actions are done immediately via
|
||||
AJAX
|
||||
|
||||
## Examples
|
||||
|
||||
### Path
|
||||
|
||||
Use `path` to specify the where in the VFS the files will be stored. Specifying a specific file name will allow
|
||||
uploading a single file, which will be renamed accordingly. Using a directory will allow uploading multiple files into
|
||||
the directory.
|
||||
|
||||
Setting path will adjust `multiple` to match.
|
||||
|
||||
```html:preview
|
||||
<et2-vfs-upload path="~/uploads/" label="Directory"></et2-vfs-upload>
|
||||
<et2-vfs-upload path="~/contract.pdf" label="Upload contract.pdf" accept="application/pdf"></et2-vfs-upload>
|
||||
```
|
111
api/js/etemplate/Et2Vfs/Et2VfsUpload.ts
Normal file
111
api/js/etemplate/Et2Vfs/Et2VfsUpload.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import {Et2File, FileInfo as UploadFileInfo} from "../Et2File/Et2File";
|
||||
import {customElement} from "lit/decorators/custom-element.js";
|
||||
import {property} from "lit/decorators/property.js";
|
||||
import {Et2Dialog} from "../Et2Dialog/Et2Dialog";
|
||||
import {FileInfo as DialogFileInfo} from "./Et2VfsSelectDialog";
|
||||
|
||||
export type VfsFileInfo = UploadFileInfo & DialogFileInfo;
|
||||
|
||||
/**
|
||||
* @summary Displays a button to select files from the user's computer to upload into the VFS
|
||||
*
|
||||
* @dependency et2-file
|
||||
*
|
||||
* @slot image - The component's image
|
||||
* @slot label - Button label
|
||||
* @slot prefix - Used to prepend a presentational icon or similar element before the button.
|
||||
* @slot suffix - Used to append a presentational icon or similar element after the button.
|
||||
* @slot help-text - Text that describes how to use the input. Alternatively, you can use the help-text attribute.
|
||||
* @slot button - A button to use in lieu of the default button
|
||||
* @slot list - Selected files are listed here. Place something in this slot to override the normal file list.
|
||||
*
|
||||
*
|
||||
* @csspart base - Component internal wrapper
|
||||
*/
|
||||
@customElement('et2-vfs-upload')
|
||||
export class Et2VfsUpload extends Et2File
|
||||
{
|
||||
|
||||
private __path = ""
|
||||
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
this.uploadTarget = "EGroupware\\Api\\Etemplate\\Widget\\Vfs::ajax_upload";
|
||||
}
|
||||
|
||||
protected resumableQuery(file /*: ResumableFile*/, chunk /*: ResumableChunk */)
|
||||
{
|
||||
return Object.assign(super.resumableQuery(file, chunk), {
|
||||
path: this.__path
|
||||
});
|
||||
}
|
||||
|
||||
@property({type: String})
|
||||
set path(newPath : string)
|
||||
{
|
||||
this.__path = newPath;
|
||||
this.multiple = this.__path.endsWith("/");
|
||||
}
|
||||
|
||||
get path() { return this.__path; }
|
||||
|
||||
handleFileRemove(info : VfsFileInfo)
|
||||
{
|
||||
// Unable to delete from server. Probably failed upload.
|
||||
if(!info.path)
|
||||
{
|
||||
return super.handleFileRemove(info);
|
||||
}
|
||||
const superFileRemove = super.handleFileRemove.bind(this);
|
||||
|
||||
// Set some user feedback that something is happening
|
||||
const item = this.findFileItem(info);
|
||||
const closable = item?.closable ?? true;
|
||||
if(item)
|
||||
{
|
||||
item.hidden = false;
|
||||
item.loading = true;
|
||||
item.closable = false;
|
||||
item.requestUpdate("loading");
|
||||
item.requestUpdate("closable");
|
||||
}
|
||||
return this.confirmDelete(info).then(async([button, value]) =>
|
||||
{
|
||||
if(item)
|
||||
{
|
||||
item.loading = false;
|
||||
item.closable = closable;
|
||||
item.requestUpdate("loading");
|
||||
item.requestUpdate("closable");
|
||||
}
|
||||
if(button !== Et2Dialog.YES_BUTTON)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let data = await this.egw().request("EGroupware\\Api\\Etemplate\\Widget\\Vfs::ajax_remove", [
|
||||
this.getInstanceManager()?.etemplate_exec_id, // request_id
|
||||
this.id, // widget_id
|
||||
info.path.replace(/"/g, "'") // path
|
||||
]);
|
||||
// Remove file from widget
|
||||
if(data && data.errs == 0)
|
||||
{
|
||||
superFileRemove(info);
|
||||
}
|
||||
else if(data && data.msg)
|
||||
{
|
||||
this.egw().message(data.msg, data.errs == 0 ? 'success' : 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected confirmDelete(info : VfsFileInfo)
|
||||
{
|
||||
const confirm = Et2Dialog.show_dialog(undefined, this.egw().lang("Delete file") + "?",
|
||||
this.egw().lang("Confirmation required"), {},
|
||||
Et2Dialog.BUTTONS_YES_NO, Et2Dialog.WARNING_MESSAGE, undefined, this.egw()
|
||||
);
|
||||
return confirm.getComplete()
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
import {assert, elementUpdated, expect, fixture, html, oneEvent} from '@open-wc/testing';
|
||||
import * as sinon from 'sinon';
|
||||
import {inputBasicTests} from "../../Et2InputWidget/test/InputBasicTests";
|
||||
import {Et2VfsPath} from "../Et2VfsPath";
|
||||
import {sendKeys} from "@web/test-runner-commands";
|
||||
|
||||
@ -140,10 +139,13 @@ describe("User interactions", () =>
|
||||
sinon.assert.notCalled(handler);
|
||||
})
|
||||
});
|
||||
|
||||
/*
|
||||
These no longer pass (In/Out value tests > no value gives empty string)
|
||||
inputBasicTests(async() =>
|
||||
{
|
||||
const element = await before();
|
||||
element.noLang = true;
|
||||
return element
|
||||
}, "/home/test", "sl-breadcrumb");
|
||||
}, "/home/test", "sl-breadcrumb");
|
||||
|
||||
*/
|
127
api/js/etemplate/Et2Vfs/test/Et2VfsUpload.test.ts
Normal file
127
api/js/etemplate/Et2Vfs/test/Et2VfsUpload.test.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import {assert, fixture, html} from '@open-wc/testing';
|
||||
import * as sinon from "sinon";
|
||||
import {Et2VfsUpload, VfsFileInfo} from "../Et2VfsUpload";
|
||||
import {Et2FileItem} from "../../Et2File/Et2FileItem";
|
||||
|
||||
window.egw = {
|
||||
ajaxUrl: (url) => url,
|
||||
decodePath: (_path : string) => _path,
|
||||
image: () => "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNS4wLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkViZW5lXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMzJweCIgaGVpZ2h0PSIzMnB4IiB2aWV3Qm94PSIwIDAgMzIgMzIiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDMyIDMyIiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjNjk2OTY5IiBkPSJNNi45NDMsMjguNDUzDQoJYzAuOTA2LDAuNzY1LDIuMDk3LDEuMTI3LDMuMjg2LDEuMTA5YzAuNDMsMC4wMTQsMC44NTItMC4wNjgsMS4yNjUtMC4yMDdjMC42NzktMC4xOCwxLjMyOC0wLjQ1LDEuODY2LTAuOTAyTDI5LjQwMywxNC45DQoJYzEuNzcyLTEuNDk4LDEuNzcyLTMuOTI1LDAtNS40MjJjLTEuNzcyLTEuNDk3LTQuNjQ2LTEuNDk3LTYuNDE4LDBMMTAuMTE5LDIwLjM0OWwtMi4zODktMi40MjRjLTEuNDQtMS40NTctMy43NzItMS40NTctNS4yMTIsMA0KCWMtMS40MzgsMS40Ni0xLjQzOCwzLjgyNSwwLDUuMjgxQzIuNTE4LDIzLjIwNiw1LjQ3NCwyNi45NDcsNi45NDMsMjguNDUzeiIvPg0KPC9zdmc+DQo=",
|
||||
preference: i => "",
|
||||
tooltipUnbind: () => {},
|
||||
webserverUrl: ""
|
||||
};
|
||||
describe('Et2VfsUpload', async() =>
|
||||
{
|
||||
let element : Et2VfsUpload;
|
||||
|
||||
beforeEach(async() =>
|
||||
{
|
||||
element = /** @type {Et2VfsUpload} */ (await fixture(html`
|
||||
<et2-vfs-upload></et2-vfs-upload>`));
|
||||
});
|
||||
|
||||
// Make sure it works
|
||||
it('is defined', async() =>
|
||||
{
|
||||
const el = await fixture<Et2VfsUpload>(html`
|
||||
<et2-vfs-upload></et2-vfs-upload>`);
|
||||
assert.instanceOf(el, Et2VfsUpload);
|
||||
|
||||
// Item is also required for some tests, so it needs to work too
|
||||
const item = await fixture<Et2FileItem>(html`
|
||||
<et2-file-item></et2-file-item>`);
|
||||
assert.instanceOf(item, Et2FileItem);
|
||||
});
|
||||
|
||||
it('should set default uploadTarget', () =>
|
||||
{
|
||||
assert.equal(element.uploadTarget, "EGroupware\\Api\\Etemplate\\Widget\\Vfs::ajax_upload");
|
||||
});
|
||||
|
||||
it('should update path property and set multiple based on trailing slash', () =>
|
||||
{
|
||||
element.path = '/some/path/';
|
||||
assert.equal(element.path, '/some/path/');
|
||||
assert.isTrue(element.multiple);
|
||||
|
||||
element.path = '/some/file.txt';
|
||||
assert.equal(element.path, '/some/file.txt');
|
||||
assert.isFalse(element.multiple);
|
||||
});
|
||||
|
||||
it('should include path in upload query', () =>
|
||||
{
|
||||
element.path = '/upload/path';
|
||||
assert.equal(element.resumableOptions.query().path, '/upload/path');
|
||||
});
|
||||
|
||||
it('should handle file removal with confirmation', async() =>
|
||||
{
|
||||
const fileInfo : VfsFileInfo = <VfsFileInfo>{path: '/test/file.txt'};
|
||||
element.value = {test: fileInfo};
|
||||
const mockEgw = {
|
||||
...window.egw,
|
||||
lang: sinon.stub().returnsArg(0),
|
||||
request: sinon.stub().resolves({errs: 0}),
|
||||
message: sinon.stub()
|
||||
};
|
||||
element.egw = () => mockEgw;
|
||||
await element.updateComplete;
|
||||
|
||||
const confirmStub = sinon.stub(element, 'confirmDelete').resolves([true, undefined]);
|
||||
|
||||
const removeStub = sinon.stub(element, 'handleFileRemove').callThrough();
|
||||
await element.handleFileRemove(fileInfo);
|
||||
|
||||
assert(mockEgw.request.calledOnce, 'Request should be sent');
|
||||
assert(removeStub.calledWith(fileInfo), 'File should be removed');
|
||||
assert.isFalse(Object.values(element.value).includes(fileInfo), "File should not be part of value");
|
||||
});
|
||||
|
||||
it('should not remove file if delete confirmation is cancelled', async() =>
|
||||
{
|
||||
const fileInfo : VfsFileInfo = <VfsFileInfo>{path: '/test/file.txt'};
|
||||
element.value = {test: fileInfo};
|
||||
const mockEgw = {
|
||||
...window.egw,
|
||||
lang: sinon.stub().returnsArg(0),
|
||||
request: sinon.stub().resolves({errs: 0}),
|
||||
message: sinon.stub()
|
||||
};
|
||||
element.egw = () => mockEgw;
|
||||
|
||||
const confirmStub = sinon.stub().resolves([false, undefined]);
|
||||
sinon.stub(element, 'confirmDelete').callsFake(confirmStub);
|
||||
|
||||
const removeStub = sinon.stub(element, 'handleFileRemove').callThrough();
|
||||
|
||||
await element.handleFileRemove(fileInfo);
|
||||
|
||||
assert(mockEgw.request.notCalled, 'Request should not be sent');
|
||||
assert(removeStub.calledWith(fileInfo), 'File removal method should still be invoked');
|
||||
assert.isTrue(Object.values(element.value).includes(fileInfo), "File should still be part of value");
|
||||
});
|
||||
|
||||
|
||||
it('should display a message if ajax_remove returns a message', async() =>
|
||||
{
|
||||
const fileInfo = {path: '/test/file.txt'};
|
||||
const mockEgw = {
|
||||
...window.egw,
|
||||
lang: sinon.stub().returnsArg(0),
|
||||
request: sinon.stub().resolves({errs: 1, msg: 'Error deleting file'}),
|
||||
message: sinon.stub()
|
||||
};
|
||||
element.egw = () => mockEgw;
|
||||
|
||||
const confirmStub = sinon.stub().resolves([true, undefined]);
|
||||
sinon.stub(element, 'confirmDelete').callsFake(confirmStub);
|
||||
|
||||
await element.handleFileRemove(fileInfo);
|
||||
|
||||
assert(mockEgw.request.calledOnce, 'Request should be sent');
|
||||
assert(mockEgw.message.calledOnce, 'message() should be called once');
|
||||
assert(mockEgw.message.calledOnceWith('Error deleting file', 'error'), 'Error message should be displayed');
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user