mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-29 00:16:39 +01:00
@@ -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,
|
||||
|
||||
@@ -233,6 +233,7 @@ export const ColumnHeader = memo(function ColumnHeader({
|
||||
sortKey={sortKey}
|
||||
currentSort={currentSort}
|
||||
tabIndex={focusInsideCell ? 0 : -1}
|
||||
canEditColumns={canEditColumns}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
98
src/components/Table/ColumnHeader/useSaveTableSorts.tsx
Normal file
98
src/components/Table/ColumnHeader/useSaveTableSorts.tsx
Normal 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;
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -112,7 +112,9 @@ export default function Filters() {
|
||||
|
||||
setLocalFilters(filtersToApply);
|
||||
// Reset order so we don’t have to make a new index
|
||||
setTableSorts([]);
|
||||
if (filtersToApply.length) {
|
||||
setTableSorts([]);
|
||||
}
|
||||
}, [
|
||||
hasTableFilters,
|
||||
hasUserFilters,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
40
src/sources/TableSourceFirestore/useApplySorts.ts
Normal file
40
src/sources/TableSourceFirestore/useApplySorts.ts
Normal 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]);
|
||||
}
|
||||
1
src/types/table.d.ts
vendored
1
src/types/table.d.ts
vendored
@@ -101,6 +101,7 @@ export type TableSchema = {
|
||||
rowHeight?: number;
|
||||
filters?: TableFilter[];
|
||||
filtersOverridable?: boolean;
|
||||
sorts?: TableSort[];
|
||||
|
||||
functionConfigPath?: string;
|
||||
functionBuilderRef?: any;
|
||||
|
||||
Reference in New Issue
Block a user