import labels from an export

This commit is contained in:
2026-01-14 19:37:37 -07:00
parent 84cad0e169
commit e94effd111
4 changed files with 128 additions and 73 deletions
+59 -19
View File
@@ -14,25 +14,10 @@
* - Mixin pattern for easy integration with existing class components * - Mixin pattern for easy integration with existing class components
* - Enhanced utility methods for common patterns * - Enhanced utility methods for common patterns
* - Robust error handling and logging * - 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 * - Settings shortcuts for ultra-frequent update patterns
* - High-level entity operations (insertContact, updateContact, etc.) * - High-level entity operations (insertContact, updateContact, etc.)
* - Result mapping helpers to eliminate verbose row processing * - 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 * @author Matthew Raymer
* @version 4.1.0 * @version 4.1.0
* @since 2025-07-02 * @since 2025-07-02
@@ -1364,6 +1349,10 @@ export const PlatformServiceMixin = {
return this._mapColumnsToValues(columns, values); return this._mapColumnsToValues(columns, values);
}, },
// =================================================
// CONTACT METHODS
// =================================================
/** /**
* Insert or replace contact - $insertContact() * Insert or replace contact - $insertContact()
* Eliminates verbose INSERT OR REPLACE patterns * Eliminates verbose INSERT OR REPLACE patterns
@@ -1520,6 +1509,10 @@ export const PlatformServiceMixin = {
} }
}, },
// =================================================
// CONTACT LABELS METHODS
// =================================================
/** /**
* Get labels for a specific contact - $getContactLabels() * Get labels for a specific contact - $getContactLabels()
* @param did Contact DID * @param did Contact DID
@@ -1622,11 +1615,54 @@ export const PlatformServiceMixin = {
} }
}, },
async $insertContactLabels(
did: string,
labels: string[],
): Promise<boolean> {
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<boolean> {
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<string[]> Array of unique labels * @returns Promise<string[]> Array of unique labels
*/ */
async $getUniqueLabels(): Promise<string[]> { async $getUniqueContactLabels(): Promise<string[]> {
try { try {
const results = (await this.$dbQuery( const results = (await this.$dbQuery(
"SELECT DISTINCT label FROM contact_labels ORDER BY label", "SELECT DISTINCT label FROM contact_labels ORDER BY label",
@@ -2241,7 +2277,9 @@ export interface IPlatformServiceMixin {
$getContactIdsWithAllLabels(labels: string[]): Promise<string[]>; $getContactIdsWithAllLabels(labels: string[]): Promise<string[]>;
$addContactLabel(did: string, label: string): Promise<boolean>; $addContactLabel(did: string, label: string): Promise<boolean>;
$deleteContactLabel(did: string, label: string): Promise<boolean>; $deleteContactLabel(did: string, label: string): Promise<boolean>;
$getUniqueLabels(): Promise<string[]>; $insertContactLabels(did: string, labels: string[]): Promise<boolean>;
$updateContactLabels(did: string, labels: string[]): Promise<boolean>;
$getUniqueContactLabels(): Promise<string[]>;
$getAllAccounts(): Promise<Account[]>; $getAllAccounts(): Promise<Account[]>;
$getAllAccountDids(): Promise<string[]>; $getAllAccountDids(): Promise<string[]>;
$insertEntity( $insertEntity(
@@ -2391,7 +2429,9 @@ declare module "@vue/runtime-core" {
$getContactIdsWithAllLabels(labels: string[]): Promise<string[]>; $getContactIdsWithAllLabels(labels: string[]): Promise<string[]>;
$addContactLabel(did: string, label: string): Promise<boolean>; $addContactLabel(did: string, label: string): Promise<boolean>;
$deleteContactLabel(did: string, label: string): Promise<boolean>; $deleteContactLabel(did: string, label: string): Promise<boolean>;
$getUniqueLabels(): Promise<string[]>; $insertContactLabels(did: string, labels: string[]): Promise<boolean>;
$updateContactLabels(did: string, labels: string[]): Promise<boolean>;
$getUniqueContactLabels(): Promise<string[]>;
$getAllAccounts(): Promise<Account[]>; $getAllAccounts(): Promise<Account[]>;
$getAllAccountDids(): Promise<string[]>; $getAllAccountDids(): Promise<string[]>;
$insertEntity( $insertEntity(
+1 -1
View File
@@ -346,7 +346,7 @@ export default class ContactEditView extends Vue {
this.originalLabels = [...labels]; this.originalLabels = [...labels];
// Load all labels for suggestions // Load all labels for suggestions
this.allUniqueLabels = await this.$getUniqueLabels(); this.allUniqueLabels = await this.$getUniqueContactLabels();
} else { } else {
this.notify.error( this.notify.error(
`${NOTIFY_CONTACT_NOT_FOUND.message} ${contactDid}`, `${NOTIFY_CONTACT_NOT_FOUND.message} ${contactDid}`,
+67 -52
View File
@@ -87,8 +87,14 @@
<div class="border font-bold p-1"> <div class="border font-bold p-1">
{{ capitalizeAndInsertSpacesBeforeCaps(contactField) }} {{ capitalizeAndInsertSpacesBeforeCaps(contactField) }}
</div> </div>
<div class="border p-1">{{ value.old }}</div> <div v-if="contactField === 'labels'" class="border p-1">
<div class="border p-1">{{ value.new }}</div> {{ value.old.join(", ") }}
</div>
<div v-else class="border p-1">{{ value.old }}</div>
<div v-if="contactField === 'labels'" class="border p-1">
{{ value.new.join(", ") }}
</div>
<div v-else class="border p-1">{{ value.new }}</div>
</div> </div>
</div> </div>
</div> </div>
@@ -176,26 +182,6 @@
* - Field-by-field comparison for existing contacts * - Field-by-field comparison for existing contacts
* - Batch visibility settings * - Batch visibility settings
* - Auto-import for single new contacts * - 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 Contact} for contact data structure
* @see {@link setVisibilityUtil} for visibility management * @see {@link setVisibilityUtil} for visibility management
@@ -209,7 +195,11 @@ import QuickNav from "../components/QuickNav.vue";
import EntityIcon from "../components/EntityIcon.vue"; import EntityIcon from "../components/EntityIcon.vue";
import OfferDialog from "../components/OfferDialog.vue"; import OfferDialog from "../components/OfferDialog.vue";
import { APP_SERVER, AppString, NotificationIface } from "../constants/app"; 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 * as libsUtil from "../libs/util";
import { import {
capitalizeAndInsertSpacesBeforeCaps, capitalizeAndInsertSpacesBeforeCaps,
@@ -220,6 +210,25 @@ import { getContactJwtFromJwtUrl } from "../libs/crypto";
import { decodeEndorserJwt } from "../libs/crypto/vc"; import { decodeEndorserJwt } from "../libs/crypto/vc";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify"; import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { ContactLabel } from "@/db/tables/contactLabels";
type ContactDifferences = Record<
string,
{
new:
| string
| boolean
| Array<ContactMethod>
| Array<ContactLabel>
| undefined;
old:
| string
| boolean
| Array<ContactMethod>
| Array<ContactLabel>
| undefined;
}
>;
/** /**
* Contact Import View Component * Contact Import View Component
@@ -287,22 +296,13 @@ export default class ContactImportView extends Vue {
/** API server URL for backend communication */ /** API server URL for backend communication */
apiServer = ""; apiServer = "";
/** Map of existing contacts keyed by DID for duplicate detection */ /** Map of existing contacts keyed by DID for duplicate detection */
contactsExisting: Record<string, Contact> = {}; contactsExisting: Record<string, ContactWithLabels> = {};
/** Array of contacts being imported from JWT */ /** Array of contacts being imported from JWT */
contactsImporting: Array<Contact> = []; contactsImporting: Array<ContactWithLabels> = [];
/** Selection state for each importing contact */ /** Selection state for each importing contact */
contactsSelected: Array<boolean> = []; contactsSelected: Array<boolean> = [];
/** Differences between existing and importing contacts */ /** Each contact's differences between existing and importing info */
contactDifferences: Record< contactDifferences: Record<string, ContactDifferences> = {};
string,
Record<
string,
{
new: string | boolean | Array<ContactMethod> | undefined;
old: string | boolean | Array<ContactMethod> | undefined;
}
>
> = {};
/** Loading state for import operations */ /** Loading state for import operations */
checkingImports = false; checkingImports = false;
/** JWT input for manual contact import */ /** JWT input for manual contact import */
@@ -412,13 +412,17 @@ export default class ContactImportView extends Vue {
* Processes contacts for import and checks for duplicates * Processes contacts for import and checks for duplicates
* @param contacts Array of contacts to process * @param contacts Array of contacts to process
*/ */
async setContactsSelected(contacts: Array<Contact>) { async setContactsSelected(contacts: Array<ContactWithLabels>) {
this.contactsImporting = contacts; 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 // Get all existing contacts for comparison
const baseContacts = await this.$getAllContacts(); const baseContacts = await this.$getAllContacts();
// get the labels for each contact
// Check for existing contacts and differences // Check for existing contacts and differences
for (let i = 0; i < this.contactsImporting.length; i++) { for (let i = 0; i < this.contactsImporting.length; i++) {
const contactIn = this.contactsImporting[i]; const contactIn = this.contactsImporting[i];
@@ -426,25 +430,24 @@ export default class ContactImportView extends Vue {
(contact) => contact.did === contactIn.did, (contact) => contact.did === contactIn.did,
); );
if (existingContact) { 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 // Compare contact fields for differences
const differences: Record< const differences: ContactDifferences = {};
string,
{
new: string | boolean | Array<ContactMethod> | undefined;
old: string | boolean | Array<ContactMethod> | undefined;
}
> = {};
Object.keys(contactIn).forEach((key) => { Object.keys(contactIn).forEach((key) => {
if ( if (
!R.equals( !R.equals(
contactIn[key as keyof Contact], contactIn[key as keyof Contact],
existingContact[key as keyof Contact], existingFullContact[key as keyof Contact],
) )
) { ) {
differences[key] = { differences[key] = {
old: existingContact[key as keyof Contact], old: existingFullContact[key as keyof Contact],
new: contactIn[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; this.contactDifferences[contactIn.did] = differences;
if (R.isEmpty(differences)) { if (R.isEmpty(differences)) {
this.sameCount++; this.sameCount++;
} else {
// auto-select contacts with differences
this.contactsSelected[i] = true;
} }
} else {
// Don't auto-select duplicates // auto-select new contacts
this.contactsSelected[i] = false; this.contactsSelected[i] = true;
} }
} }
} }
@@ -517,16 +523,25 @@ export default class ContactImportView extends Vue {
// Process selected contacts // Process selected contacts
for (let i = 0; i < this.contactsImporting.length; i++) { for (let i = 0; i < this.contactsImporting.length; i++) {
if (this.contactsSelected[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]; const existingContact = this.contactsExisting[contact.did];
if (existingContact) { if (existingContact) {
// Update existing contact // Update existing contact
await this.$updateContact(contact.did, contact); await this.$updateContact(contact.did, contact);
// update the labels for the contact
await this.$updateContactLabels(contact.did, contactLabels);
updatedCount++; updatedCount++;
} else { } else {
// Add new contact // Add new contact
await this.$insertContact(contact); await this.$insertContact(contact);
// add the labels for the contact
await this.$insertContactLabels(contact.did, contactLabels);
importedCount++; importedCount++;
} }
} }
+1 -1
View File
@@ -387,7 +387,7 @@ export default class ContactsView extends Vue {
this.contacts = await this.$getAllContacts(); this.contacts = await this.$getAllContacts();
this.contactsFiltered = await this.filteredContacts(); this.contactsFiltered = await this.filteredContacts();
this.allLabels = await this.$getUniqueLabels(); this.allLabels = await this.$getUniqueContactLabels();
} }
private async processContactJwt() { private async processContactJwt() {