#481 Polish cascade-affected meetings table in postpone/reschedule prompt
- 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).
This commit is contained in:
@@ -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<number, number>;
|
||||
// #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 (
|
||||
<div
|
||||
className="flex flex-col gap-2 rounded border border-amber-300 bg-amber-50 p-3 dark:border-amber-700 dark:bg-amber-950"
|
||||
className="flex flex-col gap-2 rounded border p-3"
|
||||
style={{ backgroundColor: promptBg, borderColor: promptBorder }}
|
||||
data-testid="cascade-prompt-loading"
|
||||
>
|
||||
<p className="text-sm font-medium">
|
||||
@@ -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({
|
||||
<p className="text-sm font-medium">
|
||||
{t("executiveMeetings.alert.cascadeMidnightError", {
|
||||
title: titleOf(prompt.blockedBy),
|
||||
projectedStart: fmtTime(prompt.blockedBy.projectedStart),
|
||||
projectedStart: formatTime(prompt.blockedBy.projectedStart, lang),
|
||||
})}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
@@ -918,7 +929,8 @@ function CascadePromptBlock({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col gap-2 rounded border border-amber-300 bg-amber-50 p-3 dark:border-amber-700 dark:bg-amber-950"
|
||||
className="flex flex-col gap-2 rounded border p-3"
|
||||
style={{ backgroundColor: promptBg, borderColor: promptBorder }}
|
||||
data-testid="cascade-prompt"
|
||||
>
|
||||
<p className="text-sm font-medium">
|
||||
@@ -942,9 +954,6 @@ function CascadePromptBlock({
|
||||
>
|
||||
<thead className="text-muted-foreground">
|
||||
<tr>
|
||||
<th scope="col" className="w-8 px-1 py-1 text-start font-medium tabular-nums">
|
||||
{t("executiveMeetings.alert.cascadeColIndex")}
|
||||
</th>
|
||||
<th scope="col" className="w-16 px-1 py-1 text-start font-medium tabular-nums">
|
||||
{t("executiveMeetings.alert.cascadeColMeetingNumber")}
|
||||
</th>
|
||||
@@ -957,7 +966,7 @@ function CascadePromptBlock({
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{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}`}
|
||||
>
|
||||
<td className="px-1 py-0.5 tabular-nums">{i + 1}</td>
|
||||
<td className="px-1 py-0.5 tabular-nums">
|
||||
{dn != null ? dn : "—"}
|
||||
</td>
|
||||
@@ -974,7 +982,7 @@ function CascadePromptBlock({
|
||||
{title}
|
||||
</td>
|
||||
<td className="px-1 py-0.5 text-end tabular-nums whitespace-nowrap">
|
||||
{fmtTime(f.oldStart)} {isRtl ? "←" : "→"} {fmtTime(f.newStart)}
|
||||
{formatTime(f.oldStart, lang)} {isRtl ? "←" : "→"} {formatTime(f.newStart, lang)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
@@ -1028,6 +1036,10 @@ type PostponeDialogProps = {
|
||||
// table. Provided by the parent so we don't refetch today's meetings
|
||||
// inside the dialog.
|
||||
meetingNumbersById: Map<number, number>;
|
||||
// #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<void>;
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -1341,7 +1341,6 @@
|
||||
"cascadePromptHeader_many": "سيتأثر {{count}} اجتماعاً لاحقاً في هذا اليوم. هل تريد تأجيلها أيضاً بـ {{minutesText}}؟",
|
||||
"cascadePromptHeader_other": "سيتأثر {{count}} اجتماع لاحق في هذا اليوم. هل تريد تأجيلها أيضاً بـ {{minutesText}}؟",
|
||||
"cascadeListTitle": "الاجتماعات المتأثرة:",
|
||||
"cascadeColIndex": "#",
|
||||
"cascadeColMeetingNumber": "رقم الاجتماع",
|
||||
"cascadeColTitle": "العنوان",
|
||||
"cascadeColTimes": "من ← إلى",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user