mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-29 00:16:39 +01:00
migrate all remaining fields
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
36
src/components/fields/File/DisplayCell.tsx
Normal file
36
src/components/fields/File/DisplayCell.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
113
src/components/fields/Image/DisplayCell.tsx
Normal file
113
src/components/fields/Image/DisplayCell.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
68
src/components/fields/Rating/DisplayCell.tsx
Normal file
68
src/components/fields/Rating/DisplayCell.tsx
Normal 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;
|
||||
27
src/components/fields/Rating/EditorCell.tsx
Normal file
27
src/components/fields/Rating/EditorCell.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
}}
|
||||
|
||||
@@ -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 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
46
src/components/fields/SingleSelect/DisplayCell.tsx
Normal file
46
src/components/fields/SingleSelect/DisplayCell.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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 },
|
||||
|
||||
71
src/components/fields/Status/DisplayCell.tsx
Normal file
71
src/components/fields/Status/DisplayCell.tsx
Normal 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;
|
||||
@@ -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();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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[];
|
||||
|
||||
Reference in New Issue
Block a user