standardize ColumnSelect, wrap more TableToolbar children in lazy & Suspense

This commit is contained in:
Sidney Alcantara
2022-06-01 13:43:31 +10:00
parent e1bc72071a
commit b32276fdaa
15 changed files with 263 additions and 135 deletions

View File

@@ -9,8 +9,13 @@ import RowyRunModal from "@src/components/RowyRunModal";
import NotFound from "@src/pages/NotFound";
import RequireAuth from "@src/layouts/RequireAuth";
import { globalScope, currentUserAtom } from "@src/atoms/globalScope";
import {
globalScope,
currentUserAtom,
altPressAtom,
} from "@src/atoms/globalScope";
import { ROUTES } from "@src/constants/routes";
import useKeyPressWithAtom from "@src/hooks/useKeyPressWithAtom";
import TableGroupRedirectPage from "./pages/TableGroupRedirect";
import JotaiTestPage from "@src/pages/Test/JotaiTest";
@@ -53,6 +58,7 @@ const ThemeTestPage = lazy(() => import("@src/pages/Test/ThemeTest" /* webpackCh
export default function App() {
const [currentUser] = useAtom(currentUserAtom, globalScope);
useKeyPressWithAtom("Alt", altPressAtom, globalScope);
return (
<Suspense fallback={<Loading fullScreen />}>

View File

@@ -10,6 +10,12 @@ import type {
} from "@src/types/table";
import { getTableSchemaAtom } from "./project";
/**
* Global state when the Alt key is pressed,
* so we dont set multiple event listeners
*/
export const altPressAtom = atom(false);
/** Nav open state stored in local storage. */
export const navOpenAtom = atomWithStorage("__ROWY__NAV_OPEN", false);
/** Nav pinned state stored in local storage. */

View File

@@ -34,6 +34,7 @@ import {
columnMenuAtom,
columnModalAtom,
tableFiltersPopoverAtom,
altPressAtom,
} from "@src/atoms/globalScope";
import {
tableScope,
@@ -45,7 +46,6 @@ import {
import { FieldType } from "@src/constants/fields";
import { getFieldProp } from "@src/components/fields";
import { analytics, logEvent } from "@src/analytics";
import useKeyPress from "@src/hooks/useKeyPress";
import { formatSubTableName } from "@src/utils/table";
export interface IMenuModalProps {
@@ -78,7 +78,7 @@ export default function ColumnMenu() {
tableFiltersPopoverAtom,
globalScope
);
const altPress = useKeyPress("Alt");
const [altPress] = useAtom(altPressAtom, globalScope);
if (!columnMenu) return null;
const { column, anchorEl } = columnMenu;

View File

@@ -22,13 +22,13 @@ import {
globalScope,
userRolesAtom,
columnMenuAtom,
altPressAtom,
} from "@src/atoms/globalScope";
import { tableScope, updateColumnAtom } from "@src/atoms/tableScope";
import { FieldType } from "@src/constants/fields";
import { getFieldProp } from "@src/components/fields";
import { COLUMN_HEADER_HEIGHT } from "@src/components/Table/Column";
import { ColumnConfig } from "@src/types/table";
import useKeyPress from "@src/hooks/useKeyPress";
export { COLUMN_HEADER_HEIGHT };
@@ -55,7 +55,7 @@ export default function DraggableHeaderRenderer({
const [userRoles] = useAtom(userRolesAtom, globalScope);
const updateColumn = useSetAtom(updateColumnAtom, tableScope);
const openColumnMenu = useSetAtom(columnMenuAtom, globalScope);
const altPress = useKeyPress("Alt");
const [altPress] = useAtom(altPressAtom, globalScope);
const [{ isDragging }, dragRef] = useDrag({
type: "COLUMN_DRAG",
@@ -191,7 +191,13 @@ export default function DraggableHeaderRenderer({
component="div"
color="inherit"
>
{altPress ? `${column.index}: ${column.fieldName}` : column.name}
{altPress ? (
<>
{column.index} <code>{column.fieldName}</code>
</>
) : (
column.name
)}
</Typography>
</LightTooltip>
</Grid>

View File

@@ -9,6 +9,7 @@ import {
globalScope,
userRolesAtom,
tableAddRowIdTypeAtom,
altPressAtom,
confirmDialogAtom,
} from "@src/atoms/globalScope";
import {
@@ -17,7 +18,6 @@ import {
addRowAtom,
deleteRowAtom,
} from "@src/atoms/tableScope";
import useKeyPress from "@src/hooks/useKeyPress";
import { TableRow } from "@src/types/table";
export default function FinalColumn({ row }: FormatterProps<TableRow, any>) {
@@ -29,7 +29,7 @@ export default function FinalColumn({ row }: FormatterProps<TableRow, any>) {
const addRow = useSetAtom(addRowAtom, tableScope);
const deleteRow = useSetAtom(deleteRowAtom, tableScope);
const altPress = useKeyPress("Alt");
const [altPress] = useAtom(altPressAtom, globalScope);
const handleDelete = () => deleteRow(row._rowy_ref.path);
if (!userRoles.includes("ADMIN") && tableSettings.readOnly === true)

View File

@@ -0,0 +1,80 @@
import { useAtom } from "jotai";
import MultiSelect, { MultiSelectProps } from "@rowy/multiselect";
import { Stack, Typography } from "@mui/material";
import { globalScope, altPressAtom } from "@src/atoms/globalScope";
import { tableScope, tableColumnsOrderedAtom } from "@src/atoms/tableScope";
import { ColumnConfig } from "@src/types/table";
import { FieldType } from "@src/constants/fields";
import { getFieldProp } from "@src/components/fields";
export type ColumnOption = {
value: string;
label: string;
type: FieldType;
index: number;
};
export interface IColumnSelectProps {
filterColumns?: (column: ColumnConfig) => boolean;
options?: ColumnOption[];
}
export default function ColumnSelect({
filterColumns,
...props
}: IColumnSelectProps & Omit<MultiSelectProps<string>, "options">) {
const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope);
const options = (
filterColumns
? tableColumnsOrdered.filter(filterColumns)
: tableColumnsOrdered
).map(({ key, name, type, index, fixed }) => ({
value: key,
label: name,
type,
index,
}));
return (
<MultiSelect
options={options}
label="Column"
labelPlural="columns"
{...(props as any)}
itemRenderer={(option: ColumnOption) => <ColumnItem option={option} />}
/>
);
}
export function ColumnItem({
option,
children,
}: React.PropsWithChildren<{ option: ColumnOption }>) {
const [altPress] = useAtom(altPressAtom, globalScope);
return (
<Stack
direction="row"
alignItems="center"
gap={1}
sx={{ color: "text.secondary", width: "100%" }}
>
{getFieldProp("icon", option.type)}
<Typography color="text.primary" style={{ flexGrow: 1 }}>
{altPress ? <code>{option.value}</code> : option.label}
</Typography>
{altPress && (
<Typography
color="text.disabled"
variant="caption"
style={{ fontVariantNumeric: "tabular-nums" }}
>
{option.index}
</Typography>
)}
{children}
</Stack>
);
}

View File

@@ -15,6 +15,7 @@ import {
FormControlLabel,
Checkbox,
} from "@mui/material";
import ColumnSelect from "@src/components/TableToolbar/ColumnSelect";
import {
tableScope,
@@ -133,17 +134,10 @@ export default function Export({
};
return (
<>
<MultiSelect
<ColumnSelect
value={columns.map((x) => x.key)}
onChange={handleChange(setColumns)}
options={tableColumnsOrdered
.filter(
(column) =>
isString(column.name) &&
isString(column.key) &&
DOWNLOADABLE_COLUMNS.includes(column.type)
)
.map((column: any) => ({ label: column.name, value: column.key }))}
filterColumns={(column) => DOWNLOADABLE_COLUMNS.includes(column.type)}
label="Columns to export"
labelPlural="columns"
TextFieldProps={{

View File

@@ -4,9 +4,7 @@ import { parse as json2csv } from "json2csv";
import { saveAs } from "file-saver";
import { useSnackbar } from "notistack";
import { getDocs } from "firebase/firestore";
import { get, find, sortBy, isString } from "lodash-es";
import MultiSelect from "@rowy/multiselect";
import { get, find } from "lodash-es";
import {
Button,
@@ -18,6 +16,7 @@ import {
Radio,
FormHelperText,
} from "@mui/material";
import ColumnSelect from "@src/components/TableToolbar/ColumnSelect";
import {
tableScope,
@@ -175,12 +174,9 @@ export default function Export({
};
return (
<>
<MultiSelect
<ColumnSelect
value={columns.map((x) => x.key)}
onChange={handleChange}
options={tableColumnsOrdered
.filter((column) => isString(column.name) && isString(column.key))
.map((column) => ({ label: column.name, value: column.key }))}
label="Columns to export"
labelPlural="columns"
TextFieldProps={{
@@ -191,23 +187,25 @@ export default function Export({
selectAll
/>
<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>
<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>
<div style={{ flexGrow: 1, marginTop: 0 }} />

View File

@@ -4,6 +4,7 @@ import { useForm } from "react-hook-form";
import { Grid, MenuItem, TextField, InputLabel } from "@mui/material";
import MultiSelect from "@rowy/multiselect";
import ColumnSelect from "@src/components/TableToolbar/ColumnSelect";
import FormAutosave from "@src/components/ColumnModals/ColumnConfigModal/FormAutosave";
import FieldSkeleton from "@src/components/SideDrawer/Form/FieldSkeleton";
@@ -35,7 +36,7 @@ export default function FilterInputs({
return (
<Grid container spacing={2} sx={{ mb: 3 }}>
<Grid item xs={4}>
<MultiSelect
<ColumnSelect
multiple={false}
label="Column"
options={filterColumns}

View File

@@ -14,7 +14,13 @@ export const useFilterInputs = (
// Get list of columns that can be filtered
const filterColumns = columns
.filter((c) => getFieldProp("filter", getFieldType(c)))
.map((c) => ({ value: c.key, label: c.name, ...c }));
.map((c) => ({
value: c.key,
label: c.name,
type: c.type,
key: c.key,
index: c.index,
}));
// State for filter inputs
const [query, setQuery] = useState<TableFilter | typeof INITIAL_QUERY>(

View File

@@ -2,37 +2,28 @@ import { useEffect, useRef, useMemo, useState } from "react";
import { useAtom } from "jotai";
import { isEqual } from "lodash-es";
import { AutocompleteProps } from "@mui/material";
import { Stack, Typography, Box, AutocompleteProps } from "@mui/material";
import VisibilityIcon from "@mui/icons-material/VisibilityOutlined";
import VisibilityOffIcon from "@mui/icons-material/VisibilityOffOutlined";
import IconSlash, {
ICON_SLASH_STROKE_DASHOFFSET,
} from "@src/components/IconSlash";
import IconSlash from "@src/components/IconSlash";
import MultiSelect from "@rowy/multiselect";
import ColumnSelect, { ColumnItem } from "./ColumnSelect";
import ButtonWithStatus from "@src/components/ButtonWithStatus";
import Column from "@src/components/Table/Column";
import {
globalScope,
userSettingsAtom,
updateUserSettingsAtom,
} from "@src/atoms/globalScope";
import {
tableScope,
tableIdAtom,
tableSchemaAtom,
tableColumnsOrderedAtom,
} from "@src/atoms/tableScope";
import { tableScope, tableIdAtom } from "@src/atoms/tableScope";
import { formatSubTableName } from "@src/utils/table";
import { getFieldProp } from "@src/components/fields";
export default function HiddenFields() {
const buttonRef = useRef<HTMLButtonElement>(null);
const [userSettings] = useAtom(userSettingsAtom, globalScope);
const [tableId] = useAtom(tableIdAtom, tableScope);
const [tableSchema] = useAtom(tableSchemaAtom, tableScope);
const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope);
const [open, setOpen] = useState(false);
@@ -50,10 +41,10 @@ export default function HiddenFields() {
setHiddenFields(userDocHiddenFields);
}, [userDocHiddenFields]);
const tableColumns = tableColumnsOrdered.map(({ key, name }) => ({
value: key,
label: name,
}));
// const tableColumns = tableColumnsOrdered.map(({ key, name }) => ({
// value: key,
// label: name,
// }));
// Save when MultiSelect closes
const [updateUserSettings] = useAtom(updateUserSettingsAtom, globalScope);
@@ -73,20 +64,22 @@ export default function HiddenFields() {
any
>["renderOption"] = (props, option, { selected }) => (
<li {...props}>
<Column
label={option.label}
type={tableSchema.columns?.[option.value]?.type}
secondaryItem={
<div
className="icon-container"
style={selected ? { opacity: 1 } : {}}
>
<VisibilityIcon />
<IconSlash style={selected ? { strokeDashoffset: 0 } : {}} />
</div>
}
// active={selected}
/>
<ColumnItem option={option}>
<Box
sx={[
{ position: "relative", height: "1.5rem" },
selected
? { color: "primary.main" }
: {
opacity: 0,
".MuiAutocomplete-option.Mui-focused &": { opacity: 0.5 },
},
]}
>
<VisibilityIcon />
<IconSlash style={selected ? { strokeDashoffset: 0 } : {}} />
</Box>
</ColumnItem>
</li>
);
@@ -100,7 +93,7 @@ export default function HiddenFields() {
>
{hiddenFields.length > 0 ? `${hiddenFields.length} hidden` : "Hide"}
</ButtonWithStatus>
<MultiSelect
<ColumnSelect
TextFieldProps={{
style: { display: "none" },
SelectProps: {
@@ -109,47 +102,12 @@ export default function HiddenFields() {
anchorEl: buttonRef.current,
anchorOrigin: { vertical: "bottom", horizontal: "left" },
transformOrigin: { vertical: "top", horizontal: "left" },
sx: {
"& .MuiAutocomplete-listbox .MuiAutocomplete-option": {
padding: 0,
paddingLeft: "0 !important",
borderRadius: 0,
marginBottom: "-1px",
"&::after": { content: "none" },
"& .icon-container": { opacity: 0 },
"&:hover, &.Mui-focused, &.Mui-focusVisible": {
position: "relative",
zIndex: 2,
"& > div": {
color: "text.primary",
borderColor: "currentColor",
boxShadow: (theme: any) =>
`0 0 0 1px ${theme.palette.text.primary} inset`,
},
"& .icon-container": { opacity: 0.5 },
},
// "&:hover .icon-container svg": { color: "primary.main" },
"&:hover .icon-slash": { strokeDashoffset: 0 },
'&[aria-selected="true"]:hover': {
"& .icon-slash": {
strokeDashoffset:
ICON_SLASH_STROKE_DASHOFFSET + " !important",
},
},
},
},
},
},
}}
{...{ AutocompleteProps: { renderOption } }}
label="Hidden fields"
labelPlural="fields"
options={tableColumns}
value={hiddenFields ?? []}
onChange={setHiddenFields}
onClose={handleSave}

View File

@@ -2,18 +2,13 @@ import { lazy, Suspense } from "react";
import { useAtom } from "jotai";
import { Stack } from "@mui/material";
import { ButtonSkeleton } from "./TableToolbarSkeleton";
import AddRow from "./AddRow";
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 "./HiddenFields";
import RowHeight from "./RowHeight";
// import Extensions from "./Extensions";
// import Webhooks from "./Webhooks";
import ReExecute from "./ReExecute";
// import BuildLogsSnack from "./CloudLogs/BuildLogs/BuildLogsSnack";
import { globalScope, userRolesAtom } from "@src/atoms/globalScope";
@@ -25,7 +20,20 @@ import {
import { FieldType } from "@src/constants/fields";
// import { useSnackLogContext } from "@src/contexts/SnackLogContext";
const Export = lazy(() => import("./Export"));
// prettier-ignore
const Filters = lazy(() => import("./Filters" /* webpackChunkName: "Filters" */));
// prettier-ignore
const Export = lazy(() => import("./Export" /* webpackChunkName: "Export" */));
// prettier-ignore
const ImportCsv = lazy(() => import("./ImportCsv" /* webpackChunkName: "ImportCsv" */));
// prettier-ignore
// const CloudLogs = lazy(() => import("./CloudLogs" /* webpackChunkName: "CloudLogs" */));
// prettier-ignore
// const Extensions = lazy(() => import("./Extensions" /* webpackChunkName: "Extensions" */));
// prettier-ignore
// const Webhooks = lazy(() => import("./Webhooks" /* webpackChunkName: "Webhooks" */));
// prettier-ignore
const ReExecute = lazy(() => import("./ReExecute" /* webpackChunkName: "ReExecute" */));
export const TABLE_TOOLBAR_HEIGHT = 44;
@@ -68,29 +76,51 @@ export default function TableToolbar() {
<AddRow />
{/* Spacer */} <div />
<HiddenFields />
<Filters />
<Suspense fallback={<ButtonSkeleton />}>
<Filters />
</Suspense>
{/* Spacer */} <div />
<LoadedRowsStatus />
<div style={{ flexGrow: 1, minWidth: 64 }} />
<RowHeight />
{/* Spacer */} <div />
{tableSettings.tableType !== "collectionGroup" && <ImportCSV />}
<Suspense fallback={null}>
{tableSettings.tableType !== "collectionGroup" && (
<Suspense fallback={<ButtonSkeleton />}>
<ImportCsv />
</Suspense>
)}
<Suspense fallback={<ButtonSkeleton />}>
<Export />
</Suspense>
{userRoles.includes("ADMIN") && (
<>
{/* Spacer */} <div />
{/* <Webhooks /> */}
{/* <Extensions /> */}
{/* <CloudLogs /> */}
{/*
<Suspense fallback={<ButtonSkeleton/>}>
<Webhooks />
</Suspense>
*/}
{/*
<Suspense fallback={<ButtonSkeleton/>}>
<Extensions />
</Suspense>
*/}
{/*
<Suspense fallback={<ButtonSkeleton/>}>
<CloudLogs />
</Suspense>
*/}
{/* {snackLogContext.isSnackLogOpen && (
<BuildLogsSnack
onClose={snackLogContext.closeSnackLog}
onOpenPanel={alert}
/>
)} */}
{(hasDerivatives || hasExtensions) && <ReExecute />}
{(hasDerivatives || hasExtensions) && (
<Suspense fallback={<ButtonSkeleton />}>
<ReExecute />
</Suspense>
)}
{/* Spacer */} <div />
<TableSettings />
</>

View File

@@ -3,11 +3,11 @@ import AddRowIcon from "@src/assets/icons/AddRow";
import { TABLE_TOOLBAR_HEIGHT } from "@src/components/TableToolbar";
const ButtonSkeleton = (props: Partial<SkeletonProps>) => (
export const ButtonSkeleton = (props: Partial<SkeletonProps>) => (
<Skeleton
variant="rectangular"
{...props}
sx={{ borderRadius: 1, ...props.sx }}
sx={{ borderRadius: 1, width: 40, height: 32, ...props.sx }}
/>
);
@@ -27,7 +27,7 @@ export default function TableToolbarSkeleton() {
height: TABLE_TOOLBAR_HEIGHT,
}}
>
<ButtonSkeleton>
<ButtonSkeleton sx={{ width: undefined, height: undefined }}>
<Button variant="contained" startIcon={<AddRowIcon />}>
Add row
</Button>
@@ -35,12 +35,12 @@ export default function TableToolbarSkeleton() {
<div />
<ButtonSkeleton>
<ButtonSkeleton sx={{ width: undefined, height: undefined }}>
<Button variant="contained" startIcon={<AddRowIcon />}>
Hide
</Button>
</ButtonSkeleton>
<ButtonSkeleton>
<ButtonSkeleton sx={{ width: undefined, height: undefined }}>
<Button variant="contained" startIcon={<AddRowIcon />}>
Filter
</Button>
@@ -48,10 +48,10 @@ export default function TableToolbarSkeleton() {
<div style={{ flexGrow: 1 }} />
<ButtonSkeleton style={{ width: 40, height: 32 }} />
<ButtonSkeleton />
<div />
<ButtonSkeleton style={{ width: 40, height: 32 }} />
<ButtonSkeleton style={{ width: 40, height: 32 }} />
<ButtonSkeleton />
<ButtonSkeleton />
</Stack>
</Fade>
);

View File

@@ -131,7 +131,9 @@ export const hasDataTypes = (dataTypes: string[]) => {
* @param column - The column to check
* @returns FieldType
*/
export const getFieldType = (column: ColumnConfig) =>
export const getFieldType = (
column: Pick<ColumnConfig, "type" | "config"> & Partial<ColumnConfig>
) =>
column.type === FieldType.derivative
? column.config?.renderFieldType
: column.type;

View File

@@ -0,0 +1,41 @@
import { useEffect } from "react";
import { useSetAtom } from "jotai";
import { PrimitiveAtom, Scope } from "jotai/core/atom";
/**
* A hook that listens to when the target key is pressed
* and updates an atom value.
* @param targetKey - The key to listen to
* @param atom - A function to update the atom
* @param scope - The scope of the atom
*/
export default function useKeyPressWithAtom(
targetKey: string,
atom: PrimitiveAtom<boolean>,
scope: Scope
) {
const setAtom = useSetAtom(atom, scope);
// Add event listeners
useEffect(() => {
if (!setAtom) return;
// If pressed key is our target key then set to true
const downHandler = ({ key }: KeyboardEvent) => {
if (key === targetKey) setAtom(true);
};
// If released key is our target key then set to false
const upHandler = ({ key }: KeyboardEvent) => {
if (key === targetKey) setAtom(false);
};
window.addEventListener("keydown", downHandler);
window.addEventListener("keyup", upHandler);
// Remove event listeners on cleanup
return () => {
window.removeEventListener("keydown", downHandler);
window.removeEventListener("keyup", upHandler);
};
}, [targetKey, setAtom]);
}