diff --git a/src/components/DataExportSection.vue b/src/components/DataExportSection.vue index 27f36418..c44cab7b 100644 --- a/src/components/DataExportSection.vue +++ b/src/components/DataExportSection.vue @@ -63,13 +63,18 @@ backup and database export, with platform-specific download instructions. * * <script lang="ts"> import { Component, Prop, Vue } from "vue-facing-decorator"; import { AppString, NotificationIface, USE_DEXIE_DB } from "../constants/app"; -import { db } from "../db/index"; + +import { Contact } from "../db/tables/contacts"; +import * as databaseUtil from "../db/databaseUtil"; + import { logger } from "../utils/logger"; import { PlatformServiceFactory } from "../services/PlatformServiceFactory"; import { PlatformService, PlatformCapabilities, } from "../services/PlatformService"; +import { contactsToExportJson } from "../libs/util"; +import { db } from "../db/index"; /** * @vue-component @@ -131,24 +136,25 @@ export default class DataExportSection extends Vue { */ public async exportDatabase() { try { - if (!USE_DEXIE_DB) { - throw new Error("Not implemented"); + let allContacts: Contact[] = []; + const platformService = PlatformServiceFactory.getInstance(); + const result = await platformService.dbQuery(`SELECT * FROM contacts`); + if (result) { + allContacts = databaseUtil.mapQueryResultToValues( + result, + ) as unknown as Contact[]; } - const blob = await db.export({ - prettyJson: true, - transform: (table, value, key) => { - if (table === "contacts") { - // Dexie inserts a number 0 when some are undefined, so we need to totally remove them. - Object.keys(value).forEach((prop) => { - if (value[prop] === undefined) { - delete value[prop]; - } - }); - } - return { value, key }; - }, - }); - const fileName = `${AppString.APP_NAME_NO_SPACES}-backup.json`; + // if (USE_DEXIE_DB) { + // await db.open(); + // allContacts = await db.contacts.toArray(); + // } + + // Convert contacts to export format + const exportData = contactsToExportJson(allContacts); + const jsonStr = JSON.stringify(exportData, null, 2); + const blob = new Blob([jsonStr], { type: "application/json" }); + + const fileName = `${AppString.APP_NAME_NO_SPACES}-backup-contacts.json`; if (this.platformCapabilities.hasFileDownload) { // Web platform: Use download link @@ -160,8 +166,7 @@ export default class DataExportSection extends Vue { setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000); } else if (this.platformCapabilities.hasFileSystem) { // Native platform: Write to app directory - const content = await blob.text(); - await this.platformService.writeAndShareFile(fileName, content); + await this.platformService.writeAndShareFile(fileName, jsonStr); } else { throw new Error("This platform does not support file downloads."); } @@ -172,10 +177,10 @@ export default class DataExportSection extends Vue { type: "success", title: "Export Successful", text: this.platformCapabilities.hasFileDownload - ? "See your downloads directory for the backup. It is in the Dexie format." - : "You should have been prompted to save your backup file.", + ? "See your downloads directory for the backup. It is in JSON format." + : "The backup file has been saved.", }, - -1, + 3000, ); } catch (error) { logger.error("Export Error:", error); diff --git a/src/db/databaseUtil.ts b/src/db/databaseUtil.ts index 69c80b05..b2f56ec5 100644 --- a/src/db/databaseUtil.ts +++ b/src/db/databaseUtil.ts @@ -79,13 +79,9 @@ const DEFAULT_SETTINGS: Settings = { // retrieves default settings export async function retrieveSettingsForDefaultAccount(): Promise<Settings> { - console.log("[databaseUtil] retrieveSettingsForDefaultAccount"); const platform = PlatformServiceFactory.getInstance(); const sql = "SELECT * FROM settings WHERE id = ?"; - console.log("[databaseUtil] sql", sql); const result = await platform.dbQuery(sql, [MASTER_SETTINGS_KEY]); - console.log("[databaseUtil] result", JSON.stringify(result, null, 2)); - console.trace("Trace from [retrieveSettingsForDefaultAccount]"); if (!result) { return DEFAULT_SETTINGS; } else { @@ -103,21 +99,21 @@ export async function retrieveSettingsForDefaultAccount(): Promise<Settings> { /** * Retrieves settings for the active account, merging with default settings - * + * * @returns Promise<Settings> Combined settings with account-specific overrides * @throws Will log specific errors for debugging but returns default settings on failure */ export async function retrieveSettingsForActiveAccount(): Promise<Settings> { - logConsoleAndDb("[databaseUtil] Starting settings retrieval for active account"); try { // Get default settings first const defaultSettings = await retrieveSettingsForDefaultAccount(); - logConsoleAndDb(`[databaseUtil] Retrieved default settings (hasActiveDid: ${!!defaultSettings.activeDid})`); // If no active DID, return defaults if (!defaultSettings.activeDid) { - logConsoleAndDb("[databaseUtil] No active DID found, returning default settings"); + logConsoleAndDb( + "[databaseUtil] No active DID found, returning default settings", + ); return defaultSettings; } @@ -130,12 +126,17 @@ export async function retrieveSettingsForActiveAccount(): Promise<Settings> { ); if (!result?.values?.length) { - logConsoleAndDb(`[databaseUtil] No account-specific settings found for ${defaultSettings.activeDid}`); + logConsoleAndDb( + `[databaseUtil] No account-specific settings found for ${defaultSettings.activeDid}`, + ); return defaultSettings; } // Map and filter settings - const overrideSettings = mapColumnsToValues(result.columns, result.values)[0] as Settings; + const overrideSettings = mapColumnsToValues( + result.columns, + result.values, + )[0] as Settings; const overrideSettingsFiltered = Object.fromEntries( Object.entries(overrideSettings).filter(([_, v]) => v !== null), ); @@ -151,30 +152,27 @@ export async function retrieveSettingsForActiveAccount(): Promise<Settings> { } catch (error) { logConsoleAndDb( `[databaseUtil] Failed to parse searchBoxes for ${defaultSettings.activeDid}: ${error}`, - true + true, ); // Reset to empty array on parse failure settings.searchBoxes = []; } } - logConsoleAndDb( - `[databaseUtil] Successfully merged settings for ${defaultSettings.activeDid} ` + - `(overrides: ${Object.keys(overrideSettingsFiltered).length})` - ); return settings; - } catch (error) { logConsoleAndDb( `[databaseUtil] Failed to retrieve account settings for ${defaultSettings.activeDid}: ${error}`, - true + true, ); // Return defaults on error return defaultSettings; } - } catch (error) { - logConsoleAndDb(`[databaseUtil] Failed to retrieve default settings: ${error}`, true); + logConsoleAndDb( + `[databaseUtil] Failed to retrieve default settings: ${error}`, + true, + ); // Return minimal default settings on complete failure return { id: MASTER_SETTINGS_KEY, diff --git a/src/libs/util.ts b/src/libs/util.ts index b286c7f0..10d61af2 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -80,7 +80,10 @@ export const UNIT_LONG: Record<string, string> = { }; /* eslint-enable prettier/prettier */ -const UNIT_CODES: Record<string, { name: string; faIcon: string; decimals: number }> = { +const UNIT_CODES: Record< + string, + { name: string; faIcon: string; decimals: number } +> = { BTC: { name: "Bitcoin", faIcon: "bitcoin-sign", @@ -558,20 +561,15 @@ export const retrieveAccountMetadata = async ( }; export const retrieveAllAccountsMetadata = async (): Promise<Account[]> => { - console.log("[retrieveAllAccountsMetadata] start"); const platformService = PlatformServiceFactory.getInstance(); const sql = `SELECT * FROM accounts`; - console.log("[retrieveAllAccountsMetadata] sql: ", sql); const dbAccounts = await platformService.dbQuery(sql); - console.log("[retrieveAllAccountsMetadata] dbAccounts: ", dbAccounts); const accounts = databaseUtil.mapQueryResultToValues(dbAccounts) as Account[]; let result = accounts.map((account) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { identity, mnemonic, ...metadata } = account; return metadata as Account; }); - console.log("[retrieveAllAccountsMetadata] result: ", result); - console.log("[retrieveAllAccountsMetadata] USE_DEXIE_DB: ", USE_DEXIE_DB); if (USE_DEXIE_DB) { // one of the few times we use accountsDBPromise directly; try to avoid more usage const accountsDB = await accountsDBPromise; @@ -582,7 +580,6 @@ export const retrieveAllAccountsMetadata = async (): Promise<Account[]> => { return metadata as Account; }); } - console.log("[retrieveAllAccountsMetadata] end", JSON.stringify(result, null, 2)); return result; }; @@ -663,10 +660,6 @@ export async function saveNewIdentity( derivationPath: string, ): Promise<void> { try { - console.log("[saveNewIdentity] identity", identity); - console.log("[saveNewIdentity] mnemonic", mnemonic); - console.log("[saveNewIdentity] newId", newId); - console.log("[saveNewIdentity] derivationPath", derivationPath); // add to the new sql db const platformService = PlatformServiceFactory.getInstance(); const secrets = await platformService.dbQuery( @@ -685,7 +678,6 @@ export async function saveNewIdentity( const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic); const sql = `INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex) VALUES (?, ?, ?, ?, ?, ?)`; - console.log("[saveNewIdentity] sql: ", sql); const params = [ new Date().toISOString(), derivationPath, @@ -694,7 +686,6 @@ export async function saveNewIdentity( encryptedMnemonicBase64, newId.keys[0].publicKeyHex, ]; - console.log("[saveNewIdentity] params: ", params); await platformService.dbExec(sql, params); await databaseUtil.updateDefaultSettings({ activeDid: newId.did }); @@ -712,7 +703,6 @@ export async function saveNewIdentity( await updateDefaultSettings({ activeDid: newId.did }); } } catch (error) { - console.log("[saveNewIdentity] error: ", error); logger.error("Failed to update default settings:", error); throw new Error( "Failed to set default settings. Please try again or restart the app.", @@ -837,3 +827,94 @@ export const sendTestThroughPushServer = async ( logger.log("Got response from web push server:", response); return response; }; + +/** + * Converts a Contact object to a CSV line string following the established format. + * The format matches CONTACT_CSV_HEADER: "name,did,pubKeyBase64,seesMe,registered,contactMethods" + * where contactMethods is stored as a stringified JSON array. + * + * @param contact - The Contact object to convert + * @returns A CSV-formatted string representing the contact + * @throws {Error} If the contact object is missing required fields + */ +export const contactToCsvLine = (contact: Contact): string => { + if (!contact.did) { + throw new Error("Contact must have a did field"); + } + + // Escape fields that might contain commas or quotes + const escapeField = (field: string | boolean | undefined): string => { + if (field === undefined) return ""; + const str = String(field); + if (str.includes(",") || str.includes('"')) { + return `"${str.replace(/"/g, '""')}"`; + } + return str; + }; + + // Handle contactMethods array by stringifying it + const contactMethodsStr = contact.contactMethods + ? escapeField(JSON.stringify(contact.contactMethods)) + : ""; + + const fields = [ + escapeField(contact.name), + escapeField(contact.did), + escapeField(contact.publicKeyBase64), + escapeField(contact.seesMe), + escapeField(contact.registered), + contactMethodsStr, + ]; + + return fields.join(","); +}; + +/** + * Interface for the JSON export format of database tables + */ +export interface TableExportData { + tableName: string; + inbound: boolean; + rows: Array<Record<string, unknown>>; +} + +/** + * Interface for the complete database export format + */ +export interface DatabaseExport { + data: { + data: Array<TableExportData>; + }; +} + +/** + * Converts an array of contacts to the standardized database export JSON format. + * This format is used for data migration and backup purposes. + * + * @param contacts - Array of Contact objects to convert + * @returns DatabaseExport object in the standardized format + */ +export const contactsToExportJson = (contacts: Contact[]): DatabaseExport => { + // Convert each contact to a plain object and ensure all fields are included + const rows = contacts.map(contact => ({ + did: contact.did, + name: contact.name || null, + contactMethods: contact.contactMethods ? JSON.stringify(contact.contactMethods) : null, + nextPubKeyHashB64: contact.nextPubKeyHashB64 || null, + notes: contact.notes || null, + profileImageUrl: contact.profileImageUrl || null, + publicKeyBase64: contact.publicKeyBase64 || null, + seesMe: contact.seesMe || false, + registered: contact.registered || false + })); + + return { + data: { + data: [{ + tableName: "contacts", + inbound: true, + rows + }] + } + }; +}; diff --git a/src/services/migrationService.ts b/src/services/migrationService.ts index 8d90b0f4..bad8af8b 100644 --- a/src/services/migrationService.ts +++ b/src/services/migrationService.ts @@ -31,8 +31,6 @@ export class MigrationService { sqlQuery: (sql: string) => Promise<T>, extractMigrationNames: (result: T) => Set<string>, ): Promise<void> { - // eslint-disable-next-line no-console - console.log("Will run migrations"); // Create migrations table if it doesn't exist const result0 = await sqlExec(` @@ -42,29 +40,21 @@ export class MigrationService { executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); `); - // eslint-disable-next-line no-console - console.log("Created migrations table", JSON.stringify(result0)); // Get list of executed migrations const result1: T = await sqlQuery("SELECT name FROM migrations;"); const executedMigrations = extractMigrationNames(result1); - // eslint-disable-next-line no-console - console.log( - "Executed migration select", - JSON.stringify(executedMigrations), - ); + // Run pending migrations in order for (const migration of this.migrations) { if (!executedMigrations.has(migration.name)) { - const result2 = await sqlExec(migration.sql); - // eslint-disable-next-line no-console - console.log("Executed migration", JSON.stringify(result2)); - const result3 = await sqlExec( + await sqlExec(migration.sql); + + await sqlExec( `INSERT INTO migrations (name) VALUES ('${migration.name}')`, ); - // eslint-disable-next-line no-console - console.log("Updated migrations table", JSON.stringify(result3)); + } } } diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts index 1ce0bca7..5c34dbfa 100644 --- a/src/services/platforms/CapacitorPlatformService.ts +++ b/src/services/platforms/CapacitorPlatformService.ts @@ -128,8 +128,6 @@ export class CapacitorPlatformService implements PlatformService { let result: unknown; switch (operation.type) { case "run": { - console.log("[CapacitorPlatformService] running sql:", operation.sql); - console.log("[CapacitorPlatformService] params:", operation.params); const runResult = await this.db.run( operation.sql, operation.params, @@ -141,8 +139,6 @@ export class CapacitorPlatformService implements PlatformService { break; } case "query": { - console.log("[CapacitorPlatformService] querying sql:", operation.sql); - console.log("[CapacitorPlatformService] params:", operation.params); const queryResult = await this.db.query( operation.sql, operation.params, @@ -158,15 +154,9 @@ export class CapacitorPlatformService implements PlatformService { } operation.resolve(result); } catch (error) { - // make sure you don't try to log to the DB... infinite loop! - // eslint-disable-next-line no-console - console.error( + logger.error( "[CapacitorPlatformService] Error while processing SQL queue:", error, - " ... for sql:", - operation.sql, - " ... with params:", - operation.params, ); operation.reject(error); } diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index 66e4bf74..7269116a 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -1119,7 +1119,6 @@ export default class AccountViewView extends Vue { */ async mounted() { try { - console.log("[AccountViewView] mounted"); // Initialize component state with values from the database or defaults await this.initializeState(); await this.processIdentity(); @@ -1172,7 +1171,6 @@ export default class AccountViewView extends Vue { } } } catch (error) { - console.log("[AccountViewView] error: ", JSON.stringify(error, null, 2)); // this can happen when running automated tests in dev mode because notifications don't work logger.error( "Telling user to clear cache at page create because:", @@ -1206,7 +1204,6 @@ export default class AccountViewView extends Vue { this.turnOffNotifyingFlags(); } } - // console.log("Got to the end of 'mounted' call in AccountViewView."); /** * Beware! I've seen where we never get to this point because "ready" never resolves. */ diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index e42f0929..b16af716 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -521,7 +521,9 @@ export default class HomeView extends Vue { logConsoleAndDb(`[HomeView] Retrieved ${this.allMyDids.length} DIDs`); } catch (error) { logConsoleAndDb(`[HomeView] Failed to retrieve DIDs: ${error}`, true); - throw new Error("Failed to load existing identities. Please try restarting the app."); + throw new Error( + "Failed to load existing identities. Please try restarting the app.", + ); } // Create new DID if needed @@ -534,7 +536,10 @@ export default class HomeView extends Vue { logConsoleAndDb(`[HomeView] Created new identity: ${newDid}`); } catch (error) { this.isCreatingIdentifier = false; - logConsoleAndDb(`[HomeView] Failed to create new identity: ${error}`, true); + logConsoleAndDb( + `[HomeView] Failed to create new identity: ${error}`, + true, + ); throw new Error("Failed to create new identity. Please try again."); } } @@ -546,34 +551,53 @@ export default class HomeView extends Vue { if (USE_DEXIE_DB) { settings = await retrieveSettingsForActiveAccount(); } - logConsoleAndDb(`[HomeView] Retrieved settings for ${settings.activeDid || 'no active DID'}`); + logConsoleAndDb( + `[HomeView] Retrieved settings for ${settings.activeDid || "no active DID"}`, + ); } catch (error) { - logConsoleAndDb(`[HomeView] Failed to retrieve settings: ${error}`, true); - throw new Error("Failed to load user settings. Some features may be limited."); + logConsoleAndDb( + `[HomeView] Failed to retrieve settings: ${error}`, + true, + ); + throw new Error( + "Failed to load user settings. Some features may be limited.", + ); } // Update component state this.apiServer = settings.apiServer || ""; this.activeDid = settings.activeDid || ""; - + // Load contacts with graceful fallback try { const platformService = PlatformServiceFactory.getInstance(); - const dbContacts = await platformService.dbQuery("SELECT * FROM contacts"); - this.allContacts = databaseUtil.mapQueryResultToValues(dbContacts) as Contact[]; + const dbContacts = await platformService.dbQuery( + "SELECT * FROM contacts", + ); + this.allContacts = databaseUtil.mapQueryResultToValues( + dbContacts, + ) as Contact[]; if (USE_DEXIE_DB) { this.allContacts = await db.contacts.toArray(); } - logConsoleAndDb(`[HomeView] Retrieved ${this.allContacts.length} contacts`); + logConsoleAndDb( + `[HomeView] Retrieved ${this.allContacts.length} contacts`, + ); } catch (error) { - logConsoleAndDb(`[HomeView] Failed to retrieve contacts: ${error}`, true); + logConsoleAndDb( + `[HomeView] Failed to retrieve contacts: ${error}`, + true, + ); this.allContacts = []; // Ensure we have a valid empty array - this.$notify({ - group: "alert", - type: "warning", - title: "Contact Loading Issue", - text: "Some contact information may be unavailable.", - }, 5000); + this.$notify( + { + group: "alert", + type: "warning", + title: "Contact Loading Issue", + text: "Some contact information may be unavailable.", + }, + 5000, + ); } // Update remaining settings @@ -583,14 +607,17 @@ export default class HomeView extends Vue { this.isFeedFilteredByNearby = !!settings.filterFeedByNearby; this.isRegistered = !!settings.isRegistered; this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId; - this.lastAckedOfferToUserProjectsJwtId = settings.lastAckedOfferToUserProjectsJwtId; + this.lastAckedOfferToUserProjectsJwtId = + settings.lastAckedOfferToUserProjectsJwtId; this.searchBoxes = settings.searchBoxes || []; this.showShortcutBvc = !!settings.showShortcutBvc; this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings); // Check onboarding status if (!settings.finishedOnboarding) { - (this.$refs.onboardingDialog as OnboardingDialog).open(OnboardPage.Home); + (this.$refs.onboardingDialog as OnboardingDialog).open( + OnboardPage.Home, + ); } // Check registration status if needed @@ -613,10 +640,15 @@ export default class HomeView extends Vue { }); } this.isRegistered = true; - logConsoleAndDb(`[HomeView] User ${this.activeDid} is now registered`); + logConsoleAndDb( + `[HomeView] User ${this.activeDid} is now registered`, + ); } } catch (error) { - logConsoleAndDb(`[HomeView] Registration check failed: ${error}`, true); + logConsoleAndDb( + `[HomeView] Registration check failed: ${error}`, + true, + ); // Continue as unregistered - this is expected for new users } } @@ -624,8 +656,11 @@ export default class HomeView extends Vue { // Initialize feed and offers try { // Start feed update in background - this.updateAllFeed().catch(error => { - logConsoleAndDb(`[HomeView] Background feed update failed: ${error}`, true); + this.updateAllFeed().catch((error) => { + logConsoleAndDb( + `[HomeView] Background feed update failed: ${error}`, + true, + ); }); // Load new offers if we have an active DID @@ -649,23 +684,28 @@ export default class HomeView extends Vue { this.newOffersToUserHitLimit = offersToUser.hitLimit; this.numNewOffersToUserProjects = offersToProjects.data.length; this.newOffersToUserProjectsHitLimit = offersToProjects.hitLimit; - + logConsoleAndDb( `[HomeView] Retrieved ${this.numNewOffersToUser} user offers and ` + - `${this.numNewOffersToUserProjects} project offers` + `${this.numNewOffersToUserProjects} project offers`, ); } } catch (error) { - logConsoleAndDb(`[HomeView] Failed to initialize feed/offers: ${error}`, true); + logConsoleAndDb( + `[HomeView] Failed to initialize feed/offers: ${error}`, + true, + ); // Don't throw - we can continue with empty feed - this.$notify({ - group: "alert", - type: "warning", - title: "Feed Loading Issue", - text: "Some feed data may be unavailable. Pull to refresh.", - }, 5000); + this.$notify( + { + group: "alert", + type: "warning", + title: "Feed Loading Issue", + text: "Some feed data may be unavailable. Pull to refresh.", + }, + 5000, + ); } - } catch (error) { this.handleError(error); throw error; // Re-throw to be caught by mounted() @@ -837,10 +877,10 @@ export default class HomeView extends Vue { private handleError(err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err); const userMessage = (err as { userMessage?: string })?.userMessage; - + logConsoleAndDb( - `[HomeView] Initialization error: ${errorMessage}${userMessage ? ` (${userMessage})` : ''}`, - true + `[HomeView] Initialization error: ${errorMessage}${userMessage ? ` (${userMessage})` : ""}`, + true, ); this.$notify( @@ -848,7 +888,9 @@ export default class HomeView extends Vue { group: "alert", type: "danger", title: "Error", - text: userMessage || "There was an error loading your data. Please try refreshing the page.", + text: + userMessage || + "There was an error loading your data. Please try refreshing the page.", }, 5000, ); diff --git a/src/views/IdentitySwitcherView.vue b/src/views/IdentitySwitcherView.vue index 04d9c4ac..1bd2a355 100644 --- a/src/views/IdentitySwitcherView.vue +++ b/src/views/IdentitySwitcherView.vue @@ -139,10 +139,8 @@ export default class IdentitySwitcherView extends Vue { this.apiServerInput = settings.apiServer || ""; const accounts = await retrieveAllAccountsMetadata(); - console.log("[IdentitySwitcherView] accounts: ", JSON.stringify(accounts, null, 2)); for (let n = 0; n < accounts.length; n++) { const acct = accounts[n]; - console.log("[IdentitySwitcherView] acct: ", JSON.stringify(acct, null, 2)); this.otherIdentities.push({ id: (acct.id ?? 0).toString(), did: acct.did, @@ -152,7 +150,6 @@ export default class IdentitySwitcherView extends Vue { } } } catch (err) { - console.log("[IdentitySwitcherView] error: ", JSON.stringify(err, null, 2)); this.$notify( { group: "alert", @@ -164,7 +161,6 @@ export default class IdentitySwitcherView extends Vue { ); logger.error("Telling user to clear cache at page create because:", err); } - console.log("[IdentitySwitcherView] end"); } async switchAccount(did?: string) { diff --git a/src/views/ProjectViewView.vue b/src/views/ProjectViewView.vue index b2f23258..7ba234b5 100644 --- a/src/views/ProjectViewView.vue +++ b/src/views/ProjectViewView.vue @@ -389,7 +389,12 @@ {{ libsUtil.formattedAmount(givenTotalHours(), "HUR") }} </span> <span v-else> - {{ libsUtil.formattedAmount(givesTotalsByUnit[0].amount, givesTotalsByUnit[0].unit) }} + {{ + libsUtil.formattedAmount( + givesTotalsByUnit[0].amount, + givesTotalsByUnit[0].unit, + ) + }} </span> <span v-if="givesTotalsByUnit.length > 1">...</span> <span>