Merge pull request #1130 from rowyio/develop

Develop
This commit is contained in:
Shams
2023-02-22 15:16:49 +01:00
committed by GitHub
18 changed files with 760 additions and 278 deletions

View File

@@ -64,6 +64,7 @@ import {
} from "@src/utils/table";
import { runRoutes } from "@src/constants/runRoutes";
import { useSnackLogContext } from "@src/contexts/SnackLogContext";
import useSaveTableSorts from "@src/components/Table/ColumnHeader/useSaveTableSorts";
export interface IMenuModalProps {
name: string;
@@ -116,6 +117,8 @@ export default function ColumnMenu({
const [altPress] = useAtom(altPressAtom, projectScope);
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
const triggerSaveTableSorts = useSaveTableSorts(canEditColumns);
if (!columnMenu) return null;
const { column, anchorEl } = columnMenu;
if (column.type === FieldType.last) return null;
@@ -189,6 +192,9 @@ export default function ColumnMenu({
setTableSorts(
isSorted && !isAsc ? [] : [{ key: sortKey, direction: "desc" }]
);
if (!isSorted || isAsc) {
triggerSaveTableSorts([{ key: sortKey, direction: "desc" }]);
}
handleClose();
},
active: isSorted && !isAsc,
@@ -203,6 +209,9 @@ export default function ColumnMenu({
setTableSorts(
isSorted && isAsc ? [] : [{ key: sortKey, direction: "asc" }]
);
if (!isSorted || !isAsc) {
triggerSaveTableSorts([{ key: sortKey, direction: "asc" }]);
}
handleClose();
},
active: isSorted && isAsc,

View File

@@ -233,6 +233,7 @@ export const ColumnHeader = memo(function ColumnHeader({
sortKey={sortKey}
currentSort={currentSort}
tabIndex={focusInsideCell ? 0 : -1}
canEditColumns={canEditColumns}
/>
)}

View File

@@ -9,6 +9,7 @@ import IconSlash, {
} from "@src/components/IconSlash";
import { tableScope, tableSortsAtom } from "@src/atoms/tableScope";
import useSaveTableSorts from "./useSaveTableSorts";
export const SORT_STATES = ["none", "desc", "asc"] as const;
@@ -16,6 +17,7 @@ export interface IColumnHeaderSortProps {
sortKey: string;
currentSort: typeof SORT_STATES[number];
tabIndex?: number;
canEditColumns: boolean;
}
/**
@@ -26,15 +28,24 @@ export const ColumnHeaderSort = memo(function ColumnHeaderSort({
sortKey,
currentSort,
tabIndex,
canEditColumns,
}: IColumnHeaderSortProps) {
const setTableSorts = useSetAtom(tableSortsAtom, tableScope);
const nextSort =
SORT_STATES[SORT_STATES.indexOf(currentSort) + 1] ?? SORT_STATES[0];
const triggerSaveTableSorts = useSaveTableSorts(canEditColumns);
const handleSortClick = () => {
if (nextSort === "none") setTableSorts([]);
else setTableSorts([{ key: sortKey, direction: nextSort }]);
triggerSaveTableSorts([
{
key: sortKey,
direction: nextSort === "none" ? "asc" : nextSort,
},
]);
};
return (

View File

@@ -0,0 +1,98 @@
import { useCallback, useState } from "react";
import { useAtom } from "jotai";
import { SnackbarKey, useSnackbar } from "notistack";
import LoadingButton from "@mui/lab/LoadingButton";
import CheckIcon from "@mui/icons-material/Check";
import CircularProgressOptical from "@src/components/CircularProgressOptical";
import {
tableIdAtom,
tableScope,
updateTableSchemaAtom,
} from "@src/atoms/tableScope";
import { projectScope, updateUserSettingsAtom } from "@src/atoms/projectScope";
import { TableSort } from "@src/types/table";
function useSaveTableSorts(canEditColumns: boolean) {
const [updateTableSchema] = useAtom(updateTableSchemaAtom, tableScope);
const [updateUserSettings] = useAtom(updateUserSettingsAtom, projectScope);
const [tableId] = useAtom(tableIdAtom, tableScope);
if (!updateTableSchema) throw new Error("Cannot update table schema");
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
const [snackbarId, setSnackbarId] = useState<SnackbarKey | null>(null);
// Offer to save when table sorts changes
const trigger = useCallback(
(sorts: TableSort[]) => {
if (updateUserSettings) {
updateUserSettings({
tables: {
[`${tableId}`]: { sorts },
},
});
}
if (!canEditColumns) return;
if (snackbarId) {
closeSnackbar(snackbarId);
}
setSnackbarId(
enqueueSnackbar("Apply this sorting for all users?", {
action: (
<SaveTableSortButton
updateTable={async () => await updateTableSchema({ sorts })}
/>
),
anchorOrigin: { horizontal: "center", vertical: "top" },
})
);
return () => (snackbarId ? closeSnackbar(snackbarId) : null);
},
[
updateUserSettings,
canEditColumns,
snackbarId,
enqueueSnackbar,
tableId,
closeSnackbar,
updateTableSchema,
]
);
return trigger;
}
function SaveTableSortButton({ updateTable }: { updateTable: Function }) {
const [state, setState] = useState<"" | "loading" | "success" | "error">("");
const handleSaveToSchema = async () => {
setState("loading");
try {
await updateTable();
setState("success");
} catch (e) {
setState("error");
}
};
return (
<LoadingButton
variant="contained"
color="primary"
onClick={handleSaveToSchema}
loading={Boolean(state)}
loadingIndicator={
state === "success" ? (
<CheckIcon color="primary" />
) : (
<CircularProgressOptical size={20} color="primary" />
)
}
>
Save
</LoadingButton>
);
}
export default useSaveTableSorts;

View File

@@ -19,6 +19,17 @@ export const StyledCell = styled("div")(({ theme }) => ({
alignItems: "center",
},
"& > .cell-contents-contain-none": {
padding: "0 var(--cell-padding)",
width: "100%",
height: "100%",
contain: "none",
overflow: "hidden",
display: "flex",
alignItems: "center",
},
backgroundColor: "var(--cell-background-color)",
border: `1px solid ${theme.palette.divider}`,

View File

@@ -192,7 +192,7 @@ export default function withRenderTableCell(
if (editorMode === "inline") {
return (
<div
className="cell-contents"
className="cell-contents-contain-none"
style={options.disablePadding ? { padding: 0 } : undefined}
ref={displayCellRef}
>

View File

@@ -48,7 +48,7 @@ const inferTypeFromValue = (value: any) => {
if (typeof value === "boolean") return FieldType.checkbox;
if (isDate(value)) return FieldType.dateTime;
// trying to convert the value to date
if (+new Date(value)) {
if (typeof value !== "number" && +new Date(value)) {
// date and time are separated by a blank space, checking if time present.
if (value.split(" ").length > 1) {
return FieldType.dateTime;

View File

@@ -112,7 +112,9 @@ export default function Filters() {
setLocalFilters(filtersToApply);
// Reset order so we dont have to make a new index
setTableSorts([]);
if (filtersToApply.length) {
setTableSorts([]);
}
}, [
hasTableFilters,
hasUserFilters,

View File

@@ -62,7 +62,20 @@ function convertJSONToCSV(rawData: string): string | false {
return false;
}
const fields = extractFields(rawDataJSONified);
const opts = { fields };
const opts = {
fields,
transforms: [
(value: any) => {
// if the value is an array, join it with a comma
for (let key in value) {
if (Array.isArray(value[key])) {
value[key] = value[key].join(",");
}
}
return value;
},
],
};
try {
const csv = parseJSON(rawDataJSONified, opts);

View File

@@ -1,4 +1,3 @@
import { useCallback } from "react";
import { IEditorCellProps } from "@src/components/fields/types";
import { useSetAtom } from "jotai";
@@ -15,6 +14,15 @@ import { DATE_TIME_FORMAT } from "@src/constants/dates";
import { FileValue } from "@src/types/table";
import useFileUpload from "./useFileUpload";
import DragIndicatorIcon from "@mui/icons-material/DragIndicator";
import {
DragDropContext,
Droppable,
Draggable,
DropResult,
ResponderProvided,
} from "react-beautiful-dnd";
export default function File_({
column,
value,
@@ -25,11 +33,40 @@ export default function File_({
}: IEditorCellProps) {
const confirm = useSetAtom(confirmDialogAtom, projectScope);
const { loading, progress, handleDelete, localFiles, dropzoneState } =
useFileUpload(_rowy_ref, column.key, { multiple: true });
const {
loading,
progress,
handleDelete,
localFiles,
dropzoneState,
handleUpdate,
} = useFileUpload(_rowy_ref, column.key, { multiple: true });
const { isDragActive, getRootProps, getInputProps } = dropzoneState;
const dropzoneProps = getRootProps();
const onDragEnd = (result: DropResult, provided: ResponderProvided) => {
const { destination, source } = result;
if (!destination) {
return;
}
if (
destination.droppableId === source.droppableId &&
destination.index === source.index
) {
return;
}
const newValue = Array.from(value);
newValue.splice(source.index, 1);
newValue.splice(destination.index, 0, value[source.index]);
handleUpdate([...newValue]);
};
return (
<Stack
direction="row"
@@ -37,6 +74,8 @@ export default function File_({
sx={{
width: "100%",
height: "100%",
py: 0,
pl: 1,
...(isDragActive
? {
@@ -54,70 +93,110 @@ export default function File_({
tabIndex={tabIndex}
onClick={undefined}
>
<ChipList rowHeight={rowHeight}>
{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)` } : {}
}
>
<Tooltip
title={`File last modified ${format(
file.lastModifiedTS,
DATE_TIME_FORMAT
)}`}
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="image-droppable" direction="horizontal">
{(provided) => (
<ChipList rowHeight={rowHeight}>
<Grid
container
spacing={0.5}
wrap="nowrap"
ref={provided.innerRef}
{...provided.droppableProps}
>
<Chip
label={file.name}
icon={<FileIcon />}
sx={{
"& .MuiChip-label": {
lineHeight: 5 / 3,
},
}}
onClick={(e: any) => e.stopPropagation()}
component="a"
href={file.downloadURL}
target="_blank"
rel="noopener noreferrer"
clickable
onDelete={
disabled
? undefined
: (e) => {
e.preventDefault();
confirm({
handleConfirm: () => handleDelete(file),
title: "Delete file?",
body: "This file cannot be recovered after",
confirm: "Delete",
confirmColor: "error",
});
}
}
tabIndex={tabIndex}
style={{ width: "100%", cursor: "pointer" }}
/>
</Tooltip>
</Grid>
))}
{localFiles &&
localFiles.map((file) => (
<Grid item key={file.name}>
<Chip
icon={<FileIcon />}
label={file.name}
deleteIcon={
<CircularProgressOptical size={20} color="inherit" />
}
/>
</Grid>
))}
</ChipList>
{Array.isArray(value) &&
value.map((file: FileValue, i) => (
<Draggable
key={file.downloadURL}
draggableId={file.downloadURL}
index={i}
>
{(provided) => (
<Grid
item
ref={provided.innerRef}
{...provided.draggableProps}
style={{
display: "flex",
alignItems: "center",
// Truncate so multiple files still visible
maxWidth: `${
value.length > 1 ? "calc(100% - 12px)" : "initial"
}`,
...provided.draggableProps.style,
}}
>
{value.length > 1 && (
<div
{...provided.dragHandleProps}
style={{
display: "flex",
alignItems: "center",
}}
>
<DragIndicatorIcon />
</div>
)}
<Tooltip
title={`File last modified ${format(
file.lastModifiedTS,
DATE_TIME_FORMAT
)}`}
>
<Chip
label={file.name}
icon={<FileIcon />}
sx={{
"& .MuiChip-label": {
lineHeight: 5 / 3,
},
}}
onClick={(e: any) => e.stopPropagation()}
component="a"
href={file.downloadURL}
target="_blank"
rel="noopener noreferrer"
clickable
onDelete={
disabled
? undefined
: (e) => {
e.preventDefault();
confirm({
handleConfirm: () => handleDelete(file),
title: "Delete file?",
body: "This file cannot be recovered after",
confirm: "Delete",
confirmColor: "error",
});
}
}
tabIndex={tabIndex}
style={{ width: "100%", cursor: "pointer" }}
/>
</Tooltip>
</Grid>
)}
</Draggable>
))}
</Grid>
{localFiles &&
localFiles.map((file) => (
<Grid item key={file.name}>
<Chip
icon={<FileIcon />}
label={file.name}
deleteIcon={
<CircularProgressOptical size={20} color="inherit" />
}
/>
</Grid>
))}
</ChipList>
)}
</Droppable>
</DragDropContext>
{!loading ? (
!disabled && (

View File

@@ -20,6 +20,15 @@ import { FileValue } from "@src/types/table";
import useFileUpload from "./useFileUpload";
import { FileIcon } from ".";
import DragIndicatorIcon from "@mui/icons-material/DragIndicator";
import {
DragDropContext,
Droppable,
Draggable,
DropResult,
ResponderProvided,
} from "react-beautiful-dnd";
export default function File_({
column,
_rowy_ref,
@@ -72,52 +81,94 @@ export default function File_({
</ButtonBase>
)}
<Grid container spacing={0.5} style={{ marginTop: 2 }}>
{Array.isArray(value) &&
value.map((file: FileValue) => (
<Grid item key={file.name}>
<Tooltip
title={`File last modified ${format(
file.lastModifiedTS,
DATE_TIME_FORMAT
)}`}
>
<div>
<Chip
icon={<FileIcon />}
label={file.name}
onClick={() => window.open(file.downloadURL)}
onDelete={
!disabled
? () =>
confirm({
title: "Delete file?",
body: "This file cannot be recovered after",
confirm: "Delete",
confirmColor: "error",
handleConfirm: () => handleDelete(file),
})
: undefined
}
/>
</div>
</Tooltip>
</Grid>
))}
<DragDropContext onDragEnd={() => console.log("onDragEnd")}>
<Droppable droppableId="sidebar-file-droppable">
{(provided) => (
<Grid
container
spacing={0.5}
style={{ marginTop: 2 }}
ref={provided.innerRef}
{...provided.droppableProps}
>
{Array.isArray(value) &&
value.map((file: FileValue, i) => (
<Draggable
key={file.downloadURL}
draggableId={file.downloadURL}
index={i}
>
{(provided) => (
<Grid
item
key={file.name}
ref={provided.innerRef}
{...provided.draggableProps}
style={{
display: "flex",
alignItems: "center",
...provided.draggableProps.style,
}}
>
{value.length > 1 && (
<div
{...provided.dragHandleProps}
style={{
display: "flex",
alignItems: "center",
}}
>
<DragIndicatorIcon />
</div>
)}
<Tooltip
title={`File last modified ${format(
file.lastModifiedTS,
DATE_TIME_FORMAT
)}`}
>
<div>
<Chip
icon={<FileIcon />}
label={file.name}
onClick={() => window.open(file.downloadURL)}
onDelete={
!disabled
? () =>
confirm({
title: "Delete file?",
body: "This file cannot be recovered after",
confirm: "Delete",
confirmColor: "error",
handleConfirm: () => handleDelete(file),
})
: undefined
}
/>
</div>
</Tooltip>
</Grid>
)}
</Draggable>
))}
{localFiles &&
localFiles.map((file) => (
<Grid item>
<Chip
icon={<FileIcon />}
label={file.name}
deleteIcon={
<CircularProgressOptical size={20} color="inherit" />
}
/>
{localFiles &&
localFiles.map((file) => (
<Grid item>
<Chip
icon={<FileIcon />}
label={file.name}
deleteIcon={
<CircularProgressOptical size={20} color="inherit" />
}
/>
</Grid>
))}
{provided.placeholder}
</Grid>
))}
</Grid>
)}
</Droppable>
</DragDropContext>
</>
);
}

View File

@@ -75,6 +75,15 @@ export default function useFileUpload(
[deleteUpload, docRef, fieldName, updateField]
);
// Drag and Drop
const handleUpdate = (files: any) => {
updateField({
path: docRef.path,
fieldName,
value: files,
});
};
return {
localFiles,
progress,
@@ -83,5 +92,6 @@ export default function useFileUpload(
handleUpload,
handleDelete,
dropzoneState,
handleUpdate,
};
}

View File

@@ -1,6 +1,6 @@
import { useMemo } from "react";
import { IEditorCellProps } from "@src/components/fields/types";
import { useAtom, useSetAtom } from "jotai";
import { useSetAtom } from "jotai";
import { assignIn } from "lodash-es";
import { alpha, Box, Stack, Grid, IconButton, ButtonBase } from "@mui/material";
@@ -11,13 +11,20 @@ import Thumbnail from "@src/components/Thumbnail";
import CircularProgressOptical from "@src/components/CircularProgressOptical";
import { projectScope, confirmDialogAtom } from "@src/atoms/projectScope";
import { tableSchemaAtom, tableScope } from "@src/atoms/tableScope";
import { DEFAULT_ROW_HEIGHT } from "@src/components/Table";
import { FileValue } from "@src/types/table";
import useFileUpload from "@src/components/fields/File/useFileUpload";
import { IMAGE_MIME_TYPES } from "./index";
import { imgSx, thumbnailSx, deleteImgHoverSx } from "./DisplayCell";
import DragIndicatorIcon from "@mui/icons-material/DragIndicator";
import {
DragDropContext,
Droppable,
Draggable,
DropResult,
ResponderProvided,
} from "react-beautiful-dnd";
export default function Image_({
column,
value,
@@ -28,11 +35,17 @@ export default function Image_({
}: IEditorCellProps) {
const confirm = useSetAtom(confirmDialogAtom, projectScope);
const { loading, progress, handleDelete, localFiles, dropzoneState } =
useFileUpload(_rowy_ref, column.key, {
multiple: true,
accept: IMAGE_MIME_TYPES,
});
const {
loading,
progress,
handleDelete,
localFiles,
dropzoneState,
handleUpdate,
} = useFileUpload(_rowy_ref, column.key, {
multiple: true,
accept: IMAGE_MIME_TYPES,
});
const localImages = useMemo(
() =>
@@ -45,6 +58,28 @@ export default function Image_({
const { getRootProps, getInputProps, isDragActive } = dropzoneState;
const dropzoneProps = getRootProps();
const onDragEnd = (result: DropResult, provided: ResponderProvided) => {
const { destination, source } = result;
if (!destination) {
return;
}
if (
destination.droppableId === source.droppableId &&
destination.index === source.index
) {
return;
}
const newValue = Array.from(value);
newValue.splice(source.index, 1);
newValue.splice(destination.index, 0, value[source.index]);
handleUpdate([...newValue]);
};
let thumbnailSize = "100x100";
if (rowHeight > 50) thumbnailSize = "200x200";
if (rowHeight > 100) thumbnailSize = "400x400";
@@ -84,62 +119,102 @@ export default function Image_({
marginLeft: "0 !important",
}}
>
<Grid container spacing={0.5} wrap="nowrap">
{Array.isArray(value) &&
value.map((file: FileValue, i) => (
<Grid item key={file.downloadURL}>
<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(file),
});
}}
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>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="image-droppable" direction="horizontal">
{(provided) => (
<Grid
container
spacing={0.5}
wrap="nowrap"
ref={provided.innerRef}
{...provided.droppableProps}
>
{Array.isArray(value) &&
value.map((file: FileValue, i) => (
<Draggable
key={file.downloadURL}
draggableId={file.downloadURL}
index={i}
>
{(provided) => (
<Grid
item
ref={provided.innerRef}
{...provided.draggableProps}
style={{
display: "flex",
alignItems: "center",
...provided.draggableProps.style,
}}
>
{value.length > 1 && (
<div
{...provided.dragHandleProps}
style={{
display: "flex",
alignItems: "center",
}}
>
<DragIndicatorIcon />
</div>
)}
<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(file),
});
}}
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>
)}
</Draggable>
))}
{localImages &&
localImages.map((image) => (
<Grid item>
<Box
sx={[
imgSx(rowHeight),
{
boxShadow: (theme) =>
`0 0 0 1px ${theme.palette.divider} inset`,
},
]}
style={{
backgroundImage: `url("${image.localURL}")`,
}}
/>
</Grid>
))}
{provided.placeholder}
</Grid>
))}
{localImages &&
localImages.map((image) => (
<Grid item>
<Box
sx={[
imgSx(rowHeight),
{
boxShadow: (theme) =>
`0 0 0 1px ${theme.palette.divider} inset`,
},
]}
style={{
backgroundImage: `url("${image.localURL}")`,
}}
/>
</Grid>
))}
</Grid>
)}
</Droppable>
</DragDropContext>
</div>
{!loading ? (

View File

@@ -26,6 +26,15 @@ import { fieldSx, getFieldId } from "@src/components/SideDrawer/utils";
import useFileUpload from "@src/components/fields/File/useFileUpload";
import { IMAGE_MIME_TYPES } from ".";
import DragIndicatorIcon from "@mui/icons-material/DragIndicator";
import {
DragDropContext,
Droppable,
Draggable,
DropResult,
ResponderProvided,
} from "react-beautiful-dnd";
const imgSx = {
position: "relative",
width: 80,
@@ -94,6 +103,7 @@ export default function Image_({
uploaderState,
localFiles,
dropzoneState,
handleUpdate,
} = useFileUpload(_rowy_ref, column.key, {
multiple: true,
accept: IMAGE_MIME_TYPES,
@@ -109,6 +119,28 @@ export default function Image_({
const { getRootProps, getInputProps, isDragActive } = dropzoneState;
const onDragEnd = (result: DropResult, provided: ResponderProvided) => {
const { destination, source } = result;
if (!destination) {
return;
}
if (
destination.droppableId === source.droppableId &&
destination.index === source.index
) {
return;
}
const newValue = Array.from(value);
newValue.splice(source.index, 1);
newValue.splice(destination.index, 0, value[source.index]);
handleUpdate([...newValue]);
};
return (
<>
{!disabled && (
@@ -151,112 +183,158 @@ export default function Image_({
</ButtonBase>
)}
<Grid container spacing={1} style={{ marginTop: 0 }}>
{Array.isArray(value) &&
value.map((image: FileValue) => (
<Grid item key={image.name}>
{disabled ? (
<Tooltip title="Open">
<ButtonBase
sx={imgSx}
onClick={() => window.open(image.downloadURL, "_blank")}
className="img"
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="sidebar-image-droppable" direction="horizontal">
{(provided) => (
<Grid
container
spacing={1}
style={{ marginTop: 0 }}
ref={provided.innerRef}
{...provided.droppableProps}
>
{Array.isArray(value) &&
value.map((image: FileValue, i) => (
<Draggable
key={image.downloadURL}
draggableId={image.downloadURL}
index={i}
>
<Thumbnail
imageUrl={image.downloadURL}
size="200x200"
objectFit="contain"
sx={thumbnailSx}
/>
<Grid
container
justifyContent="center"
alignItems="center"
sx={[overlaySx, deleteImgHoverSx]}
{(provided) => (
<Grid item>
{disabled ? (
<Tooltip title="Open">
<ButtonBase
sx={imgSx}
onClick={() =>
window.open(image.downloadURL, "_blank")
}
className="img"
>
<Thumbnail
imageUrl={image.downloadURL}
size="200x200"
objectFit="contain"
sx={thumbnailSx}
/>
<Grid
container
justifyContent="center"
alignItems="center"
sx={[overlaySx, deleteImgHoverSx]}
>
{disabled ? (
<OpenIcon />
) : (
<DeleteIcon color="error" />
)}
</Grid>
</ButtonBase>
</Tooltip>
) : (
<div
ref={provided.innerRef}
{...provided.draggableProps}
style={{
display: "flex",
alignItems: "center",
...provided.draggableProps.style,
}}
>
{value.length > 1 && (
<div
{...provided.dragHandleProps}
style={{
display: "flex",
alignItems: "center",
}}
>
<DragIndicatorIcon />
</div>
)}
<Box sx={imgSx} className="img">
<Thumbnail
imageUrl={image.downloadURL}
size="200x200"
objectFit="contain"
sx={thumbnailSx}
/>
<Grid
container
justifyContent="center"
alignItems="center"
sx={[overlaySx, deleteImgHoverSx]}
>
<Tooltip title="Delete…">
<IconButton
onClick={() =>
confirm({
title: "Delete image?",
body: "This image cannot be recovered after",
confirm: "Delete",
confirmColor: "error",
handleConfirm: () =>
handleDelete(image),
})
}
>
<DeleteIcon color="error" />
</IconButton>
</Tooltip>
<Tooltip title="Open">
<IconButton
onClick={() =>
window.open(image.downloadURL, "_blank")
}
>
<OpenIcon />
</IconButton>
</Tooltip>
</Grid>
</Box>
</div>
)}
</Grid>
)}
</Draggable>
))}
{localImages &&
localImages.map((image) => (
<Grid item key={image.name}>
<ButtonBase
sx={imgSx}
style={{
backgroundImage: `url("${image.localURL}")`,
}}
className="img"
>
{disabled ? <OpenIcon /> : <DeleteIcon color="error" />}
</Grid>
</ButtonBase>
</Tooltip>
) : (
<div>
<Box sx={imgSx} className="img">
<Thumbnail
imageUrl={image.downloadURL}
size="200x200"
objectFit="contain"
sx={thumbnailSx}
/>
<Grid
container
justifyContent="center"
alignItems="center"
sx={[overlaySx, deleteImgHoverSx]}
>
<Tooltip title="Delete…">
<IconButton
onClick={() =>
confirm({
title: "Delete image?",
body: "This image cannot be recovered after",
confirm: "Delete",
confirmColor: "error",
handleConfirm: () => handleDelete(image),
})
}
{uploaderState[image.name] && (
<Grid
container
justifyContent="center"
alignItems="center"
sx={overlaySx}
>
<DeleteIcon color="error" />
</IconButton>
</Tooltip>
<Tooltip title="Open">
<IconButton
onClick={() =>
window.open(image.downloadURL, "_blank")
}
>
<OpenIcon />
</IconButton>
</Tooltip>
</Grid>
</Box>
</div>
)}
</Grid>
))}
{localImages &&
localImages.map((image) => (
<Grid item key={image.name}>
<ButtonBase
sx={imgSx}
style={{
backgroundImage: `url("${image.localURL}")`,
}}
className="img"
>
{uploaderState[image.name] && (
<Grid
container
justifyContent="center"
alignItems="center"
sx={overlaySx}
>
<CircularProgressOptical
color="inherit"
size={48}
variant={
uploaderState[image.name].progress === 0
? "indeterminate"
: "determinate"
}
value={uploaderState[image.name].progress}
/>
<CircularProgressOptical
color="inherit"
size={48}
variant={
uploaderState[image.name].progress === 0
? "indeterminate"
: "determinate"
}
value={uploaderState[image.name].progress}
/>
</Grid>
)}
</ButtonBase>
</Grid>
)}
</ButtonBase>
))}
{provided.placeholder}
</Grid>
))}
</Grid>
)}
</Droppable>
</DragDropContext>
</>
);
}

View File

@@ -33,6 +33,7 @@ import {
tableSchemaAtom,
columnModalAtom,
tableModalAtom,
tableSortsAtom,
} from "@src/atoms/tableScope";
import useBeforeUnload from "@src/hooks/useBeforeUnload";
import ActionParamsProvider from "@src/components/fields/Action/FormDialog/Provider";

View File

@@ -39,6 +39,7 @@ import { getTableSchemaPath } from "@src/utils/table";
import { TableSchema } from "@src/types/table";
import { firebaseDbAtom } from "@src/sources/ProjectSourceFirebase";
import { projectScope } from "@src/atoms/projectScope";
import useApplySorts from "./useApplySorts";
/**
* When rendered, provides atom values for top-level tables and sub-tables
@@ -141,6 +142,7 @@ export const TableSourceFirestore = memo(function TableSourceFirestore() {
}
);
useApplySorts();
useAuditChange();
useBulkWriteDb();

View File

@@ -0,0 +1,40 @@
import { useEffect, useState } from "react";
import { useAtom, useSetAtom } from "jotai";
import { projectScope, userSettingsAtom } from "@src/atoms/projectScope";
import {
tableIdAtom,
tableSchemaAtom,
tableScope,
tableSortsAtom,
} from "@src/atoms/tableScope";
/**
* Sets the value of tableSortsAtom
*/
export default function useApplySorts() {
// Apply the sorts
const setTableSorts = useSetAtom(tableSortsAtom, tableScope);
const [userSettings] = useAtom(userSettingsAtom, projectScope);
const [tableId] = useAtom(tableIdAtom, tableScope);
const [tableSchema] = useAtom(tableSchemaAtom, tableScope);
// Apply only once
const [applySort, setApplySort] = useState(true);
useEffect(() => {
if (applySort && Object.keys(tableSchema).length) {
console.log("useApplySorts");
const userDefaultSort = userSettings.tables?.[tableId]?.sorts || [];
console.log({
userDefaultSort,
tableSchemaSorts: tableSchema.sorts,
});
setTableSorts(
userDefaultSort.length ? userDefaultSort : tableSchema.sorts || []
);
setApplySort(false);
}
}, [setTableSorts, userSettings, tableId, applySort, tableSchema]);
}

View File

@@ -101,6 +101,7 @@ export type TableSchema = {
rowHeight?: number;
filters?: TableFilter[];
filtersOverridable?: boolean;
sorts?: TableSort[];
functionConfigPath?: string;
functionBuilderRef?: any;