diff --git a/artifacts/api-server/src/routes/executive-meetings.ts b/artifacts/api-server/src/routes/executive-meetings.ts index 09b5e252..7432a57d 100644 --- a/artifacts/api-server/src/routes/executive-meetings.ts +++ b/artifacts/api-server/src/routes/executive-meetings.ts @@ -2210,6 +2210,18 @@ router.post( isHighlighted: 0, notes: stripTagsToPlainTextOrNull(source.notes), attachments: source.attachments, + // #678: Carry the external-meeting flag onto the copy. Omitting it + // silently reset duplicates/repeats to "internal" (default false), + // losing the red tint + no-auto-postpone behavior of the source. + isExternal: source.isExternal, + // Same class of bug: rowColor is a shared editorial signal about + // the meeting (urgent / VIP), and the merge overlay is user + // content — both should survive a duplicate. mergeText is + // re-sanitized like the other rich-text fields above. + rowColor: source.rowColor, + mergeStartColumn: source.mergeStartColumn, + mergeEndColumn: source.mergeEndColumn, + mergeText: source.mergeText ? sanitizeRichText(source.mergeText) : source.mergeText, createdBy: userId, updatedBy: userId, }; diff --git a/artifacts/api-server/tests/executive-meetings.test.mjs b/artifacts/api-server/tests/executive-meetings.test.mjs index 6306e544..759199be 100644 --- a/artifacts/api-server/tests/executive-meetings.test.mjs +++ b/artifacts/api-server/tests/executive-meetings.test.mjs @@ -409,6 +409,36 @@ test("Meetings: POST /duplicate clones a meeting onto another date", async () => assert.ok(dayBody.meetings.some((m) => m.id === newRow.id)); }); +// #678: duplicating/repeating must preserve the external-meeting flag +// (and the shared row colour) instead of silently resetting the copy +// to a normal internal meeting. +test("Meetings: POST /duplicate preserves isExternal and rowColor on the copy", async () => { + const create = await api(adminCookie, "POST", "/api/executive-meetings", { + titleAr: "خارجي", + titleEn: "External", + meetingDate: today, + isExternal: true, + attendees: [], + }); + assert.equal(create.status, 201); + const original = await create.json(); + created.meetingIds.push(original.id); + assert.equal(original.isExternal, true, "source must be external"); + assert.equal(original.rowColor, "red", "external source is force-tinted red"); + + const dup = await api( + adminCookie, + "POST", + `/api/executive-meetings/${original.id}/duplicate`, + { targetDate: tomorrow }, + ); + assert.equal(dup.status, 201); + const copy = await dup.json(); + created.meetingIds.push(copy.id); + assert.equal(copy.isExternal, true, "duplicate must stay external (#678)"); + assert.equal(copy.rowColor, "red", "duplicate keeps the red row colour"); +}); + // #189: plain-text sanitization for location / meetingUrl / notes. test("Sanitization: POST strips HTML from location, meetingUrl, and notes", async () => { const create = await api(adminCookie, "POST", "/api/executive-meetings", { diff --git a/artifacts/tx-os/src/components/ui/switch.tsx b/artifacts/tx-os/src/components/ui/switch.tsx index 4c43adf5..449d9ff5 100644 --- a/artifacts/tx-os/src/components/ui/switch.tsx +++ b/artifacts/tx-os/src/components/ui/switch.tsx @@ -17,7 +17,12 @@ const Switch = React.forwardRef< >