fix: textarea for pinning modal. small refactor to the api exposed by the hook

This commit is contained in:
Juan Di Toro 2024-03-25 08:59:35 +01:00
parent 415d9d14a6
commit 4d271ec421
5 changed files with 103 additions and 27 deletions

View File

@ -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,

View File

@ -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) =>

View File

@ -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 }

View File

@ -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,

View File

@ -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",
}} }}