Add appearance settings to user settings schema and manager
All checks were successful
farmcontrol/farmcontrol-ws/pipeline/head This commit looks good

- Introduced a new `appearance` field in the user settings schema to allow customization options.
- Updated the `TOP_LEVEL_CATEGORIES` to include `appearance` for better organization of settings.
- Modified the `createEmptySettings` and `normalizeSettings` functions to handle the new `appearance` field.
- Enhanced the `UserSettingsManager` to ensure proper retrieval and updating of appearance settings.
- Added a test case to verify that appearance settings are written correctly at the top level, ensuring they are not nested under defaults.
This commit is contained in:
Tom Butcher 2026-09-04 02:32:51 +01:00
parent 9447a3206e
commit 4bdd624755
3 changed files with 43 additions and 6 deletions

View File

@ -24,6 +24,7 @@ const userSettingsSchema = new mongoose.Schema({
collapseState: { type: Schema.Types.Mixed, default: () => ({}) }, collapseState: { type: Schema.Types.Mixed, default: () => ({}) },
}, },
pageLayout: { type: Schema.Types.Mixed, default: () => ({}) }, pageLayout: { type: Schema.Types.Mixed, default: () => ({}) },
appearance: { type: Schema.Types.Mixed, default: () => ({}) },
createdAt: { createdAt: {
type: Date, type: Date,

View File

@ -8,7 +8,7 @@ const DEFAULTS_CATEGORIES = [
'collapseState' 'collapseState'
]; ];
const TOP_LEVEL_CATEGORIES = ['pageLayout']; const TOP_LEVEL_CATEGORIES = ['pageLayout', 'appearance'];
const SETTINGS_CATEGORIES = [...DEFAULTS_CATEGORIES, ...TOP_LEVEL_CATEGORIES]; const SETTINGS_CATEGORIES = [...DEFAULTS_CATEGORIES, ...TOP_LEVEL_CATEGORIES];
@ -18,7 +18,8 @@ const createEmptySettings = () => ({
sortSidebarVisibility: {}, sortSidebarVisibility: {},
columnVisibility: {}, columnVisibility: {},
collapseState: {}, collapseState: {},
pageLayout: {} pageLayout: {},
appearance: {}
}); });
const normalizeCategory = category => const normalizeCategory = category =>
@ -36,7 +37,8 @@ const normalizeSettings = (userSettings = {}) => {
sortSidebarVisibility: normalizeCategory(defaults?.sortSidebarVisibility), sortSidebarVisibility: normalizeCategory(defaults?.sortSidebarVisibility),
columnVisibility: normalizeCategory(defaults?.columnVisibility), columnVisibility: normalizeCategory(defaults?.columnVisibility),
collapseState: normalizeCategory(defaults?.collapseState), collapseState: normalizeCategory(defaults?.collapseState),
pageLayout: normalizeCategory(userSettings?.pageLayout) pageLayout: normalizeCategory(userSettings?.pageLayout),
appearance: normalizeCategory(userSettings?.appearance)
}; };
}; };
@ -70,7 +72,7 @@ export class UserSettingsManager {
const userId = this.getUserId(); const userId = this.getUserId();
const userSettings = await userSettingsModel const userSettings = await userSettingsModel
.findOne({ user: userId }) .findOne({ user: userId })
.select('defaults pageLayout') .select('defaults pageLayout appearance')
.lean(); .lean();
return normalizeSettings(userSettings); return normalizeSettings(userSettings);
@ -108,7 +110,7 @@ export class UserSettingsManager {
upsert: true, upsert: true,
setDefaultsOnInsert: true setDefaultsOnInsert: true
}) })
.select('defaults pageLayout') .select('defaults pageLayout appearance')
.lean(); .lean();
} catch (error) { } catch (error) {
if (error?.code !== 11000) { if (error?.code !== 11000) {
@ -121,7 +123,7 @@ export class UserSettingsManager {
{ $set: update.$set }, { $set: update.$set },
{ new: true } { new: true }
) )
.select('defaults pageLayout') .select('defaults pageLayout appearance')
.lean(); .lean();
} }

View File

@ -73,6 +73,40 @@ describe('UserSettingsManager', () => {
); );
}); });
it('writes appearance at the top level, not under defaults', async () => {
const appearance = { theme: 'dark', density: 'compact' }
findOneAndUpdate.mockReturnValue(
queryResult({
defaults: createEmptySettings(),
appearance
})
)
await expect(
manager.updateUserSettings({
category: 'appearance',
key: 'theme',
value: 'dark'
})
).resolves.toEqual({
...createEmptySettings(),
appearance
})
expect(findOneAndUpdate).toHaveBeenCalledWith(
{ user: 'user-id' },
expect.objectContaining({
$set: expect.objectContaining({
'appearance.theme': 'dark'
})
}),
expect.anything()
)
expect(findOneAndUpdate.mock.calls[0][1].$set).not.toHaveProperty(
'defaults.appearance.theme'
)
})
it('writes pageLayout at the top level, not under defaults', async () => { it('writes pageLayout at the top level, not under defaults', async () => {
const pageLayout = { const pageLayout = {
InventoryOverview: { InventoryOverview: {