Et2LinkTo: Fix several bugs

- Linked entries had no thumbnail
- Linking to VFS before initial save lost links
- Missing icon in VFS clipboard paste
This commit is contained in:
nathan 2024-10-16 16:31:07 -06:00
parent a84dbd4c25
commit e31470c58f
3 changed files with 181 additions and 106 deletions

View File

@ -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 {repeat} from "lit/directives/repeat.js";
import {LinkInfo} from "./Et2Link"; import {LinkInfo} from "./Et2Link";
import {egw} from "../../jsapi/egw_global"; import {egw} from "../../jsapi/egw_global";
@ -46,58 +46,69 @@ export class Et2LinkList extends Et2LinkString
return [ return [
...super.styles, ...super.styles,
css` css`
:host { :host {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
column-gap: 10px; column-gap: 10px;
overflow: hidden; overflow: hidden;
} }
div { div {
display: flex; display: flex;
gap: 10px; gap: 10px;
} }
div:hover { div:hover {
background-color: var(--highlight-background-color); background-color: var(--highlight-background-color);
} }
div.zip_highlight { div.zip_highlight {
animation-name: new_entry_pulse, new_entry_clear; animation-name: new_entry_pulse, new_entry_clear;
animation-duration: 5s; animation-duration: 5s;
animation-delay: 0s, 30s; animation-delay: 0s, 30s;
animation-fill-mode: forwards; animation-fill-mode: forwards;
} }
/* CSS for child elements */ /* CSS for child elements */
::slotted(*):after { et2-link::part(title):after {
/* Reset from Et2LinkString */ /* Reset from Et2LinkString */
content: initial; content: initial;
} }
::slotted(*)::part(icon) { et2-link::part(icon) {
width: 1rem; width: 1rem;
} display: inline-block;
}
::slotted(et2-link) { et2-link {
flex: 1 1 auto; display: block;
} flex: 1 1 auto;
}
::slotted(.remark) { et2-link:hover {
flex: 1 1 auto; text-decoration: none;
width: 20%; }
}
::slotted(.delete_button) { et2-link::part(base) {
visibility: hidden; display: flex;
width: 16px; }
order: 5;
}
div:hover ::slotted(.delete_button) { .remark {
visibility: initial; 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;
}
` `
]; ];
} }
@ -164,7 +175,35 @@ export class Et2LinkList extends Et2LinkString
${repeat(this._link_list, ${repeat(this._link_list,
(link) => link.app + ":" + link.id, (link) => link.app + ":" + link.id,
(link) => this._rowTemplate(link)) (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`
<et2-button image="box-arrow-down" label="${more}" noSubmit="true"
._parent=${this}
@click="${(e) =>
{
// 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);
}}"
></et2-button>` : nothing}`;
});
} }
/** /**
@ -193,7 +232,7 @@ export class Et2LinkList extends Et2LinkString
{ {
const id = typeof link.id === "string" ? link.id : link.link_id; const id = typeof link.id === "string" ? link.id : link.link_id;
return html` return html`
<et2-link slot="${this._get_row_id(link)}" app="${link.app}" entryId="${id}" statustext="${link.title}" <et2-link app="${link.app}" entryId="${id}" statustext="${link.title}"
._parent=${this} ._parent=${this}
.value=${link}></et2-link> .value=${link}></et2-link>
${this._deleteButtonTemplate(link)} ${this._deleteButtonTemplate(link)}
@ -214,7 +253,7 @@ export class Et2LinkList extends Et2LinkString
return html` return html`
<div id="${this._get_row_id(link)}" <div id="${this._get_row_id(link)}"
@contextmenu=${this._handleRowContext}> @contextmenu=${this._handleRowContext}>
<slot name="${this._get_row_id(link)}"></slot> ${this._linkTemplate(link)}
</div>`; </div>`;
} }
@ -242,7 +281,24 @@ export class Et2LinkList extends Et2LinkString
{ {
if(_ev && typeof _ev.currentTarget) if(_ev && typeof _ev.currentTarget)
{ {
this.get_links(_ev.detail || []); // Add in new links from LinkTo
for(let link of <LinkInfo[]>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) protected _delete_link(link : LinkInfo)
{ {
let link_element = <HTMLElement>this.querySelector("et2-link[slot='" + this._get_row_id(link) + "']"); let link_element = <HTMLElement>this.shadowRoot.querySelector("[id='" + this._get_row_id(link) + "']");
link_element.classList.add("loading"); link_element.classList.add("loading");
this.dispatchEvent(new CustomEvent("et2-before-delete", {detail: link})); this.dispatchEvent(new CustomEvent("et2-before-delete", {detail: link}));
let removeLink = () => 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) if(this._link_list.indexOf(link) != -1)
{ {
this._link_list.splice(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})); this.requestUpdate();
let change = new Event("change", {bubbles: true}); this.updateComplete.then(() =>
change['data'] = link; {
this.dispatchEvent(change); 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 // Unsaved entry, had no ID yet
@ -534,7 +595,7 @@ export class Et2LinkList extends Et2LinkString
this._createContextMenu(); this._createContextMenu();
} }
// Find the link // 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); let _link_data = Object.assign({app: link.app, id: link.entryId}, link.dataset);
// Comment only available if link_id is there and not readonly // 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) 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) if(!remark)
{ {
console.warn("Could not find link to comment on", link); console.warn("Could not find link to comment on", link);

View File

@ -24,6 +24,7 @@ import {Et2VfsSelectButton} from "../Et2Vfs/Et2VfsSelectButton";
import {Et2LinkPasteDialog, getClipboardFiles} from "./Et2LinkPasteDialog"; import {Et2LinkPasteDialog, getClipboardFiles} from "./Et2LinkPasteDialog";
import {waitForEvent} from "../Et2Widget/event"; import {waitForEvent} from "../Et2Widget/event";
import {classMap} from "lit/directives/class-map.js"; 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. * 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 pasteButton() : Et2VfsSelectButton { return this.shadowRoot?.querySelector("#paste"); }
private get pasteDialog() { return this.pasteButton?.querySelector("et2-link-paste-dialog"); } private get pasteDialog() : Et2LinkPasteDialog { return <Et2LinkPasteDialog><unknown>this.pasteButton?.querySelector("et2-link-paste-dialog"); }
private get vfsDialog() : Et2VfsSelectDialog { return <Et2VfsSelectDialog><unknown>this.shadowRoot.querySelector("#link")?.shadowRoot.querySelector("et2-vfs-select-dialog")}
constructor() constructor()
{ {
@ -150,7 +153,10 @@ export class Et2LinkTo extends Et2InputWidget(LitElement)
getClipboardFiles().then((files) => 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')} .buttonLabel=${this.egw().lang('Link')}
@change=${async() => @change=${async() =>
{ {
this.handleVfsSelected(await this.shadowRoot.getElementById("link")._dialog.getComplete()); this.handleVfsSelected(await this.vfsDialog.getComplete());
}} }}
> >
<et2-button slot="footer" image="copy" id="copy" style="order:3" noSubmit="true" <et2-button slot="footer" image="copy" id="copy" style="order:3" noSubmit="true"
label=${this.egw().lang("copy")}></et2-button> label=${this.egw().lang("copy")}></et2-button>
<et2-button slot="footer" image="move" id="move" style="order:3" noSubmit="true" <et2-button slot="footer" image="move" id="move" style="order:3" noSubmit="true" ?disabled=${!method_id}
label=${this.egw().lang("move")}></et2-button> label=${this.egw().lang("move")}></et2-button>
</et2-vfs-select> </et2-vfs-select>
<et2-vfs-select <et2-vfs-select
id="paste" id="paste"
image="linkpaste" aria-label=${this.egw().lang("clipboard contents")} noSubmit="true" image="clipboard-data" aria-label=${this.egw().lang("clipboard contents")} noSubmit="true"
title=${this.egw().lang("Clipboard contents")} title=${this.egw().lang("Clipboard contents")}
?readonly=${this.readonly} ?readonly=${this.readonly}
disabled disabled
@ -192,7 +198,7 @@ export class Et2LinkTo extends Et2InputWidget(LitElement)
waitForEvent(e.target._dialog, "sl-after-show").then(async() => waitForEvent(e.target._dialog, "sl-after-show").then(async() =>
{ {
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; fileInfo.push({...this.pasteDialog.fileInfo(file), app: "link"});
} })
let values = {}; this.handleVfsFile(button, fileInfo);
for(var i = 0; i < files.length; i++) this.pasteButton.value = [];
{
values['link:' + files[i]] = {
app: 'link',
id: files[i],
type: 'unknown',
icon: 'link',
remark: '',
title: files[i]
};
}
this._link_result(values);
} }
handleVfsSelected([button, selected]) 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; return;
} }
@ -523,31 +538,30 @@ export class Et2LinkTo extends Et2InputWidget(LitElement)
if(!this.value.to_id || typeof this.value.to_id == 'object') if(!this.value.to_id || typeof this.value.to_id == 'object')
{ {
values = this.value.to_id || {}; values = this.value.to_id || {};
for(let i = 0; i < selected.length; i++) selectedFileInfo.forEach(info =>
{ {
const info = this.pasteDialog.fileInfo(selected[i]); debugger;
values['link:' + selected[i]] = { values['link:' + info.path] = {
app: info?.app == "filemanager" ? "link" : info?.app, app: info?.app,
id: info?.app == "filemanager" ? selected[i] : info?.id, id: info.path ?? info.id,
type: 'unknown', type: 'unknown',
icon: 'link', icon: 'link',
remark: '', remark: '',
title: selected[i] title: info.path
}; };
} });
} }
else else
{ {
// Send to server to link // Send to server to link
const files = []; const files = [];
const links = []; const links = [];
selected.forEach(id => selectedFileInfo.forEach(info =>
{ {
const info = this.pasteDialog.fileInfo(id);
switch(info?.app) switch(info?.app)
{ {
case "filemanager": case "filemanager":
files.push(id); files.push(info.path);
break; break;
default: default:
links.push({app: info.app, id: info.id}); links.push({app: info.app, id: info.id});
@ -567,7 +581,6 @@ export class Et2LinkTo extends Et2InputWidget(LitElement)
this.createLink(links); this.createLink(links);
} }
} }
this.pasteButton.value = [];
this._link_result(values); this._link_result(values);
} }

View File

@ -201,8 +201,8 @@ class Link extends Etemplate\Widget
$app = $value['to_app']; $app = $value['to_app'];
$id = $value['to_id']; $id = $value['to_id'];
$links = Api\Link::get_links($app, $id, $value['only_app'] ?? '', 'link_lastmod DESC', true, $value['show_deleted'], $value['limit'] ?? null); $links = Api\Link::get_links($app, $id, $value['only_app'], 'link_lastmod DESC, link_id DESC', true, $value['show_deleted'], $value['limit']);
$limit_exceeded = !empty($value['limit']) && Api\Link::$limit_exceeded;
$only_links = []; $only_links = [];
if($value['only_app']) if($value['only_app'])
{ {
@ -244,19 +244,12 @@ class Link extends Etemplate\Widget
$link['help'] = lang('Remove this link (not the entry itself)'); $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(); $response = Api\Json\Response::get();
// Strip keys, unneeded and cause index problems on the client side // 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 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);
}
} }
} }
} }