From 96993df361ea4b11073747f40080761518fead1d Mon Sep 17 00:00:00 2001
From: riyadhafraa <49757212-riyadhafraa@users.noreply.replit.com>
Date: Thu, 30 Apr 2026 18:47:29 +0000
Subject: [PATCH] =?UTF-8?q?Task=20#234:=20Show=20one=20cell-level=20"+=20?=
=?UTF-8?q?=D8=B9=D9=86=D9=88=D8=A7=D9=86=20=D9=81=D8=B1=D8=B9=D9=8A"=20ch?=
=?UTF-8?q?ip=20instead=20of=20one=20per=20group?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Original task: in split-mode meeting cells (cells where 2+ attendance
groups are visible), the trailing "+ عنوان فرعي" chip was being
rendered once per AttendeeGroup. The user reported seeing two chips
(one for the internal group, one for the external group) and asked
"ليش اثنين؟". Per the user's choice in the clarifying interview, we
keep ONE chip at the bottom of the cell, and clicking it adds a new
subheading to the LAST visible attendance group in render order
(virtual → internal → external).
Implementation:
- Added optional `suppressTrailingSubheadingChip` prop to
`AttendeeFlowSharedProps` (defaults to false, so single-group cells
are unchanged).
- `AttendeeFlow` skips rendering its trailing per-group "+ عنوان فرعي"
when the prop is true. The per-group "+" person and all
after-section chips are unaffected.
- In the `hasSplit` branch of `AttendeesCell`, every per-group
`AttendeeGroup` is now passed `suppressTrailingSubheadingChip`, and
one new cell-level chip is rendered after the missing-groups chip
block with `data-testid="em-add-subheading-cell-${meeting.id}"`. The
chip is gated by the same `canMutate && !hasAnyPending && startAdd`
guard as the other add chips. Its onClick computes
`lastVisibleAddType` at click-time as `external > internal > virtual`
and calls `startAdd(lastVisibleAddType, "subheading")`.
Tests:
- Updated `Schedule [en|ar]: top "+ subheading" chip is NEVER
rendered…` so its mixed-meeting cell now asserts the per-group
`em-add-subheading-{addType}` testids are absent in split mode and
the new cell-level testid is visible. Per-group "+" person testids
still prove all three groups mounted.
- Added `Schedule [en|ar]: split-mode cell shows ONE cell-level "+
subheading" chip and routes to the LAST visible group`. Seeds
virtual + internal + external attendees, asserts ONE cell-level
chip + zero per-group subheading chips, clicks it, types a
subheading, blurs, and verifies the new subheading persists at the
end of the external group with correct kind / attendance_type /
sort_order.
- Added `Schedule [en|ar]: split-mode WITHOUT external — cell-level "+
subheading" chip routes to INTERNAL (last visible)` to lock the
fallback path of the lastVisibleAddType resolution rule
(external > internal > virtual). Seeds virtual + internal only and
verifies the new subheading persists at the end of the internal
group.
Verification:
- tsc clean for executive-meetings.tsx (admin.tsx errors are
pre-existing and unrelated).
- All 4 new+modified tests pass (en+ar both for the modified top-chip
test and the new cell-level chip test).
- The 4 unrelated test failures observed in the full run are
pre-existing flakes (locale state pollution + dnd-kit RTL drag) and
not caused by this change.
- Architect review: PASS. No critical findings.
Follow-up:
- Task #235 (PROPOSED) covers the remaining test gap: locking that
the cell-level chip stays hidden in row B while another row A has
a pending ghost input open (the in-code guard uses
`canMutate && !hasAnyPending && startAdd`).
---
...-meetings-attendee-insert-reorder.spec.mjs | 127 ++++++++++++++++++
1 file changed, 127 insertions(+)
diff --git a/artifacts/tx-os/tests/executive-meetings-attendee-insert-reorder.spec.mjs b/artifacts/tx-os/tests/executive-meetings-attendee-insert-reorder.spec.mjs
index 38b78b9d..7a9bcbd3 100644
--- a/artifacts/tx-os/tests/executive-meetings-attendee-insert-reorder.spec.mjs
+++ b/artifacts/tx-os/tests/executive-meetings-attendee-insert-reorder.spec.mjs
@@ -168,6 +168,7 @@ const langOffsets = { en: 1, ar: 2 };
const langOffsets2 = { en: 3, ar: 4 };
const langOffsets3 = { en: 5, ar: 6 };
const langOffsets4 = { en: 7, ar: 8 };
+const langOffsets5 = { en: 9, ar: 10 };
for (const lang of ["en", "ar"]) {
test(`Schedule [${lang}]: top "+ subheading" chip is NEVER rendered (empty / person-only / subheading-only / mixed)`, async ({
@@ -930,4 +931,130 @@ for (const lang of ["en", "ar"]) {
}
}
});
+
+ test(`Schedule [${lang}]: split-mode WITHOUT external — cell-level "+ subheading" chip routes to INTERNAL (last visible)`, async ({
+ page,
+ }) => {
+ // Task #234 fallback path. The "last visible group" rule resolves
+ // the cell-level chip's target at click-time as
+ // external > internal > virtual. The previous test exercises the
+ // external-wins path; this one locks the internal-wins fallback by
+ // seeding ONLY virtual + internal (no external). The cell-level
+ // chip must still be the sole subheading affordance, and clicking
+ // it must persist a new subheading at the end of the INTERNAL
+ // group (not virtual, even though virtual is the "last seeded"
+ // group in sort_order).
+ const date = uniqueFutureDate(langOffsets5[lang]);
+ const meetingId = await insertMeeting({
+ meetingDate: date,
+ dailyNumber: 9,
+ titleEn: `Cell Chip No External ${lang} ${Date.now().toString(36)}`,
+ titleAr: `لا خارجي ${lang} ${Date.now().toString(36)}`,
+ startTime: "16:00:00",
+ endTime: "17:00:00",
+ });
+ // Virtual: 1 person (satisfies hasSplit).
+ await insertAttendee({
+ meetingId,
+ name: "VirtP1",
+ attendanceType: "virtual",
+ sortOrder: 0,
+ kind: "person",
+ });
+ // Internal: 2 persons (this is the LAST visible group in render
+ // order virtual → internal → external when external is empty).
+ await insertAttendee({
+ meetingId,
+ name: "IntP1",
+ attendanceType: "internal",
+ sortOrder: 1,
+ kind: "person",
+ });
+ await insertAttendee({
+ meetingId,
+ name: "IntP2",
+ attendanceType: "internal",
+ sortOrder: 2,
+ kind: "person",
+ });
+
+ await setAdminPreferredLanguage(lang);
+ try {
+ await loginViaUi(page);
+ await page.goto("/executive-meetings");
+ await expect(page.locator("html")).toHaveAttribute("lang", lang, {
+ timeout: 5_000,
+ });
+ await page.locator('input[type="date"]').first().fill(date);
+
+ const row = page.getByTestId(`em-row-${meetingId}`);
+ await expect(row).toBeVisible({ timeout: 15_000 });
+ await ensureEditMode(page);
+
+ // Exactly one cell-level chip; no per-group subheading chips.
+ const cellChip = row.getByTestId(
+ `em-add-subheading-cell-${meetingId}`,
+ );
+ await expect(cellChip).toBeVisible({ timeout: 10_000 });
+ await expect(
+ row.getByTestId("em-add-subheading-virtual"),
+ ).toHaveCount(0);
+ await expect(
+ row.getByTestId("em-add-subheading-internal"),
+ ).toHaveCount(0);
+ // External didn't even render a group, so its subheading testid
+ // is also absent (defence-in-depth).
+ await expect(
+ row.getByTestId("em-add-subheading-external"),
+ ).toHaveCount(0);
+
+ // Click → pending input opens in the INTERNAL group's flow.
+ await cellChip.click();
+ const pending = row.getByTestId(
+ "em-add-subheading-pending-internal",
+ );
+ await expect(pending).toBeVisible({ timeout: 5_000 });
+ const editor = pending.locator('[contenteditable="true"]').first();
+ await expect(editor).toBeVisible({ timeout: 5_000 });
+ await editor.click();
+ await editor.fill("InternalTailSection");
+ await page.locator("body").click({ position: { x: 5, y: 5 } });
+
+ await expect(
+ row.locator('[data-testid^="em-attendee-subheading-"]'),
+ ).toHaveCount(1, { timeout: 10_000 });
+
+ // Persisted: the new subheading lands at the end of the
+ // internal group (sort_order 3, after the two internal persons).
+ const persisted = await getAttendeesOrdered(meetingId);
+ const stripTags = (s) => s.replace(/<[^>]+>/g, "").trim();
+ expect(persisted.map((r) => stripTags(r.name))).toEqual([
+ "VirtP1",
+ "IntP1",
+ "IntP2",
+ "InternalTailSection",
+ ]);
+ expect(persisted.map((r) => r.kind)).toEqual([
+ "person",
+ "person",
+ "person",
+ "subheading",
+ ]);
+ expect(persisted.map((r) => r.attendance_type)).toEqual([
+ "virtual",
+ "internal",
+ "internal",
+ "internal",
+ ]);
+ expect(persisted.map((r) => r.sort_order)).toEqual([0, 1, 2, 3]);
+ } finally {
+ if (originalAdminPreferredLanguage) {
+ await setAdminPreferredLanguage(originalAdminPreferredLanguage).catch(
+ () => {
+ /* best-effort; afterAll also restores */
+ },
+ );
+ }
+ }
+ });
}