Skip to content

Commit

Permalink
Merge pull request #339 from buttercup/feat/tags
Browse files Browse the repository at this point in the history
Tags
  • Loading branch information
perry-mitchell committed Jan 29, 2024
2 parents 52403eb + 07ee57a commit b6c627f
Show file tree
Hide file tree
Showing 13 changed files with 343 additions and 25 deletions.
54 changes: 52 additions & 2 deletions source/core/Entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { generateUUID } from "../tools/uuid.js";
import { getEntryURLs, EntryURLType } from "../tools/entry.js";
import { Group } from "./Group.js";
import { Vault } from "./Vault.js";
import { isValidTag } from "../tools/tag.js";
import {
EntryChange,
EntryPropertyValueType,
Expand All @@ -20,7 +21,8 @@ export class Entry extends VaultItem {
static Attributes = Object.freeze({
AttachmentPrefix: "BC_ENTRY_ATTACHMENT:",
FacadeType: "BC_ENTRY_FACADE_TYPE",
FieldTypePrefix: "BC_ENTRY_FIELD_TYPE:"
FieldTypePrefix: "BC_ENTRY_FIELD_TYPE:",
Tags: "BC_ENTRY_TAGS"
});

/**
Expand All @@ -47,6 +49,24 @@ export class Entry extends VaultItem {
return vault.findEntryByID(id);
}

/**
* Add one or more tags to the entry
* @param tags A collection of tag strings
* @returns Self
*/
addTags(...tags: Array<string>): this {
const current = new Set(this.getTags());
for (const tag of tags) {
const tagLower = tag.toLowerCase();
if (!isValidTag(tagLower)) {
throw new Error(`Cannot add entry tag: Invalid format: ${tagLower}`);
}
current.add(tagLower);
}
this.setAttribute(Entry.Attributes.Tags, [...current].join(","));
return this;
}

/**
* Delete the entry - either trashes the entry, or removes it completely.
* If the entry is in the trash already, it is removed (including if there is no
Expand Down Expand Up @@ -108,6 +128,8 @@ export class Entry extends VaultItem {
* containing all attribute keys and their values if no attribute name
* is provided
*/
getAttribute(): PropertyKeyValueObject;
getAttribute(attribute: string): string | undefined;
getAttribute(attribute?: string): PropertyKeyValueObject | string | undefined {
const attributes = this.vault.format.getEntryAttributes(this._source) || {};
if (typeof attribute === "undefined") {
Expand Down Expand Up @@ -189,9 +211,22 @@ export class Entry extends VaultItem {
}, {});
}

/**
* Get entry tags
* @returns An array of tag strings
*/
getTags(): Array<string> {
const tags = this.getAttribute(Entry.Attributes.Tags);
return typeof tags === "string"
? tags.split(",").reduce((output, tag) => {
return isValidTag(tag) ? [...output, tag] : output;
}, [])
: [];
}

/**
* Get the entry type
* @returns
* @returns The entry type
*/
getType(): EntryType {
return (
Expand Down Expand Up @@ -233,6 +268,21 @@ export class Entry extends VaultItem {
return this;
}

/**
* Remove one or more tags
* @param tags Collection of tags to remove
* @returns Self
*/
removeTags(...tags: Array<string>): this {
const current = new Set(this.getTags());
for (const tag of tags) {
const tagLower = tag.toLowerCase();
current.delete(tagLower);
}
this.setAttribute(Entry.Attributes.Tags, [...current].join(","));
return this;
}

/**
* Set an attribute on the entry
* @param attribute The name of the attribute
Expand Down
53 changes: 53 additions & 0 deletions source/core/Vault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ export class Vault extends EventEmitter {

_onCommandExec: () => void;

_tagMap: Map<string, Array<EntryID>> = new Map();

/**
* The vault format
* @readonly
Expand Down Expand Up @@ -171,6 +173,31 @@ export class Vault extends EventEmitter {
return findEntriesByProperty(this._entries, property, value);
}

/**
* Find entries by a certain tag
* @param tag The case-insensitive tag name
* @param exact Whether to match exact tag names or use partial
* matching. Default is true (exact).
* @returns An array of entries
*/
findEntriesByTag(tag: string, exact: boolean = true): Array<Entry> {
const tagLower = tag.toLowerCase();
if (!exact) {
const entryIDs = new Set<string>();
for (const [currentTag, currentIDs] of this._tagMap.entries()) {
if (currentTag.toLowerCase().indexOf(tagLower) === 0) {
for (const id of currentIDs) {
entryIDs.add(id);
}
}
}
return [...entryIDs].map((id) => this.findEntryByID(id));
}
const entryIDs = this._tagMap.has(tagLower) ? this._tagMap.get(tagLower) : [];
console.log("SEARCHING", entryIDs, this._tagMap);
return entryIDs.map((id) => this.findEntryByID(id));
}

/**
* Find a group by its ID
* @param id The group ID to search for
Expand Down Expand Up @@ -209,6 +236,14 @@ export class Vault extends EventEmitter {
return [...this._groups];
}

/**
* Get all registered entry tags
* @returns An array of tag strings
*/
getAllTags(): Array<string> {
return [...this._tagMap.keys()];
}

/**
* Get the value of an attribute
* @param attributeName The attribute to get
Expand Down Expand Up @@ -275,6 +310,24 @@ export class Vault extends EventEmitter {
this._entries.push(new Entry(this, rawEntry));
}
});
this._rebuildTags();
}

_rebuildTags() {
this._tagMap = new Map();
this.getAllEntries().forEach((entry) => {
const tags = entry.getTags();
for (const tag of tags) {
const tagLower = tag.toLowerCase();
const existingIDs = this._tagMap.has(tagLower)
? [...this._tagMap.get(tagLower)]
: [];
if (!existingIDs.includes(entry.id)) {
existingIDs.push(entry.id);
}
this._tagMap.set(tagLower, existingIDs);
}
});
}

/**
Expand Down
18 changes: 17 additions & 1 deletion source/facades/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,22 @@ export function createEntryFromFacade(group: Group, facade: EntryFacade): Entry
return entry;
}

/**
* Convert an array of entry facade fields to a
* key-value object with only attributes
* @param facadeFields Array of fields
* @memberof module:Buttercup
*/
export function fieldsToAttributes(facadeFields: Array<EntryFacadeField>): {
[key: string]: string;
} {
return facadeFields.reduce((output, field) => {
if (field.propertyType !== EntryPropertyType.Attribute) return output;
output[field.property] = field.value;
return output;
}, {});
}

/**
* Convert an array of entry facade fields to a
* key-value object with only properties
Expand All @@ -207,7 +223,7 @@ export function fieldsToProperties(facadeFields: Array<EntryFacadeField>): {
[key: string]: string;
} {
return facadeFields.reduce((output, field) => {
if (field.propertyType !== "property") return output;
if (field.propertyType !== EntryPropertyType.Property) return output;
output[field.property] = field.value;
return output;
}, {});
Expand Down
21 changes: 21 additions & 0 deletions source/io/VaultFormatB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export class VaultFormatB extends VaultFormat {
this.cloneGroup(childGroup, newGroup.id);
}
});
this.emit("commandsExecuted");
}

createEntry(groupID: GroupID, entryID: EntryID) {
Expand All @@ -140,6 +141,7 @@ export class VaultFormatB extends VaultFormat {
a: {}
};
this.source.e.push(entry);
this.emit("commandsExecuted");
}

createGroup(parentID: GroupID, groupID: GroupID) {
Expand All @@ -150,45 +152,52 @@ export class VaultFormatB extends VaultFormat {
a: {}
};
this.source.g.push(group);
this.emit("commandsExecuted");
}

deleteEntry(entryID: EntryID) {
const ind = this.source.e.findIndex((entry) => entry.id === entryID);
if (ind >= 0) {
this.source.e.splice(ind, 1);
this.source.del.e[entryID] = Date.now();
this.emit("commandsExecuted");
}
}

deleteEntryAttribute(entryID: EntryID, attribute: string) {
const entry = this.source.e.find((e: FormatBEntry) => e.id === entryID);
if (!entry.a[attribute]) return;
entry.a[attribute].deleted = getTimestamp();
this.emit("commandsExecuted");
}

deleteEntryProperty(entryID: EntryID, property: string) {
const entry = this.source.e.find((e) => e.id === entryID);
if (!entry.p[property]) return;
entry.p[property].deleted = getTimestamp();
this.emit("commandsExecuted");
}

deleteGroup(groupID: GroupID) {
const ind = this.source.g.findIndex((group) => group.id === groupID);
if (ind >= 0) {
this.source.g.splice(ind, 1);
this.source.del.g[groupID] = Date.now();
this.emit("commandsExecuted");
}
}

deleteGroupAttribute(groupID: GroupID, attribute: string) {
const group = this.source.g.find((g) => g.id === groupID);
if (!group.a[attribute]) return;
group.a[attribute].deleted = getTimestamp();
this.emit("commandsExecuted");
}

deleteVaultAttribute(attribute: string) {
if (!this.source.a[attribute]) return;
this.source.a[attribute].deleted = getTimestamp();
this.emit("commandsExecuted");
}

erase() {
Expand Down Expand Up @@ -239,6 +248,7 @@ export class VaultFormatB extends VaultFormat {

generateID() {
this.source.id = generateUUID();
this.emit("commandsExecuted");
}

getAllEntries(parentID: GroupID = null): Array<FormatBEntry> {
Expand Down Expand Up @@ -338,18 +348,23 @@ export class VaultFormatB extends VaultFormat {
this.source.del.g = {};
}
if (!this.source.id) {
// Emits "commandsExecuted"
this.generateID();
} else {
this.emit("commandsExecuted");
}
}

moveEntry(entryID: EntryID, groupID: GroupID) {
const entry = this.source.e.find((e: FormatBEntry) => e.id === entryID);
entry.g = groupID;
this.emit("commandsExecuted");
}

moveGroup(groupID: GroupID, newParentID: GroupID) {
const group = this.source.g.find((g: FormatBGroup) => g.id === groupID);
group.g = newParentID;
this.emit("commandsExecuted");
}

optimise() {
Expand Down Expand Up @@ -393,6 +408,7 @@ export class VaultFormatB extends VaultFormat {
delete this.source.del.g[groupID];
}
}
this.emit("commandsExecuted");
}

prepareOrphansGroup(): FormatBGroup {
Expand All @@ -419,6 +435,7 @@ export class VaultFormatB extends VaultFormat {
item.value = value;
item.updated = getTimestamp();
}
this.emit("commandsExecuted");
}

setEntryProperty(entryID: EntryID, property: string, value: string) {
Expand All @@ -431,6 +448,7 @@ export class VaultFormatB extends VaultFormat {
item.value = value;
item.updated = getTimestamp();
}
this.emit("commandsExecuted");
}

setGroupAttribute(groupID: GroupID, attribute: string, value: string) {
Expand All @@ -443,11 +461,13 @@ export class VaultFormatB extends VaultFormat {
item.value = value;
item.updated = getTimestamp();
}
this.emit("commandsExecuted");
}

setGroupTitle(groupID: GroupID, title: string) {
const group = this.source.g.find((g: FormatBGroup) => g.id === groupID);
group.t = title;
this.emit("commandsExecuted");
}

setVaultAttribute(key: string, value: string) {
Expand All @@ -459,5 +479,6 @@ export class VaultFormatB extends VaultFormat {
item.value = value;
item.updated = getTimestamp();
}
this.emit("commandsExecuted");
}
}
Loading

0 comments on commit b6c627f

Please sign in to comment.