fix: textarea for pinning modal. small refactor to the api exposed by the hook
This commit is contained in:
parent
415d9d14a6
commit
4d271ec421
|
@ -4,6 +4,7 @@ import { type FieldName, useInputControl } from "@conform-to/react"
|
||||||
import { useId } from "react"
|
import { useId } from "react"
|
||||||
import { cn } from "~/utils"
|
import { cn } from "~/utils"
|
||||||
import { Checkbox } from "~/components/ui/checkbox"
|
import { Checkbox } from "~/components/ui/checkbox"
|
||||||
|
import { Textarea } from "./ui/textarea"
|
||||||
|
|
||||||
export const Field = ({
|
export const Field = ({
|
||||||
inputProps,
|
inputProps,
|
||||||
|
@ -98,6 +99,36 @@ export const FieldCheckbox = ({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function TextareaField({
|
||||||
|
labelProps,
|
||||||
|
textareaProps,
|
||||||
|
errors,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
labelProps: React.LabelHTMLAttributes<HTMLLabelElement>
|
||||||
|
textareaProps: React.TextareaHTMLAttributes<HTMLTextAreaElement>
|
||||||
|
errors?: ListOfErrors
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const fallbackId = useId()
|
||||||
|
const id = textareaProps.id ?? textareaProps.name ?? fallbackId
|
||||||
|
const errorId = errors?.length ? `${id}-error` : undefined
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<Label htmlFor={id} {...labelProps} />
|
||||||
|
<Textarea
|
||||||
|
id={id}
|
||||||
|
aria-invalid={errorId ? true : undefined}
|
||||||
|
aria-describedby={errorId}
|
||||||
|
{...textareaProps}
|
||||||
|
/>
|
||||||
|
<div className="min-h-[32px] pb-1 pt-1">
|
||||||
|
{errorId ? <ErrorList id={errorId} errors={errors} /> : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export type ListOfErrors = Array<string | null | undefined> | null | undefined
|
export type ListOfErrors = Array<string | null | undefined> | null | undefined
|
||||||
export function ErrorList({
|
export function ErrorList({
|
||||||
id,
|
id,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useContext, useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionContent,
|
AccordionContent,
|
||||||
|
@ -11,13 +11,10 @@ import { Tabs, TabsTrigger, TabsList, TabsContent } from "./ui/tabs";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { Cross2Icon } from "@radix-ui/react-icons";
|
import { Cross2Icon } from "@radix-ui/react-icons";
|
||||||
import type { PinningStatus } from "~/data/pinning";
|
import type { PinningStatus } from "~/data/pinning";
|
||||||
import { PinningContext } from "~/providers/PinningProvider";
|
|
||||||
|
|
||||||
export const PinningNetworkBanner = () => {
|
export const PinningNetworkBanner = () => {
|
||||||
// const context = useContext(PinningContext);
|
const { progressData: data } = usePinning();
|
||||||
const { data } = usePinning();
|
|
||||||
|
|
||||||
// TODO: Adapt to real API
|
|
||||||
const itemsLeft = useMemo(
|
const itemsLeft = useMemo(
|
||||||
() =>
|
() =>
|
||||||
data?.items.filter((item: PinningStatus) =>
|
data?.items.filter((item: PinningStatus) =>
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "~/utils"
|
||||||
|
|
||||||
|
export interface TextareaProps
|
||||||
|
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||||
|
|
||||||
|
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||||
|
({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
className={cn(
|
||||||
|
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Textarea.displayName = "Textarea"
|
||||||
|
|
||||||
|
export { Textarea }
|
|
@ -1,4 +1,4 @@
|
||||||
import { useNotification } from "@refinedev/core";
|
import { useInvalidate, useNotification } from "@refinedev/core";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useCallback, useContext } from "react";
|
import { useCallback, useContext } from "react";
|
||||||
import { PinningProcess } from "~/data/pinning";
|
import { PinningProcess } from "~/data/pinning";
|
||||||
|
@ -8,8 +8,9 @@ export const usePinning = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const context = useContext(PinningContext);
|
const context = useContext(PinningContext);
|
||||||
const { open } = useNotification();
|
const { open } = useNotification();
|
||||||
|
const invalidate = useInvalidate();
|
||||||
|
|
||||||
const { mutate: pinMutation } = useMutation({
|
const { status: pinStatus, data: pinData, mutate: pinMutation } = useMutation({
|
||||||
mutationKey: ["pin-mutation"],
|
mutationKey: ["pin-mutation"],
|
||||||
mutationFn: async (variables: { cid: string }) => {
|
mutationFn: async (variables: { cid: string }) => {
|
||||||
const { cid } = variables;
|
const { cid } = variables;
|
||||||
|
@ -23,11 +24,12 @@ export const usePinning = () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["pin-progress"] });
|
queryClient.invalidateQueries({ queryKey: ["pin-progress", "file"] });
|
||||||
|
invalidate({ resource: "files", invalidates: ["list"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutate: unpinMutation } = useMutation({
|
const { status: unpinStatus, data: unpinData, mutate: unpinMutation } = useMutation({
|
||||||
mutationKey: ["unpin-mutation"],
|
mutationKey: ["unpin-mutation"],
|
||||||
mutationFn: async (variables: { cid: string }) => {
|
mutationFn: async (variables: { cid: string }) => {
|
||||||
const { cid } = variables;
|
const { cid } = variables;
|
||||||
|
@ -41,6 +43,7 @@ export const usePinning = () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
queryClient.invalidateQueries({ queryKey: ["pin-progress"] });
|
queryClient.invalidateQueries({ queryKey: ["pin-progress"] });
|
||||||
|
invalidate({ resource: "files", invalidates: ["list"] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -54,7 +57,12 @@ export const usePinning = () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...context.query,
|
progressStatus: context.query.status,
|
||||||
|
progressData: context.query.data,
|
||||||
|
pinStatus,
|
||||||
|
pinData,
|
||||||
|
unpinStatus,
|
||||||
|
unpinData,
|
||||||
pin: pinMutation,
|
pin: pinMutation,
|
||||||
unpin: unpinMutation,
|
unpin: unpinMutation,
|
||||||
bulkPin,
|
bulkPin,
|
||||||
|
|
|
@ -13,18 +13,22 @@ import {
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "~/components/ui/dialog";
|
} from "~/components/ui/dialog";
|
||||||
import { Field } from "~/components/forms";
|
import { TextareaField } from "~/components/forms";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { getFormProps, useForm } from "@conform-to/react";
|
import { getFormProps, useForm } from "@conform-to/react";
|
||||||
import { getZodConstraint, parseWithZod } from "@conform-to/zod";
|
import { getZodConstraint, parseWithZod } from "@conform-to/zod";
|
||||||
import { usePinning } from "~/hooks/usePinning";
|
import { usePinning } from "~/hooks/usePinning";
|
||||||
import {CID} from "@lumeweb/libs5";
|
import { CID } from "@lumeweb/libs5";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export default function FileManager() {
|
export default function FileManager() {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const closeModal = () => setOpen(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Authenticated key="file-manager">
|
<Authenticated key="file-manager">
|
||||||
<GeneralLayout>
|
<GeneralLayout>
|
||||||
<Dialog>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<h1 className="font-bold mb-4 text-lg">File Manager</h1>
|
<h1 className="font-bold mb-4 text-lg">File Manager</h1>
|
||||||
<FileCardList>
|
<FileCardList>
|
||||||
<FileCard
|
<FileCard
|
||||||
|
@ -77,7 +81,7 @@ export default function FileManager() {
|
||||||
dataProviderName="files"
|
dataProviderName="files"
|
||||||
/>
|
/>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<PinFilesForm />
|
<PinFilesForm close={closeModal} />
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</GeneralLayout>
|
</GeneralLayout>
|
||||||
|
@ -86,23 +90,29 @@ export default function FileManager() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const PinFilesSchema = z.object({
|
const PinFilesSchema = z.object({
|
||||||
cids: z.string().transform((value) => value.split(",")).refine((value) => {
|
cids: z
|
||||||
|
.string()
|
||||||
|
.transform((value) => value.split(","))
|
||||||
|
.refine(
|
||||||
|
(value) => {
|
||||||
return value.every((cid) => {
|
return value.every((cid) => {
|
||||||
try {
|
try {
|
||||||
CID.decode(cid)
|
CID.decode(cid);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true;
|
||||||
});
|
});
|
||||||
},(val) => ({
|
},
|
||||||
|
(val) => ({
|
||||||
message: `${val} is not a valid CID`,
|
message: `${val} is not a valid CID`,
|
||||||
})),
|
}),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const PinFilesForm = () => {
|
const PinFilesForm = ({ close }: { close: () => void }) => {
|
||||||
const { bulkPin } = usePinning();
|
const { bulkPin, pinStatus } = usePinning();
|
||||||
const [form, fields] = useForm({
|
const [form, fields] = useForm({
|
||||||
id: "pin-files",
|
id: "pin-files",
|
||||||
constraint: getZodConstraint(PinFilesSchema),
|
constraint: getZodConstraint(PinFilesSchema),
|
||||||
|
@ -120,14 +130,20 @@ const PinFilesForm = () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (pinStatus === "success") {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}, [pinStatus, close]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Pin Content</DialogTitle>
|
<DialogTitle>Pin Content</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<form {...getFormProps(form)} className="w-full flex flex-col gap-y-4">
|
<form {...getFormProps(form)} className="w-full flex flex-col gap-y-4">
|
||||||
<Field
|
<TextareaField
|
||||||
inputProps={{
|
textareaProps={{
|
||||||
name: fields.cids.name,
|
name: fields.cids.name,
|
||||||
placeholder: "Comma separated CIDs",
|
placeholder: "Comma separated CIDs",
|
||||||
}}
|
}}
|
||||||
|
|
Loading…
Reference in New Issue