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)
|
||||
export const NOTIFY_CONTACT_INFO_COPY = {
|
||||
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)
|
||||
|
||||
@@ -199,6 +199,20 @@ const MIGRATIONS = [
|
||||
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()
|
||||
* Retrieves all account metadata from the accounts table
|
||||
@@ -1965,6 +2085,11 @@ export interface IPlatformServiceMixin {
|
||||
$getContact(did: string): Promise<Contact | null>;
|
||||
$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[]>;
|
||||
$getAllAccountDids(): Promise<string[]>;
|
||||
$insertEntity(
|
||||
@@ -2074,10 +2199,7 @@ declare module "@vue/runtime-core" {
|
||||
$withTransaction<T>(fn: () => Promise<T>): Promise<T>;
|
||||
$getAvailableAccountDids(): Promise<string[]>;
|
||||
|
||||
// Specialized shortcuts - contacts cached, settings fresh
|
||||
$contacts(): Promise<Contact[]>;
|
||||
$contactsByDateAdded(): Promise<Contact[]>;
|
||||
$contactCount(): Promise<number>;
|
||||
// Specialized shortcuts - settings fresh
|
||||
$settings(defaults?: Settings): Promise<Settings>;
|
||||
$accountSettings(did?: string, defaults?: Settings): Promise<Settings>;
|
||||
$normalizeContacts(rawContacts: ContactMaybeWithJsonStrings[]): Contact[];
|
||||
@@ -2091,6 +2213,9 @@ declare module "@vue/runtime-core" {
|
||||
// @deprecated; see implementation note above
|
||||
// $saveMySettings(changes: Partial<Settings>): Promise<boolean>;
|
||||
|
||||
$contacts(): Promise<Contact[]>;
|
||||
$contactsByDateAdded(): Promise<Contact[]>;
|
||||
|
||||
// Cache management methods
|
||||
$refreshSettings(): Promise<Settings>;
|
||||
$refreshContacts(): Promise<Contact[]>;
|
||||
@@ -2106,6 +2231,12 @@ declare module "@vue/runtime-core" {
|
||||
$getAllContacts(): Promise<Contact[]>;
|
||||
$getContact(did: string): Promise<Contact | null>;
|
||||
$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[]>;
|
||||
$getAllAccountDids(): Promise<string[]>;
|
||||
$insertEntity(
|
||||
|
||||
@@ -126,6 +126,67 @@
|
||||
</button>
|
||||
</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 -->
|
||||
<div class="mt-8 flex justify-between">
|
||||
<button
|
||||
@@ -224,12 +285,36 @@ export default class ContactEditView extends Vue {
|
||||
contactNotes = "";
|
||||
/** Array of editable contact methods */
|
||||
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 */
|
||||
AppString = AppString;
|
||||
/** Contact method types for datalist suggestions */
|
||||
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
|
||||
*
|
||||
@@ -254,6 +339,14 @@ export default class ContactEditView extends Vue {
|
||||
this.contactName = contact.name || "";
|
||||
this.contactNotes = contact.notes || "";
|
||||
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 {
|
||||
this.notify.error(
|
||||
`${NOTIFY_CONTACT_NOT_FOUND.message} ${contactDid}`,
|
||||
@@ -285,6 +378,50 @@ export default class ContactEditView extends Vue {
|
||||
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
|
||||
*
|
||||
@@ -320,13 +457,29 @@ export default class ContactEditView extends Vue {
|
||||
}
|
||||
|
||||
// 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 || "", {
|
||||
name: this.contactName?.trim() || null,
|
||||
notes: this.contactNotes?.trim() || null,
|
||||
name: this.contactName?.trim() || undefined,
|
||||
notes: this.contactNotes?.trim() || undefined,
|
||||
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
|
||||
this.notify.success(NOTIFY_CONTACT_SAVED.message, TIMEOUTS.STANDARD);
|
||||
this.$router.back();
|
||||
|
||||
@@ -78,6 +78,55 @@
|
||||
</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 -->
|
||||
<ul
|
||||
v-if="contacts.length > 0"
|
||||
@@ -85,7 +134,7 @@
|
||||
class="border-t border-slate-300 my-2"
|
||||
>
|
||||
<ContactListItem
|
||||
v-for="contact in filteredContacts"
|
||||
v-for="contact in contactsFiltered"
|
||||
:key="contact.did"
|
||||
:contact="contact"
|
||||
:active-did="activeDid"
|
||||
@@ -268,6 +317,7 @@ export default class ContactsView extends Vue {
|
||||
activeDid = "";
|
||||
apiServer = "";
|
||||
contacts: Array<Contact> = [];
|
||||
contactsFiltered: Array<Contact> = [];
|
||||
contactInput = "";
|
||||
contactEdit: Contact | null = null;
|
||||
contactNewName = "";
|
||||
@@ -294,6 +344,11 @@ export default class ContactsView extends Vue {
|
||||
showGiveConfirmed = true;
|
||||
showLargeIdenticon?: Contact;
|
||||
|
||||
/** Label management state */
|
||||
selectedLabels: string[] = [];
|
||||
allLabels: string[] = [];
|
||||
showLabelFilter = false;
|
||||
|
||||
APP_SERVER = APP_SERVER;
|
||||
AppString = AppString;
|
||||
libsUtil = libsUtil;
|
||||
@@ -333,8 +388,9 @@ export default class ContactsView extends Vue {
|
||||
this.loadGives();
|
||||
}
|
||||
|
||||
// Replace PlatformServiceFactory and manual SQL with mixin method
|
||||
this.contacts = await this.$getAllContacts();
|
||||
this.contactsFiltered = await this.filteredContacts();
|
||||
this.allLabels = await this.$getUniqueLabels();
|
||||
}
|
||||
|
||||
private async processContactJwt() {
|
||||
@@ -501,14 +557,48 @@ export default class ContactsView extends Vue {
|
||||
}
|
||||
|
||||
// Computed properties for template streamlining
|
||||
get filteredContacts() {
|
||||
return this.showGiveNumbers
|
||||
async filteredContacts() {
|
||||
let filtered = this.showGiveNumbers
|
||||
? this.contactsSelected.length === 0
|
||||
? this.contacts
|
||||
: this.contacts.filter((contact) =>
|
||||
this.contactsSelected.includes(contact.did),
|
||||
)
|
||||
: 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() {
|
||||
@@ -537,7 +627,7 @@ export default class ContactsView extends Vue {
|
||||
}
|
||||
|
||||
get allContactsSelected() {
|
||||
return this.contactsSelected.length === this.contacts.length;
|
||||
return this.contactsSelected.length === this.contactsFiltered.length;
|
||||
}
|
||||
|
||||
// Helper methods for template interactions
|
||||
@@ -545,7 +635,9 @@ export default class ContactsView extends Vue {
|
||||
if (this.allContactsSelected) {
|
||||
this.contactsSelected = [];
|
||||
} 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.contactsFiltered = await this.filteredContacts();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -839,6 +932,7 @@ export default class ContactsView extends Vue {
|
||||
|
||||
// Update local contacts list
|
||||
this.updateContactsList(newContact);
|
||||
this.contactsFiltered = await this.filteredContacts();
|
||||
|
||||
// Set visibility and get success message
|
||||
const addedMessage = await this.handleContactVisibility(newContact);
|
||||
|
||||
@@ -43,6 +43,19 @@
|
||||
</router-link>
|
||||
</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 -->
|
||||
<div v-if="contactFromDid.notes" class="mt-3">
|
||||
<p class="text-sm text-slate-700 whitespace-pre-wrap">
|
||||
@@ -390,6 +403,7 @@ export default class DIDView extends Vue {
|
||||
apiServer = "";
|
||||
claims: Array<GenericCredWrapper<GenericVerifiableCredential>> = [];
|
||||
contactFromDid?: Contact;
|
||||
contactLabels: string[] = [];
|
||||
|
||||
contactYaml = "";
|
||||
hitEnd = false;
|
||||
@@ -485,9 +499,13 @@ export default class DIDView extends Vue {
|
||||
if (contact) {
|
||||
this.contactFromDid = contact;
|
||||
this.contactYaml = yaml.dump(this.contactFromDid);
|
||||
|
||||
// Load labels for this contact
|
||||
this.contactLabels = await this.$getContactLabels(this.viewingDid);
|
||||
} else {
|
||||
this.contactFromDid = undefined;
|
||||
this.contactYaml = "";
|
||||
this.contactLabels = [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user