#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:
@@ -1356,7 +1356,8 @@
|
||||
"label": "إجراءات سريعة",
|
||||
"moveUp": "نقل لأعلى",
|
||||
"moveDown": "نقل لأسفل",
|
||||
"postpone": "تأجيل"
|
||||
"postpone": "تأجيل",
|
||||
"edit": "تعديل"
|
||||
},
|
||||
"merge": {
|
||||
"label": "دمج الخلايا",
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user