From 4dcad6f69aa2f3fe61fe02c6a1975704a783a66b Mon Sep 17 00:00:00 2001 From: Riyadh Date: Sun, 10 May 2026 16:07:20 +0000 Subject: [PATCH] #481 Polish cascade-affected meetings table in postpone/reschedule prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Drop the leading "#" index column; dailyNumber already serves as a stable per-row identifier. - Replace the hard-coded amber background/border with the user's chosen alert accent via hexToRgba(accent, 0.12 / 0.45). Threaded `accent` through PostponeDialog → CascadePromptBlock so children don't read prefs directly. Both the loading and main prompt blocks track the accent; the rose blocked-by-midnight variant is intentionally kept. - Render times as localized 12-hour with ص/م (AR) or AM/PM (EN) by reusing the shared formatTime helper instead of slicing "HH:mm". - Drop now-unused i18n key cascadeColIndex from ar.json + en.json. - Update the e2e test to match the 3-column schema (meeting#, title, times); kept the dailyNumber assertion in the first cell. tsc clean. Cascade specs pass (Postpone by 10, Reschedule cascade, Cascade prompt UI: Shift/Keep, no-followers fallthrough). --- .../upcoming-meeting-alert.tsx | 39 +++++++++++++------ artifacts/tx-os/src/locales/ar.json | 1 - artifacts/tx-os/src/locales/en.json | 1 - ...executive-meetings-upcoming-alert.spec.mjs | 8 ++-- 4 files changed, 32 insertions(+), 17 deletions(-) diff --git a/artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx b/artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx index 8872d147..e5bb818f 100644 --- a/artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx +++ b/artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx @@ -761,6 +761,7 @@ export function UpcomingMeetingAlert() { meeting={meeting} isRtl={isRtl} meetingNumbersById={meetingNumbersById} + accent={alertPrefs.accent} onSaved={async () => { setPostponeOpen(false); // Invalidate today and the alert-state cache so the alert @@ -831,6 +832,8 @@ function CascadePromptBlock({ isRtl, busy, meetingNumbersById, + accent, + lang, onShiftFollowing, onKeepTimes, onBack, @@ -843,11 +846,20 @@ function CascadePromptBlock({ // Built by the parent from already-loaded dayData.meetings; missing // ids fall back to "—" rather than blocking the prompt. meetingNumbersById: Map; + // #481: accent hex from the user's alert color theme, used to derive + // the prompt's background + border via hexToRgba so the cascade block + // matches the rest of the alert instead of hard-coded amber. + accent: string; + // #481: active i18n language so times render localized 12-hour with + // ص/م (AR) or AM/PM (EN). + lang: string; onShiftFollowing: () => void; onKeepTimes: () => void; onBack: () => void; }) { const { t } = useTranslation(); + const promptBg = hexToRgba(accent, 0.12); + const promptBorder = hexToRgba(accent, 0.45); // While the preview is in flight, we still render the block (so the // user sees something happen after they click "Yes") but with empty // content + a loading hint. The buttons are disabled until either @@ -855,7 +867,8 @@ function CascadePromptBlock({ if (prompt.loading) { return (

