Compare commits

..

1 Commits

Author SHA1 Message Date
Matthew Raymer
3881144c62 feat: add missing settings management functions
- Add getSettingsForAccount() to retrieve complete settings for any account DID
- Add getAccountSpecificSettings() to get account-specific settings without defaults
- Add mergeSettings() for consistent settings merging across SQLite and Dexie
- Update retrieveSettingsForActiveAccount() to use new mergeSettings() function
- Improve error handling and logging for settings operations
- Maintain backward compatibility with existing settings API

This provides better settings management capabilities for account switching,
debugging, and data validation while ensuring consistent behavior across
both SQLite and Dexie storage implementations.
2025-06-10 12:44:52 +00:00
3 changed files with 320 additions and 177 deletions

View File

@@ -117,34 +117,14 @@ export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
return defaultSettings;
}
// Map and filter settings
// Map settings
const overrideSettings = mapColumnsToValues(
result.columns,
result.values,
)[0] as Settings;
const overrideSettingsFiltered = Object.fromEntries(
Object.entries(overrideSettings).filter(([_, v]) => v !== null),
);
// Merge settings
const settings = { ...defaultSettings, ...overrideSettingsFiltered };
// Handle searchBoxes parsing
if (settings.searchBoxes) {
try {
// @ts-expect-error - the searchBoxes field is a string in the DB
settings.searchBoxes = JSON.parse(settings.searchBoxes);
} catch (error) {
logConsoleAndDb(
`[databaseUtil] Failed to parse searchBoxes for ${defaultSettings.activeDid}: ${error}`,
true,
);
// Reset to empty array on parse failure
settings.searchBoxes = [];
}
}
return settings;
// Use the new mergeSettings function for consistency
return mergeSettings(defaultSettings, overrideSettings);
} catch (error) {
logConsoleAndDb(
`[databaseUtil] Failed to retrieve account settings for ${defaultSettings.activeDid}: ${error}`,
@@ -312,3 +292,128 @@ export function mapColumnsToValues(
return obj;
});
}
/**
* Retrieves settings for a specific account by DID
* @param accountDid - The DID of the account to retrieve settings for
* @returns Promise<Settings> Combined settings for the specified account
*/
export async function getSettingsForAccount(accountDid: string): Promise<Settings> {
try {
// Get default settings first
const defaultSettings = await retrieveSettingsForDefaultAccount();
// Get account-specific settings
const platform = PlatformServiceFactory.getInstance();
const result = await platform.dbQuery(
"SELECT * FROM settings WHERE accountDid = ?",
[accountDid],
);
if (!result?.values?.length) {
logConsoleAndDb(
`[databaseUtil] No account-specific settings found for ${accountDid}`,
);
return defaultSettings;
}
// Map and merge settings
const overrideSettings = mapColumnsToValues(
result.columns,
result.values,
)[0] as Settings;
return mergeSettings(defaultSettings, overrideSettings);
} catch (error) {
logConsoleAndDb(
`[databaseUtil] Failed to retrieve settings for account ${accountDid}: ${error}`,
true,
);
// Return default settings on error
return await retrieveSettingsForDefaultAccount();
}
}
/**
* Retrieves only account-specific settings for a given DID (without merging with defaults)
* @param accountDid - The DID of the account to retrieve settings for
* @returns Promise<Settings> Account-specific settings only
*/
export async function getAccountSpecificSettings(accountDid: string): Promise<Settings> {
try {
const platform = PlatformServiceFactory.getInstance();
const result = await platform.dbQuery(
"SELECT * FROM settings WHERE accountDid = ?",
[accountDid],
);
if (!result?.values?.length) {
logConsoleAndDb(
`[databaseUtil] No account-specific settings found for ${accountDid}`,
);
return {};
}
// Map settings
const settings = mapColumnsToValues(
result.columns,
result.values,
)[0] as Settings;
// Handle searchBoxes parsing
if (settings.searchBoxes) {
try {
// @ts-expect-error - the searchBoxes field is a string in the DB
settings.searchBoxes = JSON.parse(settings.searchBoxes);
} catch (error) {
logConsoleAndDb(
`[databaseUtil] Failed to parse searchBoxes for ${accountDid}: ${error}`,
true,
);
// Reset to empty array on parse failure
settings.searchBoxes = [];
}
}
return settings;
} catch (error) {
logConsoleAndDb(
`[databaseUtil] Failed to retrieve account-specific settings for ${accountDid}: ${error}`,
true,
);
return {};
}
}
/**
* Merges default settings with account-specific settings using consistent logic
* @param defaultSettings - The default/master settings
* @param accountSettings - The account-specific settings to merge
* @returns Settings - Merged settings with account-specific overrides
*/
export function mergeSettings(defaultSettings: Settings, accountSettings: Settings): Settings {
// Filter out null values from account settings
const accountSettingsFiltered = Object.fromEntries(
Object.entries(accountSettings).filter(([_, v]) => v !== null),
);
// Perform shallow merge (account settings override defaults)
const mergedSettings = { ...defaultSettings, ...accountSettingsFiltered };
// Handle searchBoxes parsing if present
if (mergedSettings.searchBoxes) {
try {
// @ts-expect-error - the searchBoxes field is a string in the DB
mergedSettings.searchBoxes = JSON.parse(mergedSettings.searchBoxes);
} catch (error) {
logConsoleAndDb(
`[databaseUtil] Failed to parse searchBoxes during merge: ${error}`,
true,
);
// Reset to empty array on parse failure
mergedSettings.searchBoxes = [];
}
}
return mergedSettings;
}

