Task #171: Schedule Edit/View toggle (final fixes)
Add a single global "تحرير/Edit" toggle button to the schedule toolbar that hides every editing affordance by default and reveals them only when the user (with edit permission) explicitly opts in. Affordances now gated behind `effectiveCanMutate = canMutate && editMode`: - "+ Add row" button - Per-row delete, color swatch, merge trigger, drag grip - Inline cell editors (EditableCell, TimeRangeCell) - Column drag-reorder (SortableHeader.dragEnabled) - Column resize handles - "+ Add attendee" button AND its pending ghost row Persistence: - Toggle state is stored in localStorage under a per-user key `em-schedule-edit-mode-v1:<userId>`, so a shared browser cannot leak one editor's last toggle into another account that signs in. Falls back to view mode when userId is unavailable. - Always starts in view mode for users without edit permission. Toggle-off safety: - EditableCell + TimeRangeCell discard any in-progress draft and exit edit mode when their `disabled` / `canMutate` prop flips. - ScheduleSection clears `pendingAttendee` in a useEffect when effectiveCanMutate becomes false, so the ghost "+ Add attendee" row unmounts immediately. - AttendeeFlow also gates the pending render on `canMutate` as defense in depth. i18n: 4 new keys under `executiveMeetings.schedule` (editToggle / editToggleAria / editToggleOn / editToggleOff) in both en.json and ar.json. Tests: tests/executive-meetings-edit-toggle.spec.mjs covers default-hidden affordances, toggle-on reveal, reload persistence, and toggle-off re-hide. Cleanup wipes all `em-schedule-edit-mode-v1*` keys to handle the user-namespaced storage. Full e2e suite (12 tests) passes. Code review: PASS on the second pass after the per-user key + ghost row fixes. Pre-existing TS errors in admin.tsx and use-notifications-socket.ts are codegen drift from earlier tasks and are not touched by this change.
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
async function loginViaUi(page, username, password) {
|
||||
await page.goto("/login");
|
||||
await page.locator("#username").fill(username);
|
||||
await page.locator("#password").fill(password);
|
||||
await Promise.all([
|
||||
page.waitForURL((url) => !url.pathname.endsWith("/login"), {
|
||||
timeout: 15_000,
|
||||
}),
|
||||
page.locator('form button[type="submit"]').click(),
|
||||
]);
|
||||
}
|
||||
|
||||
test("Schedule: Edit toggle hides editing affordances by default and reveals them when on", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.addInitScript(() => {
|
||||
try {
|
||||
window.localStorage.setItem("tx-lang", "en");
|
||||
} catch {
|
||||
/* localStorage may not be ready before navigation */
|
||||
}
|
||||
});
|
||||
|
||||
await loginViaUi(page, "admin", "admin123");
|
||||
// Make sure we always start the test in view mode regardless of any
|
||||
// previously-persisted toggle state from another run. We clear AFTER
|
||||
// login so the cleanup isn't undone by the initScript on every nav.
|
||||
await page.goto("/executive-meetings");
|
||||
// The toggle's storage key is namespaced per-user
|
||||
// (`em-schedule-edit-mode-v1:<userId>`), so we wipe every entry that
|
||||
// starts with the base key to make sure we begin in view mode no
|
||||
// matter who admin's userId resolves to in this run.
|
||||
await page.evaluate(() => {
|
||||
try {
|
||||
const keys = Object.keys(window.localStorage);
|
||||
for (const k of keys) {
|
||||
if (k.startsWith("em-schedule-edit-mode-v1")) {
|
||||
window.localStorage.removeItem(k);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
});
|
||||
await page.reload();
|
||||
|
||||
// Toggle is rendered (admin has edit permission), and starts in
|
||||
// pressed=false / view mode.
|
||||
const toggle = page.getByTestId("em-edit-mode-toggle");
|
||||
await expect(toggle).toBeVisible();
|
||||
await expect(toggle).toHaveAttribute("aria-pressed", "false");
|
||||
|
||||
// In view mode the row-level "Add meeting" button must be hidden.
|
||||
await expect(page.getByTestId("em-add-row-button")).toHaveCount(0);
|
||||
// And no row-level overlays (delete, color picker, merge trigger,
|
||||
// grip handle) should exist anywhere in the table.
|
||||
await expect(page.locator('[data-testid^="em-delete-row-"]')).toHaveCount(0);
|
||||
await expect(page.getByTestId("em-row-color-trigger")).toHaveCount(0);
|
||||
await expect(page.locator('[data-testid^="em-merge-trigger-"]')).toHaveCount(0);
|
||||
await expect(page.locator('[data-testid^="em-row-grip-"]')).toHaveCount(0);
|
||||
|
||||
// Flip toggle on. aria-pressed becomes true and the affordances appear.
|
||||
await toggle.click();
|
||||
await expect(toggle).toHaveAttribute("aria-pressed", "true");
|
||||
|
||||
// The "+ Add meeting" row button is back.
|
||||
await expect(page.getByTestId("em-add-row-button")).toBeVisible();
|
||||
|
||||
// Persist + survive a reload: the toggle should still be on.
|
||||
await page.reload();
|
||||
const toggleAfterReload = page.getByTestId("em-edit-mode-toggle");
|
||||
await expect(toggleAfterReload).toHaveAttribute("aria-pressed", "true");
|
||||
await expect(page.getByTestId("em-add-row-button")).toBeVisible();
|
||||
|
||||
// Toggle back off — affordances disappear again.
|
||||
await toggleAfterReload.click();
|
||||
await expect(toggleAfterReload).toHaveAttribute("aria-pressed", "false");
|
||||
await expect(page.getByTestId("em-add-row-button")).toHaveCount(0);
|
||||
});
|
||||
Reference in New Issue
Block a user