diff --git a/api/js/etemplate/Et2Link/Et2LinkList.ts b/api/js/etemplate/Et2Link/Et2LinkList.ts index 81c1dcc638..b009a73357 100644 --- a/api/js/etemplate/Et2Link/Et2LinkList.ts +++ b/api/js/etemplate/Et2Link/Et2LinkList.ts @@ -10,7 +10,7 @@ */ -import {css, html, TemplateResult} from "lit"; +import {css, html, nothing, TemplateResult} from "lit"; import {repeat} from "lit/directives/repeat.js"; import {LinkInfo} from "./Et2Link"; import {egw} from "../../jsapi/egw_global"; @@ -46,58 +46,69 @@ export class Et2LinkList extends Et2LinkString return [ ...super.styles, css` - :host { - display: flex; - flex-direction: column; - column-gap: 10px; - overflow: hidden; - } + :host { + display: flex; + flex-direction: column; + column-gap: 10px; + overflow: hidden; + } - div { - display: flex; - gap: 10px; - } + div { + display: flex; + gap: 10px; + } - div:hover { - background-color: var(--highlight-background-color); - } + div:hover { + background-color: var(--highlight-background-color); + } - div.zip_highlight { - animation-name: new_entry_pulse, new_entry_clear; - animation-duration: 5s; - animation-delay: 0s, 30s; - animation-fill-mode: forwards; - } + div.zip_highlight { + animation-name: new_entry_pulse, new_entry_clear; + animation-duration: 5s; + animation-delay: 0s, 30s; + animation-fill-mode: forwards; + } - /* CSS for child elements */ + /* CSS for child elements */ - ::slotted(*):after { - /* Reset from Et2LinkString */ - content: initial; - } + et2-link::part(title):after { + /* Reset from Et2LinkString */ + content: initial; + } - ::slotted(*)::part(icon) { - width: 1rem; - } + et2-link::part(icon) { + width: 1rem; + display: inline-block; + } - ::slotted(et2-link) { - flex: 1 1 auto; - } + et2-link { + display: block; + flex: 1 1 auto; + } - ::slotted(.remark) { - flex: 1 1 auto; - width: 20%; - } + et2-link:hover { + text-decoration: none; + } - ::slotted(.delete_button) { - visibility: hidden; - width: 16px; - order: 5; - } + et2-link::part(base) { + display: flex; + } - div:hover ::slotted(.delete_button) { - visibility: initial; - } + .remark { + flex: 1 1 auto; + width: 20%; + } + + div et2-image[part=delete-button] { + visibility: hidden; + width: 16px; + order: 5; + cursor: pointer; + } + + div:hover et2-image[part=delete-button] { + visibility: initial; + } ` ]; } @@ -115,7 +126,7 @@ export class Et2LinkList extends Et2LinkString readonly: {type: Boolean} } } - + private context : egwMenu; constructor() @@ -164,7 +175,35 @@ export class Et2LinkList extends Et2LinkString ${repeat(this._link_list, (link) => link.app + ":" + link.id, (link) => this._rowTemplate(link)) - }`; + } + `; + } + + protected async moreResultsTemplate() + { + if(this._totalResults <= 0 || !this._loadingPromise) + { + return nothing; + } + return this._loadingPromise.then(() => + { + const moreCount = this._totalResults - this._link_list.length; + const more = this.egw().lang("%1 more...", moreCount); + return html`${moreCount > 0 ? html` + n.remove()); + e.target.append(Object.assign(document.createElement("sl-spinner"), {slot: "prefix"})); + + // Get the next batch + const start = this._link_list.filter(l => l.app !== "file").length; + this.get_links([], start); + }}" + >` : nothing}`; + }); } /** @@ -193,7 +232,7 @@ export class Et2LinkList extends Et2LinkString { const id = typeof link.id === "string" ? link.id : link.link_id; return html` - ${this._deleteButtonTemplate(link)} @@ -214,7 +253,7 @@ export class Et2LinkList extends Et2LinkString return html`
- + ${this._linkTemplate(link)}
`; } @@ -242,7 +281,24 @@ export class Et2LinkList extends Et2LinkString { if(_ev && typeof _ev.currentTarget) { - this.get_links(_ev.detail || []); + // Add in new links from LinkTo + for(let link of Object.values(_ev.detail || [])) + { + if(!this._link_list.some(l => l.app == link.app && l.id == link.id)) + { + this._link_list.unshift(link); + } + } + // No need to ask server if we got it in the event + if(_ev.detail.length) + { + this.requestUpdate(); + } + else + { + // Event didn't have it, need to ask + this.get_links(); + } } } @@ -290,22 +346,27 @@ export class Et2LinkList extends Et2LinkString */ protected _delete_link(link : LinkInfo) { - let link_element = this.querySelector("et2-link[slot='" + this._get_row_id(link) + "']"); + let link_element = this.shadowRoot.querySelector("[id='" + this._get_row_id(link) + "']"); link_element.classList.add("loading"); this.dispatchEvent(new CustomEvent("et2-before-delete", {detail: link})); let removeLink = () => { - this.querySelectorAll("[slot='" + this._get_row_id(link) + "']").forEach(e => e.remove()); + this.shadowRoot.querySelectorAll("[id='" + this._get_row_id(link) + "']").forEach(e => e.remove()); if(this._link_list.indexOf(link) != -1) { this._link_list.splice(this._link_list.indexOf(link), 1); + this._totalResults--; } - this.dispatchEvent(new CustomEvent("et2-delete", {bubbles: true, detail: link})); - let change = new Event("change", {bubbles: true}); - change['data'] = link; - this.dispatchEvent(change); + this.requestUpdate(); + this.updateComplete.then(() => + { + this.dispatchEvent(new CustomEvent("et2-delete", {bubbles: true, detail: link})); + let change = new Event("change", {bubbles: true}); + change['data'] = link; + this.dispatchEvent(change); + }) }; // Unsaved entry, had no ID yet @@ -534,7 +595,7 @@ export class Et2LinkList extends Et2LinkString this._createContextMenu(); } // Find the link - let link = this.querySelector("et2-link[slot='" + _ev.currentTarget.id + "']"); + let link = _ev.currentTarget.querySelector("et2-link"); let _link_data = Object.assign({app: link.app, id: link.entryId}, link.dataset); // Comment only available if link_id is there and not readonly @@ -555,7 +616,7 @@ export class Et2LinkList extends Et2LinkString protected _set_comment(link, comment) { - let remark = this.querySelector("et2-link[slot='" + this._get_row_id(link) + "']"); + let remark = this.shadowRoot.querySelector("#" + this._get_row_id(link) + " et2-link"); if(!remark) { console.warn("Could not find link to comment on", link); diff --git a/api/js/etemplate/Et2Link/Et2LinkTo.ts b/api/js/etemplate/Et2Link/Et2LinkTo.ts index 02d0e12d9b..26a6754f9f 100644 --- a/api/js/etemplate/Et2Link/Et2LinkTo.ts +++ b/api/js/etemplate/Et2Link/Et2LinkTo.ts @@ -24,6 +24,7 @@ import {Et2VfsSelectButton} from "../Et2Vfs/Et2VfsSelectButton"; import {Et2LinkPasteDialog, getClipboardFiles} from "./Et2LinkPasteDialog"; import {waitForEvent} from "../Et2Widget/event"; import {classMap} from "lit/directives/class-map.js"; +import {Et2VfsSelectDialog} from "../Et2Vfs/Et2VfsSelectDialog"; /** * Choose an existing entry, VFS file or local file, and link it to the current entry. @@ -100,7 +101,9 @@ export class Et2LinkTo extends Et2InputWidget(LitElement) private get pasteButton() : Et2VfsSelectButton { return this.shadowRoot?.querySelector("#paste"); } - private get pasteDialog() { return this.pasteButton?.querySelector("et2-link-paste-dialog"); } + private get pasteDialog() : Et2LinkPasteDialog { return this.pasteButton?.querySelector("et2-link-paste-dialog"); } + + private get vfsDialog() : Et2VfsSelectDialog { return this.shadowRoot.querySelector("#link")?.shadowRoot.querySelector("et2-vfs-select-dialog")} constructor() { @@ -150,7 +153,10 @@ export class Et2LinkTo extends Et2InputWidget(LitElement) getClipboardFiles().then((files) => { - this.pasteButton.disabled = files.length == 0; + if(files.length > 0) + { + this.pasteButton.removeAttribute("disabled"); + } }); } @@ -166,17 +172,17 @@ export class Et2LinkTo extends Et2InputWidget(LitElement) .buttonLabel=${this.egw().lang('Link')} @change=${async() => { - this.handleVfsSelected(await this.shadowRoot.getElementById("link")._dialog.getComplete()); + this.handleVfsSelected(await this.vfsDialog.getComplete()); }} > - { - this.handleVfsSelected(await this.pasteButton._dialog.getComplete()); + this.handleFilePaste(await this.pasteDialog.getComplete()); }); }} > @@ -491,30 +497,39 @@ export class Et2LinkTo extends Et2InputWidget(LitElement) } } - handleFilePaste([button, files]) + handleFilePaste([button, selected]) { - if(!button) + let fileInfo = [] + selected.forEach(file => { - return; - } - let values = {}; - for(var i = 0; i < files.length; i++) - { - values['link:' + files[i]] = { - app: 'link', - id: files[i], - type: 'unknown', - icon: 'link', - remark: '', - title: files[i] - }; - } - this._link_result(values); + fileInfo.push({...this.pasteDialog.fileInfo(file), app: "link"}); + }) + this.handleVfsFile(button, fileInfo); + this.pasteButton.value = []; } handleVfsSelected([button, selected]) { - if(!button || !selected?.length) + let fileInfo = [] + selected.forEach(file => + { + let info = { + ...this.vfsDialog.fileInfo(file) + } + + if(!this.value.to_id || typeof this.value.to_id == 'object') + { + info['app'] = button == "copy" ? "file" : "link"; + info['path'] = button == "copy" ? "vfs://default" + info.path : info.path; + } + fileInfo.push(info); + }) + this.handleVfsFile(button, fileInfo); + } + + protected handleVfsFile(button, selectedFileInfo) + { + if(!button) { return; } @@ -523,31 +538,30 @@ export class Et2LinkTo extends Et2InputWidget(LitElement) if(!this.value.to_id || typeof this.value.to_id == 'object') { values = this.value.to_id || {}; - for(let i = 0; i < selected.length; i++) + selectedFileInfo.forEach(info => { - const info = this.pasteDialog.fileInfo(selected[i]); - values['link:' + selected[i]] = { - app: info?.app == "filemanager" ? "link" : info?.app, - id: info?.app == "filemanager" ? selected[i] : info?.id, + debugger; + values['link:' + info.path] = { + app: info?.app, + id: info.path ?? info.id, type: 'unknown', icon: 'link', remark: '', - title: selected[i] + title: info.path }; - } + }); } else { // Send to server to link const files = []; const links = []; - selected.forEach(id => + selectedFileInfo.forEach(info => { - const info = this.pasteDialog.fileInfo(id); switch(info?.app) { case "filemanager": - files.push(id); + files.push(info.path); break; default: links.push({app: info.app, id: info.id}); @@ -567,7 +581,6 @@ export class Et2LinkTo extends Et2InputWidget(LitElement) this.createLink(links); } } - this.pasteButton.value = []; this._link_result(values); } diff --git a/api/src/Etemplate/Widget/Link.php b/api/src/Etemplate/Widget/Link.php index bc5f9c9253..85b3bb1f90 100644 --- a/api/src/Etemplate/Widget/Link.php +++ b/api/src/Etemplate/Widget/Link.php @@ -201,8 +201,8 @@ class Link extends Etemplate\Widget $app = $value['to_app']; $id = $value['to_id']; - $links = Api\Link::get_links($app, $id, $value['only_app'] ?? '', 'link_lastmod DESC', true, $value['show_deleted'], $value['limit'] ?? null); - $limit_exceeded = !empty($value['limit']) && Api\Link::$limit_exceeded; + $links = Api\Link::get_links($app, $id, $value['only_app'], 'link_lastmod DESC, link_id DESC', true, $value['show_deleted'], $value['limit']); + $only_links = []; if($value['only_app']) { @@ -244,19 +244,12 @@ class Link extends Etemplate\Widget $link['help'] = lang('Remove this link (not the entry itself)'); } } - if ($limit_exceeded) - { - $links[] = [ - 'app' => 'exceeded', - 'id' => 'exceeded', - 'title' => lang('Load more links ...'), - 'icon' => 'box-arrow-down', - ]; - } $response = Api\Json\Response::get(); // Strip keys, unneeded and cause index problems on the client side - $response->data(array_values($links)); + $result = array_values($links); + $result['total'] = Api\Link\Storage::$row_count; + $response->data($result); } /** @@ -321,9 +314,17 @@ class Link extends Etemplate\Widget } else { - foreach($files as $target) + if(!str_ends_with($dest_file, '/') && count($files) == 1 && is_int($id)) { - Api\Link::link_file($app, $id, $target); + // 1 file to a specific filename + Api\Vfs::symlink($files[0], Api\Link::vfs_path($app, $id)); + } + else + { + foreach($files as $target) + { + Api\Link::link_file($app, $id, $target); + } } } } @@ -479,4 +480,4 @@ class Link extends Etemplate\Widget //error_log(" " . array2string($valid)); } } -} +} \ No newline at end of file