852 lines
32 KiB
TypeScript
852 lines
32 KiB
TypeScript
import "./item-icon";
|
|
import "./popover";
|
|
import { until } from "lit/directives/until.js";
|
|
import { repeat } from "lit/directives/repeat.js";
|
|
import { VaultItemID, Field, FieldDef, FIELD_DEFS, VaultItem, FieldType } from "@padloc/core/src/item";
|
|
import { translate as $l } from "@padloc/locale/src/translate";
|
|
import { AttachmentInfo } from "@padloc/core/src/attachment";
|
|
import { parseURL } from "@padloc/core/src/otp";
|
|
import { formatDateFromNow } from "../lib/util";
|
|
import { alert, confirm, dialog } from "../lib/dialog";
|
|
// import { animateCascade } from "../lib/animation";
|
|
import { app, router } from "../globals";
|
|
import { shared } from "../styles";
|
|
import { setClipboard } from "../lib/clipboard";
|
|
import { Routing } from "../mixins/routing";
|
|
import { StateMixin } from "../mixins/state";
|
|
import "./icon";
|
|
import { Input } from "./input";
|
|
import { TagsInput } from "./tags-input";
|
|
import { MoveItemsDialog } from "./move-items-dialog";
|
|
import { FieldElement } from "./field";
|
|
import "./field";
|
|
import { GeneratorDialog } from "./generator-dialog";
|
|
import { AttachmentDialog } from "./attachment-dialog";
|
|
import { UploadDialog } from "./upload-dialog";
|
|
import { QRDialog } from "./qr-dialog";
|
|
import "./scroller";
|
|
import "./button";
|
|
import "./list";
|
|
import "./attachment";
|
|
import { customElement, property, query, queryAll, state } from "lit/decorators.js";
|
|
import { css, html, LitElement } from "lit";
|
|
import { checkFeatureDisabled } from "../lib/provisioning";
|
|
import { auditVaults } from "../lib/audit";
|
|
import { Popover } from "./popover";
|
|
|
|
@customElement("pl-item-view")
|
|
export class ItemView extends Routing(StateMixin(LitElement)) {
|
|
routePattern = /^items(?:\/([^\/]+)(?:\/([^\/]+))?)?/;
|
|
|
|
@property()
|
|
itemId: VaultItemID = "";
|
|
|
|
@property({ type: Boolean, reflect: true })
|
|
readonly = false;
|
|
|
|
@property({ type: Boolean })
|
|
isNew: boolean = false;
|
|
|
|
get hasChanges() {
|
|
return this._editing;
|
|
}
|
|
|
|
private get _item() {
|
|
const found = (this.itemId && app.getItem(this.itemId)) || null;
|
|
return found && found.item;
|
|
}
|
|
|
|
private get _vault() {
|
|
const found = (this.itemId && app.getItem(this.itemId)) || null;
|
|
return found && found.vault;
|
|
}
|
|
|
|
private get _org() {
|
|
return this._vault?.org ? app.getOrg(this._vault.org.id) : null;
|
|
}
|
|
|
|
private get _isEditable() {
|
|
return this._vault && app.isEditable(this._vault);
|
|
}
|
|
|
|
@state()
|
|
private _editing: boolean = false;
|
|
|
|
@state()
|
|
private _fields: Field[] = [];
|
|
|
|
@state()
|
|
private _isDraggingFileToAttach: boolean = false;
|
|
|
|
@query("#nameInput")
|
|
private _nameInput: Input;
|
|
|
|
@query("pl-tags-input")
|
|
private _tagsInput: TagsInput;
|
|
|
|
@query("#addFieldPopover")
|
|
private _addFieldPopover: Popover;
|
|
|
|
@queryAll("pl-field")
|
|
private _fieldInputs: FieldElement[];
|
|
|
|
@query("input[type='file']")
|
|
private _fileInput: HTMLInputElement;
|
|
|
|
@dialog("pl-move-items-dialog")
|
|
private _moveItemsDialog: MoveItemsDialog;
|
|
|
|
@dialog("pl-generator-dialog")
|
|
private _generatorDialog: GeneratorDialog;
|
|
|
|
@dialog("pl-attachment-dialog")
|
|
private _attachmentDialog: AttachmentDialog;
|
|
|
|
@dialog("pl-upload-dialog")
|
|
private _uploadDialog: UploadDialog;
|
|
|
|
@dialog("pl-qr-dialog")
|
|
private _qrDialog: QRDialog;
|
|
|
|
// @dialog("pl-field-type-dialog")
|
|
// private _fieldTypeDialog: FieldTypeDialog;
|
|
|
|
// private _draggingIndex = -1;
|
|
//
|
|
// private _dragOverIndex = -1;
|
|
|
|
async handleRoute(
|
|
[id, mode]: [string, string],
|
|
{ action, actionIndex, ...routerParams }: { [prop: string]: string }
|
|
) {
|
|
this.itemId = id;
|
|
|
|
if (this.itemId && !this._item) {
|
|
this.redirect("items");
|
|
}
|
|
|
|
this.isNew = mode === "new";
|
|
|
|
if (["new", "edit"].includes(mode)) {
|
|
if (!this._isEditable) {
|
|
this.redirect(`items/${this.itemId}`);
|
|
return;
|
|
}
|
|
this._editing = true;
|
|
setTimeout(() => {
|
|
switch (action) {
|
|
case "addAttachment":
|
|
this.addAttachment();
|
|
break;
|
|
case "addField":
|
|
this._addFieldPopover.show();
|
|
break;
|
|
case "editField":
|
|
this._fieldInputs[Number(actionIndex || 0)]?.focus();
|
|
break;
|
|
case "editTags":
|
|
this._tagsInput.focus();
|
|
break;
|
|
default:
|
|
this._nameInput?.focus();
|
|
}
|
|
}, 150);
|
|
} else {
|
|
this._editing = false;
|
|
}
|
|
|
|
this.router.params = routerParams;
|
|
|
|
await this.updateComplete;
|
|
this._itemChanged();
|
|
|
|
await this.updateComplete;
|
|
|
|
this._animateIn();
|
|
|
|
const item = this._item;
|
|
if (item) {
|
|
app.updateLastUsed(item);
|
|
}
|
|
}
|
|
|
|
async addAttachment() {
|
|
if (this._checkAttachmentsDisabled()) {
|
|
return;
|
|
}
|
|
await this.updateComplete;
|
|
this._fileInput.click();
|
|
}
|
|
|
|
private _checkAttachmentsDisabled() {
|
|
return this._org
|
|
? checkFeatureDisabled(app.getOrgFeatures(this._org).attachments, this._org.isOwner(app.account!))
|
|
: checkFeatureDisabled(app.getAccountFeatures().attachments);
|
|
}
|
|
|
|
private _checkTotpDisabled() {
|
|
return this._org
|
|
? checkFeatureDisabled(app.getOrgFeatures(this._org).totpField, this._org.isOwner(app.account!))
|
|
: checkFeatureDisabled(app.getAccountFeatures().totpField);
|
|
}
|
|
|
|
private _checkNotesDisabled() {
|
|
return this._org
|
|
? checkFeatureDisabled(app.getOrgFeatures(this._org).notesField, this._org.isOwner(app.account!))
|
|
: checkFeatureDisabled(app.getAccountFeatures().notesField);
|
|
}
|
|
|
|
private _moveField(index: number, target: "up" | "down" | number) {
|
|
const field = this._fields[index];
|
|
this._fields.splice(index, 1);
|
|
const targetIndex = target === "up" ? index - 1 : target === "down" ? index + 1 : target;
|
|
this._fields.splice(targetIndex, 0, field);
|
|
this.requestUpdate();
|
|
}
|
|
|
|
private _animateIn() {
|
|
// return animateCascade(this.renderRoot.querySelectorAll(".animated"), {
|
|
// animation: "slideIn",
|
|
// fill: "both",
|
|
// fullDuration: 800,
|
|
// duration: 500,
|
|
// });
|
|
}
|
|
|
|
private async _copyToClipboard(item: VaultItem, field: Field) {
|
|
setClipboard(await field.transform(), `${item.name} / ${field.name}`);
|
|
}
|
|
|
|
static styles = [
|
|
shared,
|
|
css`
|
|
:host {
|
|
display: block;
|
|
position: relative;
|
|
background: var(--color-background);
|
|
}
|
|
|
|
header {
|
|
overflow: visible;
|
|
z-index: 10;
|
|
}
|
|
|
|
.back-button {
|
|
margin-right: -0.1em;
|
|
z-index: 1;
|
|
}
|
|
|
|
.favorite-button {
|
|
--button-color: var(--color-shade-5);
|
|
--button-toggled-background: transparent;
|
|
--button-toggled-color: var(--color-favorite);
|
|
--button-toggled-weight: var(--font-weight-bold);
|
|
}
|
|
|
|
:host(.dragging) .content > * {
|
|
will-change: transform;
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
pl-field.dragover::after {
|
|
content: "";
|
|
display: block;
|
|
height: 1em;
|
|
border: dashed 2px var(--color-highlight);
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
position: absolute;
|
|
bottom: -1em;
|
|
border-radius: 0.5em;
|
|
}
|
|
|
|
pl-field.dragover ~ * {
|
|
transform: translate3d(0, 40px, 0);
|
|
}
|
|
|
|
.fields,
|
|
.attachments {
|
|
margin: 1em 0;
|
|
}
|
|
|
|
.field-selector {
|
|
max-height: calc(100vh - 5em);
|
|
overflow: auto;
|
|
}
|
|
|
|
.tags-label {
|
|
text-transform: uppercase;
|
|
padding: 0.6em 0.6em 0.6em 1.2em;
|
|
}
|
|
|
|
@media (max-width: 700px) {
|
|
.save-cancel {
|
|
padding-bottom: calc(var(--inset-bottom) + 0.5em);
|
|
}
|
|
}
|
|
`,
|
|
];
|
|
|
|
render() {
|
|
if (app.state.locked || !this._item || !this._vault) {
|
|
return html`
|
|
<div class="fullbleed centering double-padded text-centering vertical layout subtle">
|
|
<pl-icon icon="note" class="enormous regular"></pl-icon>
|
|
|
|
<div>${$l("No item selected.")}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
const { updated, updatedBy } = this._item!;
|
|
const vault = this._vault!;
|
|
const org = vault.org && app.getOrg(vault.org.id);
|
|
const updatedByMember = org && org.getMember({ accountId: updatedBy });
|
|
const attachments = this._item!.attachments || [];
|
|
const isFavorite = app.account!.favorites.has(this.itemId);
|
|
|
|
return html`
|
|
<div
|
|
class="fullbleed vertical layout"
|
|
@drop=${this._handleDrop}
|
|
@dragover=${this._handleDragOver}
|
|
@dragleave=${this._handleDragLeave}
|
|
>
|
|
<header class="padded animated">
|
|
<div class="start-aligning horizontal layout">
|
|
<pl-button
|
|
class="transparent slim back-button"
|
|
@click=${() => router.go("items")}
|
|
?hidden=${this._editing}
|
|
>
|
|
<pl-icon icon="backward"></pl-icon>
|
|
</pl-button>
|
|
|
|
<pl-input
|
|
id="nameInput"
|
|
class="large name-input ${!this._editing ? "transparent" : ""} stretch"
|
|
.placeholder=${$l("Enter Item Name")}
|
|
?readonly=${!this._editing}
|
|
select-on-focus
|
|
required
|
|
style="--input-padding: 0.3em 0.5em 0 0.5em;"
|
|
>
|
|
<div class="tiny regular subtle" style="margin: 0 0 -1em" slot="above">
|
|
<div style="display: inline-block; width: 3.3em;" class="wide-only"></div>
|
|
<div style="display: inline-block; width: 0.5em;" class="narrow-only"></div>
|
|
${vault.label}
|
|
</div>
|
|
<pl-item-icon
|
|
.item=${this._item}
|
|
slot="before"
|
|
style="margin: -0.7em 0 0 0.3em"
|
|
class="wide-only large"
|
|
></pl-item-icon>
|
|
</pl-input>
|
|
|
|
<div class="horizontal layout" ?hidden=${this._editing}>
|
|
<pl-button
|
|
@click=${() => this._setFavorite(!isFavorite)}
|
|
class="slim transparent favorite-button"
|
|
.label=${$l("Favorite")}
|
|
.toggled=${isFavorite}
|
|
>
|
|
<pl-icon icon="favorite"></pl-icon>
|
|
</pl-button>
|
|
|
|
<pl-button
|
|
class="slim transparent"
|
|
@click=${() => this.edit()}
|
|
?disabled=${!this._isEditable}
|
|
.label=${$l("Edit")}
|
|
>
|
|
<pl-icon icon="edit"></pl-icon>
|
|
</pl-button>
|
|
</div>
|
|
|
|
<div class="horizontal layout left-margined" ?hidden=${!this._editing}>
|
|
<pl-button .label=${$l("Field")} class="slim transparent">
|
|
<pl-icon icon="add"></pl-icon>
|
|
</pl-button>
|
|
|
|
<pl-popover hide-on-click alignment="bottom-left" id="addFieldPopover">
|
|
<div class="field-selector">
|
|
<pl-list>
|
|
${[...Object.values(FIELD_DEFS)].map(
|
|
(fieldDef) => html`
|
|
<div
|
|
class="small double-padded list-item center-aligning spacing horizontal layout hover click"
|
|
@click=${() => this._addField(fieldDef)}
|
|
>
|
|
<pl-icon icon="${fieldDef.icon}"></pl-icon>
|
|
<div class="ellipsis">${fieldDef.name}</div>
|
|
</div>
|
|
`
|
|
)}
|
|
<div
|
|
class="small double-padded list-item center-aligning spacing horizontal layout hover click"
|
|
@click=${() => this.addAttachment()}
|
|
>
|
|
<pl-icon icon="attachment"></pl-icon>
|
|
<div class="ellipsis">Attachment</div>
|
|
</div>
|
|
</pl-list>
|
|
</div>
|
|
</pl-popover>
|
|
</div>
|
|
|
|
<pl-button .label=${$l("More Options")} class="slim transparent" ?hidden=${this.isNew}>
|
|
<pl-icon icon="more"></pl-icon>
|
|
</pl-button>
|
|
|
|
<pl-popover hide-on-click hide-on-leave alignment="bottom-left">
|
|
<pl-list>
|
|
<div
|
|
class="small double-padded list-item center-aligning spacing horizontal layout hover click"
|
|
@click=${this._move}
|
|
>
|
|
<pl-icon icon="share"></pl-icon>
|
|
<div class="ellipsis">${$l("Move To Vault ...")}</div>
|
|
</div>
|
|
<div
|
|
class="small double-padded list-item center-aligning spacing horizontal layout hover click"
|
|
@click=${this._deleteItem}
|
|
>
|
|
<pl-icon icon="delete"></pl-icon>
|
|
<div class="ellipsis">${$l("Delete Item")}</div>
|
|
</div>
|
|
</pl-list>
|
|
</pl-popover>
|
|
</div>
|
|
${false
|
|
? html`
|
|
<div class="tiny wrapping spacing horizontal layout" style="padding-left: 4.3em">
|
|
${this._item!.tags.map(
|
|
(tag) =>
|
|
html`
|
|
<div class="tag hover click" @click=${() => this.go("items", { tag })}>
|
|
<pl-icon class="inline" icon="tag"></pl-icon>${tag}
|
|
</div>
|
|
`
|
|
)}
|
|
</div>
|
|
`
|
|
: ""}
|
|
</header>
|
|
|
|
<pl-scroller class="stretch">
|
|
<div class="vertical layout fill-vertically content">
|
|
<div class="vertically-margined border-bottom" ?hidden=${false}>
|
|
<h2
|
|
class="subtle horizontally-double-margined bottom-margined animated section-header"
|
|
style="margin-left: 1.2em;"
|
|
>
|
|
<pl-icon icon="tags" class="inline small light"></pl-icon>
|
|
${$l("tags")}
|
|
</h2>
|
|
|
|
<div class="border-top">
|
|
<pl-tags-input
|
|
?readonly=${!this._editing}
|
|
@move=${this._move}
|
|
style="margin: 0.2em 0.8em;"
|
|
@focus=${() => !this._editing && this.edit("editTags")}
|
|
></pl-tags-input>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="fields border-bottom">
|
|
<h2
|
|
class="subtle horizontally-double-margined bottom-margined animated section-header"
|
|
style="margin-left: 1.2em;"
|
|
>
|
|
<pl-icon icon="field" class="inline small light"></pl-icon>
|
|
${$l("Fields")}
|
|
</h2>
|
|
<pl-list class="border-top block">
|
|
${repeat(
|
|
this._fields,
|
|
(field) => `${this.itemId}_${field.name}_${field.type}`,
|
|
(field: Field, index: number) => html`
|
|
<pl-field
|
|
class="padded list-item"
|
|
.canMoveUp=${!!index}
|
|
.canMoveDown=${index < this._fields.length - 1}
|
|
.field=${field}
|
|
.editing=${this._editing}
|
|
.auditResults=${this._item?.auditResults.filter(
|
|
(auditResult) => auditResult.fieldIndex === index
|
|
) || []}
|
|
@copy-clipboard=${() => this._copyToClipboard(this._item!, field)}
|
|
@remove=${() => this._removeField(index)}
|
|
@generate=${() => this._generateValue(index)}
|
|
@get-totp-qr=${() => this._getTotpQR(index)}
|
|
@dragstart=${(e: DragEvent) => this._dragstart(e, index)}
|
|
@drop=${(e: DragEvent) => this._drop(e)}
|
|
@moveup=${() => this._moveField(index, "up")}
|
|
@movedown=${() => this._moveField(index, "down")}
|
|
@edit=${() => this.edit("editField", index)}
|
|
>
|
|
</pl-field>
|
|
`
|
|
)}
|
|
</pl-list>
|
|
|
|
<div
|
|
class="double-padded text-centering border-top hover click"
|
|
@click=${() => this.edit("addField")}
|
|
>
|
|
<span class="small subtle">
|
|
<pl-icon class="inline" icon="add"></pl-icon> ${$l("Add Field")}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="attachments">
|
|
<h2
|
|
class="subtle horizontally-double-margined bottom-margined animated section-header"
|
|
style="margin-left: 1.2em;"
|
|
>
|
|
<pl-icon icon="attachment" class="inline small light"></pl-icon>
|
|
${$l("Attachments")}
|
|
</h2>
|
|
|
|
<pl-list class="border-top block" ?hidden=${!attachments.length}>
|
|
${attachments.map(
|
|
(a) => html`
|
|
<pl-attachment
|
|
.info=${a}
|
|
.editing=${this._editing}
|
|
class="animated ${this._editing ? "" : "hover click"} list-item"
|
|
@click=${() => this._openAttachment(a)}
|
|
@delete=${() => this._deleteAttachment(a)}
|
|
>
|
|
</pl-attachment>
|
|
`
|
|
)}
|
|
</pl-list>
|
|
|
|
<div
|
|
class="double-padded text-centering border-top border-bottom hover click"
|
|
@click=${() => this.addAttachment()}
|
|
>
|
|
<span class="small ${this._isDraggingFileToAttach ? "highlighted bold" : "subtle"}">
|
|
<pl-icon class="inline" icon="add"></pl-icon> ${$l(
|
|
"Click or drag files here to add an attachment!"
|
|
)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stretch"></div>
|
|
|
|
<div class="animated double-margined spacing faded tiny centering horizontal layout">
|
|
<pl-icon icon="edit"></pl-icon>
|
|
<div>
|
|
${until(formatDateFromNow(updated!))}
|
|
${updatedByMember && " " + $l("by {0}", updatedByMember.email)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</pl-scroller>
|
|
|
|
<div
|
|
class="animated padded spacing evenly stretching horizontal layout save-cancel"
|
|
?hidden=${!this._editing}
|
|
>
|
|
<pl-button class="primary spacing horizontal layout" @click=${this.save}>
|
|
<pl-icon icon="check"></pl-icon>
|
|
<div>${$l("Save")}</div>
|
|
</pl-button>
|
|
|
|
<pl-button class="spacing horizontal layout" @click=${this.cancelEditing}>
|
|
<pl-icon icon="cancel"></pl-icon>
|
|
<div>${$l("Cancel")}</div>
|
|
</pl-button>
|
|
</div>
|
|
|
|
<input type="file" hidden @change=${this._attachFile} />
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
async edit(action?: string, actionIndex?: number) {
|
|
this.go(`items/${this.itemId}/edit`, { action, actionIndex: actionIndex?.toString() }, undefined, true);
|
|
}
|
|
|
|
async cancelEditing() {
|
|
this.clearChanges();
|
|
|
|
if (this.isNew) {
|
|
this.go("items", undefined, undefined, true);
|
|
} else {
|
|
this.go(`items/${this.itemId}`, undefined, undefined, true);
|
|
}
|
|
}
|
|
|
|
async clearChanges() {
|
|
if (this.isNew) {
|
|
app.deleteItems([this._item!]);
|
|
} else {
|
|
this._itemChanged();
|
|
}
|
|
}
|
|
|
|
save() {
|
|
if (!this._nameInput.reportValidity()) {
|
|
return;
|
|
}
|
|
app.updateItem(this._item!, {
|
|
name: this._nameInput.value,
|
|
fields: [...this._fieldInputs].map((fieldEl: FieldElement) => fieldEl.field),
|
|
tags: this._tagsInput.tags,
|
|
auditResults: [],
|
|
lastAudited: undefined,
|
|
});
|
|
auditVaults([this._vault!], { updateOnlyItemWithId: this._item!.id });
|
|
this.go(`items/${this.itemId}`, undefined, undefined, true);
|
|
}
|
|
|
|
private async _itemChanged() {
|
|
if (!this._nameInput) {
|
|
await this.updateComplete;
|
|
}
|
|
if (this._item) {
|
|
this._nameInput.value = this._item.name;
|
|
this._fields = this._item.fields.map((f) => new Field({ ...f }));
|
|
this._tagsInput.tags = [...this._item.tags];
|
|
} else {
|
|
this._nameInput && (this._nameInput.value = "");
|
|
this._fields = [];
|
|
this._tagsInput && (this._tagsInput.tags = []);
|
|
}
|
|
}
|
|
|
|
private async _removeField(index: number) {
|
|
if (
|
|
await confirm($l("Are you sure you want to remove this field?"), $l("Remove"), $l("Cancel"), {
|
|
title: $l("Remove Field"),
|
|
type: "destructive",
|
|
})
|
|
) {
|
|
this._fields = this._fields.filter((_, i) => i !== index);
|
|
}
|
|
}
|
|
|
|
private async _deleteItem() {
|
|
const confirmed = await confirm($l("Are you sure you want to delete this item?"), $l("Delete"), $l("Cancel"), {
|
|
title: $l("Delete Vault Item"),
|
|
type: "destructive",
|
|
});
|
|
if (confirmed) {
|
|
app.deleteItems([this._item!]);
|
|
this._editing = false;
|
|
router.go("items");
|
|
}
|
|
}
|
|
|
|
private async _addField(fieldDef: FieldDef) {
|
|
if (fieldDef.type === FieldType.Totp) {
|
|
if (this._checkTotpDisabled()) {
|
|
return;
|
|
}
|
|
} else if (fieldDef.type === FieldType.Note) {
|
|
if (this._checkNotesDisabled()) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
this._fields.push(new Field({ name: fieldDef.name, value: "", type: fieldDef.type }));
|
|
this.requestUpdate();
|
|
await this.updateComplete;
|
|
setTimeout(() => this._fieldInputs[this._fields.length - 1].focus(), 100);
|
|
}
|
|
|
|
private async _move() {
|
|
if (!app.hasWritePermissions(this._vault!)) {
|
|
return;
|
|
}
|
|
if (this._item!.attachments.length) {
|
|
await alert($l("Items with attachments cannot be moved!"), { type: "warning" });
|
|
} else {
|
|
const movedItems = await this._moveItemsDialog.show([{ item: this._item!, vault: this._vault! }]);
|
|
if (movedItems && movedItems.length) {
|
|
this.go(`items/${movedItems[0].id}`, undefined, true, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async _generateValue(index: number) {
|
|
const value = await this._generatorDialog.show();
|
|
if (value) {
|
|
this._fields[index] = new Field({ ...this._fields[index], value });
|
|
this.requestUpdate();
|
|
}
|
|
}
|
|
|
|
private async _addFileAttachment(file: File) {
|
|
if (this._checkAttachmentsDisabled()) {
|
|
return;
|
|
}
|
|
|
|
if (!file) {
|
|
return;
|
|
}
|
|
|
|
if (file.size > 5e6) {
|
|
alert($l("The selected file is too large! Only files of up to 5 MB are supported."), {
|
|
type: "warning",
|
|
});
|
|
return;
|
|
}
|
|
|
|
const att = await this._uploadDialog.show({ item: this.itemId, file });
|
|
if (att) {
|
|
this.requestUpdate();
|
|
await alert($l("File uploaded successfully!"), { type: "success", title: "Upload Complete" });
|
|
}
|
|
}
|
|
|
|
private async _attachFile() {
|
|
const file = this._fileInput.files![0];
|
|
this._fileInput.value = "";
|
|
this._addFileAttachment(file);
|
|
}
|
|
|
|
private async _openAttachment(info: AttachmentInfo) {
|
|
if (this._editing) {
|
|
return;
|
|
}
|
|
await this._attachmentDialog.show({ item: this.itemId, info });
|
|
}
|
|
|
|
private async _getTotpQR(index: number): Promise<void> {
|
|
const data = await this._qrDialog.show();
|
|
if (data) {
|
|
try {
|
|
const { secret } = parseURL(data);
|
|
this._fields[index] = new Field({ ...this._fields[index], value: secret });
|
|
this.requestUpdate();
|
|
} catch (e) {
|
|
await alert("Invalid Code! Please try again!", { type: "warning" });
|
|
return this._getTotpQR(index);
|
|
}
|
|
}
|
|
}
|
|
|
|
private _setFavorite(favorite: boolean) {
|
|
app.toggleFavorite(this.itemId, favorite);
|
|
this.requestUpdate();
|
|
}
|
|
|
|
private async _deleteAttachment(a: AttachmentInfo) {
|
|
const confirmed = await confirm(
|
|
$l("Are you sure you want to delete this attachment?"),
|
|
$l("Delete"),
|
|
$l("Cancel"),
|
|
{
|
|
title: $l("Delete Attachment"),
|
|
type: "destructive",
|
|
}
|
|
);
|
|
if (confirmed) {
|
|
await app.deleteAttachment(this.itemId!, a);
|
|
this.requestUpdate();
|
|
}
|
|
}
|
|
|
|
private async _handleDrop(event: DragEvent) {
|
|
event.preventDefault();
|
|
|
|
this._isDraggingFileToAttach = true;
|
|
|
|
if (event.dataTransfer?.items) {
|
|
for (const transferItem of event.dataTransfer.items) {
|
|
// Only handle files
|
|
if (transferItem.kind === "file") {
|
|
const transferFile = transferItem.getAsFile();
|
|
if (transferFile) {
|
|
await this._addFileAttachment(transferFile);
|
|
}
|
|
}
|
|
}
|
|
} else if (event.dataTransfer?.files) {
|
|
for (const transferFile of event.dataTransfer.files) {
|
|
await this._addFileAttachment(transferFile);
|
|
}
|
|
}
|
|
|
|
this._isDraggingFileToAttach = false;
|
|
}
|
|
|
|
private async _handleDragOver(event: DragEvent) {
|
|
event.preventDefault();
|
|
this._isDraggingFileToAttach = true;
|
|
}
|
|
|
|
private async _handleDragLeave(event: DragEvent) {
|
|
event.preventDefault();
|
|
this._isDraggingFileToAttach = false;
|
|
}
|
|
|
|
private _drop(e: DragEvent) {
|
|
// console.log("drop", e, this._draggingIndex, this._dragOverIndex);
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
return false;
|
|
}
|
|
|
|
private async _dragstart(event: DragEvent, index: number) {
|
|
// console.log("dragstart", event);
|
|
// this._draggingIndex = index;
|
|
this.dispatchEvent(
|
|
new CustomEvent("field-dragged", {
|
|
detail: { item: this._item, index, event },
|
|
bubbles: true,
|
|
composed: true,
|
|
})
|
|
);
|
|
(event.target as HTMLElement).classList.add("dragging");
|
|
this.classList.add("dragging");
|
|
}
|
|
|
|
// private _dragenter(e: DragEvent, index: number) {
|
|
// // console.log("dragenter", e);
|
|
// e.dataTransfer!.dropEffect = "move";
|
|
//
|
|
// this._dragOverIndex = index;
|
|
//
|
|
// for (const [i, field] of this._fieldInputs.entries()) {
|
|
// field.classList.toggle(
|
|
// "dragover",
|
|
// i === index && i !== this._draggingIndex && i !== this._draggingIndex - 1
|
|
// );
|
|
// }
|
|
// }
|
|
//
|
|
// private _dragover(e: DragEvent) {
|
|
// e.preventDefault();
|
|
// }
|
|
//
|
|
// private _dragend(_e: DragEvent) {
|
|
// // console.log("dragend", e, this._draggingIndex, this._dragOverIndex);
|
|
//
|
|
// if (this._draggingIndex !== -1 || this._dragOverIndex !== -1) {
|
|
// const field = this._fields[this._draggingIndex];
|
|
// this._fields.splice(this._draggingIndex, 1);
|
|
// const targetIndex =
|
|
// this._dragOverIndex >= this._draggingIndex ? this._dragOverIndex : this._dragOverIndex + 1;
|
|
// this._fields.splice(targetIndex, 0, field);
|
|
// this.requestUpdate();
|
|
// }
|
|
//
|
|
// for (const field of this._fieldInputs) {
|
|
// field.classList.remove("dragging");
|
|
// field.classList.remove("dragover");
|
|
// }
|
|
// this.classList.remove("dragging");
|
|
// this._dragOverIndex = -1;
|
|
// this._draggingIndex = -1;
|
|
// }
|
|
}
|