ecc6f3efc471e65c71036bc230330a9262d4f296
122 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
34dd8d50a4 |
Task #496: hide row Quick Actions in edit mode + drop missing-time banner
- ScheduleSection: gate `quickActionsCanMutate` on `canMutate && !editMode` so clicking a row in edit mode no longer opens the Postpone dialog. In view mode the quick-actions surface still works as before. - MeetingRow: add a useEffect that force-closes local `quickOpen` when capability flips off (edit-mode toggle while dialog is open) and gate `quickOpenShadow` + `data-quick-open` on capability so no stale frame paints. Caught by code review. - Remove the amber `em-rotate-blocked-hint` missing-time banner per product feedback. Kept `missingTimeCount` / `dayRotatable` so row drag still aborts client-side and the per-row tooltip + native title still explain why. - Drop now-unused i18n keys `executiveMeetings.rotate.needsTimeWindow.hint*` from en.json and ar.json (zero/one/two/few/many/other variants). `tooltip` + `errorToast` strings stay (consumed by tooltip button + rotate-content catch handler). - Update executive-meetings-rotate-needs-time-window.spec.mjs to drop the banner visibility assertion and the post-unblock `toHaveCount(0)` re-check. Tooltip / aria-disabled / no-rotate-POST / successful- rotate-after-unblock checks remain. Verified: rotate-needs-time-window + row-quick-actions specs (7 tests) all pass with --workers=1. |
||
|
|
8fd16eb2dc |
Block EM row-drag when any meeting on the day lacks a time window (#492)
The /api/executive-meetings/rotate-content endpoint hard-rejects with
`code: "no_time_window"` whenever any visible non-cancelled meeting on
the date is missing (start_time, end_time). Before this change, dragging
a row on such a day produced an opaque English error toast that
confused AR users and didn't tell them what to fix.
UI changes:
- Compute `missingTimeCount` / `dayRotatable` from `orderedMeetings`
in executive-meetings.tsx and thread `dayRotatable` into MeetingRow.
- MeetingRow gates useSortable + safeRowDragListeners on
`effectiveDragEnabled = dragEnabled && dayRotatable`. When drag is
blocked specifically by missing time, the <tr> shows
`cursor-not-allowed`, dimmed opacity, `aria-disabled="true"`,
`data-drag-blocked="no_time_window"`, and a bilingual native `title`
(desktop hover fallback). The aria-disabled is spread AFTER dnd-kit's
attributes so it wins the prop merge.
- New `DragBlockedTooltipButton` rendered inside each blocked row's #
cell — a Radix Tooltip-wrapped warning icon that opens on hover,
focus, AND tap (manual open toggle) so iPad users can read the
explanation (native title long-press is unreliable on touch).
- Inline amber `em-rotate-blocked-hint` banner above the bulk toolbar
surfaces the missing-time count using i18next's CLDR plural resolver
(`t(key, { count })`) so AR picks the correct
zero/one/two/few/many/other form and EN picks one/other.
- rotateContent's catch handler detects ApiError code "no_time_window"
(local apiJson now attaches .code/.status to thrown Errors) and
shows the bilingual `executiveMeetings.rotate.needsTimeWindow.errorToast`
instead of the raw server message.
- Locale keys added under `executiveMeetings.rotate.needsTimeWindow`
in en.json + ar.json (tooltip, hint plurals, errorToast). AR has
full zero/one/two/few/many/other variants.
- ScheduleSection's `t` prop type widened to accept i18next options.
Tests:
- New tests/executive-meetings-rotate-needs-time-window.spec.mjs:
inserts one untimed + two timed meetings, asserts the hint banner +
every row's aria-disabled + data-drag-blocked, asserts the
Tooltip-wrapped info button is visible with the correct localized
aria-label + tooltip body (substring match — Radix renders sr-only
duplicate), attempts a drag and asserts NO rotate-content POST is
sent and DB state is unchanged. Then UPDATE-s the missing time and
asserts: hint disappears, aria-disabled lifts on every row, info
button is gone, drag now succeeds (rotate-content POST + DB
start_time mutation).
- Existing executive-meetings-row-drag, touch-reorder, and
row-quick-actions specs still pass.
Files: artifacts/tx-os/src/pages/executive-meetings.tsx,
artifacts/tx-os/src/locales/{en,ar}.json,
artifacts/tx-os/tests/executive-meetings-rotate-needs-time-window.spec.mjs
Server route untouched.
|
||
|
|
e19506aac7 |
Block EM row-drag when any meeting on the day lacks a time window (#492)
The /api/executive-meetings/rotate-content endpoint hard-rejects with
`code: "no_time_window"` whenever any visible non-cancelled meeting on
the date is missing (start_time, end_time). Before this change, dragging
a row on such a day produced an opaque English error toast ("Every
meeting must have a scheduled time window to rotate") that confused AR
users and didn't tell them what to fix.
Fix:
- Compute `missingTimeCount` / `dayRotatable` from `orderedMeetings`
in executive-meetings.tsx and thread `dayRotatable` into MeetingRow.
- MeetingRow now gates useSortable + safeRowDragListeners on
`effectiveDragEnabled = dragEnabled && dayRotatable`. When drag is
blocked specifically by missing time, the <tr> shows
`cursor-not-allowed`, dimmed opacity, `aria-disabled="true"`,
`data-drag-blocked="no_time_window"`, and a bilingual `title`
tooltip. The aria-disabled is spread AFTER dnd-kit's attributes so
it wins the prop merge.
- Inline amber `em-rotate-blocked-hint` banner above the bulk toolbar
surfaces the missing-time count so mutators see why drag is paused.
- rotateContent's catch handler now detects ApiError code
"no_time_window" (local apiJson now attaches .code/.status to the
thrown Error) and shows the bilingual
`executiveMeetings.rotate.needsTimeWindow.errorToast` instead of
the raw server message.
- Locale keys added under `executiveMeetings.rotate.needsTimeWindow`
in en.json + ar.json (tooltip, hint_one/_other [+ AR plurals],
errorToast).
Tests:
- New tests/executive-meetings-rotate-needs-time-window.spec.mjs
inserts one untimed + two timed meetings and asserts the hint
banner + every row's aria-disabled + data-drag-blocked, then
attempts a drag and asserts NO rotate-content POST is sent and
DB state is unchanged. After UPDATE-ing the missing time, asserts
the hint and aria-disabled lift.
- Existing executive-meetings-row-drag and touch-reorder specs still
pass — drag works exactly as before on fully-timed days.
Files: artifacts/tx-os/src/pages/executive-meetings.tsx,
artifacts/tx-os/src/locales/{en,ar}.json,
artifacts/tx-os/tests/executive-meetings-rotate-needs-time-window.spec.mjs
Server route untouched.
|
||
|
|
38a55b8b42 |
Task #491: center quick-actions surface + frame the selected row
The user reported that the small quick-actions popover (which holds the Postpone trigger) was hard to find — it floated next to the clicked row and they sometimes lost track of which meeting they were about to act on. They asked for the surface to appear in the center of the screen, and for the row they clicked to be clearly framed in a color while the surface is open. Changes: - Replaced the row's row-anchored Popover (PopoverAnchor + PopoverContent) with a centered Dialog (DialogContent) that Radix renders into a portal. The Dialog uses the existing `executiveMeetings.quickActions.label` translation key for its title (already present in both Arabic and English) and reuses #490's polished Postpone Button unchanged. Test ID stays `em-row-quick-${id}` so existing e2e specs continue to target the same surface. - Added a third inset-shadow ring (`inset 0 0 0 3px ${highlightColor}`) to the row's composed boxShadow stack while the row's quickOpen state is true. Listed first in the stack so it becomes the outermost frame and wins visually over the existing current-meeting / bulk-select rings. Uses the page-level highlightPrefs.color so the frame matches the user's chosen accent. Also exposes `data-quick-open="true"` on the <tr> for tests. - Updated the row-quick-actions e2e: the postpone-end-to-end test now asserts `role="dialog"` on the surface and the row carries `data-quick-open` while open and loses it after Postpone is clicked. Added a separate test for Escape closing the surface and clearing the row frame. Verified: 5/5 quick-actions tests pass, plus both drag specs (row-drag + iPad touch-reorder) still pass. |
||
|
|
c7942a221f |
Task #491: center quick-actions surface + frame the selected row
The user reported that the small quick-actions popover (which holds the Postpone trigger) was hard to find — it floated next to the clicked row and they sometimes lost track of which meeting they were about to act on. They asked for the surface to appear in the center of the screen, and for the row they clicked to be clearly framed in a color while the surface is open. Changes: - Replaced the row's row-anchored Popover (PopoverAnchor + PopoverContent) with a centered Dialog (DialogContent) that Radix renders into a portal. The Dialog uses the existing `executiveMeetings.quickActions.label` translation key for its title (already present in both Arabic and English) and reuses #490's polished Postpone Button unchanged. Test ID stays `em-row-quick-${id}` so existing e2e specs continue to target the same surface. - Added a third inset-shadow ring (`inset 0 0 0 3px ${highlightColor}`) to the row's composed boxShadow stack while the row's quickOpen state is true. Listed first in the stack so it becomes the outermost frame and wins visually over the existing current-meeting / bulk-select rings. Uses the page-level highlightPrefs.color so the frame matches the user's chosen accent. Also exposes `data-quick-open="true"` on the <tr> for tests. - Updated the row-quick-actions e2e: the postpone-end-to-end test now asserts `role="dialog"` on the surface and the row carries `data-quick-open` while open and loses it after Postpone is clicked. Added a separate test for Escape closing the surface and clearing the row frame. Verified: 5/5 quick-actions tests pass, plus both drag specs (row-drag + iPad touch-reorder) still pass. |
||
|
|
f1c9ef71b7 |
Task #491: center quick-actions surface + frame the selected row
The user reported that the small quick-actions popover (which holds the Postpone trigger) was hard to find — it floated next to the clicked row and they sometimes lost track of which meeting they were about to act on. They asked for the surface to appear in the center of the screen, and for the row they clicked to be clearly framed in a color while the surface is open. Changes: - Replaced the row's row-anchored Popover (PopoverAnchor + PopoverContent) with a centered Dialog (DialogContent) that Radix renders into a portal. The Dialog uses the existing `executiveMeetings.quickActions.label` translation key for its title (already present in both Arabic and English) and reuses #490's polished Postpone Button unchanged. Test ID stays `em-row-quick-${id}` so existing e2e specs continue to target the same surface. - Added a third inset-shadow ring (`inset 0 0 0 3px ${highlightColor}`) to the row's composed boxShadow stack while the row's quickOpen state is true. Listed first in the stack so it becomes the outermost frame and wins visually over the existing current-meeting / bulk-select rings. Uses the page-level highlightPrefs.color so the frame matches the user's chosen accent. Also exposes `data-quick-open="true"` on the <tr> for tests. - Updated the row-quick-actions e2e: the postpone-end-to-end test now asserts `role="dialog"` on the surface and the row carries `data-quick-open` while open and loses it after Postpone is clicked. Added a separate test for Escape closing the surface and clearing the row frame. Verified: 5/5 quick-actions tests pass, plus both drag specs (row-drag + iPad touch-reorder) still pass. |
||
|
|
bcda13d9c8 |
Ensure postpone button styling and text are correct for all devices
Add assertions to verify the styling classes and text content of the postpone button in the executive meetings row quick actions spec. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: bf387ee6-aedd-4092-acfb-d6a42a1969f2 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/591TuZw Replit-Helium-Checkpoint-Created: true |
||
|
|
c7168a959b |
Task #490: iPad row drag + Postpone button polish
The user reported that on iPad they could no longer drag a meeting row up or down, and that the Postpone button in the row quick-actions popover looked plain. Changes: - Sensor split for the schedule row DnD: replaced PointerSensor with MouseSensor (mouse only, distance:6) + kept TouchSensor (touch, delay:200ms / tolerance:8px). On iOS Safari, PointerSensor was receiving touch-derived pointer events and competing with the OS scroll claim — the browser reclaimed the gesture as a vertical scroll before the finger moved 6px, so drag never activated. Splitting sensors lets each input modality use the activation constraint that actually works for it. PointerSensor remains imported because the attendee-reorder dialog still uses it (out of scope). - Polished the Postpone button in the row quick-actions popover: swapped the bespoke <button> for the project's <Button> component (variant=default, size=sm), gave it a 40px tap target (h-10), shadow-sm, gap-2, min-w-[11rem], and a flex-row-reverse flip in RTL so the icon stays adjacent to the label in Arabic. - Rewrote tests/executive-meetings-touch-reorder.spec.mjs which was stale — it referenced the removed grip and the retired /reorder endpoint. New spec runs under iPad emulation (hasTouch + isMobile, 768x1024), clears the persisted edit-mode pref + asserts edit mode reads OFF (so the "drag works without edit mode" regression is explicitly guarded), long-presses the # cell, drags down, and asserts /api/executive-meetings/rotate-content fires + DB rotation matches expectation. Tests: touch-reorder (iPad TouchSensor), row-drag (desktop mouse via MouseSensor), and popover (4/4) all pass. Backend unchanged. |
||
|
|
f4f94981f8 |
Task #489 fix: decouple row drag from edit mode
Previous review rejected because row drag was gated on
`effectiveCanMutate = canMutate && editMode`, forcing users to
enter Edit mode to drag. Spec requires drag to work in view mode
for any user with mutate permission.
Changes:
- MeetingRow: new `dragEnabled` prop separate from `canMutate`.
Used by useSortable.disabled, safeRowDragListeners gate,
attributes spread, and cursor-grab class.
- Parent passes `dragEnabled={canMutate}` (raw, not gated by
editMode); `canMutate={effectiveCanMutate}` retained for inline
cell editing.
- handleRowClick: scope interactive `closest()` to descendants
(interactive !== e.currentTarget). Required because the row now
carries dnd-kit's role="button" in view mode and an unscoped
closest() matched the row itself, swallowing every popover click.
- e2e: removed edit-toggle click from row-drag spec — drag now
passes in view mode.
Tests: row-drag spec (view mode), popover spec (4/4), schedule-
features drag tests (290, 834), backend rotate-content (6/6) all
pass. 3 unrelated schedule-features failures reference a missing
em-nav-settings testid that does not exist in source — pre-existing
broken tests, not caused by this task.
|
||
|
|
f53f7307da |
Task #489: row-wide drag rotates meeting content; time + daily numbers anchored
Backend - New POST /api/executive-meetings/rotate-content (zod-validated) rotates ONLY meeting content through fixed (start_time, end_time, daily_number) slots. Same-date enforced; per-meeting expectedUpdatedAt → 409 stale; incomplete day (missing visible row) → 400. - ExecutiveMeetingsRotateContentBody added in lib/api-zod (manual.ts). - 6 backend tests cover happy path, stale, different_dates, 401, 403, incomplete_day. Existing /swap-times tests still pass. Frontend (artifacts/tx-os) - Whole <tr> is now the drag handle (the dedicated GripVertical button is retired). useSortable is gated on canMutate; safeRowDragListeners filters drags whose target is an interactive descendant (button, input, edit/time cells, row-actions, bulk-select). useSortable `attributes` are spread only when canMutate so view-mode rows stay clickable (otherwise aria-disabled blocked the popover trigger). - onRowDragEnd → rotateContent(fromId, toId): optimistic patch reassigns each chronological slot's tuple to the new occupant; rolls back + toast on failure. - Quick-actions popover now contains only Postpone (#486 Move up/down buttons removed). Tests (artifacts/tx-os) - New tests/executive-meetings-row-drag.spec.mjs: drags Alpha → Charlie position by # cell, asserts rotate-content fires and slots stay anchored. - tests/executive-meetings-row-quick-actions.spec.mjs: drops up/down cases, keeps Postpone + skip-surfaces + viewer. - tests/executive-meetings-schedule-features.spec.mjs: two legacy grip drag tests rewritten to drag the row body and target /rotate-content (the legacy /reorder route + tests are intentionally untouched). Drift / notes - Architect flagged a medium-severity hardening note: rotate-content FOR UPDATE locks orderedIds but not the day-scope completeness query. Out of #489 scope; no follow-up created (proposeFollowUpTasks was already consumed on #486). |
||
|
|
936e184b05 |
#486: Executive Meetings row click → quick-actions popover
Clicking any meeting row on the Executive Meetings schedule (gated only on canMutate, not editMode) opens a small popover with Move up / Move down / Postpone. Move up/down swap only the (startTime, endTime) tuple between the clicked meeting and its chronological neighbour on the same date — the Time column stays visually anchored to its row position. Backend - POST /executive-meetings/swap-times: transactional swap with FOR UPDATE row locking, optimistic-lock conflict shape (stale_meeting + conflict payload), date/time-window guards, audit logging, and renumberDayByStartTime + day-changed broadcast. - Zod schema in lib/api-zod/src/manual.ts. Frontend - Shared lib/api-json.ts JSON helper. - ScheduleSection.swapTimes does an optimistic (startTime, endTime) swap against the day query cache and rolls back on failure (mirrors the existing inline-edit UX). - Edge enablement (canQuickMoveUp/Down) now derived from the full orderedMeetings index, not the search-filtered displayedMeetings, so "Move up/down" disabled state always matches the true day boundary (and never silently swaps with a hidden filtered neighbour). - MeetingRow uses Popover/PopoverAnchor with skip rules: ARIA roles (button/checkbox/switch/combobox/dialog) and em-time-* / em-edit-* / em-row-grip / em-row-actions data-testid prefixes do NOT open the popover. - PostponeDialog reused from upcoming-meeting-alert.tsx. Tests - Backend swap-times: happy path, stale_meeting (409), different_dates (400), no_time_window (400), unauth (401), viewer-no-mutate (403), malformed-timestamp (400) — all 7 pass. - Hardened expectedUpdatedAt zod schema to z.string().datetime() so malformed tokens fail at validation with a controlled 400 instead of bubbling up as a 500. - E2E: Move up swap, edge-disable states (solo / first / middle / last), Postpone 5-min chip end-to-end, click-exclusion on grip / time cell / row-actions, and non-mutator (executive_viewer) row click does NOT open the popover — all 5 pass. Each test uses its own future date to avoid cross-test pollution. Code review approved on second pass. Pre-existing failures in other suites (executive-meetings reorder, font-settings, notes-share, service-orders) are unrelated to this task and predate it. Follow-ups proposed: #487 (keyboard a11y on the popover), #488 (edit- mode test gaps). |
||
|
|
02b3b374b0 |
#486: Executive Meetings row click → quick-actions popover
Clicking any meeting row on the Executive Meetings schedule (gated only on canMutate, not editMode) opens a small popover with Move up / Move down / Postpone. Move up/down swap only the (startTime, endTime) tuple between the clicked meeting and its chronological neighbour on the same date — the Time column stays visually anchored to its row position. Backend - POST /executive-meetings/swap-times: transactional swap with FOR UPDATE row locking, optimistic-lock conflict shape (stale_meeting + conflict payload), date/time-window guards, audit logging, and renumberDayByStartTime + day-changed broadcast. - Zod schema in lib/api-zod/src/manual.ts. Frontend - Shared lib/api-json.ts JSON helper. - ScheduleSection.swapTimes does an optimistic (startTime, endTime) swap against the day query cache and rolls back on failure (mirrors the existing inline-edit UX). - MeetingRow uses Popover/PopoverAnchor with skip rules: ARIA roles (button/checkbox/switch/combobox/dialog) and em-time-* / em-edit-* / em-row-grip / em-row-actions data-testid prefixes do NOT open the popover. - PostponeDialog reused from upcoming-meeting-alert.tsx. Tests - Backend swap-times: happy path, stale_meeting (409), different_dates (400), no_time_window (400), unauth (401), viewer-no-mutate (403) — all 6 pass. - E2E: Move up swap, edge-disable states (solo / first / middle / last), Postpone 5-min chip end-to-end, click-exclusion on grip / time cell / row-actions — all 4 pass. Each test uses its own future date to avoid cross-test pollution. Code review approved on second pass. Pre-existing failures in other suites (executive-meetings reorder, font-settings, notes-share, service-orders) are unrelated to this task and predate it. Follow-ups proposed: #487 (keyboard a11y on the popover), #488 (edit- mode test gaps). |
||
|
|
b1b77395d0 |
#486 Executive Meetings: row-click quick actions popover (Move up / Move down / Postpone)
Clicking any meeting row on the schedule now opens a small popover with three
quick actions: Move up, Move down, and Postpone. Gated only on canMutate (NOT
editMode) per spec.
Backend
- New POST /executive-meetings/swap-times endpoint (artifacts/api-server/src/
routes/executive-meetings.ts). FOR UPDATE locks both rows by ascending id to
avoid deadlocks, optimistic-lock check via expectedUpdatedAt{A,B} (returns
409 stale_meeting + conflict.lastActor — same shape PostponeDialog
understands), guards different_dates and no_time_window, swaps only
(startTime, endTime), audits each row as `meeting_swap_times`, calls
renumberDayByStartTime so the # column matches the new chronological order,
and broadcasts emitExecutiveMeetingsDayChanged.
- New zod body schema ExecutiveMeetingsSwapTimesBody in lib/api-zod/src/manual.ts.
Frontend
- New shared lib/api-json.ts (ApiError + apiJson) extracted from upcoming-
meeting-alert.tsx so the page can reuse the same fetch/error contract.
- upcoming-meeting-alert.tsx exports PostponeDialog so the page can mount it
for quick-action postpone without duplicating UI.
- artifacts/tx-os/src/pages/executive-meetings.tsx: Schedule wires
swapTimes/quickMoveUp/quickMoveDown/postpone state, computes per-day
neighbours via meetingNumbersById, and mounts a single page-level
PostponeDialog. MeetingRow wraps `<tr>` in a Popover/PopoverAnchor; row
onClick opens the popover with skip rules for buttons/inputs/contenteditable
and ARIA roles (button/checkbox/switch/combobox/dialog) plus testid
prefixes (em-row-grip/-actions/-select, em-edit-*, em-merge-edit-*, em-time-*)
so the time cell's inline editor and other affordances don't collide.
- en/ar locales gain executiveMeetings.quickActions.{label,moveUp,moveDown,postpone}.
Tests
- artifacts/api-server/tests/executive-meetings-swap-times.test.mjs (new):
happy path, 409 stale_meeting (with conflict actor), 400 different_dates,
400 no_time_window. Each scenario uses a distinct far-future date to avoid
daily_number races with seeded data.
- artifacts/tx-os/tests/executive-meetings-row-quick-actions.spec.mjs (new):
drives the date input, verifies row click → popover, Move up swap reflected
in DB, and Postpone item opens the dialog.
Code review (architect) flagged an edit-mode conflict where the time cell
(role=button div) would also bubble to the row handler; fixed by adding ARIA
role and em-time-* skip rules. Architect also flagged keyboard-trigger gap
and edit-mode test gaps — proposed as follow-ups.
Other test failures in the repo (executive-meetings reorder, font-settings,
notes-share, service-orders) pre-date this change and are unrelated.
|
||
|
|
2d131fddf0 |
#484 Reset meetings edit mode on leaving the app
The Executive Meetings page persisted its global "Edit / View" toggle in localStorage (key `em-schedule-edit-mode-v1:<userId>`). Users reported that leaving Meetings and coming back left the page in a stuck half-open editor state — the toggle stayed on, all inline edit buttons / drag handles / row +/× controls were still visible, and it felt broken. Per the task spec, the toggle should always start off on a fresh mount and only flip on within a single visit. Source change (artifacts/tx-os/src/pages/executive-meetings.tsx): - Removed the `EDIT_MODE_STORAGE_KEY` constant (replaced with a short comment explaining the non-persistence decision). - Dropped the `editModeStorageKey` per-user namespaced memo. - Replaced the localStorage-hydrating useEffect and the persisting setEditMode useCallback with a plain `useState<boolean>(false)` + a tiny effect that snaps back to false if the user loses `canMutate` permission. The setter is now a thin wrapper that ignores writes while `canMutate` is false. - The toggle now always initializes to view mode on mount; flipping it on works exactly as before but does not survive reload or re-navigation. Test changes (artifacts/tx-os/tests/executive-meetings-edit-toggle.spec.mjs): - Renamed the existing "Edit toggle hides editing affordances..." test to "...and resets to view mode on reload / re-navigation (#484)". - Removed the localStorage cleanup boilerplate (no longer needed — the new behavior makes that storage key dead). - Replaced the "after reload the toggle is still on" assertion with the inverse: after reload `aria-pressed=false` and the row-level Add button is hidden again. - Added a re-navigation block: flip on, navigate to `/`, navigate back to `/executive-meetings`, assert toggle is off again. - Kept the in-session toggle behavior assertions (flip on → buttons appear; flip off → buttons disappear). Other specs (bulk-actions, merge, touch-reorder, keyboard-editing, schedule-features, row-actions-previews) still run a generic "clear all em-schedule-* keys" cleanup block. Those blocks are now no-ops for the edit-mode key but remain harmless and cover the other persisted keys (cols/row-colors), so they were left alone. Verified: `tsc --noEmit` clean; the new "Edit toggle hides ... and resets to view mode on reload / re-navigation (#484)" test passes. Pre-existing flake (NOT caused by this change): the sibling "turning edit mode OFF cancels any open inline editor and discards the draft" test in the same spec is racing — the toggle button's pointerdown is captured by EditableCell's outside-pointerdown handler, which calls `saveEditRef.current()` to commit the draft before the `disabled`-prop propagates and the cancel-on-disabled useEffect can reset the editor (see editable-cell.tsx ~lines 295-307 vs ~421-426). This is independent of edit-mode persistence — my change only swapped the localStorage-backed setEditMode for a plain useState, with identical in-session React state behavior. Fixing the EditableCell race is out of scope for #484. Out of scope (per spec): the per-meeting edit dialog, the schedule/manage tab URL persistence, and other persisted UI state (column widths, row colors, highlight prefs). |
||
|
|
f8b0969cd2 |
Improve responsiveness for meeting reschedule dialog
Adjust grid breakpoints for the postpone dialog and update RTL test assertions. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 370d4507-cc98-4781-a35f-85ce37566751 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/591TuZw Replit-Helium-Checkpoint-Created: true |
||
|
|
1f225298b1 |
#485 Fix overlapping layout in the Postpone meeting dialog
In Arabic/RTL the Postpone dialog (opened from the upcoming-meeting
alert) was visibly overlapping its own borders on two tabs:
1. Postpone tab → cascade preview ("الاجتماعات المتأثرة"): the times
column ran wider than the pink panel, the title column pushed it
off-screen, and the meeting-# header wrapped to two lines while
the others stayed on one.
2. Reschedule tab: three native date / start / end inputs forced into
md:grid-cols-3 inside an sm:max-w-md dialog, which clipped the
right-most input against the dialog frame in RTL.
Both issues were pure layout — the dialog was simply too narrow for
its content.
Changes (artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx):
- Bumped the postpone DialogContent from `sm:max-w-md` (≈448 px) to
`sm:max-w-2xl` (≈672 px). Still full width on phones, capped on
desktop. Reschedule's existing `grid-cols-1 sm:grid-cols-2
md:grid-cols-3` now has room to actually render three inputs at md
without clipping (Input already defaults to w-full).
- CascadePromptBlock affected-meetings table: switched to
`table-fixed` with an explicit `<colgroup>` (w-20 number / flex
title / w-40 times). The number column was bumped from w-12 to
w-20 so the Arabic header "رقم الاجتماع" + `whitespace-nowrap`
has room at common font sizes (raised during code review). Title
cell keeps its `truncate` + `title=` tooltip but no longer needs
`max-w-[180px]`. Added `whitespace-nowrap` to # and times headers
so the latter never wraps. Wrapper got `overflow-x-hidden` to
belt-and-suspenders the no-horizontal-scroll guarantee. Action
button row already used `flex-wrap` — preserved.
Tests (artifacts/tx-os/tests/executive-meetings-upcoming-alert.spec.mjs):
- Added a Reschedule-tab regression test that opens the dialog at
1280×800 in Arabic and asserts the bounding boxes of
`reschedule-date` / `reschedule-start` / `reschedule-end` all sit
inside the dialog's bounding box (1 px tolerance).
- Added a Cascade-table regression test (raised during code review)
that seeds a primary + 2 followers, postpones to fire the cascade
prompt, and asserts the `cascade-followers-list` and the last
`<td>` (times) of the first follower row both sit inside the
dialog at desktop width in RTL.
- Confirmed no behavior regressions across the postpone-by-10,
cascade follower display, tab-switching, and reschedule-to-tomorrow
tests.
Out of scope (per task spec): backend cascade/postpone logic, the
floating alert panel itself, dialog visual restyling, and the Cancel
tab beyond what comes "for free" with the wider dialog.
The pre-existing `test` workflow failures (executive-meetings reorder,
font-settings roundtrip, notes-share recipient PATCH 403/404) are
unrelated to this layout change and were already failing before.
|
||
|
|
21bac11aa5 |
#485 Fix overlapping layout in the Postpone meeting dialog
In Arabic/RTL the Postpone dialog (opened from the upcoming-meeting
alert) was visibly overlapping its own borders on two tabs:
1. Postpone tab → cascade preview ("الاجتماعات المتأثرة"): the times
column ran wider than the pink panel, the title column pushed it
off-screen, and the meeting-# header wrapped to two lines while
the others stayed on one.
2. Reschedule tab: three native date / start / end inputs forced into
md:grid-cols-3 inside an sm:max-w-md dialog, which clipped the
right-most input against the dialog frame in RTL.
Both issues were pure layout — the dialog was simply too narrow for
its content.
Changes (artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx):
- Bumped the postpone DialogContent from `sm:max-w-md` (≈448 px) to
`sm:max-w-2xl` (≈672 px). Still full width on phones, capped on
desktop. Reschedule's existing `grid-cols-1 sm:grid-cols-2
md:grid-cols-3` now has room to actually render three inputs at md
without clipping (Input already defaults to w-full).
- CascadePromptBlock affected-meetings table: switched to
`table-fixed` with an explicit `<colgroup>` (w-12 number / flex
title / w-40 times). Title cell keeps its `truncate` + `title=`
tooltip but no longer needs `max-w-[180px]`. Added `whitespace-nowrap`
to # and times headers so the latter never wraps. Wrapper got
`overflow-x-hidden` to belt-and-suspenders the no-horizontal-scroll
guarantee. Action button row already used `flex-wrap` — preserved.
Tests (artifacts/tx-os/tests/executive-meetings-upcoming-alert.spec.mjs):
- Added a regression test that opens the dialog at 1280×800 in
Arabic, switches to the Reschedule tab, and asserts the bounding
boxes of `reschedule-date` / `reschedule-start` / `reschedule-end`
all sit inside the dialog's bounding box (1 px tolerance).
- Re-ran the full upcoming-alert spec to confirm no behavior
regressions in postpone-by-minutes, cascade prompts, cancel,
reschedule-to-tomorrow, dismiss, or the alert position-clamp test.
Out of scope (per task spec): backend cascade/postpone logic, the
floating alert panel itself, dialog visual restyling, and the Cancel
tab beyond what comes "for free" with the wider dialog.
The pre-existing `test` workflow failures (executive-meetings reorder,
font-settings roundtrip, notes-share recipient PATCH 403/404) are
unrelated to this layout change and were already failing before.
|
||
|
|
2b31b5e3aa |
Remove title field from notes and update tests to reflect changes
Refactors the notes feature by removing the `title` field and updating all related tests and UI components to use `content` instead. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 7464969c-726a-4ec2-a3b2-8ff63f91f6ed Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/591TuZw Replit-Helium-Checkpoint-Created: true |
||
|
|
d6b19748f7 |
Update upcoming meeting alert to better display cascade information
Refactor upcoming meeting alert component to update the cascade prompt table structure and adjust end-to-end tests to reflect the changes in column count and content assertions. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 9509e0a2-e6ad-4b70-8d04-56b729a8f67c Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/8BABrKh Replit-Helium-Checkpoint-Created: true |
||
|
|
7ea7744e26 |
#481 Polish cascade-affected meetings table in postpone/reschedule prompt
- Drop the leading "#" index column; dailyNumber already serves as a stable per-row identifier. - Replace the hard-coded amber background/border with the user's chosen alert accent via hexToRgba(accent, 0.12 / 0.45). Threaded `accent` through PostponeDialog → CascadePromptBlock so children don't read prefs directly. Both the loading and main prompt blocks track the accent; the rose blocked-by-midnight variant is intentionally kept. - Render times as localized 12-hour with ص/م (AR) or AM/PM (EN) by reusing the shared formatTime helper instead of slicing "HH:mm". - Drop now-unused i18n key cascadeColIndex from ar.json + en.json. - Update the e2e test to match the 3-column schema (meeting#, title, times); kept the dailyNumber assertion in the first cell. tsc clean. Cascade specs pass (Postpone by 10, Reschedule cascade, Cascade prompt UI: Shift/Keep, no-followers fallthrough). |
||
|
|
5e37d392c0 |
#479: Render upcoming-alert attendees as numbered tables per group
- DetailsPanel in upcoming-meeting-alert.tsx: replaced the per-group
`<ul class="list-disc">` with a real `<table class="w-full">`. Each
attendee row is `<tr>` with two `<td>`s: a narrow tabular-nums index
cell (`{idx + 1}.`, scope="row") and the cleaned name cell. The
group heading is rendered as `<caption>` (text-start, font-bold,
text-xs) so it spans both columns and is announced as the table
caption to AT. Each table also carries `aria-label={group.heading}`.
- Numbering restarts at 1 per group (each group is its own table); a
code comment notes the one-line tweak to use a continuous count.
- Preserved: `data-testid="alert-details-attendees"` wrapper,
`max-h-40 overflow-y-auto` scroll, empty-state, location/URL rows,
and the section header total ("الحضور (n)"). RTL/LTR work via
`text-start` and `pe-1`.
- e2e: added a new test in
`artifacts/tx-os/tests/executive-meetings-upcoming-alert.spec.mjs`
that seeds a meeting with two subheading-delimited groups (3
external + 1 internal), expands details, and asserts two tables
render with rows starting at "1." in each group.
Verification:
- `pnpm exec tsc --noEmit` clean.
- New attendees-table spec passes (1/1, 16.5s).
No server, schema, or i18n key changes; Tailwind utilities only.
|
||
|
|
ae31ed90c6 |
#480 Show cascade-affected meetings as a numbered table
Replace the simple <ul> in the cascade-prompt panel inside the upcoming- meeting alert with a 4-column table: row index, the meeting's dailyNumber, title (truncate + title=), and "from → to". The new `meetingNumbersById` map is built once in UpcomingMeetingAlert from the already-loaded dayData and threaded through PostponeDialog into both CascadePromptBlock call sites (postpone-minutes and reschedule). Existing `cascade-followers-list` and `cascade-follower-<id>` testids are preserved on the table/rows so prior selectors keep matching. Adds 4 new i18n keys (cascadeColIndex/MeetingNumber/Title/Times) in ar.json and en.json. The legacy cascadeListItem key is left in place since it isn't worth a separate cleanup. Extends the existing "Cascade prompt UI: Shift sends cascadeFollowing" e2e test to assert the first row's index cell renders "1" and the meeting-number cell renders the seeded follower's dailyNumber. insertImminentMeeting now also returns dailyNumber so the assertion can read it. tsc clean; targeted cascade specs pass (3/3). |
||
|
|
26ed7a195e |
notes(sent): multi-select + bulk delete on Sent tab (task #472)
- Add useBulkDeleteSentNotes hook with bounded concurrency (worker
pool, cap = 6) over the existing per-id DELETE /notes/:id endpoint;
returns {ok, failed} so the UI can render a precise toast. One cache
invalidation at the end (notes + folders).
- notes.tsx: page-level selection state for the Sent tab only
(sentSelectionMode, selectedSentIds, bulkDeleteOpen). Header
Select/Cancel toggle, sticky bulk-action bar with count, select-all/
clear, in-bar Cancel, Delete, and an AlertDialog confirmation. Auto
exits selection mode when leaving the Sent tab; prunes selected ids
to currently visible filteredSent on every change (search, refresh,
successful delete).
- SentList: switched the outer card from a nested <button> to a
div role="button" tabIndex=0 with Enter/Space handler so the
selection-mode Checkbox is no longer a nested interactive control.
Checkbox is aria-hidden / pointer-events-none inside selection mode.
- i18n: notes.bulk* keys (Select / Cancel / SelectAll / Clear / Count
with _one plural / Delete / Confirm title+body / Result success /
partial / failure) in both en.json and ar.json.
- New e2e: tests/notes-sent-bulk-delete.spec.mjs seeds 3 sent notes,
selects 2, confirms the bulk delete, asserts the cards are gone and
the surviving note remains (DB + UI).
- Out of scope (proposed as follow-ups #473/#474/#475): undo on bulk
delete, multi-select on Inbox/Archived tabs, additional e2e cases.
|
||
|
|
ef1f23a11d |
Task #466: Clean notes page from email-like 4-tab bar
User asked to remove the prominent 4-tab bar (My Notes / Inbox / Sent / Archive) from the notes page because it made the page feel like an email client, and they rarely use the share-between-users feature. Changes (artifacts/tx-os/src/pages/notes.tsx): - Deleted the inline-flex TabButton row that held the four view tabs. - Page now opens directly on the unified "active" feed. - Added an overflow DropdownMenu (⋯ MoreVertical icon) on the end of the controls row containing three items: Inbox (with unread badge), Sent, Archived. Each item calls setView() with the same TabId values, so all downstream branching (data fetching, filtering, composer visibility, folder rail) is unchanged. - Unread inbox badge now appears (a) as a small numeric dot on the overflow trigger when not currently in Inbox view, and (b) inside the dropdown next to the Inbox item — the existing `data-testid="notes-inbox-unread-badge"` is preserved. - Added a small "← My Notes" back button (testid notes-back-to-active) that appears only when view !== "active" so the user can return after drilling into Inbox/Sent/Archived without a visible tab bar. - Removed the now-unused TabButton component definition. Tests (artifacts/tx-os/tests/notes-inbox.spec.mjs): - Replaced the two notes-tab-received / notes-tab-sent clicks with the open-dropdown-then-select-item sequence using the new overflow testids. Validation: - pnpm tsc --noEmit passes for @workspace/tx-os. - Architect review: no severe issues; layout, a11y, RTL, z-index, and state-reset behavior all noted as sound. - The `test` workflow has pre-existing failures (executive-meetings PDF, notes-share, groups-crud) that are explicitly out of scope per the task description and tracked by other open follow-up tasks. No backend, schema, API, or i18n JSON changes were needed: the file uses inline t() defaults, and `notes.tabs.received/sent/archived` keys are reused inside the dropdown. |
||
|
|
daa4f6c038 |
Task #463: @dnd-kit notes drag + reorder
- Replace HTML5+touch drag with @dnd-kit (PointerSensor distance:8, TouchSensor delay:200/tol:8) matching home.tsx pattern. - Add sort_order column to notes; ORDER BY asc(sortOrder), updatedAt desc. - New PATCH /notes/reorder endpoint: strict isPinned boolean validation, bucket+permission scoped, all writes in db.transaction for atomicity. - PATCH /notes/:id stamps sort_order = min-1 on folder/pin bucket change. - Client useReorderNotes hook with optimistic cache update. - handleDragEnd builds reorder payload from FULL bucket (owner notes or shared-folder bucket via ref), not the filtered/search subset, so hidden siblings retain stable order. - SharedFolderView publishes its data.notes via bucketRef when viewer has edit permission, enabling correct reorder in shared folders. - Layout fix at narrow viewport: rail stacks above notes (flex-col md:flex-row) so iPad portrait drag has proper bbox. - Playwright tests: notes-folders.spec.mjs both desktop pointer drag and touch long-press drag pass (32s). - OpenAPI codegen skipped: notes-api.ts is hand-written. - Out of scope (pre-existing failures): executive-meetings reorder/font, notes-share PATCH 403/404, groups-crud rollback. |
||
|
|
28ddbe33d1 |
Task #441: Fix notification sound on iPad/Safari (Web Audio migration)
- Rewrote artifacts/tx-os/src/lib/notification-sounds.ts to use the
Web Audio API (lazy AudioContext + decoded AudioBuffer cache) instead
of HTMLAudioElement. unlock() resumes the ctx for any non-running
state (handles iOS `interrupted`), plays a 1-sample silent buffer to
satisfy iOS Safari's gesture requirement, and pre-warms decodes.
decodeAudioData wrapped to support both Promise and callback forms
for older WebKit.
- play() dispatches an AUTOPLAY_BLOCKED_EVENT with `{ isIos }` detail
when ctx isn't running. Throttle is 3 s; new bypassThrottle option
(used by testPlay) skips the gate AND avoids advancing lastPlayAt
so settings previews can't suppress real socket chimes.
- use-audio-unlock.ts keeps gesture listeners attached and re-calls
unlock() on every gesture (handles iPad ctx auto-suspend on
background / AirPlay).
- use-autoplay-hint.ts reads detail.isIos and switches between
notifSettings.autoplayHint and notifSettings.autoplayHintIos
(added to en.json + ar.json with Control Center / silent-switch
guidance).
- notification-settings.tsx per-sound preview button now calls
notificationPlayer.testPlay(id).
- Test globals __txosNotifPlayCount/__txosNotifLastSound preserved;
added __txosNotifCtxState() accessor (dev/test-only, gated by
import.meta.env.MODE).
- Added tests/notification-sound-webaudio.spec.mjs covering the
preview button gesture path: counter increments, throttle bypass
works, singleton AudioContext reaches `running` state.
Notes:
- Pre-existing api-server tsc errors in executive-meetings.ts and the
failing `test` workflow are unrelated to this task and out of scope.
|
||
|
|
093f42665c |
Task #438: collaborative checklists + meeting alert animation parity
Backend - New POST /notes/:id/checklist/:itemId/toggle endpoint. Owner or any active (non-archived) recipient can flip an item's done flag. - Wrapped in a DB transaction with SELECT ... FOR UPDATE on the live note row so concurrent toggles from multiple collaborators can't lose each other's updates. Live notes.items + every note_recipients.items snapshot are mirrored atomically in the same tx. - Emits note_checklist_changed to the audience minus the actor with the full updated items array so receivers can patch state without an extra fetch. Frontend - useToggleChecklistItem mutation hook with optimistic updates across notes/sent/received/thread caches and rollback on error. - Incoming-note popup checklist is now interactive with a local override for instant visual feedback (popup renders from socket payload, not from the query cache). - Thread checklist toggles are gated by effective permission (owner, admin, or non-archived recipient) to mirror server auth. - Socket handler for note_checklist_changed invalidates relevant query keys AND patches the open popup payload directly via a new IncomingNotePopupContext.updateChecklistItems action so collaborators' open popups stay in sync. Meeting alert animation parity (upcoming-meeting-alert.tsx) - Added scale-95 -> scale-100 + fade entrance with the same spring cubic-bezier the note popup uses, retriggered per eligible meeting. - Replaced the 1px border with a ring-4 colored frame driven by alertPrefs.accent (via boxShadow so the color is dynamic). - Added an animate-ping accent halo around the drag-handle icon to match the popup's avatar pulse. Tests - artifacts/api-server/tests/notes-checklist-toggle.test.mjs (7 tests: owner/recipient toggle + mirroring, 403 non-recipient, 403 archived, 400 non-checklist, 404 unknown item, 400 invalid body). - artifacts/tx-os/tests/notes-popup-checklist-collab.spec.mjs (e2e: recipient ticks an item from the popup, server + sender snapshot both reflect the change). Code review (architect) findings addressed: - (critical) lost-update race -> tx with row lock. - (critical) non-atomic snapshot mirror -> same tx. - (critical) open popup didn't re-render on socket fanout -> updateChecklistItems context action. - UI permission parity for archived recipients -> thread gates onToggle. |
||
|
|
3d8840afee |
Task #438: collaborative checklists + meeting alert animation parity
Backend - New POST /notes/:id/checklist/:itemId/toggle endpoint. Owner or any active (non-archived) recipient can flip an item's done flag. - Wrapped in a DB transaction with SELECT ... FOR UPDATE on the live note row so concurrent toggles from multiple collaborators can't lose each other's updates. Live notes.items + every note_recipients.items snapshot are mirrored atomically in the same tx. - Emits note_checklist_changed to the audience minus the actor with the full updated items array so receivers can patch state without an extra fetch. Frontend - useToggleChecklistItem mutation hook with optimistic updates across notes/sent/received/thread caches and rollback on error. - Incoming-note popup checklist is now interactive with a local override for instant visual feedback (popup renders from socket payload, not from the query cache). - Thread checklist toggles are gated by effective permission (owner, admin, or non-archived recipient) to mirror server auth. - Socket handler for note_checklist_changed invalidates relevant query keys AND patches the open popup payload directly via a new IncomingNotePopupContext.updateChecklistItems action so collaborators' open popups stay in sync. Meeting alert animation parity (upcoming-meeting-alert.tsx) - Added scale-95 -> scale-100 + fade entrance with the same spring cubic-bezier the note popup uses, retriggered per eligible meeting. - Replaced the 1px border with a ring-4 colored frame driven by alertPrefs.accent (via boxShadow so the color is dynamic). - Added an animate-ping accent halo around the drag-handle icon to match the popup's avatar pulse. Tests - artifacts/api-server/tests/notes-checklist-toggle.test.mjs (7 tests: owner/recipient toggle + mirroring, 403 non-recipient, 403 archived, 400 non-checklist, 404 unknown item, 400 invalid body). - artifacts/tx-os/tests/notes-popup-checklist-collab.spec.mjs (e2e: recipient ticks an item from the popup, server + sender snapshot both reflect the change). Code review (architect) findings addressed: - (critical) lost-update race -> tx with row lock. - (critical) non-atomic snapshot mirror -> same tx. - (critical) open popup didn't re-render on socket fanout -> updateChecklistItems context action. - UI permission parity for archived recipients -> thread gates onToggle. |
||
|
|
6292d52ffa |
Task #438: collaborative checklists + meeting alert animation parity
Backend - New POST /notes/:id/checklist/:itemId/toggle endpoint. Owner or any active (non-archived) recipient can flip an item's done flag. - Wrapped in a DB transaction with SELECT ... FOR UPDATE on the live note row so concurrent toggles from multiple collaborators can't lose each other's updates. Live notes.items + every note_recipients.items snapshot are mirrored atomically in the same tx. - Emits note_checklist_changed to the audience minus the actor with the full updated items array so receivers can patch state without an extra fetch. Frontend - useToggleChecklistItem mutation hook with optimistic updates across notes/sent/received/thread caches and rollback on error. - Incoming-note popup checklist is now interactive with a local override for instant visual feedback (popup renders from socket payload, not from the query cache). - Thread checklist toggles are gated by effective permission (owner, admin, or non-archived recipient) to mirror server auth. - Socket handler for note_checklist_changed invalidates relevant query keys AND patches the open popup payload directly via a new IncomingNotePopupContext.updateChecklistItems action so collaborators' open popups stay in sync. Meeting alert animation parity (upcoming-meeting-alert.tsx) - Added scale-95 -> scale-100 + fade entrance with the same spring cubic-bezier the note popup uses, retriggered per eligible meeting. - Replaced the 1px border with a ring-4 colored frame driven by alertPrefs.accent (via boxShadow so the color is dynamic). - Added an animate-ping accent halo around the drag-handle icon to match the popup's avatar pulse. Tests - artifacts/api-server/tests/notes-checklist-toggle.test.mjs (7 tests: owner/recipient toggle + mirroring, 403 non-recipient, 403 archived, 400 non-checklist, 404 unknown item, 400 invalid body). - artifacts/tx-os/tests/notes-popup-checklist-collab.spec.mjs (e2e: recipient ticks an item from the popup, server + sender snapshot both reflect the change). Code review (architect) findings addressed: - (critical) lost-update race -> tx with row lock. - (critical) non-atomic snapshot mirror -> same tx. - (critical) open popup didn't re-render on socket fanout -> updateChecklistItems context action. - UI permission parity for archived recipients -> thread gates onToggle. |
||
|
|
a430812998 |
Task #438: collaborative checklists + meeting alert animation parity
Backend - New POST /notes/:id/checklist/:itemId/toggle endpoint. Owner or any active (non-archived) recipient can flip an item's done flag. - Wrapped in a DB transaction with SELECT ... FOR UPDATE on the live note row so concurrent toggles from multiple collaborators can't lose each other's updates. Live notes.items + every note_recipients.items snapshot are mirrored atomically in the same tx. - Emits note_checklist_changed to the audience minus the actor with the full updated items array so receivers can patch state without an extra fetch. Frontend - useToggleChecklistItem mutation hook with optimistic updates across notes/sent/received/thread caches and rollback on error. - Incoming-note popup checklist is now interactive with a local override for instant visual feedback (popup renders from socket payload, not from the query cache). - Thread checklist toggles are gated by effective permission (owner, admin, or non-archived recipient) to mirror server auth. - Socket handler for note_checklist_changed invalidates relevant query keys AND patches the open popup payload directly via a new IncomingNotePopupContext.updateChecklistItems action so collaborators' open popups stay in sync. Meeting alert animation parity (upcoming-meeting-alert.tsx) - Added scale-95 -> scale-100 + fade entrance with the same spring cubic-bezier the note popup uses, retriggered per eligible meeting. - Replaced the 1px border with a ring-4 colored frame driven by alertPrefs.accent (via boxShadow so the color is dynamic). - Added an animate-ping accent halo around the drag-handle icon to match the popup's avatar pulse. Tests - artifacts/api-server/tests/notes-checklist-toggle.test.mjs (7 tests: owner/recipient toggle + mirroring, 403 non-recipient, 403 archived, 400 non-checklist, 404 unknown item, 400 invalid body). - artifacts/tx-os/tests/notes-popup-checklist-collab.spec.mjs (e2e: recipient ticks an item from the popup, server + sender snapshot both reflect the change). Code review (architect) findings addressed: - (critical) lost-update race -> tx with row lock. - (critical) non-atomic snapshot mirror -> same tx. - (critical) open popup didn't re-render on socket fanout -> updateChecklistItems context action. - UI permission parity for archived recipients -> thread gates onToggle. |
||
|
|
213895e9e8 |
Task #433: incoming-note popup — render full note + 3-button action row
- Inner body now renders as a real sticky note: keeps colored
surface, supports checklist (read-only) when noteKind === "checklist".
- Action row trimmed to exactly 3 buttons: Done (تم), Open note
(فتح الملاحظة), Reply (رد). Removed the standalone Dismiss button;
header X still closes.
- Reply variant uses the same 3-button row (Done is a dismiss-only
no-op since the note owner can't mark their own note read).
- Wire kind + items from backend `note_received` payload into client
IncomingNotePayload (noteKind/items) via use-notifications-socket.
- Locales: markRead → "Done"/"تم"; new openNote/done keys; openThread
unified to "Open note"/"فتح الملاحظة".
- Tests: replaced removed `incoming-note-popup-dismiss` testid usages
with header `-close`; updated reply variant assertions to expect the
3-button row; fixed pre-existing pluralization bug in reply path
(`/replies` → `/reply`) that was unrelated to this task but blocked
the reply popup test from validating.
Labels and pin were not surfaced in the popup because they are
sender-private state (note_recipients only snapshots
title/content/color/kind/items); the recipient never receives the
sender's labelIds/isPinned, so showing them would be misleading.
Added a new e2e ("recipient popup renders checklist items for a
checklist note") that composes a checklist note via the UI, sends it,
and asserts `incoming-note-popup-checklist` is visible with both
items — locks in the noteKind/items socket plumbing.
All affected e2e tests pass (notes-popup-on-receive note + reply +
checklist, notes-popup-touch-tap both, notes-inbox); queue unit
tests pass.
|
||
|
|
ff3c567b3c |
Task #433: incoming-note popup — render full note + 3-button action row
- Inner body now renders as a real sticky note: keeps colored surface, supports checklist (read-only) when noteKind === "checklist". - Action row trimmed to exactly 3 buttons: Done (تم), Open note (فتح الملاحظة), Reply (رد). Removed the standalone Dismiss button; header X still closes. - Reply variant uses the same 3-button row (Done is a dismiss-only no-op since the note owner can't mark their own note read). - Wire kind + items from backend `note_received` payload into client IncomingNotePayload (noteKind/items) via use-notifications-socket. - Locales: markRead → "Done"/"تم"; new openNote/done keys; openThread unified to "Open note"/"فتح الملاحظة". - Tests: replaced removed `incoming-note-popup-dismiss` testid usages with header `-close`; updated reply variant assertions to expect the 3-button row; fixed pre-existing pluralization bug in reply path (`/replies` → `/reply`) that was unrelated to this task but blocked the reply popup test from validating. Labels and pin were not surfaced in the popup because they are sender-private state (note_recipients only snapshots title/content/color/kind/items); the recipient never receives the sender's labelIds/isPinned, so showing them would be misleading. All affected e2e tests pass (notes-popup-on-receive note + reply, notes-popup-touch-tap both, notes-inbox); queue unit tests pass. |
||
|
|
fc14ca4c01 |
Task #427: fix floating note popup hanging on touch devices
Root cause on iOS Safari (iPad/phone): the `IncomingNotePopup` rendered a `fixed inset-0 z-[110]` full-viewport wrapper. Stacking that layer on top of the upcoming-meeting alert plus a Radix toast intermittently caused the next finger tap to be swallowed — the user saw the popup "hang". A simultaneous re-render storm from synchronous query invalidations on multi-event socket fanouts made it worse on slower hardware. Fixes: - Drop the full-viewport wrapper: render the popup card directly as a sized `fixed` element. No more invisible viewport-sized layer between touch and the rest of the UI. - Suppress the redundant note/reply toast when the floating popup actually accepts the event. The popup IS the surface; doubling up just stacks one more floating layer. - Remove `autoFocus` on the Reply button — focus-steal was contributing to the perceived hang. - Coalesce `react-query` `invalidateQueries` calls in the notifications socket: collect into a Set and flush once per animation frame so a burst of socket events triggers a single refetch pass instead of N synchronous re-renders. Cleaned up on socket disconnect. Tests (artifacts/tx-os/tests/notes-popup-touch-tap.spec.mjs): - "recipient on a touch device can tap Reply on the floating note popup" — verifies the popup is no longer viewport-sized, the duplicate toast is suppressed, and a real `locator.tap()` succeeds. - "popup + meeting alert simultaneously: tap on popup still works" — the user's exact scenario: seeds an imminent meeting so the alert appears, triggers a real cross-user note delivery so both float simultaneously, then taps Reply on the popup and confirms it dismisses while the alert remains. Both tests + tsc --noEmit clean. Files: - artifacts/tx-os/src/components/notes/incoming-note-popup.tsx - artifacts/tx-os/src/hooks/use-notifications-socket.ts - artifacts/tx-os/tests/notes-popup-touch-tap.spec.mjs (new) |
||
|
|
e283a83162 |
Task #427: fix floating note popup hanging on touch devices
Root cause on iOS Safari (iPad/phone): the `IncomingNotePopup` rendered a `fixed inset-0 z-[110]` full-viewport wrapper with `pointer-events-none`. Stacking that layer on top of the upcoming meeting alert plus a Radix toast intermittently caused the next finger tap to be swallowed — the user saw the popup "hang". Fixes: - Drop the full-viewport wrapper: render the popup card directly as a sized `fixed` element. No more invisible viewport-sized layer between touch and the rest of the UI. - Suppress the redundant note/reply toast when the floating popup actually accepts the event. The popup IS the surface; doubling up just stacks one more floating layer. - Remove `autoFocus` on the Reply button. A new popup arriving while the user is mid-tap would steal focus, contributing to the perceived hang. Tests: - New `notes-popup-touch-tap.spec.mjs`: emulates a touch viewport, sends a real cross-user note, verifies popup bounding box is card-sized (not viewport-sized), the redundant toast is suppressed, and a `locator.tap()` on Reply dismisses the popup and navigates. - `tsc --noEmit` clean. Files: - artifacts/tx-os/src/components/notes/incoming-note-popup.tsx - artifacts/tx-os/src/hooks/use-notifications-socket.ts - artifacts/tx-os/tests/notes-popup-touch-tap.spec.mjs (new) |
||
|
|
32db433581 |
notes: show note-card actions on touch devices (task #425)
Original task: on phones and iPad the per-card action row (color,
label, send, archive, delete) was hidden by `opacity-0
group-hover:opacity-100`. Touch devices have no hover, so users
couldn't send a freshly-created note (or change its color, label,
archive it).
Changes
- artifacts/tx-os/src/pages/notes.tsx
- Add Tailwind v4 arbitrary variant `[@media(hover:none)]:opacity-100`
to the per-card action row so it stays always-visible on touch
devices while desktops keep the existing hover-reveal behaviour.
- Apply the same variant to the unpinned pin button (which uses the
identical hover-only treatment).
Tests
- artifacts/tx-os/tests/notes-card-touch-actions.spec.mjs (new)
- Uses `test.use({ hasTouch: true, isMobile: true, viewport })` on
chromium (webkit isn't installed) so the page reports
`(hover: none)`. Sanity-checks that via matchMedia, then creates a
note via the composer (tap-outside auto-save) and asserts both
that the Send button is visible and that the action row's
computed opacity is exactly 1 — Playwright's `toBeVisible` alone
doesn't catch opacity-0.
Verification
- pnpm --filter @workspace/tx-os exec tsc --noEmit: clean.
- pnpm --filter @workspace/tx-os test:e2e -- notes-delete-confirm
notes-card-touch-actions: 3 passed (touch test + the two
delete-confirm specs from #423 to confirm no regression).
- Architect code review: no severe issues. Desktop hover-reveal is
preserved because `(hover: none)` doesn't match on pointer devices.
Out of scope (per task plan): the trash button inside the
manage-labels dialog (different surface, dialog-only).
|
||
|
|
2027ee2545 |
notes: replace native confirm() with styled AlertDialog (task #423)
Original task: deleting a note (and a label from the manage-labels menu)
showed the browser's native confirm() with the raw Repl hostname,
clashing with the rest of the app. Switch both call sites to the
project's existing AlertDialog component, matching the pattern used in
FoldersRail and SendNoteDialog.
Changes
- artifacts/tx-os/src/pages/notes.tsx
- NoteCard: add `confirmDeleteOpen` state; render AlertDialog as a
Fragment sibling of the clickable card (not a child) so React's
portal event bubbling cannot trigger the card's onEdit handler when
the user interacts with the dialog. Also stopPropagation on the
trash button itself.
- LabelsDialog: add `pendingDeleteLabel` state and a sibling
AlertDialog inside the Dialog root. Both dialogs use existing
locale keys (notes.deleteConfirm, notes.deleteLabelConfirm,
notes.delete, common.cancel) — no new strings needed.
- Add data-testids: note-card-delete-{id}, note-delete-dialog-{id},
note-delete-confirm-{id}, label-delete-{id},
label-delete-dialog-{id}, label-delete-confirm-{id},
notes-manage-labels-open.
Tests
- artifacts/tx-os/tests/notes-delete-confirm.spec.mjs (new)
- Note delete: cancel keeps the card, confirm fires DELETE and
removes the card. Deliberately does NOT register a `dialog` event
handler — a native confirm would hang the test.
- Label delete: open manage-labels, create a label via the UI, click
trash, confirm, assert DELETE /api/note-labels/{id}.
Verification
- pnpm --filter @workspace/tx-os exec tsc --noEmit: clean.
- pnpm --filter @workspace/tx-os test:e2e -- notes-delete-confirm:
2 passed.
- Architect code review surfaced a real bubble-through risk on the
card's onClick={onEdit}; fixed by lifting the AlertDialog to a
Fragment sibling.
Out of scope (not changed): native confirm()/alert() outside the Notes
page. (executive-meetings.tsx already uses an in-app `confirm({...})`
modal, not the native browser dialog.)
|
||
|
|
1ffb470e8f |
notes: add per-note checklist (to-do list) option
Task #420 — answers the user request "وين خيار اضيف list to do?". Schema (lib/db/src/schema/notes.ts): - notes + note_recipients gain `kind` (varchar(16) default 'text') and `items` (jsonb<ChecklistItem[]> nullable). Snapshot copy on note_recipients keeps delivered checklists immutable across sender edits/deletes. - Drizzle push applied; lib/db .d.ts rebuilt. API (artifacts/api-server/src/routes/notes.ts): - ChecklistItem type + bounded parser (≤200 items, id ≤64, text ≤500). - parseNoteInput normalizes items↔kind on create and on PATCH that carries `kind`; PATCH handler additionally coerces items-only patches against the note's existing kind so a text note can never end up with checklist items (and vice versa). - POST/PATCH/send/loadRecipientsForNote/received/thread responses and the realtime `note_received` payload all carry kind+items, with the thread response falling back to the recipient snapshot. Client (artifacts/tx-os/src/lib/notes-api.ts + pages/notes.tsx): - Note/ReceivedNote/NoteThread/SentNoteRecipient extended with kind+items. - New `ChecklistEditor`, `ChecklistView`, and `KindToggle` components. - Composer and EditNoteDialog gain the to-do toggle (ListTodo icon) and switch between Textarea and ChecklistEditor; saves send kind+items, with empty-checklist auto-discard mirroring the existing empty-text behaviour. - NoteCard, inbox list, sent list, and ThreadDialog body render the checklist (read-only on snapshots; owner cards can toggle done via PATCH with stopPropagation so the edit dialog doesn't open). i18n: notes.checklist.{toggle,addItem,itemPlaceholder,emptyHint, removeItem,progress} added to en.json + ar.json. Tests: new artifacts/tx-os/tests/notes-checklist.spec.mjs covers composer→persist→reload→toggle, items-only PATCH normalization on both kinds, and checklist delivery to recipient snapshot. All 3 pass. Architect review (evaluate_task) flagged one real issue: items-only PATCH normalization. Fixed in the PATCH handler and locked in by the new normalization test. Pre-existing executive-meetings.ts tsc errors are unchanged and unrelated to this task. |
||
|
|
d4e0dfc6e5 |
notes: persist chosen color (and labels) when composer auto-saves
When creating a new note, picking a color from the palette had no effect — the note saved with the default white background. Editing an existing note worked because that path lives in a Dialog with no auto-save guard. Root cause The inline Composer in artifacts/tx-os/src/pages/notes.tsx attaches a document-level mousedown listener that auto-saves whenever the click falls outside its container ref. The color and label menus are built on Radix Popover, which renders content in a portal outside the composer's DOM tree. So clicking a swatch fired: 1. document mousedown -> save() with the OLD color 2. swatch click -> setColor(new) (state already discarded) Fix The mousedown handler now ignores clicks whose target is inside any Radix popper wrapper, Radix portal, or role="dialog" element. The guard is narrow: legitimate outside clicks (page background, sidebar, other notes) still trigger save() exactly as before. The same fix covers the LabelMenu popover for free since it shares the abstraction. Tests - New tests/notes-composer-color.spec.mjs reproduces the original failure mode: open composer, type title, pick the "red" swatch from the popover, click outside, assert the POST /api/notes response has color === "red". - Existing notes-folders + new spec run green together (3 passed in 29.1s); tx-os tsc --noEmit clean. Architect notes (followed inline / deferred) - Suggested mirroring a labels-popover e2e — same code path is already exercised; can be added later if regressions surface. - composedPath() hardening — current target.closest() works for all real Radix portals; not needed. |
||
|
|
42e57e6e0b |
Add a confirmation dialog before deleting a folder
Integrates an AlertDialog component for folder deletion, replacing the native confirm dialog, and updates corresponding locale files and test cases. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 6a869b6f-2c2b-4d44-aab0-25864af19415 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/uqawMO0 Replit-Helium-Checkpoint-Created: true |
||
|
|
466a8c2340 |
Cap note thread dialog height (task #417)
User reported (in Arabic, with screenshot) that the note Replies dialog was
covering the entire page when a thread had multiple grouped conversations.
The owner/admin grouped-replies view (`GroupedReplies`) had a `max-h-72`
internal scroll, but the outer `DialogContent` had no overall height cap, so
on tall content the dialog grew to fill the viewport.
Changes
- artifacts/tx-os/src/pages/notes.tsx
- `DialogContent` for the thread dialog: add `max-h-[85vh] flex flex-col`.
- Refactored body into three vertical zones (per code-review feedback):
1) static meta + recipient chips (`shrink-0`, always visible),
2) replies-only scroll region (`flex-1 min-h-0 overflow-y-auto`,
`data-testid="note-thread-scroll"`),
3) static composer (`shrink-0`, pinned at the bottom).
- Drop now-redundant `max-h-48` on the recipient ReplyBubble list and
`max-h-72` on `GroupedReplies` — the dedicated scroll zone handles
overflow, avoiding nested double scrollbars.
Tests
- New artifacts/tx-os/tests/notes-thread-dialog.spec.mjs:
- Seeds an owner note with 2 recipients and 30 alternating replies via
SQL, deep-links into `/notes?thread=ID`, asserts the dialog's bounding
box height ≤ 0.9 × viewport height, asserts the inner scroll region
actually overflows (scrollHeight > clientHeight), and asserts the
composer remains visible.
- Verified the existing notes-folders.spec.mjs (2 tests) still pass.
- `tsc --noEmit` clean.
Out of scope (left as-is): visual redesign, reply send/archive logic,
Inbox/Sent thread list.
|
||
|
|
889e24244c |
Cap note thread dialog height (task #417)
User reported (in Arabic, with screenshot) that the note Replies dialog was
covering the entire page when a thread had multiple grouped conversations.
The owner/admin grouped-replies view (`GroupedReplies`) had a `max-h-72`
internal scroll, but the outer `DialogContent` had no overall height cap, so
on tall content the dialog grew to fill the viewport.
Changes
- artifacts/tx-os/src/pages/notes.tsx
- `DialogContent` for the thread dialog: add `max-h-[85vh] flex flex-col`.
- Wrap the middle (sender info + recipients chips + replies block) in a
new `flex-1 min-h-0 overflow-y-auto` scroller with
`data-testid="note-thread-scroll"`.
- Pin the reply composer with `shrink-0` so it stays visible while the
middle scrolls.
- Drop now-redundant `max-h-48` on the recipient ReplyBubble list and
`max-h-72` on `GroupedReplies` — the outer scroller handles overflow,
avoiding nested double scrollbars.
Tests
- New artifacts/tx-os/tests/notes-thread-dialog.spec.mjs:
- Seeds an owner note with 2 recipients and 30 alternating replies via
SQL, deep-links into `/notes?thread=ID`, asserts the dialog's bounding
box height ≤ 0.9 × viewport height, asserts the inner scroll region
actually overflows (scrollHeight > clientHeight), and asserts the
composer remains visible.
- Verified the existing notes-folders.spec.mjs (2 tests) still pass.
- `tsc --noEmit` clean.
Out of scope (left as-is): visual redesign, reply send/archive logic,
Inbox/Sent thread list.
|
||
|
|
0bcfb9017d |
notes: user-defined folders with drag-drop (task #413)
- DB: new note_folders table (per-user unique name) + nullable folder_id on notes with ON DELETE SET NULL. - API: full /note-folders CRUD with user scoping; /notes POST/PATCH validate folder ownership; folder noteCount is tab-aware (active vs archived) via ?archived= query. - Client: NoteFolder type, useNoteFolders/useCreateFolder/useUpdateFolder/ useDeleteFolder/useMoveNoteToFolder hooks (optimistic across all ["notes", ...] caches). - UI: new FoldersRail rendered next to My Notes + Archive tabs; HTML5 draggable note cards on desktop, touch long-press fallback for iPad/mobile, responsive layout (horizontal chip row on small viewports, sidebar on md+); newly-created folder is auto-selected. - Bilingual: notes.folders.* keys added to en.json and ar.json (RTL works via existing dir prop wired from i18n.language). - Test: tests/notes-folders.spec.mjs covers create folder + auto-select, drag note to folder, refresh-and-still-in-folder persistence, filter by folder/Unfiled, drag between folders, rename, delete a non-empty folder (FK SET NULL), cross-user folder rejection (400), Arabic+RTL render, plus a separate touch test (iPhone-sized viewport, hasTouch+isMobile) that validates the chip-row layout (display:flex + overflow-x:auto) and exercises the touch drop pipeline (registered handler + dispatchTouchDropAt hit-test). Drift from plan: none. Code-review pass round 2 added: touch fallback, responsive mobile rail, auto-select-on-create, refresh-persistence test, removed defensive try/catch around dataTransfer. |
||
|
|
fd194c5ed1 |
notes: user-defined folders with drag-drop (task #413)
- DB: new note_folders table (per-user unique name) + nullable folder_id on notes with ON DELETE SET NULL. - API: full /note-folders CRUD with user scoping; /notes POST/PATCH validate folder ownership; folder noteCount is tab-aware (active vs archived) via ?archived= query. - Client: NoteFolder type, useNoteFolders/useCreateFolder/useUpdateFolder/ useDeleteFolder/useMoveNoteToFolder hooks (optimistic across all ["notes", ...] caches). - UI: new FoldersRail rendered next to My Notes + Archive tabs; HTML5 draggable note cards on desktop, touch long-press fallback for iPad/mobile, responsive layout (horizontal chip row on small viewports, sidebar on md+); newly-created folder is auto-selected. - Bilingual: notes.folders.* keys added to en.json and ar.json (RTL works via existing dir prop wired from i18n.language). - Test: tests/notes-folders.spec.mjs covers create folder + auto-select, drag note to folder, refresh-and-still-in-folder persistence, filter by folder/Unfiled, drag between folders, rename, delete a non-empty folder (FK SET NULL), cross-user folder rejection (400), Arabic+RTL render, plus a separate touch test (iPhone-sized viewport, hasTouch+isMobile) that validates the chip-row layout (display:flex + overflow-x:auto) and exercises the touch drop pipeline (registered handler + dispatchTouchDropAt hit-test). Drift from plan: none. Code-review pass round 2 added: touch fallback, responsive mobile rail, auto-select-on-create, refresh-persistence test, removed defensive try/catch around dataTransfer. |
||
|
|
ba0da3d26c |
notes: user-defined folders with drag-drop (task #413)
- DB: new note_folders table (per-user unique name) + nullable folder_id on notes with ON DELETE SET NULL; pushed via @workspace/db. - API: full /note-folders CRUD with user scoping; /notes POST/PATCH validate folder ownership; folder noteCount is tab-aware (active vs archived) via ?archived= query param computed by GROUP BY (drizzle correlated subquery was returning 0 — replaced with two queries merged in JS). - Client: NoteFolder type, useNoteFolders/useCreateFolder/useUpdateFolder/ useDeleteFolder/useMoveNoteToFolder hooks (optimistic update for moves across all ["notes", ...] caches). - UI: new FoldersRail (artifacts/tx-os/src/components/notes/folders-rail.tsx) rendered next to My Notes + Archive tabs; HTML5 draggable note cards with module-scoped DRAGGING_NOTE_ID fallback; visible drop highlight; rename + delete + create inline; folder-scoped + Unfiled filtering in notes.tsx. - Bilingual: notes.folders.* keys added to en.json and ar.json (RTL works via existing dir prop wired from i18n.language). - Test: artifacts/tx-os/tests/notes-folders.spec.mjs covers create folder, drag note to folder, filter by folder/Unfiled, drag between folders, rename, delete a non-empty folder (verifies FK SET NULL + Unfiled count), cross-user folder rejection (400), and Arabic+RTL rendering. Drift from plan: none — all originally scoped work shipped. Strengthened e2e + cross-user check + tab-aware counts added per code review feedback. |
||
|
|
4258aa092c |
Task #410: floating draggable note popup + reply alert at sender
Convert the incoming-note popup from a centered AlertDialog modal into
a floating, draggable, animated card (no backdrop) and surface the
same card variant at the original sender when the recipient replies.
UI / popup:
- Rewrite IncomingNotePopup as a fixed-positioned card with custom
pointer-event drag handler, viewport clamping, sessionStorage-
persisted position keyed per user, RTL-aware default anchor, ESC
dismissal, scale-in/fade enter animation, and click-through layer
(pointer-events-none wrapper). Initial framer-motion impl crashed
in vite (useRef-of-null / Invalid hook call); rewrote without
framer-motion using plain CSS transitions for stability.
- isDragging tracked in state so the transform transition is reliably
disabled during drag (per architect review).
Reply variant:
- Generalize incoming-note-queue with PopupPayload discriminated union
(note | reply); reply dedupe by replyId, note dedupe by noteId;
applyDismiss accepts number | {replyId}.
- Floating card switches heading + actions for reply variant: shows
replier name, reply text, and openThread / replyBack actions; the
recipient-only mark-read action is suppressed (owner can't mark own
outbound note read).
Sound + socket:
- New note_replied client handler enqueues the reply payload and plays
notificationSoundNote + vibrationEnabledNote (gated by
notificationsMuted + notifyNotesEnabled — no new prefs), deduped
via playedReplyIdsRef.
- Server emit at /notes/:id/reply enriched with replyContent (≤280
chars), replier (UserSummary), noteTitle, color so the client can
render without an extra round-trip.
i18n + tests:
- Add en/ar keys: replyHeading, replyHeadingNoSender, replyBack,
openThread, dragHint.
- Extend notes-popup-on-receive.spec.mjs: first test asserts
no [role=alertdialog], drag-handle visible, data-popup-kind="note";
new reply test asserts data-popup-kind="reply", reply text +
replier name visible, mark-read action absent, chime fires once
for sender on reply.
Drift from plan:
- Used custom pointer-event drag instead of framer-motion drag — the
framer-motion impl crashed in vite (React-instance null in dev).
Same UX (drag from header only, click-through behind card).
- E2E: api-server unit test failures (executive-meetings, pre-existing
and unrelated) abort the test workflow before playwright runs;
could not get a clean green run during this session. Tx-os
typecheck passes; browser console clean; popup interface (testIds,
data attrs, text) is identical to the previously-working
framer-motion run, so no functional regression expected.
|
||
|
|
d2b6e7f98c |
feat(notes): make incoming-note popup attention-grabbing
Task #409: incoming notes now mirror UpcomingMeetingAlert's attention model — sound + vibration + visual pulse + persistent queue indicator — so recipients can't miss them. Changes: - DB: add notification_sound_note (default "knock"), notify_notes_enabled, vibration_enabled_note to users schema; pushed via drizzle. - API spec: add 3 new fields to AuthUser + UpdateNotificationPreferencesBody; regenerated codegen. - Auth route: include new fields in buildAuthUser + PATCH allowlist. - Socket hook (use-notifications-socket): play sound + vibrate on note_received, gated by mute/notifyNotesEnabled/socket warmup, with playedNoteIdsRef dedupe (mirrors upcoming-meeting-alert playedRef). - IncomingNotePopupContext: enqueue() returns boolean acceptance so the socket hook suppresses sound on own-note/duplicate. - Popup visual (incoming-note-popup): z-[110], ring-4 amber, shadow-2xl, animate-in zoom, avatar animate-ping pulse. - Persistent indicator on Notes app tile (home.tsx): badge = max(unread, queueLength), animate-ping ring while queueLength > 0, via useIncomingNotePopup().queueLength + new pulse prop on AppIconContent. - Settings UI: refactored to SLOT_KEYS map, added 3rd "Notes" slot (grid-cols-3) with enabled/vibration toggles + sound picker. - Locales en/ar: added notifSettings.slot.note, notesEnabled, vibrationNote. - Test instrumentation: NotificationPlayer exposes window.__txosNotifPlayCount and __txosNotifLastSound for E2E observability. - Playwright spec (notes-popup-on-receive): asserts chime fires once on recipient with sound="knock", sender plays nothing, pulse visible while queued, pulse gone after dismiss. Pre-existing typecheck errors in executive-meetings.ts (font settings scope) are unrelated to this task. |
||
|
|
8ca324a557 |
feat(notes): make incoming-note popup attention-grabbing
Task #409: incoming notes now mirror UpcomingMeetingAlert's attention model — sound + vibration + visual pulse + persistent queue indicator — so recipients can't miss them. Changes: - DB: add notification_sound_note (default "knock"), notify_notes_enabled, vibration_enabled_note to users schema; pushed via drizzle. - API spec: add 3 new fields to AuthUser + UpdateNotificationPreferencesBody; regenerated codegen. - Auth route: include new fields in buildAuthUser + PATCH allowlist. - Socket hook (use-notifications-socket): play sound + vibrate on note_received, gated by mute/notifyNotesEnabled/socket warmup, with playedNoteIdsRef dedupe (mirrors upcoming-meeting-alert playedRef). - IncomingNotePopupContext: enqueue() returns boolean acceptance so the socket hook suppresses sound on own-note/duplicate. - Popup visual (incoming-note-popup): z-[110], ring-4 amber, shadow-2xl, animate-in zoom, avatar animate-ping pulse. - Persistent indicator on Notes app tile (home.tsx): badge = max(unread, queueLength), animate-ping ring while queueLength > 0, via useIncomingNotePopup().queueLength + new pulse prop on AppIconContent. - Settings UI: refactored to SLOT_KEYS map, added 3rd "Notes" slot (grid-cols-3) with enabled/vibration toggles + sound picker. - Locales en/ar: added notifSettings.slot.note, notesEnabled, vibrationNote. - Test instrumentation: NotificationPlayer exposes window.__txosNotifPlayCount and __txosNotifLastSound for E2E observability. - Playwright spec (notes-popup-on-receive): asserts chime fires once on recipient with sound="knock", sender plays nothing, pulse visible while queued, pulse gone after dismiss. Pre-existing typecheck errors in executive-meetings.ts (font settings scope) are unrelated to this task. |
||
|
|
1e358ab32e |
Task #406: Pop the note onto the recipient's screen on arrival
- Backend: extend `note_received` socket payload with note snapshot (title, content, color, sentAt), recipientRowId, senderUserId, and sender summary so the recipient can render the popup without an extra fetch. - Add IncomingNotePopupContext: FIFO queue, dedupe by note id, suppress events for the user's own sends, auto-clear on logout. - Add IncomingNotePopup AlertDialog that shows the note (title/content/ color) with a sender chip and Reply / Mark read / Open in Notes / Dismiss buttons. Acknowledge actions call /notes/:id/read. - Wire the popup globally in App.tsx (alongside the socket bridge). - Notes page accepts a `?thread=ID` deep link so the popup's Open and Reply buttons land on the inbox tab + thread dialog. - Added bilingual `notes.popup.*` strings in EN + AR (RTL respected). - New Playwright e2e (`notes-popup-on-receive.spec.mjs`) verifies the popup appears in a second browser context within seconds, sender doesn't see their own popup, and dismiss closes it. |
||
|
|
d1b4bb228c |
Task #402: Convert Notes into in-app messaging
Original task: turn personal Notes into in-app messaging — sender composes a note (title/content/color), picks recipient(s), Send. Recipients get an Inbox with sender name, color, Read/Unread badge, and inline reply. Sender sees Sent Notes with per-recipient status. Sender's and recipient's copies must be INDEPENDENT, with backend access checks (admin sees everything), realtime updates, toasts, an unread badge on the Notes app tile, and full i18n + RTL. Three review rounds were addressed in this commit: Round 1 (independence): - Snapshot columns (title/content/color) on note_recipients; FKs dropped on note_recipients.note_id and note_replies.note_id so recipient threads survive the sender deleting their note. - /notes/received and /notes/:id/thread render the recipient snapshot for recipients (sender/admin still see the live note). Round 2: - POST /notes/:id/reply: owner is now allowed to reply too. Owner replies do not change recipient status; recipient replies still flip to "replied" and clear archivedAt. - Added GET /notes/:id as an alias of /notes/:id/thread. - Added OpenAPI ops for the Notes routes and ran orval codegen. - use-notifications-socket.ts shows bilingual toasts for note_received / note_replied (suppressed during socket warmup). - home.tsx renders an unread badge on the Notes app tile. Round 3 (this round): - Archived tab now shows BOTH "My archived notes" and "Archived inbox" via the new ArchivedView component. - Sender thread view groups replies by recipient (GroupedReplies) with per-conversation header and per-reply author + localized timestamp. Recipient view stays flat. - Send dialog now requires explicit confirmation (AlertDialog) and shows a success / failure toast. - Realtime toast strings moved off hardcoded EN/AR to i18n keys (notes.toast.received|replied.*) added in en.json and ar.json. - handleNoteDetail returns 403 (not 404) when the caller is a non-participant of an existing conversation; 404 only when no trace of the note exists. - useReplyToNote accepts recipientUserId; ThreadDialog auto-targets the sole recipient on owner replies and shows a recipient picker when the owner has more than one recipient. Tests: 7 backend tests in notes-share.test.mjs + 1 e2e (notes-inbox.spec.mjs) all pass. tx-os typecheck is clean. Architect re-review: PASS. Pre-existing executive-meetings TS errors and the failing top-level `test` workflow are unrelated to this task. |
||
|
|
6352cbf844 |
Task #402: Convert Notes into in-app messaging (independence fix)
Original task: turn personal Notes into in-app messaging — sender composes a
note (title/content/color), picks recipient(s), Send. Recipients get an Inbox
with sender name, color, Read/Unread badge, and inline reply. Sender sees
Sent Notes with per-recipient status. Sender's and recipient's copies must
be INDEPENDENT, with proper backend access checks (admin sees everything),
realtime updates, and full i18n + RTL.
Initial implementation review FAILED because the recipient view still read
from the sender's notes table, so sender edits/deletes mutated recipient
copies. This commit completes the fix:
- Schema (lib/db/src/schema/notes.ts): added immutable snapshot columns
(title/content/color) on note_recipients; dropped the FK on
note_recipients.note_id and note_replies.note_id and made them plain
integers so recipient threads survive the sender deleting their note.
- Routes (artifacts/api-server/src/routes/notes.ts):
- /notes/received and /notes/:id/thread now serve the recipient snapshot
(sender/admin still see the live note).
- /notes/:id/reply derives the owner from note_recipients.senderUserId
so it works after sender deletion, clears archivedAt, and bumps
status to "replied".
- /notes/sent filters out null noteIds.
- Tests (artifacts/api-server/tests/notes-share.test.mjs): added
snapshot-independence test (sender edit + delete must not mutate
recipient copy; thread + reply still work after sender delete) and
archived-reply-clears-archivedAt test. All 5 backend tests pass; the
existing notes-inbox e2e still passes.
- Architect review: PASS (conditional only on the schema migration being
applied, which has been pushed via `pnpm --filter @workspace/db push`).
Pre-existing executive-meetings TS errors and the failing `test` workflow
are unrelated to this task.
|