@@ -871,8 +884,6 @@ function CascadePromptBlock({ : m.titleEn || m.titleAr; return raw.replace(/<[^>]+>/g, "").trim() || raw; }; - const fmtTime = (hhmm: string) => - hhmm.length >= 5 ? hhmm.slice(0, 5) : hhmm; if (prompt.blockedBy) { // Midnight rejection: server (or preview) refused because shifting @@ -888,7 +899,7 @@ function CascadePromptBlock({

{t("executiveMeetings.alert.cascadeMidnightError", { title: titleOf(prompt.blockedBy), - projectedStart: fmtTime(prompt.blockedBy.projectedStart), + projectedStart: formatTime(prompt.blockedBy.projectedStart, lang), })}

@@ -918,7 +929,8 @@ function CascadePromptBlock({ return (

@@ -942,9 +954,6 @@ function CascadePromptBlock({ > - - {t("executiveMeetings.alert.cascadeColIndex")} - {t("executiveMeetings.alert.cascadeColMeetingNumber")} @@ -957,7 +966,7 @@ function CascadePromptBlock({ - {prompt.followers.map((f, i) => { + {prompt.followers.map((f) => { const dn = meetingNumbersById.get(f.id); const title = titleOf(f); return ( @@ -966,7 +975,6 @@ function CascadePromptBlock({ className="border-t border-black/5 dark:border-white/10" data-testid={`cascade-follower-${f.id}`} > - {i + 1} {dn != null ? dn : "—"} @@ -974,7 +982,7 @@ function CascadePromptBlock({ {title} - {fmtTime(f.oldStart)} {isRtl ? "←" : "→"} {fmtTime(f.newStart)} + {formatTime(f.oldStart, lang)} {isRtl ? "←" : "→"} {formatTime(f.newStart, lang)} ); @@ -1028,6 +1036,10 @@ type PostponeDialogProps = { // table. Provided by the parent so we don't refetch today's meetings // inside the dialog. meetingNumbersById: Map; + // #481: accent hex from the parent's alert color theme; threaded + // into CascadePromptBlock so the cascade box matches the alert's + // chosen palette instead of hard-coded amber. + accent: string; onSaved: () => void | Promise; }; @@ -1039,6 +1051,7 @@ function PostponeDialog({ meeting, isRtl, meetingNumbersById, + accent, onSaved, }: PostponeDialogProps) { const { t, i18n } = useTranslation(); @@ -1535,6 +1548,8 @@ function PostponeDialog({ isRtl={isRtl} busy={busy !== null} meetingNumbersById={meetingNumbersById} + accent={accent} + lang={i18n.language} onShiftFollowing={() => void postponeBy(cascadePrompt.delta, undefined, true) } @@ -1721,6 +1736,8 @@ function PostponeDialog({ isRtl={isRtl} busy={busy !== null} meetingNumbersById={meetingNumbersById} + accent={accent} + lang={i18n.language} onShiftFollowing={() => void rescheduleSubmit(cascadePrompt.draft, true) } diff --git a/artifacts/tx-os/src/locales/ar.json b/artifacts/tx-os/src/locales/ar.json index cbcf9c5c..09481b3a 100644 --- a/artifacts/tx-os/src/locales/ar.json +++ b/artifacts/tx-os/src/locales/ar.json @@ -1341,7 +1341,6 @@ "cascadePromptHeader_many": "سيتأثر {{count}} اجتماعاً لاحقاً في هذا اليوم. هل تريد تأجيلها أيضاً بـ {{minutesText}}؟", "cascadePromptHeader_other": "سيتأثر {{count}} اجتماع لاحق في هذا اليوم. هل تريد تأجيلها أيضاً بـ {{minutesText}}؟", "cascadeListTitle": "الاجتماعات المتأثرة:", - "cascadeColIndex": "#", "cascadeColMeetingNumber": "رقم الاجتماع", "cascadeColTitle": "العنوان", "cascadeColTimes": "من ← إلى", diff --git a/artifacts/tx-os/src/locales/en.json b/artifacts/tx-os/src/locales/en.json index 8ed7663b..3517d74c 100644 --- a/artifacts/tx-os/src/locales/en.json +++ b/artifacts/tx-os/src/locales/en.json @@ -1224,7 +1224,6 @@ "cascadePromptHeader_one": "{{count}} later meeting on this day would be affected. Also shift it by {{minutesText}}?", "cascadePromptHeader_other": "{{count}} later meetings on this day would be affected. Also shift them by {{minutesText}}?", "cascadeListTitle": "Affected meetings:", - "cascadeColIndex": "#", "cascadeColMeetingNumber": "Meeting #", "cascadeColTitle": "Title", "cascadeColTimes": "From → To", diff --git a/artifacts/tx-os/tests/executive-meetings-upcoming-alert.spec.mjs b/artifacts/tx-os/tests/executive-meetings-upcoming-alert.spec.mjs index 6c922a1f..d9d8b052 100644 --- a/artifacts/tx-os/tests/executive-meetings-upcoming-alert.spec.mjs +++ b/artifacts/tx-os/tests/executive-meetings-upcoming-alert.spec.mjs @@ -1088,11 +1088,11 @@ test("Cascade prompt UI: Shift sends cascadeFollowing=true; Keep sends false", a // Our follower row must appear in the list with its testid. const followerRow = page.getByTestId(`cascade-follower-${follower.id}`); await expect(followerRow).toBeVisible(); - // #480: the row is rendered as a numbered table — the first cell is - // the 1-based index, the second is the meeting's dailyNumber. + // #481: the row is rendered as a 3-column table (Meeting #, Title, + // From → To). The leading "#" index column was dropped because the + // dailyNumber already gives the user a stable identifier. const followerCells = followerRow.locator("td"); - await expect(followerCells.nth(0)).toHaveText("1"); - await expect(followerCells.nth(1)).toHaveText(String(follower.dailyNumber)); + await expect(followerCells.nth(0)).toHaveText(String(follower.dailyNumber)); // Intercept the mutation so we can read its JSON body without // letting it actually fire (which would side-effect every other