Task #347: EM edit dialog — strip HTML in inputs + reorder/rename buttons

- safe-html.ts: add htmlToPlainText (DOMParser, br/block→\n, decode, collapse)
  and wrapAsParagraph (HTML-escape + <p>…</p>) helpers.
- executive-meetings.tsx:
  - openEdit() runs htmlToPlainText on titleAr, titleEn, and each attendee.name
    so the dialog's plain <Input> shows clean text instead of raw markup.
  - save() wraps titleAr, titleEn, and each attendee.name with wrapAsParagraph
    so the on-disk format stays byte-compatible with what the inline Tiptap
    EditableCell produces. Empty-title validation still fires off the plain
    string before wrapping.
  - Swapped MeetingFormDialog buttons: addSubheading now renders before the
    attendee-add button.
- ar.json: executiveMeetings.manage.attendees.add "إضافة حاضر" → "إضافة عضو".
- en.json: same key "Add attendee" → "Add member".
- Inline schedule cell chips (executiveMeetings.schedule.*) untouched.
This commit is contained in:
riyadhafraa
2026-05-03 12:58:11 +00:00
parent e695f5b4cb
commit 81f4d7cb55
@@ -102,7 +102,7 @@ import {
import { CSS } from "@dnd-kit/utilities";
import { GripVertical } from "lucide-react";
import { EditableCell } from "@/components/editable-cell";
import { safeHtml } from "@/lib/safe-html";
import { safeHtml, htmlToPlainText, wrapAsParagraph } from "@/lib/safe-html";
import {
ALERT_COLOR_PRESETS,
DEFAULT_ALERT_PREFS,
@@ -5462,8 +5462,8 @@ function ManageSection({
function openEdit(m: Meeting) {
setEditing({
id: m.id,
titleAr: m.titleAr,
titleEn: m.titleEn ?? "",
titleAr: htmlToPlainText(m.titleAr),
titleEn: htmlToPlainText(m.titleEn ?? ""),
meetingDate: m.meetingDate,
dailyNumber: String(m.dailyNumber),
startTime: m.startTime ? m.startTime.slice(0, 5) : "",
@@ -5480,7 +5480,11 @@ function ManageSection({
// the dialog don't have one until after save, so we always assign
// `_sid` here for uniformity. The save projection enumerates wire
// fields and naturally drops `_sid`.
attendees: m.attendees.map((a) => ({ ...a, _sid: genAttendeeSid() })),
attendees: m.attendees.map((a) => ({
...a,
name: htmlToPlainText(a.name),
_sid: genAttendeeSid(),
})),
});
}
@@ -5499,7 +5503,7 @@ function ManageSection({
// for new meetings, while preserving any pre-existing English
// title on edits (the dialog hides the field but openEdit still
// loads it into MeetingFormState).
const titleEn = editing.titleEn.trim() || editing.titleAr.trim();
const titleEnPlain = editing.titleEn.trim() || editing.titleAr.trim();
// Prefer the dialog-supplied canonical values from the time
// picker's commit() result over the controlled `editing.*`
// state — the picker's onChange suppresses ambiguous/invalid
@@ -5514,8 +5518,8 @@ function ManageSection({
: editing.endTime || null;
setSaving(true);
const body: Record<string, unknown> = {
titleAr: editing.titleAr.trim(),
titleEn,
titleAr: wrapAsParagraph(editing.titleAr),
titleEn: wrapAsParagraph(titleEnPlain),
meetingDate: editing.meetingDate,
startTime,
endTime,
@@ -5526,7 +5530,7 @@ function ManageSection({
isHighlighted: editing.isHighlighted ? 1 : 0,
notes: editing.notes.trim() || null,
attendees: editing.attendees.map((a, idx) => ({
name: a.name,
name: wrapAsParagraph(a.name),
title: a.title,
attendanceType: a.attendanceType,
sortOrder: idx,
@@ -6404,16 +6408,6 @@ function MeetingFormDialog({
{t("executiveMeetings.manage.attendees.removeAll")}
</Button>
)}
<Button
size="sm"
variant="outline"
onClick={addAttendee}
className="gap-1"
data-testid="em-attendee-add"
>
<Plus className="w-4 h-4" />
{t("executiveMeetings.manage.attendees.add")}
</Button>
<Button
size="sm"
variant="outline"
@@ -6424,6 +6418,16 @@ function MeetingFormDialog({
<Plus className="w-4 h-4" />
{t("executiveMeetings.manage.attendees.addSubheading")}
</Button>
<Button
size="sm"
variant="outline"
onClick={addAttendee}
className="gap-1"
data-testid="em-attendee-add"
>
<Plus className="w-4 h-4" />
{t("executiveMeetings.manage.attendees.add")}
</Button>
</div>
</div>
<div className="space-y-2">