Add entries, search, expand/collapse ++ to landing page

This commit is contained in:
Daniel Nyvik 2024-04-04 13:55:43 +02:00
parent deb35e9c3a
commit 7fee323f68
7 changed files with 252 additions and 22 deletions

View File

@ -148,6 +148,8 @@
"keyEsc": "Esc",
"iconFavTitle": "Download and use website favicon",
"iconSelCustom": "Select a custom icon",
"iconMissingIndexHeader": "Missing search index for content",
"iconMissingIndexBody": "Open password vault, go to settings and click on 'Update search index' or simply edit some data and save.",
"listEmptyTitle": "Empty",
"listEmptyAdd": "add with {} button above",
"listGroup": "Group",
@ -170,6 +172,7 @@
"searchAdvTitle": "Toggle advanced search",
"searchSearchIn": "Search in",
"searchOther": "Other fields",
"searchPlaceholder": "Search in entries",
"searchProtect": "Secure fields",
"searchOptions": "Options",
"searchCase": "Match case",
@ -801,4 +804,5 @@
"csvExtraFieldsHelperText": "It is possible to import extra fields by adding them to the csv.",
"updateSearchIndex": "Opprett search index",
"passwordBankTitle": "Passwordbank",
"openFileLoadingText": "Loading password vault"
}

View File

@ -151,6 +151,8 @@
"keyEnter": "Enter",
"iconFavTitle": "Last ned og bruk nettstedsikon",
"iconSelCustom": "Velg eget ikon",
"iconMissingIndexHeader": "Mangler søkeindeks for innhold",
"iconMissingIndexBody": "Åpne passordhvelvet, gå til innstillinger og trykk på 'Oppdater søkeindeks' eller gjør en vilkårlig endring og lagre.",
"listEmptyTitle": "Tom",
"listEmptyAdd": "legg til med {}-knappen over",
"listGroup": "Gruppe",
@ -173,6 +175,7 @@
"searchAdvTitle": "Bytt til avansert søk",
"searchSearchIn": "Søk i",
"searchOther": "Andre felt",
"searchPlaceholder": "Søk i oppføringer",
"searchProtect": "Sikre felt",
"searchOptions": "Innstillinger",
"searchCase": "Skill små/store",
@ -619,5 +622,6 @@
"uploadCSV": "Last opp CSV",
"csvFormat": "Format",
"csvExtraFieldsHelperText": "Det er mulig å importere flere felter ved å legge de til i csv-en.",
"updateSearchIndex": "Oppdater søkeindex",
"updateSearchIndex": "Oppdater søkeindeks",
"openFileLoadingText": "Låser opp passordhvelv"
}

View File

