From e94effd111362921136ef4d52bd74c4a04559d4f Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Wed, 14 Jan 2026 19:37:37 -0700 Subject: [PATCH] import labels from an export --- src/utils/PlatformServiceMixin.ts | 78 +++++++++++++++----- src/views/ContactEditView.vue | 2 +- src/views/ContactImportView.vue | 119 +++++++++++++++++------------- src/views/ContactsView.vue | 2 +- 4 files changed, 128 insertions(+), 73 deletions(-) diff --git a/src/utils/PlatformServiceMixin.ts b/src/utils/PlatformServiceMixin.ts index c773e92d..3334c396 100644 --- a/src/utils/PlatformServiceMixin.ts +++ b/src/utils/PlatformServiceMixin.ts @@ -14,25 +14,10 @@ * - Mixin pattern for easy integration with existing class components * - Enhanced utility methods for common patterns * - Robust error handling and logging - * - Ultra-concise database interaction methods - * - Smart caching layer with TTL for performance optimization * - Settings shortcuts for ultra-frequent update patterns * - High-level entity operations (insertContact, updateContact, etc.) * - Result mapping helpers to eliminate verbose row processing * - * Benefits: - * - Eliminates repeated PlatformServiceFactory.getInstance() calls - * - Provides consistent error handling across components - * - Reduces boilerplate database code by up to 80% - * - Maintains type safety with TypeScript - * - Includes common database utility patterns - * - Enhanced error handling and logging - * - Ultra-concise method names for frequent operations - * - Automatic caching for settings and contacts (massive performance gain) - * - Settings update shortcuts reduce 90% of update boilerplate - * - Entity operations eliminate verbose SQL INSERT/UPDATE patterns - * - Result mapping helpers reduce row processing boilerplate by 75% - * * @author Matthew Raymer * @version 4.1.0 * @since 2025-07-02 @@ -1364,6 +1349,10 @@ export const PlatformServiceMixin = { return this._mapColumnsToValues(columns, values); }, + // ================================================= + // CONTACT METHODS + // ================================================= + /** * Insert or replace contact - $insertContact() * Eliminates verbose INSERT OR REPLACE patterns @@ -1520,6 +1509,10 @@ export const PlatformServiceMixin = { } }, + // ================================================= + // CONTACT LABELS METHODS + // ================================================= + /** * Get labels for a specific contact - $getContactLabels() * @param did Contact DID @@ -1622,11 +1615,54 @@ export const PlatformServiceMixin = { } }, + async $insertContactLabels( + did: string, + labels: string[], + ): Promise { + try { + for (const label of labels) { + await this.$dbExec( + "INSERT INTO contact_labels (did, label) VALUES (?, ?)", + [did, label], + ); + } + return true; + } catch (error) { + logger.error( + `[PlatformServiceMixin] Error inserting labels for contact ${did}:`, + error, + ); + return false; + } + }, + + async $updateContactLabels( + did: string, + labels: string[], + ): Promise { + try { + await this.$dbExec("DELETE FROM contact_labels WHERE did = ?", [did]); + for (const label of labels) { + await this.$dbExec( + "INSERT INTO contact_labels (did, label) VALUES (?, ?)", + [did, label], + ); + } + return true; + } catch (error) { + logger.error( + `[PlatformServiceMixin] Error updating labels for contact ${did}:`, + error, + ); + return false; + } + }, + /** - * Get all unique labels available - $getUniqueLabels() + * Get all unique labels available * @returns Promise Array of unique labels */ - async $getUniqueLabels(): Promise { + async $getUniqueContactLabels(): Promise { try { const results = (await this.$dbQuery( "SELECT DISTINCT label FROM contact_labels ORDER BY label", @@ -2241,7 +2277,9 @@ export interface IPlatformServiceMixin { $getContactIdsWithAllLabels(labels: string[]): Promise; $addContactLabel(did: string, label: string): Promise; $deleteContactLabel(did: string, label: string): Promise; - $getUniqueLabels(): Promise; + $insertContactLabels(did: string, labels: string[]): Promise; + $updateContactLabels(did: string, labels: string[]): Promise; + $getUniqueContactLabels(): Promise; $getAllAccounts(): Promise; $getAllAccountDids(): Promise; $insertEntity( @@ -2391,7 +2429,9 @@ declare module "@vue/runtime-core" { $getContactIdsWithAllLabels(labels: string[]): Promise; $addContactLabel(did: string, label: string): Promise; $deleteContactLabel(did: string, label: string): Promise; - $getUniqueLabels(): Promise; + $insertContactLabels(did: string, labels: string[]): Promise; + $updateContactLabels(did: string, labels: string[]): Promise; + $getUniqueContactLabels(): Promise; $getAllAccounts(): Promise; $getAllAccountDids(): Promise; $insertEntity( diff --git a/src/views/ContactEditView.vue b/src/views/ContactEditView.vue index 7469057a..377893a7 100644 --- a/src/views/ContactEditView.vue +++ b/src/views/ContactEditView.vue @@ -346,7 +346,7 @@ export default class ContactEditView extends Vue { this.originalLabels = [...labels]; // Load all labels for suggestions - this.allUniqueLabels = await this.$getUniqueLabels(); + this.allUniqueLabels = await this.$getUniqueContactLabels(); } else { this.notify.error( `${NOTIFY_CONTACT_NOT_FOUND.message} ${contactDid}`, diff --git a/src/views/ContactImportView.vue b/src/views/ContactImportView.vue index a1702a02..d56f3712 100644 --- a/src/views/ContactImportView.vue +++ b/src/views/ContactImportView.vue @@ -87,8 +87,14 @@
{{ capitalizeAndInsertSpacesBeforeCaps(contactField) }}
-
{{ value.old }}
-
{{ value.new }}
+
+ {{ value.old.join(", ") }} +
+
{{ value.old }}
+
+ {{ value.new.join(", ") }} +
+
{{ value.new }}
@@ -176,26 +182,6 @@ * - Field-by-field comparison for existing contacts * - Batch visibility settings * - Auto-import for single new contacts - * - Error handling and validation - * - * State Management: - * - Tracks existing contacts - * - Maintains selection state for bulk imports - * - Records differences for duplicate contacts - * - Manages visibility settings - * - * Security Considerations: - * - JWT validation for imported contacts - * - Visibility control per contact - * - Error handling for malformed data - * - * @example - * // Component usage in router - * { - * path: "/contact-import/:jwt?", - * name: "contact-import", - * component: ContactImportView - * } * * @see {@link Contact} for contact data structure * @see {@link setVisibilityUtil} for visibility management @@ -209,7 +195,11 @@ import QuickNav from "../components/QuickNav.vue"; import EntityIcon from "../components/EntityIcon.vue"; import OfferDialog from "../components/OfferDialog.vue"; import { APP_SERVER, AppString, NotificationIface } from "../constants/app"; -import { Contact, ContactMethod } from "../db/tables/contacts"; +import { + Contact, + ContactWithLabels, + ContactMethod, +} from "../db/tables/contacts"; import * as libsUtil from "../libs/util"; import { capitalizeAndInsertSpacesBeforeCaps, @@ -220,6 +210,25 @@ import { getContactJwtFromJwtUrl } from "../libs/crypto"; import { decodeEndorserJwt } from "../libs/crypto/vc"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify"; +import { ContactLabel } from "@/db/tables/contactLabels"; + +type ContactDifferences = Record< + string, + { + new: + | string + | boolean + | Array + | Array + | undefined; + old: + | string + | boolean + | Array + | Array + | undefined; + } +>; /** * Contact Import View Component @@ -287,22 +296,13 @@ export default class ContactImportView extends Vue { /** API server URL for backend communication */ apiServer = ""; /** Map of existing contacts keyed by DID for duplicate detection */ - contactsExisting: Record = {}; + contactsExisting: Record = {}; /** Array of contacts being imported from JWT */ - contactsImporting: Array = []; + contactsImporting: Array = []; /** Selection state for each importing contact */ contactsSelected: Array = []; - /** Differences between existing and importing contacts */ - contactDifferences: Record< - string, - Record< - string, - { - new: string | boolean | Array | undefined; - old: string | boolean | Array | undefined; - } - > - > = {}; + /** Each contact's differences between existing and importing info */ + contactDifferences: Record = {}; /** Loading state for import operations */ checkingImports = false; /** JWT input for manual contact import */ @@ -412,13 +412,17 @@ export default class ContactImportView extends Vue { * Processes contacts for import and checks for duplicates * @param contacts Array of contacts to process */ - async setContactsSelected(contacts: Array) { + async setContactsSelected(contacts: Array) { this.contactsImporting = contacts; - this.contactsSelected = new Array(this.contactsImporting.length).fill(true); + this.contactsSelected = new Array(this.contactsImporting.length).fill( + false, + ); // Get all existing contacts for comparison const baseContacts = await this.$getAllContacts(); + // get the labels for each contact + // Check for existing contacts and differences for (let i = 0; i < this.contactsImporting.length; i++) { const contactIn = this.contactsImporting[i]; @@ -426,25 +430,24 @@ export default class ContactImportView extends Vue { (contact) => contact.did === contactIn.did, ); if (existingContact) { - this.contactsExisting[contactIn.did] = existingContact; + const labels = await this.$getContactLabelsForDid(existingContact.did); + this.contactsExisting[contactIn.did] = { + ...existingContact, + labels: labels || [], + }; + const existingFullContact = this.contactsExisting[contactIn.did]; // Compare contact fields for differences - const differences: Record< - string, - { - new: string | boolean | Array | undefined; - old: string | boolean | Array | undefined; - } - > = {}; + const differences: ContactDifferences = {}; Object.keys(contactIn).forEach((key) => { if ( !R.equals( contactIn[key as keyof Contact], - existingContact[key as keyof Contact], + existingFullContact[key as keyof Contact], ) ) { differences[key] = { - old: existingContact[key as keyof Contact], + old: existingFullContact[key as keyof Contact], new: contactIn[key as keyof Contact], }; } @@ -452,10 +455,13 @@ export default class ContactImportView extends Vue { this.contactDifferences[contactIn.did] = differences; if (R.isEmpty(differences)) { this.sameCount++; + } else { + // auto-select contacts with differences + this.contactsSelected[i] = true; } - - // Don't auto-select duplicates - this.contactsSelected[i] = false; + } else { + // auto-select new contacts + this.contactsSelected[i] = true; } } } @@ -517,16 +523,25 @@ export default class ContactImportView extends Vue { // Process selected contacts for (let i = 0; i < this.contactsImporting.length; i++) { if (this.contactsSelected[i]) { - const contact = this.contactsImporting[i]; + const contactWithLabels = this.contactsImporting[i]; + const contact = { + ...contactWithLabels, + labels: undefined, + }; + const contactLabels = contactWithLabels.labels || []; const existingContact = this.contactsExisting[contact.did]; if (existingContact) { // Update existing contact await this.$updateContact(contact.did, contact); + // update the labels for the contact + await this.$updateContactLabels(contact.did, contactLabels); updatedCount++; } else { // Add new contact await this.$insertContact(contact); + // add the labels for the contact + await this.$insertContactLabels(contact.did, contactLabels); importedCount++; } } diff --git a/src/views/ContactsView.vue b/src/views/ContactsView.vue index 6252857d..2ff35379 100644 --- a/src/views/ContactsView.vue +++ b/src/views/ContactsView.vue @@ -387,7 +387,7 @@ export default class ContactsView extends Vue { this.contacts = await this.$getAllContacts(); this.contactsFiltered = await this.filteredContacts(); - this.allLabels = await this.$getUniqueLabels(); + this.allLabels = await this.$getUniqueContactLabels(); } private async processContactJwt() {