From 6b5cf37f95715d65844c957f1620f5031c4f8618 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Thu, 13 Jan 2022 13:11:45 +1100 Subject: [PATCH 1/6] support table-level filters (#587) --- .../ColumnMenu/FieldSettings/FormAutosave.tsx | 9 +- src/components/Table/Filters/index.tsx | 316 ----------------- .../TableHeader/Filters/FilterInputs.tsx | 121 +++++++ .../TableHeader/Filters/FiltersPopover.tsx | 106 ++++++ src/components/TableHeader/Filters/index.tsx | 332 ++++++++++++++++++ .../TableHeader/Filters/useFilterInputs.ts | 50 +++ .../{Table => TableHeader}/HiddenFields.tsx | 0 src/components/TableHeader/index.tsx | 4 +- src/hooks/useTable/index.ts | 2 +- src/hooks/useTable/useTableConfig.ts | 2 +- src/theme/components.tsx | 31 ++ 11 files changed, 651 insertions(+), 322 deletions(-) delete mode 100644 src/components/Table/Filters/index.tsx create mode 100644 src/components/TableHeader/Filters/FilterInputs.tsx create mode 100644 src/components/TableHeader/Filters/FiltersPopover.tsx create mode 100644 src/components/TableHeader/Filters/index.tsx create mode 100644 src/components/TableHeader/Filters/useFilterInputs.ts rename src/components/{Table => TableHeader}/HiddenFields.tsx (100%) diff --git a/src/components/Table/ColumnMenu/FieldSettings/FormAutosave.tsx b/src/components/Table/ColumnMenu/FieldSettings/FormAutosave.tsx index 5ae4646f..52fda33d 100644 --- a/src/components/Table/ColumnMenu/FieldSettings/FormAutosave.tsx +++ b/src/components/Table/ColumnMenu/FieldSettings/FormAutosave.tsx @@ -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, }); diff --git a/src/components/Table/Filters/index.tsx b/src/components/Table/Filters/index.tsx deleted file mode 100644 index 15e5cc94..00000000 --- a/src/components/Table/Filters/index.tsx +++ /dev/null @@ -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); - - 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(); - - const [query, setQuery] = useState({ - key: "", - operator: "", - value: "", - }); - - const [selectedFilter, setSelectedFilter] = useState(); - 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) => { - 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 ( - <> - - } - 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"} - - - {(tableState?.filters ?? []).map((filter) => ( - handleUpdateFilters([])} - sx={{ - borderRadius: 1, - borderTopLeftRadius: 0, - borderBottomLeftRadius: 0, - borderLeft: "none", - - backgroundColor: "background.paper", - height: 32, - - "& .MuiChip-label": { px: 1.5 }, - }} - variant="outlined" - /> - ))} - - - theme.spacing(0.5), - right: (theme) => theme.spacing(0.5), - }} - > - - -
- - - - - Select column - - {filterColumns.map((c) => ( - - {c.label} - - ))} - - - - - { - setQuery((query) => ({ - ...query, - operator: e.target.value as string, - })); - }} - SelectProps={{ displayEmpty: true }} - > - - Select condition - - {selectedFilter?.operators.map((operator) => ( - - {operator.label} - - ))} - - - - {query.key && query.operator && ( -
- - Value - - - - setQuery((query) => ({ - ...query, - value: values[query.key], - })) - } - /> - }> - {query.operator && - createElement(getFieldProp("SideDrawerField", type), { - column: selectedColumn, - control, - docRef: {}, - disabled: false, - onChange: () => {}, - })} - - - )} -
-
- - - - - - - - -
-
- - ); -} diff --git a/src/components/TableHeader/Filters/FilterInputs.tsx b/src/components/TableHeader/Filters/FilterInputs.tsx new file mode 100644 index 00000000..4755d26a --- /dev/null +++ b/src/components/TableHeader/Filters/FilterInputs.tsx @@ -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 { + 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 ( + + + + + + + { + setQuery((query) => ({ + ...query, + operator: e.target.value as string, + })); + }} + SelectProps={{ displayEmpty: true }} + > + + Select operator + + {availableFilters?.operators.map((operator) => ( + + {operator.label} + + ))} + + + + + {query.key && query.operator && ( +
+ + Value + + + { + if (values[query.key] !== undefined) { + setQuery((query) => ({ + ...query, + value: values[query.key], + })); + } + }} + /> + }> + {createElement(getFieldProp("SideDrawerField", columnType), { + column: selectedColumn, + control, + docRef: {}, + disabled, + onChange: () => {}, + })} + + + )} +
+
+ ); +} diff --git a/src/components/TableHeader/Filters/FiltersPopover.tsx b/src/components/TableHeader/Filters/FiltersPopover.tsx new file mode 100644 index 00000000..146da7a1 --- /dev/null +++ b/src/components/TableHeader/Filters/FiltersPopover.tsx @@ -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["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(null); + const [open, setOpen] = useState(false); + const popoverId = open ? "filters-popover" : undefined; + const handleClose = () => setOpen(false); + + return ( + <> + + setOpen(true)} + startIcon={} + active={hasAppliedFilters} + sx={ + hasAppliedFilters + ? { + borderTopRightRadius: 0, + borderBottomRightRadius: 0, + position: "relative", + zIndex: 1, + } + : {} + } + aria-describedby={popoverId} + > + {hasAppliedFilters ? "Filtered" : "Filter"} + + + {appliedFilters.map((filter) => ( + setUserFilters([]) + } + sx={{ + borderRadius: 1, + borderTopLeftRadius: 0, + borderBottomLeftRadius: 0, + borderLeft: "none", + + backgroundColor: "background.paper", + height: 32, + + "& .MuiChip-label": { px: 1.5 }, + }} + variant="outlined" + /> + ))} + + + + {children({ handleClose })} + + + ); +} diff --git a/src/components/TableHeader/Filters/index.tsx b/src/components/TableHeader/Filters/index.tsx new file mode 100644 index 00000000..0f4cf204 --- /dev/null +++ b/src/components/TableHeader/Filters/index.tsx @@ -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 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 ( + + {({ handleClose }) => + // ADMIN + userClaims?.roles.includes("ADMIN") ? ( + + setTab(v)} + variant="fullWidth" + aria-label="Filter tabs" + > + + Your filter + {tableFiltersOverridden && ( + + )} + + } + value="user" + style={{ flexDirection: "row" }} + /> + + Table filter + {tableFiltersOverridden ? ( + + ) : hasTableFilters ? ( + + ) : null} + + } + value="table" + style={{ flexDirection: "row" }} + /> + + + + + + + setOverrideTableFilters(e.target.checked)} + /> + } + label="Override table filters" + sx={{ justifyContent: "center", mb: 1, mr: 0 }} + /> + + + + + + + + + + + + + The filter above will be set for all users who view this table. + Only ADMIN users can override or edit this. + + + + + + + + + + ) : // Non-ADMIN cannot override table filters + hasTableFilters ? ( +
+ + + + An ADMIN user has set the filter for this table + +
+ ) : ( + // Non-ADMIN can set own filters, since there are no table filters +
+ + + + + + + +
+ ) + } +
+ ); +} diff --git a/src/components/TableHeader/Filters/useFilterInputs.ts b/src/components/TableHeader/Filters/useFilterInputs.ts new file mode 100644 index 00000000..4dffc6ab --- /dev/null +++ b/src/components/TableHeader/Filters/useFilterInputs.ts @@ -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(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; +}; diff --git a/src/components/Table/HiddenFields.tsx b/src/components/TableHeader/HiddenFields.tsx similarity index 100% rename from src/components/Table/HiddenFields.tsx rename to src/components/TableHeader/HiddenFields.tsx diff --git a/src/components/TableHeader/index.tsx b/src/components/TableHeader/index.tsx index c07ea563..400f7ee8 100644 --- a/src/components/TableHeader/index.tsx +++ b/src/components/TableHeader/index.tsx @@ -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"; diff --git a/src/hooks/useTable/index.ts b/src/hooks/useTable/index.ts index 4b476be3..0dabda0f 100644 --- a/src/hooks/useTable/index.ts +++ b/src/hooks/useTable/index.ts @@ -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; }; }; diff --git a/src/hooks/useTable/useTableConfig.ts b/src/hooks/useTable/useTableConfig.ts index 7e2dd3ee..cb9fcbaf 100644 --- a/src/hooks/useTable/useTableConfig.ts +++ b/src/hooks/useTable/useTableConfig.ts @@ -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 }, diff --git a/src/theme/components.tsx b/src/theme/components.tsx index 21cadbbb..ab7a0f74 100644 --- a/src/theme/components.tsx +++ b/src/theme/components.tsx @@ -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: { From 4ba216c74584fe27602a5f24e89f760f43c72e91 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Thu, 13 Jan 2022 13:12:02 +1100 Subject: [PATCH 2/6] generalize InfoTooltip component --- src/components/InfoTooltip.tsx | 91 +++++++++++++++++++++++ src/components/Navigation/Breadcrumbs.tsx | 65 +++------------- 2 files changed, 102 insertions(+), 54 deletions(-) create mode 100644 src/components/InfoTooltip.tsx diff --git a/src/components/InfoTooltip.tsx b/src/components/InfoTooltip.tsx new file mode 100644 index 00000000..fd0181c4 --- /dev/null +++ b/src/components/InfoTooltip.tsx @@ -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>; + tooltipProps?: Partial>; + iconProps?: Partial>; +} + +export default function InfoTooltip({ + description, + buttonLabel = "Info", + defaultOpen, + + buttonProps, + tooltipProps, + iconProps, +}: IInfoTooltipProps) { + const [open, setOpen] = useState(defaultOpen || false); + + return ( + + {description} + setOpen(false)} + sx={{ + m: -0.5, + opacity: 0.8, + "&:hover": { + backgroundColor: (theme) => + alpha("#fff", theme.palette.action.hoverOpacity), + }, + }} + color="inherit" + > + + + + } + 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 + )} + > + setOpen((x) => !x)} + > + {buttonProps?.children || } + + + ); +} diff --git a/src/components/Navigation/Breadcrumbs.tsx b/src/components/Navigation/Breadcrumbs.tsx index 8fff15fd..84dff2eb 100644 --- a/src/components/Navigation/Breadcrumbs.tsx +++ b/src/components/Navigation/Breadcrumbs.tsx @@ -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) { )} - {crumb === table?.id && table?.description && ( - - {table?.description} - setOpenInfo(false)} - sx={{ m: -0.5 }} - color="inherit" - > - - - - } - 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 && ( + - setOpenInfo((x) => !x)} - > - - - + /> )} ); From 279670b99241b9b62d764aacaaef638f6b77302d Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Thu, 13 Jan 2022 13:12:15 +1100 Subject: [PATCH 3/6] import csv popover: add divider under tabs for consistency --- src/components/TableHeader/ImportCsv.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/TableHeader/ImportCsv.tsx b/src/components/TableHeader/ImportCsv.tsx index 89575c75..741b8983 100644 --- a/src/components/TableHeader/ImportCsv.tsx +++ b/src/components/TableHeader/ImportCsv.tsx @@ -13,6 +13,7 @@ import { Typography, TextField, FormHelperText, + Divider, } from "@mui/material"; import Tab from "@mui/material/Tab"; @@ -207,6 +208,7 @@ export default function ImportCsv({ render, PopoverProps }: IImportCsvProps) { + Date: Thu, 13 Jan 2022 13:38:47 +1100 Subject: [PATCH 4/6] add tsv export --- src/components/TableHeader/Export/Export.tsx | 55 ++++++++++++-------- src/components/TableHeader/Export/index.tsx | 6 +-- 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/src/components/TableHeader/Export/Export.tsx b/src/components/TableHeader/Export/Export.tsx index a2b527e6..cf7033a7 100644 --- a/src/components/TableHeader/Export/Export.tsx +++ b/src/components/TableHeader/Export/Export.tsx @@ -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([]); - 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 /> - { - if (v) { - setExportType(v as "csv" | "json"); - } - }} - multiple={false} - searchable={false} - clearable={false} - TextFieldProps={{ helperText: "Encoding: UTF-8" }} - /> + + Export type + { + const v = e.target.value; + if (v) setExportType(v as "csv" | "tsv" | "json"); + }} + > + } label=".csv" /> + } label=".tsv" /> + } label=".json" /> + + Encoding: UTF-8 +
diff --git a/src/components/TableHeader/Export/index.tsx b/src/components/TableHeader/Export/index.tsx index 86773430..80482f7d 100644 --- a/src/components/TableHeader/Export/index.tsx +++ b/src/components/TableHeader/Export/index.tsx @@ -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() { {(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"} Date: Thu, 13 Jan 2022 13:45:10 +1100 Subject: [PATCH 5/6] clarify admin override filter clear button --- src/components/TableHeader/Filters/index.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/TableHeader/Filters/index.tsx b/src/components/TableHeader/Filters/index.tsx index 0f4cf204..7f61e88b 100644 --- a/src/components/TableHeader/Filters/index.tsx +++ b/src/components/TableHeader/Filters/index.tsx @@ -221,6 +221,9 @@ export default function Filters() { }} > Clear + {overrideTableFilters + ? " (ignore table filter)" + : " (use table filter)"}