forked from trent_larson/crowd-funder-for-time-pwa
add labels for contacts (as a way to group them)
This commit is contained in:
@@ -842,7 +842,8 @@ export const NOTIFY_EXPORT_DATA_PROMPT = {
|
|||||||
// Used in: ContactsView.vue (showCopySelectionsInfo method - info about copying contacts)
|
// Used in: ContactsView.vue (showCopySelectionsInfo method - info about copying contacts)
|
||||||
export const NOTIFY_CONTACT_INFO_COPY = {
|
export const NOTIFY_CONTACT_INFO_COPY = {
|
||||||
title: "Info",
|
title: "Info",
|
||||||
message: "Contact info will include name, ID, profile image, and public key.",
|
message:
|
||||||
|
"Copied contact info will include name, ID, profile image, and public key.",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Used in: ContactsView.vue (copySelectedContacts method - no contacts selected error)
|
// Used in: ContactsView.vue (copySelectedContacts method - no contacts selected error)
|
||||||
|
|||||||
@@ -199,6 +199,20 @@ const MIGRATIONS = [
|
|||||||
ALTER TABLE settings ADD COLUMN lastAckedStarredPlanChangesJwtId TEXT;
|
ALTER TABLE settings ADD COLUMN lastAckedStarredPlanChangesJwtId TEXT;
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "006_add_labels_for_contacts",
|
||||||
|
sql: `
|
||||||
|
-- Create mapping table for contact labels
|
||||||
|
CREATE TABLE contact_labels (
|
||||||
|
did TEXT NOT NULL,
|
||||||
|
label TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (did, label),
|
||||||
|
FOREIGN KEY (did) REFERENCES contacts(did) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_contact_labels_label ON contact_labels(label);
|
||||||
|
CREATE INDEX idx_contact_labels_did ON contact_labels(did);
|
||||||
|
`,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
4
src/db/tables/contactLabels.ts
Normal file
4
src/db/tables/contactLabels.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export type ContactLabel = {
|
||||||
|
did: string;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
@@ -1496,6 +1496,126 @@ export const PlatformServiceMixin = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get labels for a specific contact - $getContactLabels()
|
||||||
|
* @param did Contact DID
|
||||||
|
* @returns Promise<string[]> Array of labels
|
||||||
|
*/
|
||||||
|
async $getContactLabels(did: string): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const results = (await this.$dbQuery(
|
||||||
|
"SELECT label FROM contact_labels WHERE did = ? ORDER BY label",
|
||||||
|
[did],
|
||||||
|
)) as QueryExecResult;
|
||||||
|
return results.values.map((r: SqlValue[]) => r[0] as string);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`[PlatformServiceMixin] Error getting labels for contact ${did}:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get contact IDs with all labels
|
||||||
|
* @param labels Array of labels
|
||||||
|
* @returns Promise<string[]> Array of contacts with all labels
|
||||||
|
*/
|
||||||
|
async $getContactIdsWithAllLabels(labels: string[]): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const numberOfLabels = labels.length;
|
||||||
|
const questionsForInput = labels.map(() => `?`).join(", ");
|
||||||
|
const results = (await this.$dbQuery(
|
||||||
|
`SELECT did FROM contact_labels WHERE label IN (${questionsForInput})`,
|
||||||
|
[...labels],
|
||||||
|
)) as QueryExecResult;
|
||||||
|
// count the occurrences of each did
|
||||||
|
const didCounts: Record<string, number> = results.values?.reduce(
|
||||||
|
(acc: Record<string, number>, curr: SqlValue[]) => {
|
||||||
|
acc[curr[0] as unknown as string] =
|
||||||
|
(acc[curr[0] as unknown as string] || 0) + 1;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
// filter out the dids that do not occur for as many labels as there are labels
|
||||||
|
const contactIdsWithAllLabels = Object.keys(didCounts).filter(
|
||||||
|
(did: string) => didCounts[did] === numberOfLabels,
|
||||||
|
);
|
||||||
|
return contactIdsWithAllLabels;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`[PlatformServiceMixin] Error getting contact IDs with all labels ${labels}:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a label to a contact - $addContactLabel()
|
||||||
|
* @param did Contact DID
|
||||||
|
* @param label Label string
|
||||||
|
* @returns Promise<boolean> Success status
|
||||||
|
*/
|
||||||
|
async $addContactLabel(did: string, label: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await this.$dbExec(
|
||||||
|
"INSERT OR IGNORE INTO contact_labels (did, label) VALUES (?, ?)",
|
||||||
|
[did, label],
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`[PlatformServiceMixin] Error adding label ${label} to contact ${did}:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a label from a contact - $deleteContactLabel()
|
||||||
|
* @param did Contact DID
|
||||||
|
* @param label Label string
|
||||||
|
* @returns Promise<boolean> Success status
|
||||||
|
*/
|
||||||
|
async $deleteContactLabel(did: string, label: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await this.$dbExec(
|
||||||
|
"DELETE FROM contact_labels WHERE did = ? AND label = ?",
|
||||||
|
[did, label],
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`[PlatformServiceMixin] Error deleting label ${label} from contact ${did}:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all unique labels available - $getUniqueLabels()
|
||||||
|
* @returns Promise<string[]> Array of unique labels
|
||||||
|
*/
|
||||||
|
async $getUniqueLabels(): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const results = (await this.$dbQuery(
|
||||||
|
"SELECT DISTINCT label FROM contact_labels ORDER BY label",
|
||||||
|
)) as QueryExecResult;
|
||||||
|
return results.values?.map((r: SqlValue[]) => r[0] as string) || [];
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
"[PlatformServiceMixin] Error getting unique labels:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all accounts - $getAllAccounts()
|
* Get all accounts - $getAllAccounts()
|
||||||
* Retrieves all account metadata from the accounts table
|
* Retrieves all account metadata from the accounts table
|
||||||
@@ -1965,6 +2085,11 @@ export interface IPlatformServiceMixin {
|
|||||||
$getContact(did: string): Promise<Contact | null>;
|
$getContact(did: string): Promise<Contact | null>;
|
||||||
$deleteContact(did: string): Promise<boolean>;
|
$deleteContact(did: string): Promise<boolean>;
|
||||||
$contactCount(): Promise<number>;
|
$contactCount(): Promise<number>;
|
||||||
|
$getContactLabels(did: string): Promise<string[]>;
|
||||||
|
$getContactIdsWithAllLabels(labels: string[]): Promise<string[]>;
|
||||||
|
$addContactLabel(did: string, label: string): Promise<boolean>;
|
||||||
|
$deleteContactLabel(did: string, label: string): Promise<boolean>;
|
||||||
|
$getUniqueLabels(): Promise<string[]>;
|
||||||
$getAllAccounts(): Promise<Account[]>;
|
$getAllAccounts(): Promise<Account[]>;
|
||||||
$getAllAccountDids(): Promise<string[]>;
|
$getAllAccountDids(): Promise<string[]>;
|
||||||
$insertEntity(
|
$insertEntity(
|
||||||
@@ -2074,10 +2199,7 @@ declare module "@vue/runtime-core" {
|
|||||||
$withTransaction<T>(fn: () => Promise<T>): Promise<T>;
|
$withTransaction<T>(fn: () => Promise<T>): Promise<T>;
|
||||||
$getAvailableAccountDids(): Promise<string[]>;
|
$getAvailableAccountDids(): Promise<string[]>;
|
||||||
|
|
||||||
// Specialized shortcuts - contacts cached, settings fresh
|
// Specialized shortcuts - settings fresh
|
||||||
$contacts(): Promise<Contact[]>;
|
|
||||||
$contactsByDateAdded(): Promise<Contact[]>;
|
|
||||||
$contactCount(): Promise<number>;
|
|
||||||
$settings(defaults?: Settings): Promise<Settings>;
|
$settings(defaults?: Settings): Promise<Settings>;
|
||||||
$accountSettings(did?: string, defaults?: Settings): Promise<Settings>;
|
$accountSettings(did?: string, defaults?: Settings): Promise<Settings>;
|
||||||
$normalizeContacts(rawContacts: ContactMaybeWithJsonStrings[]): Contact[];
|
$normalizeContacts(rawContacts: ContactMaybeWithJsonStrings[]): Contact[];
|
||||||
@@ -2091,6 +2213,9 @@ declare module "@vue/runtime-core" {
|
|||||||
// @deprecated; see implementation note above
|
// @deprecated; see implementation note above
|
||||||
// $saveMySettings(changes: Partial<Settings>): Promise<boolean>;
|
// $saveMySettings(changes: Partial<Settings>): Promise<boolean>;
|
||||||
|
|
||||||
|
$contacts(): Promise<Contact[]>;
|
||||||
|
$contactsByDateAdded(): Promise<Contact[]>;
|
||||||
|
|
||||||
// Cache management methods
|
// Cache management methods
|
||||||
$refreshSettings(): Promise<Settings>;
|
$refreshSettings(): Promise<Settings>;
|
||||||
$refreshContacts(): Promise<Contact[]>;
|
$refreshContacts(): Promise<Contact[]>;
|
||||||
@@ -2106,6 +2231,12 @@ declare module "@vue/runtime-core" {
|
|||||||
$getAllContacts(): Promise<Contact[]>;
|
$getAllContacts(): Promise<Contact[]>;
|
||||||
$getContact(did: string): Promise<Contact | null>;
|
$getContact(did: string): Promise<Contact | null>;
|
||||||
$deleteContact(did: string): Promise<boolean>;
|
$deleteContact(did: string): Promise<boolean>;
|
||||||
|
$contactCount(): Promise<number>;
|
||||||
|
$getContactLabels(did: string): Promise<string[]>;
|
||||||
|
$getContactIdsWithAllLabels(labels: string[]): Promise<string[]>;
|
||||||
|
$addContactLabel(did: string, label: string): Promise<boolean>;
|
||||||
|
$deleteContactLabel(did: string, label: string): Promise<boolean>;
|
||||||
|
$getUniqueLabels(): Promise<string[]>;
|
||||||
$getAllAccounts(): Promise<Account[]>;
|
$getAllAccounts(): Promise<Account[]>;
|
||||||
$getAllAccountDids(): Promise<string[]>;
|
$getAllAccountDids(): Promise<string[]>;
|
||||||
$insertEntity(
|
$insertEntity(
|
||||||
|
|||||||
@@ -126,6 +126,67 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Contact Labels -->
|
||||||
|
<div class="mt-8 border-t pt-4">
|
||||||
|
<label class="block text-sm font-medium text-gray-700"> Labels </label>
|
||||||
|
<div class="flex flex-wrap gap-2 mt-2">
|
||||||
|
<span
|
||||||
|
v-for="label in contactLabels"
|
||||||
|
:key="label"
|
||||||
|
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex-shrink-0 ml-1.5 inline-flex text-blue-400 hover:text-blue-600 focus:outline-none"
|
||||||
|
@click="removeLabel(label)"
|
||||||
|
>
|
||||||
|
<font-awesome icon="xmark" class="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="contactLabels.length === 0"
|
||||||
|
class="text-sm text-gray-400 italic"
|
||||||
|
>
|
||||||
|
No labels attached
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 flex gap-2">
|
||||||
|
<input
|
||||||
|
v-model="newLabel"
|
||||||
|
type="text"
|
||||||
|
placeholder="Add a label..."
|
||||||
|
class="block w-full border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm p-2"
|
||||||
|
@keyup.enter="addLabel"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="px-4 py-1 bg-green-500 text-white rounded-md text-sm"
|
||||||
|
@click="addLabel"
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- All Available Labels -->
|
||||||
|
<div v-if="availableLabels.length > 0" class="mt-4">
|
||||||
|
<p class="text-sm font-medium text-gray-700 mb-2">Available labels:</p>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<button
|
||||||
|
v-for="label in availableLabels"
|
||||||
|
:key="label"
|
||||||
|
class="inline-flex items-center px-3 py-1 rounded text-sm font-medium transition-colors"
|
||||||
|
:class="
|
||||||
|
contactLabels.includes(label)
|
||||||
|
? 'bg-blue-500 text-white hover:bg-blue-600'
|
||||||
|
: 'bg-gray-100 text-gray-800 hover:bg-gray-200'
|
||||||
|
"
|
||||||
|
@click="toggleLabel(label)"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Save Button -->
|
<!-- Save Button -->
|
||||||
<div class="mt-8 flex justify-between">
|
<div class="mt-8 flex justify-between">
|
||||||
<button
|
<button
|
||||||
@@ -224,12 +285,36 @@ export default class ContactEditView extends Vue {
|
|||||||
contactNotes = "";
|
contactNotes = "";
|
||||||
/** Array of editable contact methods */
|
/** Array of editable contact methods */
|
||||||
contactMethods: Array<ContactMethod> = [];
|
contactMethods: Array<ContactMethod> = [];
|
||||||
|
/** Array of contact labels */
|
||||||
|
contactLabels: string[] = [];
|
||||||
|
/** Labels before editing to track changes */
|
||||||
|
originalLabels: string[] = [];
|
||||||
|
/** New label input field */
|
||||||
|
newLabel = "";
|
||||||
|
/** All unique labels from other contacts for suggestions */
|
||||||
|
allUniqueLabels: string[] = [];
|
||||||
|
|
||||||
/** App string constants */
|
/** App string constants */
|
||||||
AppString = AppString;
|
AppString = AppString;
|
||||||
/** Contact method types for datalist suggestions */
|
/** Contact method types for datalist suggestions */
|
||||||
contactMethodTypes = CONTACT_METHOD_TYPES;
|
contactMethodTypes = CONTACT_METHOD_TYPES;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter unique labels that are not already attached to this contact
|
||||||
|
*/
|
||||||
|
get suggestedLabels() {
|
||||||
|
return this.allUniqueLabels.filter(
|
||||||
|
(label) => !this.contactLabels.includes(label),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all available labels for selection
|
||||||
|
*/
|
||||||
|
get availableLabels() {
|
||||||
|
return this.allUniqueLabels;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component lifecycle hook that initializes the contact edit form
|
* Component lifecycle hook that initializes the contact edit form
|
||||||
*
|
*
|
||||||
@@ -254,6 +339,14 @@ export default class ContactEditView extends Vue {
|
|||||||
this.contactName = contact.name || "";
|
this.contactName = contact.name || "";
|
||||||
this.contactNotes = contact.notes || "";
|
this.contactNotes = contact.notes || "";
|
||||||
this.contactMethods = contact.contactMethods || [];
|
this.contactMethods = contact.contactMethods || [];
|
||||||
|
|
||||||
|
// Load labels
|
||||||
|
const labels = await this.$getContactLabels(contactDid);
|
||||||
|
this.contactLabels = labels;
|
||||||
|
this.originalLabels = [...labels];
|
||||||
|
|
||||||
|
// Load all labels for suggestions
|
||||||
|
this.allUniqueLabels = await this.$getUniqueLabels();
|
||||||
} else {
|
} else {
|
||||||
this.notify.error(
|
this.notify.error(
|
||||||
`${NOTIFY_CONTACT_NOT_FOUND.message} ${contactDid}`,
|
`${NOTIFY_CONTACT_NOT_FOUND.message} ${contactDid}`,
|
||||||
@@ -285,6 +378,50 @@ export default class ContactEditView extends Vue {
|
|||||||
this.contactMethods.splice(index, 1);
|
this.contactMethods.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new label to the contact labels array
|
||||||
|
*/
|
||||||
|
addLabel() {
|
||||||
|
const label = this.newLabel.trim();
|
||||||
|
if (label && !this.contactLabels.includes(label)) {
|
||||||
|
this.contactLabels.push(label);
|
||||||
|
this.contactLabels.sort();
|
||||||
|
this.newLabel = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a suggested label to the contact
|
||||||
|
* @param label The label to add
|
||||||
|
*/
|
||||||
|
addSuggestedLabel(label: string) {
|
||||||
|
if (!this.contactLabels.includes(label)) {
|
||||||
|
this.contactLabels.push(label);
|
||||||
|
this.contactLabels.sort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a label from the contact labels array
|
||||||
|
* @param label The label to remove
|
||||||
|
*/
|
||||||
|
removeLabel(label: string) {
|
||||||
|
this.contactLabels = this.contactLabels.filter((l) => l !== label);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles a label on or off for the contact
|
||||||
|
* @param label The label to toggle
|
||||||
|
*/
|
||||||
|
toggleLabel(label: string) {
|
||||||
|
if (this.contactLabels.includes(label)) {
|
||||||
|
this.removeLabel(label);
|
||||||
|
} else {
|
||||||
|
this.contactLabels.push(label);
|
||||||
|
this.contactLabels.sort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the edited contact information to the database
|
* Saves the edited contact information to the database
|
||||||
*
|
*
|
||||||
@@ -320,13 +457,29 @@ export default class ContactEditView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save to database via PlatformServiceMixin
|
// Save to database via PlatformServiceMixin
|
||||||
// Normalize empty strings to null to preserve database consistency
|
// Normalize empty strings to undefined to preserve database consistency
|
||||||
await this.$updateContact(this.contact?.did || "", {
|
await this.$updateContact(this.contact?.did || "", {
|
||||||
name: this.contactName?.trim() || null,
|
name: this.contactName?.trim() || undefined,
|
||||||
notes: this.contactNotes?.trim() || null,
|
notes: this.contactNotes?.trim() || undefined,
|
||||||
contactMethods: contactMethods,
|
contactMethods: contactMethods,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Save labels
|
||||||
|
const contactDid = this.contact?.did || "";
|
||||||
|
const labelsToAdd = this.contactLabels.filter(
|
||||||
|
(l) => !this.originalLabels.includes(l),
|
||||||
|
);
|
||||||
|
const labelsToRemove = this.originalLabels.filter(
|
||||||
|
(l) => !this.contactLabels.includes(l),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const label of labelsToAdd) {
|
||||||
|
await this.$addContactLabel(contactDid, label);
|
||||||
|
}
|
||||||
|
for (const label of labelsToRemove) {
|
||||||
|
await this.$deleteContactLabel(contactDid, label);
|
||||||
|
}
|
||||||
|
|
||||||
// Notify success and redirect
|
// Notify success and redirect
|
||||||
this.notify.success(NOTIFY_CONTACT_SAVED.message, TIMEOUTS.STANDARD);
|
this.notify.success(NOTIFY_CONTACT_SAVED.message, TIMEOUTS.STANDARD);
|
||||||
this.$router.back();
|
this.$router.back();
|
||||||
|
|||||||
@@ -78,6 +78,55 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Label Filter -->
|
||||||
|
<div v-if="allLabels.length > 0" class="mt-4 mb-2">
|
||||||
|
<div class="flex items-center justify-between pl-[16.666%] pr-[16.666%]">
|
||||||
|
<button
|
||||||
|
class="text-sm font-medium text-blue-600 flex items-center gap-1"
|
||||||
|
@click="showLabelFilter = !showLabelFilter"
|
||||||
|
>
|
||||||
|
<font-awesome
|
||||||
|
:icon="showLabelFilter ? 'chevron-up' : 'filter'"
|
||||||
|
class="text-xs"
|
||||||
|
/>
|
||||||
|
{{ showLabelFilter ? "Hide Filters" : "Filter by Labels" }}
|
||||||
|
<span
|
||||||
|
v-if="selectedLabels.length > 0"
|
||||||
|
class="bg-blue-600 text-white rounded-full px-1.5 py-0.5 text-[10px]"
|
||||||
|
>
|
||||||
|
{{ selectedLabels.length }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:class="[
|
||||||
|
'text-sm text-red-500 underline',
|
||||||
|
selectedLabels.length === 0 ? 'invisible' : '',
|
||||||
|
]"
|
||||||
|
@click="clearLabelFilters"
|
||||||
|
>
|
||||||
|
Clear all
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="showLabelFilter"
|
||||||
|
class="flex flex-wrap gap-2 mt-3 p-3 bg-gray-50 rounded-lg border border-gray-200"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="label in allLabels"
|
||||||
|
:key="label"
|
||||||
|
class="px-2.5 py-1 rounded-full text-xs font-medium border transition-colors"
|
||||||
|
:class="
|
||||||
|
selectedLabels.includes(label)
|
||||||
|
? 'bg-blue-600 text-white border-blue-600'
|
||||||
|
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-100'
|
||||||
|
"
|
||||||
|
@click="toggleLabel(label)"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Results List -->
|
<!-- Results List -->
|
||||||
<ul
|
<ul
|
||||||
v-if="contacts.length > 0"
|
v-if="contacts.length > 0"
|
||||||
@@ -85,7 +134,7 @@
|
|||||||
class="border-t border-slate-300 my-2"
|
class="border-t border-slate-300 my-2"
|
||||||
>
|
>
|
||||||
<ContactListItem
|
<ContactListItem
|
||||||
v-for="contact in filteredContacts"
|
v-for="contact in contactsFiltered"
|
||||||
:key="contact.did"
|
:key="contact.did"
|
||||||
:contact="contact"
|
:contact="contact"
|
||||||
:active-did="activeDid"
|
:active-did="activeDid"
|
||||||
@@ -268,6 +317,7 @@ export default class ContactsView extends Vue {
|
|||||||
activeDid = "";
|
activeDid = "";
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
contacts: Array<Contact> = [];
|
contacts: Array<Contact> = [];
|
||||||
|
contactsFiltered: Array<Contact> = [];
|
||||||
contactInput = "";
|
contactInput = "";
|
||||||
contactEdit: Contact | null = null;
|
contactEdit: Contact | null = null;
|
||||||
contactNewName = "";
|
contactNewName = "";
|
||||||
@@ -294,6 +344,11 @@ export default class ContactsView extends Vue {
|
|||||||
showGiveConfirmed = true;
|
showGiveConfirmed = true;
|
||||||
showLargeIdenticon?: Contact;
|
showLargeIdenticon?: Contact;
|
||||||
|
|
||||||
|
/** Label management state */
|
||||||
|
selectedLabels: string[] = [];
|
||||||
|
allLabels: string[] = [];
|
||||||
|
showLabelFilter = false;
|
||||||
|
|
||||||
APP_SERVER = APP_SERVER;
|
APP_SERVER = APP_SERVER;
|
||||||
AppString = AppString;
|
AppString = AppString;
|
||||||
libsUtil = libsUtil;
|
libsUtil = libsUtil;
|
||||||
@@ -333,8 +388,9 @@ export default class ContactsView extends Vue {
|
|||||||
this.loadGives();
|
this.loadGives();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace PlatformServiceFactory and manual SQL with mixin method
|
|
||||||
this.contacts = await this.$getAllContacts();
|
this.contacts = await this.$getAllContacts();
|
||||||
|
this.contactsFiltered = await this.filteredContacts();
|
||||||
|
this.allLabels = await this.$getUniqueLabels();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async processContactJwt() {
|
private async processContactJwt() {
|
||||||
@@ -501,14 +557,48 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Computed properties for template streamlining
|
// Computed properties for template streamlining
|
||||||
get filteredContacts() {
|
async filteredContacts() {
|
||||||
return this.showGiveNumbers
|
let filtered = this.showGiveNumbers
|
||||||
? this.contactsSelected.length === 0
|
? this.contactsSelected.length === 0
|
||||||
? this.contacts
|
? this.contacts
|
||||||
: this.contacts.filter((contact) =>
|
: this.contacts.filter((contact) =>
|
||||||
this.contactsSelected.includes(contact.did),
|
this.contactsSelected.includes(contact.did),
|
||||||
)
|
)
|
||||||
: this.contacts;
|
: this.contacts;
|
||||||
|
|
||||||
|
// Apply label filtering if any labels are selected
|
||||||
|
if (this.selectedLabels.length > 0) {
|
||||||
|
const contactIdsWithAllLabels = await this.$getContactIdsWithAllLabels(
|
||||||
|
this.selectedLabels,
|
||||||
|
);
|
||||||
|
filtered = filtered.filter((contact: Contact) =>
|
||||||
|
contactIdsWithAllLabels.includes(contact.did),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle a label in the filter selection
|
||||||
|
* @param label The label to toggle
|
||||||
|
*/
|
||||||
|
async toggleLabel(label: string) {
|
||||||
|
if (this.selectedLabels.includes(label)) {
|
||||||
|
this.selectedLabels = this.selectedLabels.filter((l) => l !== label);
|
||||||
|
} else {
|
||||||
|
this.selectedLabels.push(label);
|
||||||
|
}
|
||||||
|
this.contactsSelected = [];
|
||||||
|
this.contactsFiltered = await this.filteredContacts();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all active label filters
|
||||||
|
*/
|
||||||
|
async clearLabelFilters() {
|
||||||
|
this.selectedLabels = [];
|
||||||
|
this.contactsSelected = [];
|
||||||
|
this.contactsFiltered = await this.filteredContacts();
|
||||||
}
|
}
|
||||||
|
|
||||||
get copyButtonClass() {
|
get copyButtonClass() {
|
||||||
@@ -537,7 +627,7 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get allContactsSelected() {
|
get allContactsSelected() {
|
||||||
return this.contactsSelected.length === this.contacts.length;
|
return this.contactsSelected.length === this.contactsFiltered.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper methods for template interactions
|
// Helper methods for template interactions
|
||||||
@@ -545,7 +635,9 @@ export default class ContactsView extends Vue {
|
|||||||
if (this.allContactsSelected) {
|
if (this.allContactsSelected) {
|
||||||
this.contactsSelected = [];
|
this.contactsSelected = [];
|
||||||
} else {
|
} else {
|
||||||
this.contactsSelected = this.contacts.map((contact) => contact.did);
|
this.contactsSelected = this.contactsFiltered.map(
|
||||||
|
(contact) => contact.did,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -722,6 +814,7 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.contacts = await this.$getAllContacts();
|
this.contacts = await this.$getAllContacts();
|
||||||
|
this.contactsFiltered = await this.filteredContacts();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -839,6 +932,7 @@ export default class ContactsView extends Vue {
|
|||||||
|
|
||||||
// Update local contacts list
|
// Update local contacts list
|
||||||
this.updateContactsList(newContact);
|
this.updateContactsList(newContact);
|
||||||
|
this.contactsFiltered = await this.filteredContacts();
|
||||||
|
|
||||||
// Set visibility and get success message
|
// Set visibility and get success message
|
||||||
const addedMessage = await this.handleContactVisibility(newContact);
|
const addedMessage = await this.handleContactVisibility(newContact);
|
||||||
|
|||||||
@@ -43,6 +43,19 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
<!-- Labels -->
|
||||||
|
<div v-if="contactLabels.length > 0" class="mt-3">
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<span
|
||||||
|
v-for="label in contactLabels"
|
||||||
|
:key="label"
|
||||||
|
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Notes -->
|
<!-- Notes -->
|
||||||
<div v-if="contactFromDid.notes" class="mt-3">
|
<div v-if="contactFromDid.notes" class="mt-3">
|
||||||
<p class="text-sm text-slate-700 whitespace-pre-wrap">
|
<p class="text-sm text-slate-700 whitespace-pre-wrap">
|
||||||
@@ -390,6 +403,7 @@ export default class DIDView extends Vue {
|
|||||||
apiServer = "";
|
apiServer = "";
|
||||||
claims: Array<GenericCredWrapper<GenericVerifiableCredential>> = [];
|
claims: Array<GenericCredWrapper<GenericVerifiableCredential>> = [];
|
||||||
contactFromDid?: Contact;
|
contactFromDid?: Contact;
|
||||||
|
contactLabels: string[] = [];
|
||||||
|
|
||||||
contactYaml = "";
|
contactYaml = "";
|
||||||
hitEnd = false;
|
hitEnd = false;
|
||||||
@@ -485,9 +499,13 @@ export default class DIDView extends Vue {
|
|||||||
if (contact) {
|
if (contact) {
|
||||||
this.contactFromDid = contact;
|
this.contactFromDid = contact;
|
||||||
this.contactYaml = yaml.dump(this.contactFromDid);
|
this.contactYaml = yaml.dump(this.contactFromDid);
|
||||||
|
|
||||||
|
// Load labels for this contact
|
||||||
|
this.contactLabels = await this.$getContactLabels(this.viewingDid);
|
||||||
} else {
|
} else {
|
||||||
this.contactFromDid = undefined;
|
this.contactFromDid = undefined;
|
||||||
this.contactYaml = "";
|
this.contactYaml = "";
|
||||||
|
this.contactLabels = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user