Some general UX improvements and bug fixes
This commit is contained in:
parent
ea05def083
commit
1635dc96c3
|
@ -156,6 +156,7 @@ export class App extends ServiceWorker(StateMixin(AutoSync(ErrorHandling(AutoLoc
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--app-backdrop-background);
|
||||
letter-spacing: var(--letter-spacing);
|
||||
--inset-top: max(calc(env(safe-area-inset-top, 0) - 0.5em), 0em);
|
||||
--inset-bottom: max(calc(env(safe-area-inset-bottom, 0) - 1em), 0em);
|
||||
}
|
||||
|
|
|
@ -106,7 +106,7 @@ export class CreateItemDialog extends Dialog<Vault, VaultItem> {
|
|||
|
||||
const params = { ...router.params } as any;
|
||||
if (this._template.attachment) {
|
||||
params.addattachment = "true";
|
||||
params.action = "addAttachment";
|
||||
}
|
||||
router.go(`items/${item.id}/new`, params);
|
||||
}
|
||||
|
@ -139,7 +139,7 @@ export class CreateItemDialog extends Dialog<Vault, VaultItem> {
|
|||
{
|
||||
name: $l("URL"),
|
||||
type: FieldType.Url,
|
||||
value: parsedUrl.origin + parsedUrl.pathname,
|
||||
value: parsedUrl.origin,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -156,8 +156,9 @@ export class FieldElement extends LitElement {
|
|||
: [await randomString(16, charSets.alphanum), await generatePassphrase()];
|
||||
break;
|
||||
case FieldType.Url:
|
||||
this._suggestions =
|
||||
!this._valueInput?.value && app.state.context.browser?.url ? [app.state.context.browser.url] : [];
|
||||
const url = app.state.context.browser?.url;
|
||||
const origin = url && new URL(url).origin;
|
||||
this._suggestions = !this._valueInput?.value && url && origin ? [origin, url] : [];
|
||||
break;
|
||||
default:
|
||||
this._suggestions = null;
|
||||
|
|
|
@ -577,6 +577,10 @@ export class PlIcon extends LitElement {
|
|||
:host([icon="compromised"]) > div::before {
|
||||
content: "\\f21b";
|
||||
}
|
||||
|
||||
:host([icon="field"]) > div::before {
|
||||
content: "\\e211";
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
|
|
|
@ -19,6 +19,8 @@ export class ItemIcon extends LitElement {
|
|||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
border-radius: 0.25em;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
img {
|
||||
|
|
|
@ -32,6 +32,7 @@ import { customElement, property, query, queryAll, state } from "lit/decorators.
|
|||
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)) {
|
||||
|
@ -40,6 +41,9 @@ export class ItemView extends Routing(StateMixin(LitElement)) {
|
|||
@property()
|
||||
itemId: VaultItemID = "";
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
readonly = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
isNew: boolean = false;
|
||||
|
||||
|
@ -80,6 +84,9 @@ export class ItemView extends Routing(StateMixin(LitElement)) {
|
|||
@query("pl-tags-input")
|
||||
private _tagsInput: TagsInput;
|
||||
|
||||
@query("#addFieldPopover")
|
||||
private _addFieldPopover: Popover;
|
||||
|
||||
@queryAll("pl-field")
|
||||
private _fieldInputs: FieldElement[];
|
||||
|
||||
|
@ -108,7 +115,10 @@ export class ItemView extends Routing(StateMixin(LitElement)) {
|
|||
//
|
||||
// private _dragOverIndex = -1;
|
||||
|
||||
async handleRoute([id, mode]: [string, string], { addattachment }: { [prop: string]: string }) {
|
||||
async handleRoute(
|
||||
[id, mode]: [string, string],
|
||||
{ action, actionIndex, ...routerParams }: { [prop: string]: string }
|
||||
) {
|
||||
this.itemId = id;
|
||||
|
||||
if (this.itemId && !this._item) {
|
||||
|
@ -124,24 +134,32 @@ export class ItemView extends Routing(StateMixin(LitElement)) {
|
|||
}
|
||||
this._editing = true;
|
||||
setTimeout(() => {
|
||||
if (!this.shadowRoot?.activeElement) {
|
||||
this._nameInput?.focus();
|
||||
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();
|
||||
}
|
||||
}, 300);
|
||||
}, 150);
|
||||
} else {
|
||||
this._editing = false;
|
||||
}
|
||||
|
||||
this.router.params = routerParams;
|
||||
|
||||
await this.updateComplete;
|
||||
this._itemChanged();
|
||||
|
||||
if (addattachment === "true") {
|
||||
this.addAttachment();
|
||||
const { ...params } = router.params;
|
||||
delete params.addattachment;
|
||||
router.params = params;
|
||||
}
|
||||
|
||||
await this.updateComplete;
|
||||
|
||||
this._animateIn();
|
||||
|
@ -187,12 +205,6 @@ export class ItemView extends Routing(StateMixin(LitElement)) {
|
|||
setClipboard(await field.transform(), `${item.name} / ${field.name}`);
|
||||
}
|
||||
|
||||
private async _editField(index: number) {
|
||||
this.edit();
|
||||
await this.updateComplete;
|
||||
setTimeout(() => this._fieldInputs[index]?.focus(), 100);
|
||||
}
|
||||
|
||||
static styles = [
|
||||
shared,
|
||||
css`
|
||||
|
@ -205,12 +217,10 @@ export class ItemView extends Routing(StateMixin(LitElement)) {
|
|||
header {
|
||||
overflow: visible;
|
||||
z-index: 10;
|
||||
--input-padding: 0.3em 0.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
margin-right: -0.5em;
|
||||
margin-right: -0.1em;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
|
@ -244,7 +254,7 @@ export class ItemView extends Routing(StateMixin(LitElement)) {
|
|||
|
||||
.fields,
|
||||
.attachments {
|
||||
margin: 0.5em 0;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.field-selector {
|
||||
|
@ -252,9 +262,9 @@ export class ItemView extends Routing(StateMixin(LitElement)) {
|
|||
overflow: auto;
|
||||
}
|
||||
|
||||
.tags-input {
|
||||
margin-left: 3.1em;
|
||||
margin-bottom: 0.5em;
|
||||
.tags-label {
|
||||
text-transform: uppercase;
|
||||
padding: 0.6em 0.6em 0.6em 1.2em;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
|
@ -269,7 +279,7 @@ export class ItemView extends Routing(StateMixin(LitElement)) {
|
|||
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 thin"></pl-icon>
|
||||
<pl-icon icon="note" class="enormous regular"></pl-icon>
|
||||
|
||||
<div>${$l("No item selected.")}</div>
|
||||
</div>
|
||||
|
@ -290,80 +300,88 @@ export class ItemView extends Routing(StateMixin(LitElement)) {
|
|||
@dragover=${this._handleDragOver}
|
||||
@dragleave=${this._handleDragLeave}
|
||||
>
|
||||
<header class="animated padded center-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
|
||||
>
|
||||
<pl-item-icon
|
||||
.item=${this._item}
|
||||
slot="before"
|
||||
style="margin-left: 0.3em"
|
||||
class="wide-only"
|
||||
></pl-item-icon>
|
||||
</pl-input>
|
||||
|
||||
<div class="horizontal layout" ?hidden=${this._editing}>
|
||||
<header class="padded animated">
|
||||
<div class="start-aligning horizontal layout">
|
||||
<pl-button
|
||||
@click=${() => this._setFavorite(!isFavorite)}
|
||||
class="slim transparent favorite-button"
|
||||
.label=${$l("Favorite")}
|
||||
.toggled=${isFavorite}
|
||||
class="transparent slim back-button"
|
||||
@click=${() => router.go("items")}
|
||||
?hidden=${this._editing}
|
||||
>
|
||||
<pl-icon icon="favorite"></pl-icon>
|
||||
<pl-icon icon="backward"></pl-icon>
|
||||
</pl-button>
|
||||
|
||||
<pl-button
|
||||
class="slim transparent"
|
||||
@click=${() => this.edit()}
|
||||
?disabled=${!this._isEditable}
|
||||
.label=${$l("Edit")}
|
||||
<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;"
|
||||
>
|
||||
<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">
|
||||
<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 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-popover>
|
||||
<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>
|
||||
|
@ -388,19 +406,52 @@ export class ItemView extends Routing(StateMixin(LitElement)) {
|
|||
</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">
|
||||
<pl-tags-input
|
||||
.editing=${this._editing}
|
||||
.vault=${this._vault}
|
||||
@move=${this._move}
|
||||
class="animated small horizontally-double-margined horizontally-padded tags-input"
|
||||
></pl-tags-input>
|
||||
<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="fields border-top border-bottom">
|
||||
<pl-list>
|
||||
<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}`,
|
||||
|
@ -422,19 +473,29 @@ export class ItemView extends Routing(StateMixin(LitElement)) {
|
|||
@drop=${(e: DragEvent) => this._drop(e)}
|
||||
@moveup=${() => this._moveField(index, "up")}
|
||||
@movedown=${() => this._moveField(index, "down")}
|
||||
@edit=${() => this._editField(index)}
|
||||
@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="horizontally-double-margined bottom-margined animated section-header"
|
||||
style="margin-left: 2.2em;"
|
||||
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>
|
||||
|
||||
|
@ -458,7 +519,7 @@ export class ItemView extends Routing(StateMixin(LitElement)) {
|
|||
@click=${() => this.addAttachment()}
|
||||
>
|
||||
<span class="small ${this._isDraggingFileToAttach ? "highlighted bold" : "subtle"}">
|
||||
<pl-icon class="inline" icon="attachment"></pl-icon> ${$l(
|
||||
<pl-icon class="inline" icon="add"></pl-icon> ${$l(
|
||||
"Click or drag files here to add an attachment!"
|
||||
)}
|
||||
</span>
|
||||
|
@ -497,8 +558,8 @@ export class ItemView extends Routing(StateMixin(LitElement)) {
|
|||
`;
|
||||
}
|
||||
|
||||
async edit() {
|
||||
this.go(`items/${this.itemId}/edit`);
|
||||
async edit(action?: string, actionIndex?: number) {
|
||||
this.go(`items/${this.itemId}/edit`, { action, actionIndex: actionIndex?.toString() }, undefined, true);
|
||||
}
|
||||
|
||||
async cancelEditing() {
|
||||
|
|
|
@ -151,6 +151,10 @@ export class VaultItemListItem extends LitElement {
|
|||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.item-header {
|
||||
margin: 0.25em 0 0 0.5em;
|
||||
}
|
||||
|
||||
.item-fields {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
@ -160,8 +164,8 @@ export class VaultItemListItem extends LitElement {
|
|||
/* scroll-padding: 1em; */
|
||||
scroll-behavior: smooth;
|
||||
pointer-events: auto;
|
||||
padding: 0 0.5em 0 2.7em;
|
||||
margin: 0.5em -0.5em 0 -0.5em;
|
||||
padding: 0 0.5em 0 3em;
|
||||
margin: 0.25em -0.5em 0 -0.5em;
|
||||
}
|
||||
|
||||
.item-field {
|
||||
|
@ -279,10 +283,7 @@ export class VaultItemListItem extends LitElement {
|
|||
const { item, vault, warning } = this;
|
||||
const tags = [];
|
||||
|
||||
let name = truncate(vault.name, 15);
|
||||
if (vault.org) {
|
||||
name = `${truncate(vault.org.name, 15)} / ${name}`;
|
||||
}
|
||||
// const name = vault.getLabel();
|
||||
|
||||
if (warning) {
|
||||
tags.push({ icon: "error", class: "warning", name: "" });
|
||||
|
@ -291,9 +292,17 @@ export class VaultItemListItem extends LitElement {
|
|||
if (item.tags.length) {
|
||||
tags.push({
|
||||
icon: "tag",
|
||||
name: item.tags.length.toString(),
|
||||
name: item.tags[0],
|
||||
class: "",
|
||||
});
|
||||
|
||||
if (item.tags.length > 1) {
|
||||
tags.push({
|
||||
icon: "tags",
|
||||
name: `+${item.tags.length - 1}`,
|
||||
class: "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const attCount = (item.attachments && item.attachments.length) || 0;
|
||||
|
@ -313,30 +322,31 @@ export class VaultItemListItem extends LitElement {
|
|||
});
|
||||
}
|
||||
|
||||
tags.push({ name, icon: "vault", class: "highlight" });
|
||||
// tags.push({ name, icon: "vault", class: "highlight" });
|
||||
|
||||
return html`
|
||||
<div class="margined center-aligning horizontal layout">
|
||||
<div class="stretch collapse spacing center-aligning horizontal layout">
|
||||
<div class="item-header spacing center-aligning horizontal layout">
|
||||
<div class="large">
|
||||
${this.selected === true
|
||||
? html` <pl-icon icon="checkbox-checked"></pl-icon> `
|
||||
: this.selected === false
|
||||
? html` <pl-icon icon="checkbox-unchecked" class="faded"></pl-icon> `
|
||||
: html` <pl-item-icon .item=${item}></pl-item-icon> `}
|
||||
|
||||
<div class="ellipsis semibold stretch collapse left-half-margined" ?disabled=${!item.name}>
|
||||
${item.name || $l("No Name")}
|
||||
</div>
|
||||
|
||||
${tags.map(
|
||||
(tag) => html`
|
||||
<div class="tiny tag ${tag.class} ellipsis">
|
||||
${tag.icon ? html`<pl-icon icon="${tag.icon}" class="inline"></pl-icon>` : ""}
|
||||
${tag.name ? html`${tag.name}` : ""}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class="stretch collapse left-half-margined">
|
||||
<div class="tiny subtle">${vault.label}</div>
|
||||
<div class="ellipsis semibold" ?disabled=${!item.name}>${item.name || $l("No Name")}</div>
|
||||
</div>
|
||||
|
||||
${tags.map(
|
||||
(tag) => html`
|
||||
<div class="tiny tag ${tag.class} ellipsis" style="align-self: start">
|
||||
${tag.icon ? html`<pl-icon icon="${tag.icon}" class="inline"></pl-icon>` : ""}
|
||||
${tag.name ? html`${tag.name}` : ""}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
|
|
|
@ -17,6 +17,7 @@ import { css, html, LitElement } from "lit";
|
|||
import { formatDateFromNow } from "../lib/util";
|
||||
import { until } from "lit/directives/until.js";
|
||||
import { ProvisioningStatus } from "@padloc/core/src/provisioning";
|
||||
import "./icon";
|
||||
|
||||
const orgPages = [
|
||||
{ path: "dashboard", label: $l("Dashboard"), icon: "dashboard" },
|
||||
|
|
|
@ -14,6 +14,8 @@ import "./list";
|
|||
import "./org-nav";
|
||||
import { customElement, property, query, state } from "lit/decorators.js";
|
||||
import { html, LitElement } from "lit";
|
||||
import "./scroller";
|
||||
import "./button";
|
||||
|
||||
@customElement("pl-org-members")
|
||||
export class OrgMembersView extends Routing(StateMixin(LitElement)) {
|
||||
|
|
|
@ -31,6 +31,7 @@ import { KeyStoreEntryInfo } from "@padloc/core/src/key-store";
|
|||
import { Toggle } from "./toggle";
|
||||
import { alertDisabledFeature } from "../lib/provisioning";
|
||||
import { auditVaults } from "../lib/audit";
|
||||
import "./icon";
|
||||
|
||||
@customElement("pl-settings-security")
|
||||
export class SettingsSecurity extends StateMixin(Routing(LitElement)) {
|
||||
|
@ -633,7 +634,7 @@ export class SettingsSecurity extends StateMixin(Routing(LitElement)) {
|
|||
return;
|
||||
}
|
||||
return html`
|
||||
<div class="padded list-item box center-aligning horizontal layout">
|
||||
<div class="padded list-item center-aligning horizontal layout">
|
||||
<pl-icon
|
||||
icon="${["ios", "android"].includes(device?.platform.toLowerCase() || "")
|
||||
? "mobile"
|
||||
|
|
|
@ -1,147 +1,185 @@
|
|||
import { translate as $l } from "@padloc/locale/src/translate";
|
||||
import { Vault } from "@padloc/core/src/vault";
|
||||
import { Tag } from "@padloc/core/src/item";
|
||||
import { app } from "../globals";
|
||||
import { $l } from "@padloc/locale/src/translate";
|
||||
import { css, customElement, html, LitElement, property, query } from "lit-element";
|
||||
import { app, router } from "../globals";
|
||||
import { shared } from "../styles";
|
||||
import { Input } from "./input";
|
||||
import "./icon";
|
||||
import { customElement, property, query, state } from "lit/decorators.js";
|
||||
import { css, html, LitElement } from "lit";
|
||||
|
||||
@customElement("pl-tags-input")
|
||||
export class TagsInput extends LitElement {
|
||||
@property({ type: Boolean })
|
||||
editing: boolean = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
vault: Vault | null = null;
|
||||
|
||||
@property({ attribute: false })
|
||||
tags: Tag[] = [];
|
||||
|
||||
@state()
|
||||
_showResults: Boolean = false;
|
||||
@property({ type: Boolean, reflect: true })
|
||||
readonly = false;
|
||||
|
||||
@query("pl-input")
|
||||
private _input: Input;
|
||||
focus() {
|
||||
this._input.focus();
|
||||
}
|
||||
|
||||
private _focusTimeout: number = 0;
|
||||
@query("input")
|
||||
private _input: HTMLInputElement;
|
||||
|
||||
@query(".results")
|
||||
private _results: HTMLDivElement;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("focus", () => this._focus());
|
||||
this.addEventListener("blur", () => this._blur());
|
||||
}
|
||||
|
||||
private _keydown(e: KeyboardEvent) {
|
||||
if (e.key === "Enter") {
|
||||
this._addTag(this._input.value);
|
||||
}
|
||||
}
|
||||
|
||||
private async _addTag(tag: Tag) {
|
||||
if (!tag || this.tags.some((t) => t === tag)) {
|
||||
return;
|
||||
}
|
||||
this.tags.push(tag);
|
||||
this._input.value = "";
|
||||
this.requestUpdate();
|
||||
await this.updateComplete;
|
||||
this._input.focus();
|
||||
}
|
||||
|
||||
private _tagClicked(tag: Tag) {
|
||||
if (this.readonly) {
|
||||
router.go("items", { tag });
|
||||
} else {
|
||||
this.tags = this.tags.filter((t) => t !== tag);
|
||||
}
|
||||
}
|
||||
|
||||
private _hideResultsTimeout: number;
|
||||
|
||||
private _focus() {
|
||||
window.clearTimeout(this._hideResultsTimeout);
|
||||
this._results.style.display = "";
|
||||
this.classList.add("focused");
|
||||
}
|
||||
|
||||
private _blur() {
|
||||
this.classList.remove("focused");
|
||||
this._hideResultsTimeout = window.setTimeout(() => (this._results.style.display = "none"), 150);
|
||||
}
|
||||
|
||||
static styles = [
|
||||
shared,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.3em;
|
||||
flex-wrap: wrap;
|
||||
border: solid 1px var(--input-border-color);
|
||||
border-radius: 0.5em;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
overflow: visible;
|
||||
padding: 0.3em;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 0.15em;
|
||||
border: none;
|
||||
background: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:host([readonly]) {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
:host(.focused) {
|
||||
border-color: var(--input-focused-border-color);
|
||||
}
|
||||
|
||||
:host(:not(.focused)) i.plus {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.results {
|
||||
padding: 0;
|
||||
border-radius: 0.5em;
|
||||
margin-top: 0;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.results .tag {
|
||||
margin-top: 0.5em;
|
||||
position: absolute;
|
||||
background: var(--color-background);
|
||||
box-shadow: rgba(0, 0, 0, 0.1) 0 0.3em 1em -0.2em, var(--border-color) 0 0 0 1px;
|
||||
width: 100%;
|
||||
border-radius: 0.5em;
|
||||
margin-top: 0.2em;
|
||||
box-sizing: border-box;
|
||||
transition: pointer-events 0.5s;
|
||||
max-width: 15em;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.add-tag {
|
||||
overflow: visible;
|
||||
width: 10em;
|
||||
font-family: var(--tag-font-family);
|
||||
--add-tag-height: calc(2 * var(--tag-padding) + 1em);
|
||||
height: calc(var(--add-tag-height) + 1px);
|
||||
position: relative;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.add-tag pl-input {
|
||||
--input-padding: 0 var(--tag-padding);
|
||||
height: var(--add-tag-height);
|
||||
line-height: var(--add-tag-height);
|
||||
:host([readonly]) .remove-icon,
|
||||
.tag:not(:hover) > .remove-icon {
|
||||
display: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
render() {
|
||||
const { tags, editing, vault, _showResults } = this;
|
||||
const { value } = this._input || { value: "" };
|
||||
const results = app.state.tags
|
||||
.filter(([t]) => !tags.includes(t) && t !== value && t.toLowerCase().startsWith(value))
|
||||
.map(([t]) => t);
|
||||
const existingTags = app.state.tags;
|
||||
const value = this._input?.value || "";
|
||||
const results = existingTags.filter(
|
||||
([t]) => !this.tags.includes(t) && t !== value && t.toLowerCase().startsWith(value.toLocaleLowerCase())
|
||||
);
|
||||
if (value) {
|
||||
results.push(value);
|
||||
results.push([value, 0]);
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="wrapping tags">
|
||||
<div class="tag highlight tap" @click=${() => this._vaultClicked()}>
|
||||
<pl-icon icon="vault" class="inline"></pl-icon>
|
||||
${vault}
|
||||
</div>
|
||||
|
||||
${tags.map(
|
||||
(tag) => html`
|
||||
<div class="tap tag" @click=${() => this._tagClicked(tag)}>
|
||||
<pl-icon icon="tag" class="inline"></pl-icon>
|
||||
|
||||
${tag} ${editing ? html` <pl-icon icon="cancel" class="inline"></pl-icon> ` : ""}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
|
||||
<div class="add-tag" ?hidden=${!editing}>
|
||||
<pl-input
|
||||
class="dashed"
|
||||
.placeholder=${$l("Add Tag")}
|
||||
@enter=${() => this._addTag(value)}
|
||||
@input=${() => this.requestUpdate()}
|
||||
@focus=${() => this._focusChanged()}
|
||||
@blur=${() => this._focusChanged()}
|
||||
>
|
||||
<pl-icon icon="add" slot="before" class="left-margined"></pl-icon>
|
||||
</pl-input>
|
||||
|
||||
<div class="tags results" ?hidden=${!_showResults}>
|
||||
${results.map(
|
||||
(res) => html`
|
||||
<div class="tag click" @click=${() => this._addTag(res)}>
|
||||
<pl-icon icon="tag" class="inline"></pl-icon>
|
||||
${res}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
${this.tags.map(
|
||||
(tag) => html`
|
||||
<div class="small tag click hover" @click=${() => this._tagClicked(tag)}>
|
||||
<pl-icon icon="tag" class="inline"></pl-icon>
|
||||
${tag}
|
||||
<pl-icon icon="cancel" class="inline small remove-icon"></pl-icon>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
|
||||
<div class="smaller add-tag">
|
||||
<div class="center-aligning horizontal layout">
|
||||
<pl-icon class="subtle" icon="add"></pl-icon>
|
||||
|
||||
<input
|
||||
class="stretch"
|
||||
placeholder="Add Tag..."
|
||||
@keydown=${this._keydown}
|
||||
@input=${() => this.requestUpdate()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<pl-list class="results" style="display: none">
|
||||
${results.length
|
||||
? results.map(
|
||||
([tag, count]) => html`
|
||||
<div
|
||||
class="padded half-spacing center-aligning horizontal layout list-item click hover"
|
||||
@click=${() => this._addTag(tag)}
|
||||
>
|
||||
<pl-icon icon="tag"></pl-icon>
|
||||
<div class="stretch">${tag}</div>
|
||||
<div class="small subtle right-margined">
|
||||
${count || html`<pl-icon class="inline" icon="add"></pl-icon>`}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
: html` <div class="smaller padded subtle">${$l("Type tag name...")}</div> `}
|
||||
</pl-list>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _addTag(tag: Tag) {
|
||||
if (!tag || this.tags.includes(tag)) {
|
||||
return;
|
||||
}
|
||||
this.tags.push(tag);
|
||||
this._input.value = "";
|
||||
this._showResults = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _tagClicked(tag: Tag) {
|
||||
if (this.editing) {
|
||||
this.tags = this.tags.filter((t) => t !== tag);
|
||||
}
|
||||
}
|
||||
|
||||
private _vaultClicked() {
|
||||
this.dispatchEvent(new CustomEvent("move"));
|
||||
}
|
||||
|
||||
_focusChanged() {
|
||||
clearTimeout(this._focusTimeout);
|
||||
setTimeout(() => (this._showResults = this._input.focused), this._input.focused ? 0 : 200);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -384,9 +384,5 @@ export const layout = css`
|
|||
.layout.pane:not(.open) > :last-child {
|
||||
transform: translateX(calc(100% + 6px));
|
||||
}
|
||||
|
||||
.wide-only {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -51,7 +51,7 @@ export const misc = css`
|
|||
font-family: var(--tag-font-family, var(--font-family));
|
||||
}
|
||||
|
||||
.tag:not(:last-child) {
|
||||
.tags .tag:not(:last-child) {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
|
@ -163,7 +163,7 @@ export const misc = css`
|
|||
background: var(--list-item-selected-background);
|
||||
color: var(--list-item-selected-color);
|
||||
--color-highlight: var(--list-item-selected-color-highlight);
|
||||
/* box-shadow: inset 0.2em 0 0 0 var(--color-highlight); */
|
||||
box-shadow: inset 0.2em 0 0 0 var(--color-highlight);
|
||||
}
|
||||
|
||||
.list-item:focus:not([aria-selected="true"]) {
|
||||
|
|
|
@ -92,6 +92,7 @@ export const reset = css`
|
|||
font-size: 100%;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
letter-spacing: inherit;
|
||||
vertical-align: baseline;
|
||||
background: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
|
|
@ -7,9 +7,9 @@ export const responsive = css`
|
|||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
@media (max-width: 700px) {
|
||||
.wide-only {
|
||||
display: none;
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -226,7 +226,7 @@ export class AppState extends Storable {
|
|||
}
|
||||
}
|
||||
|
||||
return [...tags.entries()];
|
||||
return [...tags.entries()].sort(([, countA], [, countB]) => countB - countA);
|
||||
}
|
||||
|
||||
/** Whether the app is in "locked" state */
|
||||
|
@ -1065,7 +1065,7 @@ export class App {
|
|||
// remove previous vault entry
|
||||
member.vaults = member.vaults.filter((v) => v.id !== id);
|
||||
// update vault entry
|
||||
const selection = members.find((m) => m.id === member.id);
|
||||
const selection = members.find((m) => m.email === member.email);
|
||||
if (selection) {
|
||||
member.vaults.push({ id, readonly: selection.readonly });
|
||||
}
|
||||
|
|
|
@ -113,7 +113,6 @@ export function upgrade(kind: string, raw: any, version: string = LATEST_VERSION
|
|||
);
|
||||
}
|
||||
|
||||
// Find nearest revision
|
||||
const targetVersion = [...VERSIONS].reverse().find((v) => norm(v) <= norm(version)) || EARLIEST_VERSION;
|
||||
const closestVersion = VERSIONS.find((v) => norm(v) > norm(raw.version)) || LATEST_VERSION;
|
||||
const migrateToVersion = norm(closestVersion) < norm(targetVersion) ? closestVersion : targetVersion;
|
||||
|
|
|
@ -487,6 +487,7 @@ export class Org extends SharedContainer implements Storable {
|
|||
orgSignature,
|
||||
role: OrgRole.Owner,
|
||||
updated: new Date(),
|
||||
status: OrgMemberStatus.Active,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -55,6 +55,13 @@ export class Vault extends SharedContainer implements Storable {
|
|||
@Exclude()
|
||||
error?: Err;
|
||||
|
||||
/**
|
||||
* Convenience getter for getting a display label truncated to a certain maximum length
|
||||
*/
|
||||
get label() {
|
||||
return this.org ? `${this.org.name} / ${this.name}` : this.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlocks the vault with the given `account`, decrypting the data stored in the vault
|
||||
* and populating the [[items]] property. For this to be successful, the `account` object
|
||||
|
|
Loading…
Reference in New Issue