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`
+
+ {
+ // Change icon for some feedback
+ e.target.querySelectorAll("[slot=prefix]").forEach(n => 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