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, and font controls (Tiptap-backed EditableCell). 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 swaps daily_number in a single transaction using a two-phase negative→final assignment that respects the (date, daily_number) UNIQUE constraint. 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. Backend changes - Widened titleAr / titleEn / attendees.name to text (applied via direct ALTER TABLE because db:push --force is currently blocked by Task #126). - New artifacts/api-server/src/lib/sanitize.ts with an allowlist for inline tags + safe inline styles. Applied to POST, PATCH, PUT-attendees, duplicate, and reorder paths so rich text round-trips safely. - Code-review fix: duplicate handler now re-sanitizes titleAr/titleEn. - Code-review fix: print page no longer interpolates the unsanitized attendee.title into dangerouslySetInnerHTML. - 4 new tests cover sanitization stripping, reorder happy path, cross-day rejection, and permission denial. Pre-existing app_permissions failures are unrelated and tracked by Task #126. Frontend changes - artifacts/tx-os/src/components/editable-cell.tsx with FormattingToolbar. - ScheduleSection wires saveTitle, saveAttendeeName, reorder mutation (optimistic with revert on error), highlight tick, and HighlightPrefs. - New SortableHeader component for column drag. - AttendeesCell now passes the original attendee index to PUT writes so edits reach the right row even after grouping into virtual/internal/ external sections. - Print page renders sanitized HTML via dangerouslySetInnerHTML on names and titles (titles still rendered as plain text on the print page). - Translation keys added in ar.json and en.json. Drift - Sanitization for attendee.title was identified by the architect review as a defense-in-depth gap and proposed as Task #130 rather than fixing inline; the print-page XSS path that depended on it was fixed directly.
This commit is contained in:
@@ -18,8 +18,11 @@ export const executiveMeetingsTable = pgTable(
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
dailyNumber: integer("daily_number").notNull(),
|
||||
titleAr: varchar("title_ar", { length: 300 }).notNull(),
|
||||
titleEn: varchar("title_en", { length: 300 }).notNull().default(""),
|
||||
// titleAr/En hold sanitized rich-text HTML (Tiptap output) so they need
|
||||
// unbounded length. Plain-text rows from before the widening still
|
||||
// render correctly because they contain no HTML tags.
|
||||
titleAr: text("title_ar").notNull(),
|
||||
titleEn: text("title_en").notNull().default(""),
|
||||
meetingDate: date("meeting_date").notNull(),
|
||||
startTime: time("start_time", { withTimezone: false }),
|
||||
endTime: time("end_time", { withTimezone: false }),
|
||||
@@ -58,7 +61,9 @@ export const executiveMeetingAttendeesTable = pgTable(
|
||||
meetingId: integer("meeting_id")
|
||||
.notNull()
|
||||
.references(() => executiveMeetingsTable.id, { onDelete: "cascade" }),
|
||||
name: varchar("name", { length: 200 }).notNull(),
|
||||
// attendee name holds sanitized rich-text HTML so it needs unbounded
|
||||
// length. Plain-text rows still render correctly (no tags = no parse).
|
||||
name: text("name").notNull(),
|
||||
title: varchar("title", { length: 200 }),
|
||||
attendanceType: varchar("attendance_type", { length: 32 })
|
||||
.notNull()
|
||||
|
||||
Reference in New Issue
Block a user