diff --git a/app/scripts/locales/base.json b/app/scripts/locales/base.json index 227ecb31..91f3845d 100644 --- a/app/scripts/locales/base.json +++ b/app/scripts/locales/base.json @@ -17,6 +17,8 @@ "group": "group", "noTitle": "no title", "or": "or", + "history": "history", + "template": "template", "notImplemented": "Not Implemented", "saveChanges": "Save changes", "discardChanges": "Discard changes", @@ -25,7 +27,6 @@ "help": "Help", "settings": "Settings", "plugins": "Plugins", - "history": "history", "cache": "cache", "file": "file", diff --git a/app/scripts/models/app-model.js b/app/scripts/models/app-model.js index 620f840b..c582513a 100644 --- a/app/scripts/models/app-model.js +++ b/app/scripts/models/app-model.js @@ -313,9 +313,30 @@ const AppModel = Backbone.Model.extend({ return matches.map(m => m[0]); }, - createNewEntry: function() { + getEntryTemplates: function() { + const entryTemplates = []; + this.files.forEach(file => { + file.forEachEntryTemplate(entry => { + entryTemplates.push({ file, entry }); + }); + }); + return entryTemplates; + }, + + createNewEntry: function(args) { const sel = this.getFirstSelectedGroup(); - return EntryModel.newEntry(sel.group, sel.file); + if (args && args.template) { + if (sel.file !== args.template.file) { + sel.file = args.template.file; + sel.group = args.template.file.get('groups').first(); + } + const templateEntry = args.template.entry; + const newEntry = EntryModel.newEntry(sel.group, sel.file); + newEntry.copyFromTemplate(templateEntry); + return newEntry; + } else { + return EntryModel.newEntry(sel.group, sel.file); + } }, createNewGroup: function() { @@ -724,27 +745,6 @@ const AppModel = Backbone.Model.extend({ }); }); }; - const saveToCacheAndStorage = () => { - logger.info('Getting file data for saving'); - file.getData((data, err) => { - if (err) { return complete(err); } - if (storage === 'file') { - logger.info('Saving to file storage'); - saveToStorage(data); - } else if (!file.get('dirty')) { - logger.info('Saving to storage, skip cache because not dirty'); - saveToStorage(data); - } else { - logger.info('Saving to cache'); - Storage.cache.save(fileInfo.id, null, data, (err) => { - if (err) { return complete(err); } - file.set('dirty', false); - logger.info('Saved to cache, saving to storage'); - saveToStorage(data); - }); - } - }); - }; const saveToStorage = (data) => { logger.info('Save data to storage'); Storage[storage].save(path, opts, data, (err, stat) => { @@ -772,6 +772,27 @@ const AppModel = Backbone.Model.extend({ } }, fileInfo.get('rev')); }; + const saveToCacheAndStorage = () => { + logger.info('Getting file data for saving'); + file.getData((data, err) => { + if (err) { return complete(err); } + if (storage === 'file') { + logger.info('Saving to file storage'); + saveToStorage(data); + } else if (!file.get('dirty')) { + logger.info('Saving to storage, skip cache because not dirty'); + saveToStorage(data); + } else { + logger.info('Saving to cache'); + Storage.cache.save(fileInfo.id, null, data, (err) => { + if (err) { return complete(err); } + file.set('dirty', false); + logger.info('Saved to cache, saving to storage'); + saveToStorage(data); + }); + } + }); + }; logger.info('Stat file'); Storage[storage].stat(path, opts, (err, stat) => { if (err) { diff --git a/app/scripts/models/entry-model.js b/app/scripts/models/entry-model.js index a28f73d0..29a08529 100644 --- a/app/scripts/models/entry-model.js +++ b/app/scripts/models/entry-model.js @@ -622,6 +622,12 @@ const EntryModel = Backbone.Model.extend({ newEntry._fillByEntry(); this.file.reload(); return newEntry; + }, + + copyFromTemplate: function(templateEntry) { + this.entry.copyFrom(templateEntry.entry); + this.entry.fields.Title = ''; + this._fillByEntry(); } }); diff --git a/app/scripts/models/file-model.js b/app/scripts/models/file-model.js index 7a1e4ac6..8741043f 100644 --- a/app/scripts/models/file-model.js +++ b/app/scripts/models/file-model.js @@ -377,6 +377,17 @@ const FileModel = Backbone.Model.extend({ return hash ? kdbxweb.ByteUtils.bytesToBase64(hash.getBinary()) : null; }, + forEachEntryTemplate: function(callback) { + if (!this.db.meta.entryTemplatesGroup) { + return; + } + const group = this.getGroup(this.subId(this.db.meta.entryTemplatesGroup.id)); + if (!group) { + return; + } + group.forEachOwnEntry({}, callback); + }, + setSyncProgress: function() { this.set({ syncing: true }); }, diff --git a/app/scripts/models/group-model.js b/app/scripts/models/group-model.js index 57eb45c2..1ef1fe20 100644 --- a/app/scripts/models/group-model.js +++ b/app/scripts/models/group-model.js @@ -29,7 +29,7 @@ const GroupModel = MenuItemModel.extend({ }, setGroup: function(group, file, parentGroup) { - const isRecycleBin = file.db.meta.recycleBinUuid && file.db.meta.recycleBinUuid.id === group.uuid.id; + const isRecycleBin = group.uuid.equals(file.db.meta.recycleBinUuid); const id = file.subId(group.uuid.id); this.set({ id: id, @@ -125,7 +125,10 @@ const GroupModel = MenuItemModel.extend({ }, matches: function(filter) { - return (filter && filter.includeDisabled || this.group.enableSearching !== false) && + return (filter && filter.includeDisabled || + this.group.enableSearching !== false && + !this.group.uuid.equals(this.file.db.meta.entryTemplatesGroup) + ) && (!filter || !filter.autoType || this.group.enableAutoType !== false); }, diff --git a/app/scripts/views/list-search-view.js b/app/scripts/views/list-search-view.js index d9137c75..9f9a1335 100644 --- a/app/scripts/views/list-search-view.js +++ b/app/scripts/views/list-search-view.js @@ -5,6 +5,7 @@ const DropdownView = require('./dropdown-view'); const FeatureDetector = require('../util/feature-detector'); const Format = require('../util/format'); const Locale = require('../util/locale'); +const Comparators = require('../util/comparators'); const ListSearchView = Backbone.View.extend({ template: require('templates/list-search.hbs'), @@ -278,6 +279,7 @@ const ListSearchView = Backbone.View.extend({ this.hideSearchOptions(); return; } + this.hideSearchOptions(); this.$el.find('.list__search-btn-new').addClass('sel--active'); const view = new DropdownView(); @@ -288,11 +290,30 @@ const ListSearchView = Backbone.View.extend({ top: this.$el.find('.list__search-btn-new')[0].getBoundingClientRect().bottom, right: this.$el[0].getBoundingClientRect().right + 1 }, - options: this.createOptions + options: this.createOptions.concat(this.getCreateEntryTemplateOptions()) }); this.views.searchDropdown = view; }, + getCreateEntryTemplateOptions: function() { + const entryTemplates = this.model.getEntryTemplates(); + const hasMultipleFiles = this.model.files.length > 1; + this.entryTemplates = {}; + const options = []; + entryTemplates.forEach(tmpl => { + const id = 'tmpl:' + tmpl.entry.id; + options.push({ + value: id, + icon: tmpl.entry.icon, + text: hasMultipleFiles ? tmpl.file.get('name') + ' / ' + tmpl.entry.title : tmpl.entry.title + }); + this.entryTemplates[id] = tmpl; + }); + options.sort(Comparators.stringComparator('text', true)); + options.push({ value: 'tmpl', icon: 'sticky-note-o', text: Format.capFirst(Locale.template) }); + return options; + }, + sortDropdownSelect: function(e) { this.hideSearchOptions(); Backbone.trigger('set-sort', e.item); @@ -307,6 +328,13 @@ const ListSearchView = Backbone.View.extend({ case 'group': this.trigger('create-group'); break; + case 'tmpl': + this.trigger('create-template'); + break; + default: + if (this.entryTemplates[e.item]) { + this.trigger('create-entry', { template: this.entryTemplates[e.item] }); + } } }, diff --git a/app/scripts/views/list-view.js b/app/scripts/views/list-view.js index 7ecb1011..01627efa 100644 --- a/app/scripts/views/list-view.js +++ b/app/scripts/views/list-view.js @@ -8,6 +8,7 @@ const DragDropInfo = require('../comp/drag-drop-info'); const AppSettingsModel = require('../models/app-settings-model'); const Locale = require('../util/locale'); const Format = require('../util/format'); +const Alerts = require('../comp/alerts'); const ListView = Backbone.View.extend({ template: require('templates/list.hbs'), @@ -47,6 +48,7 @@ const ListView = Backbone.View.extend({ this.listenTo(this.views.search, 'select-next', this.selectNext); this.listenTo(this.views.search, 'create-entry', this.createEntry); this.listenTo(this.views.search, 'create-group', this.createGroup); + this.listenTo(this.views.search, 'create-template', this.createTemplate); this.listenTo(this, 'show', this.viewShown); this.listenTo(this, 'hide', this.viewHidden); this.listenTo(this, 'view-resize', this.viewResized); @@ -147,8 +149,8 @@ const ListView = Backbone.View.extend({ } }, - createEntry: function() { - const newEntry = this.model.createNewEntry(); + createEntry: function(arg) { + const newEntry = this.model.createNewEntry(arg); this.items.unshift(newEntry); this.render(); this.selectItem(newEntry); @@ -159,6 +161,10 @@ const ListView = Backbone.View.extend({ Backbone.trigger('edit-group', newGroup); }, + createTemplate: function() { + Alerts.notImplemented(); + }, + selectItem: function(item) { this.model.activeEntryId = item.id; Backbone.trigger('entry-selected', item); diff --git a/release-notes.md b/release-notes.md index 96a25166..69856b47 100644 --- a/release-notes.md +++ b/release-notes.md @@ -4,6 +4,7 @@ Release notes `+` plugins `*` translations are available only as plugins `*` Dropbox API V2 +`+` entry templates `+` support cloud providers in iOS homescreen apps `+` mobile field editing improvements `+` file path hint in recent files list