Task #352: Make logo upload always visible for admin users

Previously, the logo upload field in Font Settings was hidden behind two
conditions: canEditGlobal AND scope === "global". Admin users had to
first switch the scope dropdown to "Global" before the logo upload
appeared, making it very hard to discover.

Changes:
- Frontend (executive-meetings.tsx): Removed `scope === "global"`
  condition from logo upload rendering — now shows whenever
  `canEditGlobal` is true (admin/executive_office_manager).
- Frontend: Save function now always sends `logoObjectPath` when
  `canEditGlobal` is true, regardless of selected scope.
- Backend (executive-meetings.ts): When an admin saves with user scope
  and includes logoObjectPath, the logo is written to the global row
  (upsert) in the same transaction. This preserves the global-only
  semantics while allowing admins to update the logo from any scope.
- Removed the 400 rejection for logoObjectPath on user-scope saves.
- Authorization preserved: only EM_ADMIN_ROLES can trigger logo writes.

Tested:
- API: PATCH with scope=user + logoObjectPath correctly updates global row
- E2E: Logo upload field visible for admin in user scope (confirmed)
- Pre-existing test failures unchanged (PDF font assertion, notification tests)
This commit is contained in:
riyadhafraa
2026-05-04 06:40:35 +00:00
parent 6a964ed9d9
commit fcdc1b8e8e
3 changed files with 43 additions and 16 deletions
@@ -2889,15 +2889,14 @@ const upsertFontSettingsHandler = async (
}
}
const targetUserId = data.scope === "global" ? null : userId;
// Reject user-scope logo writes loudly; null/undefined are fine.
if (data.scope === "user" && data.logoObjectPath != null) {
res.status(400).json({
error: "logoObjectPath is only allowed on the global font-settings row",
code: "logo_user_scope_forbidden",
});
return;
}
const logoForWrite = data.scope === "global" ? data.logoObjectPath : undefined;
const userRoleNames = await getRoleNamesForUser(userId);
const isAdmin = EM_ADMIN_ROLES.some((r) => userRoleNames.has(r));
const logoSentByAdmin =
isAdmin && data.logoObjectPath !== undefined;
const logoForFontRow =
data.scope === "global" && logoSentByAdmin
? data.logoObjectPath
: undefined;
const result = await db.transaction(async (tx) => {
const [existing] = await tx
.select()
@@ -2919,9 +2918,8 @@ const upsertFontSettingsHandler = async (
alignment: data.alignment,
fontColor: data.fontColor,
};
// Distinguish null (clear) from undefined (skip).
if (logoForWrite !== undefined) {
updateSet.logoObjectPath = logoForWrite;
if (logoForFontRow !== undefined) {
updateSet.logoObjectPath = logoForFontRow;
}
const [updated] = await tx
.update(executiveMeetingFontSettingsTable)
@@ -2938,7 +2936,7 @@ const upsertFontSettingsHandler = async (
fontWeight: data.fontWeight,
alignment: data.alignment,
fontColor: data.fontColor,
logoObjectPath: logoForWrite ?? null,
logoObjectPath: logoForFontRow ?? null,
};
const [inserted] = await tx
.insert(executiveMeetingFontSettingsTable)
@@ -2946,6 +2944,36 @@ const upsertFontSettingsHandler = async (
.returning();
row = inserted;
}
if (logoSentByAdmin && data.scope !== "global") {
const [globalRow] = await tx
.select()
.from(executiveMeetingFontSettingsTable)
.where(
and(
eq(executiveMeetingFontSettingsTable.scope, "global"),
isNull(executiveMeetingFontSettingsTable.userId),
),
);
if (globalRow) {
await tx
.update(executiveMeetingFontSettingsTable)
.set({ logoObjectPath: data.logoObjectPath })
.where(eq(executiveMeetingFontSettingsTable.id, globalRow.id));
} else {
await tx
.insert(executiveMeetingFontSettingsTable)
.values({
scope: "global",
userId: null,
fontFamily: data.fontFamily,
fontSize: data.fontSize,
fontWeight: data.fontWeight,
alignment: data.alignment,
fontColor: data.fontColor,
logoObjectPath: data.logoObjectPath ?? null,
});
}
}
await logAudit(tx, {
action: existing ? "update" : "create",
entityType: "font_settings",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

@@ -7384,9 +7384,8 @@ function FontSettingsSection({
async function save() {
setSaving(true);
try {
// Omit logoObjectPath on user scope; send it (incl. null) on global.
const body: Record<string, unknown> = { scope, ...prefs };
if (scope === "global") {
if (canEditGlobal) {
body.logoObjectPath = logoPath;
}
await apiJson(`/api/executive-meetings/font-settings`, {
@@ -7545,7 +7544,7 @@ function FontSettingsSection({
/>
</div>
</FormRow>
{canEditGlobal && scope === "global" && (
{canEditGlobal && (
<FormRow label={t("executiveMeetings.fontSettingsPage.logo.label")}>
<div className="flex flex-col gap-2">
{logoPath && (