Executive Meetings: inline editing, drag reorder, and current-meeting highlight
Implements Task #122 — four features on the Executive Meetings schedule: 1. Word-like inline editing of meeting title and attendee names with bold, italic, underline, color, font-family, font-size, and text-align controls (Tiptap-backed EditableCell + FormattingToolbar). 2. Drag-to-reorder column headers (dnd-kit), persisted in the existing em-schedule-cols-v1 storage key. Old up/down buttons removed. 3. Drag-to-reorder rows within a day. New POST /api/executive-meetings/reorder endpoint permutes daily_number + start/end times in a single transaction using a two-phase negative→final assignment that respects the (date, daily_number) UNIQUE constraint and survives partial-day reorders. Audit-logged and role-guarded. 4. Highlight the meeting whose start/end window is "now", refreshed every 60 s. Toggle and color picker live in the customize popover and persist to localStorage (em-current-meeting-highlight-v1). Backend changes - Widened titleAr / titleEn / attendees.name to text. - New artifacts/api-server/src/lib/sanitize.ts with an allowlist for inline tags + safe inline styles. Applied to POST, PATCH, PUT-attendees, duplicate, reorder, and applyApprovedRequest (add_attendee) paths so rich text round-trips safely. - Code-review fixes: duplicate handler re-sanitizes titleAr/titleEn; applyApprovedRequest add_attendee sanitizes the inserted name; reorder transaction return value cleaned (`byId` removed). - 5 new tests cover sanitization stripping, reorder happy path, cross-day rejection, permission denial, and applyApprovedRequest sanitization. Pre-existing app_permissions failures are unrelated (tracked by #126/#127). Frontend changes - artifacts/tx-os/src/components/editable-cell.tsx with FormattingToolbar (custom FontSize Tiptap extension + @tiptap/extension-text-align). - ScheduleSection wires saveTitle, saveAttendeeName, reorder mutation (optimistic with revert on error), highlight tick, and HighlightPrefs. - New SortableHeader component for column drag. - AttendeesCell passes the original attendee index to PUT writes so edits reach the right row even after grouping into virtual/internal/external. - Print page renders sanitized rich text via dangerouslySetInnerHTML on names and titles; attendee.title is rendered as a plain text node (XSS). - Translation keys added in ar.json and en.json.
This commit is contained in:
@@ -753,7 +753,8 @@
|
||||
},
|
||||
"highlight": {
|
||||
"title": "تمييز الاجتماع الحالي",
|
||||
"hint": "تظليل الاجتماع الذي يقع وقته الآن."
|
||||
"hint": "تظليل الاجتماع الذي يقع وقته الآن.",
|
||||
"reset": "إعادة إلى الافتراضي"
|
||||
},
|
||||
"dragRow": "اسحب لإعادة الترتيب",
|
||||
"editMeetingTitle": "تعديل عنوان الاجتماع",
|
||||
|
||||
@@ -706,7 +706,8 @@
|
||||
},
|
||||
"highlight": {
|
||||
"title": "Highlight current meeting",
|
||||
"hint": "Outline the meeting whose time is now."
|
||||
"hint": "Outline the meeting whose time is now.",
|
||||
"reset": "Reset to default"
|
||||
},
|
||||
"dragRow": "Drag to reorder",
|
||||
"editMeetingTitle": "Edit meeting title",
|
||||
|
||||
@@ -1274,7 +1274,7 @@ function ColumnsCustomizer({
|
||||
<p className="text-[11px] text-muted-foreground mb-2 leading-relaxed">
|
||||
{t("executiveMeetings.highlight.hint")}
|
||||
</p>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
{HIGHLIGHT_COLOR_SWATCHES.map((c) => {
|
||||
const selected = highlightPrefs.color === c;
|
||||
return (
|
||||
@@ -1297,6 +1297,16 @@ function ColumnsCustomizer({
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
onHighlightPrefsChange({ ...DEFAULT_HIGHLIGHT_PREFS })
|
||||
}
|
||||
className="ml-1 text-[11px] text-[#0B1E3F] underline hover:no-underline disabled:opacity-50"
|
||||
data-testid="em-highlight-reset"
|
||||
>
|
||||
{t("executiveMeetings.highlight.reset")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
@@ -1686,9 +1696,10 @@ function MeetingCard({
|
||||
<div className="min-w-0 flex-1 space-y-1">
|
||||
{showMeeting && (
|
||||
<>
|
||||
<div className="font-semibold text-[#0B1E3F] leading-snug break-words">
|
||||
{title}
|
||||
</div>
|
||||
<div
|
||||
className="font-semibold text-[#0B1E3F] leading-snug break-words"
|
||||
dangerouslySetInnerHTML={{ __html: safeHtml(title) }}
|
||||
/>
|
||||
{meeting.location && (
|
||||
<div className="text-xs text-muted-foreground break-words">
|
||||
{meeting.location}
|
||||
|
||||
Reference in New Issue
Block a user