Browse Source

IndexedDB migration: fix loading of data, fix object comparisons, add unmodified, etc

migrate-dexie-to-sqlite
Trent Larson 6 days ago
parent
commit
6136cafd11
  1. 4
      src/db/tables/contacts.ts
  2. 5
      src/db/tables/settings.ts
  3. 244
      src/services/indexedDBMigrationService.ts
  4. 131
      src/views/DatabaseMigration.vue
  5. 9
      src/views/TestView.vue

4
src/db/tables/contacts.ts

@ -19,6 +19,10 @@ export interface Contact {
registered?: boolean; // cached value of the server setting
}
export type ContactWithJsonStrings = Contact & {
contactMethods?: string;
}
export const ContactSchema = {
contacts: "&did, name", // no need to key by other things
};

5
src/db/tables/settings.ts

@ -64,6 +64,11 @@ export type Settings = {
webPushServer?: string; // Web Push server URL
};
// type of settings where the searchBoxes are JSON strings instead of objects
export type SettingsWithJsonStrings = Settings & {
searchBoxes: string;
};
export function checkIsAnyFeedFilterOn(settings: Settings): boolean {
return !!(settings?.filterFeedByNearby || settings?.filterFeedByVisible);
}

244
src/services/indexedDBMigrationService.ts

@ -26,11 +26,12 @@ import "dexie-export-import";
import { PlatformServiceFactory } from "./PlatformServiceFactory";
import { db, accountsDBPromise } from "../db/index";
import { Contact, ContactMethod } from "../db/tables/contacts";
import { Settings, MASTER_SETTINGS_KEY } from "../db/tables/settings";
import { Account } from "../db/tables/accounts";
import { Settings, MASTER_SETTINGS_KEY, SettingsWithJsonStrings, BoundingBox } from "../db/tables/settings";
import { Account, AccountEncrypted } from "../db/tables/accounts";
import { logger } from "../utils/logger";
import { parseJsonField } from "../db/databaseUtil";
import { mapColumnsToValues, parseJsonField } from "../db/databaseUtil";
import { importFromMnemonic } from "../libs/util";
import { IIdentifier } from "@veramo/core";
/**
* Interface for data comparison results between Dexie and SQLite databases
@ -66,22 +67,24 @@ export interface DataComparison {
dexieSettings: Settings[];
sqliteSettings: Settings[];
dexieAccounts: Account[];
sqliteAccounts: Account[];
sqliteAccounts: string[];
differences: {
contacts: {
added: Contact[];
modified: Contact[];
unmodified: Contact[];
missing: Contact[];
};
settings: {
added: Settings[];
modified: Settings[];
unmodified: Settings[];
missing: Settings[];
};
accounts: {
added: Account[];
modified: Account[];
missing: Account[];
unmodified: Account[];
missing: string[];
};
};
}
@ -184,22 +187,14 @@ export async function getSqliteContacts(): Promise<Contact[]> {
let contacts: Contact[] = [];
if (result?.values?.length) {
contacts = result.values.map((row) => {
const contact = parseJsonField(row, {}) as Contact;
return {
did: contact.did || "",
name: contact.name || "",
contactMethods: parseJsonField(
contact.contactMethods,
[],
) as ContactMethod[],
nextPubKeyHashB64: contact.nextPubKeyHashB64 || "",
notes: contact.notes || "",
profileImageUrl: contact.profileImageUrl || "",
publicKeyBase64: contact.publicKeyBase64 || "",
seesMe: contact.seesMe || false,
registered: contact.registered || false,
} as Contact;
const preContacts = mapColumnsToValues(result.columns, result.values) as unknown as Contact[];
// This is redundant since absurd-sql auto-parses JSON strings to objects.
// But we started it, and it should be known everywhere, so we're keeping it.
contacts = preContacts.map((contact) => {
if (contact.contactMethods) {
contact.contactMethods = parseJsonField(contact.contactMethods, []) as ContactMethod[];
}
return contact;
});
}
@ -281,37 +276,16 @@ export async function getSqliteSettings(): Promise<Settings[]> {
let settings: Settings[] = [];
if (result?.values?.length) {
settings = result.values.map((row) => {
const setting = parseJsonField(row, {}) as Settings;
return {
id: setting.id,
accountDid: setting.accountDid || "",
activeDid: setting.activeDid || "",
apiServer: setting.apiServer || "",
filterFeedByNearby: setting.filterFeedByNearby || false,
filterFeedByVisible: setting.filterFeedByVisible || false,
finishedOnboarding: setting.finishedOnboarding || false,
firstName: setting.firstName || "",
hideRegisterPromptOnNewContact: setting.hideRegisterPromptOnNewContact || false,
isRegistered: setting.isRegistered || false,
lastName: setting.lastName || "",
lastAckedOfferToUserJwtId: setting.lastAckedOfferToUserJwtId || "",
lastAckedOfferToUserProjectsJwtId: setting.lastAckedOfferToUserProjectsJwtId || "",
lastNotifiedClaimId: setting.lastNotifiedClaimId || "",
lastViewedClaimId: setting.lastViewedClaimId || "",
notifyingNewActivityTime: setting.notifyingNewActivityTime || "",
notifyingReminderMessage: setting.notifyingReminderMessage || "",
notifyingReminderTime: setting.notifyingReminderTime || "",
partnerApiServer: setting.partnerApiServer || "",
passkeyExpirationMinutes: setting.passkeyExpirationMinutes,
profileImageUrl: setting.profileImageUrl || "",
searchBoxes: parseJsonField(setting.searchBoxes, []),
showContactGivesInline: setting.showContactGivesInline || false,
showGeneralAdvanced: setting.showGeneralAdvanced || false,
showShortcutBvc: setting.showShortcutBvc || false,
vapid: setting.vapid || "",
} as Settings;
});
const presettings =
mapColumnsToValues(result.columns, result.values) as Settings[];
// This is redundant since absurd-sql auto-parses JSON strings to objects.
// But we started it, and it should be known everywhere, so we're keeping it.
settings = presettings.map((setting) => {
if (setting.searchBoxes) {
setting.searchBoxes = parseJsonField(setting.searchBoxes, []) as Array<{ name: string, bbox: BoundingBox }>;
}
return setting;
})
}
logger.info(
@ -336,7 +310,7 @@ export async function getSqliteSettings(): Promise<Settings[]> {
*
* @async
* @function getSqliteAccounts
* @returns {Promise<Account[]>} Array of all accounts from SQLite database
* @returns {Promise<string[]>} Array of all accounts from SQLite database
* @throws {Error} If database query fails or data conversion fails
* @example
* ```typescript
@ -348,32 +322,20 @@ export async function getSqliteSettings(): Promise<Settings[]> {
* }
* ```
*/
export async function getSqliteAccounts(): Promise<Account[]> {
export async function getSqliteAccounts(): Promise<string[]> {
try {
const platformService = PlatformServiceFactory.getInstance();
const result = await platformService.dbQuery("SELECT * FROM accounts");
const result = await platformService.dbQuery("SELECT did FROM accounts");
let accounts: Account[] = [];
let dids: string[] = [];
if (result?.values?.length) {
accounts = result.values.map((row) => {
const account = parseJsonField(row, {}) as Account;
return {
id: account.id,
dateCreated: account.dateCreated || "",
derivationPath: account.derivationPath || "",
did: account.did || "",
identity: account.identity || "",
mnemonic: account.mnemonic || "",
passkeyCredIdHex: account.passkeyCredIdHex || "",
publicKeyHex: account.publicKeyHex || "",
} as Account;
});
dids = result.values.map((row) => row[0] as string);
}
logger.info(
`[MigrationService] Retrieved ${accounts.length} accounts from SQLite`,
`[MigrationService] Retrieved ${dids.length} accounts from SQLite`,
);
return accounts;
return dids;
} catch (error) {
logger.error("[MigrationService] Error retrieving SQLite accounts:", error);
throw new Error(`Failed to retrieve SQLite accounts: ${error}`);
@ -532,6 +494,7 @@ export async function compareDatabases(): Promise<DataComparison> {
function compareContacts(dexieContacts: Contact[], sqliteContacts: Contact[]) {
const added: Contact[] = [];
const modified: Contact[] = [];
const unmodified: Contact[] = [];
const missing: Contact[] = [];
// Find contacts that exist in Dexie but not in SQLite
@ -543,6 +506,8 @@ function compareContacts(dexieContacts: Contact[], sqliteContacts: Contact[]) {
added.push(dexieContact);
} else if (!contactsEqual(dexieContact, sqliteContact)) {
modified.push(dexieContact);
} else {
unmodified.push(dexieContact);
}
}
@ -554,7 +519,7 @@ function compareContacts(dexieContacts: Contact[], sqliteContacts: Contact[]) {
}
}
return { added, modified, missing };
return { added, modified, unmodified, missing };
}
/**
@ -588,27 +553,30 @@ function compareSettings(
) {
const added: Settings[] = [];
const modified: Settings[] = [];
const unmodified: Settings[] = [];
const missing: Settings[] = [];
// Find settings that exist in Dexie but not in SQLite
for (const dexieSetting of dexieSettings) {
const sqliteSetting = sqliteSettings.find((s) => s.id === dexieSetting.id);
const sqliteSetting = sqliteSettings.find((s) => s.accountDid === dexieSetting.accountDid);
if (!sqliteSetting) {
added.push(dexieSetting);
} else if (!settingsEqual(dexieSetting, sqliteSetting)) {
modified.push(dexieSetting);
} else {
unmodified.push(dexieSetting);
}
}
// Find settings that exist in SQLite but not in Dexie
for (const sqliteSetting of sqliteSettings) {
const dexieSetting = dexieSettings.find((s) => s.id === sqliteSetting.id);
const dexieSetting = dexieSettings.find((s) => s.accountDid === sqliteSetting.accountDid);
if (!dexieSetting) {
missing.push(sqliteSetting);
}
}
return { added, modified, missing };
return { added, modified, unmodified, missing };
}
/**
@ -623,11 +591,11 @@ function compareSettings(
*
* @function compareAccounts
* @param {Account[]} dexieAccounts - Accounts from Dexie database
* @param {Account[]} sqliteAccounts - Accounts from SQLite database
* @param {Account[]} sqliteDids - Accounts from SQLite database
* @returns {Object} Object containing added, modified, and missing accounts
* @returns {Account[]} returns.added - Accounts in Dexie but not SQLite
* @returns {Account[]} returns.modified - Accounts that differ between databases
* @returns {Account[]} returns.missing - Accounts in SQLite but not Dexie
* @returns {Account[]} returns.modified - always 0 because we don't check
* @returns {string[]} returns.missing - Accounts in SQLite but not Dexie
* @example
* ```typescript
* const differences = compareAccounts(dexieAccounts, sqliteAccounts);
@ -636,30 +604,30 @@ function compareSettings(
* console.log(`Missing: ${differences.missing.length}`);
* ```
*/
function compareAccounts(dexieAccounts: Account[], sqliteAccounts: Account[]) {
function compareAccounts(dexieAccounts: Account[], sqliteDids: string[]) {
const added: Account[] = [];
const modified: Account[] = [];
const missing: Account[] = [];
const unmodified: Account[] = [];
const missing: string[] = [];
// Find accounts that exist in Dexie but not in SQLite
for (const dexieAccount of dexieAccounts) {
const sqliteAccount = sqliteAccounts.find((a) => a.id === dexieAccount.id);
if (!sqliteAccount) {
const sqliteDid = sqliteDids.find((a) => a === dexieAccount.did);
if (!sqliteDid) {
added.push(dexieAccount);
} else if (!accountsEqual(dexieAccount, sqliteAccount)) {
modified.push(dexieAccount);
} else {
unmodified.push(dexieAccount);
}
}
// Find accounts that exist in SQLite but not in Dexie
for (const sqliteAccount of sqliteAccounts) {
const dexieAccount = dexieAccounts.find((a) => a.id === sqliteAccount.id);
for (const sqliteDid of sqliteDids) {
const dexieAccount = dexieAccounts.find((a) => a.did === sqliteDid);
if (!dexieAccount) {
missing.push(sqliteAccount);
missing.push(sqliteDid);
}
}
return { added, modified, missing };
return { added, unmodified, missing };
}
/**
@ -688,15 +656,15 @@ function compareAccounts(dexieAccounts: Account[], sqliteAccounts: Account[]) {
*/
function contactsEqual(contact1: Contact, contact2: Contact): boolean {
return (
contact1.did === contact2.did &&
contact1.name === contact2.name &&
contact1.notes === contact2.notes &&
contact1.profileImageUrl === contact2.profileImageUrl &&
contact1.publicKeyBase64 === contact2.publicKeyBase64 &&
contact1.nextPubKeyHashB64 === contact2.nextPubKeyHashB64 &&
contact1.seesMe === contact2.seesMe &&
contact1.registered === contact2.registered &&
JSON.stringify(contact1.contactMethods) ===
contact1.did == contact2.did &&
contact1.name == contact2.name &&
contact1.notes == contact2.notes &&
contact1.profileImageUrl == contact2.profileImageUrl &&
contact1.publicKeyBase64 == contact2.publicKeyBase64 &&
contact1.nextPubKeyHashB64 == contact2.nextPubKeyHashB64 &&
contact1.seesMe == contact2.seesMe &&
contact1.registered == contact2.registered &&
JSON.stringify(contact1.contactMethods) ==
JSON.stringify(contact2.contactMethods)
);
}
@ -727,38 +695,38 @@ function contactsEqual(contact1: Contact, contact2: Contact): boolean {
*/
function settingsEqual(settings1: Settings, settings2: Settings): boolean {
return (
settings1.id === settings2.id &&
settings1.accountDid === settings2.accountDid &&
settings1.activeDid === settings2.activeDid &&
settings1.apiServer === settings2.apiServer &&
settings1.filterFeedByNearby === settings2.filterFeedByNearby &&
settings1.filterFeedByVisible === settings2.filterFeedByVisible &&
settings1.finishedOnboarding === settings2.finishedOnboarding &&
settings1.firstName === settings2.firstName &&
settings1.hideRegisterPromptOnNewContact ===
settings1.id == settings2.id &&
settings1.accountDid == settings2.accountDid &&
settings1.activeDid == settings2.activeDid &&
settings1.apiServer == settings2.apiServer &&
settings1.filterFeedByNearby == settings2.filterFeedByNearby &&
settings1.filterFeedByVisible == settings2.filterFeedByVisible &&
settings1.finishedOnboarding == settings2.finishedOnboarding &&
settings1.firstName == settings2.firstName &&
settings1.hideRegisterPromptOnNewContact ==
settings2.hideRegisterPromptOnNewContact &&
settings1.isRegistered === settings2.isRegistered &&
settings1.lastName === settings2.lastName &&
settings1.lastAckedOfferToUserJwtId ===
settings1.isRegistered == settings2.isRegistered &&
settings1.lastName == settings2.lastName &&
settings1.lastAckedOfferToUserJwtId ==
settings2.lastAckedOfferToUserJwtId &&
settings1.lastAckedOfferToUserProjectsJwtId ===
settings1.lastAckedOfferToUserProjectsJwtId ==
settings2.lastAckedOfferToUserProjectsJwtId &&
settings1.lastNotifiedClaimId === settings2.lastNotifiedClaimId &&
settings1.lastViewedClaimId === settings2.lastViewedClaimId &&
settings1.notifyingNewActivityTime === settings2.notifyingNewActivityTime &&
settings1.notifyingReminderMessage === settings2.notifyingReminderMessage &&
settings1.notifyingReminderTime === settings2.notifyingReminderTime &&
settings1.partnerApiServer === settings2.partnerApiServer &&
settings1.passkeyExpirationMinutes === settings2.passkeyExpirationMinutes &&
settings1.profileImageUrl === settings2.profileImageUrl &&
settings1.showContactGivesInline === settings2.showContactGivesInline &&
settings1.showGeneralAdvanced === settings2.showGeneralAdvanced &&
settings1.showShortcutBvc === settings2.showShortcutBvc &&
settings1.vapid === settings2.vapid &&
settings1.warnIfProdServer === settings2.warnIfProdServer &&
settings1.warnIfTestServer === settings2.warnIfTestServer &&
settings1.webPushServer === settings2.webPushServer &&
JSON.stringify(settings1.searchBoxes) ===
settings1.lastNotifiedClaimId == settings2.lastNotifiedClaimId &&
settings1.lastViewedClaimId == settings2.lastViewedClaimId &&
settings1.notifyingNewActivityTime == settings2.notifyingNewActivityTime &&
settings1.notifyingReminderMessage == settings2.notifyingReminderMessage &&
settings1.notifyingReminderTime == settings2.notifyingReminderTime &&
settings1.partnerApiServer == settings2.partnerApiServer &&
settings1.passkeyExpirationMinutes == settings2.passkeyExpirationMinutes &&
settings1.profileImageUrl == settings2.profileImageUrl &&
settings1.showContactGivesInline == settings2.showContactGivesInline &&
settings1.showGeneralAdvanced == settings2.showGeneralAdvanced &&
settings1.showShortcutBvc == settings2.showShortcutBvc &&
settings1.vapid == settings2.vapid &&
settings1.warnIfProdServer == settings2.warnIfProdServer &&
settings1.warnIfTestServer == settings2.warnIfTestServer &&
settings1.webPushServer == settings2.webPushServer &&
JSON.stringify(settings1.searchBoxes) ==
JSON.stringify(settings2.searchBoxes)
);
}
@ -830,23 +798,25 @@ export function generateComparisonYaml(comparison: DataComparison): string {
dexieSettings: comparison.dexieSettings.length,
sqliteSettings: comparison.sqliteSettings.filter(s => s.accountDid || s.activeDid).length,
dexieAccounts: comparison.dexieAccounts.length,
sqliteAccounts: comparison.sqliteAccounts.filter(a => a.did).length,
sqliteAccounts: comparison.sqliteAccounts.filter(a => a).length,
},
differences: {
contacts: {
added: comparison.differences.contacts.added.length,
modified: comparison.differences.contacts.modified.length,
unmodified: comparison.differences.contacts.unmodified.length,
missing: comparison.differences.contacts.missing.filter(c => c.did).length,
},
settings: {
added: comparison.differences.settings.added.length,
modified: comparison.differences.settings.modified.length,
unmodified: comparison.differences.settings.unmodified.length,
missing: comparison.differences.settings.missing.filter(s => s.accountDid || s.activeDid).length,
},
accounts: {
added: comparison.differences.accounts.added.length,
modified: comparison.differences.accounts.modified.length,
missing: comparison.differences.accounts.missing.filter(a => a.did).length,
unmodified: comparison.differences.accounts.unmodified.length,
missing: comparison.differences.accounts.missing.filter(a => a).length,
},
},
details: {
@ -888,15 +858,9 @@ export function generateComparisonYaml(comparison: DataComparison): string {
hasIdentity: !!a.identity,
hasMnemonic: !!a.mnemonic,
})),
sqlite: comparison.sqliteAccounts
.filter(a => a.did)
.map((a) => ({
id: a.id,
did: a.did,
dateCreated: a.dateCreated,
hasIdentity: !!a.identity,
hasMnemonic: !!a.mnemonic,
})),
sqlite: comparison.sqliteAccounts.map((a) => ({
did: a,
})),
},
},
};

131
src/views/DatabaseMigration.vue

@ -74,7 +74,7 @@
</div>
<div>
<p v-if="downloadMnemonic" class="text-green-500">
Here is your seed. Write it down!
Here is your seed for account {{ downloadMnemonicAddress?.substring('did:ethr:0x'.length).substring(0, 3) }} -- write it down!
<br />
{{ downloadMnemonic }}
</p>
@ -506,15 +506,15 @@
>
<div class="flex items-center">
<IconRenderer
icon-name="edit"
icon-name="check"
svg-class="h-5 w-5 text-yellow-600 mr-2"
/>
<span class="text-sm font-medium text-yellow-900"
>Modify</span
>Unmodified</span
>
</div>
<span class="text-sm font-bold text-yellow-900">{{
comparison.differences.accounts.modified.length
comparison.differences.accounts.unmodified.length
}}</span>
</div>
@ -559,48 +559,44 @@
</div>
</div>
<!-- Modify Accounts -->
<!-- Unmodified Accounts -->
<div
v-if="comparison.differences.accounts.modified.length > 0"
v-if="comparison.differences.accounts.unmodified.length > 0"
class="mt-4"
>
<h4 class="text-sm font-medium text-gray-900 mb-2">
Modify Accounts ({{ comparison.differences.accounts.modified.length }}):
Unmodified Accounts ({{ comparison.differences.accounts.unmodified.length }}):
</h4>
<div class="space-y-1">
<div
v-for="account in comparison.differences.accounts.modified"
:key="account.id"
class="text-xs text-gray-600 bg-gray-50 p-2 rounded"
>
<div class="font-medium">ID: {{ account.id }}</div>
<div class="text-gray-500">{{ account.did }}</div>
<div class="text-gray-400">Created: {{ account.dateCreated }}</div>
<div class="text-gray-400">Has Identity: {{ getAccountHasIdentity(account) ? 'Yes' : 'No' }}</div>
<div class="text-gray-400">Has Mnemonic: {{ getAccountHasMnemonic(account) ? 'Yes' : 'No' }}</div>
</div>
</div>
<div class="space-y-1">
<div
v-for="account in comparison.differences.accounts.unmodified"
:key="account.id"
class="text-xs text-gray-600 bg-gray-50 p-2 rounded"
>
<div class="font-medium">ID: {{ account.id }}</div>
<div class="text-gray-500">{{ account.did }}</div>
<div class="text-gray-400">Created: {{ account.dateCreated }}</div>
<div class="text-gray-400">Has Identity: {{ getAccountHasIdentity(account) ? 'Yes' : 'No' }}</div>
<div class="text-gray-400">Has Mnemonic: {{ getAccountHasMnemonic(account) ? 'Yes' : 'No' }}</div>
</div>
</div>
<!-- Keep Accounts -->
<div
v-if="comparison.differences.accounts.missing.filter(a => a.did).length > 0"
v-if="comparison.differences.accounts.missing.filter(a => a).length > 0"
class="mt-4"
>
<h4 class="text-sm font-medium text-gray-900 mb-2">
Keep Accounts ({{ comparison.differences.accounts.missing.filter(a => a.did).length }}):
Keep Accounts ({{ comparison.differences.accounts.missing.filter(a => a).length }}):
</h4>
<div class="space-y-1">
<div
v-for="account in comparison.differences.accounts.missing.filter(a => a.did)"
:key="account.id"
v-for="did in comparison.differences.accounts.missing.filter(a => a)"
:key="did"
class="text-xs text-gray-600 bg-gray-50 p-2 rounded"
>
<div class="font-medium">ID: {{ account.id }}</div>
<div class="text-gray-500">{{ account.did }}</div>
<div class="text-gray-400">Created: {{ account.dateCreated }}</div>
<div class="text-gray-400">Has Identity: {{ getAccountHasIdentity(account) ? 'Yes' : 'No' }}</div>
<div class="text-gray-400">Has Mnemonic: {{ getAccountHasMnemonic(account) ? 'Yes' : 'No' }}</div>
<div class="text-gray-500">{{ did }}</div>
</div>
</div>
</div>
@ -647,6 +643,23 @@
}}</span>
</div>
<div
class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg"
>
<div class="flex items-center">
<IconRenderer
icon-name="check"
svg-class="h-5 w-5 text-yellow-600 mr-2"
/>
<span class="text-sm font-medium text-yellow-900"
>Unmodified</span
>
</div>
<span class="text-sm font-bold text-yellow-900">{{
comparison.differences.settings.unmodified.length
}}</span>
</div>
<div
class="flex items-center justify-between p-3 bg-red-50 rounded-lg"
>
@ -707,6 +720,27 @@
</div>
</div>
<!-- Unmodified Settings -->
<div
v-if="comparison.differences.settings.unmodified.length > 0"
class="mt-4"
>
<h4 class="text-sm font-medium text-gray-900 mb-2">
Unmodified Settings ({{ comparison.differences.settings.unmodified.length }}):
</h4>
</div>
<div class="space-y-1">
<div
v-for="setting in comparison.differences.settings.unmodified"
:key="setting.id"
class="text-xs text-gray-600 bg-gray-50 p-2 rounded"
>
<div class="font-medium">{{ getSettingDisplayName(setting) }}</div>
<div class="text-gray-500">ID: {{ setting.id }}</div>
<div class="text-gray-400">Registered: {{ setting.isRegistered ? 'Yes' : 'No' }}</div>
</div>
</div>
<!-- Keep Settings -->
<div
v-if="comparison.differences.settings.missing.filter(s => s.accountDid || s.activeDid).length > 0"
@ -770,6 +804,23 @@
}}</span>
</div>
<div
class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg"
>
<div class="flex items-center">
<IconRenderer
icon-name="check"
svg-class="h-5 w-5 text-yellow-600 mr-2"
/>
<span class="text-sm font-medium text-yellow-900"
>Unmodified</span
>
</div>
<span class="text-sm font-bold text-yellow-900">{{
comparison.differences.contacts.unmodified.length
}}</span>
</div>
<div
class="flex items-center justify-between p-3 bg-red-50 rounded-lg"
>
@ -830,6 +881,27 @@
</div>
</div>
<!-- Unmodified Contacts -->
<div
v-if="comparison.differences.contacts.unmodified.length > 0"
class="mt-4"
>
<h4 class="text-sm font-medium text-gray-900 mb-2">
Unmodified Contacts ({{ comparison.differences.contacts.unmodified.length }}):
</h4>
</div>
<div class="space-y-1">
<div
v-for="contact in comparison.differences.contacts.unmodified"
:key="contact.did"
class="text-xs text-gray-600 bg-gray-50 p-2 rounded"
>
<div class="font-medium">{{ contact.name || '<empty>' }}</div>
<div class="text-gray-500">{{ contact.did }}</div>
<div class="text-gray-400">{{ contact.contactMethods?.length || 0 }} contact methods</div>
</div>
</div>
<!-- Keep Contacts -->
<div
v-if="comparison.differences.contacts.missing.filter(c => c.did).length > 0"
@ -871,7 +943,6 @@
</template>
<script lang="ts">
import { ref } from "vue";
import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";
@ -923,6 +994,7 @@ export default class DatabaseMigration extends Vue {
private cannotfindMainAccount = false;
private downloadSettingsContactsBlob?: Blob;
private downloadMnemonic?: string;
private downloadMnemonicAddress?: string;
private hasMultipleMnemonics = false;
private isLoading = false;
private loadingMessage = "";
@ -1024,6 +1096,7 @@ export default class DatabaseMigration extends Vue {
}
if (primaryAccount) {
this.downloadMnemonic = primaryAccount.mnemonic;
this.downloadMnemonicAddress = primaryAccount.did;
}
}

9
src/views/TestView.vue

@ -182,6 +182,15 @@
>
Accounts
</button>
<button
class="text-sm text-blue-600 hover:text-blue-800 underline"
@click="
sqlQuery = 'SELECT * FROM contacts;';
executeSql();
"
>
Contacts
</button>
<button
class="text-sm text-blue-600 hover:text-blue-800 underline"
@click="

Loading…
Cancel
Save