Some general UX improvements and bug fixes

This commit is contained in:
Martin Kleinschrodt 2022-06-18 09:42:25 +02:00
parent ea05def083
commit 1635dc96c3
19 changed files with 375 additions and 250 deletions

View File

@ -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);
}

View File

@ -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,
},
],
};

View File

@ -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;

View File

@ -577,6 +577,10 @@ export class PlIcon extends LitElement {
:host([icon="compromised"]) > div::before {
content: "\\f21b";
}
:host([icon="field"]) > div::before {
content: "\\e211";
}
`,
];

View File

@ -19,6 +19,8 @@ export class ItemIcon extends LitElement {
css`
:host {
display: flex;
border-radius: 0.25em;
overflow: hidden;
}
img {

View File

@ -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() {

View File

@ -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">

View File

@ -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" },

View File

@ -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)) {

View File

@ -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"

View File

@ -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);
}
}

View File

@ -384,9 +384,5 @@ export const layout = css`
.layout.pane:not(.open) > :last-child {
transform: translateX(calc(100% + 6px));
}
.wide-only {
display: none;
}
}
`;

View File

@ -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"]) {

View File

@ -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;

View File

@ -7,9 +7,9 @@ export const responsive = css`
}
}
@media (max-width: 1000px) {
@media (max-width: 700px) {
.wide-only {
display: none;
display: none !important;
}
}
`;

View File

@ -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 });
}

View File

@ -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;

View File

@ -487,6 +487,7 @@ export class Org extends SharedContainer implements Storable {
orgSignature,
role: OrgRole.Owner,
updated: new Date(),
status: OrgMemberStatus.Active,
})
);
}

View File

@ -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