mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-29 00:16:39 +01:00
Merge branch 'develop' of https://github.com/rowyio/rowy into develop
This commit is contained in:
91
src/components/InfoTooltip.tsx
Normal file
91
src/components/InfoTooltip.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import { useState } from "react";
|
||||
import _merge from "lodash/merge";
|
||||
|
||||
import { Tooltip, IconButton } from "@mui/material";
|
||||
import { alpha } from "@mui/material/styles";
|
||||
import InfoIcon from "@mui/icons-material/InfoOutlined";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
|
||||
export interface IInfoTooltipProps {
|
||||
description: React.ReactNode;
|
||||
buttonLabel?: string;
|
||||
defaultOpen?: boolean;
|
||||
|
||||
buttonProps?: Partial<React.ComponentProps<typeof IconButton>>;
|
||||
tooltipProps?: Partial<React.ComponentProps<typeof Tooltip>>;
|
||||
iconProps?: Partial<React.ComponentProps<typeof InfoIcon>>;
|
||||
}
|
||||
|
||||
export default function InfoTooltip({
|
||||
description,
|
||||
buttonLabel = "Info",
|
||||
defaultOpen,
|
||||
|
||||
buttonProps,
|
||||
tooltipProps,
|
||||
iconProps,
|
||||
}: IInfoTooltipProps) {
|
||||
const [open, setOpen] = useState(defaultOpen || false);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<>
|
||||
{description}
|
||||
<IconButton
|
||||
aria-label={`Close ${buttonLabel}`}
|
||||
size="small"
|
||||
onClick={() => setOpen(false)}
|
||||
sx={{
|
||||
m: -0.5,
|
||||
opacity: 0.8,
|
||||
"&:hover": {
|
||||
backgroundColor: (theme) =>
|
||||
alpha("#fff", theme.palette.action.hoverOpacity),
|
||||
},
|
||||
}}
|
||||
color="inherit"
|
||||
>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</>
|
||||
}
|
||||
disableFocusListener
|
||||
disableHoverListener
|
||||
disableTouchListener
|
||||
arrow
|
||||
placement="right-start"
|
||||
describeChild
|
||||
{...tooltipProps}
|
||||
open={open}
|
||||
componentsProps={_merge(
|
||||
{
|
||||
tooltip: {
|
||||
style: {
|
||||
marginLeft: "8px",
|
||||
transformOrigin: "-8px 14px",
|
||||
},
|
||||
sx: {
|
||||
typography: "body2",
|
||||
|
||||
display: "flex",
|
||||
gap: 1.5,
|
||||
alignItems: "flex-start",
|
||||
pr: 0.5,
|
||||
},
|
||||
},
|
||||
},
|
||||
tooltipProps?.componentsProps
|
||||
)}
|
||||
>
|
||||
<IconButton
|
||||
aria-label={buttonLabel}
|
||||
size="small"
|
||||
{...buttonProps}
|
||||
onClick={() => setOpen((x) => !x)}
|
||||
>
|
||||
{buttonProps?.children || <InfoIcon fontSize="small" {...iconProps} />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import InfoIcon from "@mui/icons-material/InfoOutlined";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import ReadOnlyIcon from "@mui/icons-material/EditOffOutlined";
|
||||
|
||||
import InfoTooltip from "@src/components/InfoTooltip";
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
import useRouter from "@src/hooks/useRouter";
|
||||
@@ -104,62 +105,18 @@ export default function Breadcrumbs({ sx = [], ...props }: BreadcrumbsProps) {
|
||||
<ReadOnlyIcon fontSize="small" sx={{ ml: 0.5 }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{crumb === table?.id && table?.description && (
|
||||
<Tooltip
|
||||
title={
|
||||
<>
|
||||
{table?.description}
|
||||
<IconButton
|
||||
aria-label="Close table info"
|
||||
size="small"
|
||||
onClick={() => setOpenInfo(false)}
|
||||
sx={{ m: -0.5 }}
|
||||
color="inherit"
|
||||
>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</>
|
||||
}
|
||||
disableFocusListener
|
||||
disableHoverListener
|
||||
disableTouchListener
|
||||
arrow
|
||||
placement="right-start"
|
||||
describeChild
|
||||
open={openInfo}
|
||||
componentsProps={{
|
||||
popper: { sx: { zIndex: "appBar" } } as any,
|
||||
tooltip: {
|
||||
style: { marginLeft: "8px" },
|
||||
sx: {
|
||||
// bgcolor: "background.paper",
|
||||
// color: "text.primary",
|
||||
typography: "body2",
|
||||
boxShadow: 2,
|
||||
maxWidth: "75vw",
|
||||
|
||||
display: "flex",
|
||||
gap: 1.5,
|
||||
alignItems: "flex-start",
|
||||
pr: 0.5,
|
||||
},
|
||||
},
|
||||
arrow: {
|
||||
sx: {
|
||||
// color: "background.paper",
|
||||
"&::before": { boxShadow: 2 },
|
||||
},
|
||||
},
|
||||
{crumb === table?.id && table?.description && (
|
||||
<InfoTooltip
|
||||
description={table?.description}
|
||||
buttonLabel="Table info"
|
||||
tooltipProps={{
|
||||
componentsProps: {
|
||||
popper: { sx: { zIndex: "appBar" } },
|
||||
tooltip: { sx: { maxWidth: "75vw" } },
|
||||
} as any,
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
aria-label="Table info"
|
||||
size="small"
|
||||
onClick={() => setOpenInfo((x) => !x)}
|
||||
>
|
||||
<InfoIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useContext } from "react";
|
||||
import { useState } from "react";
|
||||
import { parse as json2csv } from "json2csv";
|
||||
import { saveAs } from "file-saver";
|
||||
import { useSnackbar } from "notistack";
|
||||
@@ -9,7 +9,16 @@ import _sortBy from "lodash/sortBy";
|
||||
import { isString } from "lodash";
|
||||
import MultiSelect from "@rowy/multiselect";
|
||||
|
||||
import { Button, DialogActions } from "@mui/material";
|
||||
import {
|
||||
Button,
|
||||
DialogActions,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
RadioGroup,
|
||||
FormControlLabel,
|
||||
Radio,
|
||||
FormHelperText,
|
||||
} from "@mui/material";
|
||||
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
@@ -100,7 +109,7 @@ export default function Export({ query, closeModal }) {
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const [columns, setColumns] = useState<any[]>([]);
|
||||
const [exportType, setExportType] = useState<"csv" | "json">("csv");
|
||||
const [exportType, setExportType] = useState<"csv" | "tsv" | "json">("csv");
|
||||
|
||||
const handleClose = () => {
|
||||
closeModal();
|
||||
@@ -130,10 +139,14 @@ export default function Export({ query, closeModal }) {
|
||||
.id!}-${new Date().toISOString()}.${exportType}`;
|
||||
switch (exportType) {
|
||||
case "csv":
|
||||
case "tsv":
|
||||
const csvData = docs.map((doc: any) =>
|
||||
columns.reduce(selectedColumnsCsvReducer(doc), {})
|
||||
);
|
||||
const csv = json2csv(csvData);
|
||||
const csv = json2csv(
|
||||
csvData,
|
||||
exportType === "tsv" ? { delimiter: "\t" } : undefined
|
||||
);
|
||||
const csvBlob = new Blob([csv], {
|
||||
type: `text/${exportType};charset=utf-8`,
|
||||
});
|
||||
@@ -174,23 +187,23 @@ export default function Export({ query, closeModal }) {
|
||||
selectAll
|
||||
/>
|
||||
|
||||
<MultiSelect
|
||||
value={exportType}
|
||||
options={[
|
||||
{ label: ".json", value: "json" },
|
||||
{ label: ".csv", value: "csv" },
|
||||
]}
|
||||
label="Export type"
|
||||
onChange={(v) => {
|
||||
if (v) {
|
||||
setExportType(v as "csv" | "json");
|
||||
}
|
||||
}}
|
||||
multiple={false}
|
||||
searchable={false}
|
||||
clearable={false}
|
||||
TextFieldProps={{ helperText: "Encoding: UTF-8" }}
|
||||
/>
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">Export type</FormLabel>
|
||||
<RadioGroup
|
||||
aria-label="export type"
|
||||
name="export-type-radio-buttons-group"
|
||||
value={exportType}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value;
|
||||
if (v) setExportType(v as "csv" | "tsv" | "json");
|
||||
}}
|
||||
>
|
||||
<FormControlLabel value="csv" control={<Radio />} label=".csv" />
|
||||
<FormControlLabel value="tsv" control={<Radio />} label=".tsv" />
|
||||
<FormControlLabel value="json" control={<Radio />} label=".json" />
|
||||
</RadioGroup>
|
||||
<FormHelperText>Encoding: UTF-8</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
<div style={{ flexGrow: 1, marginTop: 0 }} />
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ const useStyles = makeStyles((theme) =>
|
||||
paper: {
|
||||
[theme.breakpoints.up("sm")]: {
|
||||
maxWidth: 440,
|
||||
height: 610,
|
||||
height: 640,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -103,8 +103,8 @@ export default function Export() {
|
||||
<DialogContent style={{ flexGrow: 0, flexShrink: 0 }}>
|
||||
{(tableState?.filters && tableState?.filters.length !== 0) ||
|
||||
(tableState?.orderBy && tableState?.orderBy.length !== 0)
|
||||
? "The filters and sorting applied to the table will be used in the export."
|
||||
: "No filters or sorting will be applied on the exported data."}
|
||||
? "The filters and sorting applied to the table will be applied to the export"
|
||||
: "No filters or sorting will be applied on the exported data"}
|
||||
</DialogContent>
|
||||
|
||||
<TabList
|
||||
|
||||
121
src/components/TableHeader/Filters/FilterInputs.tsx
Normal file
121
src/components/TableHeader/Filters/FilterInputs.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
106
src/components/TableHeader/Filters/FiltersPopover.tsx
Normal file
106
src/components/TableHeader/Filters/FiltersPopover.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
335
src/components/TableHeader/Filters/index.tsx
Normal file
335
src/components/TableHeader/Filters/index.tsx
Normal file
@@ -0,0 +1,335 @@
|
||||
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 don’t 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
|
||||
{overrideTableFilters
|
||||
? " (ignore table filter)"
|
||||
: " (use table filter)"}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
50
src/components/TableHeader/Filters/useFilterInputs.ts
Normal file
50
src/components/TableHeader/Filters/useFilterInputs.ts
Normal 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;
|
||||
};
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
Typography,
|
||||
TextField,
|
||||
FormHelperText,
|
||||
Divider,
|
||||
} from "@mui/material";
|
||||
|
||||
import Tab from "@mui/material/Tab";
|
||||
@@ -109,7 +110,7 @@ export default function ImportCsv({ render, PopoverProps }: IImportCsvProps) {
|
||||
const popoverId = open ? "csv-popover" : undefined;
|
||||
|
||||
const parseCsv = (csvString: string) =>
|
||||
parse(csvString, {}, (err, rows) => {
|
||||
parse(csvString, { delimiter: [",", "\t"] }, (err, rows) => {
|
||||
if (err) {
|
||||
setError(err.message);
|
||||
} else {
|
||||
@@ -135,7 +136,7 @@ export default function ImportCsv({ render, PopoverProps }: IImportCsvProps) {
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
onDrop,
|
||||
multiple: false,
|
||||
accept: "text/csv",
|
||||
accept: ["text/csv", "text/tab-separated-values"],
|
||||
});
|
||||
|
||||
const [handlePaste] = useDebouncedCallback(
|
||||
@@ -169,7 +170,7 @@ export default function ImportCsv({ render, PopoverProps }: IImportCsvProps) {
|
||||
render(handleOpen)
|
||||
) : (
|
||||
<TableHeaderButton
|
||||
title="Import CSV"
|
||||
title="Import CSV or TSV"
|
||||
onClick={handleOpen}
|
||||
icon={<ImportIcon />}
|
||||
/>
|
||||
@@ -207,6 +208,7 @@ export default function ImportCsv({ render, PopoverProps }: IImportCsvProps) {
|
||||
<Tab label="Paste" value="paste" />
|
||||
<Tab label="URL" value="url" />
|
||||
</TabList>
|
||||
<Divider style={{ marginTop: -1 }} />
|
||||
|
||||
<TabPanel value="upload" className={classes.tabPanel}>
|
||||
<Grid
|
||||
@@ -221,7 +223,7 @@ export default function ImportCsv({ render, PopoverProps }: IImportCsvProps) {
|
||||
<input {...getInputProps()} />
|
||||
{isDragActive ? (
|
||||
<Typography variant="button" color="primary">
|
||||
Drop CSV file here…
|
||||
Drop CSV or TSV file here…
|
||||
</Typography>
|
||||
) : (
|
||||
<>
|
||||
@@ -231,8 +233,8 @@ export default function ImportCsv({ render, PopoverProps }: IImportCsvProps) {
|
||||
<Grid item>
|
||||
<Typography variant="button" color="inherit">
|
||||
{validCsv
|
||||
? "Valid CSV"
|
||||
: "Click to upload or drop CSV file here"}
|
||||
? "Valid CSV or TSV"
|
||||
: "Click to upload or drop CSV or TSV file here"}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</>
|
||||
@@ -253,7 +255,7 @@ export default function ImportCsv({ render, PopoverProps }: IImportCsvProps) {
|
||||
inputProps={{ minRows: 3 }}
|
||||
autoFocus
|
||||
fullWidth
|
||||
label="Paste CSV text"
|
||||
label="Paste CSV or TSV text"
|
||||
placeholder="column, column, …"
|
||||
onChange={(e) => {
|
||||
if (csvData !== null) setCsvData(null);
|
||||
@@ -276,13 +278,13 @@ export default function ImportCsv({ render, PopoverProps }: IImportCsvProps) {
|
||||
variant="filled"
|
||||
autoFocus
|
||||
fullWidth
|
||||
label="Paste URL to CSV file"
|
||||
label="Paste URL to CSV or TSV file"
|
||||
placeholder="https://"
|
||||
onChange={(e) => {
|
||||
if (csvData !== null) setCsvData(null);
|
||||
handleUrl(e.target.value);
|
||||
}}
|
||||
helperText={loading ? "Fetching CSV…" : error}
|
||||
helperText={loading ? "Fetching…" : error}
|
||||
error={!!error}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -111,7 +111,7 @@ export default function ImportCsvWizard({
|
||||
setOpen(false);
|
||||
setTimeout(handleClose, 300);
|
||||
}}
|
||||
title="Import CSV"
|
||||
title="Import CSV or TSV"
|
||||
steps={
|
||||
[
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user