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 { cn } from "~/utils"
|
||||
import { Checkbox } from "~/components/ui/checkbox"
|
||||
import { Textarea } from "./ui/textarea"
|
||||
|
||||
export const Field = ({
|
||||
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 function ErrorList({
|
||||
id,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useContext, useMemo } from "react";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
|
@ -11,13 +11,10 @@ import { Tabs, TabsTrigger, TabsList, TabsContent } from "./ui/tabs";
|
|||
import { Button } from "./ui/button";
|
||||
import { Cross2Icon } from "@radix-ui/react-icons";
|
||||
import type { PinningStatus } from "~/data/pinning";
|
||||
import { PinningContext } from "~/providers/PinningProvider";
|
||||
|
||||
export const PinningNetworkBanner = () => {
|
||||
// const context = useContext(PinningContext);
|
||||
const { data } = usePinning();
|
||||
const { progressData: data } = usePinning();
|
||||
|
||||
// TODO: Adapt to real API
|
||||
const itemsLeft = useMemo(
|
||||
() =>
|
||||
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 { useCallback, useContext } from "react";
|
||||
import { PinningProcess } from "~/data/pinning";
|
||||
|
@ -8,8 +8,9 @@ export const usePinning = () => {
|
|||
const queryClient = useQueryClient();
|
||||
const context = useContext(PinningContext);
|
||||
const { open } = useNotification();
|
||||
const invalidate = useInvalidate();
|
||||
|
||||
const { mutate: pinMutation } = useMutation({
|
||||
const { status: pinStatus, data: pinData, mutate: pinMutation } = useMutation({
|
||||
mutationKey: ["pin-mutation"],
|
||||
mutationFn: async (variables: { cid: string }) => {
|
||||
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"],
|
||||
mutationFn: async (variables: { cid: string }) => {
|
||||
const { cid } = variables;
|
||||
|
@ -41,6 +43,7 @@ export const usePinning = () => {
|
|||
});
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: ["pin-progress"] });
|
||||
invalidate({ resource: "files", invalidates: ["list"] });
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -54,7 +57,12 @@ export const usePinning = () => {
|
|||
);
|
||||
|
||||
return {
|
||||
...context.query,
|
||||
progressStatus: context.query.status,
|
||||
progressData: context.query.data,
|
||||
pinStatus,
|
||||
pinData,
|
||||
unpinStatus,
|
||||
unpinData,
|
||||
pin: pinMutation,
|
||||
unpin: unpinMutation,
|
||||
bulkPin,
|
||||
|
|
|
@ -13,18 +13,22 @@ import {
|
|||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "~/components/ui/dialog";
|
||||
import { Field } from "~/components/forms";
|
||||
import { TextareaField } from "~/components/forms";
|
||||
import { z } from "zod";
|
||||
import { getFormProps, useForm } from "@conform-to/react";
|
||||
import { getZodConstraint, parseWithZod } from "@conform-to/zod";
|
||||
import { usePinning } from "~/hooks/usePinning";
|
||||
import { CID } from "@lumeweb/libs5";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function FileManager() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const closeModal = () => setOpen(false);
|
||||
|
||||
return (
|
||||
<Authenticated key="file-manager">
|
||||
<GeneralLayout>
|
||||
<Dialog>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<h1 className="font-bold mb-4 text-lg">File Manager</h1>
|
||||
<FileCardList>
|
||||
<FileCard
|
||||
|
@ -77,7 +81,7 @@ export default function FileManager() {
|
|||
dataProviderName="files"
|
||||
/>
|
||||
<DialogContent>
|
||||
<PinFilesForm />
|
||||
<PinFilesForm close={closeModal} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</GeneralLayout>
|
||||
|
@ -86,23 +90,29 @@ export default function FileManager() {
|
|||
}
|
||||
|
||||
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) => {
|
||||
try {
|
||||
CID.decode(cid)
|
||||
CID.decode(cid);
|
||||
} catch (e) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
return true
|
||||
return true;
|
||||
});
|
||||
},(val) => ({
|
||||
},
|
||||
(val) => ({
|
||||
message: `${val} is not a valid CID`,
|
||||
})),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const PinFilesForm = () => {
|
||||
const { bulkPin } = usePinning();
|
||||
const PinFilesForm = ({ close }: { close: () => void }) => {
|
||||
const { bulkPin, pinStatus } = usePinning();
|
||||
const [form, fields] = useForm({
|
||||
id: "pin-files",
|
||||
constraint: getZodConstraint(PinFilesSchema),
|
||||
|
@ -120,14 +130,20 @@ const PinFilesForm = () => {
|
|||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (pinStatus === "success") {
|
||||
close();
|
||||
}
|
||||
}, [pinStatus, close]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Pin Content</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form {...getFormProps(form)} className="w-full flex flex-col gap-y-4">
|
||||
<Field
|
||||
inputProps={{
|
||||
<TextareaField
|
||||
textareaProps={{
|
||||
name: fields.cids.name,
|
||||
placeholder: "Comma separated CIDs",
|
||||
}}
|
||||
|
|
Loading…
Reference in New Issue