@bsuite/page-builder 2.7.1-next.6 → 2.7.1-next.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/PageGridLayout.d.ts.map +1 -1
- package/dist/PageGridLayout.js +131 -23
- package/dist/types.d.ts +58 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/usePageGridLayout.d.ts +20 -1
- package/dist/usePageGridLayout.d.ts.map +1 -1
- package/dist/usePageGridLayout.js +123 -15
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PageGridLayout.d.ts","sourceRoot":"","sources":["../src/PageGridLayout.tsx"],"names":[],"mappings":"AAGA,OAAO,KAQN,MAAM,OAAO,CAAC;AAGf,OAAO,kCAAkC,CAAC;AAC1C,OAAO,gCAAgC,CAAC;AAsBxC,OAAO,KAAK,EAAe,mBAAmB,EAA4B,MAAM,YAAY,CAAC;AAE7F,4EAA4E;AAC5E,eAAO,MAAM,oBAAoB,0BAA0B,CAAC;AAC5D,MAAM,WAAW,uBAAuB;IAAG,OAAO,EAAE,MAAM,CAAA;CAAE;
|
|
1
|
+
{"version":3,"file":"PageGridLayout.d.ts","sourceRoot":"","sources":["../src/PageGridLayout.tsx"],"names":[],"mappings":"AAGA,OAAO,KAQN,MAAM,OAAO,CAAC;AAGf,OAAO,kCAAkC,CAAC;AAC1C,OAAO,gCAAgC,CAAC;AAsBxC,OAAO,KAAK,EAAe,mBAAmB,EAA4B,MAAM,YAAY,CAAC;AAE7F,4EAA4E;AAC5E,eAAO,MAAM,oBAAoB,0BAA0B,CAAC;AAC5D,MAAM,WAAW,uBAAuB;IAAG,OAAO,EAAE,MAAM,CAAA;CAAE;AAmrB5D,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,cAAc,EACd,WAAW,EACX,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,iBAAiB,EACjB,OAAO,EACP,UAAU,EACV,SAAS,EAMT,WAAkB,EAClB,aAAsC,EACtC,QAAQ,EACR,iBAAiB,EACjB,gBAAgB,EAKhB,UAAkB,EAClB,yBAAiE,EACjE,kBAAkB,EAClB,sBAAsB,EACtB,mBAAmB,EACnB,+BAA6E,EAC7E,wBAAwB,EACxB,4BAA4B,EAC5B,4BAA4B,GAC7B,EAAE,mBAAmB,qBAoqCrB"}
|
package/dist/PageGridLayout.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { ElementScopeProvider } from './elementScope.js';
|
|
3
3
|
import { CardPaddingBoundary } from './cardPadding.js';
|
|
4
|
-
import { ArrowDown, ArrowUp, ChevronDown, ChevronUp, Eye, EyeOff, Layers, LayoutGrid, Lock, Plus, RotateCcw, Save, Settings2, Unlock } from 'lucide-react';
|
|
4
|
+
import { ArrowDown, ArrowUp, ChevronDown, ChevronUp, Eye, EyeOff, Layers, LayoutGrid, Lock, Plus, RefreshCw, RotateCcw, Save, Settings2, Unlock, X } from 'lucide-react';
|
|
5
5
|
import React, { startTransition, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
6
6
|
import { Responsive } from 'react-grid-layout';
|
|
7
7
|
import { gridBounds, minMaxSize, minSize } from 'react-grid-layout/core';
|
|
@@ -11,7 +11,7 @@ import { computeAutoHeightRows } from './autoHeight.js';
|
|
|
11
11
|
import { BORDER_STYLES, BORDER_TONES, BORDER_WIDTH_RANGE, DEFAULT_CARD_STYLE, PADDING_RANGE, RADIUS_RANGE, describeCardStyle, isDefaultCardStyle, normaliseCardStyle, toCssVars, } from './cardStyle.js';
|
|
12
12
|
import { defaultPreferenceAdapter } from './preferences.js';
|
|
13
13
|
import { isRelationshipWritable } from './relationshipCatalog.js';
|
|
14
|
-
import { usePageGridLayout } from './usePageGridLayout.js';
|
|
14
|
+
import { combinePreferenceStatuses, preferenceStatusOf, usePageGridLayout } from './usePageGridLayout.js';
|
|
15
15
|
import { cn } from './utils.js';
|
|
16
16
|
/** Ask one mounted canvas to acknowledge its preferences before closing. */
|
|
17
17
|
export const PAGE_GRID_SAVE_EVENT = 'bsuite-page-grid-save';
|
|
@@ -131,8 +131,8 @@ const RGLResizeHandle = React.forwardRef(function RGLResizeHandle({ axis = 'se',
|
|
|
131
131
|
* scroll away. Drag still starts from the strip (outer `.drag-handle`); hide
|
|
132
132
|
* is a `button` + `data-no-drag` so the existing cancel selector ignores it.
|
|
133
133
|
*/
|
|
134
|
-
function GridItemEditorChrome({ id, label, onHide, }) {
|
|
135
|
-
return (_jsxs("div", { "data-slot": "grid-item-editor-chrome", className: "flex min-w-0 flex-none items-center gap-1 px-2 py-1 bg-muted text-muted-foreground", children: [_jsx("span", { className: "min-w-0 truncate text-[10px] px-1.5 py-0.5 rounded-md font-medium", title: label, children: label }), _jsx("button", { type: "button", className: "ml-auto h-6 w-6 shrink-0 rounded-full flex items-center justify-center bg-destructive hover:bg-destructive text-destructive-foreground shadow transition-colors", "data-no-drag": true, onPointerDown: (event) => event.stopPropagation(), onClick: (event) => {
|
|
134
|
+
function GridItemEditorChrome({ id, label, onHide, hideDisabled = false, }) {
|
|
135
|
+
return (_jsxs("div", { "data-slot": "grid-item-editor-chrome", className: "flex min-w-0 flex-none items-center gap-1 px-2 py-1 bg-muted text-muted-foreground", children: [_jsx("span", { className: "min-w-0 truncate text-[10px] px-1.5 py-0.5 rounded-md font-medium", title: label, children: label }), _jsx("button", { type: "button", className: "ml-auto h-6 w-6 shrink-0 rounded-full flex items-center justify-center bg-destructive hover:bg-destructive text-destructive-foreground shadow transition-colors disabled:opacity-50 disabled:pointer-events-none", disabled: hideDisabled, "data-no-drag": true, onPointerDown: (event) => event.stopPropagation(), onClick: (event) => {
|
|
136
136
|
event.stopPropagation();
|
|
137
137
|
onHide(id);
|
|
138
138
|
}, title: `Hide ${label}`, "aria-label": `Hide ${label}`, children: _jsx(EyeOff, { className: "h-3 w-3", "aria-hidden": "true" }) })] }));
|
|
@@ -169,7 +169,7 @@ function warnOnceAboutCollapsedContent(el) {
|
|
|
169
169
|
+ '(an aspect ratio, a px/rem height, or min-height), or pass autoHeight={false} on '
|
|
170
170
|
+ 'this card so it renders into the constrained scrollable wrapper instead.');
|
|
171
171
|
}
|
|
172
|
-
const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, isEditing, label, pageKey, onHide, autoHeight, onAutoHeightChange, chrome = false, cardStyleVars, children: injectedChildren, className: injectedClassName, style: injectedStyle, ...rest }, ref) {
|
|
172
|
+
const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, isEditing, label, pageKey, onHide, editingPaused = false, autoHeight, onAutoHeightChange, chrome = false, cardStyleVars, children: injectedChildren, className: injectedClassName, style: injectedStyle, ...rest }, ref) {
|
|
173
173
|
// Auto-height measurement (blueprint amendment A1). `measureRef` wraps
|
|
174
174
|
// `content` with NO height constraint of its own — its parent has
|
|
175
175
|
// `overflow-hidden` + `flex-1 min-h-0` (clips *painting* to the current
|
|
@@ -321,7 +321,9 @@ const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, is
|
|
|
321
321
|
// the GridItem has it as an ancestor → drag fires. Interactive elements
|
|
322
322
|
// (buttons, inputs, textarea, select, links, [data-no-drag]) are still
|
|
323
323
|
// protected via the `cancel` selector below in the <Responsive> render.
|
|
324
|
-
const outerClass = cn('relative group overflow-visible', isEditing && 'drag-handle
|
|
324
|
+
const outerClass = cn('relative group overflow-visible', isEditing && 'drag-handle',
|
|
325
|
+
// Paused editing (bsuite#3277) cannot move a card, so it must not promise to.
|
|
326
|
+
isEditing && !editingPaused && 'cursor-move', injectedClassName);
|
|
325
327
|
return (_jsxs("div", { ref: ref, className: outerClass, style: injectedStyle, ...rest, children: [_jsxs("div", { className: "h-full w-full relative", children: [isEditing && (_jsx("div", { className: "absolute inset-0 z-10 pointer-events-none rounded-3xl border-2 border-transparent group-hover:border-primary/50 transition-colors bg-primary/5" })), _jsx("div", { "data-slot": "grid-item-surface", "data-chrome": chrome ? 'on' : 'off', "data-grid-slot-key": id, ref: surfaceRef, "data-card-padding": !chrome && cardStyleVars && '--card-padding' in cardStyleVars ? '' : undefined, className: chrome
|
|
326
328
|
? // ONE radius token, read by the grid item AND available to any
|
|
327
329
|
// app card that opts into the same surface. Two surfaces
|
|
@@ -380,7 +382,7 @@ const GridItem = React.memo(React.forwardRef(function GridItem({ id, content, is
|
|
|
380
382
|
...(chrome && cardStyleVars && '--card-padding' in cardStyleVars
|
|
381
383
|
? { padding: cardStyleVars['--card-padding'] }
|
|
382
384
|
: null),
|
|
383
|
-
}, children: autoHeight ? (_jsx("div", { "data-slot": "grid-item-body", className: "min-h-0 flex-none overflow-visible", children: _jsxs("div", { ref: measureRef, className: "flow-root", children: [isEditing ? (_jsx(GridItemEditorChrome, { id: id, label: label, onHide: onHide })) : null, scopedContent] }) })) : (_jsxs(_Fragment, { children: [isEditing ? (_jsx(GridItemEditorChrome, { id: id, label: label, onHide: onHide })) : null, _jsx("div", { "data-slot": "grid-item-body", className: "min-h-0 flex-1 overflow-auto", children: scopedContent })] })) })] }), injectedChildren] }));
|
|
385
|
+
}, children: autoHeight ? (_jsx("div", { "data-slot": "grid-item-body", className: "min-h-0 flex-none overflow-visible", children: _jsxs("div", { ref: measureRef, className: "flow-root", children: [isEditing ? (_jsx(GridItemEditorChrome, { id: id, label: label, onHide: onHide, hideDisabled: editingPaused })) : null, scopedContent] }) })) : (_jsxs(_Fragment, { children: [isEditing ? (_jsx(GridItemEditorChrome, { id: id, label: label, onHide: onHide, hideDisabled: editingPaused })) : null, _jsx("div", { "data-slot": "grid-item-body", className: "min-h-0 flex-1 overflow-auto", children: scopedContent })] })) })] }), injectedChildren] }));
|
|
384
386
|
}));
|
|
385
387
|
export function PageGridLayout({ pageKey, defaultLayouts, defaultCols, layoutVersion, canEditPage, editorEventNames, preferenceAdapter, widgets, widgetMeta, className,
|
|
386
388
|
// Resize default flipped 2026-05-12: rgl v2 dropped auto-handle rendering
|
|
@@ -394,7 +396,7 @@ isResizable = true, resizeHandles = DEFAULT_RESIZE_HANDLES, tenantId, defaultAut
|
|
|
394
396
|
// `itemChrome` to get the pre-2.0.0 behaviour back for every slot, and
|
|
395
397
|
// migrates page by page with the per-item `chrome` flag.
|
|
396
398
|
itemChrome = false, addEntityWidgetEventNames = DEFAULT_ADD_ENTITY_WIDGET_EVENT_NAMES, createEntityWidget, onRegisterEntityWidget, relationshipCatalog, addRelationshipWidgetEventNames = DEFAULT_ADD_RELATIONSHIP_WIDGET_EVENT_NAMES, createRelationshipWidget, onRegisterRelationshipWidget, onRelationshipWidgetRejected, }) {
|
|
397
|
-
const { currentLayouts, layoutCols, isEditing, setIsEditing, flushPreferences, activeCols, activeCompactor, onLayoutChange, handleColumnChange, handleBreakpointChange, handleCompact, handleReset, addWidget, moveWidget, setWidgetLocked, applyAutoHeightRows, autoHeightRows, resetConfirmOpen, setResetConfirmOpen, containerRef, containerWidth, } = usePageGridLayout({
|
|
399
|
+
const { currentLayouts, layoutCols, isEditing, setIsEditing, flushPreferences, preferencesStatus, canRetryPreferences, retryPreferences, activeCols, activeCompactor, onLayoutChange, handleColumnChange, handleBreakpointChange, handleCompact, handleReset, addWidget, moveWidget, setWidgetLocked, applyAutoHeightRows, autoHeightRows, resetConfirmOpen, setResetConfirmOpen, containerRef, containerWidth, } = usePageGridLayout({
|
|
398
400
|
pageKey,
|
|
399
401
|
defaultLayouts,
|
|
400
402
|
defaultCols,
|
|
@@ -469,7 +471,6 @@ itemChrome = false, addEntityWidgetEventNames = DEFAULT_ADD_ENTITY_WIDGET_EVENT_
|
|
|
469
471
|
autoHeightFrameRef.current = scheduleFrame(flushAutoHeightUpdates);
|
|
470
472
|
}
|
|
471
473
|
}, [flushAutoHeightUpdates, scheduleFrame]);
|
|
472
|
-
const resizeEnabled = isEditing && isResizable;
|
|
473
474
|
const resizeConstraints = useMemo(() => [
|
|
474
475
|
// `gridBounds` caps width at the active column count, so we don't need
|
|
475
476
|
// a global maxSize(...). Width and height are unbounded by default;
|
|
@@ -481,8 +482,10 @@ itemChrome = false, addEntityWidgetEventNames = DEFAULT_ADD_ENTITY_WIDGET_EVENT_
|
|
|
481
482
|
], []);
|
|
482
483
|
const [extraWidgetConfigs, setExtraWidgetConfigs] = useState({});
|
|
483
484
|
const [extraRelationshipWidgetConfigs, setExtraRelationshipWidgetConfigs] = useState({});
|
|
484
|
-
const
|
|
485
|
-
const { value:
|
|
485
|
+
const layerNamesPreference = (preferenceAdapter ?? defaultPreferenceAdapter)(`page:${pageKey}_grid_layer_names`, {});
|
|
486
|
+
const { value: layerNames, setValue: setLayerNames, flush: flushLayerNames } = layerNamesPreference;
|
|
487
|
+
const hiddenLayersPreference = (preferenceAdapter ?? defaultPreferenceAdapter)(`page:${pageKey}_grid_hidden_layers`, {});
|
|
488
|
+
const { value: hiddenLayerIds, setValue: setHiddenLayerIds, flush: flushHiddenLayers } = hiddenLayersPreference;
|
|
486
489
|
/*
|
|
487
490
|
* Card appearance, per page, persisted beside the layout.
|
|
488
491
|
*
|
|
@@ -490,13 +493,92 @@ itemChrome = false, addEntityWidgetEventNames = DEFAULT_ADD_ENTITY_WIDGET_EVENT_
|
|
|
490
493
|
* survives a reload, follows the operator across devices wherever the app
|
|
491
494
|
* backs the adapter with the database, and needs no new storage concept.
|
|
492
495
|
*/
|
|
493
|
-
const
|
|
496
|
+
const cardStylePreference = (preferenceAdapter ?? defaultPreferenceAdapter)(`page:${pageKey}_card_style`, DEFAULT_CARD_STYLE);
|
|
497
|
+
const { value: storedCardStyle, setValue: setStoredCardStyle, flush: flushCardStyle } = cardStylePreference;
|
|
498
|
+
/*
|
|
499
|
+
* THE EDIT GATE (bsuite#3277).
|
|
500
|
+
*
|
|
501
|
+
* The hook refuses every write while any of ITS keys is loading or failed.
|
|
502
|
+
* This canvas reads and writes three more keys the hook never sees — layer
|
|
503
|
+
* names, hidden layers and card appearance — so the same rule is applied to
|
|
504
|
+
* them here, and the two are combined into the one state the editor shows.
|
|
505
|
+
*
|
|
506
|
+
* While it is not `'loaded'` the editor stays open but PAUSED: the banner says
|
|
507
|
+
* why, a failed read offers Retry, every control in the banner body is
|
|
508
|
+
* disabled by a single `<fieldset disabled>` (so a control added later cannot
|
|
509
|
+
* forget the gate), drag/resize/hide are off, and every write path in this
|
|
510
|
+
* component is refused underneath the disabled controls as well.
|
|
511
|
+
*
|
|
512
|
+
* Retry is in place. It calls `retry` on each FAILED key's adapter — the
|
|
513
|
+
* hook's four through `retryPreferences`, this component's three directly —
|
|
514
|
+
* and nothing else: no remount, no key change, no reset. The editor, its
|
|
515
|
+
* expanded/collapsed state and everything already loaded stay exactly as they
|
|
516
|
+
* are; the pause lifts on the render in which the last failed key loads.
|
|
517
|
+
*/
|
|
518
|
+
const layerNamesStatus = preferenceStatusOf(layerNamesPreference);
|
|
519
|
+
const hiddenLayersStatus = preferenceStatusOf(hiddenLayersPreference);
|
|
520
|
+
const cardStyleStatus = preferenceStatusOf(cardStylePreference);
|
|
521
|
+
const editingPreferencesStatus = combinePreferenceStatuses([
|
|
522
|
+
preferencesStatus,
|
|
523
|
+
layerNamesStatus,
|
|
524
|
+
hiddenLayersStatus,
|
|
525
|
+
cardStyleStatus,
|
|
526
|
+
]);
|
|
527
|
+
const editingPaused = editingPreferencesStatus !== 'loaded';
|
|
528
|
+
const resizeEnabled = isEditing && isResizable && !editingPaused;
|
|
529
|
+
const { retry: retryLayerNames } = layerNamesPreference;
|
|
530
|
+
const { retry: retryHiddenLayers } = hiddenLayersPreference;
|
|
531
|
+
const { retry: retryCardStyle } = cardStylePreference;
|
|
532
|
+
const canRetryEditingPreferences = canRetryPreferences ||
|
|
533
|
+
(layerNamesStatus === 'failed' && typeof retryLayerNames === 'function') ||
|
|
534
|
+
(hiddenLayersStatus === 'failed' && typeof retryHiddenLayers === 'function') ||
|
|
535
|
+
(cardStyleStatus === 'failed' && typeof retryCardStyle === 'function');
|
|
536
|
+
// Keyed by page so a retry on one page never says "still" about another.
|
|
537
|
+
const [retriedPage, setRetriedPage] = useState(null);
|
|
538
|
+
const retriedAndStillFailed = retriedPage === pageKey && editingPreferencesStatus === 'failed';
|
|
539
|
+
const retryEditingPreferences = useCallback(() => {
|
|
540
|
+
setRetriedPage(pageKey);
|
|
541
|
+
retryPreferences();
|
|
542
|
+
if (layerNamesStatus === 'failed')
|
|
543
|
+
retryLayerNames?.();
|
|
544
|
+
if (hiddenLayersStatus === 'failed')
|
|
545
|
+
retryHiddenLayers?.();
|
|
546
|
+
if (cardStyleStatus === 'failed')
|
|
547
|
+
retryCardStyle?.();
|
|
548
|
+
}, [
|
|
549
|
+
cardStyleStatus,
|
|
550
|
+
hiddenLayersStatus,
|
|
551
|
+
layerNamesStatus,
|
|
552
|
+
pageKey,
|
|
553
|
+
retryCardStyle,
|
|
554
|
+
retryHiddenLayers,
|
|
555
|
+
retryLayerNames,
|
|
556
|
+
retryPreferences,
|
|
557
|
+
]);
|
|
494
558
|
const cardStyle = useMemo(() => normaliseCardStyle(storedCardStyle), [storedCardStyle]);
|
|
495
559
|
const cardStyleVars = useMemo(() => toCssVars(cardStyle), [cardStyle]);
|
|
496
|
-
const updateCardStyle = useCallback((patch) =>
|
|
560
|
+
const updateCardStyle = useCallback((patch) => {
|
|
561
|
+
if (editingPaused)
|
|
562
|
+
return;
|
|
563
|
+
setStoredCardStyle((previous) => ({ ...normaliseCardStyle(previous), ...patch }));
|
|
564
|
+
}, [editingPaused, setStoredCardStyle]);
|
|
565
|
+
const resetCardStyle = useCallback(() => {
|
|
566
|
+
if (editingPaused)
|
|
567
|
+
return;
|
|
568
|
+
setStoredCardStyle({ ...DEFAULT_CARD_STYLE });
|
|
569
|
+
}, [editingPaused, setStoredCardStyle]);
|
|
497
570
|
const saveAndExit = useCallback(async () => {
|
|
498
571
|
if (canEditPage === false || !isEditing || savingAttempt.current !== null)
|
|
499
572
|
return;
|
|
573
|
+
if (editingPaused) {
|
|
574
|
+
// Every write is refused while paused, so the paused keys hold nothing to
|
|
575
|
+
// save. Flushing them would only let an adapter that refuses to flush a
|
|
576
|
+
// failed key hold the user in an editor they cannot use. (A write made
|
|
577
|
+
// before the pause — the page switched mid-session — is already with its
|
|
578
|
+
// adapter and keeps saving there; exiting does not cancel it.)
|
|
579
|
+
startTransition(() => setIsEditing(false));
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
500
582
|
const attempt = ++saveAttempt.current;
|
|
501
583
|
savingAttempt.current = attempt;
|
|
502
584
|
setSaveState({ pageKey, status: 'saving' });
|
|
@@ -515,7 +597,7 @@ itemChrome = false, addEntityWidgetEventNames = DEFAULT_ADD_ENTITY_WIDGET_EVENT_
|
|
|
515
597
|
if (savingAttempt.current === attempt)
|
|
516
598
|
savingAttempt.current = null;
|
|
517
599
|
}
|
|
518
|
-
}, [canEditPage, isEditing, pageKey, flushPreferences, flushLayerNames, flushHiddenLayers, flushCardStyle, setIsEditing]);
|
|
600
|
+
}, [canEditPage, isEditing, editingPaused, pageKey, flushPreferences, flushLayerNames, flushHiddenLayers, flushCardStyle, setIsEditing]);
|
|
519
601
|
useEffect(() => {
|
|
520
602
|
const onSaveRequest = (event) => {
|
|
521
603
|
const detail = event.detail;
|
|
@@ -670,10 +752,17 @@ itemChrome = false, addEntityWidgetEventNames = DEFAULT_ADD_ENTITY_WIDGET_EVENT_
|
|
|
670
752
|
}
|
|
671
753
|
return filtered;
|
|
672
754
|
}, [autoHeightRows, currentLayouts, hiddenLayerIds, renderableWidgetKeys]);
|
|
755
|
+
// Every write below is refused while editing is paused (bsuite#3277) — the
|
|
756
|
+
// controls that reach them are disabled too, but a disabled control is a
|
|
757
|
+
// presentation; the refusal is the guarantee.
|
|
673
758
|
const hideLayer = (layerId) => {
|
|
759
|
+
if (editingPaused)
|
|
760
|
+
return;
|
|
674
761
|
setHiddenLayerIds((previous) => ({ ...previous, [layerId]: true }));
|
|
675
762
|
};
|
|
676
763
|
const showLayer = (layerId, defaultSize) => {
|
|
764
|
+
if (editingPaused)
|
|
765
|
+
return;
|
|
677
766
|
setHiddenLayerIds((previous) => {
|
|
678
767
|
if (!previous[layerId])
|
|
679
768
|
return previous;
|
|
@@ -692,6 +781,12 @@ itemChrome = false, addEntityWidgetEventNames = DEFAULT_ADD_ENTITY_WIDGET_EVENT_
|
|
|
692
781
|
const detail = event.detail;
|
|
693
782
|
if (!detail?.entityType)
|
|
694
783
|
return;
|
|
784
|
+
if (editingPaused) {
|
|
785
|
+
// Nothing is registered or added while paused; the user lands in the
|
|
786
|
+
// editor, where the reason and Retry are shown.
|
|
787
|
+
startTransition(() => setIsEditing(true));
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
695
790
|
const widgetId = `entity:${detail.entityType}`;
|
|
696
791
|
const alreadyInLayout = activeLayouts.lg?.some((item) => item.i === widgetId);
|
|
697
792
|
if (alreadyInLayout) {
|
|
@@ -719,6 +814,7 @@ itemChrome = false, addEntityWidgetEventNames = DEFAULT_ADD_ENTITY_WIDGET_EVENT_
|
|
|
719
814
|
addEntityWidgetEventNames,
|
|
720
815
|
addWidget,
|
|
721
816
|
createEntityWidget,
|
|
817
|
+
editingPaused,
|
|
722
818
|
onRegisterEntityWidget,
|
|
723
819
|
setIsEditing,
|
|
724
820
|
]);
|
|
@@ -737,6 +833,10 @@ itemChrome = false, addEntityWidgetEventNames = DEFAULT_ADD_ENTITY_WIDGET_EVENT_
|
|
|
737
833
|
const detail = event.detail;
|
|
738
834
|
if (!detail?.hostEntityType || !detail.fkColumn || !detail.targetEntityType)
|
|
739
835
|
return;
|
|
836
|
+
if (editingPaused) {
|
|
837
|
+
startTransition(() => setIsEditing(true));
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
740
840
|
if (!isRelationshipWritable(relationshipCatalog, detail)) {
|
|
741
841
|
onRelationshipWidgetRejected?.(detail);
|
|
742
842
|
return;
|
|
@@ -768,6 +868,7 @@ itemChrome = false, addEntityWidgetEventNames = DEFAULT_ADD_ENTITY_WIDGET_EVENT_
|
|
|
768
868
|
addRelationshipWidgetEventNames,
|
|
769
869
|
addWidget,
|
|
770
870
|
createRelationshipWidget,
|
|
871
|
+
editingPaused,
|
|
771
872
|
onRegisterRelationshipWidget,
|
|
772
873
|
onRelationshipWidgetRejected,
|
|
773
874
|
relationshipCatalog,
|
|
@@ -786,6 +887,8 @@ itemChrome = false, addEntityWidgetEventNames = DEFAULT_ADD_ENTITY_WIDGET_EVENT_
|
|
|
786
887
|
};
|
|
787
888
|
}), [activeLayouts.lg, layerNames, widgetMeta]);
|
|
788
889
|
const handleLayerRename = (layerId, value) => {
|
|
890
|
+
if (editingPaused)
|
|
891
|
+
return;
|
|
789
892
|
const trimmed = value.trim();
|
|
790
893
|
setLayerNames((previous) => {
|
|
791
894
|
if (trimmed.length === 0) {
|
|
@@ -882,13 +985,18 @@ itemChrome = false, addEntityWidgetEventNames = DEFAULT_ADD_ENTITY_WIDGET_EVENT_
|
|
|
882
985
|
// partially visible — mitigates Issue 2 (banner consuming
|
|
883
986
|
// vertical space). `bg-card/95` + `backdrop-blur` keeps text
|
|
884
987
|
// legible while showing form context behind the banner.
|
|
885
|
-
'bg-card/95 supports-[backdrop-filter]:bg-card/80 backdrop-blur border-primary'), role: "region", "aria-label": "Canvas editor controls", "aria-busy": isSaving, children: [_jsxs("div", { className: "flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: cn('rounded-full flex items-center justify-center bg-primary/10', controlsCollapsed ? 'h-8 w-8' : 'h-10 w-10 animate-pulse'), children: _jsx(Settings2, { className: cn('text-primary', controlsCollapsed ? 'h-4 w-4' : 'h-5 w-5') }) }), _jsxs("div", { children: [_jsx("h3", { className: cn('font-semibold text-foreground', controlsCollapsed ? 'text-sm' : 'text-lg'), children: "Canvas Editor Active" }), !controlsCollapsed && (_jsx("span", { className: "text-sm text-muted-foreground", children: "Drag anywhere on a card to move it. Resize with the bottom-right handle." }))] })] }), _jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [_jsxs("button", { type: "button", className: "inline-flex items-center gap-1 rounded-md border border-border-interactive px-2 py-2 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1", "aria-expanded": !controlsCollapsed, "aria-controls": "page-grid-editor-controls-body", "aria-label": controlsCollapsed ? 'Expand controls: columns, cards and layers' : 'Collapse controls', onClick: () => setControlsCollapsed((previous) => !previous), children: [controlsCollapsed ? _jsx(ChevronDown, { className: "h-4 w-4" }) : _jsx(ChevronUp, { className: "h-4 w-4" }), _jsx("span", { className: "sr-only sm:not-sr-only", children: controlsCollapsed ? 'Columns, cards & layers' : 'Collapse' })] }), _jsxs("button", { type: "button", className: "inline-flex items-center rounded-md px-3 py-2 text-sm font-medium bg-primary text-primary-foreground shadow transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1", disabled: isSaving, onClick: () => { void saveAndExit(); }, children: [_jsx(
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
988
|
+
'bg-card/95 supports-[backdrop-filter]:bg-card/80 backdrop-blur border-primary'), role: "region", "aria-label": "Canvas editor controls", "aria-busy": isSaving, children: [_jsxs("div", { className: "flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: cn('rounded-full flex items-center justify-center bg-primary/10', controlsCollapsed ? 'h-8 w-8' : 'h-10 w-10 animate-pulse'), children: _jsx(Settings2, { className: cn('text-primary', controlsCollapsed ? 'h-4 w-4' : 'h-5 w-5') }) }), _jsxs("div", { children: [_jsx("h3", { className: cn('font-semibold text-foreground', controlsCollapsed ? 'text-sm' : 'text-lg'), children: "Canvas Editor Active" }), !controlsCollapsed && !editingPaused && (_jsx("span", { className: "text-sm text-muted-foreground", children: "Drag anywhere on a card to move it. Resize with the bottom-right handle." }))] })] }), _jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [_jsxs("button", { type: "button", className: "inline-flex items-center gap-1 rounded-md border border-border-interactive px-2 py-2 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1", "aria-expanded": !controlsCollapsed, "aria-controls": "page-grid-editor-controls-body", "aria-label": controlsCollapsed ? 'Expand controls: columns, cards and layers' : 'Collapse controls', onClick: () => setControlsCollapsed((previous) => !previous), children: [controlsCollapsed ? _jsx(ChevronDown, { className: "h-4 w-4" }) : _jsx(ChevronUp, { className: "h-4 w-4" }), _jsx("span", { className: "sr-only sm:not-sr-only", children: controlsCollapsed ? 'Columns, cards & layers' : 'Collapse' })] }), _jsxs("button", { type: "button", className: "inline-flex items-center rounded-md px-3 py-2 text-sm font-medium bg-primary text-primary-foreground shadow transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1", disabled: isSaving, onClick: () => { void saveAndExit(); }, children: [editingPaused ? _jsx(X, { className: "h-4 w-4 mr-2" }) : _jsx(Save, { className: "h-4 w-4 mr-2" }), isSaving ? 'Saving…' : editingPaused ? 'Exit' : 'Save & Exit'] })] })] }), editingPaused && (_jsxs("div", { role: editingPreferencesStatus === 'failed' ? 'alert' : 'status', "data-page-grid-editing-paused": editingPreferencesStatus, className: cn('flex flex-wrap items-center gap-2 text-sm', editingPreferencesStatus === 'failed' ? 'text-destructive' : 'text-muted-foreground'), children: [_jsx("p", { children: editingPreferencesStatus === 'failed'
|
|
989
|
+
? `Editing is paused: your saved settings for this page ${retriedAndStillFailed ? 'still couldn\'t' : 'couldn\'t'} be loaded. Nothing can be changed until they load, so nothing you saved is overwritten.${canRetryEditingPreferences ? '' : ' Reload the page to try again.'}`
|
|
990
|
+
: 'Loading your saved settings for this page… Editing starts as soon as they arrive.' }), editingPreferencesStatus === 'failed' && canRetryEditingPreferences && (_jsxs("button", { type: "button", onClick: retryEditingPreferences, className: "inline-flex items-center gap-1 rounded-md border border-border-interactive px-2 py-1 text-sm text-foreground transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1", children: [_jsx(RefreshCw, { className: "h-3.5 w-3.5", "aria-hidden": "true" }), "Retry"] }))] })), saveFailed && (_jsx("p", { role: "alert", className: "mt-2 text-sm text-destructive", children: "Changes could not be saved. Your changes are still here. Try Save & Exit again." })), _jsx("div", { id: "page-grid-editor-controls-body", hidden: controlsCollapsed,
|
|
991
|
+
// Dimmed while paused so the disabled state reads at a glance; inline
|
|
992
|
+
// rather than a utility so no consumer's CSS build can drop it.
|
|
993
|
+
style: editingPaused ? { opacity: 0.6 } : undefined, className: "flex flex-wrap items-center gap-x-4 gap-y-2 pt-2 border-t border-border max-h-[60vh] overflow-y-auto", children: _jsxs("fieldset", { disabled: editingPaused, style: { display: 'contents' }, children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(LayoutGrid, { className: "h-4 w-4 shrink-0 text-muted-foreground" }), _jsx("span", { className: "text-sm shrink-0 text-muted-foreground", children: "Columns:" }), _jsx("input", { type: "range", min: 1, max: 24, value: layoutCols, onChange: (event) => handleColumnChange(Number(event.target.value)), className: "w-24 cursor-pointer accent-primary", "aria-label": "Column count" }), _jsx("span", { className: "text-sm font-mono w-5 text-center tabular-nums", children: layoutCols })] }), _jsx("div", { className: "flex items-center gap-1", role: "group", "aria-label": "Column presets", children: [1, 2, 3, 4, 6, 12].map((columnCount) => (_jsx("button", { type: "button", onClick: () => handleColumnChange(columnCount), "data-active": layoutCols === columnCount || undefined, "aria-pressed": layoutCols === columnCount, className: cn('px-2 py-0.5 rounded text-xs font-medium transition-colors border', 'bg-muted text-muted-foreground border-border-interactive', 'hover:bg-muted/80 hover:text-foreground', 'data-[active]:bg-primary data-[active]:text-primary-foreground data-[active]:border-primary', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1'), children: columnCount }, columnCount))) }), _jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-2 w-full pt-2 border-t border-border", role: "group", "aria-label": "Card appearance", children: [_jsx("span", { className: "text-sm shrink-0 font-medium text-foreground", children: "Card:" }), _jsxs("label", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "text-sm text-muted-foreground", children: "Corners" }), _jsx("input", { type: "range", min: RADIUS_RANGE.min, max: RADIUS_RANGE.max, value: cardStyle.radius, onChange: (event) => updateCardStyle({ radius: Number(event.target.value) }), className: "w-20 cursor-pointer accent-primary", "aria-label": "Card corner radius in pixels" }), _jsx("span", { className: "text-sm font-mono w-8 text-right tabular-nums", children: cardStyle.radius })] }), _jsxs("label", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "text-sm text-muted-foreground", children: "Border" }), _jsx("input", { type: "range", min: BORDER_WIDTH_RANGE.min, max: BORDER_WIDTH_RANGE.max, value: cardStyle.borderWidth, onChange: (event) => updateCardStyle({ borderWidth: Number(event.target.value) }), className: "w-16 cursor-pointer accent-primary", "aria-label": "Card border width in pixels" }), _jsx("span", { className: "text-sm font-mono w-6 text-right tabular-nums", children: cardStyle.borderWidth })] }), _jsxs("label", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "sr-only", children: "Border colour" }), _jsx("select", { value: cardStyle.borderTone, onChange: (event) => updateCardStyle({ borderTone: event.target.value }), className: "rounded-md border border-border-interactive bg-background px-2 py-1 text-sm text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", "aria-label": "Card border colour", children: BORDER_TONES.map((tone) => (_jsx("option", { value: tone, children: tone === 'border' ? 'default colour' : tone }, tone))) })] }), _jsxs("label", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "sr-only", children: "Border style" }), _jsx("select", { value: cardStyle.borderStyle, onChange: (event) => updateCardStyle({ borderStyle: event.target.value }), className: "rounded-md border border-border-interactive bg-background px-2 py-1 text-sm text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", "aria-label": "Card border style", children: BORDER_STYLES.map((style) => (_jsx("option", { value: style, children: style }, style))) })] }), _jsxs("label", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "sr-only", children: "Card shadow" }), _jsxs("select", { value: cardStyle.elevation === null ? 'default' : String(cardStyle.elevation), onChange: (event) => updateCardStyle({
|
|
994
|
+
elevation: event.target.value === 'default' ? null : Number(event.target.value),
|
|
995
|
+
}), className: "rounded-md border border-border-interactive bg-background px-2 py-1 text-sm text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", "aria-label": "Card shadow depth", children: [_jsx("option", { value: "default", children: "default shadow" }), [0, 1, 2, 3, 4].map((level) => (_jsx("option", { value: level, children: level === 0 ? 'no shadow' : `shadow ${level}` }, level)))] })] }), _jsxs("label", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "text-sm text-muted-foreground", children: "Padding" }), _jsx("input", { type: "range", min: PADDING_RANGE.min, max: PADDING_RANGE.max, value: cardStyle.padding ?? 0, onChange: (event) => updateCardStyle({ padding: Number(event.target.value) }), className: "w-16 cursor-pointer accent-primary", "aria-label": "Card inner padding in pixels" }), _jsx("span", { className: "text-sm font-mono w-8 text-right tabular-nums", children: cardStyle.padding === null ? '\u2014' : cardStyle.padding })] }), _jsxs("button", { type: "button", onClick: resetCardStyle, disabled: isDefaultCardStyle(cardStyle), className: "inline-flex items-center gap-1 rounded-md border border-border-interactive px-2 py-1 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-50 disabled:pointer-events-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: [_jsx(RotateCcw, { className: "h-3.5 w-3.5" }), "Reset cards"] }), _jsx("output", { className: "text-sm text-muted-foreground", "aria-live": "polite", children: describeCardStyle(cardStyle) })] }), hiddenWidgetKeys.length > 0 && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Eye, { className: "h-4 w-4 shrink-0 text-muted-foreground" }), _jsx("span", { className: "text-sm shrink-0 text-muted-foreground", children: "Add widget:" }), _jsx("div", { className: "flex flex-wrap gap-1", children: hiddenWidgetKeys.map((key) => {
|
|
996
|
+
const meta = widgetMeta?.[key];
|
|
997
|
+
const Icon = meta?.icon;
|
|
998
|
+
return (_jsxs("button", { type: "button", onClick: () => showLayer(key, meta?.defaultSize), className: cn('flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium border transition-colors', 'bg-muted text-muted-foreground border-border-interactive', 'hover:bg-muted/80 hover:text-foreground hover:border-primary/60', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1'), children: [_jsx(Plus, { className: "h-3 w-3" }), Icon && _jsx(Icon, { className: "h-3 w-3" }), layerNames[key] || meta?.label || key] }, key));
|
|
999
|
+
}) })] })), layerItems.length > 0 && (_jsxs("div", { className: "basis-full rounded-md border border-border bg-background/80 p-2", children: [_jsxs("div", { className: "mb-2 flex items-center gap-2 text-xs font-medium uppercase tracking-wide text-muted-foreground", children: [_jsx(Layers, { className: "h-3.5 w-3.5" }), "Layers"] }), _jsx("div", { className: "flex flex-col gap-1", children: layerItems.map((layer) => (_jsxs("div", { className: "flex items-center gap-1 rounded border border-border bg-card px-2 py-1", children: [_jsx("input", { type: "text", value: layer.label, onChange: (event) => handleLayerRename(layer.id, event.target.value), className: "min-w-0 flex-1 bg-transparent text-xs text-foreground outline-none", "aria-label": `Rename ${layer.label}` }), _jsx("button", { type: "button", onClick: () => moveWidget(layer.id, 'up'), disabled: !layer.canMoveUp, "aria-label": `Move ${layer.label} up`, className: "rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-40", children: _jsx(ArrowUp, { className: "h-3.5 w-3.5" }) }), _jsx("button", { type: "button", onClick: () => moveWidget(layer.id, 'down'), disabled: !layer.canMoveDown, "aria-label": `Move ${layer.label} down`, className: "rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-40", children: _jsx(ArrowDown, { className: "h-3.5 w-3.5" }) }), _jsx("button", { type: "button", onClick: () => setWidgetLocked(layer.id, !layer.locked), "aria-label": `${layer.locked ? 'Unlock' : 'Lock'} ${layer.label}`, className: "rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground", children: layer.locked ? _jsx(Lock, { className: "h-3.5 w-3.5" }) : _jsx(Unlock, { className: "h-3.5 w-3.5" }) }), _jsx("button", { type: "button", onClick: () => hideLayer(layer.id), "aria-label": `Hide ${layer.label}`, className: "rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground", children: _jsx(EyeOff, { className: "h-3.5 w-3.5" }) })] }, layer.id))) })] })), _jsxs("button", { type: "button", onClick: handleCompact, className: cn('ml-auto inline-flex items-center rounded-md border border-border-interactive px-2 py-1 text-sm transition-colors', 'text-muted-foreground hover:bg-muted hover:text-foreground', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1'), children: [_jsx(Layers, { className: "h-4 w-4 mr-1" }), "Compact Layout"] }), _jsxs("button", { type: "button", onClick: () => setResetConfirmOpen(true), className: cn('inline-flex items-center rounded-md border border-border-interactive px-2 py-1 text-sm transition-colors', 'text-muted-foreground hover:bg-muted hover:text-foreground', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1'), children: [_jsx(RotateCcw, { className: "h-4 w-4 mr-1" }), "Reset to Default"] })] }) })] })), resetConfirmOpen && (_jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-overlay/50 p-4", role: "dialog", "aria-modal": "true", "aria-labelledby": "page-grid-reset-title", "aria-describedby": "page-grid-reset-description", children: _jsxs("div", { className: "max-w-md rounded-lg bg-card p-5 shadow-lg border border-border", children: [_jsx("h2", { id: "page-grid-reset-title", className: "text-lg font-semibold", children: "Reset page layout?" }), _jsx("p", { id: "page-grid-reset-description", className: "mt-2 text-sm text-muted-foreground", children: "This will discard your current layout and restore all widgets to their default positions." }), _jsxs("div", { className: "mt-4 flex justify-end gap-2", children: [_jsx("button", { type: "button", ref: resetCancelButtonRef, onClick: () => setResetConfirmOpen(false), className: cn('inline-flex items-center rounded-md border border-border-interactive px-3 py-2 text-sm transition-colors', 'bg-card text-foreground hover:bg-muted hover:text-foreground', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1'), children: "Cancel" }), _jsx("button", { type: "button", ref: resetConfirmButtonRef, onClick: () => {
|
|
892
1000
|
handleReset();
|
|
893
1001
|
setResetConfirmOpen(false);
|
|
894
1002
|
}, className: cn('inline-flex items-center rounded-md px-3 py-2 text-sm font-medium transition-colors', 'bg-destructive text-destructive-foreground shadow hover:bg-destructive/90', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-destructive/40 focus-visible:ring-offset-1'), children: "Reset to Default" })] })] }) })), _jsx("div", { ref: containerRef, className: cn('relative z-0 overflow-visible', isEditing && 'min-h-[200px] bg-muted/30'), "data-page-grid-editing": isEditing || undefined, children: _jsx("div", { className: "relative z-0", style: { opacity: containerWidth > 0 ? 1 : 0 }, "aria-busy": containerWidth <= 0, children: _jsx(Responsive, { width: Math.max(containerWidth, 1), className: "layout page-grid-canvas", layouts: activeLayouts, breakpoints: { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }, rowHeight: DEFAULT_ROW_HEIGHT, onLayoutChange: (layout, layouts) => {
|
|
@@ -900,7 +1008,7 @@ itemChrome = false, addEntityWidgetEventNames = DEFAULT_ADD_ENTITY_WIDGET_EVENT_
|
|
|
900
1008
|
// is folded back onto `lg` instead of into a derived breakpoint
|
|
901
1009
|
// that is regenerated (and therefore discarded) on the next render.
|
|
902
1010
|
onBreakpointChange: handleBreakpointChange, dragConfig: {
|
|
903
|
-
enabled: isEditing,
|
|
1011
|
+
enabled: isEditing && !editingPaused,
|
|
904
1012
|
handle: '.drag-handle',
|
|
905
1013
|
bounded: false,
|
|
906
1014
|
cancel: '.react-resizable-handle, button, input, textarea, select, [contenteditable="true"], [data-no-drag], a[href], [role="button"], [role="combobox"], [role="menuitem"], [role="tab"], [role="checkbox"], [role="switch"], [role="slider"], [role="textbox"]',
|
|
@@ -918,6 +1026,6 @@ itemChrome = false, addEntityWidgetEventNames = DEFAULT_ADD_ENTITY_WIDGET_EVENT_
|
|
|
918
1026
|
const content = allWidgets[layoutItem.i];
|
|
919
1027
|
if (content === undefined || content === null)
|
|
920
1028
|
return null;
|
|
921
|
-
return (_jsx(GridItem, { id: layoutItem.i, content: content, isEditing: isEditing, label: layerNames[layoutItem.i] || widgetMeta?.[layoutItem.i]?.label || layoutItem.i, pageKey: pageKey, onHide: hideLayer, autoHeight: layoutItem.autoHeight, onAutoHeightChange: handleAutoHeightChange, chrome: layoutItem.chrome ?? itemChrome, cardStyleVars: cardStyleVars }, layoutItem.i));
|
|
1029
|
+
return (_jsx(GridItem, { id: layoutItem.i, content: content, isEditing: isEditing, label: layerNames[layoutItem.i] || widgetMeta?.[layoutItem.i]?.label || layoutItem.i, pageKey: pageKey, onHide: hideLayer, editingPaused: editingPaused, autoHeight: layoutItem.autoHeight, onAutoHeightChange: handleAutoHeightChange, chrome: layoutItem.chrome ?? itemChrome, cardStyleVars: cardStyleVars }, layoutItem.i));
|
|
922
1030
|
}) }) }) })] }));
|
|
923
1031
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -64,10 +64,52 @@ export interface GridLayouts {
|
|
|
64
64
|
* reference widgets that no longer exist.
|
|
65
65
|
*/
|
|
66
66
|
export type LayoutMigration = (saved: GridLayouts, defaults: GridLayouts) => GridLayouts;
|
|
67
|
+
/**
|
|
68
|
+
* Where the READ of one preference key stands (bsuite#3277).
|
|
69
|
+
*
|
|
70
|
+
* - `'loading'` — the stored value has not arrived yet. `value` is a local
|
|
71
|
+
* mirror or the fallback.
|
|
72
|
+
* - `'loaded'` — `value` is what is stored for this key, or nothing is stored
|
|
73
|
+
* and `value` is the fallback. Only this state authorises a write.
|
|
74
|
+
* - `'failed'` — the read errored. `value` is a local mirror or the fallback
|
|
75
|
+
* and says NOTHING about what is stored.
|
|
76
|
+
*
|
|
77
|
+
* A failed read is not an empty read. On a device with no local copy, crm7 and
|
|
78
|
+
* business-suite-unified reported a failed `_grid_version` read as "loaded,
|
|
79
|
+
* version 0"; the version gate then took its discard branch and wrote the
|
|
80
|
+
* defaults over the user's saved layout — 200 of 200 pages in the measurement.
|
|
81
|
+
*/
|
|
82
|
+
export type PageGridPreferenceStatus = 'loading' | 'loaded' | 'failed';
|
|
67
83
|
export interface PageGridPreferenceAdapter<T> {
|
|
68
84
|
value: T;
|
|
69
85
|
setValue: (value: T | ((previous: T) => T)) => void;
|
|
86
|
+
/**
|
|
87
|
+
* The two-state read signal every adapter has always reported. An adapter
|
|
88
|
+
* that implements `status` must keep this equal to `status === 'loaded'` —
|
|
89
|
+
* never true after a failed read.
|
|
90
|
+
*
|
|
91
|
+
* An adapter that does NOT implement `status` keeps exactly the meaning this
|
|
92
|
+
* had before: `true` is `'loaded'`, `false` is `'loading'`.
|
|
93
|
+
*/
|
|
70
94
|
loaded: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* The three-state read signal. When present it is AUTHORITATIVE over
|
|
97
|
+
* `loaded`: `usePageGridLayout` treats a key as readable only when `status`
|
|
98
|
+
* is `'loaded'` AND `loaded` is true, so an adapter that adds `status` beside
|
|
99
|
+
* an older "the read settled" `loaded` still cannot authorise a write after a
|
|
100
|
+
* failure.
|
|
101
|
+
*
|
|
102
|
+
* Once a key has loaded, a later background re-read that fails must NOT move
|
|
103
|
+
* it back to `'failed'` — the value it holds is still the stored one.
|
|
104
|
+
*/
|
|
105
|
+
status?: PageGridPreferenceStatus;
|
|
106
|
+
/**
|
|
107
|
+
* Re-read THIS key after a failed read, in place: no remount, no reset of any
|
|
108
|
+
* other key, and no write. The adapter moves the key to `'loading'` and then
|
|
109
|
+
* to `'loaded'` or back to `'failed'`. Omit it only if the adapter genuinely
|
|
110
|
+
* cannot read again without a reload.
|
|
111
|
+
*/
|
|
112
|
+
retry?: () => void;
|
|
71
113
|
/** Wait for pending writes and reject if the latest change was not saved.
|
|
72
114
|
* A subsequent explicit call may retry a failed write without losing the draft.
|
|
73
115
|
* Synchronous adapters may omit this; remote adapters should implement it. */
|
|
@@ -258,6 +300,22 @@ export interface UsePageGridLayoutResult {
|
|
|
258
300
|
isEditing: boolean;
|
|
259
301
|
setIsEditing: React.Dispatch<React.SetStateAction<boolean>>;
|
|
260
302
|
flushPreferences: () => Promise<void>;
|
|
303
|
+
/**
|
|
304
|
+
* The read state of every preference key this hook reads OR writes
|
|
305
|
+
* (`_grid_version`, `_grid_layouts`, `_grid_cols`, `_grid_base_cols`),
|
|
306
|
+
* combined: `'failed'` if any failed, else `'loading'` if any is still
|
|
307
|
+
* loading, else `'loaded'`. Until it is `'loaded'` the hook runs no version
|
|
308
|
+
* gate, no migration, no discard and no write of any kind (bsuite#3277).
|
|
309
|
+
*/
|
|
310
|
+
preferencesStatus: PageGridPreferenceStatus;
|
|
311
|
+
/** True when at least one failed key's adapter offers `retry`. */
|
|
312
|
+
canRetryPreferences: boolean;
|
|
313
|
+
/**
|
|
314
|
+
* Re-read ONLY the keys whose read failed, via each adapter's own `retry`.
|
|
315
|
+
* Nothing remounts: editing state, the draft and every loaded key are kept,
|
|
316
|
+
* and the gate simply runs once every key has loaded.
|
|
317
|
+
*/
|
|
318
|
+
retryPreferences: () => void;
|
|
261
319
|
activeCols: Record<string, number>;
|
|
262
320
|
activeCompactor: Compactor;
|
|
263
321
|
onLayoutChange: (_layout: unknown, layouts: unknown, wasGesture?: boolean) => void;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,23 @@
|
|
|
1
|
-
import type { GridLayouts, LayoutMigration, UsePageGridLayoutOptions, UsePageGridLayoutResult } from './types.js';
|
|
1
|
+
import type { GridLayouts, LayoutMigration, PageGridPreferenceAdapter, PageGridPreferenceStatus, UsePageGridLayoutOptions, UsePageGridLayoutResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* One key's read state, resolved from what its adapter reports (bsuite#3277).
|
|
4
|
+
*
|
|
5
|
+
* `status` is authoritative when present, and a key counts as readable only if
|
|
6
|
+
* `status` is `'loaded'` AND `loaded` is true. The conjunction is the point: the
|
|
7
|
+
* first adapter a consumer ships will add `status` beside its old "the read
|
|
8
|
+
* settled" `loaded`, and that `loaded` is true after an error. Anything that is
|
|
9
|
+
* not positively loaded — including a `status` value this version does not know —
|
|
10
|
+
* resolves to not-loaded, so an unrecognised state fails closed.
|
|
11
|
+
*
|
|
12
|
+
* An adapter without `status` keeps the meaning `loaded` always had.
|
|
13
|
+
*/
|
|
14
|
+
export declare function preferenceStatusOf(adapter: Pick<PageGridPreferenceAdapter<unknown>, 'loaded' | 'status'>): PageGridPreferenceStatus;
|
|
15
|
+
/**
|
|
16
|
+
* Several keys' read states as one: a failure outranks a key still loading,
|
|
17
|
+
* because the failure is what the user can act on (Retry) and a still-loading
|
|
18
|
+
* key must not hide it.
|
|
19
|
+
*/
|
|
20
|
+
export declare function combinePreferenceStatuses(statuses: readonly PageGridPreferenceStatus[]): PageGridPreferenceStatus;
|
|
2
21
|
export declare const DEFAULT_EDITOR_EVENT_NAMES: readonly ["bsuite-open-page-editor", "bsu-open-page-editor", "crm7-open-page-editor", "conduit-open-page-editor", "r80-open-page-editor"];
|
|
3
22
|
/**
|
|
4
23
|
* Broadcast after a PageGridLayout commits an editing-state change. Header
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usePageGridLayout.d.ts","sourceRoot":"","sources":["../src/usePageGridLayout.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAEV,WAAW,EACX,eAAe,EACf,wBAAwB,EACxB,uBAAuB,EACxB,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,0BAA0B,2IAM7B,CAAC;AAEX;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,6BAA6B,CAAC;AAElE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,oBAAoB,OAAO,CAAC;AAiBzC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,KAAK,EACL,QAAQ,EACR,IAAI,EACJ,EAAE,EACF,UAAU,GACX,EAAE;IACD,KAAK,EAAE,WAAW,GAAG,SAAS,CAAC;IAC/B,QAAQ,EAAE,WAAW,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,GAAG,IAAI,CAAC;CACzD,GAAG,WAAW,GAAG,IAAI,CA+BrB;AAED,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,iBAAiB,CAAC,EAChC,OAAO,EACP,cAAc,EACd,WAAgB,EAChB,aAAiB,EACjB,WAAkB,EAClB,gBAA6C,EAC7C,iBAA4C,EAC5C,iBAAiB,EACjB,gBAAgB,GACjB,EAAE,wBAAwB,GAAG,uBAAuB,
|
|
1
|
+
{"version":3,"file":"usePageGridLayout.d.ts","sourceRoot":"","sources":["../src/usePageGridLayout.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAEV,WAAW,EACX,eAAe,EACf,yBAAyB,EACzB,wBAAwB,EACxB,wBAAwB,EACxB,uBAAuB,EACxB,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,IAAI,CAAC,yBAAyB,CAAC,OAAO,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC,GACrE,wBAAwB,CAI1B;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,SAAS,wBAAwB,EAAE,GAC5C,wBAAwB,CAI1B;AAED,eAAO,MAAM,0BAA0B,2IAM7B,CAAC;AAEX;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,6BAA6B,CAAC;AAElE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,oBAAoB,OAAO,CAAC;AAiBzC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,KAAK,EACL,QAAQ,EACR,IAAI,EACJ,EAAE,EACF,UAAU,GACX,EAAE;IACD,KAAK,EAAE,WAAW,GAAG,SAAS,CAAC;IAC/B,QAAQ,EAAE,WAAW,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,GAAG,IAAI,CAAC;CACzD,GAAG,WAAW,GAAG,IAAI,CA+BrB;AAED,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,iBAAiB,CAAC,EAChC,OAAO,EACP,cAAc,EACd,WAAgB,EAChB,aAAiB,EACjB,WAAkB,EAClB,gBAA6C,EAC7C,iBAA4C,EAC5C,iBAAiB,EACjB,gBAAgB,GACjB,EAAE,wBAAwB,GAAG,uBAAuB,CAg3BpD"}
|
|
@@ -3,6 +3,37 @@ import { verticalCompactor } from 'react-grid-layout';
|
|
|
3
3
|
import { buildResponsiveLayouts, isCanonicalisableBreakpoint, isDerivedBreakpoint, } from './buildResponsiveLayouts.js';
|
|
4
4
|
import { defaultPreferenceAdapter } from './preferences.js';
|
|
5
5
|
import { rescaleLayout } from './rescaleLayout.js';
|
|
6
|
+
/**
|
|
7
|
+
* One key's read state, resolved from what its adapter reports (bsuite#3277).
|
|
8
|
+
*
|
|
9
|
+
* `status` is authoritative when present, and a key counts as readable only if
|
|
10
|
+
* `status` is `'loaded'` AND `loaded` is true. The conjunction is the point: the
|
|
11
|
+
* first adapter a consumer ships will add `status` beside its old "the read
|
|
12
|
+
* settled" `loaded`, and that `loaded` is true after an error. Anything that is
|
|
13
|
+
* not positively loaded — including a `status` value this version does not know —
|
|
14
|
+
* resolves to not-loaded, so an unrecognised state fails closed.
|
|
15
|
+
*
|
|
16
|
+
* An adapter without `status` keeps the meaning `loaded` always had.
|
|
17
|
+
*/
|
|
18
|
+
export function preferenceStatusOf(adapter) {
|
|
19
|
+
if (adapter.status === 'failed')
|
|
20
|
+
return 'failed';
|
|
21
|
+
if (adapter.status === undefined)
|
|
22
|
+
return adapter.loaded ? 'loaded' : 'loading';
|
|
23
|
+
return adapter.status === 'loaded' && adapter.loaded ? 'loaded' : 'loading';
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Several keys' read states as one: a failure outranks a key still loading,
|
|
27
|
+
* because the failure is what the user can act on (Retry) and a still-loading
|
|
28
|
+
* key must not hide it.
|
|
29
|
+
*/
|
|
30
|
+
export function combinePreferenceStatuses(statuses) {
|
|
31
|
+
if (statuses.includes('failed'))
|
|
32
|
+
return 'failed';
|
|
33
|
+
if (statuses.includes('loading'))
|
|
34
|
+
return 'loading';
|
|
35
|
+
return 'loaded';
|
|
36
|
+
}
|
|
6
37
|
export const DEFAULT_EDITOR_EVENT_NAMES = [
|
|
7
38
|
'bsuite-open-page-editor',
|
|
8
39
|
'bsu-open-page-editor',
|
|
@@ -152,14 +183,70 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
|
|
|
152
183
|
window.addEventListener('resize', onResize);
|
|
153
184
|
return () => window.removeEventListener('resize', onResize);
|
|
154
185
|
}, []);
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
const
|
|
158
|
-
const
|
|
186
|
+
const versionPreference = preferenceAdapter(`page:${pageKey}_grid_version`, 0);
|
|
187
|
+
const layoutPreference = preferenceAdapter(`page:${pageKey}_grid_layouts`, defaultLayouts);
|
|
188
|
+
const colsPreference = preferenceAdapter(`page:${pageKey}_grid_cols`, defaultCols);
|
|
189
|
+
const baseColsPreference = preferenceAdapter(`page:${pageKey}_grid_base_cols`, defaultCols);
|
|
190
|
+
const { value: savedLayoutVersion, setValue: setSavedLayoutVersion, flush: flushVersion, retry: retryVersion } = versionPreference;
|
|
191
|
+
const { value: savedLayout, setValue: setSavedLayout, flush: flushLayout, retry: retryLayout } = layoutPreference;
|
|
192
|
+
const { value: savedLayoutCols, setValue: setLayoutCols, flush: flushCols, retry: retryCols } = colsPreference;
|
|
193
|
+
const { value: savedBaseCols, setValue: setBaseCols, flush: flushBaseCols, retry: retryBaseCols } = baseColsPreference;
|
|
159
194
|
const flushPreferences = useCallback(async () => {
|
|
160
195
|
await Promise.all([flushVersion?.(), flushLayout?.(), flushCols?.(), flushBaseCols?.()]);
|
|
161
196
|
}, [flushVersion, flushLayout, flushCols, flushBaseCols]);
|
|
162
|
-
|
|
197
|
+
/*
|
|
198
|
+
* NOTHING IS WRITTEN UNTIL EVERY KEY THIS HOOK TOUCHES HAS BEEN READ (bsuite#3277).
|
|
199
|
+
*
|
|
200
|
+
* The gate below used to wait for `_grid_layouts` and `_grid_version` only, and
|
|
201
|
+
* trusted `loaded` to mean "the stored value is here". crm7 and
|
|
202
|
+
* business-suite-unified set `loaded` on a read ERROR too, so on a device with
|
|
203
|
+
* no local copy a failed `_grid_version` read looked like "loaded, version 0" —
|
|
204
|
+
* the gate took its discard branch and wrote the page defaults over the user's
|
|
205
|
+
* saved layout. Measured with the real 2.7.0 hook: 200 of 200 pages.
|
|
206
|
+
*
|
|
207
|
+
* The rule now covers every key the hook READS or WRITES — `_grid_cols` and
|
|
208
|
+
* `_grid_base_cols` included, because a discard writes them — and every write
|
|
209
|
+
* the hook can make, not only the gate's: a gesture, compact, reset, add, move,
|
|
210
|
+
* lock, remove and a column change are all refused while any key is loading or
|
|
211
|
+
* failed. `PageGridLayout` shows the reason and a Retry while that holds.
|
|
212
|
+
*/
|
|
213
|
+
const versionStatus = preferenceStatusOf(versionPreference);
|
|
214
|
+
const layoutStatus = preferenceStatusOf(layoutPreference);
|
|
215
|
+
const colsStatus = preferenceStatusOf(colsPreference);
|
|
216
|
+
const baseColsStatus = preferenceStatusOf(baseColsPreference);
|
|
217
|
+
const preferencesStatus = combinePreferenceStatuses([versionStatus, layoutStatus, colsStatus, baseColsStatus]);
|
|
218
|
+
const preferencesWritable = preferencesStatus === 'loaded';
|
|
219
|
+
/*
|
|
220
|
+
* Retry is PER KEY and IN PLACE. Each adapter's own `retry` re-runs that one
|
|
221
|
+
* key's read — crm7 re-runs its fetch effect, business-suite-unified refetches
|
|
222
|
+
* that key's query, conduit re-reads that key's local copy. This hook, its
|
|
223
|
+
* editing state and every key that already loaded stay mounted; the gate above
|
|
224
|
+
* simply runs once the last failed key lands. A key that loaded is never
|
|
225
|
+
* re-read, so a retry cannot disturb a value that is already right.
|
|
226
|
+
*/
|
|
227
|
+
const canRetryPreferences = (versionStatus === 'failed' && typeof retryVersion === 'function') ||
|
|
228
|
+
(layoutStatus === 'failed' && typeof retryLayout === 'function') ||
|
|
229
|
+
(colsStatus === 'failed' && typeof retryCols === 'function') ||
|
|
230
|
+
(baseColsStatus === 'failed' && typeof retryBaseCols === 'function');
|
|
231
|
+
const retryPreferences = useCallback(() => {
|
|
232
|
+
if (versionStatus === 'failed')
|
|
233
|
+
retryVersion?.();
|
|
234
|
+
if (layoutStatus === 'failed')
|
|
235
|
+
retryLayout?.();
|
|
236
|
+
if (colsStatus === 'failed')
|
|
237
|
+
retryCols?.();
|
|
238
|
+
if (baseColsStatus === 'failed')
|
|
239
|
+
retryBaseCols?.();
|
|
240
|
+
}, [
|
|
241
|
+
baseColsStatus,
|
|
242
|
+
colsStatus,
|
|
243
|
+
layoutStatus,
|
|
244
|
+
retryBaseCols,
|
|
245
|
+
retryCols,
|
|
246
|
+
retryLayout,
|
|
247
|
+
retryVersion,
|
|
248
|
+
versionStatus,
|
|
249
|
+
]);
|
|
163
250
|
/*
|
|
164
251
|
* Migration keys arrive in the CONSUMER's version space — `layoutVersion={2}`
|
|
165
252
|
* pairs with `{ 2: … }` — and are shifted onto the stored (effective) space
|
|
@@ -199,7 +286,8 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
|
|
|
199
286
|
savedLayoutForVersionGateRef.current = savedLayout;
|
|
200
287
|
}, [savedLayout]);
|
|
201
288
|
useEffect(() => {
|
|
202
|
-
|
|
289
|
+
// Every key read AND written must be loaded without failure — see above.
|
|
290
|
+
if (!preferencesWritable)
|
|
203
291
|
return;
|
|
204
292
|
const storedVersion = savedLayoutVersion ?? 0;
|
|
205
293
|
if (storedVersion >= effectiveLayoutVersion)
|
|
@@ -245,7 +333,7 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
|
|
|
245
333
|
defaultLayouts,
|
|
246
334
|
effectiveLayoutVersion,
|
|
247
335
|
effectiveMigrations,
|
|
248
|
-
|
|
336
|
+
preferencesWritable,
|
|
249
337
|
savedLayoutVersion,
|
|
250
338
|
setBaseCols,
|
|
251
339
|
setLayoutCols,
|
|
@@ -590,12 +678,15 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
|
|
|
590
678
|
* to agree, so it is the one moment `baseCols` may move.
|
|
591
679
|
*/
|
|
592
680
|
const commitLayout = useCallback((layouts) => {
|
|
681
|
+
// bsuite#3277: no write while any key is loading or failed.
|
|
682
|
+
if (!preferencesWritable)
|
|
683
|
+
return;
|
|
593
684
|
const canonical = canonicaliseLayoutForPersist(layouts);
|
|
594
685
|
if (!canonical)
|
|
595
686
|
return;
|
|
596
687
|
setSavedLayout(stripAutoHeightRows(canonical));
|
|
597
688
|
setBaseCols(layoutCols);
|
|
598
|
-
}, [canonicaliseLayoutForPersist, layoutCols, setBaseCols, setSavedLayout, stripAutoHeightRows]);
|
|
689
|
+
}, [canonicaliseLayoutForPersist, layoutCols, preferencesWritable, setBaseCols, setSavedLayout, stripAutoHeightRows]);
|
|
599
690
|
const onLayoutChange = useCallback((_layout, layouts, wasGesture = true) => {
|
|
600
691
|
if (!isEditing)
|
|
601
692
|
return;
|
|
@@ -681,8 +772,11 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
|
|
|
681
772
|
* undo. Storage moves only when the user actually edits — see `commitLayout`.
|
|
682
773
|
*/
|
|
683
774
|
const handleColumnChange = useCallback((newCols) => {
|
|
775
|
+
// bsuite#3277: no write while any key is loading or failed.
|
|
776
|
+
if (!preferencesWritable)
|
|
777
|
+
return;
|
|
684
778
|
setLayoutCols(newCols);
|
|
685
|
-
}, [setLayoutCols]);
|
|
779
|
+
}, [preferencesWritable, setLayoutCols]);
|
|
686
780
|
/**
|
|
687
781
|
* Build the object handed to the preference adapter from a new `lg` array.
|
|
688
782
|
*
|
|
@@ -702,9 +796,12 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
|
|
|
702
796
|
}, [currentLayouts]);
|
|
703
797
|
/** Persist a bare `lg` item list from a mutator, re-basing the same way. */
|
|
704
798
|
const commitItems = useCallback((items) => {
|
|
799
|
+
// bsuite#3277: no write while any key is loading or failed.
|
|
800
|
+
if (!preferencesWritable)
|
|
801
|
+
return;
|
|
705
802
|
setSavedLayout(layoutsWithLg(items));
|
|
706
803
|
setBaseCols(layoutCols);
|
|
707
|
-
}, [layoutCols, layoutsWithLg, setBaseCols, setSavedLayout]);
|
|
804
|
+
}, [layoutCols, layoutsWithLg, preferencesWritable, setBaseCols, setSavedLayout]);
|
|
708
805
|
const handleCompact = useCallback(() => {
|
|
709
806
|
if (!currentLayouts.lg)
|
|
710
807
|
return;
|
|
@@ -724,12 +821,15 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
|
|
|
724
821
|
startTransition(() => commitItems(result));
|
|
725
822
|
}, [currentLayouts, commitItems]);
|
|
726
823
|
const handleReset = useCallback(() => {
|
|
824
|
+
// bsuite#3277: no write while any key is loading or failed.
|
|
825
|
+
if (!preferencesWritable)
|
|
826
|
+
return;
|
|
727
827
|
startTransition(() => {
|
|
728
828
|
setSavedLayout(defaultLayouts);
|
|
729
829
|
setLayoutCols(defaultCols);
|
|
730
830
|
setBaseCols(defaultCols);
|
|
731
831
|
});
|
|
732
|
-
}, [defaultCols, defaultLayouts, setBaseCols, setLayoutCols, setSavedLayout]);
|
|
832
|
+
}, [defaultCols, defaultLayouts, preferencesWritable, setBaseCols, setLayoutCols, setSavedLayout]);
|
|
733
833
|
const addWidget = useCallback((widgetKey, initialSize) => {
|
|
734
834
|
const maxY = (currentLayouts.lg ?? []).reduce((max, item) => Math.max(max, item.y + item.h), 0);
|
|
735
835
|
const newItem = {
|
|
@@ -742,7 +842,12 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
|
|
|
742
842
|
minH: initialSize?.minH,
|
|
743
843
|
};
|
|
744
844
|
startTransition(() => commitItems([...(currentLayouts.lg ?? []), newItem]));
|
|
745
|
-
},
|
|
845
|
+
},
|
|
846
|
+
// `commitItems` is the write funnel and carries the read-state refusal
|
|
847
|
+
// (bsuite#3277). Omit it and the mutator keeps the funnel from the render it
|
|
848
|
+
// was created in: paused editing never un-pauses for this control after a
|
|
849
|
+
// Retry, because retrying a key that changes no layout re-creates nothing.
|
|
850
|
+
[commitItems, currentLayouts, layoutCols]);
|
|
746
851
|
const moveWidget = useCallback((widgetKey, direction) => {
|
|
747
852
|
const items = [...(currentLayouts.lg ?? [])];
|
|
748
853
|
const index = items.findIndex((item) => item.i === widgetKey);
|
|
@@ -759,7 +864,7 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
|
|
|
759
864
|
...withoutMoved.slice(targetIndex),
|
|
760
865
|
];
|
|
761
866
|
startTransition(() => commitItems(nextItems));
|
|
762
|
-
}, [
|
|
867
|
+
}, [commitItems, currentLayouts]);
|
|
763
868
|
const setWidgetLocked = useCallback((widgetKey, locked) => {
|
|
764
869
|
const nextItems = (currentLayouts.lg ?? []).map((item) => {
|
|
765
870
|
if (item.i !== widgetKey)
|
|
@@ -772,11 +877,11 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
|
|
|
772
877
|
};
|
|
773
878
|
});
|
|
774
879
|
startTransition(() => commitItems(nextItems));
|
|
775
|
-
}, [
|
|
880
|
+
}, [commitItems, currentLayouts]);
|
|
776
881
|
const removeWidget = useCallback((widgetKey) => {
|
|
777
882
|
const nextItems = (currentLayouts.lg ?? []).filter((item) => item.i !== widgetKey);
|
|
778
883
|
startTransition(() => commitItems(nextItems));
|
|
779
|
-
}, [
|
|
884
|
+
}, [commitItems, currentLayouts]);
|
|
780
885
|
/**
|
|
781
886
|
* Measured auto-height overrides (widgetKey -> rows). DERIVED, in-memory-
|
|
782
887
|
* only state (quality-review design ruling, 2026-07-14):
|
|
@@ -850,6 +955,9 @@ export function usePageGridLayout({ pageKey, defaultLayouts, defaultCols = 12, l
|
|
|
850
955
|
isEditing,
|
|
851
956
|
setIsEditing,
|
|
852
957
|
flushPreferences,
|
|
958
|
+
preferencesStatus,
|
|
959
|
+
canRetryPreferences,
|
|
960
|
+
retryPreferences,
|
|
853
961
|
activeCols,
|
|
854
962
|
activeCompactor,
|
|
855
963
|
onLayoutChange,
|