@aglyn/shared-ui-jsx 1.0.0-beta.235 → 1.0.0-beta.237
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/package.json +5 -5
- package/src/lib/components/lightbox/lightbox-appearance.d.ts +137 -0
- package/src/lib/components/lightbox/lightbox-appearance.js +308 -0
- package/src/lib/components/lightbox/lightbox-appearance.js.map +1 -0
- package/src/lib/components/lightbox/lightbox-dialog.d.ts +78 -0
- package/src/lib/components/lightbox/lightbox-dialog.js +236 -0
- package/src/lib/components/lightbox/lightbox-dialog.js.map +1 -0
- package/src/lib/components/list-table.component.js +6 -2
- package/src/lib/components/list-table.component.js.map +1 -1
- package/src/lib/components/row-actions-menu.component.d.ts +6 -0
- package/src/lib/components/row-actions-menu.component.js +1 -0
- package/src/lib/components/row-actions-menu.component.js.map +1 -1
- package/src/lib/const/list-grid-filter.d.ts +12 -12
- package/src/lib/const/list-grid-filter.js +61 -12
- package/src/lib/const/list-grid-filter.js.map +1 -1
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* The one lightbox every element opens (AGL-3717).
|
|
18
|
+
*
|
|
19
|
+
* The Video element's dialog (AGL-2744) was the first and, until this, the
|
|
20
|
+
* only lightbox on a published page. Its shell — the MUI `Dialog`, the label
|
|
21
|
+
* on the paper, the close control, the reduced-motion rule — is lifted out
|
|
22
|
+
* here so the Image, the Image List's gallery and the Lightbox container open
|
|
23
|
+
* the SAME dialog rather than three that drift apart. What each of them puts
|
|
24
|
+
* inside it stays theirs.
|
|
25
|
+
*
|
|
26
|
+
* ## Load it lazily
|
|
27
|
+
*
|
|
28
|
+
* This module names `@mui/material/Dialog`. The barrel withholds it, and every
|
|
29
|
+
* caller reaches it through `lazy(() => import())` once a visitor has asked
|
|
30
|
+
* for the lightbox, because the Dialog/Modal/FocusTrap stack measured in the
|
|
31
|
+
* first-load chunks of pages that open no dialog (AGL-1290). Import it by its
|
|
32
|
+
* subpath, `@aglyn/shared-ui-jsx/components/lightbox/lightbox-dialog`, and
|
|
33
|
+
* only from a module that is itself loaded on demand.
|
|
34
|
+
*
|
|
35
|
+
* ## The accessibility is the reason it is MUI's Dialog
|
|
36
|
+
*
|
|
37
|
+
* `Dialog` traps focus for the life of the overlay, returns it to the element
|
|
38
|
+
* that opened it on close, closes on `Escape`, marks the paper
|
|
39
|
+
* `aria-modal="true"` and the rest of the document inert for assistive
|
|
40
|
+
* technology. Each of those is a thing a bespoke overlay gets wrong. What this
|
|
41
|
+
* shell adds is the part `Dialog` cannot know: a NAME on the element that has
|
|
42
|
+
* `role="dialog"` (the paper — props spread onto `<Dialog>` land on the
|
|
43
|
+
* `role="presentation"` root, which once left the Video dialog unnamed), a
|
|
44
|
+
* close control that is always there and always labelled, and motion that
|
|
45
|
+
* stops for a visitor who asked for less of it.
|
|
46
|
+
*
|
|
47
|
+
* ## Nothing renders while closed
|
|
48
|
+
*
|
|
49
|
+
* `Dialog` keeps no children mounted while closed, and the callers mount this
|
|
50
|
+
* only once armed. Whatever a lightbox shows — a film, a full-size picture, a
|
|
51
|
+
* form — is not in the document until it opens, so it costs a page nothing.
|
|
52
|
+
*/ 'use client';
|
|
53
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
54
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
55
|
+
import { mdiClose } from "@aglyn/shared-data-mdi";
|
|
56
|
+
import Box from "@mui/material/Box";
|
|
57
|
+
import Button from "@mui/material/Button";
|
|
58
|
+
import Dialog from "@mui/material/Dialog";
|
|
59
|
+
import Fade from "@mui/material/Fade";
|
|
60
|
+
import IconButton from "@mui/material/IconButton";
|
|
61
|
+
import useMediaQuery from "@mui/material/useMediaQuery";
|
|
62
|
+
import Zoom from "@mui/material/Zoom";
|
|
63
|
+
import { MdiIcon } from "../mdi-icon/index.js";
|
|
64
|
+
import { lightboxBackdropBackground } from "./lightbox-appearance.js";
|
|
65
|
+
/** The appearance every lightbox takes when an element sets none. */ export const DEFAULT_LIGHTBOX_APPEARANCE = {
|
|
66
|
+
closeOnBackdrop: true,
|
|
67
|
+
closeOnEscape: true
|
|
68
|
+
};
|
|
69
|
+
/** `prefers-reduced-motion` — the transitions drop to nothing under it. */ export function useLightboxReducedMotion() {
|
|
70
|
+
return useMediaQuery('(prefers-reduced-motion: reduce)');
|
|
71
|
+
}
|
|
72
|
+
/** Where the close control sits, by the appearance's position. */ function closePositionSx(position) {
|
|
73
|
+
if (position === 'outside-end') {
|
|
74
|
+
// Fixed to the screen: the dialog's container is not transformed once
|
|
75
|
+
// open, so `fixed` resolves against the viewport, and the button stays
|
|
76
|
+
// inside the dialog's focus trap where a portal of its own would not.
|
|
77
|
+
return (theme)=>({
|
|
78
|
+
position: 'fixed',
|
|
79
|
+
top: theme.spacing(2),
|
|
80
|
+
right: theme.spacing(2),
|
|
81
|
+
zIndex: 2
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return (theme)=>_extends({
|
|
85
|
+
position: 'absolute',
|
|
86
|
+
top: theme.spacing(1)
|
|
87
|
+
}, position === 'inside-start' ? {
|
|
88
|
+
left: theme.spacing(1)
|
|
89
|
+
} : {
|
|
90
|
+
right: theme.spacing(1)
|
|
91
|
+
}, {
|
|
92
|
+
zIndex: 1
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/** The close control: the same one in every lightbox, styled by appearance. */ export function LightboxCloseButton(props) {
|
|
96
|
+
var _ref;
|
|
97
|
+
const { onClick, label, appearance, sx } = props;
|
|
98
|
+
const style = (_ref = appearance == null ? void 0 : appearance.closeStyle) != null ? _ref : 'icon';
|
|
99
|
+
const position = closePositionSx(appearance == null ? void 0 : appearance.closePosition);
|
|
100
|
+
const extra = Array.isArray(sx) ? sx : sx ? [
|
|
101
|
+
sx
|
|
102
|
+
] : [];
|
|
103
|
+
if (style === 'text') {
|
|
104
|
+
return /*#__PURE__*/ _jsx(Button, {
|
|
105
|
+
onClick: onClick,
|
|
106
|
+
"aria-label": label,
|
|
107
|
+
size: "small",
|
|
108
|
+
variant: "contained",
|
|
109
|
+
color: "inherit",
|
|
110
|
+
sx: [
|
|
111
|
+
position,
|
|
112
|
+
...extra
|
|
113
|
+
],
|
|
114
|
+
children: "Close"
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return /*#__PURE__*/ _jsx(IconButton, {
|
|
118
|
+
onClick: onClick,
|
|
119
|
+
"aria-label": label,
|
|
120
|
+
size: "small",
|
|
121
|
+
sx: [
|
|
122
|
+
position,
|
|
123
|
+
...style === 'filled' ? [
|
|
124
|
+
{
|
|
125
|
+
bgcolor: 'common.white',
|
|
126
|
+
color: 'common.black',
|
|
127
|
+
boxShadow: 2,
|
|
128
|
+
'&:hover': {
|
|
129
|
+
bgcolor: 'grey.200'
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
] : [],
|
|
133
|
+
...extra
|
|
134
|
+
],
|
|
135
|
+
children: /*#__PURE__*/ _jsx(MdiIcon, {
|
|
136
|
+
path: mdiClose.path,
|
|
137
|
+
fontSize: "small"
|
|
138
|
+
})
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
LightboxCloseButton.displayName = 'LightboxCloseButton';
|
|
142
|
+
/**
|
|
143
|
+
* The dialog. Mounted by a caller once armed; renders nothing while closed.
|
|
144
|
+
*/ export function LightboxDialog(props) {
|
|
145
|
+
var _appearance_transition;
|
|
146
|
+
const { open, onClose, label, closeLabel = 'Close', appearance = DEFAULT_LIGHTBOX_APPEARANCE, interceptClose, onKeyDown, closeButton = true, maxWidth, fullWidth, paperSx, closeButtonSx, children } = props;
|
|
147
|
+
const reduceMotion = useLightboxReducedMotion();
|
|
148
|
+
const transition = reduceMotion ? 'none' : (_appearance_transition = appearance.transition) != null ? _appearance_transition : 'fade';
|
|
149
|
+
const requestClose = (reason)=>{
|
|
150
|
+
if (reason === 'backdropClick' && !appearance.closeOnBackdrop) return;
|
|
151
|
+
if (reason === 'escapeKeyDown' && !appearance.closeOnEscape) return;
|
|
152
|
+
if (interceptClose == null ? void 0 : interceptClose(reason)) return;
|
|
153
|
+
onClose();
|
|
154
|
+
};
|
|
155
|
+
const backdropBackground = lightboxBackdropBackground(appearance);
|
|
156
|
+
const blur = appearance.backdropBlur;
|
|
157
|
+
const extraPaperSx = Array.isArray(paperSx) ? paperSx : paperSx ? [
|
|
158
|
+
paperSx
|
|
159
|
+
] : [];
|
|
160
|
+
return /*#__PURE__*/ _jsxs(Dialog, _extends({
|
|
161
|
+
open: open,
|
|
162
|
+
onClose: (_event, reason)=>requestClose(reason)
|
|
163
|
+
}, onKeyDown ? {
|
|
164
|
+
onKeyDown
|
|
165
|
+
} : {}, {
|
|
166
|
+
maxWidth: appearance.maxWidth ? false : maxWidth,
|
|
167
|
+
fullWidth: fullWidth,
|
|
168
|
+
transitionDuration: transition === 'none' ? 0 : undefined,
|
|
169
|
+
slots: transition === 'zoom' ? {
|
|
170
|
+
transition: Zoom
|
|
171
|
+
} : {
|
|
172
|
+
transition: Fade
|
|
173
|
+
},
|
|
174
|
+
slotProps: {
|
|
175
|
+
backdrop: {
|
|
176
|
+
sx: _extends({}, backdropBackground ? {
|
|
177
|
+
backgroundColor: backdropBackground
|
|
178
|
+
} : {}, blur ? {
|
|
179
|
+
backdropFilter: `blur(${blur}px)`
|
|
180
|
+
} : {})
|
|
181
|
+
},
|
|
182
|
+
paper: {
|
|
183
|
+
// ⚠️ The label goes on the PAPER, which carries `role="dialog"`;
|
|
184
|
+
// see the module comment.
|
|
185
|
+
'aria-label': label,
|
|
186
|
+
sx: [
|
|
187
|
+
_extends({
|
|
188
|
+
position: 'relative'
|
|
189
|
+
}, appearance.maxWidth ? {
|
|
190
|
+
maxWidth: `min(${appearance.maxWidth}, calc(100% - 64px))`
|
|
191
|
+
} : {}, appearance.maxWidth && fullWidth ? {
|
|
192
|
+
width: '100%'
|
|
193
|
+
} : {}, appearance.maxHeight ? {
|
|
194
|
+
maxHeight: `min(${appearance.maxHeight}, calc(100% - 64px))`
|
|
195
|
+
} : {}, appearance.padding ? {
|
|
196
|
+
p: appearance.padding
|
|
197
|
+
} : {}, appearance.radius !== undefined ? {
|
|
198
|
+
borderRadius: `${appearance.radius}px`
|
|
199
|
+
} : {}),
|
|
200
|
+
...extraPaperSx
|
|
201
|
+
]
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
children: [
|
|
205
|
+
closeButton ? /*#__PURE__*/ _jsx(LightboxCloseButton, {
|
|
206
|
+
onClick: ()=>requestClose('closeButton'),
|
|
207
|
+
label: closeLabel,
|
|
208
|
+
appearance: appearance,
|
|
209
|
+
sx: closeButtonSx
|
|
210
|
+
}) : null,
|
|
211
|
+
children
|
|
212
|
+
]
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
215
|
+
LightboxDialog.displayName = 'LightboxDialog';
|
|
216
|
+
/** The frame's inner box, for content that wants the paper's full height. */ export function LightboxFrame(props) {
|
|
217
|
+
const extra = Array.isArray(props.sx) ? props.sx : props.sx ? [
|
|
218
|
+
props.sx
|
|
219
|
+
] : [];
|
|
220
|
+
return /*#__PURE__*/ _jsx(Box, {
|
|
221
|
+
sx: [
|
|
222
|
+
{
|
|
223
|
+
position: 'relative',
|
|
224
|
+
display: 'flex',
|
|
225
|
+
flexDirection: 'column',
|
|
226
|
+
flex: '1 1 auto',
|
|
227
|
+
minHeight: 0
|
|
228
|
+
},
|
|
229
|
+
...extra
|
|
230
|
+
],
|
|
231
|
+
children: props.children
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
export default LightboxDialog;
|
|
235
|
+
|
|
236
|
+
//# sourceMappingURL=lightbox-dialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../../libs/shared/ui/jsx/src/lib/components/lightbox/lightbox-dialog.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The one lightbox every element opens (AGL-3717).\n *\n * The Video element's dialog (AGL-2744) was the first and, until this, the\n * only lightbox on a published page. Its shell — the MUI `Dialog`, the label\n * on the paper, the close control, the reduced-motion rule — is lifted out\n * here so the Image, the Image List's gallery and the Lightbox container open\n * the SAME dialog rather than three that drift apart. What each of them puts\n * inside it stays theirs.\n *\n * ## Load it lazily\n *\n * This module names `@mui/material/Dialog`. The barrel withholds it, and every\n * caller reaches it through `lazy(() => import())` once a visitor has asked\n * for the lightbox, because the Dialog/Modal/FocusTrap stack measured in the\n * first-load chunks of pages that open no dialog (AGL-1290). Import it by its\n * subpath, `@aglyn/shared-ui-jsx/components/lightbox/lightbox-dialog`, and\n * only from a module that is itself loaded on demand.\n *\n * ## The accessibility is the reason it is MUI's Dialog\n *\n * `Dialog` traps focus for the life of the overlay, returns it to the element\n * that opened it on close, closes on `Escape`, marks the paper\n * `aria-modal=\"true\"` and the rest of the document inert for assistive\n * technology. Each of those is a thing a bespoke overlay gets wrong. What this\n * shell adds is the part `Dialog` cannot know: a NAME on the element that has\n * `role=\"dialog\"` (the paper — props spread onto `<Dialog>` land on the\n * `role=\"presentation\"` root, which once left the Video dialog unnamed), a\n * close control that is always there and always labelled, and motion that\n * stops for a visitor who asked for less of it.\n *\n * ## Nothing renders while closed\n *\n * `Dialog` keeps no children mounted while closed, and the callers mount this\n * only once armed. Whatever a lightbox shows — a film, a full-size picture, a\n * form — is not in the document until it opens, so it costs a page nothing.\n */\n'use client'\n\nimport { mdiClose } from '@aglyn/shared-data-mdi'\nimport Box from '@mui/material/Box'\nimport Button from '@mui/material/Button'\nimport Dialog, { type DialogProps } from '@mui/material/Dialog'\nimport Fade from '@mui/material/Fade'\nimport IconButton from '@mui/material/IconButton'\nimport type { SxProps, Theme } from '@mui/material/styles'\nimport useMediaQuery from '@mui/material/useMediaQuery'\nimport Zoom from '@mui/material/Zoom'\nimport type { KeyboardEvent, ReactNode } from 'react'\nimport { MdiIcon } from '../mdi-icon'\nimport {\n type LightboxAppearance,\n lightboxBackdropBackground,\n} from './lightbox-appearance'\n\n/** Why the dialog was asked to close. */\nexport type LightboxCloseReason = 'escapeKeyDown' | 'backdropClick' | 'closeButton'\n\n/** The appearance every lightbox takes when an element sets none. */\nexport const DEFAULT_LIGHTBOX_APPEARANCE: LightboxAppearance = {\n closeOnBackdrop: true,\n closeOnEscape: true,\n}\n\nexport interface LightboxDialogProps {\n open: boolean\n onClose: () => void\n /** The dialog's accessible name. Required: an unnamed dialog is a defect. */\n label: string\n /** The close control's accessible name. */\n closeLabel?: string\n appearance?: LightboxAppearance\n /**\n * A chance to refuse one close: return `true` to keep the dialog open. The\n * Video uses it so `Escape` in full screen leaves full screen first.\n */\n interceptClose?: (reason: LightboxCloseReason) => boolean\n onKeyDown?: (event: KeyboardEvent<HTMLDivElement>) => void\n /**\n * `false` when the content draws its own {@link LightboxCloseButton} — the\n * Video does, inside the frame that goes full screen.\n */\n closeButton?: boolean\n /** The frame's width rule when the appearance names no max width. */\n maxWidth?: DialogProps['maxWidth']\n fullWidth?: boolean\n /** Extra styles for the paper — the Video's black ground, for one. */\n paperSx?: SxProps<Theme>\n /** Extra styles for the close control. */\n closeButtonSx?: SxProps<Theme>\n children?: ReactNode\n}\n\n/** `prefers-reduced-motion` — the transitions drop to nothing under it. */\nexport function useLightboxReducedMotion(): boolean {\n return useMediaQuery('(prefers-reduced-motion: reduce)')\n}\n\nexport interface LightboxCloseButtonProps {\n onClick: () => void\n label: string\n appearance?: LightboxAppearance\n sx?: SxProps<Theme>\n}\n\n/** Where the close control sits, by the appearance's position. */\nfunction closePositionSx(\n position: LightboxAppearance['closePosition'],\n): SxProps<Theme> {\n if (position === 'outside-end') {\n // Fixed to the screen: the dialog's container is not transformed once\n // open, so `fixed` resolves against the viewport, and the button stays\n // inside the dialog's focus trap where a portal of its own would not.\n return (theme) => ({\n position: 'fixed',\n top: theme.spacing(2),\n right: theme.spacing(2),\n zIndex: 2,\n })\n }\n return (theme) => ({\n position: 'absolute',\n top: theme.spacing(1),\n ...(position === 'inside-start'\n ? { left: theme.spacing(1) }\n : { right: theme.spacing(1) }),\n zIndex: 1,\n })\n}\n\n/** The close control: the same one in every lightbox, styled by appearance. */\nexport function LightboxCloseButton(props: LightboxCloseButtonProps) {\n const { onClick, label, appearance, sx } = props\n const style = appearance?.closeStyle ?? 'icon'\n const position = closePositionSx(appearance?.closePosition)\n const extra = Array.isArray(sx) ? sx : sx ? [sx] : []\n if (style === 'text') {\n return (\n <Button\n onClick={onClick}\n aria-label={label}\n size=\"small\"\n variant=\"contained\"\n color=\"inherit\"\n sx={[position, ...extra]}\n >\n Close\n </Button>\n )\n }\n return (\n <IconButton\n onClick={onClick}\n aria-label={label}\n size=\"small\"\n sx={[\n position,\n ...(style === 'filled'\n ? [\n {\n bgcolor: 'common.white',\n color: 'common.black',\n boxShadow: 2,\n '&:hover': { bgcolor: 'grey.200' },\n },\n ]\n : []),\n ...extra,\n ]}\n >\n <MdiIcon path={mdiClose.path} fontSize=\"small\" />\n </IconButton>\n )\n}\nLightboxCloseButton.displayName = 'LightboxCloseButton'\n\n/**\n * The dialog. Mounted by a caller once armed; renders nothing while closed.\n */\nexport function LightboxDialog(props: LightboxDialogProps) {\n const {\n open,\n onClose,\n label,\n closeLabel = 'Close',\n appearance = DEFAULT_LIGHTBOX_APPEARANCE,\n interceptClose,\n onKeyDown,\n closeButton = true,\n maxWidth,\n fullWidth,\n paperSx,\n closeButtonSx,\n children,\n } = props\n const reduceMotion = useLightboxReducedMotion()\n const transition = reduceMotion ? 'none' : (appearance.transition ?? 'fade')\n const requestClose = (reason: LightboxCloseReason) => {\n if (reason === 'backdropClick' && !appearance.closeOnBackdrop) return\n if (reason === 'escapeKeyDown' && !appearance.closeOnEscape) return\n if (interceptClose?.(reason)) return\n onClose()\n }\n const backdropBackground = lightboxBackdropBackground(appearance)\n const blur = appearance.backdropBlur\n const extraPaperSx = Array.isArray(paperSx) ? paperSx : paperSx ? [paperSx] : []\n return (\n <Dialog\n open={open}\n onClose={(_event, reason) => requestClose(reason)}\n // Only when given: an explicit `onKeyDown={undefined}` reaches the\n // Modal root after its own handler and replaces it, which silently\n // took Escape away from every lightbox that passed none.\n {...(onKeyDown ? { onKeyDown } : {})}\n maxWidth={appearance.maxWidth ? false : maxWidth}\n fullWidth={fullWidth}\n transitionDuration={transition === 'none' ? 0 : undefined}\n slots={transition === 'zoom' ? { transition: Zoom } : { transition: Fade }}\n slotProps={{\n backdrop: {\n sx: {\n ...(backdropBackground ? { backgroundColor: backdropBackground } : {}),\n ...(blur ? { backdropFilter: `blur(${blur}px)` } : {}),\n },\n },\n paper: {\n // ⚠️ The label goes on the PAPER, which carries `role=\"dialog\"`;\n // see the module comment.\n 'aria-label': label,\n sx: [\n {\n position: 'relative',\n ...(appearance.maxWidth\n ? { maxWidth: `min(${appearance.maxWidth}, calc(100% - 64px))` }\n : {}),\n ...(appearance.maxWidth && fullWidth ? { width: '100%' } : {}),\n ...(appearance.maxHeight\n ? { maxHeight: `min(${appearance.maxHeight}, calc(100% - 64px))` }\n : {}),\n ...(appearance.padding ? { p: appearance.padding } : {}),\n ...(appearance.radius !== undefined\n ? { borderRadius: `${appearance.radius}px` }\n : {}),\n },\n ...extraPaperSx,\n ],\n },\n }}\n >\n {closeButton ? (\n <LightboxCloseButton\n onClick={() => requestClose('closeButton')}\n label={closeLabel}\n appearance={appearance}\n sx={closeButtonSx}\n />\n ) : null}\n {children}\n </Dialog>\n )\n}\nLightboxDialog.displayName = 'LightboxDialog'\n\n/** The frame's inner box, for content that wants the paper's full height. */\nexport function LightboxFrame(props: { children?: ReactNode; sx?: SxProps<Theme> }) {\n const extra = Array.isArray(props.sx) ? props.sx : props.sx ? [props.sx] : []\n return (\n <Box\n sx={[\n {\n position: 'relative',\n display: 'flex',\n flexDirection: 'column',\n flex: '1 1 auto',\n minHeight: 0,\n },\n ...extra,\n ]}\n >\n {props.children}\n </Box>\n )\n}\n\nexport default LightboxDialog\n"],"names":["mdiClose","Box","Button","Dialog","Fade","IconButton","useMediaQuery","Zoom","MdiIcon","lightboxBackdropBackground","DEFAULT_LIGHTBOX_APPEARANCE","closeOnBackdrop","closeOnEscape","useLightboxReducedMotion","closePositionSx","position","theme","top","spacing","right","zIndex","left","LightboxCloseButton","props","onClick","label","appearance","sx","style","closeStyle","closePosition","extra","Array","isArray","aria-label","size","variant","color","bgcolor","boxShadow","path","fontSize","displayName","LightboxDialog","open","onClose","closeLabel","interceptClose","onKeyDown","closeButton","maxWidth","fullWidth","paperSx","closeButtonSx","children","reduceMotion","transition","requestClose","reason","backdropBackground","blur","backdropBlur","extraPaperSx","_event","transitionDuration","undefined","slots","slotProps","backdrop","backgroundColor","backdropFilter","paper","width","maxHeight","padding","p","radius","borderRadius","LightboxFrame","display","flexDirection","flex","minHeight"],"mappings":"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"}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
18
|
import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
|
|
19
19
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
20
|
+
import { mdiOpenInNew } from "@aglyn/shared-data-mdi";
|
|
20
21
|
import { AppLink } from "./app-link.js";
|
|
21
22
|
import { MdiIcon } from "./mdi-icon/mdi-icon.js";
|
|
22
23
|
import { DataTableComponent } from "./data-table.component.js";
|
|
@@ -34,17 +35,20 @@ import { TABLE_PAGE_SIZE_DEFAULT, TABLE_PAGE_SIZE_OPTIONS, TABLE_ROWS_PER_PAGE_L
|
|
|
34
35
|
* destination and the reason it is refused are read once and cannot drift.
|
|
35
36
|
*/ function quickActionMenuItem(quick) {
|
|
36
37
|
var _quick_href;
|
|
38
|
+
const external = opensInNewTab(quick);
|
|
37
39
|
return {
|
|
38
40
|
key: 'quick',
|
|
39
41
|
label: quick.label,
|
|
40
|
-
|
|
42
|
+
// A new-tab item already ends in the open-in-new mark, so an open-in-new
|
|
43
|
+
// glyph in front of it drew the same arrow on both sides of one label.
|
|
44
|
+
icon: external && quick.icon === mdiOpenInNew.path ? undefined : /*#__PURE__*/ _jsx(MdiIcon, {
|
|
41
45
|
path: quick.icon,
|
|
42
46
|
size: 0.8
|
|
43
47
|
}),
|
|
44
48
|
// `href` is off-site and opens a new tab; `to` is an in-app route,
|
|
45
49
|
// which a preview opens in a new tab too.
|
|
46
50
|
href: (_quick_href = quick.href) != null ? _quick_href : quick.to,
|
|
47
|
-
external
|
|
51
|
+
external,
|
|
48
52
|
onClick: quick.href || quick.to ? undefined : quick.onClick,
|
|
49
53
|
disabled: Boolean(quick.unavailableReason),
|
|
50
54
|
disabledReason: quick.unavailableReason
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/components/list-table.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { AppLink } from './app-link'\nimport { MdiIcon } from './mdi-icon/mdi-icon'\nimport {\n DataTableComponent,\n type DataTableProps,\n} from './data-table.component'\nimport { IconButton, Stack, Tooltip } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport type { ReactNode } from 'react'\nimport { LIST_PAGE_SORT_DESCRIPTION } from '@aglyn/shared-util-tools/list-query/list-column-sort'\nimport type { ListColumnSort } from '../hooks/use-list-column-sort'\nimport RowActionsMenu, {\n type RowActionsMenuItem,\n} from './row-actions-menu.component'\nimport {\n TABLE_PAGE_SIZE_DEFAULT,\n TABLE_PAGE_SIZE_OPTIONS,\n TABLE_ROWS_PER_PAGE_LABEL,\n} from '../const/table-pagination'\n\n/**\n * ONE row grammar for every artifact list (AGL-2501).\n *\n *\n * They did. Three of the four are `DataTableComponent`, but each declared its\n * own column set, its own action arrangement and its own row-click, and the\n * fourth — screens — is a hand-rolled tree because it has to be: drag to\n * reorder, expand to nest, no grid can do that. So \"the same table\" cannot\n * literally be one component for all four, and pretending otherwise would mean\n * either losing the hierarchy or bolting a tree onto a grid.\n *\n * What IS shared is the row GRAMMAR, and that is what actually differed on\n * screen. Every artifact row now reads the same way:\n *\n * ```\n * [ drag · expand ] name id description updated created [ ⇗ ⋮ ]\n * screens only quick menu\n * ```\n *\n * - **The row opens the detail view.** Clicking anywhere that is not a control\n * navigates. It is the primary action on every one of these lists and it was\n * previously a link buried in one column.\n * - **Rows are NOT selectable.** The grid lists shipped with selection on, so\n * a click both navigated AND tinted the row, and the footer counted \"1 row\n * selected\" for a selection nothing could act on. A selection with no bulk\n * action is a state the reader has to dismiss and cannot use.\n * - **Exactly one quick action, then the menu.** Everything else moves behind\n * the overflow — a delete sitting inline is one mis-click from the row's own\n * open handler, which is the argument AGL-701 already made for components and\n * never carried to the rest.\n *\n * The quick action is the one thing worth a direct click on that kind of\n * artifact: **Open live page** for a screen, **Preview** for anything that has\n * no address of its own. It is the only per-kind variation in the trailing\n * cluster, and it is chosen by the caller rather than inferred here.\n *\n * A screen is the only artifact of the four with a route the public can reach,\n * so it is the only one whose quick action says \"live\". Layouts, components\n * and templates render inside something else and have no address to open, so\n * theirs is a **Preview** into the console's own canvas render.\n *\n * **A preview or view quick action opens in a NEW TAB** (AGL-3660). It shows\n * the reader something beside the list, not instead of it, so it must not\n * take the list's tab — that is `href` (always a new tab) or `to` with\n * `newTab: true`. `apps/console/specs/preview-links-open-in-new-tab.spec.ts`\n * holds every eye-icon / Preview / View quick action to it.\n *\n * The quick action is ALSO the menu's first entry — see\n * {@link quickActionMenuItem}. The icon alone is a glyph with a tooltip; the\n * menu is where every action on the row is spelled out, and an action missing\n * from it reads as an action the row does not have.\n */\n\n/**\n * The single trailing icon beside the overflow menu.\n *\n * Three shapes, because the destinations genuinely differ: an EXTERNAL live\n * page (always a new tab), an IN-APP route (a new tab with `newTab`, which a\n * preview or view route always sets), and a plain handler for the rare\n * action that opens a dialog. `unavailableReason` is the fourth state and the\n * one most easily got wrong — a screen with no single live page (a collection\n * template renders under routes it does not own) must still show the control,\n * disabled, saying why. Hiding it would read as the feature being absent.\n */\nexport interface ListQuickAction {\n /** `mdi` icon path. */\n icon: string\n /** Accessible name; also the tooltip when the action is unavailable. */\n label: string\n /** A live, external URL — opened in a new tab. */\n href?: string\n /** An in-app route — a client navigation, or a new tab with `newTab`. */\n to?: string\n /**\n * Open `to` in a new tab (AGL-3660). Required on every Preview / View\n * quick action: the reader is glancing at something beside the list, and\n * taking the list's tab throws away their place in it. `href` is always a\n * new tab and does not need it.\n */\n newTab?: boolean\n onClick?: () => void\n /**\n * Why this row cannot use the action. Renders it DISABLED with the reason as\n * its tooltip, rather than removing it — an absent control and an\n * inapplicable one look identical, and only one of them is honest.\n */\n unavailableReason?: string\n}\n\nexport interface ListRowActionsProps {\n /** The artifact's name — every control is labelled with it, because these\n * Repeat once per row and \"More actions\" alone says nothing about which. */\n label: string\n quick?: ListQuickAction | null\n items: RowActionsMenuItem[]\n}\n\n/**\n * The quick action, restated as the menu's first entry.\n *\n * The icon and the menu are two ways into ONE action, not two features: the\n * icon is a bare glyph, so a reader who does not recognize it has the overflow\n * as the place every action is named. Deriving the item from the same\n * {@link ListQuickAction} is what keeps the two in step — availability,\n * destination and the reason it is refused are read once and cannot drift.\n */\nfunction quickActionMenuItem(\n quick: ListQuickAction,\n): RowActionsMenuItem {\n return {\n key: 'quick',\n label: quick.label,\n icon: <MdiIcon path={quick.icon} size={0.8} />,\n // `href` is off-site and opens a new tab; `to` is an in-app route,\n // which a preview opens in a new tab too.\n href: quick.href ?? quick.to,\n external: opensInNewTab(quick),\n onClick: quick.href || quick.to ? undefined : quick.onClick,\n disabled: Boolean(quick.unavailableReason),\n disabledReason: quick.unavailableReason,\n }\n}\n\n/** Whether a quick action's destination opens in a tab of its own. */\nexport function opensInNewTab(quick: ListQuickAction): boolean {\n return Boolean(quick.href || (quick.to && quick.newTab))\n}\n\n/** The trailing cluster: one quick action, then the overflow menu. */\nexport function ListRowActions(props: ListRowActionsProps) {\n const { label, quick, items } = props\n const menuItems = quick ? [quickActionMenuItem(quick), ...items] : items\n return (\n <Stack\n direction=\"row\"\n spacing={0.5}\n /*\n `height: '100%'` is what actually centres these (AGL-2501). A DataGrid\n cell is a flex box, but `renderCell` content is auto-height inside it —\n so at the taller `TABLE_ROW_HEIGHT` these icons ride at the top of the\n row while the text beside them is centred. `alignItems` alone cannot\n fix it: there is no spare height to distribute until the child claims\n the row.\n */\n sx={{\n height: '100%',\n alignItems: 'center',\n justifyContent: 'flex-end',\n }}\n // The row's own click opens the detail view; without this every control\n // in here would navigate out from under the thing it just opened.\n onClick={(event) => event.stopPropagation()}\n >\n {quick ? <ListQuickButton label={label} action={quick} /> : null}\n <RowActionsMenu label={label} items={menuItems} />\n </Stack>\n )\n}\nListRowActions.displayName = 'ListRowActions'\n\nfunction ListQuickButton(props: {\n label: string\n action: ListQuickAction\n}) {\n const { label, action } = props\n const icon = <MdiIcon path={action.icon} size={0.8} />\n const name = `${action.label} — ${label}`\n\n if (action.unavailableReason) {\n return (\n <Tooltip title={action.unavailableReason}>\n {/* span: a disabled button emits no events, so the tooltip needs a\n wrapper that does. */}\n <span>\n <IconButton size=\"small\" aria-label={name} disabled>\n {icon}\n </IconButton>\n </span>\n </Tooltip>\n )\n }\n const destination = action.href ?? action.to\n if (destination) {\n return (\n <Tooltip title={action.label}>\n <AppLink\n componentVariant=\"icon-button\"\n size=\"small\"\n aria-label={name}\n href={destination}\n newTab={opensInNewTab(action)}\n >\n {icon}\n </AppLink>\n </Tooltip>\n )\n }\n return (\n <Tooltip title={action.label}>\n <IconButton size=\"small\" aria-label={name} onClick={action.onClick}>\n {icon}\n </IconButton>\n </Tooltip>\n )\n}\n\n/** The field `listActionsColumn` draws, which a row click never opens through. */\nexport const LIST_ACTIONS_FIELD = 'actions'\n\n/**\n * The trailing ACTIONS column, identical on every grid list.\n *\n * Deliberately NOT MUI's `type: 'actions'`. That type renders each\n * `GridActionsCellItem` as its own icon and moves the ones marked `showInMenu`\n * into a menu it owns — which is how the three lists ended up with two, three\n * and five inline icons respectively, each a different width, and how the\n * screens table (which cannot use it at all) grew a second implementation.\n * One `renderCell` gives every list the same cluster and the same width.\n */\nexport function listActionsColumn(\n render: (row: any) => ReactNode,\n options: { width?: number } = {},\n): GridColDef {\n return {\n field: LIST_ACTIONS_FIELD,\n headerName: 'Actions',\n width: options.width ?? 110,\n align: 'right',\n headerAlign: 'right',\n sortable: false,\n filterable: false,\n hideable: false,\n disableColumnMenu: true,\n renderCell: ({ row }: any) => render(row),\n }\n}\n\n/**\n * Row selection, opted into by a list that has something to DO with a\n * selection (AGL-2595).\n *\n * Controlled, and in the caller's vocabulary — an array of `$id`s in, an\n * array out — rather than MUI's own model. The grid's model is a\n * `{ type, ids: Set }` whose `exclude` form appears the moment the header\n * checkbox is ticked, and a caller that had to fold \"everything except\n * these\" back into \"these\" would do it wrong on the page boundary; the\n * table holds the rows and does the fold once, here.\n */\nexport interface ListTableSelection {\n /** The `$id`s currently selected; ids not in `rows` are ignored. */\n selected: readonly string[]\n /** Every change, as the full new selection. */\n onChange: (ids: string[]) => void\n}\n\nexport interface ListTableProps extends DataTableProps {\n /** Row click → the artifact's detail view. */\n onOpen?: (id: string, row: any) => void\n /**\n * The toolbar's free-text search box.\n *\n * Defaults to shown where the GRID answers it, which is client filtering.\n * Under `filterMode=\"server\"` the grid applies no filter at all and the\n * model goes to the caller, so the box defaults to hidden: a list whose\n * handler reads only the column filter would otherwise offer a search that\n * narrows nothing, which reads as \"no match\" rather than \"not supported\".\n * Pass it on a server-filtered list whose handler reads `quickFilterValues`.\n */\n quickFilter?: boolean\n /**\n * Present → a checkbox column and a controlled selection. Absent → the\n * navigation-only grid every list had before, checkbox column and all.\n * The row click keeps opening the record either way: a checkbox is the\n * only thing that selects, so the two gestures cannot collide.\n */\n selectable?: ListTableSelection\n /**\n * The toolbar, its Filters panel and its search, and none of the rows\n * (AGL-3327).\n *\n * For a list the reader can also see some other way — the media library's\n * thumbnail grid — that must filter exactly as its table view does. The\n * panel, the quick search and Export all live inside the grid, bound to\n * its API, so a second, hand-built toolbar for the other view would be a\n * second filter UI to keep in step with this one. Instead the grid is\n * drawn with its body collapsed and no rows: the toolbar stays, and the\n * panel still opens under it (its anchor is kept, at the top of the\n * collapsed body). The columns chooser and Export go — there are no\n * columns on screen to choose, and the rows are the other view's to draw\n * and to export.\n */\n toolbarOnly?: boolean\n /**\n * The list's header sorts (AGL-3680), from `useListColumnSort`.\n *\n * Present, the table draws `columnSort.rows`, sorts nothing itself\n * (`sortingMode=\"server\"`), and decides every column's `sortable` from it:\n * a column the hook names sorts — a page-sorted one says so on its header\n * — and every other column does not. Pass this rather than\n * `disableColumnSorting`, which `specs/list-tables-sort-by-their-headers`\n * refuses on any new caller.\n */\n columnSort?: ListColumnSort\n}\n\n/**\n * The columns as `columnSort` sorts them: the hook's columns sortable in its\n * directions, a page-sorted header described as such, every other column not\n * sortable. The actions column never sorts.\n */\nfunction columnsSortedBy(\n columns: readonly GridColDef[] | undefined,\n columnSort: ListColumnSort,\n): GridColDef[] | undefined {\n return columns?.map((column) => {\n const sorted = column.field === LIST_ACTIONS_FIELD ? undefined : columnSort.columns[column.field]\n if (!sorted) return { ...column, sortable: false }\n return {\n ...column,\n sortable: true,\n sortingOrder: sorted.sortingOrder,\n ...(sorted.mode === 'page'\n ? {\n description: column.description\n ? `${column.description} ${LIST_PAGE_SORT_DESCRIPTION}.`\n : `${LIST_PAGE_SORT_DESCRIPTION}.`,\n }\n : {}),\n }\n })\n}\n\nconst NO_ROWS = [] as const\n\n/** No column chooser and no Export: the columns and rows are drawn elsewhere. */\nconst TOOLBAR_ONLY_PROPS = { disableColumnSelector: true }\n\n/** Nothing, for an overlay a toolbar-only grid has no body to draw in. */\nconst NoOverlay = () => null\n\n/** A toolbar-only grid's overlays: its body is collapsed, so they are too. */\nconst TOOLBAR_ONLY_SLOTS = {\n noRowsOverlay: NoOverlay,\n noResultsOverlay: NoOverlay,\n loadingOverlay: NoOverlay,\n}\n\n/**\n * The body of a toolbar-only grid, collapsed to nothing.\n *\n * Everything in the main container goes except the panel anchor, which the\n * Filters panel positions itself against: the anchor sits at the top of the\n * collapsed body, which is directly under the toolbar, where the panel opens\n * on every other list. The body keeps no height, so the virtualized rows it\n * would draw into have none to fill.\n */\nconst TOOLBAR_ONLY_SX = {\n '& .MuiDataGrid-main': {\n flex: '0 0 0px',\n height: 0,\n minHeight: 0,\n overflow: 'visible',\n },\n '& .MuiDataGrid-main > :not([data-id=\"gridPanelAnchor\"])': {\n display: 'none',\n },\n '& [data-id=\"gridPanelAnchor\"]': { top: 0 },\n} as const\n\n/**\n * The grid every FLAT artifact list uses — layouts, components, templates.\n *\n * Screens is the deliberate exception and keeps its own tree; it adopts\n * {@link ListRowActions} so the two still read alike, which is the part a\n * reader actually compares.\n */\nexport function ListTable(props: ListTableProps) {\n const {\n onOpen,\n sx,\n initialState,\n hideFooter,\n rows,\n selectable,\n quickFilter,\n slotProps,\n toolbarOnly = false,\n slots,\n columnSort,\n ...rest\n } = props\n const sortProps = columnSort\n ? {\n columns: columnsSortedBy(rest.columns, columnSort),\n sortingMode: columnSort.sortingMode,\n sortModel: columnSort.sortModel,\n onSortModelChange: columnSort.onSortModelChange,\n }\n : {}\n const showQuickFilter = quickFilter ?? rest.filterMode !== 'server'\n /*==========================================\n * SELECTION IS OPT-IN, AND NAVIGATION-ONLY IS THE DEFAULT.\n *\n * Without `selectable`, the three properties that make a row a NAVIGATION\n * target rather than a selectable record: `disableRowSelectionOnClick`\n * alone is not enough — it stops the click selecting, and leaves the\n * checkbox column and the \"n rows selected\" footer behind.\n *\n * With it, the grid's model is built from the caller's id list on every\n * render and folded back into one on every change. MUI's model since v8 is\n * `{ type: 'include' | 'exclude', ids }`, and `exclude` is what the header\n * checkbox produces — \"everything but these\" — so the fold has to consult\n * the rows the grid was given, which is why it lives here and not in the\n * caller. `disableRowSelectionOnClick` stays on in this branch too: a row\n * click still opens the record, and only the checkbox selects.\n *=========================================*/\n const selection = selectable\n ? {\n rowSelection: true,\n checkboxSelection: true,\n rowSelectionModel: {\n type: 'include' as const,\n ids: new Set<string>(selectable.selected),\n },\n onRowSelectionModelChange: (model: {\n type: 'include' | 'exclude'\n ids: Set<unknown>\n }) => {\n const all = (rows ?? []).map((row: any) => String(row.$id))\n selectable.onChange(\n model.type === 'include'\n ? all.filter((id) => model.ids.has(id))\n : all.filter((id) => !model.ids.has(id)),\n )\n },\n }\n : { rowSelection: false, checkboxSelection: false }\n /*==========================================\n * A HIDDEN FOOTER STILL PAGED THE ROWS.\n *\n * `hideFooter` is how a caller says \"something else owns the page\" — a\n * server-paged staff list, a cursor feed, a card whose `ListPagination`\n * sits under the grid. It hides the CONTROL and nothing else: the free\n * DataGrid always paginates, so the grid went on slicing at the shared\n * default and rows eleven onward were drawn by nothing and reachable by\n * nothing. The outer footer then described the rows it was HANDED, so it\n * reported a page two rows longer than the one on screen and offered no\n * way to see the difference.\n *\n * Eight lists pass `hideFooter`, and every one of them was losing rows past\n * the tenth. It went unnoticed because a list has to hold more than ten\n * rows to show a symptom, and most of these do not yet.\n *\n * So when the footer is hidden the grid takes ONE page as tall as the rows\n * it was given. CONTROLLED rather than `initialState`, because a\n * server-paged list hands over a different number of rows on every page and\n * initial state is read once. A floor of one because MUI rejects a page\n * size of zero, and an empty grid has nothing to slice anyway.\n *=========================================*/\n const rowCount = toolbarOnly ? 0 : (rows?.length ?? 0)\n const footerHidden = hideFooter || toolbarOnly\n const unpaged = footerHidden\n ? {\n paginationModel: { page: 0, pageSize: Math.max(rowCount, 1) },\n // A controlled model with no handler logs a MUI warning, and the\n // grid must not move off page zero in any case: the page is not\n // this component's to change while somebody else owns the footer.\n onPaginationModelChange: () => undefined,\n // Keeps the size out of the \"not preset in pageSizeOptions\" warning.\n // Nothing renders it — the footer carrying the menu is hidden.\n pageSizeOptions: [Math.max(rowCount, 1)],\n }\n : {}\n return (\n <DataTableComponent\n getRowId={(row: any) => row.$id}\n {...selection}\n disableRowSelectionOnClick\n onRowClick={\n onOpen\n ? ({ id, row }, event) => {\n /*\n * A press anywhere in the actions cell is aimed at the actions.\n * `ListRowActions` stops its own clicks, but the cell is wider\n * and taller than the cluster, and a press on the padding\n * beside the menu would otherwise open the record from under\n * the reader.\n */\n const target = event.target as Element | null\n if (target?.closest?.(`[data-field=\"${LIST_ACTIONS_FIELD}\"]`)) return\n onOpen(String(id), row)\n }\n : undefined\n }\n /*\n * ENTER OPENS IT TOO. The grid moves focus cell by cell with the arrow\n * keys, and a row a mouse can open must open from the keyboard as\n * well. Only when the CELL itself holds focus: Enter on a link or a\n * button inside one is that control's, and neither the actions cell\n * nor the grid's own columns (the selection checkbox) open the record.\n */\n onCellKeyDown={\n onOpen\n ? (params, event) => {\n if (event.key !== 'Enter') return\n if (params.field === LIST_ACTIONS_FIELD || params.field.startsWith('__')) return\n if (event.target !== event.currentTarget) return\n event.defaultMuiPrevented = true\n onOpen(String(params.id), params.row)\n }\n : undefined\n }\n /*\n * The list GROWS; the PAGE scrolls.\n *\n * `DataTableComponent` gives its wrapper a fixed `height: 400`, which\n * is right for a grid sitting in a pane of its own and wrong for a\n * card in a column of cards: the table got its own scrollbar, the card\n * stopped at 400px whatever it held, and the last row was cut through\n * the middle. Two scroll regions on one page is the thing to avoid —\n * a reader scrolling the page expects the page to move.\n *\n * `autoHeight` sizes the grid to its rows; clearing the wrapper's\n * height is what lets the card follow it. Caller `sx` is spread after,\n * so a list that genuinely wants a viewport can still take one.\n */\n autoHeight\n sx={[\n { height: 'auto' },\n { '& .MuiDataGrid-row': { cursor: onOpen ? 'pointer' : 'default' } },\n /*\n A cell whose content is taller than one line sets the row's height,\n and every OTHER cell then draws its content at the top of it — so a\n two-line date column leaves the counts beside it riding high while\n `ListRowActions`, which claims `height: '100%'` for itself, sits\n centred. One row, three vertical positions.\n\n Centring the cell itself fixes every column at once, including the\n ones a caller renders with a bare `<Typography>`; `alignItems`\n alone cannot, because an auto-height child has no spare height to\n distribute until something claims the row. `justifyContent` is\n deliberately not set here — a column's own `align` is what decides\n its horizontal placement, and overriding it would silently undo\n every right-aligned figure.\n */\n {\n '& .MuiDataGrid-cell': {\n display: 'flex',\n alignItems: 'center',\n },\n },\n ...(toolbarOnly ? [TOOLBAR_ONLY_SX] : []),\n ...(Array.isArray(sx) ? sx : sx ? [sx] : []),\n ]}\n /*\n * The column menu can hide a column; only the toolbar can bring it\n * back.\n *\n * Every column here is hideable, `Manage columns` lives in the toolbar,\n * and the toolbar was off — so hiding the last column left a grid with\n * no columns and no control that could restore one. Not a rare misuse\n * either: the per-column menu offers `Hide column` on every column, and\n * the way out was never on screen.\n *\n * The Actions column is the reason the grid was not left completely\n * blank — `listActionsColumn` marks it `hideable: false` — and \"one\n * unhideable column\" is not a recovery path.\n */\n showToolbar\n pagination\n // The console's ONE footer (AGL-2501): same options, same default, same\n // label.\n pageSizeOptions={TABLE_PAGE_SIZE_OPTIONS}\n /*\n * MERGED, not replaced. `initialState` used to ride in through `rest`,\n * so a caller that set anything in it — a column visibility model, a\n * sort — silently dropped the shared page size along with it. The two\n * keys a caller is likely to set are spread individually so that a page\n * size and a hidden column can coexist.\n */\n initialState={{\n ...initialState,\n pagination: {\n paginationModel: { pageSize: TABLE_PAGE_SIZE_DEFAULT },\n ...initialState?.pagination,\n },\n columns: { ...initialState?.columns },\n }}\n /*\n * MERGED with the caller's, for the reason `initialState` is: a caller\n * setting one slot's props must not drop the shared footer label or the\n * search box's filter-mode default along with the object they live in.\n */\n slotProps={{\n ...slotProps,\n toolbar: {\n showQuickFilter,\n ...slotProps?.toolbar,\n ...(toolbarOnly\n ? {\n csvOptions: { disableToolbarButton: true },\n printOptions: { disableToolbarButton: true },\n }\n : {}),\n },\n pagination: {\n labelRowsPerPage: TABLE_ROWS_PER_PAGE_LABEL,\n ...slotProps?.pagination,\n },\n }}\n // A toolbar-only grid holds no rows: the reader sees them some other\n // way, and rows laid out under a collapsed body would be drawn for no\n // one.\n rows={toolbarOnly ? NO_ROWS : columnSort ? (columnSort.rows as any[]) : rows}\n hideFooter={footerHidden}\n slots={toolbarOnly ? { ...slots, ...TOOLBAR_ONLY_SLOTS } : slots}\n {...(toolbarOnly ? TOOLBAR_ONLY_PROPS : {})}\n {...rest}\n {...sortProps}\n /*\n * AFTER `rest`, deliberately. \"The footer is hidden, so every row I was\n * handed is on screen\" is an invariant of this component, not a default\n * a call site may talk it out of — a caller that could re-slice here\n * would reintroduce exactly the rows nobody can reach.\n */\n {...unpaged}\n />\n )\n}\nListTable.displayName = 'ListTable'\n\nexport default ListTable\n"],"names":["AppLink","MdiIcon","DataTableComponent","IconButton","Stack","Tooltip","LIST_PAGE_SORT_DESCRIPTION","RowActionsMenu","TABLE_PAGE_SIZE_DEFAULT","TABLE_PAGE_SIZE_OPTIONS","TABLE_ROWS_PER_PAGE_LABEL","quickActionMenuItem","quick","key","label","icon","path","size","href","to","external","opensInNewTab","onClick","undefined","disabled","Boolean","unavailableReason","disabledReason","newTab","ListRowActions","props","items","menuItems","direction","spacing","sx","height","alignItems","justifyContent","event","stopPropagation","ListQuickButton","action","displayName","name","title","span","aria-label","destination","componentVariant","LIST_ACTIONS_FIELD","listActionsColumn","render","options","field","headerName","width","align","headerAlign","sortable","filterable","hideable","disableColumnMenu","renderCell","row","columnsSortedBy","columns","columnSort","map","column","sorted","sortingOrder","mode","description","NO_ROWS","TOOLBAR_ONLY_PROPS","disableColumnSelector","NoOverlay","TOOLBAR_ONLY_SLOTS","noRowsOverlay","noResultsOverlay","loadingOverlay","TOOLBAR_ONLY_SX","flex","minHeight","overflow","display","top","ListTable","onOpen","initialState","hideFooter","rows","selectable","quickFilter","slotProps","toolbarOnly","slots","rest","sortProps","sortingMode","sortModel","onSortModelChange","showQuickFilter","filterMode","selection","rowSelection","checkboxSelection","rowSelectionModel","type","ids","Set","selected","onRowSelectionModelChange","model","all","String","$id","onChange","filter","id","has","rowCount","length","footerHidden","unpaged","paginationModel","page","pageSize","Math","max","onPaginationModelChange","pageSizeOptions","getRowId","disableRowSelectionOnClick","onRowClick","target","closest","onCellKeyDown","params","startsWith","currentTarget","defaultMuiPrevented","autoHeight","cursor","Array","isArray","showToolbar","pagination","toolbar","csvOptions","disableToolbarButton","printOptions","labelRowsPerPage"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/components/list-table.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { mdiOpenInNew } from '@aglyn/shared-data-mdi'\nimport { AppLink } from './app-link'\nimport { MdiIcon } from './mdi-icon/mdi-icon'\nimport {\n DataTableComponent,\n type DataTableProps,\n} from './data-table.component'\nimport { IconButton, Stack, Tooltip } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport type { ReactNode } from 'react'\nimport { LIST_PAGE_SORT_DESCRIPTION } from '@aglyn/shared-util-tools/list-query/list-column-sort'\nimport type { ListColumnSort } from '../hooks/use-list-column-sort'\nimport RowActionsMenu, {\n type RowActionsMenuItem,\n} from './row-actions-menu.component'\nimport {\n TABLE_PAGE_SIZE_DEFAULT,\n TABLE_PAGE_SIZE_OPTIONS,\n TABLE_ROWS_PER_PAGE_LABEL,\n} from '../const/table-pagination'\n\n/**\n * ONE row grammar for every artifact list (AGL-2501).\n *\n *\n * They did. Three of the four are `DataTableComponent`, but each declared its\n * own column set, its own action arrangement and its own row-click, and the\n * fourth — screens — is a hand-rolled tree because it has to be: drag to\n * reorder, expand to nest, no grid can do that. So \"the same table\" cannot\n * literally be one component for all four, and pretending otherwise would mean\n * either losing the hierarchy or bolting a tree onto a grid.\n *\n * What IS shared is the row GRAMMAR, and that is what actually differed on\n * screen. Every artifact row now reads the same way:\n *\n * ```\n * [ drag · expand ] name id description updated created [ ⇗ ⋮ ]\n * screens only quick menu\n * ```\n *\n * - **The row opens the detail view.** Clicking anywhere that is not a control\n * navigates. It is the primary action on every one of these lists and it was\n * previously a link buried in one column.\n * - **Rows are NOT selectable.** The grid lists shipped with selection on, so\n * a click both navigated AND tinted the row, and the footer counted \"1 row\n * selected\" for a selection nothing could act on. A selection with no bulk\n * action is a state the reader has to dismiss and cannot use.\n * - **Exactly one quick action, then the menu.** Everything else moves behind\n * the overflow — a delete sitting inline is one mis-click from the row's own\n * open handler, which is the argument AGL-701 already made for components and\n * never carried to the rest.\n *\n * The quick action is the one thing worth a direct click on that kind of\n * artifact: **Open live page** for a screen, **Preview** for anything that has\n * no address of its own. It is the only per-kind variation in the trailing\n * cluster, and it is chosen by the caller rather than inferred here.\n *\n * A screen is the only artifact of the four with a route the public can reach,\n * so it is the only one whose quick action says \"live\". Layouts, components\n * and templates render inside something else and have no address to open, so\n * theirs is a **Preview** into the console's own canvas render.\n *\n * **A preview or view quick action opens in a NEW TAB** (AGL-3660). It shows\n * the reader something beside the list, not instead of it, so it must not\n * take the list's tab — that is `href` (always a new tab) or `to` with\n * `newTab: true`. `apps/console/specs/preview-links-open-in-new-tab.spec.ts`\n * holds every eye-icon / Preview / View quick action to it.\n *\n * The quick action is ALSO the menu's first entry — see\n * {@link quickActionMenuItem}. The icon alone is a glyph with a tooltip; the\n * menu is where every action on the row is spelled out, and an action missing\n * from it reads as an action the row does not have.\n */\n\n/**\n * The single trailing icon beside the overflow menu.\n *\n * Three shapes, because the destinations genuinely differ: an EXTERNAL live\n * page (always a new tab), an IN-APP route (a new tab with `newTab`, which a\n * preview or view route always sets), and a plain handler for the rare\n * action that opens a dialog. `unavailableReason` is the fourth state and the\n * one most easily got wrong — a screen with no single live page (a collection\n * template renders under routes it does not own) must still show the control,\n * disabled, saying why. Hiding it would read as the feature being absent.\n */\nexport interface ListQuickAction {\n /** `mdi` icon path. */\n icon: string\n /** Accessible name; also the tooltip when the action is unavailable. */\n label: string\n /** A live, external URL — opened in a new tab. */\n href?: string\n /** An in-app route — a client navigation, or a new tab with `newTab`. */\n to?: string\n /**\n * Open `to` in a new tab (AGL-3660). Required on every Preview / View\n * quick action: the reader is glancing at something beside the list, and\n * taking the list's tab throws away their place in it. `href` is always a\n * new tab and does not need it.\n */\n newTab?: boolean\n onClick?: () => void\n /**\n * Why this row cannot use the action. Renders it DISABLED with the reason as\n * its tooltip, rather than removing it — an absent control and an\n * inapplicable one look identical, and only one of them is honest.\n */\n unavailableReason?: string\n}\n\nexport interface ListRowActionsProps {\n /** The artifact's name — every control is labelled with it, because these\n * Repeat once per row and \"More actions\" alone says nothing about which. */\n label: string\n quick?: ListQuickAction | null\n items: RowActionsMenuItem[]\n}\n\n/**\n * The quick action, restated as the menu's first entry.\n *\n * The icon and the menu are two ways into ONE action, not two features: the\n * icon is a bare glyph, so a reader who does not recognize it has the overflow\n * as the place every action is named. Deriving the item from the same\n * {@link ListQuickAction} is what keeps the two in step — availability,\n * destination and the reason it is refused are read once and cannot drift.\n */\nfunction quickActionMenuItem(\n quick: ListQuickAction,\n): RowActionsMenuItem {\n const external = opensInNewTab(quick)\n return {\n key: 'quick',\n label: quick.label,\n // A new-tab item already ends in the open-in-new mark, so an open-in-new\n // glyph in front of it drew the same arrow on both sides of one label.\n icon:\n external && quick.icon === mdiOpenInNew.path ? undefined : (\n <MdiIcon path={quick.icon} size={0.8} />\n ),\n // `href` is off-site and opens a new tab; `to` is an in-app route,\n // which a preview opens in a new tab too.\n href: quick.href ?? quick.to,\n external,\n onClick: quick.href || quick.to ? undefined : quick.onClick,\n disabled: Boolean(quick.unavailableReason),\n disabledReason: quick.unavailableReason,\n }\n}\n\n/** Whether a quick action's destination opens in a tab of its own. */\nexport function opensInNewTab(quick: ListQuickAction): boolean {\n return Boolean(quick.href || (quick.to && quick.newTab))\n}\n\n/** The trailing cluster: one quick action, then the overflow menu. */\nexport function ListRowActions(props: ListRowActionsProps) {\n const { label, quick, items } = props\n const menuItems = quick ? [quickActionMenuItem(quick), ...items] : items\n return (\n <Stack\n direction=\"row\"\n spacing={0.5}\n /*\n `height: '100%'` is what actually centres these (AGL-2501). A DataGrid\n cell is a flex box, but `renderCell` content is auto-height inside it —\n so at the taller `TABLE_ROW_HEIGHT` these icons ride at the top of the\n row while the text beside them is centred. `alignItems` alone cannot\n fix it: there is no spare height to distribute until the child claims\n the row.\n */\n sx={{\n height: '100%',\n alignItems: 'center',\n justifyContent: 'flex-end',\n }}\n // The row's own click opens the detail view; without this every control\n // in here would navigate out from under the thing it just opened.\n onClick={(event) => event.stopPropagation()}\n >\n {quick ? <ListQuickButton label={label} action={quick} /> : null}\n <RowActionsMenu label={label} items={menuItems} />\n </Stack>\n )\n}\nListRowActions.displayName = 'ListRowActions'\n\nfunction ListQuickButton(props: {\n label: string\n action: ListQuickAction\n}) {\n const { label, action } = props\n const icon = <MdiIcon path={action.icon} size={0.8} />\n const name = `${action.label} — ${label}`\n\n if (action.unavailableReason) {\n return (\n <Tooltip title={action.unavailableReason}>\n {/* span: a disabled button emits no events, so the tooltip needs a\n wrapper that does. */}\n <span>\n <IconButton size=\"small\" aria-label={name} disabled>\n {icon}\n </IconButton>\n </span>\n </Tooltip>\n )\n }\n const destination = action.href ?? action.to\n if (destination) {\n return (\n <Tooltip title={action.label}>\n <AppLink\n componentVariant=\"icon-button\"\n size=\"small\"\n aria-label={name}\n href={destination}\n newTab={opensInNewTab(action)}\n >\n {icon}\n </AppLink>\n </Tooltip>\n )\n }\n return (\n <Tooltip title={action.label}>\n <IconButton size=\"small\" aria-label={name} onClick={action.onClick}>\n {icon}\n </IconButton>\n </Tooltip>\n )\n}\n\n/** The field `listActionsColumn` draws, which a row click never opens through. */\nexport const LIST_ACTIONS_FIELD = 'actions'\n\n/**\n * The trailing ACTIONS column, identical on every grid list.\n *\n * Deliberately NOT MUI's `type: 'actions'`. That type renders each\n * `GridActionsCellItem` as its own icon and moves the ones marked `showInMenu`\n * into a menu it owns — which is how the three lists ended up with two, three\n * and five inline icons respectively, each a different width, and how the\n * screens table (which cannot use it at all) grew a second implementation.\n * One `renderCell` gives every list the same cluster and the same width.\n */\nexport function listActionsColumn(\n render: (row: any) => ReactNode,\n options: { width?: number } = {},\n): GridColDef {\n return {\n field: LIST_ACTIONS_FIELD,\n headerName: 'Actions',\n width: options.width ?? 110,\n align: 'right',\n headerAlign: 'right',\n sortable: false,\n filterable: false,\n hideable: false,\n disableColumnMenu: true,\n renderCell: ({ row }: any) => render(row),\n }\n}\n\n/**\n * Row selection, opted into by a list that has something to DO with a\n * selection (AGL-2595).\n *\n * Controlled, and in the caller's vocabulary — an array of `$id`s in, an\n * array out — rather than MUI's own model. The grid's model is a\n * `{ type, ids: Set }` whose `exclude` form appears the moment the header\n * checkbox is ticked, and a caller that had to fold \"everything except\n * these\" back into \"these\" would do it wrong on the page boundary; the\n * table holds the rows and does the fold once, here.\n */\nexport interface ListTableSelection {\n /** The `$id`s currently selected; ids not in `rows` are ignored. */\n selected: readonly string[]\n /** Every change, as the full new selection. */\n onChange: (ids: string[]) => void\n}\n\nexport interface ListTableProps extends DataTableProps {\n /** Row click → the artifact's detail view. */\n onOpen?: (id: string, row: any) => void\n /**\n * The toolbar's free-text search box.\n *\n * Defaults to shown where the GRID answers it, which is client filtering.\n * Under `filterMode=\"server\"` the grid applies no filter at all and the\n * model goes to the caller, so the box defaults to hidden: a list whose\n * handler reads only the column filter would otherwise offer a search that\n * narrows nothing, which reads as \"no match\" rather than \"not supported\".\n * Pass it on a server-filtered list whose handler reads `quickFilterValues`.\n */\n quickFilter?: boolean\n /**\n * Present → a checkbox column and a controlled selection. Absent → the\n * navigation-only grid every list had before, checkbox column and all.\n * The row click keeps opening the record either way: a checkbox is the\n * only thing that selects, so the two gestures cannot collide.\n */\n selectable?: ListTableSelection\n /**\n * The toolbar, its Filters panel and its search, and none of the rows\n * (AGL-3327).\n *\n * For a list the reader can also see some other way — the media library's\n * thumbnail grid — that must filter exactly as its table view does. The\n * panel, the quick search and Export all live inside the grid, bound to\n * its API, so a second, hand-built toolbar for the other view would be a\n * second filter UI to keep in step with this one. Instead the grid is\n * drawn with its body collapsed and no rows: the toolbar stays, and the\n * panel still opens under it (its anchor is kept, at the top of the\n * collapsed body). The columns chooser and Export go — there are no\n * columns on screen to choose, and the rows are the other view's to draw\n * and to export.\n */\n toolbarOnly?: boolean\n /**\n * The list's header sorts (AGL-3680), from `useListColumnSort`.\n *\n * Present, the table draws `columnSort.rows`, sorts nothing itself\n * (`sortingMode=\"server\"`), and decides every column's `sortable` from it:\n * a column the hook names sorts — a page-sorted one says so on its header\n * — and every other column does not. Pass this rather than\n * `disableColumnSorting`, which `specs/list-tables-sort-by-their-headers`\n * refuses on any new caller.\n */\n columnSort?: ListColumnSort\n}\n\n/**\n * The columns as `columnSort` sorts them: the hook's columns sortable in its\n * directions, a page-sorted header described as such, every other column not\n * sortable. The actions column never sorts.\n */\nfunction columnsSortedBy(\n columns: readonly GridColDef[] | undefined,\n columnSort: ListColumnSort,\n): GridColDef[] | undefined {\n return columns?.map((column) => {\n const sorted = column.field === LIST_ACTIONS_FIELD ? undefined : columnSort.columns[column.field]\n if (!sorted) return { ...column, sortable: false }\n return {\n ...column,\n sortable: true,\n sortingOrder: sorted.sortingOrder,\n ...(sorted.mode === 'page'\n ? {\n description: column.description\n ? `${column.description} ${LIST_PAGE_SORT_DESCRIPTION}.`\n : `${LIST_PAGE_SORT_DESCRIPTION}.`,\n }\n : {}),\n }\n })\n}\n\nconst NO_ROWS = [] as const\n\n/** No column chooser and no Export: the columns and rows are drawn elsewhere. */\nconst TOOLBAR_ONLY_PROPS = { disableColumnSelector: true }\n\n/** Nothing, for an overlay a toolbar-only grid has no body to draw in. */\nconst NoOverlay = () => null\n\n/** A toolbar-only grid's overlays: its body is collapsed, so they are too. */\nconst TOOLBAR_ONLY_SLOTS = {\n noRowsOverlay: NoOverlay,\n noResultsOverlay: NoOverlay,\n loadingOverlay: NoOverlay,\n}\n\n/**\n * The body of a toolbar-only grid, collapsed to nothing.\n *\n * Everything in the main container goes except the panel anchor, which the\n * Filters panel positions itself against: the anchor sits at the top of the\n * collapsed body, which is directly under the toolbar, where the panel opens\n * on every other list. The body keeps no height, so the virtualized rows it\n * would draw into have none to fill.\n */\nconst TOOLBAR_ONLY_SX = {\n '& .MuiDataGrid-main': {\n flex: '0 0 0px',\n height: 0,\n minHeight: 0,\n overflow: 'visible',\n },\n '& .MuiDataGrid-main > :not([data-id=\"gridPanelAnchor\"])': {\n display: 'none',\n },\n '& [data-id=\"gridPanelAnchor\"]': { top: 0 },\n} as const\n\n/**\n * The grid every FLAT artifact list uses — layouts, components, templates.\n *\n * Screens is the deliberate exception and keeps its own tree; it adopts\n * {@link ListRowActions} so the two still read alike, which is the part a\n * reader actually compares.\n */\nexport function ListTable(props: ListTableProps) {\n const {\n onOpen,\n sx,\n initialState,\n hideFooter,\n rows,\n selectable,\n quickFilter,\n slotProps,\n toolbarOnly = false,\n slots,\n columnSort,\n ...rest\n } = props\n const sortProps = columnSort\n ? {\n columns: columnsSortedBy(rest.columns, columnSort),\n sortingMode: columnSort.sortingMode,\n sortModel: columnSort.sortModel,\n onSortModelChange: columnSort.onSortModelChange,\n }\n : {}\n const showQuickFilter = quickFilter ?? rest.filterMode !== 'server'\n /*==========================================\n * SELECTION IS OPT-IN, AND NAVIGATION-ONLY IS THE DEFAULT.\n *\n * Without `selectable`, the three properties that make a row a NAVIGATION\n * target rather than a selectable record: `disableRowSelectionOnClick`\n * alone is not enough — it stops the click selecting, and leaves the\n * checkbox column and the \"n rows selected\" footer behind.\n *\n * With it, the grid's model is built from the caller's id list on every\n * render and folded back into one on every change. MUI's model since v8 is\n * `{ type: 'include' | 'exclude', ids }`, and `exclude` is what the header\n * checkbox produces — \"everything but these\" — so the fold has to consult\n * the rows the grid was given, which is why it lives here and not in the\n * caller. `disableRowSelectionOnClick` stays on in this branch too: a row\n * click still opens the record, and only the checkbox selects.\n *=========================================*/\n const selection = selectable\n ? {\n rowSelection: true,\n checkboxSelection: true,\n rowSelectionModel: {\n type: 'include' as const,\n ids: new Set<string>(selectable.selected),\n },\n onRowSelectionModelChange: (model: {\n type: 'include' | 'exclude'\n ids: Set<unknown>\n }) => {\n const all = (rows ?? []).map((row: any) => String(row.$id))\n selectable.onChange(\n model.type === 'include'\n ? all.filter((id) => model.ids.has(id))\n : all.filter((id) => !model.ids.has(id)),\n )\n },\n }\n : { rowSelection: false, checkboxSelection: false }\n /*==========================================\n * A HIDDEN FOOTER STILL PAGED THE ROWS.\n *\n * `hideFooter` is how a caller says \"something else owns the page\" — a\n * server-paged staff list, a cursor feed, a card whose `ListPagination`\n * sits under the grid. It hides the CONTROL and nothing else: the free\n * DataGrid always paginates, so the grid went on slicing at the shared\n * default and rows eleven onward were drawn by nothing and reachable by\n * nothing. The outer footer then described the rows it was HANDED, so it\n * reported a page two rows longer than the one on screen and offered no\n * way to see the difference.\n *\n * Eight lists pass `hideFooter`, and every one of them was losing rows past\n * the tenth. It went unnoticed because a list has to hold more than ten\n * rows to show a symptom, and most of these do not yet.\n *\n * So when the footer is hidden the grid takes ONE page as tall as the rows\n * it was given. CONTROLLED rather than `initialState`, because a\n * server-paged list hands over a different number of rows on every page and\n * initial state is read once. A floor of one because MUI rejects a page\n * size of zero, and an empty grid has nothing to slice anyway.\n *=========================================*/\n const rowCount = toolbarOnly ? 0 : (rows?.length ?? 0)\n const footerHidden = hideFooter || toolbarOnly\n const unpaged = footerHidden\n ? {\n paginationModel: { page: 0, pageSize: Math.max(rowCount, 1) },\n // A controlled model with no handler logs a MUI warning, and the\n // grid must not move off page zero in any case: the page is not\n // this component's to change while somebody else owns the footer.\n onPaginationModelChange: () => undefined,\n // Keeps the size out of the \"not preset in pageSizeOptions\" warning.\n // Nothing renders it — the footer carrying the menu is hidden.\n pageSizeOptions: [Math.max(rowCount, 1)],\n }\n : {}\n return (\n <DataTableComponent\n getRowId={(row: any) => row.$id}\n {...selection}\n disableRowSelectionOnClick\n onRowClick={\n onOpen\n ? ({ id, row }, event) => {\n /*\n * A press anywhere in the actions cell is aimed at the actions.\n * `ListRowActions` stops its own clicks, but the cell is wider\n * and taller than the cluster, and a press on the padding\n * beside the menu would otherwise open the record from under\n * the reader.\n */\n const target = event.target as Element | null\n if (target?.closest?.(`[data-field=\"${LIST_ACTIONS_FIELD}\"]`)) return\n onOpen(String(id), row)\n }\n : undefined\n }\n /*\n * ENTER OPENS IT TOO. The grid moves focus cell by cell with the arrow\n * keys, and a row a mouse can open must open from the keyboard as\n * well. Only when the CELL itself holds focus: Enter on a link or a\n * button inside one is that control's, and neither the actions cell\n * nor the grid's own columns (the selection checkbox) open the record.\n */\n onCellKeyDown={\n onOpen\n ? (params, event) => {\n if (event.key !== 'Enter') return\n if (params.field === LIST_ACTIONS_FIELD || params.field.startsWith('__')) return\n if (event.target !== event.currentTarget) return\n event.defaultMuiPrevented = true\n onOpen(String(params.id), params.row)\n }\n : undefined\n }\n /*\n * The list GROWS; the PAGE scrolls.\n *\n * `DataTableComponent` gives its wrapper a fixed `height: 400`, which\n * is right for a grid sitting in a pane of its own and wrong for a\n * card in a column of cards: the table got its own scrollbar, the card\n * stopped at 400px whatever it held, and the last row was cut through\n * the middle. Two scroll regions on one page is the thing to avoid —\n * a reader scrolling the page expects the page to move.\n *\n * `autoHeight` sizes the grid to its rows; clearing the wrapper's\n * height is what lets the card follow it. Caller `sx` is spread after,\n * so a list that genuinely wants a viewport can still take one.\n */\n autoHeight\n sx={[\n { height: 'auto' },\n { '& .MuiDataGrid-row': { cursor: onOpen ? 'pointer' : 'default' } },\n /*\n A cell whose content is taller than one line sets the row's height,\n and every OTHER cell then draws its content at the top of it — so a\n two-line date column leaves the counts beside it riding high while\n `ListRowActions`, which claims `height: '100%'` for itself, sits\n centred. One row, three vertical positions.\n\n Centring the cell itself fixes every column at once, including the\n ones a caller renders with a bare `<Typography>`; `alignItems`\n alone cannot, because an auto-height child has no spare height to\n distribute until something claims the row. `justifyContent` is\n deliberately not set here — a column's own `align` is what decides\n its horizontal placement, and overriding it would silently undo\n every right-aligned figure.\n */\n {\n '& .MuiDataGrid-cell': {\n display: 'flex',\n alignItems: 'center',\n },\n },\n ...(toolbarOnly ? [TOOLBAR_ONLY_SX] : []),\n ...(Array.isArray(sx) ? sx : sx ? [sx] : []),\n ]}\n /*\n * The column menu can hide a column; only the toolbar can bring it\n * back.\n *\n * Every column here is hideable, `Manage columns` lives in the toolbar,\n * and the toolbar was off — so hiding the last column left a grid with\n * no columns and no control that could restore one. Not a rare misuse\n * either: the per-column menu offers `Hide column` on every column, and\n * the way out was never on screen.\n *\n * The Actions column is the reason the grid was not left completely\n * blank — `listActionsColumn` marks it `hideable: false` — and \"one\n * unhideable column\" is not a recovery path.\n */\n showToolbar\n pagination\n // The console's ONE footer (AGL-2501): same options, same default, same\n // label.\n pageSizeOptions={TABLE_PAGE_SIZE_OPTIONS}\n /*\n * MERGED, not replaced. `initialState` used to ride in through `rest`,\n * so a caller that set anything in it — a column visibility model, a\n * sort — silently dropped the shared page size along with it. The two\n * keys a caller is likely to set are spread individually so that a page\n * size and a hidden column can coexist.\n */\n initialState={{\n ...initialState,\n pagination: {\n paginationModel: { pageSize: TABLE_PAGE_SIZE_DEFAULT },\n ...initialState?.pagination,\n },\n columns: { ...initialState?.columns },\n }}\n /*\n * MERGED with the caller's, for the reason `initialState` is: a caller\n * setting one slot's props must not drop the shared footer label or the\n * search box's filter-mode default along with the object they live in.\n */\n slotProps={{\n ...slotProps,\n toolbar: {\n showQuickFilter,\n ...slotProps?.toolbar,\n ...(toolbarOnly\n ? {\n csvOptions: { disableToolbarButton: true },\n printOptions: { disableToolbarButton: true },\n }\n : {}),\n },\n pagination: {\n labelRowsPerPage: TABLE_ROWS_PER_PAGE_LABEL,\n ...slotProps?.pagination,\n },\n }}\n // A toolbar-only grid holds no rows: the reader sees them some other\n // way, and rows laid out under a collapsed body would be drawn for no\n // one.\n rows={toolbarOnly ? NO_ROWS : columnSort ? (columnSort.rows as any[]) : rows}\n hideFooter={footerHidden}\n slots={toolbarOnly ? { ...slots, ...TOOLBAR_ONLY_SLOTS } : slots}\n {...(toolbarOnly ? TOOLBAR_ONLY_PROPS : {})}\n {...rest}\n {...sortProps}\n /*\n * AFTER `rest`, deliberately. \"The footer is hidden, so every row I was\n * handed is on screen\" is an invariant of this component, not a default\n * a call site may talk it out of — a caller that could re-slice here\n * would reintroduce exactly the rows nobody can reach.\n */\n {...unpaged}\n />\n )\n}\nListTable.displayName = 'ListTable'\n\nexport default ListTable\n"],"names":["mdiOpenInNew","AppLink","MdiIcon","DataTableComponent","IconButton","Stack","Tooltip","LIST_PAGE_SORT_DESCRIPTION","RowActionsMenu","TABLE_PAGE_SIZE_DEFAULT","TABLE_PAGE_SIZE_OPTIONS","TABLE_ROWS_PER_PAGE_LABEL","quickActionMenuItem","quick","external","opensInNewTab","key","label","icon","path","undefined","size","href","to","onClick","disabled","Boolean","unavailableReason","disabledReason","newTab","ListRowActions","props","items","menuItems","direction","spacing","sx","height","alignItems","justifyContent","event","stopPropagation","ListQuickButton","action","displayName","name","title","span","aria-label","destination","componentVariant","LIST_ACTIONS_FIELD","listActionsColumn","render","options","field","headerName","width","align","headerAlign","sortable","filterable","hideable","disableColumnMenu","renderCell","row","columnsSortedBy","columns","columnSort","map","column","sorted","sortingOrder","mode","description","NO_ROWS","TOOLBAR_ONLY_PROPS","disableColumnSelector","NoOverlay","TOOLBAR_ONLY_SLOTS","noRowsOverlay","noResultsOverlay","loadingOverlay","TOOLBAR_ONLY_SX","flex","minHeight","overflow","display","top","ListTable","onOpen","initialState","hideFooter","rows","selectable","quickFilter","slotProps","toolbarOnly","slots","rest","sortProps","sortingMode","sortModel","onSortModelChange","showQuickFilter","filterMode","selection","rowSelection","checkboxSelection","rowSelectionModel","type","ids","Set","selected","onRowSelectionModelChange","model","all","String","$id","onChange","filter","id","has","rowCount","length","footerHidden","unpaged","paginationModel","page","pageSize","Math","max","onPaginationModelChange","pageSizeOptions","getRowId","disableRowSelectionOnClick","onRowClick","target","closest","onCellKeyDown","params","startsWith","currentTarget","defaultMuiPrevented","autoHeight","cursor","Array","isArray","showToolbar","pagination","toolbar","csvOptions","disableToolbarButton","printOptions","labelRowsPerPage"],"mappings":"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"}
|
|
@@ -28,6 +28,12 @@ export interface RowActionsMenuItem {
|
|
|
28
28
|
* nothing on its own, and an absent one and an inapplicable one look alike.
|
|
29
29
|
*/
|
|
30
30
|
disabledReason?: string;
|
|
31
|
+
/**
|
|
32
|
+
* A short second line under the label, always on screen — where a
|
|
33
|
+
* disabled item says why without waiting for a hover, or an item says
|
|
34
|
+
* where it goes when its label alone cannot.
|
|
35
|
+
*/
|
|
36
|
+
description?: string;
|
|
31
37
|
}
|
|
32
38
|
export interface RowActionsMenuProps {
|
|
33
39
|
items: RowActionsMenuItem[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/components/row-actions-menu.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { AppLink, type AppLinkNakedLinkProps } from './app-link'\nimport { MdiIcon } from './mdi-icon/mdi-icon'\nimport { mdiDotsVertical, mdiOpenInNew } from '@aglyn/shared-data-mdi'\nimport { newTabLinkProps, withNewTabHint } from '../utils/new-tab'\nimport {\n IconButton,\n ListItemIcon,\n ListItemText,\n Menu,\n MenuItem,\n Tooltip,\n} from '@mui/material'\nimport {\n forwardRef,\n useCallback,\n useState,\n type MouseEvent,\n type ReactNode,\n} from 'react'\n\nexport interface RowActionsMenuItem {\n key: string\n label: string\n icon?: ReactNode\n /**\n * Where the item navigates. An item that carries one renders as a real\n * anchor, so it can be middle-clicked into a new tab, copied, or opened\n * from the browser's own context menu — the affordances a click handler\n * cannot offer however faithfully it calls `router.push`.\n */\n href?: string\n /**\n * Open `href` in a new tab: a page off the console, or a preview / view of\n * something the reader is glancing at beside this list (AGL-3660). The item\n * gains `noopener noreferrer`, an \"(opens in a new tab)\" accessible name,\n * and a small open-in-new mark after its label.\n */\n external?: boolean\n /** For items that open a dialog rather than navigate. */\n onClick?: () => void\n /** Renders the row in the error colour, for destructive actions. */\n destructive?: boolean\n disabled?: boolean\n /**\n * Why the item cannot be used, shown as its tooltip. Mirrors the quick\n * action's `unavailableReason`: a control that is present but inert says\n * nothing on its own, and an absent one and an inapplicable one look alike.\n */\n disabledReason?: string\n}\n\n/**\n * The anchor a linked menu item renders as.\n *\n * `naked` is the variant that adds no styling of its own — the MenuItem it is\n * the root of already resets colour and text-decoration, so a linked item is\n * pixel-identical to a handler-driven one.\n */\nconst MenuItemLinkComponent = forwardRef<any, AppLinkNakedLinkProps>(\n (props, ref) => <AppLink ref={ref} {...props} componentVariant={'naked'} />,\n)\nMenuItemLinkComponent.displayName = 'MenuItemLinkComponent'\n\nexport interface RowActionsMenuProps {\n items: RowActionsMenuItem[]\n /** Distinguishes this row's menu for screen readers. */\n label?: string\n}\n\n/**\n * Overflow menu for a table row's secondary actions (AGL-701).\n *\n * The DataGrid lists get this from `GridActionsCellItem showInMenu`, but the\n * screens table is hand-rolled markup driven by a `renderRowActions`\n * render-prop, so `showInMenu` is not available to it and it needs a real\n * menu of its own.\n *\n * Every handler closes the menu first. Delete opens a confirmation, and a\n * menu left standing over that dialog reads as though the click missed.\n *\n * An item that names an `href` is an ANCHOR, not a handler — a navigation\n * action must be middle-clickable and copyable like any other link. It still\n * renders as a menu item: the anchor is the MenuItem's own root element, so\n * it inherits the item's colour, padding and focus ring rather than the\n * browser's link styling.\n */\nexport function RowActionsMenu(props: RowActionsMenuProps) {\n const { items, label } = props\n const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)\n const handleOpen = useCallback((event: MouseEvent<HTMLElement>) => {\n // The row itself opens the screen; without this the menu button would\n // navigate out from under the menu it just opened.\n event.stopPropagation()\n setAnchorEl(event.currentTarget)\n }, [])\n const handleClose = useCallback(() => setAnchorEl(null), [])\n if (!items.length) return null\n return (\n <>\n <IconButton\n size=\"small\"\n aria-label={label ? `More actions for ${label}` : 'More actions'}\n aria-haspopup=\"true\"\n onClick={handleOpen}\n >\n <MdiIcon path={mdiDotsVertical.path} size={0.8} />\n </IconButton>\n <Menu\n anchorEl={anchorEl}\n open={Boolean(anchorEl)}\n onClose={handleClose}\n onClick={(event) => event.stopPropagation()}\n >\n {items.map((item) => {\n // A disabled item is never a link: an anchor whose destination is\n // refused still navigates on a middle-click, which is the one route\n // around the disabled state that `pointer-events: none` misses.\n const linkProps =\n item.href && !item.disabled\n ? {\n component: MenuItemLinkComponent,\n href: item.href,\n ...(item.external\n ? {\n ...newTabLinkProps,\n 'aria-label': withNewTabHint(item.label),\n }\n : {}),\n }\n : {}\n const menuItem = (\n <MenuItem\n key={item.key}\n {...(linkProps as any)}\n disabled={item.disabled}\n onClick={() => {\n handleClose()\n item.onClick?.()\n }}\n >\n {item.icon ? (\n <ListItemIcon\n sx={item.destructive ? { color: 'error.main' } : undefined}\n >\n {item.icon}\n </ListItemIcon>\n ) : null}\n <ListItemText\n slotProps={\n item.destructive\n ? { primary: { color: 'error.main' } }\n : undefined\n }\n >\n {item.label}\n </ListItemText>\n {item.external && item.href ? (\n <MdiIcon\n path={mdiOpenInNew.path}\n aria-hidden\n sx={{ fontSize: '0.875rem', ml: 1, color: 'text.secondary' }}\n />\n ) : null}\n </MenuItem>\n )\n // span: a disabled item takes no pointer events, so the tooltip\n // needs a wrapper that does. `MenuList` reads its items from\n // context rather than from its direct children, so the wrapper\n // costs nothing in keyboard navigation.\n return item.disabled && item.disabledReason ? (\n <Tooltip key={item.key} title={item.disabledReason}>\n <span>{menuItem}</span>\n </Tooltip>\n ) : (\n menuItem\n )\n })}\n </Menu>\n </>\n )\n}\n\nRowActionsMenu.displayName = 'RowActionsMenu'\n\nexport default RowActionsMenu\n"],"names":["AppLink","MdiIcon","mdiDotsVertical","mdiOpenInNew","newTabLinkProps","withNewTabHint","IconButton","ListItemIcon","ListItemText","Menu","MenuItem","Tooltip","forwardRef","useCallback","useState","MenuItemLinkComponent","props","ref","componentVariant","displayName","RowActionsMenu","items","label","anchorEl","setAnchorEl","handleOpen","event","stopPropagation","currentTarget","handleClose","length","size","aria-label","aria-haspopup","onClick","path","open","Boolean","onClose","map","item","linkProps","href","disabled","component","external","menuItem","icon","sx","destructive","color","undefined","slotProps","primary","aria-hidden","fontSize","ml","key","disabledReason","title","span"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/components/row-actions-menu.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { AppLink, type AppLinkNakedLinkProps } from './app-link'\nimport { MdiIcon } from './mdi-icon/mdi-icon'\nimport { mdiDotsVertical, mdiOpenInNew } from '@aglyn/shared-data-mdi'\nimport { newTabLinkProps, withNewTabHint } from '../utils/new-tab'\nimport {\n IconButton,\n ListItemIcon,\n ListItemText,\n Menu,\n MenuItem,\n Tooltip,\n} from '@mui/material'\nimport {\n forwardRef,\n useCallback,\n useState,\n type MouseEvent,\n type ReactNode,\n} from 'react'\n\nexport interface RowActionsMenuItem {\n key: string\n label: string\n icon?: ReactNode\n /**\n * Where the item navigates. An item that carries one renders as a real\n * anchor, so it can be middle-clicked into a new tab, copied, or opened\n * from the browser's own context menu — the affordances a click handler\n * cannot offer however faithfully it calls `router.push`.\n */\n href?: string\n /**\n * Open `href` in a new tab: a page off the console, or a preview / view of\n * something the reader is glancing at beside this list (AGL-3660). The item\n * gains `noopener noreferrer`, an \"(opens in a new tab)\" accessible name,\n * and a small open-in-new mark after its label.\n */\n external?: boolean\n /** For items that open a dialog rather than navigate. */\n onClick?: () => void\n /** Renders the row in the error colour, for destructive actions. */\n destructive?: boolean\n disabled?: boolean\n /**\n * Why the item cannot be used, shown as its tooltip. Mirrors the quick\n * action's `unavailableReason`: a control that is present but inert says\n * nothing on its own, and an absent one and an inapplicable one look alike.\n */\n disabledReason?: string\n /**\n * A short second line under the label, always on screen — where a\n * disabled item says why without waiting for a hover, or an item says\n * where it goes when its label alone cannot.\n */\n description?: string\n}\n\n/**\n * The anchor a linked menu item renders as.\n *\n * `naked` is the variant that adds no styling of its own — the MenuItem it is\n * the root of already resets colour and text-decoration, so a linked item is\n * pixel-identical to a handler-driven one.\n */\nconst MenuItemLinkComponent = forwardRef<any, AppLinkNakedLinkProps>(\n (props, ref) => <AppLink ref={ref} {...props} componentVariant={'naked'} />,\n)\nMenuItemLinkComponent.displayName = 'MenuItemLinkComponent'\n\nexport interface RowActionsMenuProps {\n items: RowActionsMenuItem[]\n /** Distinguishes this row's menu for screen readers. */\n label?: string\n}\n\n/**\n * Overflow menu for a table row's secondary actions (AGL-701).\n *\n * The DataGrid lists get this from `GridActionsCellItem showInMenu`, but the\n * screens table is hand-rolled markup driven by a `renderRowActions`\n * render-prop, so `showInMenu` is not available to it and it needs a real\n * menu of its own.\n *\n * Every handler closes the menu first. Delete opens a confirmation, and a\n * menu left standing over that dialog reads as though the click missed.\n *\n * An item that names an `href` is an ANCHOR, not a handler — a navigation\n * action must be middle-clickable and copyable like any other link. It still\n * renders as a menu item: the anchor is the MenuItem's own root element, so\n * it inherits the item's colour, padding and focus ring rather than the\n * browser's link styling.\n */\nexport function RowActionsMenu(props: RowActionsMenuProps) {\n const { items, label } = props\n const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)\n const handleOpen = useCallback((event: MouseEvent<HTMLElement>) => {\n // The row itself opens the screen; without this the menu button would\n // navigate out from under the menu it just opened.\n event.stopPropagation()\n setAnchorEl(event.currentTarget)\n }, [])\n const handleClose = useCallback(() => setAnchorEl(null), [])\n if (!items.length) return null\n return (\n <>\n <IconButton\n size=\"small\"\n aria-label={label ? `More actions for ${label}` : 'More actions'}\n aria-haspopup=\"true\"\n onClick={handleOpen}\n >\n <MdiIcon path={mdiDotsVertical.path} size={0.8} />\n </IconButton>\n <Menu\n anchorEl={anchorEl}\n open={Boolean(anchorEl)}\n onClose={handleClose}\n onClick={(event) => event.stopPropagation()}\n >\n {items.map((item) => {\n // A disabled item is never a link: an anchor whose destination is\n // refused still navigates on a middle-click, which is the one route\n // around the disabled state that `pointer-events: none` misses.\n const linkProps =\n item.href && !item.disabled\n ? {\n component: MenuItemLinkComponent,\n href: item.href,\n ...(item.external\n ? {\n ...newTabLinkProps,\n 'aria-label': withNewTabHint(item.label),\n }\n : {}),\n }\n : {}\n const menuItem = (\n <MenuItem\n key={item.key}\n {...(linkProps as any)}\n disabled={item.disabled}\n onClick={() => {\n handleClose()\n item.onClick?.()\n }}\n >\n {item.icon ? (\n <ListItemIcon\n sx={item.destructive ? { color: 'error.main' } : undefined}\n >\n {item.icon}\n </ListItemIcon>\n ) : null}\n <ListItemText\n slotProps={\n item.destructive\n ? { primary: { color: 'error.main' } }\n : undefined\n }\n secondary={item.description}\n >\n {item.label}\n </ListItemText>\n {item.external && item.href ? (\n <MdiIcon\n path={mdiOpenInNew.path}\n aria-hidden\n sx={{ fontSize: '0.875rem', ml: 1, color: 'text.secondary' }}\n />\n ) : null}\n </MenuItem>\n )\n // span: a disabled item takes no pointer events, so the tooltip\n // needs a wrapper that does. `MenuList` reads its items from\n // context rather than from its direct children, so the wrapper\n // costs nothing in keyboard navigation.\n return item.disabled && item.disabledReason ? (\n <Tooltip key={item.key} title={item.disabledReason}>\n <span>{menuItem}</span>\n </Tooltip>\n ) : (\n menuItem\n )\n })}\n </Menu>\n </>\n )\n}\n\nRowActionsMenu.displayName = 'RowActionsMenu'\n\nexport default RowActionsMenu\n"],"names":["AppLink","MdiIcon","mdiDotsVertical","mdiOpenInNew","newTabLinkProps","withNewTabHint","IconButton","ListItemIcon","ListItemText","Menu","MenuItem","Tooltip","forwardRef","useCallback","useState","MenuItemLinkComponent","props","ref","componentVariant","displayName","RowActionsMenu","items","label","anchorEl","setAnchorEl","handleOpen","event","stopPropagation","currentTarget","handleClose","length","size","aria-label","aria-haspopup","onClick","path","open","Boolean","onClose","map","item","linkProps","href","disabled","component","external","menuItem","icon","sx","destructive","color","undefined","slotProps","primary","secondary","description","aria-hidden","fontSize","ml","key","disabledReason","title","span"],"mappings":"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"}
|
|
@@ -34,28 +34,28 @@ export declare function listFilterColumn(fields: readonly ListFilterField[], col
|
|
|
34
34
|
filterable: boolean;
|
|
35
35
|
filterOperators?: GridFilterOperator[];
|
|
36
36
|
};
|
|
37
|
+
/**
|
|
38
|
+
* How a filter-only column's cell reads a stored value: an option's label
|
|
39
|
+
* when the field has choices (`true` → "Suspended"), a list joined, a
|
|
40
|
+
* Firestore timestamp or a date as the local date and time, Yes/No for a
|
|
41
|
+
* bare boolean, and the value itself otherwise.
|
|
42
|
+
*/
|
|
43
|
+
export declare function listFilterCellText(value: unknown, labels?: ReadonlyMap<string, string>): string;
|
|
37
44
|
/**
|
|
38
45
|
* Fields a reader can filter by that are NOT columns on the table.
|
|
39
46
|
*
|
|
40
47
|
* MUI's filter panel lists COLUMNS — `gridFilterableColumnDefinitionsSelector`
|
|
41
48
|
* reads every column definition, hidden ones included — so a filterable field
|
|
42
49
|
* with no column is a field nobody can reach however well the route answers it.
|
|
43
|
-
* Declaring it as a
|
|
44
|
-
*
|
|
50
|
+
* Declaring it as a hidden column keeps one source of truth, the field list,
|
|
51
|
+
* instead of a second list of "extra filters" that drifts from it.
|
|
45
52
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* would get a strip of blank cells for their trouble.
|
|
53
|
+
* Each draws the row's value (`filterOnlyColumnProps`), so it is an ordinary
|
|
54
|
+
* column in Manage columns — hidden by default, never greyed out as locked.
|
|
49
55
|
*
|
|
50
56
|
* Pair with {@link hiddenFilterVisibility}, which is what actually hides them.
|
|
51
57
|
*/
|
|
52
|
-
export declare function hiddenFilterColumns(fields: readonly ListFilterField[], visible: readonly string[], headers?: Readonly<Record<string, string>>):
|
|
53
|
-
field: string;
|
|
54
|
-
headerName: string;
|
|
55
|
-
hideable: boolean;
|
|
56
|
-
filterable: boolean;
|
|
57
|
-
filterOperators?: GridFilterOperator[];
|
|
58
|
-
}>;
|
|
58
|
+
export declare function hiddenFilterColumns(fields: readonly ListFilterField[], visible: readonly string[], headers?: Readonly<Record<string, string>>): GridColDef[];
|
|
59
59
|
/** The select operators a field's allowed clause operators map to. */
|
|
60
60
|
export declare function listSelectOperators(allowed: readonly string[]): GridFilterOperator<any, any, any, import("@mui/x-data-grid").GridFilterInputValueProps>[];
|
|
61
61
|
/**
|