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

View File

@@ -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<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
*/
async $getUniqueLabels(): Promise<string[]> {
async $getUniqueContactLabels(): Promise<string[]> {
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<string[]>;
$addContactLabel(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[]>;
$getAllAccountDids(): Promise<string[]>;
$insertEntity(
@@ -2391,7 +2429,9 @@ declare module "@vue/runtime-core" {
$getContactIdsWithAllLabels(labels: string[]): Promise<string[]>;
$addContactLabel(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[]>;
$getAllAccountDids(): Promise<string[]>;
$insertEntity(

View File

@@ -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}`,

View File

@@ -87,8 +87,14 @@
<div class="border font-bold p-1">
{{ capitalizeAndInsertSpacesBeforeCaps(contactField) }}
</div>
<div class="border p-1">{{ value.old }}</div>
<div class="border p-1">{{ value.new }}</div>
<div v-if="contactField === 'labels'" class="border p-1">
{{ 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>
@@ -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<ContactMethod>
| Array<ContactLabel>
| undefined;
old:
| string
| boolean
| Array<ContactMethod>
| Array<ContactLabel>
| 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<string, Contact> = {};
contactsExisting: Record<string, ContactWithLabels> = {};
/** Array of contacts being imported from JWT */
contactsImporting: Array<Contact> = [];
contactsImporting: Array<ContactWithLabels> = [];
/** Selection state for each importing contact */
contactsSelected: Array<boolean> = [];
/** Differences between existing and importing contacts */
contactDifferences: Record<
string,
Record<
string,
{
new: string | boolean | Array<ContactMethod> | undefined;
old: string | boolean | Array<ContactMethod> | undefined;
}
>
> = {};
/** Each contact's differences between existing and importing info */
contactDifferences: Record<string, ContactDifferences> = {};
/** 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<Contact>) {
async setContactsSelected(contacts: Array<ContactWithLabels>) {
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<ContactMethod> | undefined;
old: string | boolean | Array<ContactMethod> | 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++;
}
}

View File

@@ -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() {