View File

@@ -265,6 +265,43 @@ export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
}
}
/**
* Retrieves settings for a specific account by DID
* @param accountDid - The DID of the account to retrieve settings for
* @returns Promise<Settings> Combined settings for the specified account
*/
export async function getSettingsForAccount(accountDid: string): Promise<Settings> {
const defaultSettings = await retrieveSettingsForDefaultAccount();
const overrideSettings =
(await db.settings
.where("accountDid")
.equals(accountDid)
.first()) || {};
return R.mergeDeepRight(defaultSettings, overrideSettings);
}
/**
* Retrieves only account-specific settings for a given DID (without merging with defaults)
* @param accountDid - The DID of the account to retrieve settings for
* @returns Promise<Settings> Account-specific settings only
*/
export async function getAccountSpecificSettings(accountDid: string): Promise<Settings> {
return (await db.settings
.where("accountDid")
.equals(accountDid)
.first()) || {};
}
/**
* Merges default settings with account-specific settings using consistent logic
* @param defaultSettings - The default/master settings
* @param accountSettings - The account-specific settings to merge
* @returns Settings - Merged settings with account-specific overrides
*/
export function mergeSettings(defaultSettings: Settings, accountSettings: Settings): Settings {
return R.mergeDeepRight(defaultSettings, accountSettings);
}
export async function updateAccountSettings(
accountDid: string,
settingsChanges: Settings,

View File

@@ -78,7 +78,7 @@
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2 h-10"
/>
<button
class="px-4 rounded-r bg-green-200 border border-green-400"
class="px-4 rounded-r bg-green-200 border border-l-0 border-green-400"
@click="onClickNewContact()"
>
<font-awesome icon="plus" class="fa-fw" />
@@ -86,8 +86,8 @@
</div>
<div v-if="contacts.length > 0" class="flex justify-between">
<div class="">
<div v-if="!showGiveNumbers" class="flex items-center">
<div class="w-full text-left">
<div v-if="!showGiveNumbers">
<input
type="checkbox"
:checked="contactsSelected.length === contacts.length"
@@ -101,29 +101,52 @@
/>
<button
v-if="!showGiveNumbers"
:class="
href=""
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-3 px-3 py-1.5 rounded-md"
:style="
contactsSelected.length > 0
? 'text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-3 px-3 py-1.5 rounded-md cursor-pointer'
: 'text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-slate-300 ml-3 px-3 py-1.5 rounded-md cursor-not-allowed'
? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);'
: 'background-image: linear-gradient(to bottom, #94a3b8, #374151);'
"
data-testId="copySelectedContactsButtonTop"
@click="copySelectedContacts()"
>
Copy
Copy Selections
</button>
<button @click="showCopySelectionsInfo()">
<font-awesome
icon="circle-info"
class="text-xl text-blue-500 ml-4"
/>
</button>
<font-awesome
@click="showCopySelectionsInfo()"
icon="circle-info"
class="text-2xl text-blue-500 ml-2"
/>
</div>
</div>
<div class="flex items-center gap-2">
<div class="w-full text-right">
<button
v-if="showGiveNumbers"
href=""
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
class="text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
@click="toggleShowContactAmounts()"
>
{{
showGiveNumbers ? "Hide Hours, Offer, etc" : "See Hours, Offer, etc"
}}
</button>
</div>
</div>
<div v-if="showGiveNumbers" class="flex justify-between mt-1">
<div class="w-full text-right">
In the following, only the most recent hours are included. To see more,
click
<span
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 py-1 rounded-md"
>
<font-awesome icon="file-lines" class="fa-fw" />
</span>
<br />
<button
href=""
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md mt-1"
:class="showGiveAmountsClassNames()"
@click="toggleShowGiveTotals()"
>
@@ -136,28 +159,6 @@
}}
<font-awesome icon="left-right" class="fa-fw" />
</button>
<button
href=""
class="text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
@click="toggleShowContactAmounts()"
>
{{
showGiveNumbers ? "Hide Actions" : "See Actions"
}}
</button>
</div>
</div>
<div v-if="showGiveNumbers" class="my-3">
<div class="w-full text-center text-sm italic text-slate-600">
Only the most recent hours are included. <br />To see more,
click
<span
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 py-0.5 rounded"
>
<font-awesome icon="file-lines" class="text-xs fa-fw" />
</span>
<br />
</div>
</div>
@@ -165,7 +166,7 @@
<ul
v-if="contacts.length > 0"
id="listContacts"
class="border-t border-slate-300 my-2"
class="border-t border-slate-300 mt-1"
>
<li
v-for="contact in filteredContacts()"
@@ -173,127 +174,125 @@
class="border-b border-slate-300 pt-1 pb-1"
data-testId="contactListItem"
>
<div class="flex items-center justify-between gap-3">
<div class="flex overflow-hidden min-w-0 items-center gap-3">
<input
v-if="!showGiveNumbers"
type="checkbox"
:checked="contactsSelected.includes(contact.did)"
class="ml-2 h-6 w-6 flex-shrink-0"
data-testId="contactCheckOne"
@click="
contactsSelected.includes(contact.did)
? contactsSelected.splice(
contactsSelected.indexOf(contact.did),
1,
)
: contactsSelected.push(contact.did)
"
/>
<div class="grow overflow-hidden">
<div class="flex items-center justify-between gap-3">
<div class="flex items-center gap-3">
<input
v-if="!showGiveNumbers"
type="checkbox"
:checked="contactsSelected.includes(contact.did)"
class="ml-2 h-6 w-6 flex-shrink-0"
data-testId="contactCheckOne"
@click="
contactsSelected.includes(contact.did)
? contactsSelected.splice(
contactsSelected.indexOf(contact.did),
1,
)
: contactsSelected.push(contact.did)
"
/>
<EntityIcon
:contact="contact"
:icon-size="48"
class="shrink-0 align-text-bottom border border-slate-300 rounded cursor-pointer overflow-hidden"
@click="showLargeIdenticon = contact"
/>
<div
class="flex-shrink-0 w-12 h-12 flex items-center justify-center"
>
<EntityIcon
:contact="contact"
:icon-size="48"
class="inline-block align-text-bottom border border-slate-300 rounded cursor-pointer overflow-hidden"
@click="showLargeIdenticon = contact"
/>
</div>
<div class="overflow-hidden">
<h2 class="text-base font-semibold truncate">
<router-link
:to="{
path: '/did/' + encodeURIComponent(contact.did),
}"
title="See more about this person"
>
{{ contactNameNonBreakingSpace(contact.name) }}
</router-link>
<h2 class="text-base font-semibold w-1/3 truncate flex-shrink-0">
{{ contactNameNonBreakingSpace(contact.name) }}
</h2>
<div class="flex gap-1.5 items-center overflow-hidden">
<router-link
:to="{
path: '/did/' + encodeURIComponent(contact.did),
}"
title="See more about this person"
>
<font-awesome
icon="circle-info"
class="text-base text-blue-500"
/>
</router-link>
<span>
<div class="flex gap-2 items-center">
<router-link
:to="{
path: '/did/' + encodeURIComponent(contact.did),
}"
title="See more about this person"
>
<font-awesome
icon="circle-info"
class="text-xl text-blue-500"
/>
</router-link>
<span class="text-xs truncate">{{
contact.did
}}</span>
</div>
<div class="text-sm">
{{ contact.notes }}
</div>
</div>
</div>
<div
v-if="showGiveNumbers && contact.did != activeDid"
class="flex gap-1.5 items-end"
>
<div class='text-center'>
<div class="text-xs leading-none mb-1">From/To</div>
<div class="flex items-center">
<button
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2.5 py-1.5 rounded-l-md"
:title="givenToMeDescriptions[contact.did] || ''"
@click="confirmShowGiftedDialog(contact.did, activeDid)"
>
{{
/* eslint-disable prettier/prettier */
showGiveTotals
? ((givenToMeConfirmed[contact.did] || 0)
+ (givenToMeUnconfirmed[contact.did] || 0))
: showGiveConfirmed
? (givenToMeConfirmed[contact.did] || 0)
: (givenToMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
}}
</button>
<button
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2.5 py-1.5 rounded-r-md border-l"
:title="givenByMeDescriptions[contact.did] || ''"
@click="confirmShowGiftedDialog(activeDid, contact.did)"
>
{{
/* eslint-disable prettier/prettier */
showGiveTotals
? ((givenByMeConfirmed[contact.did] || 0)
+ (givenByMeUnconfirmed[contact.did] || 0))
: showGiveConfirmed
? (givenByMeConfirmed[contact.did] || 0)
: (givenByMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
}}
</button>
</div>
<span class="text-sm overflow-hidden">{{
libsUtil.shortDid(contact.did)
}}</span>
</div>
<div class="text-sm">
{{ contact.notes }}
</div>
</span>
</div>
<button
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-md"
data-testId="offerButton"
@click="openOfferDialog(contact.did, contact.name)"
<div
v-if="showGiveNumbers && contact.did != activeDid"
class="flex gap-2 items-center"
>
Offer
</button>
<button
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-l-md"
:title="givenToMeDescriptions[contact.did] || ''"
@click="confirmShowGiftedDialog(contact.did, activeDid)"
>
From:
<br />
{{
/* eslint-disable prettier/prettier */
showGiveTotals
? ((givenToMeConfirmed[contact.did] || 0)
+ (givenToMeUnconfirmed[contact.did] || 0))
: showGiveConfirmed
? (givenToMeConfirmed[contact.did] || 0)
: (givenToMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
}}
</button>
<router-link
:to="{
name: 'contact-amounts',
query: { contactDid: contact.did },
}"
class="text-sm bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-md"
title="See more given activity"
>
<font-awesome icon="file-lines" class="fa-fw" />
</router-link>
<button
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white -ml-1.5 px-2 py-1.5 rounded-r-md border-l"
:title="givenByMeDescriptions[contact.did] || ''"
@click="confirmShowGiftedDialog(activeDid, contact.did)"
>
To:
<br />
{{
/* eslint-disable prettier/prettier */
showGiveTotals
? ((givenByMeConfirmed[contact.did] || 0)
+ (givenByMeUnconfirmed[contact.did] || 0))
: showGiveConfirmed
? (givenByMeConfirmed[contact.did] || 0)
: (givenByMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
}}
</button>
<button
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-md border border-blue-400"
data-testId="offerButton"
@click="openOfferDialog(contact.did, contact.name)"
>
Offer
</button>
<router-link
:to="{
name: 'contact-amounts',
query: { contactDid: contact.did },
}"
class="text-sm bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-md border border-slate-400"
title="See more given activity"
>
<font-awesome icon="file-lines" class="fa-fw" />
</router-link>
</div>
</div>
</div>
</li>
@@ -315,14 +314,16 @@
/>
<button
v-if="!showGiveNumbers"
:class="
href=""
class="text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-3 px-3 py-1.5 rounded-md"
:style="
contactsSelected.length > 0
? 'text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-3 px-3 py-1.5 rounded-md cursor-pointer'
: 'text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-slate-300 ml-3 px-3 py-1.5 rounded-md cursor-not-allowed'
? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);'
: 'background-image: linear-gradient(to bottom, #94a3b8, #374151);'
"
@click="copySelectedContacts()"
>
Copy
Copy Selections
</button>
</div>