chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged: - Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances) - country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk after the per-subpath dynamic-import approach silently failed in webpack) - Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index, redirects (ocr to ai, reports to dashboard, invitations to users), docs/admin-ia-proposal.md - Per-template email tester (registry + endpoint + UI on Email admin page) - Cancel-document mode picker (delete-from-Documenso vs keep-for-audit) - Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers - Customize-widgets per-region sortables at xl+ (charts/rails/feed); single flat sortable below xl when the layout stacks; per-viewport saved orders - Audit doc updates capturing each shipped item - Lint fixes: react-compiler immutability in DonutChart (reduce instead of let-reassign), set-state-in-effect disables in CountryFlag and UploadForSigning preview-bytes effect, unused 'confirm' destructures in interest contract + reservation tabs, unescaped apostrophe in test-template card copy
This commit is contained in:
@@ -33,23 +33,35 @@ import {
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useDashboardWidgets } from '@/hooks/use-dashboard-widgets';
|
||||
import type { DashboardWidget } from './widget-registry';
|
||||
import type { DashboardWidget, WidgetGroup } from './widget-registry';
|
||||
|
||||
// The dashboard renders widgets in three independent visual regions at
|
||||
// xl (1280+): charts (main column), rails (right aside), feed (full-
|
||||
// width). Below xl, all three regions stack into one visual column -
|
||||
// from the rep's eye it reads as a single ordered list, so the modal
|
||||
// flattens its sortable in that tier. At xl it splits into three
|
||||
// region-scoped sortables to match the actual side-by-side layout.
|
||||
const GROUP_LABELS: Record<WidgetGroup, string> = {
|
||||
chart: 'Charts',
|
||||
rail: 'Side rail',
|
||||
feed: 'Activity',
|
||||
};
|
||||
const GROUP_ORDER: readonly WidgetGroup[] = ['chart', 'rail', 'feed'];
|
||||
|
||||
/**
|
||||
* Combined visibility + reorder picker for the dashboard header. Two
|
||||
* sections in one modal:
|
||||
* Combined visibility + reorder picker for the dashboard header.
|
||||
*
|
||||
* 1. "On dashboard" — visible widgets, each row with a drag handle
|
||||
* (reorder via dnd-kit single SortableContext, no buckets); flipping
|
||||
* a switch off moves the row to section 2.
|
||||
* 2. "Hidden" — widgets currently off; flipping a switch on appends to
|
||||
* the bottom of section 1.
|
||||
* The dashboard renders widgets in three independent visual regions -
|
||||
* Charts (main column), Side rail (right aside), Activity (full-width
|
||||
* feed). A drag across regions can't change the visual outcome, so the
|
||||
* modal exposes one sortable list per region instead of a single flat
|
||||
* list that silently fails on cross-region moves. Toggling a widget off
|
||||
* moves it to the "Hidden" section; toggling on appends it to the
|
||||
* bottom of its native region.
|
||||
*
|
||||
* Both visibility toggles and order changes commit optimistically via
|
||||
* `useDashboardWidgets` so the dashboard reflows in the background and
|
||||
* the rep can keep editing. The "Rearrange" button on the header is
|
||||
* gone — order lives here too now, keeping all dashboard layout
|
||||
* controls in one place.
|
||||
* the rep can keep editing.
|
||||
*/
|
||||
export function CustomizeWidgetsMenu() {
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -57,6 +69,7 @@ export function CustomizeWidgetsMenu() {
|
||||
allWidgets,
|
||||
visibleWidgets,
|
||||
visibility,
|
||||
isXlLayout,
|
||||
setVisible,
|
||||
setAll,
|
||||
setOrder,
|
||||
@@ -79,7 +92,53 @@ export function CustomizeWidgetsMenu() {
|
||||
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
||||
);
|
||||
|
||||
function onDragEnd(event: DragEndEvent) {
|
||||
// Visible widgets split per region. Empty regions render nothing so
|
||||
// we don't show an "On dashboard / Side rail (0)" tease.
|
||||
const visibleByGroup: Record<WidgetGroup, DashboardWidget[]> = {
|
||||
chart: visibleWidgets.filter((w) => w.group === 'chart'),
|
||||
rail: visibleWidgets.filter((w) => w.group === 'rail'),
|
||||
feed: visibleWidgets.filter((w) => w.group === 'feed'),
|
||||
};
|
||||
|
||||
// A drag inside group X only moves widgets within that group. Rebuild
|
||||
// the flat order by walking `visibleWidgets` in its current sequence
|
||||
// and replacing each group-X slot with the next id from the reordered
|
||||
// group list. This preserves the relative position of every other
|
||||
// widget - only the dragged group's internal order changes.
|
||||
function reorderGroup(group: WidgetGroup, oldIndex: number, newIndex: number) {
|
||||
const groupIds = visibleByGroup[group].map((w) => w.id);
|
||||
if (
|
||||
oldIndex < 0 ||
|
||||
newIndex < 0 ||
|
||||
oldIndex >= groupIds.length ||
|
||||
newIndex >= groupIds.length
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const reordered = arrayMove(groupIds, oldIndex, newIndex);
|
||||
let cursor = 0;
|
||||
const nextOrder = visibleWidgets.map((w) =>
|
||||
w.group === group ? (reordered[cursor++] ?? w.id) : w.id,
|
||||
);
|
||||
setOrder(nextOrder);
|
||||
}
|
||||
|
||||
function makeDragEndHandler(group: WidgetGroup) {
|
||||
return (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
if (!over || active.id === over.id) return;
|
||||
const ids = visibleByGroup[group].map((w) => w.id);
|
||||
const oldIndex = ids.indexOf(String(active.id));
|
||||
const newIndex = ids.indexOf(String(over.id));
|
||||
if (oldIndex === -1 || newIndex === -1) return;
|
||||
reorderGroup(group, oldIndex, newIndex);
|
||||
};
|
||||
}
|
||||
|
||||
// Flat reorder used by the stacked layout (< xl). One SortableContext
|
||||
// over every visible widget; drops persist via setOrder, which the
|
||||
// hook routes to the mobile order field.
|
||||
function onFlatDragEnd(event: DragEndEvent) {
|
||||
const { active, over } = event;
|
||||
if (!over || active.id === over.id) return;
|
||||
const ids = visibleWidgets.map((w) => w.id);
|
||||
@@ -97,24 +156,64 @@ export function CustomizeWidgetsMenu() {
|
||||
Customize
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-xl">
|
||||
<DialogContent className="sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Customize dashboard</DialogTitle>
|
||||
<DialogDescription>
|
||||
Drag a visible widget to change its position. Toggle the switch to show or hide. Hidden
|
||||
widgets leave no empty space - the layout reflows to fill the available width.
|
||||
{isXlLayout
|
||||
? 'Editing the desktop layout - drag a widget to reorder it within its region.'
|
||||
: 'Editing the stacked layout for this device - drag a widget to reorder. Your desktop arrangement is saved separately.'}{' '}
|
||||
Toggle the switch to show or hide. Hidden widgets leave no empty space - the layout
|
||||
reflows to fill the available width.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Toggle + reorder list. Capped at ~60vh with internal scroll so
|
||||
the modal doesn't push the action footer off-screen. */}
|
||||
the modal doesn't push the action footer off-screen. The
|
||||
layout matches what the rep is actually seeing: at xl the
|
||||
dashboard renders charts | rails | feed as three independent
|
||||
slots, so the picker exposes three region-scoped sortables.
|
||||
Below xl everything stacks into one column visually, so the
|
||||
picker collapses to a single flat sortable that reorders
|
||||
across the whole list. */}
|
||||
<div className="max-h-[60vh] -mx-2 overflow-y-auto px-2">
|
||||
{visibleWidgets.length > 0 ? (
|
||||
{isXlLayout ? (
|
||||
GROUP_ORDER.map((group) => {
|
||||
const widgets = visibleByGroup[group];
|
||||
if (widgets.length === 0) return null;
|
||||
return (
|
||||
<Section key={group} title={`${GROUP_LABELS[group]} (${widgets.length})`}>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={makeDragEndHandler(group)}
|
||||
>
|
||||
<SortableContext
|
||||
items={widgets.map((w) => w.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<ul className="space-y-1">
|
||||
{widgets.map((w, idx) => (
|
||||
<SortableVisibleRow
|
||||
key={w.id}
|
||||
widget={w}
|
||||
position={idx + 1}
|
||||
disabled={isSaving}
|
||||
onToggle={(checked) => setVisible(w.id, checked)}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</Section>
|
||||
);
|
||||
})
|
||||
) : visibleWidgets.length > 0 ? (
|
||||
<Section title={`On dashboard (${visibleWidgets.length})`}>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={onDragEnd}
|
||||
onDragEnd={onFlatDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={visibleWidgets.map((w) => w.id)}
|
||||
|
||||
Reference in New Issue
Block a user