Fix PDF font color not reflecting system settings via per-field merge
Root cause: resolveFontPrefsForUser() used `userRow ?? globalRow` whole-row precedence. When an admin saved font settings with scope="global", only the global row was updated. If the admin also had a user-scope row (created by prior saves with the default scope="user"), ALL fields from the user-scope row overrode the global row — including fontColor — causing the PDF to show the old color even after changing global settings. Schema change (executive-meetings.ts): - Made fontFamily, fontSize, fontWeight, alignment, fontColor nullable. User-scope rows now store NULL for fields that inherit from global, and only store non-null values for fields the user explicitly overrode. Backend fix (executive-meetings.ts): - resolveFontPrefsForUser: per-field merge — user non-null → global non-null → schema default. - PATCH handler for user-scope: after upsert, compares each field with the current global values. Fields matching global are set to NULL (= inherit). The post-nullification row is returned in the API response and audit log. - No user-scope rows are deleted; scope isolation is preserved. Frontend fix (executive-meetings.tsx): - effectiveFont computed via per-field merge (u?.field ?? g?.field ?? default) - FontSettingsResponse type updated for nullable fields (FontSettingsRow) - Scope switching in FontSettingsSection loads the selected scope's values (global → globalFont ?? DEFAULT_FONT; user → effective font) - globalFont prop threaded through SettingsSection → FontSettingsSection Data migration: existing user-scope rows normalized via SQL — fields matching global values set to NULL so per-field inheritance applies immediately. Verified: TypeScript clean, e2e Playwright test passes, API tests confirm per-field merge, nullification, and PDF color propagation.
This commit is contained in:
@@ -270,16 +270,11 @@ export const executiveMeetingFontSettingsTable = pgTable(
|
||||
id: serial("id").primaryKey(),
|
||||
scope: varchar("scope", { length: 16 }).notNull().default("global"),
|
||||
userId: integer("user_id").references(() => usersTable.id, { onDelete: "cascade" }),
|
||||
fontFamily: varchar("font_family", { length: 100 }).notNull().default("system"),
|
||||
fontSize: integer("font_size").notNull().default(14),
|
||||
fontWeight: varchar("font_weight", { length: 16 }).notNull().default("regular"),
|
||||
alignment: varchar("alignment", { length: 16 }).notNull().default("start"),
|
||||
// Hex color (#RRGGBB) applied to body cell text in both the on-screen
|
||||
// schedule and the generated PDF. Default "#000000" preserves the
|
||||
// historical black-text appearance for rows created before this
|
||||
// column existed. Validated app-side by the regex in
|
||||
// `fontSettingsSchema`.
|
||||
fontColor: varchar("font_color", { length: 16 }).notNull().default("#000000"),
|
||||
fontFamily: varchar("font_family", { length: 100 }).default("system"),
|
||||
fontSize: integer("font_size").default(14),
|
||||
fontWeight: varchar("font_weight", { length: 16 }).default("regular"),
|
||||
alignment: varchar("alignment", { length: 16 }).default("start"),
|
||||
fontColor: varchar("font_color", { length: 16 }).default("#000000"),
|
||||
// Object-storage path (`/objects/<id>`) of the brand logo embedded
|
||||
// in the top-left of the generated PDF header. NULL = no logo. Only
|
||||
// meaningful on the global-scope row (the per-user row ignores it).
|
||||
|
||||
Reference in New Issue
Block a user