mirror of https://github.com/keeweb/keeweb
menu models
parent
2f93e53716
commit
4d7cef70d2
|
@ -293,7 +293,7 @@ AutoTypeRunner.prototype.getEntryFieldKeys = function(field, op) {
|
|||
};
|
||||
|
||||
AutoTypeRunner.prototype.getEntryGroupName = function() {
|
||||
return this.entry && this.entry.group.get('title');
|
||||
return this.entry && this.entry.group.title;
|
||||
};
|
||||
|
||||
AutoTypeRunner.prototype.dt = function(part) {
|
||||
|
|
|
@ -26,7 +26,8 @@ const AutoType = {
|
|||
}
|
||||
this.appModel = appModel;
|
||||
Events.on('auto-type', e => this.handleEvent(e));
|
||||
Events.on('main-window-blur main-window-will-close', e => this.resetPendingEvent(e));
|
||||
Events.on('main-window-blur', e => this.resetPendingEvent(e));
|
||||
Events.on('main-window-will-close', e => this.resetPendingEvent(e));
|
||||
},
|
||||
|
||||
handleEvent(e) {
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import Backbone from 'backbone';
|
||||
import { Collection } from 'framework/collection';
|
||||
import { EntryModel } from 'models/entry-model';
|
||||
import { Comparators } from 'util/data/comparators';
|
||||
|
||||
const EntryCollection = Backbone.Collection.extend({
|
||||
model: EntryModel,
|
||||
class EntryCollection extends Collection {
|
||||
static model = EntryModel;
|
||||
|
||||
comparator: null,
|
||||
|
||||
comparators: {
|
||||
comparators = {
|
||||
'none': null,
|
||||
'title': Comparators.stringComparator('title', true),
|
||||
'-title': Comparators.stringComparator('title', false),
|
||||
|
@ -23,22 +21,23 @@ const EntryCollection = Backbone.Collection.extend({
|
|||
return this.attachmentSortVal(x).localeCompare(this.attachmentSortVal(y));
|
||||
},
|
||||
'-rank': Comparators.rankComparator()
|
||||
},
|
||||
};
|
||||
|
||||
defaultComparator: 'title',
|
||||
defaultComparator = 'title';
|
||||
|
||||
filter: null,
|
||||
filter = null;
|
||||
|
||||
initialize(models, options) {
|
||||
constructor(models, options) {
|
||||
super(models);
|
||||
const comparatorName = (options && options.comparator) || this.defaultComparator;
|
||||
this.comparator = this.comparators[comparatorName];
|
||||
},
|
||||
}
|
||||
|
||||
sortEntries(comparator, filter) {
|
||||
this.filter = filter;
|
||||
this.comparator = this.comparators[comparator] || this.comparators[this.defaultComparator];
|
||||
this.sort();
|
||||
},
|
||||
}
|
||||
|
||||
attachmentSortVal(entry) {
|
||||
const att = entry.attachments;
|
||||
|
@ -48,6 +47,6 @@ const EntryCollection = Backbone.Collection.extend({
|
|||
}
|
||||
return str;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export { EntryCollection };
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import Backbone from 'backbone';
|
||||
import { Collection } from 'framework/collection';
|
||||
import { GroupModel } from 'models/group-model';
|
||||
|
||||
const GroupCollection = Backbone.Collection.extend({
|
||||
model: GroupModel
|
||||
});
|
||||
class GroupCollection extends Collection {
|
||||
static model = GroupModel;
|
||||
}
|
||||
|
||||
export { GroupCollection };
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import Backbone from 'backbone';
|
||||
import { Collection } from 'framework/collection';
|
||||
import { MenuItemModel } from 'models/menu/menu-item-model';
|
||||
|
||||
const MenuItemCollection = Backbone.Collection.extend({
|
||||
model: MenuItemModel
|
||||
});
|
||||
class MenuItemCollection extends Collection {
|
||||
static model = MenuItemModel;
|
||||
}
|
||||
|
||||
export { MenuItemCollection };
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import Backbone from 'backbone';
|
||||
import { Collection } from 'framework/collection';
|
||||
import { MenuOptionModel } from 'models/menu/menu-option-model';
|
||||
|
||||
const MenuOptionCollection = Backbone.Collection.extend({
|
||||
model: MenuOptionModel
|
||||
});
|
||||
class MenuOptionCollection extends Collection {
|
||||
static model = MenuOptionModel;
|
||||
}
|
||||
|
||||
export { MenuOptionCollection };
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import Backbone from 'backbone';
|
||||
import { Collection } from 'framework/collection';
|
||||
import { MenuSectionModel } from 'models/menu/menu-section-model';
|
||||
|
||||
const MenuSectionCollection = Backbone.Collection.extend({
|
||||
model: MenuSectionModel
|
||||
});
|
||||
class MenuSectionCollection extends Collection {
|
||||
static model = MenuSectionModel;
|
||||
}
|
||||
|
||||
export { MenuSectionCollection };
|
||||
|
|
|
@ -40,7 +40,8 @@ const ProxyDef = {
|
|||
}
|
||||
const numProp = parseInt(property);
|
||||
if (isNaN(numProp)) {
|
||||
return false;
|
||||
target[property] = value;
|
||||
return true;
|
||||
}
|
||||
const modelClass = target.constructor.model;
|
||||
if (!modelClass) {
|
||||
|
@ -61,8 +62,8 @@ const ProxyDef = {
|
|||
};
|
||||
|
||||
class Collection extends Array {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
constructor(items) {
|
||||
super();
|
||||
|
||||
const emitter = new EventEmitter();
|
||||
emitter.setMaxListeners(100);
|
||||
|
@ -73,7 +74,13 @@ class Collection extends Array {
|
|||
|
||||
Object.defineProperties(this, properties);
|
||||
|
||||
return new Proxy(this, ProxyDef);
|
||||
const object = new Proxy(this, ProxyDef);
|
||||
|
||||
if (items) {
|
||||
object.push(...items);
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
push(...items) {
|
||||
|
|
|
@ -40,7 +40,10 @@ const ProxyDef = {
|
|||
}
|
||||
return true;
|
||||
} else {
|
||||
new Logger(receiver.constructor.name).warn(`Unknown property: ${property}`);
|
||||
new Logger(receiver.constructor.name).warn(
|
||||
`Unknown property: ${property}`,
|
||||
new Error().stack
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -106,7 +109,7 @@ class Model {
|
|||
}
|
||||
|
||||
static defineModelProperties(properties, options) {
|
||||
this.prototype[SymbolDefaults] = properties;
|
||||
this.prototype[SymbolDefaults] = { ...this.prototype[SymbolDefaults], ...properties };
|
||||
if (options && options.extensions) {
|
||||
this.prototype[SymbolExtensions] = true;
|
||||
}
|
||||
|
|
|
@ -158,9 +158,9 @@ class AppModel {
|
|||
return false;
|
||||
}
|
||||
this.files.push(file);
|
||||
file.groups.forEach(function(group) {
|
||||
for (const group of file.groups) {
|
||||
this.menu.groupsSection.addItem(group);
|
||||
}, this);
|
||||
}
|
||||
this._addTags(file);
|
||||
this._tagsChanged();
|
||||
this.menu.filesSection.addItem({
|
||||
|
@ -175,7 +175,7 @@ class AppModel {
|
|||
}
|
||||
|
||||
reloadFile(file) {
|
||||
this.menu.groupsSection.replaceByFile(file, file.groups.first());
|
||||
this.menu.groupsSection.replaceByFile(file, file.groups[0]);
|
||||
this.updateTags();
|
||||
}
|
||||
|
||||
|
@ -197,7 +197,7 @@ class AppModel {
|
|||
|
||||
_tagsChanged() {
|
||||
if (this.tags.length) {
|
||||
this.menu.tagsSection.set('scrollable', true);
|
||||
this.menu.tagsSection.scrollable = true;
|
||||
this.menu.tagsSection.setItems(
|
||||
this.tags.map(tag => {
|
||||
return {
|
||||
|
@ -210,7 +210,7 @@ class AppModel {
|
|||
})
|
||||
);
|
||||
} else {
|
||||
this.menu.tagsSection.set('scrollable', false);
|
||||
this.menu.tagsSection.scrollable = false;
|
||||
this.menu.tagsSection.removeAllItems();
|
||||
}
|
||||
}
|
||||
|
@ -238,7 +238,7 @@ class AppModel {
|
|||
}
|
||||
this.files.length = 0;
|
||||
this.menu.groupsSection.removeAllItems();
|
||||
this.menu.tagsSection.set('scrollable', false);
|
||||
this.menu.tagsSection.scrollable = false;
|
||||
this.menu.tagsSection.removeAllItems();
|
||||
this.menu.filesSection.removeAllItems();
|
||||
this.tags.splice(0, this.tags.length);
|
||||
|
@ -253,7 +253,7 @@ class AppModel {
|
|||
this.updateTags();
|
||||
this.menu.groupsSection.removeByFile(file);
|
||||
this.menu.filesSection.removeByFile(file);
|
||||
this.menu.select({ item: this.menu.allItemsSection.get('items').first() });
|
||||
this.menu.select({ item: this.menu.allItemsSection.items[0] });
|
||||
}
|
||||
|
||||
emptyTrash() {
|
||||
|
@ -269,7 +269,7 @@ class AppModel {
|
|||
}
|
||||
const entries = this.getEntries();
|
||||
if (!this.activeEntryId || !entries.get(this.activeEntryId)) {
|
||||
const firstEntry = entries.first();
|
||||
const firstEntry = entries[0];
|
||||
this.activeEntryId = firstEntry ? firstEntry.id : null;
|
||||
}
|
||||
Events.emit('filter', { filter: this.filter, sort: this.sort, entries });
|
||||
|
@ -341,8 +341,8 @@ class AppModel {
|
|||
});
|
||||
}
|
||||
if (!group) {
|
||||
file = this.files.first();
|
||||
group = file.groups.first();
|
||||
file = this.files[0];
|
||||
group = file.groups[0];
|
||||
}
|
||||
return { group, file };
|
||||
}
|
||||
|
@ -384,7 +384,7 @@ class AppModel {
|
|||
if (args && args.template) {
|
||||
if (sel.file !== args.template.file) {
|
||||
sel.file = args.template.file;
|
||||
sel.group = args.template.file.groups.first();
|
||||
sel.group = args.template.file.groups[0];
|
||||
}
|
||||
const templateEntry = args.template.entry;
|
||||
const newEntry = EntryModel.newEntry(sel.group, sel.file);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Backbone from 'backbone';
|
||||
import kdbxweb from 'kdbxweb';
|
||||
import { Model } from 'framework/model';
|
||||
import { KdbxToHtml } from 'comp/format/kdbx-to-html';
|
||||
import { IconMap } from 'const/icon-map';
|
||||
import { AttachmentModel } from 'models/attachment-model';
|
||||
|
@ -9,43 +9,40 @@ import { Ranking } from 'util/data/ranking';
|
|||
import { IconUrlFormat } from 'util/formatting/icon-url-format';
|
||||
import { omit } from 'util/fn';
|
||||
|
||||
const EntryModel = Backbone.Model.extend({
|
||||
defaults: {},
|
||||
|
||||
urlRegex: /^https?:\/\//i,
|
||||
fieldRefRegex: /^\{REF:([TNPAU])@I:(\w{32})}$/,
|
||||
|
||||
builtInFields: [
|
||||
'Title',
|
||||
'Password',
|
||||
'UserName',
|
||||
'URL',
|
||||
'Notes',
|
||||
'TOTP Seed',
|
||||
'TOTP Settings',
|
||||
'_etm_template_uuid'
|
||||
],
|
||||
fieldRefFields: ['title', 'password', 'user', 'url', 'notes'],
|
||||
fieldRefIds: { T: 'Title', U: 'UserName', P: 'Password', A: 'URL', N: 'Notes' },
|
||||
const UrlRegex = /^https?:\/\//i;
|
||||
const FieldRefRegex = /^\{REF:([TNPAU])@I:(\w{32})}$/;
|
||||
const BuiltInFields = [
|
||||
'Title',
|
||||
'Password',
|
||||
'UserName',
|
||||
'URL',
|
||||
'Notes',
|
||||
'TOTP Seed',
|
||||
'TOTP Settings',
|
||||
'_etm_template_uuid'
|
||||
];
|
||||
const FieldRefFields = ['title', 'password', 'user', 'url', 'notes'];
|
||||
const FieldRefIds = { T: 'Title', U: 'UserName', P: 'Password', A: 'URL', N: 'Notes' };
|
||||
|
||||
class EntryModel extends Model {
|
||||
setEntry(entry, group, file) {
|
||||
this.entry = entry;
|
||||
this.group = group;
|
||||
this.file = file;
|
||||
if (this.get('uuid') === entry.uuid.id) {
|
||||
if (this.uuid === entry.uuid.id) {
|
||||
this._checkUpdatedEntry();
|
||||
}
|
||||
// we cannot calculate field references now because database index has not yet been built
|
||||
this.hasFieldRefs = false;
|
||||
this._fillByEntry();
|
||||
this.hasFieldRefs = true;
|
||||
},
|
||||
}
|
||||
|
||||
_fillByEntry() {
|
||||
const entry = this.entry;
|
||||
this.set({ id: this.file.subId(entry.uuid.id), uuid: entry.uuid.id }, { silent: true });
|
||||
this.fileName = this.file.name;
|
||||
this.groupName = this.group.get('title');
|
||||
this.groupName = this.group.title;
|
||||
this.title = this._getFieldString('Title');
|
||||
this.password = entry.fields.Password || kdbxweb.ProtectedValue.fromString('');
|
||||
this.notes = this._getFieldString('Notes');
|
||||
|
@ -71,7 +68,7 @@ const EntryModel = Backbone.Model.extend({
|
|||
if (this.hasFieldRefs) {
|
||||
this.resolveFieldReferences();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
_getFieldString(field) {
|
||||
const val = this.entry.fields[field];
|
||||
|
@ -82,7 +79,7 @@ const EntryModel = Backbone.Model.extend({
|
|||
return val.getText();
|
||||
}
|
||||
return val.toString();
|
||||
},
|
||||
}
|
||||
|
||||
_checkUpdatedEntry() {
|
||||
if (this.isJustCreated) {
|
||||
|
@ -94,7 +91,7 @@ const EntryModel = Backbone.Model.extend({
|
|||
if (this.unsaved && +this.updated !== +this.entry.times.lastModTime) {
|
||||
this.unsaved = false;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
_buildSearchText() {
|
||||
let text = '';
|
||||
|
@ -110,7 +107,7 @@ const EntryModel = Backbone.Model.extend({
|
|||
text += att.title.toLowerCase() + '\n';
|
||||
});
|
||||
this.searchText = text;
|
||||
},
|
||||
}
|
||||
|
||||
_buildCustomIcon() {
|
||||
this.customIcon = null;
|
||||
|
@ -121,15 +118,15 @@ const EntryModel = Backbone.Model.extend({
|
|||
);
|
||||
this.customIconId = this.entry.customIcon.toString();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
_buildSearchTags() {
|
||||
this.searchTags = this.entry.tags.map(tag => tag.toLowerCase());
|
||||
},
|
||||
}
|
||||
|
||||
_buildSearchColor() {
|
||||
this.searchColor = this.color;
|
||||
},
|
||||
}
|
||||
|
||||
_buildAutoType() {
|
||||
this.autoTypeEnabled = this.entry.autoType.enabled;
|
||||
|
@ -138,30 +135,30 @@ const EntryModel = Backbone.Model.extend({
|
|||
kdbxweb.Consts.AutoTypeObfuscationOptions.UseClipboard;
|
||||
this.autoTypeSequence = this.entry.autoType.defaultSequence;
|
||||
this.autoTypeWindows = this.entry.autoType.items.map(this._convertAutoTypeItem);
|
||||
},
|
||||
}
|
||||
|
||||
_convertAutoTypeItem(item) {
|
||||
return { window: item.window, sequence: item.keystrokeSequence };
|
||||
},
|
||||
}
|
||||
|
||||
_iconFromId(id) {
|
||||
return IconMap[id];
|
||||
},
|
||||
}
|
||||
|
||||
_getDisplayUrl(url) {
|
||||
if (!url) {
|
||||
return '';
|
||||
}
|
||||
return url.replace(this.urlRegex, '');
|
||||
},
|
||||
return url.replace(UrlRegex, '');
|
||||
}
|
||||
|
||||
_colorToModel(color) {
|
||||
return color ? Color.getNearest(color) : null;
|
||||
},
|
||||
}
|
||||
|
||||
_fieldsToModel(fields) {
|
||||
return omit(fields, this.builtInFields);
|
||||
},
|
||||
}
|
||||
|
||||
_attachmentsToModel(binaries) {
|
||||
const att = [];
|
||||
|
@ -174,7 +171,7 @@ const EntryModel = Backbone.Model.extend({
|
|||
}
|
||||
}
|
||||
return att;
|
||||
},
|
||||
}
|
||||
|
||||
_entryModified() {
|
||||
if (!this.unsaved) {
|
||||
|
@ -186,7 +183,7 @@ const EntryModel = Backbone.Model.extend({
|
|||
this.isJustCreated = false;
|
||||
}
|
||||
this.entry.times.update();
|
||||
},
|
||||
}
|
||||
|
||||
setSaved() {
|
||||
if (this.unsaved) {
|
||||
|
@ -195,7 +192,7 @@ const EntryModel = Backbone.Model.extend({
|
|||
if (this.canBeDeleted) {
|
||||
this.canBeDeleted = false;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
matches(filter) {
|
||||
return (
|
||||
|
@ -210,7 +207,7 @@ const EntryModel = Backbone.Model.extend({
|
|||
this.searchColor === filter.color) &&
|
||||
(!filter.autoType || this.autoTypeEnabled))
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
matchesAdv(filter) {
|
||||
const adv = filter.advanced;
|
||||
|
@ -240,28 +237,28 @@ const EntryModel = Backbone.Model.extend({
|
|||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
}
|
||||
|
||||
matchString(str, find) {
|
||||
if (str.isProtected) {
|
||||
return str.includes(find);
|
||||
}
|
||||
return str.indexOf(find) >= 0;
|
||||
},
|
||||
}
|
||||
|
||||
matchStringLower(str, findLower) {
|
||||
if (str.isProtected) {
|
||||
return str.includesLower(findLower);
|
||||
}
|
||||
return str.toLowerCase().indexOf(findLower) >= 0;
|
||||
},
|
||||
}
|
||||
|
||||
matchRegex(str, regex) {
|
||||
if (str.isProtected) {
|
||||
str = str.getText();
|
||||
}
|
||||
return regex.test(str);
|
||||
},
|
||||
}
|
||||
|
||||
matchEntry(entry, adv, compare, search) {
|
||||
const matchField = this.matchField;
|
||||
|
@ -282,10 +279,9 @@ const EntryModel = Backbone.Model.extend({
|
|||
}
|
||||
let matches = false;
|
||||
if (adv.other || adv.protect) {
|
||||
const builtInFields = this.builtInFields;
|
||||
const fieldNames = Object.keys(entry.fields);
|
||||
matches = fieldNames.some(field => {
|
||||
if (builtInFields.indexOf(field) >= 0) {
|
||||
if (BuiltInFields.indexOf(field) >= 0) {
|
||||
return false;
|
||||
}
|
||||
if (typeof entry.fields[field] === 'string') {
|
||||
|
@ -296,16 +292,16 @@ const EntryModel = Backbone.Model.extend({
|
|||
});
|
||||
}
|
||||
return matches;
|
||||
},
|
||||
}
|
||||
|
||||
matchField(entry, field, compare, search) {
|
||||
const val = entry.fields[field];
|
||||
return val ? compare(val, search) : false;
|
||||
},
|
||||
}
|
||||
|
||||
resolveFieldReferences() {
|
||||
this.hasFieldRefs = false;
|
||||
this.fieldRefFields.forEach(field => {
|
||||
FieldRefFields.forEach(field => {
|
||||
const fieldValue = this[field];
|
||||
const refValue = this._resolveFieldReference(fieldValue);
|
||||
if (refValue !== undefined) {
|
||||
|
@ -313,7 +309,7 @@ const EntryModel = Backbone.Model.extend({
|
|||
this.hasFieldRefs = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
getFieldValue(field) {
|
||||
field = field.toLowerCase();
|
||||
|
@ -333,7 +329,7 @@ const EntryModel = Backbone.Model.extend({
|
|||
}
|
||||
return fieldValue;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
_resolveFieldReference(fieldValue) {
|
||||
if (!fieldValue) {
|
||||
|
@ -345,12 +341,12 @@ const EntryModel = Backbone.Model.extend({
|
|||
if (typeof fieldValue !== 'string') {
|
||||
return;
|
||||
}
|
||||
const match = fieldValue.match(this.fieldRefRegex);
|
||||
const match = fieldValue.match(FieldRefRegex);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
return this._getReferenceValue(match[1], match[2]);
|
||||
},
|
||||
}
|
||||
|
||||
_getReferenceValue(fieldRefId, idStr) {
|
||||
const id = new Uint8Array(16);
|
||||
|
@ -362,40 +358,40 @@ const EntryModel = Backbone.Model.extend({
|
|||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
return entry.entry.fields[this.fieldRefIds[fieldRefId]];
|
||||
},
|
||||
return entry.entry.fields[FieldRefIds[fieldRefId]];
|
||||
}
|
||||
|
||||
setColor(color) {
|
||||
this._entryModified();
|
||||
this.entry.bgColor = Color.getKnownBgColor(color);
|
||||
this._fillByEntry();
|
||||
},
|
||||
}
|
||||
|
||||
setIcon(iconId) {
|
||||
this._entryModified();
|
||||
this.entry.icon = iconId;
|
||||
this.entry.customIcon = undefined;
|
||||
this._fillByEntry();
|
||||
},
|
||||
}
|
||||
|
||||
setCustomIcon(customIconId) {
|
||||
this._entryModified();
|
||||
this.entry.customIcon = new kdbxweb.KdbxUuid(customIconId);
|
||||
this._fillByEntry();
|
||||
},
|
||||
}
|
||||
|
||||
setExpires(dt) {
|
||||
this._entryModified();
|
||||
this.entry.times.expiryTime = dt instanceof Date ? dt : undefined;
|
||||
this.entry.times.expires = !!dt;
|
||||
this._fillByEntry();
|
||||
},
|
||||
}
|
||||
|
||||
setTags(tags) {
|
||||
this._entryModified();
|
||||
this.entry.tags = tags;
|
||||
this._fillByEntry();
|
||||
},
|
||||
}
|
||||
|
||||
renameTag(from, to) {
|
||||
const ix = this.entry.tags.findIndex(tag => tag.toLowerCase() === from.toLowerCase());
|
||||
|
@ -408,11 +404,11 @@ const EntryModel = Backbone.Model.extend({
|
|||
this.entry.tags.push(to);
|
||||
}
|
||||
this._fillByEntry();
|
||||
},
|
||||
}
|
||||
|
||||
setField(field, val, allowEmpty) {
|
||||
const hasValue = val && (typeof val === 'string' || (val.isProtected && val.byteLength));
|
||||
if (hasValue || allowEmpty || this.builtInFields.indexOf(field) >= 0) {
|
||||
if (hasValue || allowEmpty || BuiltInFields.indexOf(field) >= 0) {
|
||||
this._entryModified();
|
||||
val = this.sanitizeFieldValue(val);
|
||||
this.entry.fields[field] = val;
|
||||
|
@ -421,7 +417,7 @@ const EntryModel = Backbone.Model.extend({
|
|||
delete this.entry.fields[field];
|
||||
}
|
||||
this._fillByEntry();
|
||||
},
|
||||
}
|
||||
|
||||
sanitizeFieldValue(val) {
|
||||
if (val && !val.isProtected && val.indexOf('\x1A') >= 0) {
|
||||
|
@ -430,11 +426,11 @@ const EntryModel = Backbone.Model.extend({
|
|||
val = val.replace(/\x1A/g, '');
|
||||
}
|
||||
return val;
|
||||
},
|
||||
}
|
||||
|
||||
hasField(field) {
|
||||
return Object.prototype.hasOwnProperty.call(this.entry.fields, field);
|
||||
},
|
||||
}
|
||||
|
||||
addAttachment(name, data) {
|
||||
this._entryModified();
|
||||
|
@ -442,13 +438,13 @@ const EntryModel = Backbone.Model.extend({
|
|||
this.entry.binaries[name] = binaryRef;
|
||||
this._fillByEntry();
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
removeAttachment(name) {
|
||||
this._entryModified();
|
||||
delete this.entry.binaries[name];
|
||||
this._fillByEntry();
|
||||
},
|
||||
}
|
||||
|
||||
getHistory() {
|
||||
const history = this.entry.history.map(function(rec) {
|
||||
|
@ -457,7 +453,7 @@ const EntryModel = Backbone.Model.extend({
|
|||
history.push(this);
|
||||
history.sort((x, y) => x.updated - y.updated);
|
||||
return history;
|
||||
},
|
||||
}
|
||||
|
||||
deleteHistory(historyEntry) {
|
||||
const ix = this.entry.history.indexOf(historyEntry);
|
||||
|
@ -466,7 +462,7 @@ const EntryModel = Backbone.Model.extend({
|
|||
this.file.setModified();
|
||||
}
|
||||
this._fillByEntry();
|
||||
},
|
||||
}
|
||||
|
||||
revertToHistoryState(historyEntry) {
|
||||
const ix = this.entry.history.indexOf(historyEntry);
|
||||
|
@ -481,7 +477,7 @@ const EntryModel = Backbone.Model.extend({
|
|||
this.entry.copyFrom(historyEntry);
|
||||
this._entryModified();
|
||||
this._fillByEntry();
|
||||
},
|
||||
}
|
||||
|
||||
discardUnsaved() {
|
||||
if (this.unsaved && this.entry.history.length) {
|
||||
|
@ -493,7 +489,7 @@ const EntryModel = Backbone.Model.extend({
|
|||
this.entry.copyFrom(historyEntry);
|
||||
this._fillByEntry();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
moveToTrash() {
|
||||
this.file.setModified();
|
||||
|
@ -502,13 +498,13 @@ const EntryModel = Backbone.Model.extend({
|
|||
}
|
||||
this.file.db.remove(this.entry);
|
||||
this.file.reload();
|
||||
},
|
||||
}
|
||||
|
||||
deleteFromTrash() {
|
||||
this.file.setModified();
|
||||
this.file.db.move(this.entry, null);
|
||||
this.file.reload();
|
||||
},
|
||||
}
|
||||
|
||||
removeWithoutHistory() {
|
||||
if (this.canBeDeleted) {
|
||||
|
@ -518,12 +514,12 @@ const EntryModel = Backbone.Model.extend({
|
|||
}
|
||||
this.file.reload();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
moveToFile(file) {
|
||||
if (this.canBeDeleted) {
|
||||
this.removeWithoutHistory();
|
||||
this.group = file.groups.first();
|
||||
this.group = file.groups[0];
|
||||
this.file = file;
|
||||
this._fillByEntry();
|
||||
this.entry.times.update();
|
||||
|
@ -533,7 +529,7 @@ const EntryModel = Backbone.Model.extend({
|
|||
this.unsaved = true;
|
||||
this.file.setModified();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
initOtpGenerator() {
|
||||
let otpUrl;
|
||||
|
@ -592,35 +588,35 @@ const EntryModel = Backbone.Model.extend({
|
|||
} else {
|
||||
this.otpGenerator = null;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
setOtp(otp) {
|
||||
this.otpGenerator = otp;
|
||||
this.setOtpUrl(otp.url);
|
||||
},
|
||||
}
|
||||
|
||||
setOtpUrl(url) {
|
||||
this.setField('otp', url ? kdbxweb.ProtectedValue.fromString(url) : undefined);
|
||||
delete this.entry.fields['TOTP Seed'];
|
||||
delete this.entry.fields['TOTP Settings'];
|
||||
},
|
||||
}
|
||||
|
||||
getEffectiveEnableAutoType() {
|
||||
if (typeof this.entry.autoType.enabled === 'boolean') {
|
||||
return this.entry.autoType.enabled;
|
||||
}
|
||||
return this.group.getEffectiveEnableAutoType();
|
||||
},
|
||||
}
|
||||
|
||||
getEffectiveAutoTypeSeq() {
|
||||
return this.entry.autoType.defaultSequence || this.group.getEffectiveAutoTypeSeq();
|
||||
},
|
||||
}
|
||||
|
||||
setEnableAutoType(enabled) {
|
||||
this._entryModified();
|
||||
this.entry.autoType.enabled = enabled;
|
||||
this._buildAutoType();
|
||||
},
|
||||
}
|
||||
|
||||
setAutoTypeObfuscation(enabled) {
|
||||
this._entryModified();
|
||||
|
@ -628,23 +624,23 @@ const EntryModel = Backbone.Model.extend({
|
|||
? kdbxweb.Consts.AutoTypeObfuscationOptions.UseClipboard
|
||||
: kdbxweb.Consts.AutoTypeObfuscationOptions.None;
|
||||
this._buildAutoType();
|
||||
},
|
||||
}
|
||||
|
||||
setAutoTypeSeq(seq) {
|
||||
this._entryModified();
|
||||
this.entry.autoType.defaultSequence = seq || undefined;
|
||||
this._buildAutoType();
|
||||
},
|
||||
}
|
||||
|
||||
getGroupPath() {
|
||||
let group = this.group;
|
||||
const groupPath = [];
|
||||
while (group) {
|
||||
groupPath.unshift(group.get('title'));
|
||||
groupPath.unshift(group.title);
|
||||
group = group.parentGroup;
|
||||
}
|
||||
return groupPath;
|
||||
},
|
||||
}
|
||||
|
||||
cloneEntry(nameSuffix) {
|
||||
const newEntry = EntryModel.newEntry(this.group, this.file);
|
||||
|
@ -657,7 +653,7 @@ const EntryModel = Backbone.Model.extend({
|
|||
newEntry._fillByEntry();
|
||||
this.file.reload();
|
||||
return newEntry;
|
||||
},
|
||||
}
|
||||
|
||||
copyFromTemplate(templateEntry) {
|
||||
const uuid = this.entry.uuid;
|
||||
|
@ -667,7 +663,7 @@ const EntryModel = Backbone.Model.extend({
|
|||
this.entry.times.creationTime = this.entry.times.lastModTime;
|
||||
this.entry.fields.Title = '';
|
||||
this._fillByEntry();
|
||||
},
|
||||
}
|
||||
|
||||
getRank(filter) {
|
||||
const searchString = filter.textLower;
|
||||
|
@ -702,30 +698,32 @@ const EntryModel = Backbone.Model.extend({
|
|||
const fieldWeight = fieldWeights[fieldName] || defaultFieldWeight;
|
||||
return rank + stringRank * fieldWeight;
|
||||
}, 0);
|
||||
},
|
||||
}
|
||||
|
||||
getHtml() {
|
||||
return KdbxToHtml.entryToHtml(this.file.db, this.entry);
|
||||
}
|
||||
});
|
||||
|
||||
EntryModel.fromEntry = function(entry, group, file) {
|
||||
const model = new EntryModel();
|
||||
model.setEntry(entry, group, file);
|
||||
return model;
|
||||
};
|
||||
static fromEntry(entry, group, file) {
|
||||
const model = new EntryModel();
|
||||
model.setEntry(entry, group, file);
|
||||
return model;
|
||||
}
|
||||
|
||||
EntryModel.newEntry = function(group, file) {
|
||||
const model = new EntryModel();
|
||||
const entry = file.db.createEntry(group.group);
|
||||
model.setEntry(entry, group, file);
|
||||
model.entry.times.update();
|
||||
model.unsaved = true;
|
||||
model.isJustCreated = true;
|
||||
model.canBeDeleted = true;
|
||||
group.addEntry(model);
|
||||
file.setModified();
|
||||
return model;
|
||||
};
|
||||
static newEntry(group, file) {
|
||||
const model = new EntryModel();
|
||||
const entry = file.db.createEntry(group.group);
|
||||
model.setEntry(entry, group, file);
|
||||
model.entry.times.update();
|
||||
model.unsaved = true;
|
||||
model.isJustCreated = true;
|
||||
model.canBeDeleted = true;
|
||||
group.addEntry(model);
|
||||
file.setModified();
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
||||
EntryModel.defineModelProperties({}, { extensions: true });
|
||||
|
||||
export { EntryModel };
|
||||
|
|
|
@ -172,7 +172,7 @@ class FileModel extends Model {
|
|||
} else {
|
||||
groupModel = GroupModel.fromGroup(group, this);
|
||||
}
|
||||
groups.add(groupModel);
|
||||
groups.push(groupModel);
|
||||
}, this);
|
||||
this.buildObjectMap();
|
||||
this.resolveFieldReferences();
|
||||
|
@ -384,7 +384,7 @@ class FileModel extends Model {
|
|||
}
|
||||
|
||||
createEntryTemplatesGroup() {
|
||||
const rootGroup = this.groups.first();
|
||||
const rootGroup = this.groups[0];
|
||||
const templatesGroup = GroupModel.newGroup(rootGroup, this);
|
||||
templatesGroup.setName('Templates');
|
||||
this.db.meta.entryTemplatesGroup = templatesGroup.group.uuid;
|
||||
|
@ -553,7 +553,7 @@ class FileModel extends Model {
|
|||
this.db.meta.name = name;
|
||||
this.db.meta.nameChanged = new Date();
|
||||
this.name = name;
|
||||
this.groups.first().setName(name);
|
||||
this.groups[0].setName(name);
|
||||
this.setModified();
|
||||
this.reload();
|
||||
}
|
||||
|
@ -630,8 +630,8 @@ class FileModel extends Model {
|
|||
this.db.move(entry, null);
|
||||
modified = true;
|
||||
}, this);
|
||||
trashGroup.get('items').reset();
|
||||
trashGroup.get('entries').reset();
|
||||
trashGroup.items.length = 0;
|
||||
trashGroup.entries.length = 0;
|
||||
if (modified) {
|
||||
this.setModified();
|
||||
}
|
||||
|
|
|
@ -10,20 +10,7 @@ const KdbxIcons = kdbxweb.Consts.Icons;
|
|||
|
||||
const DefaultAutoTypeSequence = '{USERNAME}{TAB}{PASSWORD}{ENTER}';
|
||||
|
||||
const GroupModel = MenuItemModel.extend({
|
||||
defaults: Object.assign({}, MenuItemModel.prototype.defaults, {
|
||||
iconId: 0,
|
||||
entries: null,
|
||||
filterKey: 'group',
|
||||
editable: true,
|
||||
top: false,
|
||||
drag: true,
|
||||
drop: true,
|
||||
enableSearching: true,
|
||||
enableAutoType: null,
|
||||
autoTypeSeq: null
|
||||
}),
|
||||
|
||||
class GroupModel extends MenuItemModel {
|
||||
setGroup(group, file, parentGroup) {
|
||||
const isRecycleBin = group.uuid.equals(file.db.meta.recycleBinUuid);
|
||||
const id = file.subId(group.uuid.id);
|
||||
|
@ -49,8 +36,8 @@ const GroupModel = MenuItemModel.extend({
|
|||
this.file = file;
|
||||
this.parentGroup = parentGroup;
|
||||
this._fillByGroup(true);
|
||||
const items = this.get('items');
|
||||
const entries = this.get('entries');
|
||||
const items = this.items;
|
||||
const entries = this.entries;
|
||||
|
||||
const itemsArray = group.groups.map(subGroup => {
|
||||
let g = file.getGroup(file.subId(subGroup.uuid.id));
|
||||
|
@ -61,7 +48,7 @@ const GroupModel = MenuItemModel.extend({
|
|||
}
|
||||
return g;
|
||||
}, this);
|
||||
items.add(itemsArray);
|
||||
items.push(...itemsArray);
|
||||
|
||||
const entriesArray = group.entries.map(entry => {
|
||||
let e = file.getEntry(file.subId(entry.uuid.id));
|
||||
|
@ -72,8 +59,8 @@ const GroupModel = MenuItemModel.extend({
|
|||
}
|
||||
return e;
|
||||
}, this);
|
||||
entries.add(entriesArray);
|
||||
},
|
||||
entries.push(...entriesArray);
|
||||
}
|
||||
|
||||
_fillByGroup(silent) {
|
||||
this.set(
|
||||
|
@ -87,14 +74,14 @@ const GroupModel = MenuItemModel.extend({
|
|||
},
|
||||
{ silent }
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
_iconFromId(id) {
|
||||
if (id === KdbxIcons.Folder || id === KdbxIcons.FolderOpen) {
|
||||
return undefined;
|
||||
}
|
||||
return IconMap[id];
|
||||
},
|
||||
}
|
||||
|
||||
_buildCustomIcon() {
|
||||
this.customIcon = null;
|
||||
|
@ -102,7 +89,7 @@ const GroupModel = MenuItemModel.extend({
|
|||
return IconUrlFormat.toDataUrl(this.file.db.meta.customIcons[this.group.customIcon]);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
}
|
||||
|
||||
_groupModified() {
|
||||
if (this.isJustCreated) {
|
||||
|
@ -110,26 +97,26 @@ const GroupModel = MenuItemModel.extend({
|
|||
}
|
||||
this.file.setModified();
|
||||
this.group.times.update();
|
||||
},
|
||||
}
|
||||
|
||||
forEachGroup(callback, filter) {
|
||||
let result = true;
|
||||
this.get('items').forEach(group => {
|
||||
this.items.forEach(group => {
|
||||
if (group.matches(filter)) {
|
||||
result =
|
||||
callback(group) !== false && group.forEachGroup(callback, filter) !== false;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
},
|
||||
}
|
||||
|
||||
forEachOwnEntry(filter, callback) {
|
||||
this.get('entries').forEach(function(entry) {
|
||||
this.entries.forEach(function(entry) {
|
||||
if (entry.matches(filter)) {
|
||||
callback(entry, this);
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
matches(filter) {
|
||||
return (
|
||||
|
@ -138,52 +125,52 @@ const GroupModel = MenuItemModel.extend({
|
|||
!this.group.uuid.equals(this.file.db.meta.entryTemplatesGroup))) &&
|
||||
(!filter || !filter.autoType || this.group.enableAutoType !== false)
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
getOwnSubGroups() {
|
||||
return this.group.groups;
|
||||
},
|
||||
}
|
||||
|
||||
addEntry(entry) {
|
||||
this.get('entries').add(entry);
|
||||
},
|
||||
this.entries.push(entry);
|
||||
}
|
||||
|
||||
addGroup(group) {
|
||||
this.get('items').add(group);
|
||||
},
|
||||
this.items.push(group);
|
||||
}
|
||||
|
||||
setName(name) {
|
||||
this._groupModified();
|
||||
this.group.name = name;
|
||||
this._fillByGroup();
|
||||
},
|
||||
}
|
||||
|
||||
setIcon(iconId) {
|
||||
this._groupModified();
|
||||
this.group.icon = iconId;
|
||||
this.group.customIcon = undefined;
|
||||
this._fillByGroup();
|
||||
},
|
||||
}
|
||||
|
||||
setCustomIcon(customIconId) {
|
||||
this._groupModified();
|
||||
this.group.customIcon = new kdbxweb.KdbxUuid(customIconId);
|
||||
this._fillByGroup();
|
||||
},
|
||||
}
|
||||
|
||||
setExpanded(expanded) {
|
||||
// this._groupModified(); // it's not good to mark the file as modified when a group is collapsed
|
||||
this.group.expanded = expanded;
|
||||
this.set('expanded', expanded);
|
||||
},
|
||||
this.expanded = expanded;
|
||||
}
|
||||
|
||||
setEnableSearching(enabled) {
|
||||
this._groupModified();
|
||||
let parentEnableSearching = true;
|
||||
let parentGroup = this.parentGroup;
|
||||
while (parentGroup) {
|
||||
if (typeof parentGroup.get('enableSearching') === 'boolean') {
|
||||
parentEnableSearching = parentGroup.get('enableSearching');
|
||||
if (typeof parentGroup.enableSearching === 'boolean') {
|
||||
parentEnableSearching = parentGroup.enableSearching;
|
||||
break;
|
||||
}
|
||||
parentGroup = parentGroup.parentGroup;
|
||||
|
@ -192,27 +179,27 @@ const GroupModel = MenuItemModel.extend({
|
|||
enabled = null;
|
||||
}
|
||||
this.group.enableSearching = enabled;
|
||||
this.set('enableSearching', this.group.enableSearching);
|
||||
},
|
||||
this.enableSearching = this.group.enableSearching;
|
||||
}
|
||||
|
||||
getEffectiveEnableSearching() {
|
||||
let grp = this;
|
||||
while (grp) {
|
||||
if (typeof grp.get('enableSearching') === 'boolean') {
|
||||
return grp.get('enableSearching');
|
||||
if (typeof grp.enableSearching === 'boolean') {
|
||||
return grp.enableSearching;
|
||||
}
|
||||
grp = grp.parentGroup;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
}
|
||||
|
||||
setEnableAutoType(enabled) {
|
||||
this._groupModified();
|
||||
let parentEnableAutoType = true;
|
||||
let parentGroup = this.parentGroup;
|
||||
while (parentGroup) {
|
||||
if (typeof parentGroup.get('enableAutoType') === 'boolean') {
|
||||
parentEnableAutoType = parentGroup.get('enableAutoType');
|
||||
if (typeof parentGroup.enableAutoType === 'boolean') {
|
||||
parentEnableAutoType = parentGroup.enableAutoType;
|
||||
break;
|
||||
}
|
||||
parentGroup = parentGroup.parentGroup;
|
||||
|
@ -221,46 +208,46 @@ const GroupModel = MenuItemModel.extend({
|
|||
enabled = null;
|
||||
}
|
||||
this.group.enableAutoType = enabled;
|
||||
this.set('enableAutoType', this.group.enableAutoType);
|
||||
},
|
||||
this.enableAutoType = this.group.enableAutoType;
|
||||
}
|
||||
|
||||
getEffectiveEnableAutoType() {
|
||||
let grp = this;
|
||||
while (grp) {
|
||||
if (typeof grp.get('enableAutoType') === 'boolean') {
|
||||
return grp.get('enableAutoType');
|
||||
if (typeof grp.enableAutoType === 'boolean') {
|
||||
return grp.enableAutoType;
|
||||
}
|
||||
grp = grp.parentGroup;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
}
|
||||
|
||||
setAutoTypeSeq(seq) {
|
||||
this._groupModified();
|
||||
this.group.defaultAutoTypeSeq = seq || undefined;
|
||||
this.set('autoTypeSeq', this.group.defaultAutoTypeSeq);
|
||||
},
|
||||
this.autoTypeSeq = this.group.defaultAutoTypeSeq;
|
||||
}
|
||||
|
||||
getEffectiveAutoTypeSeq() {
|
||||
let grp = this;
|
||||
while (grp) {
|
||||
if (grp.get('autoTypeSeq')) {
|
||||
return grp.get('autoTypeSeq');
|
||||
if (grp.autoTypeSeq) {
|
||||
return grp.autoTypeSeq;
|
||||
}
|
||||
grp = grp.parentGroup;
|
||||
}
|
||||
return DefaultAutoTypeSequence;
|
||||
},
|
||||
}
|
||||
|
||||
getParentEffectiveAutoTypeSeq() {
|
||||
return this.parentGroup
|
||||
? this.parentGroup.getEffectiveAutoTypeSeq()
|
||||
: DefaultAutoTypeSequence;
|
||||
},
|
||||
}
|
||||
|
||||
isEntryTemplatesGroup() {
|
||||
return this.group.uuid.equals(this.file.db.meta.entryTemplatesGroup);
|
||||
},
|
||||
}
|
||||
|
||||
moveToTrash() {
|
||||
this.file.setModified();
|
||||
|
@ -269,12 +256,12 @@ const GroupModel = MenuItemModel.extend({
|
|||
this.file.db.meta.entryTemplatesGroup = undefined;
|
||||
}
|
||||
this.file.reload();
|
||||
},
|
||||
}
|
||||
|
||||
deleteFromTrash() {
|
||||
this.file.db.move(this.group, null);
|
||||
this.file.reload();
|
||||
},
|
||||
}
|
||||
|
||||
removeWithoutHistory() {
|
||||
const ix = this.parentGroup.group.groups.indexOf(this.group);
|
||||
|
@ -282,7 +269,7 @@ const GroupModel = MenuItemModel.extend({
|
|||
this.parentGroup.group.groups.splice(ix, 1);
|
||||
}
|
||||
this.file.reload();
|
||||
},
|
||||
}
|
||||
|
||||
moveHere(object) {
|
||||
if (!object || object.id === this.id || object.file !== this.file) {
|
||||
|
@ -307,7 +294,7 @@ const GroupModel = MenuItemModel.extend({
|
|||
this.file.db.move(object.entry, this.group);
|
||||
this.file.reload();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
moveToTop(object) {
|
||||
if (
|
||||
|
@ -334,24 +321,37 @@ const GroupModel = MenuItemModel.extend({
|
|||
}
|
||||
this.file.reload();
|
||||
}
|
||||
|
||||
static fromGroup(group, file, parentGroup) {
|
||||
const model = new GroupModel();
|
||||
model.setGroup(group, file, parentGroup);
|
||||
return model;
|
||||
}
|
||||
|
||||
static newGroup(group, file) {
|
||||
const model = new GroupModel();
|
||||
const grp = file.db.createGroup(group.group);
|
||||
model.setGroup(grp, file, group);
|
||||
model.group.times.update();
|
||||
model.isJustCreated = true;
|
||||
group.addGroup(model);
|
||||
file.setModified();
|
||||
file.reload();
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
||||
GroupModel.defineModelProperties({
|
||||
iconId: 0,
|
||||
entries: null,
|
||||
filterKey: 'group',
|
||||
editable: true,
|
||||
top: false,
|
||||
drag: true,
|
||||
drop: true,
|
||||
enableSearching: true,
|
||||
enableAutoType: null,
|
||||
autoTypeSeq: null
|
||||
});
|
||||
|
||||
GroupModel.fromGroup = function(group, file, parentGroup) {
|
||||
const model = new GroupModel();
|
||||
model.setGroup(group, file, parentGroup);
|
||||
return model;
|
||||
};
|
||||
|
||||
GroupModel.newGroup = function(group, file) {
|
||||
const model = new GroupModel();
|
||||
const grp = file.db.createGroup(group.group);
|
||||
model.setGroup(grp, file, group);
|
||||
model.group.times.update();
|
||||
model.isJustCreated = true;
|
||||
group.addGroup(model);
|
||||
file.setModified();
|
||||
file.reload();
|
||||
return model;
|
||||
};
|
||||
|
||||
export { GroupModel };
|
||||
|
|
|
@ -1,19 +1,15 @@
|
|||
import { GroupCollection } from 'collections/group-collection';
|
||||
import { MenuSectionModel } from 'models/menu/menu-section-model';
|
||||
|
||||
const GroupsMenuModel = MenuSectionModel.extend({
|
||||
defaults: Object.assign({}, MenuSectionModel.prototype.defaults, {
|
||||
scrollable: true,
|
||||
grow: true
|
||||
}),
|
||||
|
||||
initialize() {
|
||||
this.set('items', new GroupCollection());
|
||||
},
|
||||
|
||||
_loadItemCollectionType() {
|
||||