e2564e2a66cfa71ac1fb492b85abcd56f68cff9b
367 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
95522dacbe |
Task #501: Default new notes to yellow
The notes composer now starts with the yellow swatch pre-selected
instead of the neutral "default" color, so a freshly created note
becomes a yellow sticky by default. The user can still pick any
other color from the swatch picker before saving, and that choice
is honored. After a successful save (or send), the composer resets
back to yellow rather than "default", so the next note in the same
session is also yellow unless the user changes it.
Changes:
- artifacts/tx-os/src/pages/notes.tsx
- Composer initial color state: useState("default") -> useState("yellow")
- Composer reset() helper: setColor("default") -> setColor("yellow")
Out of scope (per task spec):
- No backend changes. The API's data.color = "default" fallback for
notes created via other paths (shared-folder stamping, incoming-
note flows) is intentionally untouched.
- No changes to existing notes already saved in the database.
- No new "default color" user setting.
- NOTE_COLORS palette and color-picker UI unchanged.
Verification:
- `pnpm exec tsc --noEmit` passes clean.
- "yellow" is a valid id in NOTE_COLORS (src/lib/notes-api.ts:801),
so colorBg/colorAccent/colorMeta resolve correctly and the swatch
shows pre-selected.
- Color-picker onChange path unchanged, so user-picked colors still
override the new yellow default before save.
|
||
|
|
d119480684 |
Task #499: fix Quick Actions dialog X overlap + iPad long-press drag feedback
Two scoped UX fixes on Executive Meetings schedule, both confined to
artifacts/tx-os/src/pages/executive-meetings.tsx (shared
src/components/ui/dialog.tsx untouched per task spec).
1) Quick Actions dialog header overlap
- Radix DialogContent renders the Close (X) at physical
`right-4 top-4` regardless of `dir`. In AR (RTL) the
`text-right`-aligned title "إجراءات سريعة" pinned to the same
edge and visibly overlapped the X.
- Fix: added `pr-8` to the per-row Quick Actions DialogHeader so
the title's right edge always clears the close button. Works
for AR (overlap eliminated) and EN (just adds trailing slack).
2) iPad long-press drag feedback
- dnd-kit's TouchSensor has a 200ms activation delay; previously
the row gave no cue during the hold window so users lifted
early thinking the schedule didn't register.
- Fix: added `isPressing` state toggled via capture-phase
onPointerDown/Up/Cancel handlers on the row tr, gated on
`pointerType === "touch"` && `effectiveDragEnabled`. Capture
phase chosen so handlers don't collide with safeRowDragListeners
(which spreads dnd-kit's bubble-phase onPointerDown).
- Visual cue: subtle `inset 0 0 0 2px hexToRgba(highlightColor,
0.45)` ring composed AFTER quickOpen/selection/current rings so
it never outranks a real selection state. Per spec, the cue
appears during the 200ms hold AND continues throughout the
active drag (we deliberately do NOT clear isPressing on the
rising edge of isDragging). Two defensive useEffects clear
`isPressing`: one when drag becomes disabled (capability flip /
edit-mode toggle), one tied to the FALLING edge of isDragging
(drag just ended) as a belt-and-suspenders guard against stale
visual state when dnd-kit absorbs the touch sequence and our
row's own pointerup never fires.
- data-pressing="true" attribute exposed on tr for tests.
- TouchSensor delay (200ms) and tolerance (8px) unchanged.
Tests added:
- executive-meetings-quick-actions-dialog-layout.spec.mjs: asserts
close button + title bounding boxes don't overlap in AR + EN.
- executive-meetings-row-touch-press-feedback.spec.mjs: uses
hasTouch+isMobile + CDP Input.dispatchTouchEvent, asserts
data-pressing flips true on touchstart, clears on touchend, and
tap-to-open quick-actions dialog still works for short taps;
second test verifies the cue persists past the 200ms window.
Verification: pnpm tsc --noEmit clean. Pre-existing `test` workflow
failures (rotate-content/font-settings/notes-share) are unrelated to
this change.
|
||
|
|
9b0dbb037c |
Task #499: fix Quick Actions dialog X overlap + iPad long-press drag feedback
Two scoped UX fixes on Executive Meetings schedule, both confined to
artifacts/tx-os/src/pages/executive-meetings.tsx (shared
src/components/ui/dialog.tsx untouched per task spec).
1) Quick Actions dialog header overlap
- Radix DialogContent renders the Close (X) at physical
`right-4 top-4` regardless of `dir`. In AR (RTL) the
`text-right`-aligned title "إجراءات سريعة" pinned to the same
edge and visibly overlapped the X.
- Fix: added `pr-8` to the per-row Quick Actions DialogHeader so
the title's right edge always clears the close button. Works
for AR (overlap eliminated) and EN (just adds trailing slack).
2) iPad long-press drag feedback
- dnd-kit's TouchSensor has a 200ms activation delay; previously
the row gave no cue during the hold window so users lifted
early thinking the schedule didn't register.
- Fix: added `isPressing` state toggled via capture-phase
onPointerDown/Up/Cancel handlers on the row tr, gated on
`pointerType === "touch"` && `effectiveDragEnabled`. Capture
phase chosen so handlers don't collide with safeRowDragListeners
(which spreads dnd-kit's bubble-phase onPointerDown).
- Visual cue: subtle `inset 0 0 0 2px hexToRgba(highlightColor,
0.45)` ring composed AFTER quickOpen/selection/current rings so
it never outranks a real selection state. Two defensive
useEffects clear `isPressing`: one when drag becomes disabled
(capability flip / edit-mode toggle), one tied to the
`isDragging` lifecycle so dnd-kit-managed release paths (drop
outside the row, sensor-managed cancel) can't leave a stale
ring behind.
- data-pressing="true" attribute exposed on tr for tests.
- TouchSensor delay (200ms) and tolerance (8px) unchanged.
Tests added:
- executive-meetings-quick-actions-dialog-layout.spec.mjs: asserts
close button + title bounding boxes don't overlap in AR + EN.
- executive-meetings-row-touch-press-feedback.spec.mjs: uses
hasTouch+isMobile + CDP Input.dispatchTouchEvent, asserts
data-pressing flips true on touchstart, clears on touchend, and
tap-to-open quick-actions dialog still works for short taps;
second test verifies the cue persists past the 200ms window.
Verification: pnpm tsc --noEmit clean. Pre-existing `test` workflow
failures (rotate-content/font-settings/notes-share) are unrelated to
this change.
|
||
|
|
27c5cc381c |
Task #499: fix Quick Actions dialog X overlap + iPad long-press drag feedback
Two scoped UX fixes on Executive Meetings schedule, both confined to
artifacts/tx-os/src/pages/executive-meetings.tsx (shared
src/components/ui/dialog.tsx untouched per task spec).
1) Quick Actions dialog header overlap
- Radix DialogContent renders the Close (X) at physical
`right-4 top-4` regardless of `dir`. In AR (RTL) the
`text-right`-aligned title "إجراءات سريعة" pinned to the same
edge and visibly overlapped the X.
- Fix: added `pr-8` to the per-row Quick Actions DialogHeader so
the title's right edge always clears the close button. Works
for AR (overlap eliminated) and EN (just adds trailing slack).
2) iPad long-press drag feedback
- dnd-kit's TouchSensor has a 200ms activation delay; previously
the row gave no cue during the hold window so users lifted
early thinking the schedule didn't register.
- Fix: added `isPressing` state toggled via capture-phase
onPointerDown/Up/Cancel handlers on the row tr, gated on
`pointerType === "touch"` && `effectiveDragEnabled`. Capture
phase chosen so handlers don't collide with safeRowDragListeners
(which spreads dnd-kit's bubble-phase onPointerDown).
- Visual cue: subtle `inset 0 0 0 2px hexToRgba(highlightColor,
0.45)` ring composed AFTER quickOpen/selection/current rings so
it never outranks a real selection state. Defensive useEffect
clears `isPressing` if drag becomes disabled mid-press.
- data-pressing="true" attribute exposed on tr for tests.
- TouchSensor delay (200ms) and tolerance (8px) unchanged.
Tests added:
- executive-meetings-quick-actions-dialog-layout.spec.mjs: asserts
close button + title bounding boxes don't overlap in AR + EN.
- executive-meetings-row-touch-press-feedback.spec.mjs: uses
hasTouch+isMobile + CDP Input.dispatchTouchEvent, asserts
data-pressing flips true on touchstart, clears on touchend, and
tap-to-open quick-actions dialog still works for short taps;
second test verifies the cue persists past the 200ms window.
Verification: pnpm tsc --noEmit clean. Pre-existing `test` workflow
failures (rotate-content/font-settings/notes-share) are unrelated to
this change.
|
||
|
|
b5b55f89fb |
Task #497: Allow row-drag rotation without time windows
Lifted the per-row missing-time drag block (originally #492). Meetings without a (startTime, endTime) tuple are a normal state and now rotate freely alongside timed rows. Server (artifacts/api-server/src/routes/executive-meetings.ts): - Dropped the `no_time_window` early-return guard in /api/executive-meetings/rotate-content. - Sort + slot construction now tolerate null start times (NULLS LAST, tie-break by id) so a null tuple rotates as a real slot. - All other safeguards (cancelled_in_rotate, optimistic lock, renumberDayByStartTime, audit logging) remain intact. Client (artifacts/tx-os/src/pages/executive-meetings.tsx): - Removed missingTimeCount / dayRotatable memos, the dayRotatable prop wiring, dragBlockedByMissingTime + effectiveDragEnabled composition, the row's data-drag-blocked / native title / cursor-not-allowed-opacity-80 / aria-disabled overrides, the entire DragBlockedTooltipButton component + its render site, and the no_time_window errorToast branch in rotateContent. Pruned now-dead Tooltip + AlertTriangle imports. i18n: removed the `executiveMeetings.rotate.needsTimeWindow` block (tooltip + errorToast) from en.json and ar.json. Tests: deleted `executive-meetings-rotate-needs-time-window.spec.mjs` and added `executive-meetings-rotate-allows-missing-times.spec.mjs`, which inserts 3 meetings (one with null times), verifies no drag-blocked warning button / aria-disabled, drags the top row to the bottom slot, asserts the rotate-content POST succeeds, and confirms exactly one row still has a null tuple after rotation. Verified: row-drag, row-quick-actions, and the new spec all pass (8/8). Architect code review PASSED. |
||
|
|
59d838b8f2 |
Task #497: Allow row-drag rotation without time windows
Lifted the per-row missing-time drag block (originally #492). Meetings without a (startTime, endTime) tuple are a normal state and now rotate freely alongside timed rows. Server (artifacts/api-server/src/routes/executive-meetings.ts): - Dropped the `no_time_window` early-return guard in /api/executive-meetings/rotate-content. - Sort + slot construction now tolerate null start times (NULLS LAST, tie-break by id) so a null tuple rotates as a real slot. - All other safeguards (cancelled_in_rotate, optimistic lock, renumberDayByStartTime, audit logging) remain intact. Client (artifacts/tx-os/src/pages/executive-meetings.tsx): - Removed missingTimeCount / dayRotatable memos, the dayRotatable prop wiring, dragBlockedByMissingTime + effectiveDragEnabled composition, the row's data-drag-blocked / native title / cursor-not-allowed-opacity-80 / aria-disabled overrides, the entire DragBlockedTooltipButton component + its render site, and the no_time_window errorToast branch in rotateContent. Pruned now-dead Tooltip + AlertTriangle imports. i18n: removed the `executiveMeetings.rotate.needsTimeWindow` block (tooltip + errorToast) from en.json and ar.json. Tests: deleted `executive-meetings-rotate-needs-time-window.spec.mjs` and added `executive-meetings-rotate-allows-missing-times.spec.mjs`, which inserts 3 meetings (one with null times), verifies no drag-blocked warning button / aria-disabled, drags the top row to the bottom slot, asserts the rotate-content POST succeeds, and confirms exactly one row still has a null tuple after rotation. Verified: row-drag, row-quick-actions, and the new spec all pass (8/8). Architect code review PASSED. |
||
|
|
ecc6f3efc4 |
Prevent quick actions popover from opening when in edit mode
Update conditional logic for opening the quick actions popover to account for edit mode, ensuring it only opens when mutation is allowed and edit mode is disabled. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 70a0c645-f927-4c80-a04a-32ae50f404f9 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 |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
5a0c8a9ed1 |
Task #489 fix: decouple row drag from edit mode + fix optimistic order
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.
Second rejection: optimistic UI patched per-meeting (start/end/
dailyNumber) but did not reorder the rendered rows, so post-drop
slots could display rotated times against the wrong content
(e.g. 11:00, 09:00, 10:00) until refetch.
Changes:
- MeetingRow: new `dragEnabled` prop separate from `canMutate`.
Drives 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.
- rotateContent: also call `setOptimisticOrder(newOrder)` so slot[i]
visually shows newOrder[i] in the same React commit; rollback
restores the prior optimistic order on rotate failure.
- 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.
|
||
|
|
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). |
||
|
|
d4cc77c8cc |
#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 — 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). |
||
|
|
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). |
||
|
|
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 |
||
|
|
76f2f7ce51 |
notes(ipad): keep dialog and inputs above the on-screen keyboard
Task #476. On iPad Safari, opening the keyboard while writing a new note or replying inside the note thread dialog covered the bottom of the dialog (textarea + send button). The dialog used `top:50%; translate(-50%,-50%)` against the layout viewport and never reacted to the visual viewport shrinking when the keyboard appeared. Changes: - artifacts/tx-os/index.html: added `interactive-widget=resizes-content` to the viewport meta so iOS 17+ resizes the layout viewport when the software keyboard opens. - artifacts/tx-os/src/hooks/use-visual-viewport.ts: new hook that subscribes to `window.visualViewport` `resize` / `scroll` / `orientationchange` (rAF-throttled) and exposes the visible height, the visual viewport `offsetTop`, and the bottom keyboard inset. - artifacts/tx-os/src/components/ui/dialog.tsx: `DialogContent` now reads the hook and, only when the keyboard inset exceeds ~80px, pins its center to `offsetTop + height/2` and caps `max-height` to the visible height. With the existing `translate(-50%,-50%)`, both edges stay inside the visible region above the keyboard. Desktop unaffected (inset is 0 → no inline style applied). - artifacts/tx-os/src/pages/notes.tsx: thread reply textarea and the top composer textarea now call `scrollIntoView({ block: "center" })` on focus and again whenever the keyboard inset changes while the field is focused. Both paths are gated on `keyboardInset > 80`, so desktop focus is untouched. Verification: - `tsc --noEmit` clean. - e2e: `notes-thread-dialog` and `notes-composer-color` both pass. |
||
|
|
db6e7726eb |
notes(ipad): keep dialog above the on-screen keyboard
Task #476. On iPad Safari, opening the keyboard while writing a new note or replying inside the note thread dialog covered the bottom of the dialog (textarea + send button). The dialog used `top:50%; translate(-50%,-50%)` against the layout viewport and never reacted to the visual viewport shrinking when the keyboard appeared. Changes: - artifacts/tx-os/index.html: added `interactive-widget=resizes-content` to the viewport meta so iOS 17+ resizes the layout viewport when the software keyboard opens. - artifacts/tx-os/src/hooks/use-visual-viewport.ts: new hook that subscribes to `window.visualViewport` `resize` / `scroll` / `orientationchange` (rAF-throttled) and exposes the visible height, the visual viewport `offsetTop`, and the bottom keyboard inset. - artifacts/tx-os/src/components/ui/dialog.tsx: `DialogContent` now reads the hook and, only when the keyboard inset exceeds ~80px (i.e. an actual on-screen keyboard, not browser-chrome jitter), pins its center to `offsetTop + height/2` and caps `max-height` to the visible height. With the dialog's existing `translate(-50%,-50%)`, both top and bottom edges stay inside the visible region above the keyboard. Desktop renders unchanged (inset is 0 → no inline style applied beyond what the caller passes). Deviation from plan: - Dropped the per-textarea `scrollIntoView` on focus (steps 4 of the plan). Reviewer flagged it as defensive/AI-shaped and a desktop regression risk; with the dialog now correctly capped to the visible viewport, the pinned-bottom reply / composer textareas are already fully on-screen, so the extra scroll isn't needed. Verification: - `tsc --noEmit` clean. - `notes-thread-dialog` e2e still passes. |
||
|
|
5dac662421 |
notes(ipad): keep dialog above the on-screen keyboard
Task #476. On iPad Safari, opening the keyboard while writing a new note or replying inside the note thread dialog covered the bottom of the dialog (textarea + send button). The dialog used `top:50%; translate(-50%,-50%)` against the layout viewport and never reacted to the visual viewport shrinking when the keyboard appeared. Changes: - artifacts/tx-os/index.html: added `interactive-widget=resizes-content` to the viewport meta so iOS 17+ resizes the layout viewport when the software keyboard opens. - artifacts/tx-os/src/hooks/use-visual-viewport.ts: new hook that subscribes to `window.visualViewport` `resize` / `scroll` / `orientationchange` events (rAF-throttled) and exposes the current visible height plus the bottom keyboard inset. - artifacts/tx-os/src/components/ui/dialog.tsx: `DialogContent` now reads the hook and, when the keyboard inset is non-trivial (> 80px), pins its center to the visible viewport mid-line and caps `max-height` to the visible height. Desktop renders unchanged (inset is 0 → no inline style applied beyond what the caller passes). - artifacts/tx-os/src/pages/notes.tsx: the thread dialog reply textarea and the top composer textarea now `scrollIntoView({ block: "center" })` on focus (after a 250ms delay so the keyboard animation settles), so the cursor lands inside the visible region on touch devices. Desktop focus is unaffected — `scrollIntoView` is a no-op when the element is already in the viewport. Verification: - `tsc --noEmit` clean. - Existing `notes-thread-dialog` e2e still passes (dialog stays capped at 85% viewport height, scroller still overflows, recipient chips and composer remain in viewport). |
||
|
|
36f6d1f5e6 |
notes(shared): show Send button on notes inside shared folders
User report (AR, RH on iPad): no Send button visible inside a shared "desk" folder. The shared-folder view was passing `hideSend` to every NoteCard for editors and rendered no card actions at all for read-only viewers, so nobody could send a note from a shared folder. Changes (artifacts/tx-os/src/pages/notes.tsx, SharedFolderView): - Editors: removed `hideSend` from the NoteCard so the existing Send affordance (and color/labels/archive/delete) appears as it does on the user's own notes. - Read-only viewers: added a small Send icon button to the static card (testid `shared-folder-note-send-<id>`) so they can also send the note via the existing send dialog. The send endpoint authorizes off the authenticated caller, so the note is sent under RH's identity (no server change needed). |
||
|
|
d0e907e409 |
notes(folders): allow drag-drop of notes onto shared folders (edit perm)
User report (AR): "shared folders also appear here — the user should
be able to move notes wherever they want." The folders rail already
lists shared-with-me folders, but only OWN folder rows registered as
@dnd-kit drop targets. Recipients with edit permission could see a
shared folder but couldn't drop a note onto it.
Changes:
- folders-rail.tsx: extend the inline `DroppableRow` to accept an
optional `ownerId`. Wrap each shared-folder row in a DroppableRow
when `sf.myPermission === "edit"`, passing the owning user's id;
read-only viewers stay non-droppable. The droppable id is namespaced
with `-shared-<ownerId>` so it never collides with the owner-side
row for the same folder id.
- notes.tsx: extend `sharedFolderBucketRef` to carry the bucket's
`ownerId` (sourced from `data.folder.ownerId`). In `handleDragEnd`,
read `o.ownerId` from the drop data:
• undefined → own folder / Unfiled. Allow only own notes.
• number → shared folder. Allow only when the source note
came from sharedBucket and `sharedBucket.ownerId
=== o.ownerId`.
This mirrors the server-side rule that an editor may move notes
only between folders owned by the same owner — cross-owner drops
are blocked client-side so we never round-trip a 400.
Out of scope:
- Cross-owner moves (would require a copy/share flow, not supported
by the server today).
- Drop targets on shared folders inside the FoldersBrowser tile grid
(the rail covers the user's reported screenshot; tiles can follow).
|
||
|
|
2629588f32 |
notes(folders): allow drag-drop of notes onto shared folders (edit perm)
User report (AR): "shared folders also appear here — the user should
be able to move notes wherever they want." The folders rail already
lists shared-with-me folders, but only OWN folder rows registered as
@dnd-kit drop targets. Recipients with edit permission could see a
shared folder but couldn't drop a note onto it.
Changes:
- folders-rail.tsx: extend the inline `DroppableRow` to accept an
optional `ownerId`. Wrap each shared-folder row in a DroppableRow
when `sf.myPermission === "edit"`, passing the owning user's id;
read-only viewers stay non-droppable. The droppable id is namespaced
with `-shared-<ownerId>` so it never collides with the owner-side
row for the same folder id.
- notes.tsx: extend `sharedFolderBucketRef` to carry the bucket's
`ownerId` (sourced from `data.folder.ownerId`). In `handleDragEnd`,
read `o.ownerId` from the drop data:
• undefined → own folder / Unfiled. Allow only own notes.
• number → shared folder. Allow only when the source note
came from sharedBucket and `sharedBucket.ownerId
=== o.ownerId`.
This mirrors the server-side rule that an editor may move notes
only between folders owned by the same owner — cross-owner drops
are blocked client-side so we never round-trip a 400.
Out of scope:
- Cross-owner moves (would require a copy/share flow, not supported
by the server today).
- Drop targets on shared folders inside the FoldersBrowser tile grid
(the rail covers the user's reported screenshot; tiles can follow).
|
||
|
|
d7a386cf75 |
notes: bulk-select + bulk-delete on Sent tab (task #472)
- Add `useBulkDeleteSentNotes` hook that fans out parallel DELETE
/notes/:id calls via Promise.allSettled and returns {ok, failed}
so the UI can render a precise toast on partial failure. Caches
invalidated once via `invalidateNotesAndFolders`.
- NotesPage: page-level `sentSelectionMode` + `selectedSentIds`
Set, auto-reset when navigating away from the Sent tab.
- Header gets a Select / Cancel toggle, gated on
`view === "sent" && filteredSent.length > 0`.
- New sticky bulk-action bar above the sent list with count,
select-all/clear toggle, and Delete button.
- SentList accepts optional selectionMode/selectedIds/onToggle
props; cards swap onClick (open → toggle) and show a Checkbox
overlay + rose ring when checked.
- AlertDialog confirms before deleting; toast reports all-success,
partial, or all-failed using pluralized i18n keys.
- New i18n keys under `notes.bulk*` in ar.json + en.json.
No server changes (existing DELETE /notes/:id is reused).
Out of scope (per task spec): undo, bulk-archive, bulk endpoint,
multi-select on Active/Received/Archived tabs.
|
||
|
|
05eeb177ff |
Add a plus icon to indicate creating a new note
Update the notes composer component to include a Plus icon next to the "Take a note..." placeholder, visually indicating the action to create a new note. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 01f3a5db-a2fa-45cb-9733-81a92ebf3216 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/mkNwf1z Replit-Helium-Checkpoint-Created: true |
||
|
|
b0647cb9b8 |
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 appears only inside the dropdown next to the Inbox item (the existing `data-testid="notes-inbox-unread-badge"` is preserved). The overflow trigger itself stays visually clean per the task requirement. - 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. |
||
|
|
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. |
||
|
|
f937362868 |
Task #463: @dnd-kit notes drag + reorder
- Replace HTML5+touch drag with @dnd-kit; MouseSensor (desktop, 8px) + TouchSensor (iPad, 200ms long-press) so input sources never overlap. - Add sort_order column; ORDER BY asc(sortOrder), updatedAt desc. - PATCH /notes/reorder: strict isPinned boolean check, bucket+permission scoped, all writes wrapped in db.transaction for atomicity. - PATCH /notes/:id stamps sort_order = min-1 on folder/pin bucket change. - Client useReorderNotes (PATCH) with optimistic cache update. - handleDragEnd builds reorder payload from FULL bucket (owner notes or shared-folder bucket via ref), not the filtered subset, so hidden siblings under search/label filters keep their order. - Drag guarded for view-only contexts: source must be owned OR in editable shared bucket; folder-drop additionally requires isOwn. - 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 (33s). - 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. |
||
|
|
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. |
||
|
|
920d224a68 |
Task #459: Hide iPad Safari URL bar across all apps (notes + home)
Problem: On iPad Safari, the executive-meetings app collapsed the URL bar on scroll (native-app feel), but Notes and Home kept it visible permanently because their root containers were not document-scrollable. - notes.tsx used `h-screen ... overflow-hidden` with an internal `flex-1 min-h-0 overflow-y-auto` scroll region. - home.tsx used `min-h-screen ... overflow-hidden`. With no document-level scroll, iOS Safari has no trigger to hide the URL bar. Fix (quick-fix path the user picked, no PWA install meta): - notes.tsx: root → `min-h-[100dvh] os-bg flex flex-col` (removed `overflow-hidden`, swapped `h-screen` → `min-h-[100dvh]`). Header now `sticky top-0 z-10` so it stays pinned. Inner body wrapper changed from `flex-1 min-h-0 overflow-y-auto flex flex-col` to `flex-1 flex flex-col` so the document (not an inner div) scrolls — giving Safari the trigger it needs. - home.tsx: root → `min-h-[100dvh] os-bg flex flex-col relative` (removed `overflow-hidden`). Why `100dvh`: dynamic viewport height sizes against the *visible* area, so the layout reflows cleanly when the URL bar appears/hides. Out of scope (matches task): no PWA meta tags, no other pages touched (spot-checked others: chat/services/orders/notifications/admin already use min-h-screen without overflow-hidden), no styling changes, executive- meetings page untouched (it's the reference). Verification: tsc --noEmit clean. Pre-existing test workflow failure on unrelated executive-meetings.ts type errors is out of scope. |
||
|
|
dba168b299 |
notes(#457): enlarge notes page header bar
- Outer header padding: px-4 py-3 → px-6 py-5 for a noticeably
taller bar.
- Title row gap-3 → gap-4; back button p-2 → p-2.5 with size 24 icon
(was 20).
- Title "Notes" text-lg → text-2xl; StickyNote icon size 20 → 28;
inner gap-2 → gap-3.
- Search:
- container max-w-md → max-w-xl, min-w-[180px] → min-w-[220px]
- Input gets h-12 text-base + ps-11
- Search icon size 16 → 20, offset bumped from 10 → 14 to match
the new padding (RTL + LTR).
- Labels button: removed size="sm", now h-12 px-4 text-base with
Tag icon size 20 (was 16) + me-2 (was me-1) for the larger label.
- Tab row spacing: mt-3 gap-2 → mt-4 gap-3; tab container padding
p-1 → p-1.5.
- Tab icons size 14 → 18 with me-2 (was me-1).
- Inbox unread badge: text-[10px] / 18×18 → text-xs / 22×22 with
px-1.5 so two-digit counts still fit.
- TabButton: px-3 py-1 text-sm → px-4 py-2 text-base.
- Header still uses flex-wrap so it folds gracefully on narrow
screens; works in both RTL and LTR (search icon position uses
the existing isRtl conditional).
- Out of scope (per task): no color/glass-panel changes, no behavior
changes, no enlargement of the body content under the header,
and no changes to the SharedFolderView header.
- Pre-existing executive-meetings.ts type errors unchanged.
|
||
|
|
1aad708cb7 |
notes(#454): per-recipient view/edit folder sharing
- Schema: noteFolderShares.permission ('view'|'edit', default 'view').
- Server: resolveFolderEditAccess + emitFolderChanged helpers.
- POST/PATCH/DELETE/checklist now allow folder editors.
- Editors stamp notes with folder owner's userId; labels validated
against owner; PATCH editors blocked from unfile (folderId=null)
and cross-folder moves to non-matching owners.
- emitFolderChanged fires for owner AND editor mutations across
create / patch (old + new folder) / delete / checklist toggle.
- PUT /shares accepts both legacy recipientUserIds and new
recipients:[{userId,permission}]; diffs add/update/remove/perm-flip.
- GET /shares, /shared-with-me, /shared-notes return permission /
myPermission / readOnly.
- Client:
- notes-api types: FolderSharePermission, myPermission on
SharedFolder/SharedFolderView, permission on FolderShareRecipient,
readOnly:boolean on SharedFolderNote.
- useUpdateFolderShares takes recipients[].
- useCreate/Update/DeleteNote also invalidate ['note-folders'] so
actor's shared-folder rail badge stays fresh.
- FolderShareDialog: per-row checkbox + segmented View/Edit toggle.
- SharedFolderView: editor mode mounts Composer + NoteCard with
full edit/delete/archive affordances; Send hidden in editor
context (Composer + NoteCard hideSend prop) since /send is
owner-only.
- folders-rail: per-folder permission badge.
- socket: note-folder-shared also invalidates ['notes'] +
['note-folders'] for fanout to owner / other editors.
- Locales: shareDescriptionPerm, permissionView/Edit, canEdit
(en + ar).
- Pre-existing executive-meetings.ts type errors are out of scope.
|
||
|
|
5753aa176d |
Notes: lift top bar out of the scroll area so it never scrolls off
Task #452. After #451, the page wrapper was h-screen + overflow-y-auto with the header still using `sticky top-0 z-20`. Users kept reporting that the bar slides up out of view on scroll — the well-known `position: sticky` failure mode when the sticky child is a direct flex item of the same element that is also the scroll container. Restructured artifacts/tx-os/src/pages/notes.tsx so the bar is a sibling of the scroller, not inside it: - Page wrapper: `h-screen os-bg flex flex-col overflow-hidden` (was `... overflow-y-auto`). The wrapper itself never scrolls. - Header div: dropped `sticky top-0 z-20`, added `shrink-0`. As a non-shrinking flex child of the column wrapper it stays pinned at the top by layout, with no sticky math involved. - Wrapped the composer block and the main content row (`flex-1 px-4 py-6 max-w-6xl ... flex gap-6` plus its inner `flex-1 min-w-0` column) inside a single new scrolling region: `<div className="flex-1 min-h-0 overflow-y-auto flex flex-col">`. `min-h-0` is required so the flex child can shrink below its content height and let `overflow-y-auto` actually engage. - Dialogs (Edit, Labels, Send, FolderShare, Thread) remain siblings of the scroller, so popovers/modals are unaffected. No changes to index.css, header styling, composer styling, folder layout, or any other page. RTL/LTR behavior unchanged. Out of scope: pre-existing test failures (executive-meetings, notes-share, service-orders) are unrelated to this layout fix. |
||
|
|
5f7bb77985 |
Task #451: pin Notes header via an internal scroll container
Despite Tasks #448–#450 (sticky header + fixing .os-bg / html,body overflow rules), the user kept reporting the bar disappears on scroll. position: sticky is sensitive — the browser was picking the wrong scroll container in this layout. Robust fix: stop relying on document/body for scrolling on the Notes page. Changed the page wrapper className in artifacts/tx-os/src/pages/notes.tsx from min-h-screen os-bg flex flex-col to h-screen os-bg flex flex-col overflow-y-auto Now the Notes wrapper is a known scroll container that's exactly one viewport tall. The sticky header inside it pins reliably to the top edge of that container (== top of the viewport). Composer flush-placement (Task #448), index.css overflow-x:clip on html,body (Task #450), and other pages using .os-bg are all untouched. Single-line change, no other refactors. |
||
|
|
6ed0fea0e9 |
Task #448: remove gap under Notes header by switching back to sticky
Task #446 made the Notes header `position: fixed` and added an aria-hidden spacer below it sized to the header's measured height (via ResizeObserver). The spacer was exactly the "big empty band" the user kept seeing between the header bar and the "اكتب ملاحظة..." composer — the page background gradient showed through the reserved space. Switched the header back to `sticky top-0 z-20`. Sticky keeps it pinned to the top of the viewport while the page scrolls, but it stays in document flow, so the composer renders immediately under the bar with no artificial gap. Removed the now-unused `headerRef`, `headerHeight` state, ResizeObserver effect, and the spacer div. Bumped composer wrapper padding from `pt-1` back to `pt-3` for a small breathing space (no longer competing with the spacer). No other changes; folders rail, composer behavior, RTL/LTR all untouched. |
||
|
|
4dd28a821c |
Task #448: remove gap under Notes header by switching back to sticky
Task #446 made the Notes header `position: fixed` and added an aria-hidden spacer below it sized to the header's measured height (via ResizeObserver). The spacer was exactly the "big empty band" the user kept seeing between the header bar and the "اكتب ملاحظة..." composer — the page background gradient showed through the reserved space. Switched the header back to `sticky top-0 z-20`. Sticky keeps it pinned to the top of the viewport while the page scrolls, but it stays in document flow, so the composer renders immediately under the bar with no artificial gap. Removed the now-unused `headerRef`, `headerHeight` state, ResizeObserver effect, and the spacer div. Bumped composer wrapper padding from `pt-1` back to `pt-3` for a small breathing space (no longer competing with the spacer). No other changes; folders rail, composer behavior, RTL/LTR all untouched. |
||
|
|
32cda2d86b |
notes: tighten gap above top composer (Task #447)
The composer wrapper that sits directly under the fixed header's height-matching spacer had `pt-4` (16px) on top, which made an obvious empty band between the bottom border of the frozen header and the top of the "اكتب ملاحظة..." card. Reduced that to `pt-1` (4px) so the composer now sits flush under the bar with a minimal breathing space, without affecting the rest of the page (folders rail / browser keep their own `py-6`). artifacts/tx-os/src/pages/notes.tsx:497 — single class change. |
||
|
|
c3999b9af7 |
notes: freeze header + move composer to top of page (Task #446)
User wanted the Notes top bar to truly stay pinned (فريز / ثابت) while scrolling, and wanted the "اكتب ملاحظة..." composer to live at the top of the page above the folders area instead of being tucked under the folders chips and only showing inside a folder. Changes (artifacts/tx-os/src/pages/notes.tsx): - Header bar switched from `sticky top-0` to `fixed top-0 inset-x-0 z-30`. Measured its live height with a `ResizeObserver` + `offsetHeight` (header wraps differently per tab / label set / viewport width), and rendered a same-height aria-hidden spacer immediately below so the first row of content never slides under the bar. - Composer was lifted out of the in-folder branch and is now rendered as the first content block under the spacer (above FoldersRail and FoldersBrowser) on the active "My Notes" view. It is hidden on Inbox, Sent, Archived, and the Shared-with-me read-only folder view, so the read-only contract from Task #445 stays intact. `folderSelection` is still passed in unchanged, so a new note created from this top composer continues to land in the currently-selected folder (or Unfiled if none). - Removed the duplicate Composer mount inside the per-folder branch and left a comment explaining the move. Notes: - No backend or i18n changes — composer placeholder, search input, tabs, label chips and grid/list toggle all kept their existing markup and behavior. - Pre-existing TS errors in artifacts/api-server/src/routes/executive-meetings.ts remain (out of scope) and the `test` workflow continues to fail for that unrelated reason. |
||
|
|
7384711705 |
notes: address review feedback for folder sharing (Task #445)
Re-applied review fixes on top of the live folder-sharing implementation: - PATCH/DELETE /notes/:id now do an id-only lookup and return an explicit 403 when the caller is not the owner (previously returned an ambiguous 404). This makes the read-only contract for shared-folder recipients precise instead of silently masquerading as "not found". - PUT /note-folders/:id/shares rejects self-share with 400 "Cannot share folder with yourself" instead of silently filtering the caller's id out of the recipient set. - GET /note-folders/shared-with-me now includes a `noteCount` (live count of the owner's non-archived notes in the folder) computed via SQL subquery; SharedFolder TS type updated; folders rail renders the count next to each shared folder. - SharedFolderView accepts the page's `search` value and filters the read-only list client-side over title + content + checklist item text, matching the owner's search experience. Empty state distinguishes "no notes" from "no matches". - Added GET /notes?sharedFolderId=:id as a spec-aligned alias that returns the owner's live notes inside a folder shared with the caller (gated by an active row in note_folder_shares). - Updated the stale comment block above the share routes that claimed recipient writes "just 404"; documents the new 403 contract. Deviations / out of scope: - Project uses `drizzle-kit push` (no migrations directory); no SQL migration file was added. - Pre-existing TS errors in artifacts/api-server/src/routes/executive-meetings.ts remain; unrelated to Task #445. - "Strict reject for nonexistent recipient ids in share PUT" left as optional follow-up (currently silently dropped per existing behavior). |
||
|
|
a4949983f3 |
Notes: live folder-sharing (read-only) — Task #445
Owner can share a whole folder with other users; recipients see it
under "Shared with me" in the folders rail and view notes read-only
(cannot edit, add, move, or delete).
- DB: new noteFolderSharesTable (folderId/recipientUserId, cascade,
unique idx). Pushed via drizzle-kit.
- API (artifacts/api-server/src/routes/notes.ts):
- GET/PATCH/POST /note-folders return sharedWithCount.
- GET /note-folders/shared-with-me, GET /note-folders/:id/shares,
PUT /note-folders/:id/shares (idempotent diff), DELETE
/note-folders/:id/shares/:userId, GET /note-folders/:id/shared-notes
(verifies recipient via noteFolderSharesTable, stamps readOnly).
- Emits note-folder-shared / note-folder-unshared on share changes.
- Existing note write paths remain owner-scoped → recipients can't
mutate owner notes.
- Frontend types/hooks (notes-api.ts): SharedFolder, FolderShareRecipient,
SharedFolderNote/View; useFolderShares, useUpdateFolderShares,
useSharedWithMeFolders, useSharedFolderNotes.
- FoldersRail: Share menu item, sharedWithCount badge, "Shared with me"
section, "shared-folder" selection kind, onShareFolder prop.
- notes.tsx: SharedFolderView (no Composer, read-only cards using
ChecklistView), FolderShareDialog (seeds existing recipients,
idempotent PUT on save), revocation fallback effect, dialog mount.
- use-notifications-socket.ts: subscribe to note-folder-shared /
-unshared → invalidate shared-with-me + open shared-folder notes for
live rail/view refresh.
- i18n: AR/EN keys for share/sharedBy/sharedByName/sharedWithCount/
sharedWithMe/readOnly/shareRevoked/sharedEmpty/shareSaved/shareFailed.
Pre-existing failing `test` workflow (executive-meetings type errors)
is unrelated and out of scope.
|
||
|
|
5ef0250195 |
Task #444: Add Send button to note composer
Problem: the new-note composer in the Notes page only exposed Save — to send a freshly-typed note the user had to save it, hover the resulting card, and click the small Send icon. The user asked for an explicit Send action right inside the composer and confirmed they're fine with auto-saving as a side effect of pressing Send. Implementation (artifacts/tx-os/src/pages/notes.tsx): - Added `onSend: (id: number) => void` prop to <Composer> and wired the NotesPage call site to forward to the existing `setSendForNoteId(id)` flow that already mounts <SendNoteDialog>. - Extracted a `buildPayload()` helper from save() so save() and the new sendNew() use the exact same payload shape (title, content, color, labels, kind, items, folderId). - Added `sendNew()` that runs the same `create.mutate` save and, on success, resets the composer and calls `onSend(created.id)` so the send dialog opens for the freshly created note. On failure the composer state is left intact (same UX as save() failures). - Added a Send icon button next to Save inside the composer footer, aria-labeled with the existing `notes.send` translation and tagged `data-testid="notes-composer-send"`. The button is disabled while the composer is empty (no title and no content/checklist text) and while a create request is in flight to prevent duplicate notes from rapid clicks. The button lives inside the composer's `ref` so it doesn't trip the existing click-outside-to-save handler, and <SendNoteDialog> renders in a Radix portal which is already excluded from that handler. No changes to send dialog logic, the per-card hover Send button, or any other surface. tsc --noEmit passes for tx-os. The pre-existing api-server `test` workflow failures (executive-meetings, service- orders) are unrelated and out of scope. |
||
|
|
2a89b38b04 |
Task #443: Fix notes search icon overlapping placeholder in Arabic
Problem: in the Notes page header search field, the magnifier icon is
absolutely positioned on the start side (right in RTL, left in LTR)
via inline style, but the input's reserved padding was using
`isRtl ? "pe-9" : "ps-9"`. In RTL, `pe-9` resolves to padding-LEFT —
the opposite side from the icon — so the placeholder text "ابحث في
الملاحظات" started flush to the right edge and visibly overlapped
the icon. In LTR it happened to look correct because both `ps-9` and
the icon's `left: 10` line up.
Fix: pin the padding to the start side unconditionally
(`className="ps-9"`) so it always matches the icon's start-side
placement in both directions. Added an inline comment documenting
the invariant ("icon on start side ⇒ padding on start side") so a
future edit doesn't re-introduce the asymmetry.
Files:
- artifacts/tx-os/src/pages/notes.tsx (search input around line 304-321)
No behavioural changes to LTR rendering, no other search inputs
affected (only this one bound the padding to `isRtl`). Verified with
`pnpm exec tsc --noEmit` (clean). The pre-existing failing `test`
workflow stems from unrelated api-server tsc errors in
executive-meetings.ts and is out of scope for this task.
|
||
|
|
11439abd39 |
Task #442: Checklist checkbox color matches the note's color
- artifacts/tx-os/src/lib/notes-api.ts: extended NOTE_COLORS so each entry carries an `accent` Tailwind class alongside `bg`/`ring` (default→amber, red→red, orange→orange, yellow→yellow, green→green, teal→teal, blue→sky, purple→purple, pink→pink, gray→slate; all *-500 for visibility in light + dark). Added a `colorAccent(id)` helper that resolves to a literal accent-* class, falling back to the default amber for unknown/null ids. The class strings are written out as literals so Tailwind v4's source scanner emits all of them into the generated CSS — no dynamic class names, no safelist needed. - artifacts/tx-os/src/pages/notes.tsx: ChecklistView gained an optional `noteColor?: string | null` prop and now resolves its checkbox accent via `colorAccent(noteColor)` instead of the hard-coded `accent-amber-500`. Updated all four call sites (NoteCard, received list, sent list, thread dialog) to pass `note.color` / `n.color` / `thread.color`. - artifacts/tx-os/src/components/notes/incoming-note-popup.tsx: PopupChecklistPreview gained the same optional `noteColor` prop and uses `colorAccent` for the checkbox className. Call site now passes `current.color` from the popup queue. - Unchecked boxes still render with the neutral browser default; only the checked accent (the filled tint) follows the note color. The default (white/card) note keeps amber for visibility against the neutral surface in both themes. - Verified: `pnpm exec tsc --noEmit` is clean and the `tests/notes-popup-on-receive.spec.mjs` regression suite (incl. the Task #433 checklist popup case) still passes 3/3. - Pre-existing failing `test` workflow (api-server tsc errors in executive-meetings.ts) is unrelated and out of scope. |
||
|
|
f6af6ea5ce |
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. |
||
|
|
ea3092bb2b |
Task #431: polish Send Note dialog (RTL X + search overlap)
Two visual issues in the "إرسال الملاحظة" dialog:
1. The shadcn DialogContent renders a built-in close (X) button hard
pinned at `right-4 top-4`. In Arabic the dialog title is
right-aligned and crashed into the X.
Fix: add `pr-8` to the SendNoteDialog header so the title row
reserves physical right-padding matching where the X actually
lives in both LTR and RTL. (Initially used `pe-8` but corrected to
`pr-8` after code review noted the close button is pinned with
physical `right-4`, so a logical end-padding flips to the wrong
side in RTL.) The shared dialog component is unchanged so other
dialogs are untouched (out of scope per the task plan).
2. The recipient search field used inline `style={{ right|left: 10 }}`
for the magnifier and a conditional `pe-9` / `ps-9` based on
`i18n.language`. In RTL the icon collided with the placeholder.
Fix: drop the conditional branching — position the icon with the
logical class `start-2.5` and always pad the input with `ps-9`.
The icon now sits at the inline-start edge in both LTR and RTL
with a clear gutter to the placeholder/caret.
Skipped step 3 of the plan (i18n aria-label override on the X). It
required no locale changes and no rendering improvement, so per the
plan's own conditional ("only if step 3 keeps the override; otherwise
no locale changes") it was dropped to keep scope tight.
Verification:
- `pnpm exec tsc --noEmit` clean.
- Playwright: `notes-inbox.spec.mjs` (1/1) and
`notes-popup-touch-tap.spec.mjs` (2/2) green — both exercise the
send-note dialog rendering. `notes-popup-on-receive.spec.mjs` runs
two browser contexts and exceeds the local 120s shell cap; not
re-run here, but the diff is CSS-only and cannot affect the wire
flow it covers.
|
||
|
|
b5e063d6ed |
Task #431: polish Send Note dialog (RTL X + search overlap)
Two visual issues in the "إرسال الملاحظة" dialog:
1. The shadcn DialogContent renders a built-in close (X) button hard
pinned at `right-4 top-4`. In Arabic the dialog title is
right-aligned and crashed into the X.
Fix: add `pe-8` to the SendNoteDialog header so the title row
reserves logical end-padding for the X in both directions. The
shared dialog component is unchanged so other dialogs are untouched
(out of scope per the task plan).
2. The recipient search field used inline `style={{ right|left: 10 }}`
for the magnifier and a conditional `pe-9` / `ps-9` based on
`i18n.language`. In RTL the icon collided with the placeholder.
Fix: drop the conditional branching — position the icon with the
logical class `start-2.5` and always pad the input with `ps-9`.
The icon now sits at the inline-start edge in both LTR and RTL
with a clear gutter to the placeholder/caret.
Skipped step 3 of the plan (i18n aria-label override on the X). It
required no locale changes and no rendering improvement, so per the
plan's own conditional ("only if step 3 keeps the override; otherwise
no locale changes") it was dropped to keep scope tight.
Verification:
- `pnpm exec tsc --noEmit` clean.
- Playwright: `notes-inbox.spec.mjs` (1/1) and
`notes-popup-touch-tap.spec.mjs` (2/2) green — both exercise the
send-note dialog rendering. `notes-popup-on-receive.spec.mjs` runs
two browser contexts and exceeds the local 120s shell cap; not
re-run here, but the diff is CSS-only and cannot affect the wire
flow it covers.
|