@ -870,6 +870,11 @@ class AppModel {
}
fileOpened(file, data, params) {
if (params.entryId) {
// File id always changes.
this.activeEntryId = file.id + ':' + params.entryId.split(':')[1];
this.refresh();
}
if (file.storage === 'file') {
Storage.file.watch(
file.path,

View File

@ -25,6 +25,7 @@ import { GeneratorView } from 'views/generator-view';
import { CreateNewPasswordVaultView } from 'views/create-new-password-vault-view';
import { getPasswordForPasswordVault } from 'util/passwordbank';
import template from 'templates/open.hbs';
import throttle from 'lodash/throttle';
const logger = new Logger('open-view');
@ -51,9 +52,15 @@ class OpenView extends View {
'click .open__pass-enter-btn': 'openDb',
'click .open__settings-key-file': 'openKeyFile',
'click .open__settings-yubikey': 'selectYubiKeyChalResp',
'click .open__last-item': 'openLast',
'click .open__last-item-text': 'openLast',
'click .open__icon-generate': 'toggleGenerator',
'click .open__message-cancel-btn': 'openMessageCancelClick',
'input .list__search-field': 'searchInputChanged',
'click .list__search-icon-clear': 'clearSearchInput',
'click .open__last-item-expand': 'expandItem',
'click .open__last-item-collapse': 'collapseItem',
'click .open__last-item-entry-text': 'openEntry',
'click .icon-missing-index': 'showNotIndexedAlert',
dragover: 'dragover',
dragleave: 'dragleave',
drop: 'drop'
@ -64,6 +71,12 @@ class OpenView extends View {
busy = false;
currentSelectedIndex = -1;
encryptedPassword = null;
searchInput = null;
lastOpenFiles = null;
searchText = null;
loadingContainer = null;
openPassArea = null;
openIcons = null;
constructor(model) {
super(model);
@ -111,9 +124,9 @@ class OpenView extends View {
this.model.settings.yubiKeyShowIcon &&
!this.model.files.get('yubikey');
const canUseChalRespYubiKey = hasYubiKeys && this.model.settings.yubiKeyShowChalResp;
this.lastOpenFiles = this.getLastOpenFiles();
super.render({
lastOpenFiles: this.getLastOpenFiles(),
lastOpenFiles: this.lastOpenFiles,
canOpenKeyFromDropbox: !Launcher && Storage.dropbox.enabled,
demoOpened: this.model.settings.demoOpened,
storageProviders,
@ -130,6 +143,10 @@ class OpenView extends View {
});
this.inputEl = this.$el.find('.open__pass-input');
this.passwordInput.setElement(this.inputEl);
this.searchInput = this.$el.find('.list__search-field');
this.loadingContainer = this.$el.find('.loading__container');
this.openPassArea = this.$el.find('.open__pass-area');
this.openIcons = this.$el.find('.open__icons');
}
resetParams() {
@ -144,7 +161,8 @@ class OpenView extends View {
fileData: null,
rev: null,
opts: null,
chalResp: null
chalResp: null,
entryId: null
};
}
@ -161,24 +179,28 @@ class OpenView extends View {
getLastOpenFiles() {
return this.model.fileInfos.map((fileInfo) => {
let icon = 'file-alt';
const storage = Storage[fileInfo.storage];
if (storage && storage.icon) {
icon = storage.icon;
}
if (fileInfo.icon) {
icon = fileInfo.icon;
}
return {
id: fileInfo.id,
name: fileInfo.name,
path: this.getDisplayedPath(fileInfo),
icon,
tenantName: fileInfo.tenantName
icon: this.getFileIcon(fileInfo),
tenantName: fileInfo.tenantName,
entries: fileInfo.entries
};
});
}
getFileIcon(fileInfo) {
if (fileInfo.icon) {
return fileInfo.icon;
}
const storage = Storage[fileInfo.storage];
if (storage && storage.icon) {
return storage.icon;
}
return 'file-alt';
}
getDisplayedPath(fileInfo) {
const storage = fileInfo.storage;
if (storage === 'file' || storage === 'webdav') {
@ -463,9 +485,12 @@ class OpenView extends View {
this.removeFile(id);
return;
}
this.openFileById(id);
}
const fileInfo = this.model.fileInfos.get(id);
this.showOpenFileInfo(fileInfo, true);
openFileById(fileId, entryId) {
const fileInfo = this.model.fileInfos.get(fileId);
this.showOpenFileInfo(fileInfo, true, entryId);
}
removeFile(id) {
@ -598,7 +623,7 @@ class OpenView extends View {
}
}
showOpenFileInfo(fileInfo, fileWasClicked) {
showOpenFileInfo(fileInfo, fileWasClicked, entryId) {
if (this.busy || !fileInfo) {
return;
}
@ -616,6 +641,7 @@ class OpenView extends View {
this.params.tenantName = fileInfo.tenantName;
this.params.writeAccess = fileInfo.writeAccess;
this.params.deleteAccess = fileInfo.deleteAccess;
this.params.entryId = entryId;
if (fileWasClicked) {
this.openDb();
}
@ -667,6 +693,7 @@ class OpenView extends View {
this.views.createDb.render();
this.$el.find('.open__last').addClass('hide');
this.$el.find('.open__icons').addClass('hide');
this.$el.find('.list__search-field-wrap--text').addClass('hide');
// this.model.createNewFile();
}
@ -680,6 +707,7 @@ class OpenView extends View {
}
this.$el.find('.open__last').removeClass('hide');
this.$el.find('.open__icons').removeClass('hide');
this.$el.find('.list__search-field-wrap--text').removeClass('hide');
this.focusInput();
}
@ -696,15 +724,23 @@ class OpenView extends View {
this.busy = true;
// EncryptedPassword is used by kdbx to support iOS fingerprint unlock in the keeweb iOS app => Set this to null since we don't support iOS
this.params.encryptedPassword = null;
const password = await getPasswordForPasswordVault(this.params.path);
const password = await getPasswordForPasswordVault(this.params.path, this.params.entryId);
this.params.password = kdbxweb.ProtectedValue.fromString(password);
this.afterPaint(() => {
this.toggleLoadingIndicator(true);
this.model.openFile(this.params, (err) => this.openDbComplete(err));
});
}
toggleLoadingIndicator(isLoading) {
this.loadingContainer.toggleClass('hide', !isLoading);
this.openPassArea.toggleClass('hide', isLoading);
this.openIcons.toggleClass('hide', isLoading);
}
openDbComplete(err) {
this.busy = false;
this.toggleLoadingIndicator(false);
this.$el.toggleClass('open--opening', false);
const showInputError = err && !err.userCanceled;
this.inputEl.removeAttr('disabled').toggleClass('input--error', !!showInputError);
@ -1168,6 +1204,126 @@ class OpenView extends View {
openMessageCancelClick() {
this.model.rejectPendingFileUnlockPromise('User canceled');
}
clearSearchInput() {
this.searchInput.val('');
this.searchInputChanged();
}
searchInputChanged() {
this.searchText = this.searchInput.val();
const hasSearchText = this.searchText?.length > 0;
this.el.querySelector('.list__search-icon-clear').classList.toggle('hide', !hasSearchText);
if (hasSearchText) {
// Hide all expand/collapse icons.
this.el.querySelectorAll('.expand-collapse-container').forEach((element) => {
element.children.item(0).classList.toggle('hide', true);
element.children.item(1).classList.toggle('hide', true);
});
this.searchThrottler();
} else {
// Show all files.
this.el.querySelectorAll('.open__last-item-header').forEach((element) => {
element.classList.toggle('hide', false);
});
// Hide all entries.
this.el.querySelectorAll('.open__last-item-entry').forEach((element) => {
element.classList.toggle('hide', true);
});
// Reset expand/collapse icons.
this.el.querySelectorAll('.expand-collapse-container').forEach((element) => {
element.children.item(0).classList.toggle('hide', false);
element.children.item(1).classList.toggle('hide', true);
});
}
}
showNotIndexedAlert() {
Alerts.info({
header: Locale.iconMissingIndexHeader,
body: Locale.iconMissingIndexBody
});
}
openEntry(e) {
if (this.busy) {
return;
}
const parent = $(e.target).closest('.open__last-item');
const entryId = parent.data('id').toString();
const fileId = parent.data('parent-id').toString();
this.openFileById(fileId, entryId);
// Details is not shown by default on mobile.
if (entryId && Features.isMobile) {
Events.emit('toggle-details', true);
}
}
expandItem(e) {
$(e.target).toggleClass('hide', true);
const fileId = e.target.getAttribute('data-id');
this.$el
.find('.open__last-item-collapse[data-id=' + fileId + ']')
.toggleClass('hide', false);
this.toggleVisibilityForEntries(fileId, true);
}
collapseItem(e) {
$(e.target).toggleClass('hide', true);
const fileId = e.target.getAttribute('data-id');
this.$el.find('.open__last-item-expand[data-id=' + fileId + ']').toggleClass('hide', false);
this.toggleVisibilityForEntries(fileId, false);
}
toggleVisibilityForEntries(fileId, toggle) {
this.$el
.find('.open__last-item-entry[data-parent-id=' + fileId + ']')
.toggleClass('hide', !toggle);
}
toggleSearchResults(searchText) {
const filesIdsToShow = new Set();
const entryIdsToShow = new Set();
this.lastOpenFiles.forEach((file) => {
const entryIds = this.getEntryIdsToShow(file.entries, searchText);
if (entryIds?.length > 0) {
entryIds.forEach((id) => entryIdsToShow.add(id));
filesIdsToShow.add(file.id);
} else if (
file.name.includes(searchText) ||
file.tenantName.toLowerCase().includes(searchText)
) {
filesIdsToShow.add(file.id);
}
});
// Toggle files.
this.el.querySelectorAll('.open__last-item-header').forEach((element) => {
element.classList.toggle('hide', !filesIdsToShow.has(element.getAttribute('data-id')));
});
// Toggle entries.
this.el.querySelectorAll('.open__last-item-entry').forEach((element) => {
element.classList.toggle('hide', !entryIdsToShow.has(element.getAttribute('data-id')));
});
}
getEntryIdsToShow(entries, searchText) {
if (!entries) {
return null;
}
return entries
.filter((entry) => entry.searchText.includes(searchText))
.map((entry) => entry.id);
}
searchThrottler = throttle(() => {
// Use current value in case it has changed during throttling.
if (this.searchText?.length > 0) {
this.toggleSearchResults(this.searchText.toLowerCase());
}
}, 300);
}
export { OpenView };

View File

@ -23,6 +23,33 @@
}
}
.loading__container {
display: flex;
.loading__text {
margin-left: 0.5em;
}
}
.open__pass-area {
.open__last-item-entry-text {
flex-grow: 1;
}
min-width: 23em;
@media (max-width:25em) {
min-width: 10em;
}
}
.expand-collapse-container .fa {
padding-left: 0.5em;
padding-right: 0.5em;
}
.fa-info-circle {
padding-left: 0.5em;
padding-right: 0.5em;
}
&__icon {
@include icon-btn();
color: var(--open-icon-color);
@ -47,6 +74,10 @@
}
}
.icon-missing-index, .open__last-item-entry {
margin-left: 1em;
}
&__pass {
&-area {
display: flex;

View File

@ -182,6 +182,7 @@ $fa-var-circle-o: next-fa-glyph();
$fa-var-arrow-circle-left: next-fa-glyph();
$fa-var-cloud-download-alt: next-fa-glyph();
$fa-var-caret-down: next-fa-glyph();
$fa-var-caret-up: next-fa-glyph();
$fa-var-long-arrow-alt-left: next-fa-glyph();
$fa-var-long-arrow-alt-right: next-fa-glyph();
$fa-var-github-alt: next-fa-glyph();

View File

@ -8,10 +8,14 @@
</div>
<div class="open__icon open__icon-more id=open__icon-more">
<a href="/">
<img src="icons/brand.png" height="100" width="100"/>
<img src="icons/brand.png" height="100" width="100"/>
</a>
<div class="open__icon-text">{{res 'passwordBankTitle'}}</div>
</div>
<div class="loading__container hide">
<i class="fa fa-spinner spin"></i>
<span class="loading__text">{{res 'openFileLoadingText'}}</span>
</div>
<div class="open__icons">
{{#if canOpen}}
<div class="open__icon open__icon-open" tabindex="1" id="open__icon-open">
@ -73,6 +77,15 @@
{{!-- we need these inputs to screw browsers passwords autocompletion --}}
<input type="text" name="username">
<input type="password" name="password">
</div>
<div class="list__search-field-wrap list__search-field-wrap--text">
<input type="text" placeholder="{{res 'searchPlaceholder'}}" class="list__search-field input-search" autocomplete="off" spellcheck="false">
<div class="list__search-icon-search" >
<i class="fa fa-search"></i>
</div>
<div class="list__search-icon-clear hide">
<i class="fa fa-times-circle"></i>
</div>
</div>
<div class="open__pass-warn-wrap hide">
<div class="open__pass-warning muted-color invisible"><i class="fa fa-exclamation-triangle"></i> {{res 'openCaps'}}</div>
@ -99,13 +112,29 @@
</div>
<div class="open__last">
{{#each lastOpenFiles as |file|}}
<div class="open__last-item" data-id="{{file.id}}" tabindex="{{add @index 30}}"
<div class="open__last-item open__last-item-header" data-id="{{file.id}}" tabindex="{{add @index 30}}"
id="open__last-item--{{file.id}}">
{{#if file.icon}}<i class="fa fa-{{file.icon}} open__last-item-icon"></i>{{/if}}
<span class="open__last-item-text">{{#if file.tenantName}}{{file.tenantName}} - {{/if}}{{file.name}}</span>
{{#if file.entries}}
<span class="expand-collapse-container">
<i class="fa fa-caret-down open__last-item-expand" data-id="{{file.id}}"></i>
<i class="fa fa-caret-up open__last-item-collapse hide" data-id="{{file.id}}"></i>
</span>
{{else}}
<i class="fa fa-info-circle icon-missing-index" title="{{res 'iconMissingIndexBody'}}"></i>
{{/if}}
</div>
{{#if file.entries}}
{{#each file.entries as |entry|}}
<div class="open__last-item open__last-item-entry hide" data-id="{{entry.id}}" data-parent-id="{{file.id}}">
<i class="fa fa-key open__last-item-icon"></i>
<span class="list__item-title open__last-item-entry-text">{{#if entry.title}}{{entry.title}}{{else}}({{res 'noTitle'}}){{/if}}</span>
</div>
{{/each}}
{{/if}}
{{/each}}
</div>
</div>
</div>
<div class="open__config-wrap">
</div>