anyone-oslo/pages

View on GitHub
app/javascript/components/ImageGrid/Grid.tsx

Summary

Maintainability
C
1 day
Test Coverage
import { useRef } from "react";
import FileUploadButton from "../FileUploadButton";
import Deleted from "./Deleted";
import DragElement from "./DragElement";
import FilePlaceholder from "./FilePlaceholder";
import GridImage from "./GridImage";
import useToastStore from "../../stores/useToastStore";
import { post } from "../../lib/request";
import * as Drag from "../../types/Drag";
import * as Images from "../../types/Images";
 
import { createDraggable, collectionOrder, useDragUploader } from "../drag";
 
type Props = Images.GridOptions & {
state: Images.GridState;
};
 
function filterFiles(files: File[]): File[] {
const validMimeTypes = [
"image/gif",
"image/jpeg",
"image/pjpeg",
"image/png",
"image/tiff"
];
return files.filter((f) => validMimeTypes.indexOf(f.type) !== -1);
}
 
function draggedImageOrder(
primaryCollection: Drag.Collection<Images.Record>,
imagesCollection: Drag.Collection<Images.Record>,
dragState: Drag.State<Images.Record>
): [
Drag.DraggableOrFiles<Images.Record>,
Drag.DraggableOrFiles<Images.Record>[]
] {
const [primary, ...rest] = collectionOrder(primaryCollection, dragState);
let images = [...rest, ...collectionOrder(imagesCollection, dragState)];
 
if (
dragState.dragging &&
[primary, ...images].indexOf(dragState.dragging) === -1
) {
if (
dragState.y < imagesCollection.ref.current.getBoundingClientRect().top
) {
images = [dragState.dragging, ...images];
} else {
images.push(dragState.dragging);
}
}
 
return [primary, images];
}
 
Function `Grid` has 189 lines of code (exceeds 25 allowed). Consider refactoring.
Function `Grid` has a Cognitive Complexity of 16 (exceeds 5 allowed). Consider refactoring.
export default function Grid(props: Props) {
const { primary, images, deleted, setDeleted } = props.state;
 
const containerRef = useRef();
const error = useToastStore((state) => state.error);
 
const dispatchAll = (action) => {
primary.dispatch(action);
images.dispatch(action);
};
 
const dragEnd = (dragState: Drag.State<Images.Record>, files: File[]) => {
const [draggedPrimary, draggedImages] = draggedImageOrder(
primary,
images,
dragState
);
 
primary.dispatch({
type: "reorder",
payload: draggedPrimary ? [draggedPrimary] : []
});
images.dispatch({ type: "reorder", payload: draggedImages });
 
if (files) {
const uploads = filterFiles(files).map((f) => uploadImage(f));
dispatchAll({ type: "insertFiles", payload: uploads });
}
};
 
const [dragState, dragStart, listeners] = useDragUploader(
[primary, images],
dragEnd
);
 
const position = (record: Images.Record) => {
return (
[
...primary.draggables.map(
(d: Drag.Draggable<Images.Record>) => d.record
),
...images.draggables.map(
(d: Drag.Draggable<Images.Record>) => d.record
),
...deleted
].indexOf(record) + 1
);
};
 
Similar blocks of code found in 2 locations. Consider refactoring.
const attrName = (record: Images.Record) => {
return `${props.attribute}[${position(record)}]`;
};
 
const uploadImage = (file: File) => {
const draggable = createDraggable({ image: null, file: file });
 
const data = new FormData();
 
data.append("image[file]", file);
 
void post("/admin/images.json", data).then((json: Images.Response) => {
if ("status" in json && json.status === "error") {
error(`Error uploading image: ${json.error}`);
dispatchAll({ type: "remove", payload: draggable });
} else {
dispatchAll({
type: "update",
payload: {
...draggable,
record: { image: json }
} as Drag.Draggable<Images.Record>
});
}
});
 
return draggable;
};
 
const update =
(draggable: Drag.Draggable<Images.Record>) => (image: Images.Resource) => {
const { record } = draggable;
const updated = {
...draggable,
record: {
...record,
image: { ...record.image, ...image }
}
};
dispatchAll({ type: "update", payload: updated });
};
 
const remove = (draggable: Drag.Draggable<Images.Record>) => () => {
dispatchAll({ type: "remove", payload: draggable });
if (draggable.record.id) {
setDeleted([...deleted, draggable.record]);
}
};
 
const renderImage = (
draggable: Drag.DraggableOrFiles<Images.Record>,
isPrimary: boolean
) => {
const { dragging } = dragState;
 
if (draggable === "Files") {
return <FilePlaceholder key="placeholder" />;
}
 
return (
<GridImage
key={draggable.handle}
draggable={draggable}
locale={props.locale}
locales={props.locales}
showEmbed={props.showEmbed}
startDrag={dragStart}
position={position(draggable.record)}
primary={isPrimary}
onUpdate={update(draggable)}
enablePrimary={props.enablePrimary}
deleteImage={remove(draggable)}
attributeName={attrName(draggable.record)}
placeholder={dragging && dragging == draggable}
/>
);
};
 
const uploadPrimary = (files: File[]) => {
const [first, ...rest] = filterFiles(files).map((f) => uploadImage(f));
if (first) {
images.dispatch({
type: "prepend",
payload: [...primary.draggables, ...rest]
});
primary.dispatch({ type: "replace", payload: [first] });
}
};
 
const uploadAdditional = (files: File[]) => {
images.dispatch({
type: "append",
payload: filterFiles(files).map((f) => uploadImage(f))
});
};
 
const classNames = ["image-grid"];
if (props.enablePrimary) {
classNames.push("with-primary-image");
}
 
const [draggedPrimary, draggedImages] = draggedImageOrder(
primary,
images,
dragState
);
 
return (
<div className={classNames.join(" ")} ref={containerRef} {...listeners}>
{dragState.dragging && (
<DragElement
draggable={dragState.dragging}
dragState={dragState}
container={containerRef}
/>
)}
{props.enablePrimary && (
<div className="primary-image" ref={primary.ref}>
<h3>Main image</h3>
{draggedPrimary && (
<>
{renderImage(draggedPrimary, true)}
{props.primaryAttribute && (
<input
type="hidden"
name={props.primaryAttribute}
value={
(draggedPrimary !== "Files" &&
draggedPrimary.record.image &&
draggedPrimary.record.image.id) ||
""
}
/>
)}
</>
)}
{!draggedPrimary && (
<div className="drop-target">
<FileUploadButton
multiple={true}
type="image"
multiline={true}
callback={uploadPrimary}
/>
</div>
)}
</div>
)}
<div className="grid" ref={images.ref}>
<h3>{props.enablePrimary ? "More images" : "Images"}</h3>
<div className="drop-target">
<FileUploadButton
multiple={true}
type="image"
callback={uploadAdditional}
/>
</div>
<div className="images">
{draggedImages.map((r) => renderImage(r, false))}
</div>
</div>
<Deleted attributeName={attrName} deleted={deleted} />
</div>
);
}