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

View File

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

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

View File

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