Improve image editing, allow null times
This commit is contained in:
parent
c483cf6dc6
commit
5a9f19484b
@ -1,6 +1,5 @@
|
|||||||
import CollapsibleFieldset from "@/ui/display/collapsible-fieldset";
|
import CollapsibleFieldset from "@/ui/display/collapsible-fieldset";
|
||||||
import { VerticalLogItem } from "@/ui/display/log-item";
|
import { VerticalLogItem } from "@/ui/display/log-item";
|
||||||
import SecureImage from "@/ui/display/secure-img";
|
|
||||||
import ErrorDisplay from "@/ui/error-display";
|
import ErrorDisplay from "@/ui/error-display";
|
||||||
import { useApi } from "@/util/api";
|
import { useApi } from "@/util/api";
|
||||||
import {
|
import {
|
||||||
@ -18,8 +17,7 @@ import {
|
|||||||
Modal,
|
Modal,
|
||||||
Button,
|
Button,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { Carousel } from "@mantine/carousel";
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
import { randomId, useDisclosure } from "@mantine/hooks";
|
|
||||||
import { useNavigate, useParams } from "@remix-run/react";
|
import { useNavigate, useParams } from "@remix-run/react";
|
||||||
import { IconPencil, IconTrash } from "@tabler/icons-react";
|
import { IconPencil, IconTrash } from "@tabler/icons-react";
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
@ -142,7 +140,7 @@ export default function Flight() {
|
|||||||
<CollapsibleFieldset legend="Images" mt="sm" w="100%">
|
<CollapsibleFieldset legend="Images" mt="sm" w="100%">
|
||||||
<ImageLogItem
|
<ImageLogItem
|
||||||
imageIds={imageIds}
|
imageIds={imageIds}
|
||||||
id={params.id}
|
id={params.id ?? ""}
|
||||||
mah="700px"
|
mah="700px"
|
||||||
/>
|
/>
|
||||||
</CollapsibleFieldset>
|
</CollapsibleFieldset>
|
||||||
|
@ -23,19 +23,21 @@ export default function NewFlight() {
|
|||||||
const imageForm = new FormData();
|
const imageForm = new FormData();
|
||||||
|
|
||||||
// Upload images
|
// Upload images
|
||||||
for (const img of values.images) {
|
if (values.images.length > 0) {
|
||||||
imageForm.append("images", img);
|
for (const img of values.images) {
|
||||||
}
|
imageForm.append("images", img);
|
||||||
|
}
|
||||||
|
|
||||||
const img_id = await client.post(
|
const img_id = await client.post(
|
||||||
`/flights/${id}/add_images`,
|
`/flights/${id}/add_images`,
|
||||||
imageForm,
|
imageForm,
|
||||||
{ headers: { "Content-Type": "multipart/form-data" } }
|
{ headers: { "Content-Type": "multipart/form-data" } }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!img_id) {
|
if (!img_id) {
|
||||||
await queryClient.invalidateQueries({ queryKey: ["flights-list"] });
|
await queryClient.invalidateQueries({ queryKey: ["flights-list"] });
|
||||||
throw new Error("Image upload failed");
|
throw new Error("Image upload failed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.data;
|
return res.data;
|
||||||
|
@ -13,9 +13,9 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { IconPencil } from "@tabler/icons-react";
|
import { IconPencil } from "@tabler/icons-react";
|
||||||
import ListInput from "@/ui/input/list-input";
|
import ImageListInput from "@/ui/input/image-list-input";
|
||||||
import ImageUpload from "@/ui/input/image-upload";
|
import ImageUpload from "@/ui/input/image-upload";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useApi } from "@/util/api";
|
import { useApi } from "@/util/api";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
@ -66,18 +66,23 @@ export default function ImageLogItem({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!img_id) {
|
if (!img_id) {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: [id] });
|
||||||
await queryClient.invalidateQueries({ queryKey: ["flights-list"] });
|
await queryClient.invalidateQueries({ queryKey: ["flights-list"] });
|
||||||
throw new Error("Image upload failed");
|
throw new Error("Image upload failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: async () => {
|
||||||
queryClient.invalidateQueries({ queryKey: [id] });
|
await queryClient.invalidateQueries({ queryKey: [id] });
|
||||||
queryClient.invalidateQueries({ queryKey: ["flights-list"] });
|
await queryClient.invalidateQueries({ queryKey: ["flights-list"] });
|
||||||
closeEdit();
|
closeEdit();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setExistingImages(imageIds);
|
||||||
|
}, [imageIds]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
@ -87,12 +92,6 @@ export default function ImageLogItem({
|
|||||||
centered
|
centered
|
||||||
>
|
>
|
||||||
<Stack>
|
<Stack>
|
||||||
<ListInput
|
|
||||||
label="Existing Images"
|
|
||||||
value={existingImages}
|
|
||||||
setValue={setExistingImages}
|
|
||||||
canAdd={false}
|
|
||||||
/>
|
|
||||||
<ImageUpload
|
<ImageUpload
|
||||||
value={newImages}
|
value={newImages}
|
||||||
setValue={setNewImages}
|
setValue={setNewImages}
|
||||||
@ -100,6 +99,13 @@ export default function ImageLogItem({
|
|||||||
mt="md"
|
mt="md"
|
||||||
placeholder="Images"
|
placeholder="Images"
|
||||||
/>
|
/>
|
||||||
|
<ImageListInput
|
||||||
|
label="Existing Images"
|
||||||
|
imageIds={existingImages}
|
||||||
|
setImageIds={setExistingImages}
|
||||||
|
collapsible
|
||||||
|
startCollapsed
|
||||||
|
/>
|
||||||
|
|
||||||
<Group justify="flex-end">
|
<Group justify="flex-end">
|
||||||
{updateValue.isPending ? <Loader /> : null}
|
{updateValue.isPending ? <Loader /> : null}
|
||||||
|
@ -35,6 +35,7 @@ import { useApi } from "@/util/api";
|
|||||||
import { useAircraft } from "@/util/hooks";
|
import { useAircraft } from "@/util/hooks";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import ImageUpload from "./image-upload";
|
import ImageUpload from "./image-upload";
|
||||||
|
import ImageListInput from "./image-list-input";
|
||||||
|
|
||||||
export default function FlightForm({
|
export default function FlightForm({
|
||||||
onSubmit,
|
onSubmit,
|
||||||
@ -59,7 +60,8 @@ export default function FlightForm({
|
|||||||
cancelFunc?: () => void;
|
cancelFunc?: () => void;
|
||||||
autofillHobbs?: boolean;
|
autofillHobbs?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const validate_time = (value) => {
|
const validate_time = (value: number | null) => {
|
||||||
|
if (value === null) return;
|
||||||
if (value > 2359) return "Time must be between 0000 and 2359";
|
if (value > 2359) return "Time must be between 0000 and 2359";
|
||||||
if (value % 100 > 59) return "Minutes must not exceed 59";
|
if (value % 100 > 59) return "Minutes must not exceed 59";
|
||||||
};
|
};
|
||||||
@ -163,6 +165,7 @@ export default function FlightForm({
|
|||||||
getHobbs.data.hobbs ?? form.getTransformedValues()["hobbs_start"]
|
getHobbs.data.hobbs ?? form.getTransformedValues()["hobbs_start"]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [getHobbs.data]);
|
}, [getHobbs.data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -332,7 +335,9 @@ export default function FlightForm({
|
|||||||
style={{
|
style={{
|
||||||
display:
|
display:
|
||||||
["", null].indexOf(
|
["", null].indexOf(
|
||||||
form.getTransformedValues()["hobbs_start"]
|
form.getTransformedValues()["hobbs_start"] as
|
||||||
|
| string
|
||||||
|
| null
|
||||||
) > -1
|
) > -1
|
||||||
? "none"
|
? "none"
|
||||||
: undefined,
|
: undefined,
|
||||||
@ -509,12 +514,12 @@ export default function FlightForm({
|
|||||||
{...form.getInputProps("comments")}
|
{...form.getInputProps("comments")}
|
||||||
/>
|
/>
|
||||||
{initialValues?.existing_images?.length ?? 0 > 0 ? (
|
{initialValues?.existing_images?.length ?? 0 > 0 ? (
|
||||||
<ListInput
|
<ImageListInput
|
||||||
form={form}
|
form={form}
|
||||||
field="existing_images"
|
field="existing_images"
|
||||||
mt="md"
|
mt="md"
|
||||||
label="Existing Images"
|
label="Existing Images"
|
||||||
canAdd={false}
|
// canAdd={false}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<ImageUpload
|
<ImageUpload
|
||||||
|
75
web/app/ui/form/image-list-input.tsx
Normal file
75
web/app/ui/form/image-list-input.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { Button, Card, Group, Text } from "@mantine/core";
|
||||||
|
import SecureImage from "../display/secure-img";
|
||||||
|
import { randomId } from "@mantine/hooks";
|
||||||
|
import { IconTrash } from "@tabler/icons-react";
|
||||||
|
import { UseFormReturnType } from "@mantine/form";
|
||||||
|
import { FlightFormSchema } from "@/util/types";
|
||||||
|
|
||||||
|
export default function ImageListInput({
|
||||||
|
label,
|
||||||
|
form,
|
||||||
|
field,
|
||||||
|
mt = "sm",
|
||||||
|
h = "100px",
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
form: UseFormReturnType<
|
||||||
|
FlightFormSchema,
|
||||||
|
(values: FlightFormSchema) => FlightFormSchema
|
||||||
|
>;
|
||||||
|
field: string;
|
||||||
|
mt?: string;
|
||||||
|
w?: string;
|
||||||
|
h?: string;
|
||||||
|
}) {
|
||||||
|
const field_key = field as keyof typeof form.getTransformedValues;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* <Grid> */}
|
||||||
|
<Text size="sm" fw={700} mt={mt} mb="xs">
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
<Group display="flex" gap="xs" style={{ flexWrap: "wrap" }}>
|
||||||
|
{(form.getTransformedValues()[field_key] as string[]).map((id) => (
|
||||||
|
// <Grid.Col key={randomId()}>
|
||||||
|
<Card key={randomId()} padding="md" shadow="md" withBorder>
|
||||||
|
{/* <Card.Section> */}
|
||||||
|
<SecureImage id={id} h={h} />
|
||||||
|
{/* </Card.Section> */}
|
||||||
|
<Button
|
||||||
|
mt="md"
|
||||||
|
leftSection={<IconTrash />}
|
||||||
|
onClick={() =>
|
||||||
|
form.setFieldValue(
|
||||||
|
field,
|
||||||
|
(form.getTransformedValues()[field_key] as string[]).filter(
|
||||||
|
(i) => i !== id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
// </Grid.Col>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
</>
|
||||||
|
// </Grid>
|
||||||
|
// <PillsInput label={label}>
|
||||||
|
// <Pill.Group>
|
||||||
|
// {imageIds.map((id: string) => (
|
||||||
|
// <Pill
|
||||||
|
// radius="sm"
|
||||||
|
// key={id}
|
||||||
|
// withRemoveButton
|
||||||
|
// onRemove={() => setImageIds(imageIds.filter((i) => i !== id))}
|
||||||
|
// >
|
||||||
|
// <SecureImage id={id} h="20px" />
|
||||||
|
// </Pill>
|
||||||
|
// ))}
|
||||||
|
// </Pill.Group>
|
||||||
|
// </PillsInput>
|
||||||
|
);
|
||||||
|
}
|
65
web/app/ui/input/image-list-input.tsx
Normal file
65
web/app/ui/input/image-list-input.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Collapse,
|
||||||
|
Group,
|
||||||
|
Text,
|
||||||
|
Tooltip,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { Dispatch, SetStateAction, useState } from "react";
|
||||||
|
import SecureImage from "../display/secure-img";
|
||||||
|
import { randomId } from "@mantine/hooks";
|
||||||
|
import { IconMinus, IconPlus, IconTrash } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
export default function ImageListInput({
|
||||||
|
label,
|
||||||
|
imageIds,
|
||||||
|
setImageIds,
|
||||||
|
collapsible = false,
|
||||||
|
startCollapsed = true,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
imageIds: string[];
|
||||||
|
setImageIds: Dispatch<SetStateAction<string[]>>;
|
||||||
|
collapsible?: boolean;
|
||||||
|
startCollapsed?: boolean;
|
||||||
|
}) {
|
||||||
|
const [collapsed, setCollapsed] = useState(startCollapsed);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Group gap="0">
|
||||||
|
<Text size="sm" fw={700} span>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{collapsible ? (
|
||||||
|
<Tooltip label={collapsed ? "Expand" : "Collapse"}>
|
||||||
|
<ActionIcon
|
||||||
|
variant="transparent"
|
||||||
|
onClick={() => setCollapsed(!collapsed)}
|
||||||
|
>
|
||||||
|
{collapsed ? <IconPlus size="1rem" /> : <IconMinus size="1rem" />}
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
) : null}
|
||||||
|
</Group>
|
||||||
|
<Collapse in={!collapsed}>
|
||||||
|
<Group variant="column">
|
||||||
|
{imageIds.map((id) => (
|
||||||
|
<Card key={randomId()} padding="md" shadow="md" withBorder>
|
||||||
|
<SecureImage id={id} />
|
||||||
|
<Button
|
||||||
|
mt="md"
|
||||||
|
leftSection={<IconTrash />}
|
||||||
|
onClick={() => setImageIds(imageIds.filter((i) => i !== id))}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
</Collapse>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -58,10 +58,10 @@ type FlightFormSchema = FlightBaseSchema & {
|
|||||||
type FlightCreateSchema = FlightBaseSchema & {
|
type FlightCreateSchema = FlightBaseSchema & {
|
||||||
date: string;
|
date: string;
|
||||||
|
|
||||||
time_start: string;
|
time_start: string | null;
|
||||||
time_off: string;
|
time_off: string | null;
|
||||||
time_down: string;
|
time_down: string | null;
|
||||||
time_stop: string;
|
time_stop: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type FlightDisplaySchema = FlightBaseSchema & {
|
type FlightDisplaySchema = FlightBaseSchema & {
|
||||||
@ -101,34 +101,42 @@ const flightCreateHelper = (
|
|||||||
date: date.utc().startOf("day").toISOString(),
|
date: date.utc().startOf("day").toISOString(),
|
||||||
hobbs_start: values.hobbs_start ? Number(values.hobbs_start) : null,
|
hobbs_start: values.hobbs_start ? Number(values.hobbs_start) : null,
|
||||||
hobbs_end: values.hobbs_end ? Number(values.hobbs_end) : null,
|
hobbs_end: values.hobbs_end ? Number(values.hobbs_end) : null,
|
||||||
time_start: date
|
time_start: values.time_start
|
||||||
.utc()
|
? date
|
||||||
.hour(Math.floor((values.time_start ?? 0) / 100))
|
.utc()
|
||||||
.minute(Math.floor((values.time_start ?? 0) % 100))
|
.hour(Math.floor((values.time_start ?? 0) / 100))
|
||||||
.second(0)
|
.minute(Math.floor((values.time_start ?? 0) % 100))
|
||||||
.millisecond(0)
|
.second(0)
|
||||||
.toISOString(),
|
.millisecond(0)
|
||||||
time_off: date
|
.toISOString()
|
||||||
.utc()
|
: null,
|
||||||
.hour(Math.floor((values.time_off ?? 0) / 100))
|
time_off: values.time_off
|
||||||
.minute(Math.floor((values.time_off ?? 0) % 100))
|
? date
|
||||||
.second(0)
|
.utc()
|
||||||
.millisecond(0)
|
.hour(Math.floor((values.time_off ?? 0) / 100))
|
||||||
.toISOString(),
|
.minute(Math.floor((values.time_off ?? 0) % 100))
|
||||||
time_down: date
|
.second(0)
|
||||||
.utc()
|
.millisecond(0)
|
||||||
.hour(Math.floor((values.time_down ?? 0) / 100))
|
.toISOString()
|
||||||
.minute(Math.floor((values.time_down ?? 0) % 100))
|
: null,
|
||||||
.second(0)
|
time_down: values.time_down
|
||||||
.millisecond(0)
|
? date
|
||||||
.toISOString(),
|
.utc()
|
||||||
time_stop: date
|
.hour(Math.floor((values.time_down ?? 0) / 100))
|
||||||
.utc()
|
.minute(Math.floor((values.time_down ?? 0) % 100))
|
||||||
.hour(Math.floor((values.time_stop ?? 0) / 100))
|
.second(0)
|
||||||
.minute(Math.floor((values.time_stop ?? 0) % 100))
|
.millisecond(0)
|
||||||
.second(0)
|
.toISOString()
|
||||||
.millisecond(0)
|
: null,
|
||||||
.toISOString(),
|
time_stop: values.time_stop
|
||||||
|
? date
|
||||||
|
.utc()
|
||||||
|
.hour(Math.floor((values.time_stop ?? 0) / 100))
|
||||||
|
.minute(Math.floor((values.time_stop ?? 0) % 100))
|
||||||
|
.second(0)
|
||||||
|
.millisecond(0)
|
||||||
|
.toISOString()
|
||||||
|
: null,
|
||||||
};
|
};
|
||||||
return newFlight;
|
return newFlight;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user