support table-level filters (#587)

This commit is contained in:
Sidney Alcantara
2022-01-13 13:11:45 +11:00
parent 5fa768fcd4
commit 6b5cf37f95
11 changed files with 651 additions and 322 deletions

View File

@@ -7,12 +7,17 @@ import { Control, useWatch } from "react-hook-form";
export interface IAutosaveProps {
control: Control;
handleSave: (values: any) => void;
debounce?: number;
}
export default function FormAutosave({ control, handleSave }: IAutosaveProps) {
export default function FormAutosave({
control,
handleSave,
debounce = 1000,
}: IAutosaveProps) {
const values = useWatch({ control });
const [debouncedValue] = useDebounce(values, 1000, {
const [debouncedValue] = useDebounce(values, debounce, {
equalityFn: _isEqual,
});

View File

@@ -1,316 +0,0 @@
import { useState, useEffect, Suspense, createElement } from "react";
import _find from "lodash/find";
import _sortBy from "lodash/sortBy";
import _isEmpty from "lodash/isEmpty";
import { useForm } from "react-hook-form";
import {
Popover,
Button,
IconButton,
Grid,
MenuItem,
TextField,
Chip,
InputLabel,
} from "@mui/material";
import FilterIcon from "@mui/icons-material/FilterList";
import CloseIcon from "@mui/icons-material/Close";
import ButtonWithStatus from "@src/components/ButtonWithStatus";
import FormAutosave from "@src/components/Table/ColumnMenu/FieldSettings/FormAutosave";
import FieldSkeleton from "@src/components/SideDrawer/Form/FieldSkeleton";
import { FieldType } from "@src/constants/fields";
import { TableFilter } from "@src/hooks/useTable";
import { useProjectContext } from "@src/contexts/ProjectContext";
import { useAppContext } from "@src/contexts/AppContext";
import { DocActions } from "@src/hooks/useDoc";
import { getFieldProp } from "@src/components/fields";
const getType = (column) =>
column.type === FieldType.derivative
? column.config.renderFieldType
: column.type;
export default function Filters() {
const { tableState, tableActions } = useProjectContext();
const { userDoc } = useAppContext();
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
useEffect(() => {
if (userDoc.state.doc && tableState?.config.id) {
if (userDoc.state.doc.tables?.[tableState?.config.id]?.filters) {
tableActions?.table.filter(
userDoc.state.doc.tables[tableState?.config.id].filters
);
tableActions?.table.orderBy();
}
}
}, [userDoc.state, tableState?.config.id]);
const filterColumns = _sortBy(Object.values(tableState!.columns), "index")
.filter((c) => getFieldProp("filter", c.type))
.map((c) => ({
key: c.key,
label: c.name,
type: c.type,
options: c.options,
...c,
}));
const [selectedColumn, setSelectedColumn] = useState<any>();
const [query, setQuery] = useState<TableFilter>({
key: "",
operator: "",
value: "",
});
const [selectedFilter, setSelectedFilter] = useState<any>();
const type = selectedColumn ? getType(selectedColumn) : null;
useEffect(() => {
if (selectedColumn) {
const _filter = getFieldProp("filter", selectedColumn.type);
setSelectedFilter(_filter);
let updatedQuery: TableFilter = {
key: selectedColumn.key,
operator: _filter.operators[0].value,
value: _filter.defaultValue,
};
setQuery(updatedQuery);
}
}, [selectedColumn]);
const handleClose = () => setAnchorEl(null);
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(anchorEl ? null : event.currentTarget);
};
const handleChangeColumn = (e) => {
const column = _find(filterColumns, (c) => c.key === e.target.value);
setSelectedColumn(column);
};
const open = Boolean(anchorEl);
const id = open ? "simple-popper" : undefined;
const handleUpdateFilters = (filters: TableFilter[]) => {
userDoc.dispatch({
action: DocActions.update,
data: {
tables: { [`${tableState?.config.id}`]: { filters } },
},
});
};
const { control } = useForm({
mode: "onBlur",
});
return (
<>
<Grid container direction="row" wrap="nowrap" style={{ width: "auto" }}>
<ButtonWithStatus
variant="outlined"
color="primary"
onClick={handleClick}
startIcon={<FilterIcon />}
active={tableState?.filters && tableState?.filters.length > 0}
sx={
tableState?.filters && tableState?.filters.length > 0
? {
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
position: "relative",
zIndex: 1,
}
: {}
}
>
{tableState?.filters && tableState?.filters.length > 0
? "Filtered"
: "Filter"}
</ButtonWithStatus>
{(tableState?.filters ?? []).map((filter) => (
<Chip
key={filter.key}
label={`${filter.key} ${filter.operator} ${
selectedFilter?.valueFormatter
? selectedFilter.valueFormatter(filter.value)
: filter.value
}`}
onDelete={() => handleUpdateFilters([])}
sx={{
borderRadius: 1,
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
borderLeft: "none",
backgroundColor: "background.paper",
height: 32,
"& .MuiChip-label": { px: 1.5 },
}}
variant="outlined"
/>
))}
</Grid>
<Popover
id={id}
open={open}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
transformOrigin={{ vertical: "top", horizontal: "left" }}
sx={{
"& .MuiPaper-root": { width: 640 },
"& .content": { py: 3, px: 2 },
}}
>
<IconButton
onClick={handleClose}
sx={{
position: "absolute",
top: (theme) => theme.spacing(0.5),
right: (theme) => theme.spacing(0.5),
}}
>
<CloseIcon />
</IconButton>
<div className="content">
<Grid container spacing={2}>
<Grid item xs={4}>
<TextField
label="Column"
select
variant="filled"
hiddenLabel
fullWidth
value={selectedColumn?.key ?? ""}
onChange={handleChangeColumn}
SelectProps={{ displayEmpty: true }}
>
<MenuItem disabled value="" style={{ display: "none" }}>
Select column
</MenuItem>
{filterColumns.map((c) => (
<MenuItem key={c.key} value={c.key}>
{c.label}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={4}>
<TextField
label="Condition"
select
variant="filled"
hiddenLabel
fullWidth
value={query.operator}
disabled={!query.key || selectedFilter?.operators?.length === 0}
onChange={(e) => {
setQuery((query) => ({
...query,
operator: e.target.value as string,
}));
}}
SelectProps={{ displayEmpty: true }}
>
<MenuItem disabled value="" style={{ display: "none" }}>
Select condition
</MenuItem>
{selectedFilter?.operators.map((operator) => (
<MenuItem key={operator.value} value={operator.value}>
{operator.label}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={4}>
{query.key && query.operator && (
<form>
<InputLabel
variant="filled"
id={`filters-label-${query.key}`}
htmlFor={`sidedrawer-field-${query.key}`}
>
Value
</InputLabel>
<FormAutosave
control={control}
handleSave={(values) =>
setQuery((query) => ({
...query,
value: values[query.key],
}))
}
/>
<Suspense fallback={<FieldSkeleton />}>
{query.operator &&
createElement(getFieldProp("SideDrawerField", type), {
column: selectedColumn,
control,
docRef: {},
disabled: false,
onChange: () => {},
})}
</Suspense>
</form>
)}
</Grid>
</Grid>
<Grid
container
sx={{
mt: 3,
"& .MuiButton-root": { minWidth: 100 },
}}
justifyContent="center"
spacing={1}
>
<Grid item>
<Button
disabled={query.key === ""}
onClick={() => {
handleUpdateFilters([]);
setQuery({
key: "",
operator: "",
value: "",
});
setSelectedColumn(null);
}}
>
Clear
</Button>
</Grid>
<Grid item>
<Button
disabled={
query.value !== true &&
query.value !== false &&
_isEmpty(query.value) &&
typeof query.value !== "number" &&
typeof query.value !== "object"
}
color="primary"
variant="contained"
onClick={() => {
handleUpdateFilters([query]);
handleClose();
}}
>
Apply
</Button>
</Grid>
</Grid>
</div>
</Popover>
</>
);
}

View File

@@ -0,0 +1,121 @@
import { Suspense, createElement } from "react";
import { useForm } from "react-hook-form";
import { Grid, MenuItem, TextField, InputLabel } from "@mui/material";
import MultiSelect from "@rowy/multiselect";
import FormAutosave from "@src/components/Table/ColumnMenu/FieldSettings/FormAutosave";
import FieldSkeleton from "@src/components/SideDrawer/Form/FieldSkeleton";
import type { useFilterInputs } from "./useFilterInputs";
import { FieldType } from "@src/constants/fields";
import { getFieldProp } from "@src/components/fields";
export interface IFilterInputsProps extends ReturnType<typeof useFilterInputs> {
disabled?: boolean;
}
export default function FilterInputs({
filterColumns,
selectedColumn,
handleChangeColumn,
availableFilters,
query,
setQuery,
disabled,
}: IFilterInputsProps) {
// Need to use react-hook-form with autosave for the value field,
// since we render the side drawer field for that type
const { control } = useForm({
mode: "onBlur",
defaultValues: selectedColumn ? { [selectedColumn.key]: query.value } : {},
});
// Get column type to render for the value field
const columnType = selectedColumn
? selectedColumn.type === FieldType.derivative
? selectedColumn.config.renderFieldType
: selectedColumn.type
: null;
return (
<Grid container spacing={2} sx={{ mb: 3 }}>
<Grid item xs={4}>
<MultiSelect
multiple={false}
label="Column"
options={filterColumns}
value={query.key}
onChange={handleChangeColumn}
disabled={disabled}
/>
</Grid>
<Grid item xs={4}>
<TextField
label="Operator"
select
variant="filled"
fullWidth
value={query.operator}
disabled={
disabled || !query.key || availableFilters?.operators?.length === 0
}
onChange={(e) => {
setQuery((query) => ({
...query,
operator: e.target.value as string,
}));
}}
SelectProps={{ displayEmpty: true }}
>
<MenuItem disabled value="" style={{ display: "none" }}>
Select operator
</MenuItem>
{availableFilters?.operators.map((operator) => (
<MenuItem key={operator.value} value={operator.value}>
{operator.label}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={4}>
{query.key && query.operator && (
<form>
<InputLabel
variant="filled"
id={`filters-label-${query.key}`}
htmlFor={`sidedrawer-field-${query.key}`}
>
Value
</InputLabel>
<FormAutosave
debounce={0}
control={control}
handleSave={(values) => {
if (values[query.key] !== undefined) {
setQuery((query) => ({
...query,
value: values[query.key],
}));
}
}}
/>
<Suspense fallback={<FieldSkeleton />}>
{createElement(getFieldProp("SideDrawerField", columnType), {
column: selectedColumn,
control,
docRef: {},
disabled,
onChange: () => {},
})}
</Suspense>
</form>
)}
</Grid>
</Grid>
);
}

View File

@@ -0,0 +1,106 @@
import { useRef, useState } from "react";
import { Popover, Stack, Chip } from "@mui/material";
import FilterIcon from "@mui/icons-material/FilterList";
import ButtonWithStatus from "@src/components/ButtonWithStatus";
import type { TableFilter } from "@src/hooks/useTable";
import type { useFilterInputs } from "./useFilterInputs";
export interface IFiltersPopoverProps {
appliedFilters: TableFilter[];
hasAppliedFilters: boolean;
hasTableFilters: boolean;
tableFiltersOverridden: boolean;
availableFilters: ReturnType<typeof useFilterInputs>["availableFilters"];
setUserFilters: (filters: TableFilter[]) => void;
children: (props: { handleClose: () => void }) => React.ReactNode;
}
export default function FiltersPopover({
appliedFilters,
hasAppliedFilters,
hasTableFilters,
tableFiltersOverridden,
setUserFilters,
availableFilters,
children,
}: IFiltersPopoverProps) {
const anchorEl = useRef<HTMLButtonElement>(null);
const [open, setOpen] = useState(false);
const popoverId = open ? "filters-popover" : undefined;
const handleClose = () => setOpen(false);
return (
<>
<Stack direction="row" style={{ width: "auto" }}>
<ButtonWithStatus
ref={anchorEl}
variant="outlined"
color="primary"
onClick={() => setOpen(true)}
startIcon={<FilterIcon />}
active={hasAppliedFilters}
sx={
hasAppliedFilters
? {
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
position: "relative",
zIndex: 1,
}
: {}
}
aria-describedby={popoverId}
>
{hasAppliedFilters ? "Filtered" : "Filter"}
</ButtonWithStatus>
{appliedFilters.map((filter) => (
<Chip
key={filter.key}
label={`${filter.key} ${filter.operator} ${
availableFilters?.valueFormatter
? availableFilters.valueFormatter(filter.value)
: filter.value
}`}
onDelete={
hasTableFilters && !tableFiltersOverridden
? undefined
: () => setUserFilters([])
}
sx={{
borderRadius: 1,
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
borderLeft: "none",
backgroundColor: "background.paper",
height: 32,
"& .MuiChip-label": { px: 1.5 },
}}
variant="outlined"
/>
))}
</Stack>
<Popover
id={popoverId}
open={open}
anchorEl={anchorEl.current}
onClose={handleClose}
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
transformOrigin={{ vertical: "top", horizontal: "left" }}
sx={{
"& .MuiPaper-root": { width: 640 },
"& .content": { p: 3 },
}}
>
{children({ handleClose })}
</Popover>
</>
);
}

View File

@@ -0,0 +1,332 @@
import { useState, useEffect } from "react";
import _isEmpty from "lodash/isEmpty";
import {
Tab,
Badge,
Button,
Stack,
Divider,
FormControlLabel,
Checkbox,
Alert,
} from "@mui/material";
import TabContext from "@mui/lab/TabContext";
import TabList from "@mui/lab/TabList";
import TabPanel from "@mui/lab/TabPanel";
import FiltersPopover from "./FiltersPopover";
import FilterInputs from "./FilterInputs";
import { useFilterInputs, INITIAL_QUERY } from "./useFilterInputs";
import type { TableFilter } from "@src/hooks/useTable";
import { useProjectContext } from "@src/contexts/ProjectContext";
import { useAppContext } from "@src/contexts/AppContext";
import { DocActions } from "@src/hooks/useDoc";
const shouldDisableApplyButton = (value: any) =>
_isEmpty(value) &&
typeof value !== "boolean" &&
typeof value !== "number" &&
typeof value !== "object";
export default function Filters() {
const { table, tableState, tableActions } = useProjectContext();
const { userDoc, userClaims } = useAppContext();
const tableFilterInputs = useFilterInputs(tableState?.columns || []);
const userFilterInputs = useFilterInputs(tableState?.columns || []);
const { availableFilters } = userFilterInputs;
// Get table filters & user filters from config documents
const tableId = table?.id;
const userDocData = userDoc.state.doc;
const tableSchemaDoc = tableState?.config?.tableConfig?.doc;
const tableFilters = tableSchemaDoc?.filters;
const userFilters = tableId
? userDocData.tables?.[tableId]?.filters
: undefined;
// Helper booleans
const hasTableFilters =
Array.isArray(tableFilters) && tableFilters.length > 0;
const hasUserFilters = Array.isArray(userFilters) && userFilters.length > 0;
// Set the local table filter
useEffect(() => {
// Set local state for UI
tableFilterInputs.setQuery(
Array.isArray(tableFilters) && tableFilters[0]
? tableFilters[0]
: INITIAL_QUERY
);
userFilterInputs.setQuery(
Array.isArray(userFilters) && userFilters[0]
? userFilters[0]
: INITIAL_QUERY
);
if (!tableActions) return;
let filtersToApply: TableFilter[] = [];
// Allow admin to override table-level filters with their own
// Set to null to show all filters for the admin user
if (
userClaims?.roles.includes("ADMIN") &&
(hasUserFilters || userFilters === null)
) {
filtersToApply = userFilters ?? [];
} else if (hasTableFilters) {
filtersToApply = tableFilters;
} else if (hasUserFilters) {
filtersToApply = userFilters;
}
tableActions.table.filter(filtersToApply);
// Reset order so we dont have to make a new index
tableActions.table.orderBy();
}, [tableFilters, userFilters, userClaims?.roles]);
// Helper booleans for local table filter state
const appliedFilters = tableState?.filters || [];
const hasAppliedFilters = Boolean(
appliedFilters && appliedFilters.length > 0
);
const tableFiltersOverridden =
userClaims?.roles.includes("ADMIN") &&
(hasUserFilters || userFilters === null) &&
hasTableFilters;
// ADMIN overrides
const [tab, setTab] = useState<"user" | "table">(
hasTableFilters && !tableFiltersOverridden ? "table" : "user"
);
const [overrideTableFilters, setOverrideTableFilters] = useState(
tableFiltersOverridden
);
// Save table filters to table schema document
const setTableFilters = (filters: TableFilter[]) => {
tableActions?.table.updateConfig("filters", filters);
};
// Save user filters to user document
// null overrides table filters - only available to ADMINs
const setUserFilters = (filters: TableFilter[] | null) => {
userDoc.dispatch({
action: DocActions.update,
data: {
tables: { [`${tableState?.config.id}`]: { filters } },
},
});
};
return (
<FiltersPopover
appliedFilters={appliedFilters}
hasAppliedFilters={hasAppliedFilters}
hasTableFilters={hasTableFilters}
tableFiltersOverridden={tableFiltersOverridden}
availableFilters={availableFilters}
setUserFilters={setUserFilters}
>
{({ handleClose }) =>
// ADMIN
userClaims?.roles.includes("ADMIN") ? (
<TabContext value={tab}>
<TabList
onChange={(_, v) => setTab(v)}
variant="fullWidth"
aria-label="Filter tabs"
>
<Tab
label={
<>
Your filter
{tableFiltersOverridden && (
<Badge
aria-label="(overrides table filters)"
color="primary"
variant="inlineDot"
invisible={false}
/>
)}
</>
}
value="user"
style={{ flexDirection: "row" }}
/>
<Tab
label={
<>
Table filter
{tableFiltersOverridden ? (
<Badge
aria-label="(overridden by your filters)"
color="primary"
variant="inlineDot"
invisible={false}
sx={{
"& .MuiBadge-badge": {
bgcolor: "transparent",
border: "1px solid currentColor",
color: "inherit",
},
}}
/>
) : hasTableFilters ? (
<Badge
aria-label="(active)"
color="primary"
variant="inlineDot"
invisible={false}
/>
) : null}
</>
}
value="table"
style={{ flexDirection: "row" }}
/>
</TabList>
<Divider style={{ marginTop: -1 }} />
<TabPanel value="user" className="content">
<FilterInputs {...userFilterInputs} />
<FormControlLabel
control={
<Checkbox
checked={overrideTableFilters}
onChange={(e) => setOverrideTableFilters(e.target.checked)}
/>
}
label="Override table filters"
sx={{ justifyContent: "center", mb: 1, mr: 0 }}
/>
<Stack
direction="row"
sx={{ "& .MuiButton-root": { minWidth: 100 } }}
justifyContent="center"
spacing={1}
>
<Button
disabled={
!overrideTableFilters &&
!tableFiltersOverridden &&
userFilterInputs.query.key === ""
}
onClick={() => {
setUserFilters(overrideTableFilters ? null : []);
userFilterInputs.resetQuery();
}}
>
Clear
</Button>
<Button
disabled={
(!overrideTableFilters && hasTableFilters) ||
shouldDisableApplyButton(userFilterInputs.query.value)
}
color="primary"
variant="contained"
onClick={() => {
setUserFilters([userFilterInputs.query]);
handleClose();
}}
>
Apply
</Button>
</Stack>
</TabPanel>
<TabPanel value="table" className="content">
<FilterInputs {...tableFilterInputs} />
<Alert severity="info" style={{ width: "auto" }} sx={{ mb: 3 }}>
The filter above will be set for all users who view this table.
Only ADMIN users can override or edit this.
</Alert>
<Stack
direction="row"
sx={{ "& .MuiButton-root": { minWidth: 100 } }}
justifyContent="center"
spacing={1}
>
<Button
disabled={tableFilterInputs.query.key === ""}
onClick={() => {
setTableFilters([]);
tableFilterInputs.resetQuery();
}}
>
Clear
</Button>
<Button
disabled={shouldDisableApplyButton(
tableFilterInputs.query.value
)}
color="primary"
variant="contained"
onClick={() => {
setTableFilters([tableFilterInputs.query]);
handleClose();
}}
>
Apply
</Button>
</Stack>
</TabPanel>
</TabContext>
) : // Non-ADMIN cannot override table filters
hasTableFilters ? (
<div className="content">
<FilterInputs {...tableFilterInputs} disabled />
<Alert severity="info" style={{ width: "auto" }}>
An ADMIN user has set the filter for this table
</Alert>
</div>
) : (
// Non-ADMIN can set own filters, since there are no table filters
<div className="content">
<FilterInputs {...userFilterInputs} />
<Stack
direction="row"
sx={{ "& .MuiButton-root": { minWidth: 100 } }}
justifyContent="center"
spacing={1}
>
<Button
disabled={userFilterInputs.query.key === ""}
onClick={() => {
setUserFilters([]);
userFilterInputs.resetQuery();
}}
>
Clear
</Button>
<Button
disabled={shouldDisableApplyButton(
userFilterInputs.query.value
)}
color="primary"
variant="contained"
onClick={() => {
setUserFilters([userFilterInputs.query]);
handleClose();
}}
>
Apply
</Button>
</Stack>
</div>
)
}
</FiltersPopover>
);
}

View File

@@ -0,0 +1,50 @@
import { useState } from "react";
import _find from "lodash/find";
import _sortBy from "lodash/sortBy";
import { TableState, TableFilter } from "@src/hooks/useTable";
import { getFieldProp } from "@src/components/fields";
export const INITIAL_QUERY = { key: "", operator: "", value: "" };
export const useFilterInputs = (columns: TableState["columns"]) => {
// Get list of columns that can be filtered
const filterColumns = _sortBy(Object.values(columns), "index")
.filter((c) => getFieldProp("filter", c.type))
.map((c) => ({ value: c.key, label: c.name, ...c }));
// State for filter inputs
const [query, setQuery] = useState<TableFilter>(INITIAL_QUERY);
const resetQuery = () => setQuery(INITIAL_QUERY);
// When the user sets a new column, automatically set the operator and value
const handleChangeColumn = (value: string | null) => {
const column = _find(filterColumns, ["key", value]);
if (column) {
const filter = getFieldProp("filter", column.type);
setQuery({
key: column.key,
operator: filter.operators[0].value,
value: filter.defaultValue ?? "",
});
} else {
setQuery(INITIAL_QUERY);
}
};
// Get the column config
const selectedColumn = _find(filterColumns, ["key", query?.key]);
// Get available filters from selected column type
const availableFilters = getFieldProp("filter", selectedColumn?.type);
return {
filterColumns,
selectedColumn,
handleChangeColumn,
availableFilters,
query,
setQuery,
resetQuery,
} as const;
};

View File

@@ -3,13 +3,13 @@ import { Stack } from "@mui/material";
import { isCollectionGroup } from "@src/utils/fns";
import AddRow from "./AddRow";
import Filters from "../Table/Filters";
import Filters from "./Filters";
import ImportCSV from "./ImportCsv";
import Export from "./Export";
import LoadedRowsStatus from "./LoadedRowsStatus";
import TableSettings from "./TableSettings";
import CloudLogs from "./CloudLogs";
import HiddenFields from "../Table/HiddenFields";
import HiddenFields from "./HiddenFields";
import RowHeight from "./RowHeight";
import Extensions from "./Extensions";
import Webhooks from "./Webhooks";

View File

@@ -15,7 +15,7 @@ export type TableActions = {
table: {
set: (id: string, collection: string, filters: TableFilter[]) => void;
filter: Function;
updateConfig: Function;
updateConfig: (key: string, value: any, callback?: Function) => void;
orderBy: Function;
};
};

View File

@@ -154,7 +154,7 @@ const useTableConfig = (tableId?: string) => {
* @param key name of parameter eg. rowHeight
* @param value new value eg. 65
*/
const updateConfig = (key: string, value: unknown, callback?: Function) => {
const updateConfig = (key: string, value: any, callback?: Function) => {
documentDispatch({
action: DocActions.update,
data: { [key]: value },

View File

@@ -35,6 +35,11 @@ declare module "@mui/material/MenuItem" {
error: true;
}
}
declare module "@mui/material/Badge" {
interface BadgePropsVariantOverrides {
inlineDot: true;
}
}
export const components = (theme: Theme): ThemeOptions => {
const buttonPrimaryHover = colord(theme.palette.primary.main)
@@ -1002,6 +1007,10 @@ export const components = (theme: Theme): ThemeOptions => {
marginTop: 4,
},
},
"&:hover .MuiCheckbox-root, &:hover .MuiRadio-root": {
backgroundColor: theme.palette.action.hover,
},
},
label: {
marginTop: 10,
@@ -1068,6 +1077,28 @@ export const components = (theme: Theme): ThemeOptions => {
},
},
MuiBadge: {
variants: [
{
props: { variant: "inlineDot" },
style: {
marginLeft: theme.spacing(1),
marginRight: theme.spacing(-1),
"& .MuiBadge-badge": {
position: "static",
transform: "none",
minWidth: theme.spacing(1),
height: theme.spacing(1),
borderRadius: theme.spacing(0.5),
padding: 0,
},
},
},
],
},
MuiAlertTitle: {
styleOverrides: {
root: {