#570: stronger selected-row highlight + Edit button in Quick Actions

Task: on iPad the bulk-selected row was nearly invisible (only a 2px
inset navy ring), and the row-tap Quick Actions dialog had only a
Postpone button — user asked for a real visible selection fill and
an Edit (pencil) action wired to the existing edit-meeting flow.

Changes (artifacts/tx-os/src/pages/executive-meetings.tsx):
- MeetingRow: add `selectionBg = rgba(11,30,63,0.18)` painted on the
  row's `<tr>` backgroundColor in addition to the existing
  `selectionShadow` 2px navy ring. Composition order in trStyle is
  `quickOpenBg ?? selectionBg ?? baseBg` so an open quick-actions
  surface still wins (transient action state), user-picked row
  colours still show when nothing is selected, and the navy tint
  reads clearly on iPad against the table grid.
- MeetingRow: new optional `onQuickEdit?: () => void` prop. Quick
  Actions Dialog gains a pencil-icon "Edit" button before the
  existing Postpone button, wrapped in `flex flex-wrap gap-2` so the
  two buttons sit side-by-side on iPad/desktop and stack on narrow
  viewports. Button only renders when a handler is wired
  (defensive — `quickActionsCanMutate` already gates the surface).
- ScheduleSection: host a local `editingMeeting` + `editingSaving`
  state plus a `MeetingFormDialog` mount, with a save handler whose
  body mirrors ManageSection's `save()` exactly (same PATCH payload
  projection, same `wrapAsParagraph` / attendee shaping, same
  toast/error translation). Edit button on a row opens this in-place
  dialog instead of switching the user to the Manage tab.
- Locale: add `executiveMeetings.quickActions.edit` = "تعديل" / "Edit".

Notes:
- No new icon import needed — `Pencil` was already in the lucide
  import group.
- Selection ring (`#0B1E3F`) and tint use the same navy so they
  read as one cohesive selected state, not competing layers.
