From 3d7244e1edc408f7061a372ff8a28fed7fb3d91d Mon Sep 17 00:00:00 2001 From: eric Date: Thu, 7 Sep 2023 19:42:00 -0400 Subject: [PATCH 1/5] Remove generic assertion error; use localhost in dev seed data --- core/prisma/exampleCommunitySeeds/unjournal.ts | 8 ++++++-- packages/utils/src/assert.ts | 10 ++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/core/prisma/exampleCommunitySeeds/unjournal.ts b/core/prisma/exampleCommunitySeeds/unjournal.ts index 075ee9fc4f..da09e70748 100644 --- a/core/prisma/exampleCommunitySeeds/unjournal.ts +++ b/core/prisma/exampleCommunitySeeds/unjournal.ts @@ -492,17 +492,21 @@ export default async function main(prisma: PrismaClient, communityUUID: string) }, }); + const submissionsIntegrationUrl = + process.env.NODE_ENV === "production" + ? "https://integration-submissions.onrender.com" + : "http://localhost:3002"; const submissionsIntegration = await prisma.integration.create({ data: { name: "Submission Manager", actions: [ { text: "Submit Pub", - href: "https://integration-submissions.onrender.com/actions/submit", + href: `${submissionsIntegrationUrl}/actions/submit`, kind: "stage", }, ], - settingsUrl: "https://integration-submissions.onrender.com/configure", + settingsUrl: `${submissionsIntegrationUrl}/configure`, }, }); diff --git a/packages/utils/src/assert.ts b/packages/utils/src/assert.ts index 38f4e819bf..af5b4d5b98 100644 --- a/packages/utils/src/assert.ts +++ b/packages/utils/src/assert.ts @@ -1,14 +1,8 @@ -export class AssertionError extends Error { - readonly cause; - constructor(message: string, cause: T) { - super(message, { cause }); - this.cause = cause; - } -} +export class AssertionError extends Error {} export function assert(value: unknown, cause?: T): asserts value { if (value === false || value === null || value === undefined) { - throw new AssertionError("Assertion failed", cause); + throw new AssertionError("Assertion failed", { cause }); } } From 342625718c2d27aa24ac047c17c6eabdc80a0b82 Mon Sep 17 00:00:00 2001 From: eric Date: Thu, 7 Sep 2023 19:51:44 -0400 Subject: [PATCH 2/5] Use localhost for evaluations integration in dev --- core/prisma/exampleCommunitySeeds/unjournal.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/prisma/exampleCommunitySeeds/unjournal.ts b/core/prisma/exampleCommunitySeeds/unjournal.ts index da09e70748..68b0089ea9 100644 --- a/core/prisma/exampleCommunitySeeds/unjournal.ts +++ b/core/prisma/exampleCommunitySeeds/unjournal.ts @@ -510,16 +510,20 @@ export default async function main(prisma: PrismaClient, communityUUID: string) }, }); + const evaluationIntegrationUrl = + process.env.NODE_ENV === "production" + ? "https://integration-evaluations.onrender.com" + : "http://localhost:3001"; const evaluationIntegration = await prisma.integration.create({ data: { name: "Evaluation Manager", actions: [ { text: "Manage Evaluation", - href: "https://integration-evaluations.onrender.com/run", + href: `${evaluationIntegrationUrl}/actions/manage`, }, ], - settingsUrl: "https://integration-evaluations.onrender.com/configure", + settingsUrl: `${evaluationIntegrationUrl}/configure`, }, }); From ceb2bef830c2d291f4e1baa49ab5856229a04e69 Mon Sep 17 00:00:00 2001 From: eric Date: Thu, 7 Sep 2023 22:37:43 -0400 Subject: [PATCH 3/5] Add several shadcn components; improve submission integration config UI --- ...{tailwind.config.ts => tailwind.config.js} | 0 .../submissions/app/actions/submit/page.tsx | 11 +- .../submissions/app/configure/actions.ts | 8 +- .../submissions/app/configure/configure.tsx | 84 ++++++-- .../submissions/app/configure/page.tsx | 11 +- integrations/submissions/app/layout.tsx | 11 +- integrations/submissions/package.json | 5 +- ...{tailwind.config.ts => tailwind.config.js} | 0 packages/ui/package.json | 12 +- packages/ui/src/alert.tsx | 49 +++++ packages/ui/src/form.tsx | 169 ++++++++++++++++ packages/ui/src/icon.tsx | 7 + packages/ui/src/index.tsx | 8 + packages/ui/src/input.tsx | 24 +++ packages/ui/src/label.tsx | 21 ++ packages/ui/src/toast.tsx | 126 ++++++++++++ packages/ui/src/toaster.tsx | 33 +++ packages/ui/src/use-toast.tsx | 189 ++++++++++++++++++ pnpm-lock.yaml | 129 +++++++++++- 19 files changed, 860 insertions(+), 37 deletions(-) rename integrations/evaluations/{tailwind.config.ts => tailwind.config.js} (100%) rename integrations/submissions/{tailwind.config.ts => tailwind.config.js} (100%) create mode 100644 packages/ui/src/alert.tsx create mode 100644 packages/ui/src/form.tsx create mode 100644 packages/ui/src/icon.tsx create mode 100644 packages/ui/src/input.tsx create mode 100644 packages/ui/src/label.tsx create mode 100644 packages/ui/src/toast.tsx create mode 100644 packages/ui/src/toaster.tsx create mode 100644 packages/ui/src/use-toast.tsx diff --git a/integrations/evaluations/tailwind.config.ts b/integrations/evaluations/tailwind.config.js similarity index 100% rename from integrations/evaluations/tailwind.config.ts rename to integrations/evaluations/tailwind.config.js diff --git a/integrations/submissions/app/actions/submit/page.tsx b/integrations/submissions/app/actions/submit/page.tsx index 205e11a108..48e279d555 100644 --- a/integrations/submissions/app/actions/submit/page.tsx +++ b/integrations/submissions/app/actions/submit/page.tsx @@ -1,5 +1,6 @@ import { client } from "~/lib/pubpub"; import { Submit } from "./submit"; +import { Avatar, AvatarFallback, AvatarImage } from "ui"; type Props = { searchParams: { @@ -13,10 +14,12 @@ export default async function Page(props: Props) { const user = await client.auth(instanceId, token); return ( -
-

Hello {user.name}

- + <> + + + {user.name[0]} + -
+ ); } diff --git a/integrations/submissions/app/configure/actions.ts b/integrations/submissions/app/configure/actions.ts index 18f1e78349..06b403d062 100644 --- a/integrations/submissions/app/configure/actions.ts +++ b/integrations/submissions/app/configure/actions.ts @@ -1,14 +1,10 @@ "use server"; -import { assert } from "utils"; import { updateInstance } from "~/lib/instance"; -export const configure = (form: FormData) => { - const instanceId = form.get("instance-id"); - const pubTypeId = form.get("pub-type-id"); - assert(typeof instanceId === "string"); - assert(typeof pubTypeId === "string"); +export const configure = (instanceId: string, pubTypeId: string) => { try { + // return { error: "We couldn't update the instance." }; return updateInstance(instanceId, { pubTypeId }); } catch (error) { return { error: error.message }; diff --git a/integrations/submissions/app/configure/configure.tsx b/integrations/submissions/app/configure/configure.tsx index 39e916feee..30b0b9d8cc 100644 --- a/integrations/submissions/app/configure/configure.tsx +++ b/integrations/submissions/app/configure/configure.tsx @@ -1,6 +1,21 @@ "use client"; -import { useState, useTransition } from "react"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { + Button, + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, + Icon, + Input, + useToast, +} from "ui"; +import * as z from "zod"; import { configure } from "./actions"; type Props = { @@ -8,21 +23,66 @@ type Props = { pubTypeId?: string; }; +const schema = z.object({ + pubTypeId: z.string().length(36), + instanceId: z.string(), +}); + export function Configure(props: Props) { - const [message, setMessage] = useState(""); - const [isPending, startTransition] = useTransition(); + const { toast } = useToast(); + const form = useForm>({ + resolver: zodResolver(schema), + defaultValues: { + pubTypeId: props.pubTypeId, + instanceId: props.instanceId, + }, + }); - async function onConfigure(form: FormData) { - const response = await configure(form); - setMessage("error" in response ? response.error : "Instance configured!"); + async function onSubmit(values: z.infer) { + const result = await configure(values.instanceId, values.pubTypeId); + if ("error" in result) { + toast({ + title: "Error", + description: result.error, + variant: "destructive", + }); + } else { + toast({ + title: "Success", + description: "The instance was updated successfully.", + }); + } } return ( -
startTransition(() => onConfigure(form))}> - - - -

{isPending ? "Configuring instance..." : message}

-
+
+ + ( + + PubType Id + + + + Submitted pubs will be of this type. + + + )} + /> + + + + + + ); } diff --git a/integrations/submissions/app/configure/page.tsx b/integrations/submissions/app/configure/page.tsx index 29f44637b3..da4e1aa778 100644 --- a/integrations/submissions/app/configure/page.tsx +++ b/integrations/submissions/app/configure/page.tsx @@ -1,6 +1,7 @@ import { client } from "~/lib/pubpub"; import { findInstance } from "~/lib/instance"; import { Configure } from "./configure"; +import { Avatar, AvatarFallback, AvatarImage } from "ui"; type Props = { searchParams: { @@ -14,10 +15,12 @@ export default async function Page(props: Props) { const user = await client.auth(instanceId, token); const instance = await findInstance(instanceId); return ( -
-

Hello {user.name}

- + <> + + + {user.name[0]} + -
+ ); } diff --git a/integrations/submissions/app/layout.tsx b/integrations/submissions/app/layout.tsx index eda1917abd..9abf74fd6f 100644 --- a/integrations/submissions/app/layout.tsx +++ b/integrations/submissions/app/layout.tsx @@ -1,5 +1,7 @@ import "ui/styles.css"; import "./globals.css"; +import { Button, Toaster } from "ui"; +import { cn } from "utils"; export const metadata = { title: "PubPub Submissions Integration", @@ -9,9 +11,12 @@ export const metadata = { export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - -

Submissions

- {children} + +
+

Submissions

+ {children} +
+ ); diff --git a/integrations/submissions/package.json b/integrations/submissions/package.json index b8365263f1..bae6885df6 100644 --- a/integrations/submissions/package.json +++ b/integrations/submissions/package.json @@ -9,15 +9,18 @@ "lint": "next lint" }, "dependencies": { + "@hookform/resolvers": "^3.3.1", "@pubpub/sdk": "workspace:*", "clsx": "^2.0.0", "next": "13.4.5", "react": "18.2.0", "react-dom": "18.2.0", + "react-hook-form": "^7.46.1", "redis": "^4.6.8", "tailwind-merge": "^1.14.0", "ui": "workspace:*", - "utils": "workspace:*" + "utils": "workspace:*", + "zod": "^3.21.4" }, "devDependencies": { "@types/node": "20.3.1", diff --git a/integrations/submissions/tailwind.config.ts b/integrations/submissions/tailwind.config.js similarity index 100% rename from integrations/submissions/tailwind.config.ts rename to integrations/submissions/tailwind.config.js diff --git a/packages/ui/package.json b/packages/ui/package.json index 83015ae02e..63cae45897 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -16,27 +16,35 @@ "type-check": "tsc" }, "dependencies": { + "@hookform/resolvers": "^3.3.1", "@radix-ui/react-avatar": "^1.0.3", "@radix-ui/react-dropdown-menu": "^2.0.5", "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-popover": "^1.0.6", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-toast": "^1.1.4", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", + "lucide-react": "^0.274.0", "tailwind-merge": "^1.14.0", "tailwindcss-animate": "^1.0.6", "utils": "workspace:*" }, "peerDependencies": { "react": "^18.2.0", - "tailwindcss": "^3.3.3" + "react-hook-form": "^7.46.1", + "tailwindcss": "^3.3.3", + "zod": "^3.21.4" }, "devDependencies": { "@types/react": "^18.2.5", "postcss": "^8.4.20", "react": "^18.2.0", + "react-hook-form": "^7.46.1", "tsconfig": "workspace:*", "tsup": "^7.2.0", - "typescript": "^4.9.4" + "typescript": "^4.9.4", + "zod": "^3.21.4" } } diff --git a/packages/ui/src/alert.tsx b/packages/ui/src/alert.tsx new file mode 100644 index 0000000000..a146938af9 --- /dev/null +++ b/packages/ui/src/alert.tsx @@ -0,0 +1,49 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "utils"; + +const alertVariants = cva( + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +); + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)); +Alert.displayName = "Alert"; + +const AlertTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +); +AlertTitle.displayName = "AlertTitle"; + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +AlertDescription.displayName = "AlertDescription"; + +export { Alert, AlertTitle, AlertDescription }; diff --git a/packages/ui/src/form.tsx b/packages/ui/src/form.tsx new file mode 100644 index 0000000000..48b62fd5e7 --- /dev/null +++ b/packages/ui/src/form.tsx @@ -0,0 +1,169 @@ +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { Slot } from "@radix-ui/react-slot"; +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form"; + +import { cn } from "utils"; +import { Label } from "./label"; + +const Form = FormProvider; + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName; +}; + +const FormFieldContext = React.createContext({} as FormFieldContextValue); + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ); +}; + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState, formState } = useFormContext(); + + const fieldState = getFieldState(fieldContext.name, formState); + + if (!fieldContext) { + throw new Error("useFormField should be used within "); + } + + const { id } = itemContext; + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + }; +}; + +type FormItemContextValue = { + id: string; +}; + +const FormItemContext = React.createContext({} as FormItemContextValue); + +const FormItem = React.forwardRef>( + ({ className, ...props }, ref) => { + const id = React.useId(); + + return ( + +
+ + ); + } +); +FormItem.displayName = "FormItem"; + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField(); + + return ( +