fix: admin/options: bad layout of "block" on mobile

Massimo Melina committed Sep 2, 2024 at 15:14 UTC 522147b128c61ede36a1d409beb76d7a1f988548
3 files changed +40 -16
admin/src/ArrayField.ts
+23 -9
@@ -4,12 +4,13 @@ import { createElement as h, Fragment, useMemo, useState } from 'react'
4 import { Dict, isOrderedEqual, setHidden, swap } from './misc'
5 import { Add, Edit, Delete, ArrowUpward, ArrowDownward, Undo, Check } from '@mui/icons-material'
6 import { formDialog } from './dialog'
7 -import { DataGrid, GridActionsCellItem, GridAlignment, GridColDef } from '@mui/x-data-grid'
7 +import { GridActionsCellItem, GridAlignment, GridColDef } from '@mui/x-data-grid'
8 import { BoolField, FieldDescriptor, FieldProps, labelFromKey } from '@hfs/mui-grid-form'
9 import { Box, FormHelperText, FormLabel } from '@mui/material'
10 import { DateTimeField } from './DateTimeField'
11 import _ from 'lodash'
12 import { Center, IconBtn } from './mui'
13 +import { DataTable } from './DataTable'
14
15 type ArrayFieldProps<T> = FieldProps<T[]> & { fields: FieldDescriptor[], height?: number, reorder?: boolean, prepend?: boolean, autoRowHeight?: boolean }
16 export function ArrayField<T extends object>({ label, helperText, fields, value, onChange, onError, setApi, reorder, prepend, noRows, valuesForAdd, autoRowHeight, ...rest }: ArrayFieldProps<T>) {
@@ -19,7 +20,7 @@ export function ArrayField<T extends object>({ label, helperText, fields, value,
20 setHidden({ ...x } as any, x.hasOwnProperty('id') ? { $idx } : { id: $idx })),
21 [JSON.stringify(value)]) //eslint-disable-line
22 const form = {
22 - fields: fields.map(({ $width, $column, $type, ...rest }) => _.defaults(rest, byType[$type]?.field))
23 + fields: fields.map(({ $width, $column, $type, $hideUnder, ...rest }) => _.defaults(rest, byType[$type]?.field))
24 }
25 setApi?.({ isEqual: isOrderedEqual }) // don't rely on stringify, as it wouldn't work with non-json values
26 const [undo, setUndo] = useState<typeof value>()
@@ -27,12 +28,14 @@ export function ArrayField<T extends object>({ label, helperText, fields, value,
28 label && h(FormLabel, { sx: { ml: 1 } }, label),
29 helperText && h(FormHelperText, {}, helperText),
30 h(Box, { ...rest },
30 - h(DataGrid, {
31 + h(DataTable, {
32 rows,
33 ...autoRowHeight && { getRowHeight: () => 'auto' as const },
33 - sx: { '.MuiDataGrid-virtualScroller': { minHeight: '3em' },
34 + sx: {
35 + '.MuiDataGrid-virtualScroller': { minHeight: '3em' },
36 ...autoRowHeight && { '.MuiDataGrid-cell': { minHeight: '52px !important' } }
37 },
38 + style: undefined, // override style making it fill the flex
39 hideFooterSelectedRowCount: true,
40 hideFooter: true,
41 slots: {
@@ -56,6 +59,7 @@ export function ArrayField<T extends object>({ label, helperText, fields, value,
59 },
60 ...def,
61 ...f.$width ? { [f.$width >= 8 ? 'width' : 'flex']: f.$width } : (!def?.width && !def?.flex && { flex: 1 }),
62 + hideUnder: f.$hideUnder,
63 ...f.$column,
64 }
65 }),
@@ -96,10 +100,11 @@ export function ArrayField<T extends object>({ label, helperText, fields, value,
100 icon: h(Edit),
101 label: title,
102 title,
99 - onClick(event: MouseEvent) {
103 + onClick(ev: MouseEvent) {
104 + ev.stopPropagation()
105 formDialog<T>({ values: row as any, form, title }).then(x => {
106 if (x)
102 - set(value!.map((oldRec, i) => i === $idx ? x : oldRec), event)
107 + set(value!.map((oldRec, i) => i === $idx ? x : oldRec), ev)
108 })
109 }
110 }),
@@ -107,19 +112,28 @@ export function ArrayField<T extends object>({ label, helperText, fields, value,
112 icon: h(Delete),
113 label: "Delete",
114 showInMenu: reorder,
110 - onClick: ev => set(value!.filter((rec, i) => i !== $idx), ev),
115 + onClick: ev => {
116 + ev.stopPropagation()
117 + set(value!.filter((rec, i) => i !== $idx), ev)
118 + },
119 }),
120 reorder && $idx && h(GridActionsCellItem as any, {
121 icon: h(ArrowUpward),
122 label: "Move up",
123 showInMenu: true,
116 - onClick: ev => set(swap(value!.slice(), $idx, $idx - 1), ev),
124 + onClick: ev => {
125 + ev.stopPropagation()
126 + set(swap(value!.slice(), $idx, $idx - 1), ev)
127 + },
128 }),
129 reorder && $idx < rows.length - 1 && h(GridActionsCellItem as any, {
130 icon: h(ArrowDownward),
131 label: "Move down",
132 showInMenu: true,
122 - onClick: ev => set(swap(value!.slice(), $idx, $idx + 1), ev),
133 + onClick: ev => {
134 + ev.stopPropagation()
135 + set(swap(value!.slice(), $idx, $idx + 1), ev)
136 + },
137 }),
138 ].filter(Boolean)
139 }
admin/src/DataTable.ts
+10 -4
@@ -34,6 +34,7 @@ export function DataTable({ columns, initialState={}, actions, actionsProps, ini
34 const theme = useTheme()
35 const apiRef = useGridApiRef()
36 const [actionsLength, setActionsLength] = useState(0)
37 + const [merged, setMerged] = useState(0)
38 const manipulatedColumns = useMemo(() => {
39 const { localeText } = enUS.components.MuiDataGrid.defaultProps as any
40 const ret = columns.map(col => {
@@ -100,6 +101,8 @@ export function DataTable({ columns, initialState={}, actions, actionsProps, ini
101 && field))
102 const o = Object.fromEntries(fields.map(x => [x, false]))
103 _.merge(initialState, { columns: { columnVisibilityModel: o } })
104 + // count the hidden columns that are merged into visible columns
105 + setMerged(_.sumBy(fields, k => _.find(columns, col => !fields.includes(col.field) && col.mergeRender?.[k]) ? 1 : 0))
106 return fields
107 }, [manipulatedColumns, width])
108 const [vis, setVis] = useState({})
@@ -128,6 +131,9 @@ export function DataTable({ columns, initialState={}, actions, actionsProps, ini
131 columns: manipulatedColumns,
132 apiRef,
133 ...rest,
134 + sx: {
135 + '& .MuiDataGrid-virtualScroller': { minHeight: '3em' } // without this, no-entries gets just 1px
136 + },
137 slots: {
138 noRowsOverlay: () => initializing ? null : h(Center, {}, noRows || "No entries"),
139 footer: CustomFooter,
@@ -142,10 +148,10 @@ export function DataTable({ columns, initialState={}, actions, actionsProps, ini
148 onCellClick({ field, row }) {
149 if (field === ACTIONS) return
150 if (window.getSelection()?.type === 'Range') return // not a click but a drag
145 - const n = apiRef.current.getVisibleColumns().length
146 - const showCols = manipulatedColumns.filter(x =>
151 + const visibleInList = merged + apiRef.current.getVisibleColumns().length
152 + const showInDialog = manipulatedColumns.filter(x =>
153 !x.dialogHidden && (x.renderCell || x.field === ACTIONS || row[x.field] !== undefined))
148 - if (showCols.length <= n) return
154 + if (showInDialog.length <= visibleInList) return // no need for dialog
155 newDialog({
156 title: "Details",
157 onClose() {
@@ -163,7 +169,7 @@ export function DataTable({ columns, initialState={}, actions, actionsProps, ini
169 gridAutoFlow: 'dense',
170 minWidth: 'max(16em, 40vw)',
171 sx: { opacity: curRow ? undefined : .5 },
166 - }, showCols.map(col =>
172 + }, showInDialog.map(col =>
173 h(Box, { key: col.field, gridColumn: col.flex && '1/-1' },
174 h(Box, { bgcolor: '#0003', p: 1 }, col.headerName || col.field),
175 h(Flex, { minHeight: '2.5em', px: 1, wordBreak: 'break-word' },
admin/src/OptionsPage.ts
+7 -3
@@ -155,8 +155,11 @@ export default function OptionsPage() {
155
156 { k: 'block', label: false, comp: ArrayField, prepend: true, sm: true, autoRowHeight: true,
157 fields: [
158 - { k: 'ip', label: "Blocked IP", sm: 12, required: true, wrap: true, helperText: h(WildcardsSupported) },
159 - { k: 'expire', $type: 'dateTime', minDate: new Date(), sm: 6, helperText: "Leave empty for no expiration" },
158 + { k: 'ip', label: "Blocked IP", sm: 12, required: true, wrap: true, $width: 2,
159 + $column: { mergeRender: { comment: {}, expire: {} } },
160 + helperText: h(WildcardsSupported) },
161 + { k: 'expire', $type: 'dateTime', minDate: new Date(), sm: 6, $hideUnder: 'sm',
162 + helperText: "Leave empty for no expiration" },
163 {
164 k: 'disabled',
165 $type: 'boolean',
@@ -165,8 +168,9 @@ export default function OptionsPage() {
168 toField: (x: any) => !x,
169 fromField: (x: any) => x ? undefined : true,
170 sm: 6,
171 + $width: 80,
172 },
169 - { k: 'comment' },
173 + { k: 'comment', $hideUnder: 'sm' },
174 ],
175 },
176