- Pre-existing `use-push-subscription.ts` TS noise unchanged.
This commit is contained in:
riyadhafraa
2026-05-17 14:40:34 +00:00
parent dc30d75e11
commit c40384b0d9
3 changed files with 153 additions and 5 deletions
+2 -1
View File
@@ -1356,7 +1356,8 @@
"label": "إجراءات سريعة",
"moveUp": "نقل لأعلى",
"moveDown": "نقل لأسفل",
"postpone": "تأجيل"
"postpone": "تأجيل",
"edit": "تعديل"
},
"merge": {
"label": "دمج الخلايا",
+2 -1
View File
@@ -1082,7 +1082,8 @@
"label": "Quick actions",
"moveUp": "Move up",
"moveDown": "Move down",
"postpone": "Postpone"
"postpone": "Postpone",
"edit": "Edit"
},
"merge": {
"label": "Merge cells",
@@ -2307,6 +2307,15 @@ function ScheduleSection({
const [postponeMeetingId, setPostponeMeetingId] = useState<number | null>(
null,
);
// #570: editing state for the in-place Meeting edit dialog opened
// from the row's quick-actions Edit button. We host the dialog in
// ScheduleSection (rather than switching the user over to the
// Manage tab) so the edit flow feels local to the row the user
// just tapped. Mirrors ManageSection's `editing` shape so we can
// reuse the same MeetingFormDialog + save body unchanged.
const [editingMeeting, setEditingMeeting] =
useState<MeetingFormState | null>(null);
const [editingSaving, setEditingSaving] = useState(false);
const postponeMeeting = useMemo(() => {
if (postponeMeetingId == null) return null;
return meetings.find((m) => m.id === postponeMeetingId) ?? null;
@@ -3011,6 +3020,33 @@ function ScheduleSection({
onBulkSelectChange={(v) => toggleMeetingSelected(m.id, v)}
quickActionsCanMutate={canMutate && !editMode}
onQuickPostpone={() => setPostponeMeetingId(m.id)}
onQuickEdit={
canMutate && !editMode
? () =>
setEditingMeeting({
id: m.id,
titleAr: htmlToPlainText(m.titleAr),
titleEn: htmlToPlainText(m.titleEn ?? ""),
meetingDate: m.meetingDate,
dailyNumber: String(m.dailyNumber),
startTime: m.startTime
? m.startTime.slice(0, 5)
: "",
endTime: m.endTime ? m.endTime.slice(0, 5) : "",
location: m.location ?? "",
meetingUrl: m.meetingUrl ?? "",
platform: m.platform,
status: m.status,
isHighlighted: m.isHighlighted === 1,
notes: m.notes ?? "",
attendees: m.attendees.map((a) => ({
...a,
name: htmlToPlainText(a.name),
_sid: genAttendeeSid(),
})),
})
: undefined
}
/>
))}
</SortableContext>
@@ -3051,6 +3087,77 @@ function ScheduleSection({
same component the upcoming-meeting alert renders so the
postpone-minutes / reschedule / cancel flows + the 409
stale_meeting handling stay in one place. */}
{/* #570: In-place Meeting edit dialog hosted by ScheduleSection,
opened from the row's quick-actions Edit button. Save body
mirrors ManageSection's `save()` exactly (PATCH with the
same field projection) so an edit started here writes the
same shape the Manage tab would. Invalidates the same
react-query key the rest of the page uses so the row
reflects new values instantly. */}
{editingMeeting && (
<MeetingFormDialog
state={editingMeeting}
onChange={setEditingMeeting}
onSave={async (committedStart, committedEnd) => {
if (!editingMeeting) return;
if (!editingMeeting.titleAr.trim()) {
toast({
title: t("executiveMeetings.manage.errors.titleRequired"),
variant: "destructive",
});
return;
}
const titleEnPlain =
editingMeeting.titleEn.trim() || editingMeeting.titleAr.trim();
setEditingSaving(true);
const body: Record<string, unknown> = {
titleAr: wrapAsParagraph(editingMeeting.titleAr),
titleEn: wrapAsParagraph(titleEnPlain),
meetingDate: editingMeeting.meetingDate,
startTime: committedStart || null,
endTime: committedEnd || null,
location: editingMeeting.location.trim() || null,
meetingUrl: editingMeeting.meetingUrl.trim() || null,
platform: editingMeeting.platform,
status: editingMeeting.status,
isHighlighted: editingMeeting.isHighlighted ? 1 : 0,
notes: editingMeeting.notes.trim() || null,
attendees: editingMeeting.attendees.map((a, idx) => ({
name: wrapAsParagraph(a.name),
title: a.title,
attendanceType: a.attendanceType,
sortOrder: idx,
kind: a.kind ?? "person",
})),
};
if (editingMeeting.dailyNumber.trim()) {
body.dailyNumber = Number(editingMeeting.dailyNumber);
}
try {
await apiJson(
`/api/executive-meetings/${editingMeeting.id}`,
{ method: "PATCH", body },
);
toast({ title: t("executiveMeetings.common.saved") });
setEditingMeeting(null);
refreshDay();
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
toast({
title: t("executiveMeetings.manage.errors.saveFailed"),
description: translateSaveError(msg, t),
variant: "destructive",
});
} finally {
setEditingSaving(false);
}
}}
onClose={() => setEditingMeeting(null)}
saving={editingSaving}
t={t}
/>
)}
{postponeMeeting && canMutate ? (
<PostponeDialog
open={postponeMeetingId !== null}
@@ -3811,6 +3918,7 @@ function MeetingRow({
dragEnabled,
quickActionsCanMutate,
onQuickPostpone,
onQuickEdit,
}: {
meeting: Meeting;
displayNumber?: number;
@@ -3836,6 +3944,12 @@ function MeetingRow({
// MUTATE_ROLES server-side gating as the postpone-minutes endpoint.
quickActionsCanMutate?: boolean;
onQuickPostpone?: () => void;
// #570: opens the full Meeting edit dialog (same one used in
// ManageSection) and closes the quick-actions surface. Wired by
// ScheduleSection to its own `setEditingMeeting` lifted state so
// the Schedule tab can host the edit dialog without switching the
// user to the Manage tab.
onQuickEdit?: () => void;
onSaveTitle: (html: string) => Promise<void>;
onSaveAttendeeName: (idx: number, html: string) => Promise<void>;
onSaveTimes: (
@@ -4327,7 +4441,16 @@ function MeetingRow({
if (!quickActionsCanMutate && quickOpen) setQuickOpen(false);
}, [quickActionsCanMutate, quickOpen]);
// #570: bulk-selected rows previously only got a 2px inset ring in
// navy, which is easy to miss on iPad against the table grid. We now
// *also* paint a visible navy tint behind the row so the selected
// state reads at a glance. The tint composes after the per-row
// `baseBg` so user-picked row colours still win when nothing is
// selected, but loses to `quickOpenBg` (the open quick-actions
// surface is a transient, action-scoped state) — see the
// `backgroundColor` composition in `trStyle` below.
const selectionShadow = bulkSelected ? "inset 0 0 0 2px #0B1E3F" : null;
const selectionBg = bulkSelected ? "rgba(11, 30, 63, 0.18)" : null;
const currentShadow = isCurrent
? `inset 0 0 0 2px ${highlightColor}`
: null;
@@ -4402,7 +4525,7 @@ function MeetingRow({
.filter(Boolean)
.join(", ");
const trStyle: CSSProperties = {
backgroundColor: quickOpenBg ?? baseBg,
backgroundColor: quickOpenBg ?? selectionBg ?? baseBg,
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : reordering ? 0.7 : 1,
@@ -4527,8 +4650,31 @@ function MeetingRow({
</DialogHeader>
{/* #490: polished Postpone button primary-styled, larger
tap target for iPad, icon flips with locale via flex
direction so it stays adjacent to the label in RTL. */}
<div className="flex justify-center pt-2">
direction so it stays adjacent to the label in RTL.
#570: added an Edit (pencil) button alongside Postpone
that opens the same Meeting edit dialog ManageSection
uses. Wrapped in a flex row + `flex-wrap` so the two
buttons sit side by side on iPad/desktop and stack on
very narrow viewports. Edit button only renders when a
handler is wired (defensive quickActionsCanMutate
already gates the surface itself). */}
<div className="flex flex-wrap justify-center gap-2 pt-2">
{onQuickEdit ? (
<Button
type="button"
variant="outline"
size="sm"
onClick={() => {
setQuickOpen(false);
onQuickEdit();
}}
className={`min-w-[11rem] gap-2 px-3 py-2 h-10 text-sm font-medium shadow-sm ${isRtl ? "flex-row-reverse" : ""}`}
data-testid={`em-row-quick-edit-${meeting.id}`}
>
<Pencil className="w-4 h-4" aria-hidden="true" />
<span>{t("executiveMeetings.quickActions.edit")}</span>
</Button>
) : null}
<Button
type="button"
variant="default"