migrate all remaining fields

This commit is contained in:
Sidney Alcantara
2022-11-11 15:49:47 +11:00
parent 742a992098
commit f9656e2c4e
25 changed files with 527 additions and 671 deletions

View File

@@ -15,7 +15,6 @@ import {
Draggable,
} from "react-beautiful-dnd";
import { get } from "lodash-es";
import { Portal } from "@mui/material";
import { ErrorBoundary } from "react-error-boundary";
import StyledTable from "./Styled/StyledTable";
@@ -426,6 +425,14 @@ export default function Table({
selectedCell?.path === row.original._rowy_ref.path &&
selectedCell?.columnKey === cell.column.id;
const fieldTypeGroup = getFieldProp(
"group",
cell.column.columnDef.meta?.type
);
const isReadOnlyCell =
fieldTypeGroup === "Auditing" ||
fieldTypeGroup === "Metadata";
return (
<CellValidation
key={cell.id}
@@ -447,7 +454,7 @@ export default function Table({
)}
aria-selected={isSelectedCell}
aria-describedby={
canEditCell
canEditCell && !isReadOnlyCell
? "rowy-table-editable-cell-description"
: undefined
}
@@ -540,14 +547,12 @@ export default function Table({
</div>
</StyledTable>
<Portal>
<div
id="rowy-table-editable-cell-description"
style={{ display: "none" }}
>
Press Enter to edit.
</div>
</Portal>
<div
id="rowy-table-editable-cell-description"
style={{ display: "none" }}
>
Press Enter to edit.
</div>
<ContextMenu />
</div>

View File

@@ -1,252 +0,0 @@
import { colord } from "colord";
import { styled, alpha, darken, lighten } from "@mui/material";
import { TOP_BAR_HEIGHT } from "@src/layouts/Navigation/TopBar";
import { TABLE_TOOLBAR_HEIGHT } from "@src/components/TableToolbar";
import {
DRAWER_COLLAPSED_WIDTH,
DRAWER_WIDTH,
} from "@src/components/SideDrawer";
export const OUT_OF_ORDER_MARGIN = 8;
export const TableContainer = styled("div", {
shouldForwardProp: (prop) => prop !== "rowHeight",
})<{ rowHeight: number }>(({ theme, rowHeight }) => ({
display: "flex",
position: "relative",
flexDirection: "column",
height: `calc(100vh - ${TOP_BAR_HEIGHT}px - ${TABLE_TOOLBAR_HEIGHT}px)`,
"& .left-scroll-divider": {
position: "absolute",
top: 0,
bottom: 0,
left: 0,
width: 1,
zIndex: 1,
backgroundColor: colord(theme.palette.background.paper)
.mix(theme.palette.divider, 0.12)
.alpha(1)
.toHslString(),
},
"& > .rdg": {
width: `calc(100% - ${DRAWER_COLLAPSED_WIDTH}px)`,
flex: 1,
paddingBottom: `max(env(safe-area-inset-bottom), ${theme.spacing(2)})`,
},
[theme.breakpoints.down("sm")]: { width: "100%" },
"& .rdg": {
"--color": theme.palette.text.primary,
"--border-color": theme.palette.divider,
// "--summary-border-color": "#aaa",
"--cell-background-color":
theme.palette.mode === "light"
? theme.palette.background.paper
: colord(theme.palette.background.paper)
.mix("#fff", 0.04)
.alpha(1)
.toHslString(),
"--header-background-color": theme.palette.background.default,
"--row-hover-background-color": colord(theme.palette.background.paper)
.mix(theme.palette.action.hover, theme.palette.action.hoverOpacity)
.alpha(1)
.toHslString(),
"--row-selected-background-color":
theme.palette.mode === "light"
? lighten(theme.palette.primary.main, 0.9)
: darken(theme.palette.primary.main, 0.8),
"--row-selected-hover-background-color":
theme.palette.mode === "light"
? lighten(theme.palette.primary.main, 0.8)
: darken(theme.palette.primary.main, 0.7),
"--checkbox-color": theme.palette.primary.main,
"--checkbox-focus-color": theme.palette.primary.main,
"--checkbox-disabled-border-color": "#ccc",
"--checkbox-disabled-background-color": "#ddd",
"--selection-color": theme.palette.primary.main,
"--font-size": "0.75rem",
"--cell-padding": theme.spacing(0, 1.25),
border: "none",
backgroundColor: "transparent",
...(theme.typography.caption as any),
// fontSize: "0.8125rem",
lineHeight: "inherit !important",
"& .rdg-cell": {
display: "flex",
alignItems: "center",
padding: 0,
overflow: "visible",
contain: "none",
position: "relative",
lineHeight: "calc(var(--row-height) - 1px)",
},
"& .rdg-cell-frozen": {
position: "sticky",
},
"& .rdg-cell-frozen-last": {
boxShadow: theme.shadows[2]
.replace(/, 0 (\d+px)/g, ", $1 0")
.split("),")
.slice(1)
.join("),"),
"&[aria-selected=true]": {
boxShadow:
theme.shadows[2]
.replace(/, 0 (\d+px)/g, ", $1 0")
.split("),")
.slice(1)
.join("),") + ", inset 0 0 0 2px var(--selection-color)",
},
},
"& .rdg-cell-copied": {
backgroundColor:
theme.palette.mode === "light"
? lighten(theme.palette.primary.main, 0.7)
: darken(theme.palette.primary.main, 0.6),
},
"& .final-column-cell": {
backgroundColor: "var(--header-background-color)",
borderColor: "var(--header-background-color)",
color: theme.palette.text.disabled,
padding: "var(--cell-padding)",
},
},
".rdg-row, .rdg-header-row": {
marginLeft: `max(env(safe-area-inset-left), ${theme.spacing(2)})`,
marginRight: `max(env(safe-area-inset-right), ${DRAWER_WIDTH}px)`,
display: "inline-grid", // Fix Safari not showing margin-right
},
".rdg-header-row .rdg-cell:first-of-type": {
borderTopLeftRadius: theme.shape.borderRadius,
},
".rdg-header-row .rdg-cell:last-of-type": {
borderTopRightRadius: theme.shape.borderRadius,
},
".rdg-header-row .rdg-cell.final-column-header": {
border: "none",
padding: theme.spacing(0, 0.75),
borderBottomRightRadius: theme.shape.borderRadius,
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
position: "relative",
"&::before": {
content: "''",
display: "block",
width: 88,
height: "100%",
position: "absolute",
top: 0,
left: 0,
border: "1px solid var(--border-color)",
borderLeftWidth: 0,
borderTopRightRadius: theme.shape.borderRadius,
borderBottomRightRadius: theme.shape.borderRadius,
},
},
".rdg-row .rdg-cell:first-of-type, .rdg-header-row .rdg-cell:first-of-type": {
borderLeft: "1px solid var(--border-color)",
},
".rdg-row:last-of-type": {
borderBottomLeftRadius: theme.shape.borderRadius,
borderBottomRightRadius: theme.shape.borderRadius,
"& .rdg-cell:first-of-type": {
borderBottomLeftRadius: theme.shape.borderRadius,
},
"& .rdg-cell:nth-last-of-type(2)": {
borderBottomRightRadius: theme.shape.borderRadius,
},
},
".rdg-header-row .rdg-cell": {
borderTop: "1px solid var(--border-color)",
},
".rdg-row:hover": { color: theme.palette.text.primary },
".row-hover-iconButton": {
color: theme.palette.text.disabled,
transitionDuration: "0s",
},
".rdg-row:hover .row-hover-iconButton": {
color: theme.palette.text.primary,
backgroundColor: alpha(
theme.palette.action.hover,
theme.palette.action.hoverOpacity * 1.5
),
},
".cell-collapse-padding": {
margin: theme.spacing(0, -1.25),
width: `calc(100% + ${theme.spacing(1.25 * 2)})`,
},
".rdg-row.out-of-order": {
"--row-height": rowHeight + 1 + "px !important",
marginTop: -1,
marginBottom: OUT_OF_ORDER_MARGIN,
borderBottomLeftRadius: theme.shape.borderRadius,
"& .rdg-cell:not(:last-of-type)": {
borderTop: `1px solid var(--border-color)`,
},
"& .rdg-cell:first-of-type": {
borderBottomLeftRadius: theme.shape.borderRadius,
},
"& .rdg-cell:nth-last-of-type(2)": {
borderBottomRightRadius: theme.shape.borderRadius,
},
"&:not(:nth-of-type(4))": {
borderTopLeftRadius: theme.shape.borderRadius,
"& .rdg-cell:first-of-type": {
borderTopLeftRadius: theme.shape.borderRadius,
},
"& .rdg-cell:nth-last-of-type(2)": {
borderTopRightRadius: theme.shape.borderRadius,
},
},
"& + .rdg-row:not(.out-of-order)": {
"--row-height": rowHeight + 1 + "px !important",
marginTop: -1,
borderTopLeftRadius: theme.shape.borderRadius,
"& .rdg-cell:not(:last-of-type)": {
borderTop: `1px solid var(--border-color)`,
},
"& .rdg-cell:first-of-type": {
borderTopLeftRadius: theme.shape.borderRadius,
},
"& .rdg-cell:nth-last-of-type(2)": {
borderTopRightRadius: theme.shape.borderRadius,
},
},
},
}));
TableContainer.displayName = "TableContainer";
export default TableContainer;

View File

@@ -0,0 +1,36 @@
import { IDisplayCellProps } from "@src/components/fields/types";
import { Grid, Chip } from "@mui/material";
import ChipList from "@src/components/Table/formatters/ChipList";
import { FileIcon } from ".";
import { FileValue } from "@src/types/table";
export default function File_({ value, tabIndex }: IDisplayCellProps) {
return (
<ChipList>
{Array.isArray(value) &&
value.map((file: FileValue) => (
<Grid
item
key={file.downloadURL}
style={
// Truncate so multiple files still visible
value.length > 1 ? { maxWidth: `calc(100% - 12px)` } : {}
}
>
<Chip
icon={<FileIcon />}
label={file.name}
onClick={(e) => {
window.open(file.downloadURL);
e.stopPropagation();
}}
style={{ width: "100%" }}
tabIndex={tabIndex}
/>
</Grid>
))}
</ChipList>
);
}

View File

@@ -1,5 +1,5 @@
import { useCallback } from "react";
import { IHeavyCellProps } from "@src/components/fields/types";
import { IEditorCellProps } from "@src/components/fields/types";
import { useSetAtom } from "jotai";
import { findIndex } from "lodash-es";
@@ -20,12 +20,13 @@ import { FileValue } from "@src/types/table";
export default function File_({
column,
row,
value,
onChange,
onSubmit,
disabled,
docRef,
}: IHeavyCellProps) {
_rowy_ref,
tabIndex,
}: IEditorCellProps) {
const confirm = useSetAtom(confirmDialogAtom, projectScope);
const updateField = useSetAtom(updateFieldAtom, tableScope);
@@ -38,13 +39,13 @@ export default function File_({
if (file) {
upload({
docRef: docRef! as any,
docRef: _rowy_ref,
fieldName: column.key,
files: [file],
previousValue: value,
onComplete: (newValue) => {
updateField({
path: docRef.path,
path: _rowy_ref.path,
fieldName: column.key,
value: newValue,
});
@@ -60,7 +61,8 @@ export default function File_({
const index = findIndex(newValue, ["ref", ref]);
const toBeDeleted = newValue.splice(index, 1);
toBeDeleted.length && deleteUpload(toBeDeleted[0]);
onSubmit(newValue);
onChange(newValue);
onSubmit();
};
const { getRootProps, getInputProps, isDragActive } = useDropzone({
@@ -73,11 +75,10 @@ export default function File_({
return (
<Stack
direction="row"
className="cell-collapse-padding"
alignItems="center"
sx={{
width: "100%",
height: "100%",
pr: 0.5,
...(isDragActive
? {
@@ -92,6 +93,7 @@ export default function File_({
: {}),
}}
{...dropzoneProps}
tabIndex={tabIndex}
onClick={undefined}
>
<ChipList>
@@ -130,6 +132,7 @@ export default function File_({
confirmColor: "error",
})
}
tabIndex={tabIndex}
style={{ width: "100%" }}
/>
</Tooltip>
@@ -146,8 +149,9 @@ export default function File_({
e.stopPropagation();
}}
style={{ display: "flex" }}
className={docRef && "row-hover-iconButton"}
disabled={!docRef}
className={_rowy_ref && "row-hover-iconButton end"}
disabled={!_rowy_ref}
tabIndex={tabIndex}
>
<UploadIcon />
</IconButton>
@@ -163,7 +167,7 @@ export default function File_({
</div>
)}
<input {...getInputProps()} />
<input {...getInputProps()} tabIndex={tabIndex} />
</Stack>
);
}

View File

@@ -1,13 +1,12 @@
import { lazy } from "react";
import { IFieldConfig, FieldType } from "@src/components/fields/types";
import withHeavyCell from "@src/components/fields/_withTableCell/withHeavyCell";
import withTableCell from "@src/components/Table/withTableCell";
import FileIcon from "@mui/icons-material/AttachFile";
import BasicCell from "@src/components/fields/_BasicCell/BasicCellNull";
import NullEditor from "@src/components/Table/editors/NullEditor";
import DisplayCell from "./DisplayCell";
const TableCell = lazy(
() => import("./TableCell" /* webpackChunkName: "TableCell-File" */)
const EditorCell = lazy(
() => import("./EditorCell" /* webpackChunkName: "EditorCell-File" */)
);
const SideDrawerField = lazy(
() =>
@@ -23,8 +22,9 @@ export const config: IFieldConfig = {
initialValue: [],
icon: <FileIcon />,
description: "File uploaded to Firebase Storage. Supports any file type.",
TableCell: withHeavyCell(BasicCell, TableCell),
TableEditor: NullEditor as any,
TableCell: withTableCell(DisplayCell, EditorCell, "inline", {
disablePadding: true,
}),
SideDrawerField,
};
export default config;

View File

@@ -0,0 +1,113 @@
import { IDisplayCellProps } from "@src/components/fields/types";
import { useAtom } from "jotai";
import { alpha, Theme, Stack, Grid, ButtonBase } from "@mui/material";
import OpenIcon from "@mui/icons-material/OpenInNewOutlined";
import Thumbnail from "@src/components/Thumbnail";
import { tableSchemaAtom, tableScope } from "@src/atoms/tableScope";
import { DEFAULT_ROW_HEIGHT } from "@src/components/Table";
import { FileValue } from "@src/types/table";
// MULTIPLE
export const imgSx = (rowHeight: number) => ({
position: "relative",
display: "flex",
width: (theme: Theme) => `calc(${rowHeight}px - ${theme.spacing(1)} - 1px)`,
height: (theme: Theme) => `calc(${rowHeight}px - ${theme.spacing(1)} - 1px)`,
backgroundSize: "contain",
backgroundPosition: "center center",
backgroundRepeat: "no-repeat",
borderRadius: 1,
});
export const thumbnailSx = {
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
};
export const deleteImgHoverSx = {
position: "absolute",
top: 0,
left: 0,
bottom: 0,
right: 0,
color: "text.secondary",
boxShadow: (theme: Theme) => `0 0 0 1px ${theme.palette.divider} inset`,
borderRadius: 1,
transition: (theme: Theme) =>
theme.transitions.create("background-color", {
duration: theme.transitions.duration.shortest,
}),
"& *": {
opacity: 0,
transition: (theme: Theme) =>
theme.transitions.create("opacity", {
duration: theme.transitions.duration.shortest,
}),
},
".img:hover &, .img:focus &": {
backgroundColor: (theme: Theme) =>
alpha(theme.palette.background.paper, 0.8),
"& *": { opacity: 1 },
},
};
export default function Image_({ value, tabIndex }: IDisplayCellProps) {
const [tableSchema] = useAtom(tableSchemaAtom, tableScope);
const rowHeight = tableSchema.rowHeight ?? DEFAULT_ROW_HEIGHT;
let thumbnailSize = "100x100";
if (rowHeight > 50) thumbnailSize = "200x200";
if (rowHeight > 100) thumbnailSize = "400x400";
return (
<Stack
direction="row"
sx={[{ py: 0, pl: 1, height: "100%" }]}
alignItems="center"
>
<Grid container spacing={0.5} wrap="nowrap">
{Array.isArray(value) &&
value.map((file: FileValue, i) => (
<Grid item key={file.downloadURL}>
{
<ButtonBase
aria-label="Open"
sx={imgSx(rowHeight)}
className="img"
onClick={() => window.open(file.downloadURL, "_blank")}
tabIndex={tabIndex}
>
<Thumbnail
imageUrl={file.downloadURL}
size={thumbnailSize}
objectFit="contain"
sx={thumbnailSx}
tabIndex={tabIndex}
/>
<Grid
container
justifyContent="center"
alignItems="center"
sx={deleteImgHoverSx}
>
<OpenIcon />
</Grid>
</ButtonBase>
}
</Grid>
))}
</Grid>
</Stack>
);
}

View File

@@ -1,23 +1,12 @@
import { useCallback, useState } from "react";
import { IHeavyCellProps } from "@src/components/fields/types";
import { IEditorCellProps } from "@src/components/fields/types";
import { useAtom, useSetAtom } from "jotai";
import { findIndex } from "lodash-es";
import { useDropzone } from "react-dropzone";
import {
alpha,
Theme,
Box,
Stack,
Grid,
IconButton,
ButtonBase,
Tooltip,
} from "@mui/material";
import { alpha, Box, Stack, Grid, IconButton, ButtonBase } from "@mui/material";
import AddIcon from "@mui/icons-material/AddAPhotoOutlined";
import DeleteIcon from "@mui/icons-material/DeleteOutlined";
import OpenIcon from "@mui/icons-material/OpenInNewOutlined";
import Thumbnail from "@src/components/Thumbnail";
import CircularProgressOptical from "@src/components/CircularProgressOptical";
@@ -32,66 +21,17 @@ import useUploader from "@src/hooks/useFirebaseStorageUploader";
import { IMAGE_MIME_TYPES } from "./index";
import { DEFAULT_ROW_HEIGHT } from "@src/components/Table";
import { FileValue } from "@src/types/table";
// MULTIPLE
const imgSx = (rowHeight: number) => ({
position: "relative",
display: "flex",
width: (theme: Theme) => `calc(${rowHeight}px - ${theme.spacing(1)} - 1px)`,
height: (theme: Theme) => `calc(${rowHeight}px - ${theme.spacing(1)} - 1px)`,
backgroundSize: "contain",
backgroundPosition: "center center",
backgroundRepeat: "no-repeat",
borderRadius: 1,
});
const thumbnailSx = {
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
};
const deleteImgHoverSx = {
position: "absolute",
top: 0,
left: 0,
bottom: 0,
right: 0,
color: "text.secondary",
boxShadow: (theme: Theme) => `0 0 0 1px ${theme.palette.divider} inset`,
borderRadius: 1,
transition: (theme: Theme) =>
theme.transitions.create("background-color", {
duration: theme.transitions.duration.shortest,
}),
"& *": {
opacity: 0,
transition: (theme: Theme) =>
theme.transitions.create("opacity", {
duration: theme.transitions.duration.shortest,
}),
},
".img:hover &": {
backgroundColor: (theme: Theme) =>
alpha(theme.palette.background.paper, 0.8),
"& *": { opacity: 1 },
},
};
import { imgSx, thumbnailSx, deleteImgHoverSx } from "./DisplayCell";
export default function Image_({
column,
value,
onChange,
onSubmit,
disabled,
docRef,
}: IHeavyCellProps) {
_rowy_ref,
tabIndex,
}: IEditorCellProps) {
const confirm = useSetAtom(confirmDialogAtom, projectScope);
const updateField = useSetAtom(updateFieldAtom, tableScope);
const [tableSchema] = useAtom(tableSchemaAtom, tableScope);
@@ -107,13 +47,13 @@ export default function Image_({
if (imageFile) {
upload({
docRef: docRef! as any,
docRef: _rowy_ref,
fieldName: column.key,
files: [imageFile],
previousValue: value,
onComplete: (newValue) => {
updateField({
path: docRef.path,
path: _rowy_ref.path,
fieldName: column.key,
value: newValue,
});
@@ -130,7 +70,8 @@ export default function Image_({
const newValue = [...value];
const toBeDeleted = newValue.splice(index, 1);
toBeDeleted.length && deleteUpload(toBeDeleted[0]);
onSubmit(newValue);
onChange(newValue);
onSubmit();
};
const { getRootProps, getInputProps, isDragActive } = useDropzone({
@@ -154,9 +95,8 @@ export default function Image_({
{
py: 0,
pl: 1,
pr: 0.5,
outline: "none",
height: "100%",
width: "100%",
},
isDragActive
? {
@@ -172,6 +112,7 @@ export default function Image_({
]}
alignItems="center"
{...dropzoneProps}
tabIndex={tabIndex}
onClick={undefined}
>
<div
@@ -185,67 +126,37 @@ export default function Image_({
{Array.isArray(value) &&
value.map((file: FileValue, i) => (
<Grid item key={file.downloadURL}>
{disabled ? (
<Tooltip title="Open">
<ButtonBase
sx={imgSx(rowHeight)}
className="img"
onClick={() => window.open(file.downloadURL, "_blank")}
>
<Thumbnail
imageUrl={file.downloadURL}
size={thumbnailSize}
objectFit="contain"
sx={thumbnailSx}
/>
<Grid
container
justifyContent="center"
alignItems="center"
sx={deleteImgHoverSx}
>
{disabled ? (
<OpenIcon />
) : (
<DeleteIcon color="inherit" />
)}
</Grid>
</ButtonBase>
</Tooltip>
) : (
<Tooltip title="Delete…">
<div>
<ButtonBase
sx={imgSx(rowHeight)}
className="img"
onClick={() => {
confirm({
title: "Delete image?",
body: "This image cannot be recovered after",
confirm: "Delete",
confirmColor: "error",
handleConfirm: handleDelete(i),
});
}}
>
<Thumbnail
imageUrl={file.downloadURL}
size={thumbnailSize}
objectFit="contain"
sx={thumbnailSx}
/>
<Grid
container
justifyContent="center"
alignItems="center"
sx={deleteImgHoverSx}
>
<DeleteIcon color="error" />
</Grid>
</ButtonBase>
</div>
</Tooltip>
)}
<ButtonBase
aria-label="Delete…"
sx={imgSx(rowHeight)}
className="img"
onClick={() => {
confirm({
title: "Delete image?",
body: "This image cannot be recovered after",
confirm: "Delete",
confirmColor: "error",
handleConfirm: handleDelete(i),
});
}}
disabled={disabled}
tabIndex={tabIndex}
>
<Thumbnail
imageUrl={file.downloadURL}
size={thumbnailSize}
objectFit="contain"
sx={thumbnailSx}
/>
<Grid
container
justifyContent="center"
alignItems="center"
sx={deleteImgHoverSx}
>
<DeleteIcon color="error" />
</Grid>
</ButtonBase>
</Grid>
))}
@@ -275,8 +186,9 @@ export default function Image_({
e.stopPropagation();
}}
style={{ display: "flex" }}
className={docRef && "row-hover-iconButton"}
disabled={!docRef}
className={_rowy_ref && "row-hover-iconButton end"}
disabled={!_rowy_ref}
tabIndex={tabIndex}
>
<AddIcon />
</IconButton>
@@ -292,7 +204,7 @@ export default function Image_({
</div>
)}
<input {...getInputProps()} />
<input {...getInputProps()} tabIndex={tabIndex} />
</Stack>
);
}

View File

@@ -1,14 +1,13 @@
import { lazy } from "react";
import { IFieldConfig, FieldType } from "@src/components/fields/types";
import withHeavyCell from "@src/components/fields/_withTableCell/withHeavyCell";
import withTableCell from "@src/components/Table/withTableCell";
import { Image as ImageIcon } from "@src/assets/icons";
import BasicCell from "@src/components/fields/_BasicCell/BasicCellNull";
import NullEditor from "@src/components/Table/editors/NullEditor";
import DisplayCell from "./DisplayCell";
import ContextMenuActions from "./ContextMenuActions";
const TableCell = lazy(
() => import("./TableCell" /* webpackChunkName: "TableCell-Image" */)
const EditorCell = lazy(
() => import("./EditorCell" /* webpackChunkName: "EditorCell-Image" */)
);
const SideDrawerField = lazy(
() =>
@@ -24,8 +23,9 @@ export const config: IFieldConfig = {
icon: <ImageIcon />,
description:
"Image file uploaded to Firebase Storage. Supports JPEG, PNG, SVG, GIF, WebP, AVIF, JPEG XL.",
TableCell: withHeavyCell(BasicCell, TableCell),
TableEditor: NullEditor as any,
TableCell: withTableCell(DisplayCell, EditorCell, "inline", {
disablePadding: true,
}),
SideDrawerField,
contextMenuActions: ContextMenuActions,
};

View File

@@ -0,0 +1,68 @@
import React, { forwardRef } from "react";
import { IDisplayCellProps } from "@src/components/fields/types";
import MuiRating, { RatingProps as MuiRatingProps } from "@mui/material/Rating";
import RatingIcon from "@mui/icons-material/Star";
import RatingOutlineIcon from "@mui/icons-material/StarBorder";
import { get } from "lodash-es";
export const getStateIcon = (config: any) => {
// only use the config to get the custom rating icon if enabled via toggle
if (!get(config, "customIcons.enabled")) {
return <RatingIcon />;
}
return get(config, "customIcons.rating") || <RatingIcon />;
};
export const getStateOutline = (config: any) => {
if (!get(config, "customIcons.enabled")) {
return <RatingOutlineIcon />;
}
return get(config, "customIcons.rating") || <RatingOutlineIcon />;
};
export const Rating = forwardRef(function Rating(
{
_rowy_ref,
column,
value,
disabled,
onChange,
}: IDisplayCellProps & Pick<MuiRatingProps, "onChange">,
ref: React.Ref<HTMLElement>
) {
// Set max and precision from config
const {
max,
precision,
}: {
max: number;
precision: number;
} = {
max: 5,
precision: 1,
...column.config,
};
return (
<MuiRating
ref={ref}
onChange={onChange}
name={`${_rowy_ref.path}-${column.key}`}
value={typeof value === "number" ? value : 0}
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => {
if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(e.key))
e.stopPropagation();
}}
icon={getStateIcon(column.config)}
size="small"
readOnly={disabled}
emptyIcon={getStateOutline(column.config)}
max={max}
precision={precision}
sx={{ mx: -0.25 }}
/>
);
});
export default Rating;

View File

@@ -0,0 +1,27 @@
import { useRef, useEffect } from "react";
import { IEditorCellProps } from "@src/components/fields/types";
import DisplayCell from "./DisplayCell";
export default function Rating({
onChange,
tabIndex,
...props
}: IEditorCellProps) {
const ref = useRef<HTMLElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const inputs = el.querySelectorAll("input");
for (const input of inputs)
input.setAttribute("tabindex", tabIndex.toString());
}, [tabIndex]);
return (
<DisplayCell
{...props}
tabIndex={tabIndex}
onChange={(_, newValue) => onChange(newValue)}
ref={ref}
/>
);
}

View File

@@ -3,7 +3,7 @@ import { ISideDrawerFieldProps } from "@src/components/fields/types";
import { Grid } from "@mui/material";
import { Rating as MuiRating } from "@mui/material";
import "@mui/lab";
import { getStateIcon, getStateOutline } from "./TableCell";
import { getStateIcon, getStateOutline } from "./DisplayCell";
import { fieldSx } from "@src/components/SideDrawer/utils";
export default function Rating({
@@ -24,7 +24,6 @@ export default function Rating({
value={typeof value === "number" ? value : 0}
disabled={disabled}
onChange={(_, newValue) => {
console.log("onChange", newValue);
onChange(newValue);
onSubmit();
}}

View File

@@ -1,55 +0,0 @@
import { IHeavyCellProps } from "@src/components/fields/types";
import MuiRating from "@mui/material/Rating";
import RatingIcon from "@mui/icons-material/Star";
import RatingOutlineIcon from "@mui/icons-material/StarBorder"
import { get } from "lodash-es";
export const getStateIcon = (config: any) => {
// only use the config to get the custom rating icon if enabled via toggle
if (!get(config, "customIcons.enabled")) { return <RatingIcon /> }
return get(config, "customIcons.rating") || <RatingIcon />;
};
export const getStateOutline = (config: any) => {
if (!get(config, "customIcons.enabled")) { return <RatingOutlineIcon /> }
return get(config, "customIcons.rating") || <RatingOutlineIcon />;
}
export default function Rating({
row,
column,
value,
onSubmit,
disabled,
}: IHeavyCellProps) {
// Set max and precision from config
const {
max,
precision,
}: {
max: number;
precision: number;
} = {
max: 5,
precision: 1,
...column.config,
};
return (
<MuiRating
name={`${row.id}-${column.key}`}
value={typeof value === "number" ? value : 0}
onClick={(e) => e.stopPropagation()}
icon={getStateIcon(column.config)}
size="small"
disabled={disabled}
onChange={(_, newValue) => onSubmit(newValue)}
emptyIcon={getStateOutline(column.config)}
max={max}
precision={precision}
sx={{ mx: -0.25 }}
/>
);
}

View File

@@ -1,15 +1,12 @@
import { lazy } from "react";
import { IFieldConfig, FieldType } from "@src/components/fields/types";
import withHeavyCell from "@src/components/fields/_withTableCell/withHeavyCell";
import withTableCell from "@src/components/Table/withTableCell";
import RatingIcon from "@mui/icons-material/StarBorder";
import BasicCell from "@src/components/fields/_BasicCell/BasicCellNull";
import NullEditor from "@src/components/Table/editors/NullEditor";
import DisplayCell from "./DisplayCell";
import EditorCell from "./EditorCell";
import { filterOperators } from "@src/components/fields/Number/Filter";
const TableCell = lazy(
() => import("./TableCell" /* webpackChunkName: "TableCell-Rating" */)
);
const SideDrawerField = lazy(
() =>
import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-Rating" */)
@@ -29,8 +26,7 @@ export const config: IFieldConfig = {
requireConfiguration: true,
description:
"Rating displayed as stars. Max stars is configurable, default: 5 stars.",
TableCell: withHeavyCell(BasicCell, TableCell),
TableEditor: NullEditor as any,
TableCell: withTableCell(DisplayCell, EditorCell, "inline"),
settings: Settings,
SideDrawerField,
filter: {

View File

@@ -0,0 +1,46 @@
import { IDisplayCellProps } from "@src/components/fields/types";
import { ButtonBase } from "@mui/material";
import { ChevronDown } from "@src/assets/icons";
import { sanitiseValue } from "./utils";
export default function SingleSelect({
value,
showPopoverCell,
disabled,
tabIndex,
}: IDisplayCellProps) {
const rendered = (
<div
style={{
flexGrow: 1,
overflow: "hidden",
paddingLeft: "var(--cell-padding)",
}}
>
{sanitiseValue(value)}
</div>
);
if (disabled) return rendered;
return (
<ButtonBase
onClick={() => showPopoverCell(true)}
style={{
width: "100%",
height: "100%",
font: "inherit",
color: "inherit !important",
letterSpacing: "inherit",
textAlign: "inherit",
justifyContent: "flex-start",
}}
tabIndex={tabIndex}
>
{rendered}
<ChevronDown className="row-hover-iconButton end" />
</ButtonBase>
);
}

View File

@@ -1,23 +1,24 @@
import { IPopoverCellProps } from "@src/components/fields/types";
import { IEditorCellProps } from "@src/components/fields/types";
import MultiSelect_ from "@rowy/multiselect";
import MultiSelectComponent from "@rowy/multiselect";
import { sanitiseValue } from "./utils";
export default function SingleSelect({
value,
onChange,
onSubmit,
column,
parentRef,
showPopoverCell,
disabled,
}: IPopoverCellProps) {
}: IEditorCellProps) {
const config = column.config ?? {};
return (
<MultiSelect_
<MultiSelectComponent
value={sanitiseValue(value)}
onChange={onSubmit}
onChange={onChange}
options={config.options ?? []}
multiple={false}
freeText={config.freeText}
@@ -30,12 +31,18 @@ export default function SingleSelect({
open: true,
MenuProps: {
anchorEl: parentRef,
anchorOrigin: { vertical: "bottom", horizontal: "left" },
transformOrigin: { vertical: "top", horizontal: "left" },
anchorOrigin: { vertical: "bottom", horizontal: "center" },
transformOrigin: { vertical: "top", horizontal: "center" },
sx: {
"& .MuiPaper-root": { minWidth: `${column.width}px !important` },
},
},
},
}}
onClose={() => showPopoverCell(false)}
onClose={() => {
showPopoverCell(false);
onSubmit();
}}
/>
);
}

View File

@@ -1,51 +0,0 @@
import { forwardRef } from "react";
import { IPopoverInlineCellProps } from "@src/components/fields/types";
import { ButtonBase } from "@mui/material";
import { ChevronDown } from "@src/assets/icons";
import { sanitiseValue } from "./utils";
export const SingleSelect = forwardRef(function SingleSelect(
{ value, showPopoverCell, disabled }: IPopoverInlineCellProps,
ref: React.Ref<any>
) {
return (
<ButtonBase
onClick={() => showPopoverCell(true)}
ref={ref}
disabled={disabled}
className="cell-collapse-padding"
style={{
padding: "var(--cell-padding)",
paddingRight: 0,
height: "100%",
font: "inherit",
color: "inherit !important",
letterSpacing: "inherit",
textAlign: "inherit",
justifyContent: "flex-start",
}}
>
<div style={{ flexGrow: 1, overflow: "hidden" }}>
{sanitiseValue(value)}
</div>
{!disabled && (
<ChevronDown
className="row-hover-iconButton"
sx={{
flexShrink: 0,
mr: 0.5,
borderRadius: 1,
p: (32 - 20) / 2 / 8,
boxSizing: "content-box !important",
}}
/>
)}
</ButtonBase>
);
});
export default SingleSelect;

View File

@@ -1,17 +1,12 @@
import { lazy } from "react";
import { IFieldConfig, FieldType } from "@src/components/fields/types";
import withPopoverCell from "@src/components/fields/_withTableCell/withPopoverCell";
import withTableCell from "@src/components/Table/withTableCell";
import { SingleSelect as SingleSelectIcon } from "@src/assets/icons";
import BasicCell from "@src/components/fields/_BasicCell/BasicCellNull";
import InlineCell from "./InlineCell";
import NullEditor from "@src/components/Table/editors/NullEditor";
import DisplayCell from "./DisplayCell";
import EditorCell from "./EditorCell";
import { filterOperators } from "@src/components/fields/ShortText/Filter";
const PopoverCell = lazy(
() =>
import("./PopoverCell" /* webpackChunkName: "PopoverCell-SingleSelect" */)
);
const SideDrawerField = lazy(
() =>
import(
@@ -32,11 +27,10 @@ export const config: IFieldConfig = {
icon: <SingleSelectIcon />,
description:
"Single value from predefined options. Options are searchable and users can optionally input custom values.",
TableCell: withPopoverCell(BasicCell, InlineCell, PopoverCell, {
anchorOrigin: { horizontal: "left", vertical: "bottom" },
transparent: true,
TableCell: withTableCell(DisplayCell, EditorCell, "popover", {
disablePadding: true,
transparentPopover: true,
}),
TableEditor: NullEditor as any,
SideDrawerField,
settings: Settings,
filter: { operators: filterOperators },

View File

@@ -0,0 +1,71 @@
import { forwardRef, useMemo } from "react";
import { IDisplayCellProps } from "@src/components/fields/types";
import { ButtonBase } from "@mui/material";
import { ChevronDown } from "@src/assets/icons";
import getLabel from "./utils/getLabelHelper";
export const StatusSingleSelect = forwardRef(function StatusSingleSelect({
column,
value,
showPopoverCell,
disabled,
tabIndex,
}: IDisplayCellProps) {
const conditions = column.config?.conditions;
const rendered = useMemo(() => {
const lowPriorityOperator = ["<", "<=", ">=", ">"];
const otherOperator = (conditions ?? []).filter(
(c: any) => !lowPriorityOperator.includes(c.operator)
);
/**Revisit this */
const sortLowPriorityList = (conditions ?? [])
.filter((c: any) => {
return lowPriorityOperator.includes(c.operator);
})
.sort((a: any, b: any) => {
const aDistFromValue = Math.abs(value - a.value);
const bDistFromValue = Math.abs(value - b.value);
//return the smallest distance
return aDistFromValue - bDistFromValue;
});
const sortedConditions = [...otherOperator, ...sortLowPriorityList];
return (
<div
style={{
flexGrow: 1,
overflow: "hidden",
paddingLeft: "var(--cell-padding)",
}}
>
{getLabel(value, sortedConditions)}
</div>
);
}, [value, conditions]);
if (disabled) return rendered;
return (
<ButtonBase
onClick={() => showPopoverCell(true)}
style={{
width: "100%",
height: "100%",
font: "inherit",
color: "inherit !important",
letterSpacing: "inherit",
textAlign: "inherit",
justifyContent: "flex-start",
}}
tabIndex={tabIndex}
>
{rendered}
<ChevronDown className="row-hover-iconButton end" />
</ButtonBase>
);
});
export default StatusSingleSelect;

View File

@@ -1,14 +1,15 @@
import { IPopoverCellProps } from "@src/components/fields/types";
import MultiSelect_ from "@rowy/multiselect";
import { IEditorCellProps } from "@src/components/fields/types";
import MultiSelectComponent from "@rowy/multiselect";
export default function StatusSingleSelect({
value,
onChange,
onSubmit,
column,
parentRef,
showPopoverCell,
disabled,
}: IPopoverCellProps) {
}: IEditorCellProps) {
const config = column.config ?? {};
const conditions = config.conditions ?? [];
/**Revisit eventually, can we abstract or use a helper function to clean this? */
@@ -22,9 +23,9 @@ export default function StatusSingleSelect({
});
return (
// eslint-disable-next-line react/jsx-pascal-case
<MultiSelect_
<MultiSelectComponent
value={value}
onChange={(v) => onSubmit(v)}
onChange={(v) => onChange(v)}
options={conditions.length >= 1 ? reMappedConditions : []} // this handles when conditions are deleted
multiple={false}
freeText={config.freeText}
@@ -37,12 +38,18 @@ export default function StatusSingleSelect({
open: true,
MenuProps: {
anchorEl: parentRef,
anchorOrigin: { vertical: "bottom", horizontal: "left" },
transformOrigin: { vertical: "top", horizontal: "left" },
anchorOrigin: { vertical: "bottom", horizontal: "center" },
transformOrigin: { vertical: "top", horizontal: "center" },
sx: {
"& .MuiPaper-root": { minWidth: `${column.width}px !important` },
},
},
},
}}
onClose={() => showPopoverCell(false)}
onClose={() => {
showPopoverCell(false);
onSubmit();
}}
/>
);
}

View File

@@ -1,69 +0,0 @@
import { forwardRef, useMemo } from "react";
import { IPopoverInlineCellProps } from "@src/components/fields/types";
import { ButtonBase } from "@mui/material";
import { ChevronDown } from "@src/assets/icons";
import getLabel from "./utils/getLabelHelper";
export const StatusSingleSelect = forwardRef(function StatusSingleSelect(
{ column, value, showPopoverCell, disabled }: IPopoverInlineCellProps,
ref: React.Ref<any>
) {
const conditions = column.config?.conditions ?? [];
const lowPriorityOperator = ["<", "<=", ">=", ">"];
const otherOperator = conditions.filter(
(c: any) => !lowPriorityOperator.includes(c.operator)
);
/**Revisit this */
const sortLowPriorityList = conditions
.filter((c: any) => {
return lowPriorityOperator.includes(c.operator);
})
.sort((a: any, b: any) => {
const aDistFromValue = Math.abs(value - a.value);
const bDistFromValue = Math.abs(value - b.value);
//return the smallest distance
return aDistFromValue - bDistFromValue;
});
const sortedConditions = [...otherOperator, ...sortLowPriorityList];
const label = useMemo(
() => getLabel(value, sortedConditions),
[value, sortedConditions]
);
return (
<ButtonBase
onClick={() => showPopoverCell(true)}
ref={ref}
disabled={disabled}
className="cell-collapse-padding"
style={{
padding: "var(--cell-padding)",
paddingRight: 0,
height: "100%",
font: "inherit",
color: "inherit !important",
letterSpacing: "inherit",
textAlign: "inherit",
justifyContent: "flex-start",
}}
>
<div style={{ flexGrow: 1, overflow: "hidden" }}>{label}</div>
{!disabled && (
<ChevronDown
className="row-hover-iconButton"
sx={{
flexShrink: 0,
mr: 0.5,
borderRadius: 1,
p: (32 - 20) / 2 / 8,
boxSizing: "content-box !important",
}}
/>
)}
</ButtonBase>
);
});
export default StatusSingleSelect;

View File

@@ -1,13 +1,11 @@
import { lazy } from "react";
import { IFieldConfig, FieldType } from "@src/components/fields/types";
import { Status as StatusIcon } from "@src/assets/icons";
import NullEditor from "@src/components/Table/editors/NullEditor";
import withTableCell from "@src/components/Table/withTableCell";
import { Status as StatusIcon } from "@src/assets/icons";
import DisplayCell from "./DisplayCell";
import EditorCell from "./EditorCell";
import { filterOperators } from "./Filter";
import BasicCell from "@src/components/fields/_BasicCell/BasicCellNull";
import PopoverCell from "./PopoverCell";
import InlineCell from "./InlineCell";
import withPopoverCell from "@src/components/fields/_withTableCell/withPopoverCell";
const SideDrawerField = lazy(
() =>
@@ -25,12 +23,11 @@ export const config: IFieldConfig = {
initialValue: undefined,
initializable: true,
icon: <StatusIcon />,
description: "Displays field value as custom status text. Read-only. ",
TableCell: withPopoverCell(BasicCell, InlineCell, PopoverCell, {
anchorOrigin: { horizontal: "left", vertical: "bottom" },
transparent: true,
description: "Displays field value as custom status text.",
TableCell: withTableCell(DisplayCell, EditorCell, "popover", {
disablePadding: true,
transparentPopover: true,
}),
TableEditor: NullEditor as any,
settings: Settings,
SideDrawerField,
requireConfiguration: true,

View File

@@ -1,27 +1,31 @@
import { IHeavyCellProps } from "@src/components/fields/types";
import { IDisplayCellProps } from "@src/components/fields/types";
import { Link } from "react-router-dom";
import { Stack, IconButton } from "@mui/material";
import LaunchIcon from "@mui/icons-material/Launch";
import OpenIcon from "@mui/icons-material/OpenInBrowser";
import { useSubTableData } from "./utils";
export default function SubTable({ column, row }: IHeavyCellProps) {
export default function SubTable({
column,
row,
_rowy_ref,
tabIndex,
}: IDisplayCellProps) {
const { documentCount, label, subTablePath } = useSubTableData(
column as any,
row,
row._rowy_ref
_rowy_ref
);
if (!row._rowy_ref) return null;
if (!_rowy_ref) return null;
return (
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
className="cell-collapse-padding"
sx={{ p: "var(--cell-padding)", pr: 0.5 }}
style={{ paddingLeft: "var(--cell-padding)", width: "100%" }}
>
<div style={{ flexGrow: 1, overflow: "hidden" }}>
{documentCount} {column.name as string}: {label}
@@ -30,12 +34,12 @@ export default function SubTable({ column, row }: IHeavyCellProps) {
<IconButton
component={Link}
to={subTablePath}
className="row-hover-iconButton"
className="row-hover-iconButton end"
size="small"
disabled={!subTablePath}
style={{ flexShrink: 0 }}
tabIndex={tabIndex}
>
<LaunchIcon />
<OpenIcon />
</IconButton>
</Stack>
);

View File

@@ -6,7 +6,7 @@ import { ISideDrawerFieldProps } from "@src/components/fields/types";
import { Link } from "react-router-dom";
import { Box, Stack, IconButton } from "@mui/material";
import LaunchIcon from "@mui/icons-material/Launch";
import OpenIcon from "@mui/icons-material/OpenInBrowser";
import { tableScope, tableRowsAtom } from "@src/atoms/tableScope";
import { fieldSx, getFieldId } from "@src/components/SideDrawer/utils";
@@ -46,7 +46,7 @@ export default function SubTable({ column, _rowy_ref }: ISideDrawerFieldProps) {
sx={{ ml: 1 }}
disabled={!subTablePath}
>
<LaunchIcon />
<OpenIcon />
</IconButton>
</Stack>
);

View File

@@ -1,14 +1,10 @@
import { lazy } from "react";
import { IFieldConfig, FieldType } from "@src/components/fields/types";
import withHeavyCell from "@src/components/fields/_withTableCell/withHeavyCell";
import withTableCell from "@src/components/Table/withTableCell";
import { SubTable as SubTableIcon } from "@src/assets/icons";
import BasicCell from "@src/components/fields/_BasicCell/BasicCellName";
import NullEditor from "@src/components/Table/editors/NullEditor";
import DisplayCell from "./DisplayCell";
const TableCell = lazy(
() => import("./TableCell" /* webpackChunkName: "TableCell-SubTable" */)
);
const SideDrawerField = lazy(
() =>
import(
@@ -28,8 +24,10 @@ export const config: IFieldConfig = {
settings: Settings,
description:
"Connects to a sub-table in the current row. Also displays number of rows inside the sub-table. Max sub-table depth: 100.",
TableCell: withHeavyCell(BasicCell, TableCell),
TableEditor: NullEditor as any,
TableCell: withTableCell(DisplayCell, null, "focus", {
usesRowData: true,
disablePadding: true,
}),
SideDrawerField,
initializable: false,
requireConfiguration: true,

View File

@@ -1,7 +1,6 @@
import { useReducer } from "react";
import { useAtom } from "jotai";
import { useSnackbar } from "notistack";
import type { DocumentReference } from "firebase/firestore";
import {
ref,
uploadBytesResumable,
@@ -15,7 +14,7 @@ import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
import { projectScope } from "@src/atoms/projectScope";
import { firebaseStorageAtom } from "@src/sources/ProjectSourceFirebase";
import { WIKI_LINKS } from "@src/constants/externalLinks";
import { FileValue } from "@src/types/table";
import { FileValue, TableRowRef } from "@src/types/table";
export type UploaderState = {
progress: number;
@@ -30,7 +29,7 @@ const uploadReducer = (
) => ({ ...prevState, ...newProps });
export type UploadProps = {
docRef: DocumentReference;
docRef: TableRowRef;
fieldName: string;
files: File[];
previousValue?: FileValue[];