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:
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user