Task #349 follow-up: brand-logo embed test + label module + user-scope guard
Original task: Executive Meetings PDF improvements — respect user
font prefs, render saved per-meeting rowColor, drop legacy
isHighlighted baking, brand logo on left of PDF header with title
"قائمة بأسماء حضور الاجتماعات", and a logo upload + font color
picker in the font-settings page.
Prior mark_task_complete was rejected for three issues. This commit
closes all of them:
- Extract bilingual PDF labels into
artifacts/api-server/src/lib/pdf-labels.ts and import it from the
PDF route. Mirror the same keys under executiveMeetings.pdf.* in
the tx-os ar.json / en.json locales so the frontend stays in
sync.
- Add an end-to-end test that exercises the brand-logo path: sign
an upload URL, PUT the bytes through the storage sidecar, save
the path on the global font-settings row, render the PDF, and
assert PDFKit emitted "/Subtype /Image" with "/Width 1". This
test caught a real silent failure: PDFKit's png-js decoder
rejected the canonical 67-byte base64 "smallest valid PNG", so
the renderer was logging a warning and proceeding without a
logo. The fixture now constructs a real 1x1 RGB PNG inline using
zlib + a CRC32 routine (no new dependencies). Skip is narrowed
to network errors / 5xx (4xx fails loudly), and the global-row
mutation is wrapped in try/finally so cleanup always runs.
- Reject user-scope writes that try to set logoObjectPath with a
400 ("logo_user_scope_forbidden") instead of silently dropping
the field. The brand logo is a global asset; silent drops would
mislead callers into thinking their upload was saved. Added a
focused test asserting the 400 response and that explicit
logoObjectPath:null on the user row still works.
Also removed 12 stray backup/snapshot files at the repo root
(*.old, *-base.{ts,tsx,mjs}, locale snapshots) that were
accidentally tracked in the previous commit.
Out of scope: three pre-existing tsc errors at lines 635/778/2921
of executive-meetings.ts (unrelated to PDF code) and a flaky
"Reorder: POST /reorder" test that was already failing before
these changes.
This commit is contained in:
@@ -207,8 +207,7 @@ type FontPrefs = {
|
||||
fontSize: number;
|
||||
fontWeight: "regular" | "bold";
|
||||
alignment: "start" | "center";
|
||||
// Hex color (#RRGGBB) the API now persists alongside the other font
|
||||
// prefs. Default "#000000" matches the historical hard-coded black.
|
||||
// #RRGGBB persisted with font prefs.
|
||||
fontColor: string;
|
||||
};
|
||||
|
||||
@@ -7360,9 +7359,7 @@ function FontSettingsSection({
|
||||
const { toast } = useToast();
|
||||
const [scope, setScope] = useState<"user" | "global">("user");
|
||||
const [prefs, setPrefs] = useState<FontPrefs>(font);
|
||||
// Local mirror of the org-wide logo path. Tracks server state but
|
||||
// also reflects in-flight upload/clear gestures so the preview
|
||||
// updates immediately without waiting for the next refetch.
|
||||
// Local mirror of the global logo path for instant preview.
|
||||
const [logoPath, setLogoPath] = useState<string | null>(globalLogoObjectPath);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
@@ -7373,10 +7370,7 @@ function FontSettingsSection({
|
||||
setLogoPath(globalLogoObjectPath);
|
||||
}, [globalLogoObjectPath]);
|
||||
|
||||
// Logo uploads always target the global brand asset, regardless of
|
||||
// the scope toggle, because per-user logos aren't a thing. The
|
||||
// upload completes against object storage; we only persist the
|
||||
// resulting `/objects/<id>` path when the user clicks "Save" below.
|
||||
// Logo is global-only; persisted on Save.
|
||||
const { uploadFile: uploadLogo, isUploading: isUploadingLogo } = useUpload({
|
||||
onSuccess: (resp: UploadResponse) => setLogoPath(resp.objectPath),
|
||||
onError: (err: Error) =>
|
||||
@@ -7390,10 +7384,7 @@ function FontSettingsSection({
|
||||
async function save() {
|
||||
setSaving(true);
|
||||
try {
|
||||
// Body shape: at the user scope we omit logoObjectPath entirely
|
||||
// (the API ignores it for user rows anyway, but omitting keeps
|
||||
// the payload tight). At the global scope we always send the
|
||||
// current path so "remove logo" persists as `null`.
|
||||
// Omit logoObjectPath on user scope; send it (incl. null) on global.
|
||||
const body: Record<string, unknown> = { scope, ...prefs };
|
||||
if (scope === "global") {
|
||||
body.logoObjectPath = logoPath;
|
||||
@@ -7418,9 +7409,7 @@ function FontSettingsSection({
|
||||
|
||||
function onLogoFileChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const file = e.target.files?.[0];
|
||||
// Reset the input so picking the same file twice in a row still
|
||||
// re-fires onChange — otherwise users hit a confusing dead state
|
||||
// after a failed upload.
|
||||
// Reset input so re-picking the same file fires onChange.
|
||||
e.target.value = "";
|
||||
if (!file) return;
|
||||
void uploadLogo(file);
|
||||
|
||||
Reference in New Issue
Block a user