mirror of https://github.com/keeweb/keeweb.git
Add entries, search, expand/collapse ++ to landing page
This commit is contained in:
parent
deb35e9c3a
commit
7fee323f68
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue