Some file rearranging

This commit is contained in:
Donkie
2024-11-20 21:08:43 +01:00
parent 37b6d1fcee
commit ce42d687ed
6 changed files with 406 additions and 378 deletions

View File

@@ -0,0 +1,129 @@
import { Button, Input } from "antd";
import type { Identifier, XYCoord } from "dnd-core";
import { useRef, useState } from "react";
import { useDrag, useDrop } from "react-dnd";
import { DeleteOutlined } from "@ant-design/icons";
import { ISpool } from "../../spools/model";
import { ItemTypes } from "../itemTypes";
import { SpoolList } from "./spoolList";
interface DragItem {
index: number;
title: string;
}
export function Location({
index,
title,
spools,
showDelete,
onDelete,
moveLocation,
onEditTitle,
}: {
index: number;
title: string;
spools: ISpool[];
showDelete?: boolean;
onDelete?: () => void;
moveLocation: (dragIndex: number, hoverIndex: number) => void;
onEditTitle: (newTitle: string) => void;
}) {
const [editTitle, setEditTitle] = useState(false);
const [newTitle, setNewTitle] = useState(title);
const ref = useRef<HTMLDivElement>(null);
const [{ handlerId }, drop] = useDrop<DragItem, void, { handlerId: Identifier | null }>({
accept: ItemTypes.CONTAINER,
collect(monitor) {
return {
handlerId: monitor.getHandlerId(),
};
},
hover(item, monitor) {
if (!ref.current) {
return null;
}
const dragIndex = item.index;
const hoverIndex = index;
// Don't replace items with themselves
if (dragIndex === hoverIndex) {
return;
}
// Determine rectangle on screen
const hoverBoundingRect = ref.current?.getBoundingClientRect();
// Get horizontal middle
const hoverMiddleX = (hoverBoundingRect.right - hoverBoundingRect.left) / 2;
// Determine mouse position
const clientOffset = monitor.getClientOffset();
// Get pixels to the left
const hoverClientX = (clientOffset as XYCoord).x - hoverBoundingRect.left;
// Dragging downwards
if (dragIndex < hoverIndex && hoverClientX < hoverMiddleX) {
return;
}
// Dragging upwards
if (dragIndex > hoverIndex && hoverClientX > hoverMiddleX) {
return;
}
// Time to actually perform the action
moveLocation(dragIndex, hoverIndex);
item.index = hoverIndex;
},
});
const [{ isDragging }, drag] = useDrag({
type: ItemTypes.CONTAINER,
canDrag: !editTitle,
item: () => {
return { title, index };
},
collect: (monitor: any) => ({
isDragging: monitor.isDragging(),
}),
});
const opacity = isDragging ? 0 : 1;
drag(drop(ref));
return (
<div className="loc-container" ref={ref} style={{ opacity }} data-handler-id={handlerId}>
<h3>
{editTitle ? (
<Input
autoFocus
variant="borderless"
value={newTitle}
onBlur={() => setEditTitle(false)}
onChange={(e) => setNewTitle(e.target.value)}
onPressEnter={() => {
setEditTitle(false);
return onEditTitle(newTitle);
}}
/>
) : (
<span
onClick={() => {
setNewTitle(title);
setEditTitle(true);
}}
>
{title}
</span>
)}
{showDelete && <Button icon={<DeleteOutlined />} size="small" type="text" onClick={onDelete} />}
</h3>
<SpoolList location={title} spools={spools} />
</div>
);
}

View File

@@ -0,0 +1,153 @@
import { PlusOutlined } from "@ant-design/icons";
import { useInvalidate, useList, useTranslate } from "@refinedev/core";
import { Button } from "antd";
import { useEffect, useMemo } from "react";
import { useSetSetting } from "../../../utils/querySettings";
import { ISpool } from "../../spools/model";
import { renameSpoolLocation, useLocations } from "../functions";
import { Location } from "./location";
export function LocationContainer() {
const t = useTranslate();
const invalidate = useInvalidate();
const settingsLocations = useLocations();
const setLocationsSetting = useSetSetting<string[]>("locations");
const { data, isLoading, isError } = useList<ISpool>({
resource: "spool",
meta: {
queryParams: {
["allow_archived"]: false,
},
},
pagination: {
mode: "off",
},
});
// Grab spools and sort by ID
const spools = data?.data ?? [];
spools.sort((a, b) => a.id - b.id);
// Group spools by location
const spoolLocations = useMemo(() => {
const grouped: Record<string, ISpool[]> = {};
spools.forEach((spool) => {
const loc = spool.location ?? t("spool.no_location");
if (!grouped[loc]) {
grouped[loc] = [];
}
grouped[loc].push(spool);
});
return grouped;
}, [spools]);
// Create list of locations that's sorted
const locationsList = useMemo(() => {
// Start with the locations setting
let allLocs = settingsLocations;
console.log("settingsLocations", settingsLocations);
// Add any missing locations from the spools
for (const loc of Object.keys(spoolLocations)) {
if (!allLocs.includes(loc)) {
allLocs.push(loc);
}
}
return allLocs;
}, [spoolLocations, settingsLocations]);
const moveLocation = (dragIndex: number, hoverIndex: number) => {
const newLocs = [...locationsList];
newLocs.splice(dragIndex, 1);
newLocs.splice(hoverIndex, 0, locationsList[dragIndex]);
setLocationsSetting.mutate(newLocs);
};
const onEditTitle = async (location: string, newTitle: string) => {
if (newTitle == location) return;
if (newTitle == "") return;
if (locationsList.includes(newTitle)) return;
// Update all spool locations in the database
if (spoolLocations[location] && spoolLocations[location].length > 0) {
await renameSpoolLocation(location, newTitle);
await invalidate({ resource: "spool", invalidates: ["list", "detail"] });
}
// Update the value in the settings
const newLocs = [...locationsList];
newLocs[locationsList.indexOf(location)] = newTitle;
setLocationsSetting.mutate(newLocs);
};
// Create containers
const containers = locationsList.map((loc, idx) => {
const spools = spoolLocations[loc] ?? [];
return (
<Location
key={loc}
index={idx}
title={loc}
spools={spools}
showDelete={spools.length == 0}
onDelete={() => {
setLocationsSetting.mutate(locationsList.filter((l) => l !== loc));
}}
moveLocation={moveLocation}
onEditTitle={(newTitle: string) => onEditTitle(loc, newTitle)}
/>
);
});
// Update locations settings so it always includes all spool locations
useEffect(() => {
// Check if they're not the same
if (JSON.stringify(locationsList) !== JSON.stringify(settingsLocations)) {
console.log("Updating locations settings", locationsList, settingsLocations);
setLocationsSetting.mutate(locationsList);
}
}, [spoolLocations]);
if (isLoading) {
return <div>Loading...</div>;
}
if (isError) {
return <div>Failed to load spools</div>;
}
const addNewLocation = () => {
let newLocationName = "New Location";
const newLocs = [...locationsList];
let i = 1;
while (newLocs.includes(newLocationName)) {
newLocationName = "New Location " + i;
i++;
}
newLocs.push(newLocationName);
setLocationsSetting.mutate(newLocs);
};
return (
<div className="loc-metacontainer">
{containers}
<div className="newLocContainer">
<Button
type="dashed"
shape="circle"
icon={<PlusOutlined />}
size="large"
style={{
margin: "1em",
}}
onClick={addNewLocation}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,75 @@
import { useNavigation, useTranslate } from "@refinedev/core";
import { Button, theme } from "antd";
import { useDrag } from "react-dnd";
import { EditOutlined, EyeOutlined } from "@ant-design/icons";
import { Link } from "react-router-dom";
import SpoolIcon from "../../../components/spoolIcon";
import { ISpool } from "../../spools/model";
import { ItemTypes } from "../itemTypes";
const { useToken } = theme;
export function SpoolCard({ spool }: { spool: ISpool }) {
const { token } = useToken();
const t = useTranslate();
const [{ opacity }, dragRef] = useDrag(
() => ({
type: ItemTypes.SPOOL,
item: spool,
collect: (monitor) => ({
opacity: monitor.isDragging() ? 0.5 : 1,
}),
}),
[]
);
const { editUrl, showUrl } = useNavigation();
const colorObj = spool.filament.multi_color_hexes
? {
colors: spool.filament.multi_color_hexes.split(","),
vertical: spool.filament.multi_color_direction === "longitudinal",
}
: spool.filament.color_hex || "#000000";
let filament_name: string;
if (spool.filament.vendor && "name" in spool.filament.vendor) {
filament_name = `${spool.filament.vendor.name} - ${spool.filament.name}`;
} else {
filament_name = spool.filament.name ?? spool.filament.id.toString();
}
const style = {
opacity,
backgroundColor: token.colorBgContainerDisabled,
};
return (
<div className="spool" ref={dragRef} style={style}>
<SpoolIcon color={colorObj} />
<div className="info">
<div className="title">
<span>
#{spool.id} {filament_name}
</span>
<div>
<Link to={editUrl("spool", spool.id)}>
<Button icon={<EditOutlined />} title={t("buttons.edit")} size="small" type="text" />
</Link>
<Link to={showUrl("spool", spool.id)}>
<Button icon={<EyeOutlined />} title={t("buttons.show")} size="small" type="text" />
</Link>
</div>
</div>
<div
className="subtitle"
style={{
color: token.colorTextSecondary,
}}
>
{spool.remaining_weight} / {spool.filament.weight} g
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,40 @@
import { useInvalidate } from "@refinedev/core";
import { useDrop } from "react-dnd";
import { theme } from "antd";
import { ISpool } from "../../spools/model";
import { ItemTypes } from "../itemTypes";
import { setSpoolLocation } from "./../functions";
import { SpoolCard } from "./spoolCard";
const { useToken } = theme;
export function SpoolList({ location, spools }: { location: string; spools: ISpool[] }) {
const { token } = useToken();
const invalidate = useInvalidate();
const [, spoolDrop] = useDrop(() => ({
accept: ItemTypes.SPOOL,
drop: (item: ISpool) => {
setSpoolLocation(item.id, location).then(() => {
invalidate({
resource: "spool",
id: item.id,
invalidates: ["list", "detail"],
});
});
},
}));
const style = {
backgroundColor: token.colorBgContainer,
borderRadius: token.borderRadiusLG,
};
return (
<div className="loc-spools" ref={spoolDrop} style={style}>
{spools.map((spool) => (
<SpoolCard key={spool.id} spool={spool} />
))}
</div>
);
}

View File

@@ -1,392 +1,19 @@
import { IResourceComponentsProps, useInvalidate, useList, useNavigation, useTranslate } from "@refinedev/core";
import { Button, Input, theme } from "antd";
import { IResourceComponentsProps } from "@refinedev/core";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import type { Identifier, XYCoord } from "dnd-core";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { DndProvider, useDrag, useDrop } from "react-dnd";
import React from "react";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { DeleteOutlined, EditOutlined, EyeOutlined, PlusOutlined } from "@ant-design/icons";
import { Link } from "react-router-dom";
import SpoolIcon from "../../components/spoolIcon";
import { useSetSetting } from "../../utils/querySettings";
import { ISpool } from "../spools/model";
import { renameSpoolLocation, setSpoolLocation, useLocations } from "./functions";
import { LocationContainer } from "./components/locationContainer";
import "./locations.css";
dayjs.extend(utc);
const ItemTypes = {
SPOOL: "spool",
CONTAINER: "spool-container",
};
const { useToken } = theme;
function SpoolCard({ spool }: { spool: ISpool }) {
const { token } = useToken();
const t = useTranslate();
const [{ opacity }, dragRef] = useDrag(
() => ({
type: ItemTypes.SPOOL,
item: spool,
collect: (monitor) => ({
opacity: monitor.isDragging() ? 0.5 : 1,
}),
}),
[]
);
const { editUrl, showUrl } = useNavigation();
const colorObj = spool.filament.multi_color_hexes
? {
colors: spool.filament.multi_color_hexes.split(","),
vertical: spool.filament.multi_color_direction === "longitudinal",
}
: spool.filament.color_hex || "#000000";
let filament_name: string;
if (spool.filament.vendor && "name" in spool.filament.vendor) {
filament_name = `${spool.filament.vendor.name} - ${spool.filament.name}`;
} else {
filament_name = spool.filament.name ?? spool.filament.id.toString();
}
const style = {
opacity,
backgroundColor: token.colorBgContainerDisabled,
};
return (
<div className="spool" ref={dragRef} style={style}>
<SpoolIcon color={colorObj} />
<div className="info">
<div className="title">
<span>
#{spool.id} {filament_name}
</span>
<div>
<Link to={editUrl("spool", spool.id)}>
<Button icon={<EditOutlined />} title={t("buttons.edit")} size="small" type="text" />
</Link>
<Link to={showUrl("spool", spool.id)}>
<Button icon={<EyeOutlined />} title={t("buttons.show")} size="small" type="text" />
</Link>
</div>
</div>
<div
className="subtitle"
style={{
color: token.colorTextSecondary,
}}
>
{spool.remaining_weight} / {spool.filament.weight} g
</div>
</div>
</div>
);
}
function SpoolList({ location, spools }: { location: string; spools: ISpool[] }) {
const { token } = useToken();
const invalidate = useInvalidate();
const [, spoolDrop] = useDrop(() => ({
accept: ItemTypes.SPOOL,
drop: (item: ISpool) => {
setSpoolLocation(item.id, location).then(() => {
invalidate({
resource: "spool",
id: item.id,
invalidates: ["list", "detail"],
});
});
},
}));
const style = {
backgroundColor: token.colorBgContainer,
borderRadius: token.borderRadiusLG,
};
return (
<div className="loc-spools" ref={spoolDrop} style={style}>
{spools.map((spool) => (
<SpoolCard key={spool.id} spool={spool} />
))}
</div>
);
}
interface DragItem {
index: number;
title: string;
}
function LocationContainer({
index,
title,
spools,
showDelete,
onDelete,
moveLocation,
onEditTitle,
}: {
index: number;
title: string;
spools: ISpool[];
showDelete?: boolean;
onDelete?: () => void;
moveLocation: (dragIndex: number, hoverIndex: number) => void;
onEditTitle: (newTitle: string) => void;
}) {
const [editTitle, setEditTitle] = useState(false);
const [newTitle, setNewTitle] = useState(title);
const ref = useRef<HTMLDivElement>(null);
const [{ handlerId }, drop] = useDrop<DragItem, void, { handlerId: Identifier | null }>({
accept: ItemTypes.CONTAINER,
collect(monitor) {
return {
handlerId: monitor.getHandlerId(),
};
},
hover(item, monitor) {
if (!ref.current) {
return null;
}
const dragIndex = item.index;
const hoverIndex = index;
// Don't replace items with themselves
if (dragIndex === hoverIndex) {
return;
}
// Determine rectangle on screen
const hoverBoundingRect = ref.current?.getBoundingClientRect();
// Get horizontal middle
const hoverMiddleX = (hoverBoundingRect.right - hoverBoundingRect.left) / 2;
// Determine mouse position
const clientOffset = monitor.getClientOffset();
// Get pixels to the left
const hoverClientX = (clientOffset as XYCoord).x - hoverBoundingRect.left;
// Dragging downwards
if (dragIndex < hoverIndex && hoverClientX < hoverMiddleX) {
return;
}
// Dragging upwards
if (dragIndex > hoverIndex && hoverClientX > hoverMiddleX) {
return;
}
// Time to actually perform the action
moveLocation(dragIndex, hoverIndex);
item.index = hoverIndex;
},
});
const [{ isDragging }, drag] = useDrag({
type: ItemTypes.CONTAINER,
canDrag: !editTitle,
item: () => {
return { title, index };
},
collect: (monitor: any) => ({
isDragging: monitor.isDragging(),
}),
});
const opacity = isDragging ? 0 : 1;
drag(drop(ref));
return (
<div className="loc-container" ref={ref} style={{ opacity }} data-handler-id={handlerId}>
<h3>
{editTitle ? (
<Input
autoFocus
variant="borderless"
value={newTitle}
onBlur={() => setEditTitle(false)}
onChange={(e) => setNewTitle(e.target.value)}
onPressEnter={() => {
setEditTitle(false);
return onEditTitle(newTitle);
}}
/>
) : (
<span
onClick={() => {
setNewTitle(title);
setEditTitle(true);
}}
>
{title}
</span>
)}
{showDelete && <Button icon={<DeleteOutlined />} size="small" type="text" onClick={onDelete} />}
</h3>
<SpoolList location={title} spools={spools} />
</div>
);
}
function LocationMetaContainer() {
const t = useTranslate();
const invalidate = useInvalidate();
const settingsLocations = useLocations();
const setLocationsSetting = useSetSetting<string[]>("locations");
const { data, isLoading, isError } = useList<ISpool>({
resource: "spool",
meta: {
queryParams: {
["allow_archived"]: false,
},
},
pagination: {
mode: "off",
},
});
// Grab spools and sort by ID
const spools = data?.data ?? [];
spools.sort((a, b) => a.id - b.id);
// Group spools by location
const spoolLocations = useMemo(() => {
const grouped: Record<string, ISpool[]> = {};
spools.forEach((spool) => {
const loc = spool.location ?? t("spool.no_location");
if (!grouped[loc]) {
grouped[loc] = [];
}
grouped[loc].push(spool);
});
return grouped;
}, [spools]);
// Create list of locations that's sorted
const locationsList = useMemo(() => {
// Start with the locations setting
let allLocs = settingsLocations;
console.log("settingsLocations", settingsLocations);
// Add any missing locations from the spools
for (const loc of Object.keys(spoolLocations)) {
if (!allLocs.includes(loc)) {
allLocs.push(loc);
}
}
return allLocs;
}, [spoolLocations, settingsLocations]);
const moveLocation = (dragIndex: number, hoverIndex: number) => {
const newLocs = [...locationsList];
newLocs.splice(dragIndex, 1);
newLocs.splice(hoverIndex, 0, locationsList[dragIndex]);
setLocationsSetting.mutate(newLocs);
};
const onEditTitle = async (location: string, newTitle: string) => {
if (newTitle == location) return;
if (newTitle == "") return;
if (locationsList.includes(newTitle)) return;
// Update all spool locations in the database
if (spoolLocations[location] && spoolLocations[location].length > 0) {
await renameSpoolLocation(location, newTitle);
await invalidate({ resource: "spool", invalidates: ["list", "detail"] });
}
// Update the value in the settings
const newLocs = [...locationsList];
newLocs[locationsList.indexOf(location)] = newTitle;
setLocationsSetting.mutate(newLocs);
};
// Create containers
const containers = locationsList.map((loc, idx) => {
const spools = spoolLocations[loc] ?? [];
return (
<LocationContainer
key={loc}
index={idx}
title={loc}
spools={spools}
showDelete={spools.length == 0}
onDelete={() => {
setLocationsSetting.mutate(locationsList.filter((l) => l !== loc));
}}
moveLocation={moveLocation}
onEditTitle={(newTitle: string) => onEditTitle(loc, newTitle)}
/>
);
});
// Update locations settings so it always includes all spool locations
useEffect(() => {
// Check if they're not the same
if (JSON.stringify(locationsList) !== JSON.stringify(settingsLocations)) {
console.log("Updating locations settings", locationsList, settingsLocations);
setLocationsSetting.mutate(locationsList);
}
}, [spoolLocations]);
if (isLoading) {
return <div>Loading...</div>;
}
if (isError) {
return <div>Failed to load spools</div>;
}
const addNewLocation = () => {
let newLocationName = "New Location";
const newLocs = [...locationsList];
let i = 1;
while (newLocs.includes(newLocationName)) {
newLocationName = "New Location " + i;
i++;
}
newLocs.push(newLocationName);
setLocationsSetting.mutate(newLocs);
};
return (
<div className="loc-metacontainer">
{containers}
<div className="newLocContainer">
<Button
type="dashed"
shape="circle"
icon={<PlusOutlined />}
size="large"
style={{
margin: "1em",
}}
onClick={addNewLocation}
/>
</div>
</div>
);
}
export const Locations: React.FC<IResourceComponentsProps> = () => {
return (
<DndProvider backend={HTML5Backend}>
<LocationMetaContainer />
<LocationContainer />
</DndProvider>
);
};

View File

@@ -0,0 +1,4 @@
export const ItemTypes = {
SPOOL: "spool",
CONTAINER: "spool-container",
};