From 94e33c6f836f61219dc23818a3f100ff259e2047 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 29 Feb 2024 16:41:35 +0100 Subject: [PATCH 01/12] RFC/refactor: code-split ui package (#234) * refactor: split the code * 'chore: update @preconstruct/cli and typescript versions for compatibility * chore: update imports * fix: use correct styles.css imports * docs: add documentation on how to add components in ui package --- core/app/(user)/forgot/ForgotForm.tsx | 11 +- core/app/(user)/login/LoginForm.tsx | 4 +- core/app/(user)/reset/ResetForm.tsx | 5 +- core/app/(user)/settings/SettingsForm.tsx | 6 +- core/app/(user)/signup/SignupForm.tsx | 2 +- core/app/InitClient.tsx | 9 +- .../c/[communitySlug]/CommunitySwitcher.tsx | 6 +- core/app/c/[communitySlug]/LoginSwitcher.tsx | 7 +- .../integrations/IntegrationsList.tsx | 4 +- core/app/c/[communitySlug]/pubs/PubHeader.tsx | 2 +- .../c/[communitySlug]/pubs/[pubId]/page.tsx | 18 +- .../stages/components/Assign.tsx | 20 +- .../stages/components/Move.tsx | 4 +- .../stages/components/StageList.tsx | 2 +- .../stages/manage/StageEditor.tsx | 3 +- .../stages/manage/StageForm.tsx | 8 +- .../stages/manage/StageManagement.tsx | 2 +- .../app/c/[communitySlug]/types/TypeBlock.tsx | 3 +- core/app/components/IntegrationActions.tsx | 2 +- core/app/components/LogoutButton.tsx | 2 +- core/app/components/PubRow.tsx | 8 +- core/app/components/Row.tsx | 2 +- core/app/layout.tsx | 14 +- core/lib/supabase.ts | 3 +- core/scripts/invite.ts | 2 +- infrastructure/terraform/aws/README.md | 2 +- .../app/actions/evaluate/evaluate.tsx | 8 +- .../actions/manage/EvaluatorInviteForm.tsx | 24 +- .../EvaluatorInviteFormInviteButton.tsx | 6 +- .../manage/EvaluatorInviteFormSaveButton.tsx | 2 +- .../app/actions/manage/EvaluatorInviteRow.tsx | 7 +- .../manage/EvaluatorInviteRowEmailDialog.tsx | 18 +- .../actions/manage/EvaluatorSuggestButton.tsx | 5 +- .../manage/EvalutorInviteRowStatus.tsx | 2 +- .../app/actions/respond/respond.tsx | 3 +- .../evaluations/app/configure/configure.tsx | 28 +-- integrations/evaluations/app/layout.tsx | 2 +- .../evaluations/lib/components/Research.tsx | 2 +- .../actions/submit/FetchMetadataButton.tsx | 19 +- .../submissions/app/actions/submit/submit.tsx | 23 +- .../submissions/app/configure/configure.tsx | 20 +- integrations/submissions/app/layout.tsx | 2 +- package.json | 2 +- packages/sdk/src/react/Integration.tsx | 2 +- packages/sdk/src/react/IntegrationAvatar.tsx | 2 +- packages/sdk/src/react/generateFormFields.tsx | 21 +- packages/ui/README.md | 81 ++++--- packages/ui/alert/package.json | 4 + packages/ui/avatar/package.json | 4 + packages/ui/badge/package.json | 4 + packages/ui/button/package.json | 4 + packages/ui/calandar/package.json | 4 + packages/ui/card/package.json | 4 + packages/ui/checkbox/package.json | 4 + packages/ui/collapsible/package.json | 4 + .../confidence/confidence/package.json | 4 + .../fileUpload/fileUpload/package.json | 4 + packages/ui/dialog/package.json | 4 + packages/ui/dropdown-menu/package.json | 4 + packages/ui/form/package.json | 4 + packages/ui/hooks/package.json | 4 + packages/ui/hover-card/package.json | 4 + packages/ui/icon/package.json | 4 + packages/ui/input/package.json | 4 + packages/ui/label/package.json | 4 + packages/ui/package.json | 209 +++++++++++++++++- packages/ui/popover/package.json | 4 + packages/ui/select/package.json | 4 + packages/ui/separator/package.json | 4 + packages/ui/src/index.tsx | 34 --- packages/ui/tabs/package.json | 4 + packages/ui/textarea/package.json | 4 + packages/ui/toast/package.json | 4 + packages/ui/toaster/package.json | 4 + packages/ui/tooltip/package.json | 4 + packages/ui/use-toast/package.json | 4 + pnpm-lock.yaml | 41 ++-- 77 files changed, 537 insertions(+), 285 deletions(-) create mode 100644 packages/ui/alert/package.json create mode 100644 packages/ui/avatar/package.json create mode 100644 packages/ui/badge/package.json create mode 100644 packages/ui/button/package.json create mode 100644 packages/ui/calandar/package.json create mode 100644 packages/ui/card/package.json create mode 100644 packages/ui/checkbox/package.json create mode 100644 packages/ui/collapsible/package.json create mode 100644 packages/ui/customRenderers/confidence/confidence/package.json create mode 100644 packages/ui/customRenderers/fileUpload/fileUpload/package.json create mode 100644 packages/ui/dialog/package.json create mode 100644 packages/ui/dropdown-menu/package.json create mode 100644 packages/ui/form/package.json create mode 100644 packages/ui/hooks/package.json create mode 100644 packages/ui/hover-card/package.json create mode 100644 packages/ui/icon/package.json create mode 100644 packages/ui/input/package.json create mode 100644 packages/ui/label/package.json create mode 100644 packages/ui/popover/package.json create mode 100644 packages/ui/select/package.json create mode 100644 packages/ui/separator/package.json delete mode 100644 packages/ui/src/index.tsx create mode 100644 packages/ui/tabs/package.json create mode 100644 packages/ui/textarea/package.json create mode 100644 packages/ui/toast/package.json create mode 100644 packages/ui/toaster/package.json create mode 100644 packages/ui/tooltip/package.json create mode 100644 packages/ui/use-toast/package.json diff --git a/core/app/(user)/forgot/ForgotForm.tsx b/core/app/(user)/forgot/ForgotForm.tsx index d9653bf9c..0b926d41a 100644 --- a/core/app/(user)/forgot/ForgotForm.tsx +++ b/core/app/(user)/forgot/ForgotForm.tsx @@ -1,12 +1,13 @@ "use client"; import React, { FormEvent, useState } from "react"; -import { Button, Icon } from "ui"; +import { Button } from "ui/button"; +import { Loader2 } from "ui/icon"; + import { supabase } from "lib/supabase"; import { useEnvContext } from "next-runtime-env"; - export default function ForgotForm() { - const { NEXT_PUBLIC_PUBPUB_URL } = useEnvContext() + const { NEXT_PUBLIC_PUBPUB_URL } = useEnvContext(); const [email, setEmail] = useState(""); const [isLoading, setIsLoading] = useState(false); @@ -50,9 +51,7 @@ export default function ForgotForm() { {failure && ( diff --git a/core/app/(user)/login/LoginForm.tsx b/core/app/(user)/login/LoginForm.tsx index 07b3dfa71..4774b9248 100644 --- a/core/app/(user)/login/LoginForm.tsx +++ b/core/app/(user)/login/LoginForm.tsx @@ -1,6 +1,6 @@ "use client"; import React, { useState, FormEvent } from "react"; -import { Button } from "ui"; +import { Button } from "ui/button"; import { supabase } from "lib/supabase"; import Link from "next/link"; import { useRouter } from "next/navigation"; @@ -38,7 +38,7 @@ export default function LoginForm() { router.push("/settings"); } } - } + }; return (
diff --git a/core/app/(user)/reset/ResetForm.tsx b/core/app/(user)/reset/ResetForm.tsx index eec2f397f..3d20f3283 100644 --- a/core/app/(user)/reset/ResetForm.tsx +++ b/core/app/(user)/reset/ResetForm.tsx @@ -1,6 +1,7 @@ "use client"; import React, { useState, FormEvent } from "react"; -import { Button, Icon } from "ui"; +import { Button } from "ui/button"; +import { Loader2 } from "ui/icon"; import { formatSupabaseError, supabase } from "lib/supabase"; import { useRouter } from "next/navigation"; @@ -57,7 +58,7 @@ export default function ResetForm() { {error && ( diff --git a/core/app/(user)/settings/SettingsForm.tsx b/core/app/(user)/settings/SettingsForm.tsx index da40f7aaf..5af65b54b 100644 --- a/core/app/(user)/settings/SettingsForm.tsx +++ b/core/app/(user)/settings/SettingsForm.tsx @@ -4,7 +4,9 @@ import { supabase } from "lib/supabase"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { FormEvent, useState } from "react"; -import { Avatar, AvatarFallback, AvatarImage, Button, Icon } from "ui"; +import { Button } from "ui/button"; +import { Avatar, AvatarFallback, AvatarImage } from "ui/avatar"; +import { Loader2 } from "ui/icon"; import LogoutButton from "~/app/components/LogoutButton"; import { UserPutBody, UserSettings } from "~/lib/types"; import { useEnvContext } from "next-runtime-env"; @@ -162,7 +164,7 @@ export default function SettingsForm({ {!resetSuccess && ( )} {resetSuccess && ( diff --git a/core/app/(user)/signup/SignupForm.tsx b/core/app/(user)/signup/SignupForm.tsx index 0d6d6389f..9ade935c2 100644 --- a/core/app/(user)/signup/SignupForm.tsx +++ b/core/app/(user)/signup/SignupForm.tsx @@ -1,6 +1,6 @@ "use client"; import React, { useState, FormEvent } from "react"; -import { Button } from "ui"; +import { Button } from "ui/button"; import { UserPostBody } from "~/lib/types"; export default function SignupForm() { diff --git a/core/app/InitClient.tsx b/core/app/InitClient.tsx index e42f407a2..c126e8135 100644 --- a/core/app/InitClient.tsx +++ b/core/app/InitClient.tsx @@ -3,19 +3,16 @@ import { useEffect } from "react"; import { REFRESH_NAME, TOKEN_NAME } from "lib/auth/cookies"; import { createBrowserSupabase, supabase } from "lib/supabase"; import { usePathname } from "next/navigation"; -import { useEnvContext } from 'next-runtime-env'; +import { useEnvContext } from "next-runtime-env"; export default function InitClient() { - const { NEXT_PUBLIC_SUPABASE_PUBLIC_KEY , NEXT_PUBLIC_SUPABASE_URL } = useEnvContext(); + const { NEXT_PUBLIC_SUPABASE_PUBLIC_KEY, NEXT_PUBLIC_SUPABASE_URL } = useEnvContext(); const pathname = usePathname(); useEffect(() => { const isLocalhost = window.location.origin.includes("localhost"); const securityValue = isLocalhost ? "secure" : ""; - createBrowserSupabase( - NEXT_PUBLIC_SUPABASE_URL, - NEXT_PUBLIC_SUPABASE_PUBLIC_KEY - ); + createBrowserSupabase(NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_PUBLIC_KEY); supabase.auth.onAuthStateChange(async (event, session) => { if (event === "SIGNED_OUT") { // delete cookies on sign out diff --git a/core/app/c/[communitySlug]/CommunitySwitcher.tsx b/core/app/c/[communitySlug]/CommunitySwitcher.tsx index aa538d6c9..4f8d7eb75 100644 --- a/core/app/c/[communitySlug]/CommunitySwitcher.tsx +++ b/core/app/c/[communitySlug]/CommunitySwitcher.tsx @@ -1,13 +1,11 @@ import Link from "next/link"; +import { Avatar, AvatarFallback, AvatarImage } from "ui/avatar"; import { - Avatar, - AvatarFallback, - AvatarImage, DropdownMenu, DropdownMenuItem, DropdownMenuContent, DropdownMenuTrigger, -} from "ui"; +} from "ui/dropdown-menu"; import { CommunityData } from "./layout"; type Props = { diff --git a/core/app/c/[communitySlug]/LoginSwitcher.tsx b/core/app/c/[communitySlug]/LoginSwitcher.tsx index cf82d75a7..0853ebf2f 100644 --- a/core/app/c/[communitySlug]/LoginSwitcher.tsx +++ b/core/app/c/[communitySlug]/LoginSwitcher.tsx @@ -1,5 +1,6 @@ import { getLoginData } from "~/lib/auth/loginData"; -import { Avatar, AvatarFallback, AvatarImage, Button } from "ui"; +import { Avatar, AvatarFallback, AvatarImage } from "ui/avatar"; +import { Button } from "ui/button"; import LogoutButton from "../../components/LogoutButton"; import Link from "next/link"; @@ -27,7 +28,9 @@ export default async function LoginSwitcher() {
- +
diff --git a/core/app/c/[communitySlug]/integrations/IntegrationsList.tsx b/core/app/c/[communitySlug]/integrations/IntegrationsList.tsx index 58af08ad8..ab2945bfc 100644 --- a/core/app/c/[communitySlug]/integrations/IntegrationsList.tsx +++ b/core/app/c/[communitySlug]/integrations/IntegrationsList.tsx @@ -1,7 +1,9 @@ "use client"; import NextLink from "next/link"; -import { Button, Card, CardContent, CardHeader } from "ui"; + +import { Button } from "ui/button"; +import { Card, CardContent, CardHeader } from "ui/card"; import { IntegrationData } from "./page"; import { Row, RowContent, RowFooter } from "~/app/components/Row"; diff --git a/core/app/c/[communitySlug]/pubs/PubHeader.tsx b/core/app/c/[communitySlug]/pubs/PubHeader.tsx index 185423274..83e571c25 100644 --- a/core/app/c/[communitySlug]/pubs/PubHeader.tsx +++ b/core/app/c/[communitySlug]/pubs/PubHeader.tsx @@ -1,5 +1,5 @@ import Link from "next/link"; -import { Button } from "ui"; +import { Button } from "ui/button"; type Props = {}; diff --git a/core/app/c/[communitySlug]/pubs/[pubId]/page.tsx b/core/app/c/[communitySlug]/pubs/[pubId]/page.tsx index 0641e404f..670c781b2 100644 --- a/core/app/c/[communitySlug]/pubs/[pubId]/page.tsx +++ b/core/app/c/[communitySlug]/pubs/[pubId]/page.tsx @@ -1,19 +1,11 @@ import { Prisma, PubField, PubFieldSchema, PubValue } from "@prisma/client"; import { AnySchema, JSONSchemaType } from "ajv"; import Link from "next/link"; -import { - Avatar, - AvatarFallback, - AvatarImage, - Button, - CardContent, - CardHeader, - CardTitle, - HoverCard, - HoverCardContent, - HoverCardTrigger, - Separator, -} from "ui"; +import { Avatar, AvatarFallback, AvatarImage } from "ui/avatar"; +import { Button } from "ui/button"; +import { CardContent, CardHeader, CardTitle } from "ui/card"; +import { HoverCard, HoverCardContent, HoverCardTrigger } from "ui/hover-card"; +import { Separator } from "ui/separator"; import IntegrationActions from "~/app/components/IntegrationActions"; import { PubTitle } from "~/app/components/PubTitle"; diff --git a/core/app/c/[communitySlug]/stages/components/Assign.tsx b/core/app/c/[communitySlug]/stages/components/Assign.tsx index eaff45b1f..8ef30806d 100644 --- a/core/app/c/[communitySlug]/stages/components/Assign.tsx +++ b/core/app/c/[communitySlug]/stages/components/Assign.tsx @@ -1,20 +1,12 @@ "use client"; import Image from "next/image"; import React from "react"; -import { - Button, - Card, - CardContent, - CardFooter, - CardTitle, - Dialog, - DialogContent, - DialogTrigger, - Popover, - PopoverContent, - PopoverTrigger, - useToast, -} from "ui"; +import { Button } from "ui/button"; +import { Card, CardContent, CardFooter, CardTitle } from "ui/card"; +import { Dialog, DialogContent, DialogTrigger } from "ui/dialog"; +import { Popover, PopoverContent, PopoverTrigger } from "ui/popover"; +import { useToast } from "ui/use-toast"; + import { PermissionPayloadUser, PubPayload, diff --git a/core/app/c/[communitySlug]/stages/components/Move.tsx b/core/app/c/[communitySlug]/stages/components/Move.tsx index 1fa604a6d..7a43b3d0b 100644 --- a/core/app/c/[communitySlug]/stages/components/Move.tsx +++ b/core/app/c/[communitySlug]/stages/components/Move.tsx @@ -1,5 +1,7 @@ "use client"; -import { Button, Popover, PopoverContent, PopoverTrigger, useToast } from "ui"; +import { Button } from "ui/button"; +import { useToast } from "ui/use-toast"; +import { Popover, PopoverContent, PopoverTrigger } from "ui/popover"; import { PubPayload, StagePayload, StagePayloadMoveConstraintDestination } from "~/lib/types"; import { move } from "./lib/actions"; diff --git a/core/app/c/[communitySlug]/stages/components/StageList.tsx b/core/app/c/[communitySlug]/stages/components/StageList.tsx index 1204a231e..0dc39dcd0 100644 --- a/core/app/c/[communitySlug]/stages/components/StageList.tsx +++ b/core/app/c/[communitySlug]/stages/components/StageList.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import { Fragment } from "react"; -import { Button } from "ui"; +import { Button } from "ui/button"; import PubRow from "~/app/components/PubRow"; import { getPubUsers } from "~/lib/permissions"; import { StagesById, StagePayload, UserLoginData } from "~/lib/types"; diff --git a/core/app/c/[communitySlug]/stages/manage/StageEditor.tsx b/core/app/c/[communitySlug]/stages/manage/StageEditor.tsx index e0d666f11..48e709874 100644 --- a/core/app/c/[communitySlug]/stages/manage/StageEditor.tsx +++ b/core/app/c/[communitySlug]/stages/manage/StageEditor.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; -import { Tabs, TabsContent, TabsList, TabsTrigger, toast } from "ui"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "ui/tabs"; +import { toast } from "ui/use-toast"; import { StageFormSchema, moveConstraintSourcesForStage } from "~/lib/stages"; import { DeepPartial, StagesById, StagePayload } from "~/lib/types"; import StageForm from "./StageForm"; diff --git a/core/app/c/[communitySlug]/stages/manage/StageForm.tsx b/core/app/c/[communitySlug]/stages/manage/StageForm.tsx index 84d953f61..bc8b23e55 100644 --- a/core/app/c/[communitySlug]/stages/manage/StageForm.tsx +++ b/core/app/c/[communitySlug]/stages/manage/StageForm.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect } from "react"; import { useForm } from "react-hook-form"; +import { Button } from "ui/button"; import { - Button, Form, FormControl, FormDescription, @@ -9,9 +9,9 @@ import { FormItem, FormLabel, FormMessage, - Icon, - Input, -} from "ui"; +} from "ui/form"; +import * as Icon from "ui/icon"; +import { Input } from "ui/input"; import { assert } from "utils"; import { StageFormSchema } from "~/lib/stages"; import { StagePayload, StagesById, DeepPartial } from "~/lib/types"; diff --git a/core/app/c/[communitySlug]/stages/manage/StageManagement.tsx b/core/app/c/[communitySlug]/stages/manage/StageManagement.tsx index 012aae1e7..e6ca67bc9 100644 --- a/core/app/c/[communitySlug]/stages/manage/StageManagement.tsx +++ b/core/app/c/[communitySlug]/stages/manage/StageManagement.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "ui"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "ui/tabs"; import StageEditor from "./StageEditor"; import { StagePayload, StagesById } from "~/lib/types"; diff --git a/core/app/c/[communitySlug]/types/TypeBlock.tsx b/core/app/c/[communitySlug]/types/TypeBlock.tsx index b243d6c7c..52b7efbd1 100644 --- a/core/app/c/[communitySlug]/types/TypeBlock.tsx +++ b/core/app/c/[communitySlug]/types/TypeBlock.tsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; -import { Button, Card, CardContent } from "ui"; +import { Button } from "ui/button"; +import { Card, CardContent } from "ui/card"; import { TypesData } from "./page"; type Props = { type: NonNullable[number] }; diff --git a/core/app/components/IntegrationActions.tsx b/core/app/components/IntegrationActions.tsx index eb8e63328..ddf6a05be 100644 --- a/core/app/components/IntegrationActions.tsx +++ b/core/app/components/IntegrationActions.tsx @@ -1,4 +1,4 @@ -import { Popover, PopoverTrigger, Button, PopoverContent } from "ui"; +import { Button } from "ui/button"; import { PubPayload } from "~/lib/types"; type Props = { diff --git a/core/app/components/LogoutButton.tsx b/core/app/components/LogoutButton.tsx index 550a44292..5dab03a8a 100644 --- a/core/app/components/LogoutButton.tsx +++ b/core/app/components/LogoutButton.tsx @@ -1,6 +1,6 @@ "use client"; import { supabase } from "~/lib/supabase"; -import { Button } from "ui"; +import { Button } from "ui/button"; import { useRouter } from "next/navigation"; export default function LogoutButton() { diff --git a/core/app/components/PubRow.tsx b/core/app/components/PubRow.tsx index 2b3e1496e..a5bba352a 100644 --- a/core/app/components/PubRow.tsx +++ b/core/app/components/PubRow.tsx @@ -2,7 +2,8 @@ import Link from "next/link"; import React, { Fragment } from "react"; -import { Button, Collapsible, CollapsibleContent, CollapsibleTrigger } from "ui"; +import { Button } from "ui/button"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "ui/collapsible"; import { cn } from "utils"; import { PubPayload } from "~/lib/types"; import IntegrationActions from "./IntegrationActions"; @@ -48,7 +49,10 @@ const ChildHierarchy = ({ pub }: { pub: PubPayload["children"][number] }) => { {group.pubType.name} - + {getTitle(child)} diff --git a/core/app/components/Row.tsx b/core/app/components/Row.tsx index 31e642034..1ce0d9a36 100644 --- a/core/app/components/Row.tsx +++ b/core/app/components/Row.tsx @@ -1,7 +1,7 @@ "use client"; import { PropsWithChildren } from "react"; -import { Card, CardContent, CardFooter, CardHeader } from "ui"; +import { Card, CardContent, CardFooter, CardHeader } from "ui/card"; import { cn } from "utils"; type Props = PropsWithChildren<{ className?: string }>; diff --git a/core/app/layout.tsx b/core/app/layout.tsx index 557e407e4..029e332da 100644 --- a/core/app/layout.tsx +++ b/core/app/layout.tsx @@ -1,7 +1,7 @@ -import { Toaster } from "ui"; +import { Toaster } from "ui/toaster"; import "ui/styles.css"; import InitClient from "./InitClient"; -import { PublicEnvProvider } from 'next-runtime-env'; +import { PublicEnvProvider } from "next-runtime-env"; import "./globals.css"; export const metadata = { @@ -13,11 +13,11 @@ export default function RootLayout({ children }: { children: React.ReactNode }) return ( - - - {children} - - + + + {children} + + ); diff --git a/core/lib/supabase.ts b/core/lib/supabase.ts index cbe42e909..32cd7efe1 100644 --- a/core/lib/supabase.ts +++ b/core/lib/supabase.ts @@ -11,4 +11,5 @@ export const createBrowserSupabase = (url, publicKey) => { }); }; -export const formatSupabaseError = (error: AuthError) => `${error.name} ${error.status}: ${error.message}` \ No newline at end of file +export const formatSupabaseError = (error: AuthError) => + `${error.name} ${error.status}: ${error.message}`; diff --git a/core/scripts/invite.ts b/core/scripts/invite.ts index 77fcf90f2..e2827c408 100644 --- a/core/scripts/invite.ts +++ b/core/scripts/invite.ts @@ -8,7 +8,7 @@ import { randomUUID } from "crypto"; import { unJournalId } from "../prisma/exampleCommunitySeeds/unjournal"; const getServerSupabase = () => { - // can use the process.env here, because this code only runs in server context + // can use the process.env here, because this code only runs in server context const url = process.env.NEXT_PUBLIC_SUPABASE_URL; const key = process.env.SUPABASE_SERVICE_ROLE_KEY; if (!url || !key) { diff --git a/infrastructure/terraform/aws/README.md b/infrastructure/terraform/aws/README.md index 432719351..d97fc0b6f 100644 --- a/infrastructure/terraform/aws/README.md +++ b/infrastructure/terraform/aws/README.md @@ -3,6 +3,7 @@ ## Dependencies ### State file bucket/config + You must have some way of storing terraform state files. We use and recommend the s3 backend, but you can change that configuration. In order to keep this code generic, however, @@ -24,7 +25,6 @@ If you need to change the backend, update this file and `init` again. the module exposes its configuration area, but those configurations need to be supplied at plan/apply time using the flag `-var-file=demo-env.tfvars`. - ## Adding secrets To provide secrets to ECS containers, you should put them in AWS Secrets Manager. diff --git a/integrations/evaluations/app/actions/evaluate/evaluate.tsx b/integrations/evaluations/app/actions/evaluate/evaluate.tsx index f40b24825..baa483e46 100644 --- a/integrations/evaluations/app/actions/evaluate/evaluate.tsx +++ b/integrations/evaluations/app/actions/evaluate/evaluate.tsx @@ -7,12 +7,16 @@ import Ajv from "ajv"; import { fullFormats } from "ajv-formats/dist/formats"; import { useEffect, useMemo } from "react"; import { useForm } from "react-hook-form"; -import { Button, Form, Icon, useLocalStorage, useToast } from "ui"; import { Process } from "~/lib/components/Process"; import { Research } from "~/lib/components/Research"; import { EvaluatorWhoAccepted, InstanceConfig } from "~/lib/types"; import { submit, upload } from "./actions"; import { calculateDeadline } from "~/lib/emails"; +import { useToast } from "ui/use-toast"; +import { useLocalStorage } from "ui/hooks"; +import { Form } from "ui/form"; +import { Button } from "ui/button"; +import { Loader2 } from "ui/icon"; type Props = { instanceId: string; @@ -124,7 +128,7 @@ export function Evaluate(props: Props) { {formFieldsFromSchema} diff --git a/integrations/evaluations/app/actions/manage/EvaluatorInviteForm.tsx b/integrations/evaluations/app/actions/manage/EvaluatorInviteForm.tsx index 91981c810..26deec554 100644 --- a/integrations/evaluations/app/actions/manage/EvaluatorInviteForm.tsx +++ b/integrations/evaluations/app/actions/manage/EvaluatorInviteForm.tsx @@ -4,21 +4,6 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { GetPubResponseBody } from "@pubpub/sdk"; import React, { useCallback } from "react"; import { useFieldArray, useForm } from "react-hook-form"; -import { - Button, - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, - Form, - FormDescription, - FormItem, - FormLabel, - Icon, - useToast, -} from "ui"; import { cn } from "utils"; import { EmailTemplate, Evaluator, InstanceConfig, hasUser, isInvited, isSaved } from "~/lib/types"; import { EvaluatorInviteFormInviteButton } from "./EvaluatorInviteFormInviteButton"; @@ -26,6 +11,11 @@ import { EvaluatorInviteFormSaveButton } from "./EvaluatorInviteFormSaveButton"; import { EvaluatorInviteRow } from "./EvaluatorInviteRow"; import * as actions from "./actions"; import { InviteFormEvaluator, InviteFormSchema } from "./types"; +import { useToast } from "ui/use-toast"; +import { Form, FormDescription, FormItem, FormLabel } from "ui/form"; +import { Button } from "ui/button"; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "ui/card"; +import { Loader2, Plus } from "ui/icon"; type Props = { evaluators: Evaluator[]; @@ -223,7 +213,7 @@ export function EvaluatorInviteForm(props: Props) { /> ))} @@ -235,7 +225,7 @@ export function EvaluatorInviteForm(props: Props) {
{form.formState.isSubmitting && ( - + )} onSubmit(values, true))} diff --git a/integrations/evaluations/app/actions/manage/EvaluatorInviteFormInviteButton.tsx b/integrations/evaluations/app/actions/manage/EvaluatorInviteFormInviteButton.tsx index 8dbf7eb2e..c8306b238 100644 --- a/integrations/evaluations/app/actions/manage/EvaluatorInviteFormInviteButton.tsx +++ b/integrations/evaluations/app/actions/manage/EvaluatorInviteFormInviteButton.tsx @@ -1,5 +1,7 @@ import { useFormState, useWatch } from "react-hook-form"; -import { Button, Icon } from "ui"; +import { Button } from "ui/button"; +import { Send } from "ui/icon"; + import { InviteFormSchema } from "./types"; export type EvaluatorInviteFormInviteButtonProps = { @@ -16,7 +18,7 @@ export function EvaluatorInviteFormInviteButton(props: EvaluatorInviteFormInvite !form.evaluators?.some((evaluator) => evaluator.selected) || formState.isSubmitting } > - + Invite ); diff --git a/integrations/evaluations/app/actions/manage/EvaluatorInviteFormSaveButton.tsx b/integrations/evaluations/app/actions/manage/EvaluatorInviteFormSaveButton.tsx index 8bb912330..e3beff5fd 100644 --- a/integrations/evaluations/app/actions/manage/EvaluatorInviteFormSaveButton.tsx +++ b/integrations/evaluations/app/actions/manage/EvaluatorInviteFormSaveButton.tsx @@ -1,5 +1,5 @@ import { useFormState } from "react-hook-form"; -import { Button, Icon } from "ui"; +import { Button } from "ui/button"; export type EvaluatorInviteFormSaveButtonProps = { onClick: () => void; diff --git a/integrations/evaluations/app/actions/manage/EvaluatorInviteRow.tsx b/integrations/evaluations/app/actions/manage/EvaluatorInviteRow.tsx index f0e783ab0..4b6d2fe71 100644 --- a/integrations/evaluations/app/actions/manage/EvaluatorInviteRow.tsx +++ b/integrations/evaluations/app/actions/manage/EvaluatorInviteRow.tsx @@ -2,7 +2,10 @@ import { SuggestedMembersQuery } from "@pubpub/sdk"; import { Control, useWatch } from "react-hook-form"; -import { Button, FormControl, FormField, FormItem, FormMessage, Icon, Input } from "ui"; +import { Button } from "ui/button"; +import { Input } from "ui/input"; +import { FormControl, FormField, FormItem, FormMessage } from "ui/form"; +import { X } from "ui/icon"; import { cn } from "utils"; import { hasUser } from "~/lib/types"; import { EvaluatorInviteRowEmailDialog } from "./EvaluatorInviteRowEmailDialog"; @@ -115,7 +118,7 @@ export const EvaluatorInviteRow = (props: Props) => { )}
diff --git a/integrations/evaluations/app/actions/manage/EvaluatorInviteRowEmailDialog.tsx b/integrations/evaluations/app/actions/manage/EvaluatorInviteRowEmailDialog.tsx index cffb11cec..5de62873e 100644 --- a/integrations/evaluations/app/actions/manage/EvaluatorInviteRowEmailDialog.tsx +++ b/integrations/evaluations/app/actions/manage/EvaluatorInviteRowEmailDialog.tsx @@ -1,22 +1,18 @@ "use client"; +import { Button } from "ui/button"; import { - Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, - Icon, - Input, - Textarea, -} from "ui"; +} from "ui/dialog"; +import { FormControl, FormField, FormItem, FormLabel, FormMessage } from "ui/form"; +import { Mail } from "ui/icon"; +import { Input } from "ui/input"; +import { Textarea } from "ui/textarea"; import { isInvited } from "~/lib/types"; import { InviteFormEvaluator } from "./types"; @@ -31,7 +27,7 @@ export const EvaluatorInviteRowEmailDialog = (props: EvaluatorInviteRowEmailDial diff --git a/integrations/evaluations/app/actions/manage/EvaluatorSuggestButton.tsx b/integrations/evaluations/app/actions/manage/EvaluatorSuggestButton.tsx index b337430d2..b303a280e 100644 --- a/integrations/evaluations/app/actions/manage/EvaluatorSuggestButton.tsx +++ b/integrations/evaluations/app/actions/manage/EvaluatorSuggestButton.tsx @@ -1,5 +1,6 @@ import { useTransition } from "react"; -import { Button, Icon } from "ui"; +import { Button } from "ui/button"; +import { Loader2, Wand2 } from "ui/icon"; type Props = { onClick: () => void; @@ -16,7 +17,7 @@ export const EvaluatorSuggestButton = (props: Props) => { }} disabled={pending} > - {pending ? : } + {pending ? : } ); }; diff --git a/integrations/evaluations/app/actions/manage/EvalutorInviteRowStatus.tsx b/integrations/evaluations/app/actions/manage/EvalutorInviteRowStatus.tsx index 27ead54a5..e4c050fbc 100644 --- a/integrations/evaluations/app/actions/manage/EvalutorInviteRowStatus.tsx +++ b/integrations/evaluations/app/actions/manage/EvalutorInviteRowStatus.tsx @@ -1,5 +1,5 @@ import { memo } from "react"; -import { Badge } from "ui"; +import { Badge } from "ui/badge"; import { cn } from "utils"; import { InviteStatus } from "~/lib/types"; diff --git a/integrations/evaluations/app/actions/respond/respond.tsx b/integrations/evaluations/app/actions/respond/respond.tsx index cd5d3e0a0..efe0eb6a1 100644 --- a/integrations/evaluations/app/actions/respond/respond.tsx +++ b/integrations/evaluations/app/actions/respond/respond.tsx @@ -2,7 +2,8 @@ import { GetPubResponseBody } from "@pubpub/sdk"; import { useCallback } from "react"; -import { Button, toast } from "ui"; +import { Button } from "ui/button"; +import { toast } from "ui/use-toast"; import { accept, contact, decline } from "./actions"; import { InstanceConfig } from "~/lib/types"; import Link from "next/link"; diff --git a/integrations/evaluations/app/configure/configure.tsx b/integrations/evaluations/app/configure/configure.tsx index c9d442ba8..cfc5a39b8 100644 --- a/integrations/evaluations/app/configure/configure.tsx +++ b/integrations/evaluations/app/configure/configure.tsx @@ -2,14 +2,9 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useEffect, useMemo } from "react"; import { useForm } from "react-hook-form"; +import { Button } from "ui/button"; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "ui/card"; import { - Button, - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, Form, FormControl, FormDescription, @@ -17,16 +12,13 @@ import { FormItem, FormLabel, FormMessage, - Icon, - Input, - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, - Textarea, - useToast, -} from "ui"; +} from "ui/form"; +import { Loader2 } from "ui/icon"; +import { Input } from "ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "ui/select"; +import { Textarea } from "ui/textarea"; + +import { useToast } from "ui/use-toast"; import { cn } from "utils"; import * as z from "zod"; import { InstanceConfig } from "~/lib/types"; @@ -281,7 +273,7 @@ export function Configure(props: Props) { diff --git a/integrations/evaluations/app/layout.tsx b/integrations/evaluations/app/layout.tsx index ff835d29c..58e527e86 100644 --- a/integrations/evaluations/app/layout.tsx +++ b/integrations/evaluations/app/layout.tsx @@ -1,5 +1,5 @@ import { User } from "@pubpub/sdk"; -import { Toaster } from "ui"; +import { Toaster } from "ui/toaster"; import "ui/styles.css"; import { expect } from "utils"; import { Integration } from "~/lib/Integration"; diff --git a/integrations/evaluations/lib/components/Research.tsx b/integrations/evaluations/lib/components/Research.tsx index 1a8724379..87a52f612 100644 --- a/integrations/evaluations/lib/components/Research.tsx +++ b/integrations/evaluations/lib/components/Research.tsx @@ -1,4 +1,4 @@ -import { Card, CardContent } from "ui"; +import { Card, CardContent } from "ui/card"; export type Props = { title: string; diff --git a/integrations/submissions/app/actions/submit/FetchMetadataButton.tsx b/integrations/submissions/app/actions/submit/FetchMetadataButton.tsx index 828428d49..d7493014b 100644 --- a/integrations/submissions/app/actions/submit/FetchMetadataButton.tsx +++ b/integrations/submissions/app/actions/submit/FetchMetadataButton.tsx @@ -2,16 +2,11 @@ import { useTransition } from "react"; import { useFormContext } from "react-hook-form"; -import { - Button, - Icon, - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, - useFormField, - useToast, -} from "ui"; +import { Button } from "ui/button"; +import { Loader2, Wand2 } from "ui/icon"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "ui/tooltip"; +import { useFormField } from "ui/form"; +import { useToast } from "ui/use-toast"; import { cn } from "utils"; import { resolveMetadata } from "./actions"; @@ -79,9 +74,9 @@ export const FetchMetadataButton = (props: FetchMetadataButtonProps) => { disabled={!state.isDirty || state.invalid} > {pending ? ( - + ) : ( - + )} diff --git a/integrations/submissions/app/actions/submit/submit.tsx b/integrations/submissions/app/actions/submit/submit.tsx index 437104632..2486af026 100644 --- a/integrations/submissions/app/actions/submit/submit.tsx +++ b/integrations/submissions/app/actions/submit/submit.tsx @@ -3,14 +3,9 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useEffect } from "react"; import { useForm } from "react-hook-form"; +import { Button } from "ui/button"; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "ui/card"; import { - Button, - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, Form, FormControl, FormDescription, @@ -18,12 +13,12 @@ import { FormItem, FormLabel, FormMessage, - Icon, - Input, - Textarea, - useLocalStorage, - useToast, -} from "ui"; +} from "ui/form"; +import { Loader2 } from "ui/icon"; +import { Input } from "ui/input"; +import { Textarea } from "ui/textarea"; +import { useLocalStorage } from "ui/hooks"; +import { useToast } from "ui/use-toast"; import { DOI_REGEX, URL_REGEX, cn, isDoi, normalizeDoi } from "utils"; import * as z from "zod"; import { FetchMetadataButton } from "./FetchMetadataButton"; @@ -230,7 +225,7 @@ export function Submit(props: Props) { diff --git a/integrations/submissions/app/configure/configure.tsx b/integrations/submissions/app/configure/configure.tsx index 102a5e587..cb549ffc3 100644 --- a/integrations/submissions/app/configure/configure.tsx +++ b/integrations/submissions/app/configure/configure.tsx @@ -2,8 +2,8 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; +import { Button } from "ui/button"; import { - Button, Form, FormControl, FormDescription, @@ -11,16 +11,12 @@ import { FormItem, FormLabel, FormMessage, - Icon, - Input, - useToast, - Card, - CardHeader, - CardFooter, - CardContent, - CardTitle, - CardDescription, -} from "ui"; +} from "ui/form"; +import { Loader2 } from "ui/icon"; +import { Input } from "ui/input"; +import { useToast } from "ui/use-toast"; +import { Card, CardHeader, CardFooter, CardContent, CardTitle, CardDescription } from "ui/card"; + import { cn } from "utils"; import * as z from "zod"; import { configure } from "./actions"; @@ -104,7 +100,7 @@ export function Configure(props: Props) { diff --git a/integrations/submissions/app/layout.tsx b/integrations/submissions/app/layout.tsx index 525024139..4f2ced229 100644 --- a/integrations/submissions/app/layout.tsx +++ b/integrations/submissions/app/layout.tsx @@ -1,6 +1,6 @@ import { User } from "@pubpub/sdk"; import { cookies, headers } from "next/headers"; -import { Toaster } from "ui"; +import { Toaster } from "ui/toaster"; import "ui/styles.css"; import { expect } from "utils"; import { Integration } from "~/lib/Integration"; diff --git a/package.json b/package.json index 4900379b6..59701ebab 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.22.15", "@changesets/cli": "^2.26.2", - "@preconstruct/cli": "^2.8.1", + "@preconstruct/cli": "^2.8.3", "husky": "^8.0.3", "lint-staged": "^13.2.2", "prettier": "^2.7.1", diff --git a/packages/sdk/src/react/Integration.tsx b/packages/sdk/src/react/Integration.tsx index 7679df75c..f36210eb7 100644 --- a/packages/sdk/src/react/Integration.tsx +++ b/packages/sdk/src/react/Integration.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { LocalStorageProvider } from "ui"; +import { LocalStorageProvider } from "ui/hooks"; import { IntegrationLayout } from "./IntegrationLayout"; import { IntegrationProvider, IntegrationProviderProps } from "./IntegrationProvider"; diff --git a/packages/sdk/src/react/IntegrationAvatar.tsx b/packages/sdk/src/react/IntegrationAvatar.tsx index 659d01c99..046b6ba55 100644 --- a/packages/sdk/src/react/IntegrationAvatar.tsx +++ b/packages/sdk/src/react/IntegrationAvatar.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { Avatar, AvatarImage, AvatarFallback } from "ui"; +import { Avatar, AvatarImage, AvatarFallback } from "ui/avatar"; type Props = { firstName: string; diff --git a/packages/sdk/src/react/generateFormFields.tsx b/packages/sdk/src/react/generateFormFields.tsx index 0c4a5fb75..faf117eb5 100644 --- a/packages/sdk/src/react/generateFormFields.tsx +++ b/packages/sdk/src/react/generateFormFields.tsx @@ -3,20 +3,13 @@ import * as React from "react"; import Ajv, { JSONSchemaType } from "ajv"; import { GetPubTypeResponseBody } from "contracts"; import { Control, ControllerRenderProps } from "react-hook-form"; -import { - Checkbox, - Confidence, - FileUpload, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, - Input, - Separator, - Textarea, -} from "ui"; +import { Checkbox } from "ui/checkbox"; +import { Confidence } from "ui/customRenderers/confidence/confidence"; +import { FileUpload } from "ui/customRenderers/fileUpload/fileUpload"; +import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "ui/form"; +import { Input } from "ui/input"; +import { Separator } from "ui/separator"; +import { Textarea } from "ui/textarea"; import { cn } from "utils"; // a bit of a hack, but allows us to use AJV's JSON schema type diff --git a/packages/ui/README.md b/packages/ui/README.md index 396595032..7bbc155b5 100644 --- a/packages/ui/README.md +++ b/packages/ui/README.md @@ -1,27 +1,46 @@ -# PubPub UI +# PubPub UI + +## How to import + +You need to import the component from the `ui/\` path. For example: + +```tsx +import { Button } from "ui/button"; +import { Loader2 } from "ui/icon"; +``` + +## How to add a new component + +1. Create a file in src, something like `src/component.tsx`, that exports a _named_ Component. Ideally copy-paste something from https://ui.shadcn.com/components +2. Add that file to `package.json['entryPoints']` as `component.tsx` +3. Add component to the `package.json['files']` array +4. In packages/ui, run `pnpm preconstruct fix && pnpm preconstruct dev` +5. Everything should be good! ## Rationale for our style approach + In the past, PubPub has stuck pretty close to the style system offered by the off-the-shelf component library we've used (primarily BlueprintJS). This is convenient because it allowed us to move quickly early on, but has become a liability over time as our design needs to deviate from the Blueprint design, and as the Blueprint library introduces breaking changes. In v7, we find ourselves with an additional styling objective, which is to allow integrations (both 1st and 3rd party) to easily adopt the styles and design of `core`. Further, we hope to do this without requiring every integration to use the exact same technical stack as `core`. Other important considerations include site-wide theming capabilities, robust Figma offerings, and performance (this was a noticeable issue with Blueprint as PubPub v6 scaled). -Other minor considerations include a preference to avoid CSS-in-JS approaches, size of the open source community, sustainability of the library (having a library stop maintenance because it had no business model puts us in a tough position), component-level bundling (*all* of BlueprintJS ships with every page even if we only used a single component), and friendliness with server-side rendering (i.e. no FOUC while it waits for client-side JS to load). +Other minor considerations include a preference to avoid CSS-in-JS approaches, size of the open source community, sustainability of the library (having a library stop maintenance because it had no business model puts us in a tough position), component-level bundling (_all_ of BlueprintJS ships with every page even if we only used a single component), and friendliness with server-side rendering (i.e. no FOUC while it waits for client-side JS to load). In experimenting with different component libraries and workflows, a consistent fork in the road kept appearing. We can either: + 1. pick a React library and require integrations to use React if they want to be styled similarly, or 2. we need to pick an approach that works in vanilla HTML -The first approach would let us choose something like Chakra, Mantine, or Blueprint. The second approach leaves us essentially choosing between writing all CSS classes ourselves or picking a tool like Tailwind. +The first approach would let us choose something like Chakra, Mantine, or Blueprint. The second approach leaves us essentially choosing between writing all CSS classes ourselves or picking a tool like Tailwind. -I feel strongly that integrations shouldn't *have* to use the same stack as us (i.e. React), so spent time focusing on approach (2). Down that path, Tailwind offers a lot that writing our own classes from scratch does not. Writing raw CSS everywhere feels too difficult to maintain for a large project and I think we’d frankly wind up essentially re-implementing tailwind, but with less familiarity for integration developers and worse documentation. Further, +I feel strongly that integrations shouldn't _have_ to use the same stack as us (i.e. React), so spent time focusing on approach (2). Down that path, Tailwind offers a lot that writing our own classes from scratch does not. Writing raw CSS everywhere feels too difficult to maintain for a large project and I think we’d frankly wind up essentially re-implementing tailwind, but with less familiarity for integration developers and worse documentation. Further, -- It’s got great documentation with best practices and a huge community -- It has tons of community Figma boards -- It lets us offer a simple configuration plugin (i.e. `plugins: [require("pubpub/styles")]`) that defines colors, borders, fonts, etc that could allow integrations to easily get good-enough similarity with little other work. +- It’s got great documentation with best practices and a huge community +- It has tons of community Figma boards +- It lets us offer a simple configuration plugin (i.e. `plugins: [require("pubpub/styles")]`) that defines colors, borders, fonts, etc that could allow integrations to easily get good-enough similarity with little other work. -So, if we run with the assumption that we'll use Tailwind and integrations have to drop in Tailwind to achieve similar styling, the next question is: Do we build components ourselves or take off the shelf ones? The question feels trivial: we shouldn't try to rebuild complex UI components from scratch. Accessibility is hard. Cross-browser support for edge case interactions is hard. +So, if we run with the assumption that we'll use Tailwind and integrations have to drop in Tailwind to achieve similar styling, the next question is: Do we build components ourselves or take off the shelf ones? The question feels trivial: we shouldn't try to rebuild complex UI components from scratch. Accessibility is hard. Cross-browser support for edge case interactions is hard. I spent some time playing with Flowbite, which offers an extensive component library built from native HTML and tailwind css. Flowbite even offers a 1st-party React library of their components. Unfortunately, Flowbite-React seems insufficient for us. It still has a good deal of bugs, does not have complete component coverage, and their experimental theming is doing a JS theming thing instead of just using tailwind config (! So it winds up essentially being the same as Chakra or Mantine, in that the component library bundles its styles with it). @@ -36,33 +55,37 @@ Because integrations that are written with a different framework won't be able t This also gives us a lot of flexibility in building components. We can mix and match headless libraries if we want — there's no problem having, for example, both Radix and AriaKit since they aren’t fighting for style. We're not forced into a monolithic decision. Of course, headless component libraries have a lot of overlap, so I expect we would mostly use the same one, but from the perspective integration developers and performance, there's really no "commitment" beyond tailwind. On top of Tailwind, there is a lot of good community work to draw from: -- Radix and shadcn/ui -- AriaKit components -- HeadlessUI and open source TailwindUI components built on top of it -- Countless Tailwind component libraries -- Countless Tailwind Figma boards + +- Radix and shadcn/ui +- AriaKit components +- HeadlessUI and open source TailwindUI components built on top of it +- Countless Tailwind component libraries +- Countless Tailwind Figma boards The other perk of this approach is that all component styling is kept in pubpub-core, as opposed to deep in some node module. This makes it a bit easier for someone who's trying to port a component into Vue or Svelte and want it to look like PubPub components — the core HTML and tailwind classes are easily found. In the end, this approach resonates with me because: -- It feels as close to vanilla CSS as we can get without forcing ourselves to write all our classes and components from scratch. -- It's performant since it's just CSS at the end of the day (especially compared to monolithic component libraries like Blueprint). -- It offers integration developers a simple approach to add our tailwind config as a plugin. -- It allows our components to use headless UI libraries focused on accessibility. -- We maintain complete control of top-level components and styling. + +- It feels as close to vanilla CSS as we can get without forcing ourselves to write all our classes and components from scratch. +- It's performant since it's just CSS at the end of the day (especially compared to monolithic component libraries like Blueprint). +- It offers integration developers a simple approach to add our tailwind config as a plugin. +- It allows our components to use headless UI libraries focused on accessibility. +- We maintain complete control of top-level components and styling. The primary tradeoff to all of this is a little more up front work compared to using something like Chakra, but there is so much community offering that I don't feel concerned about our ability to build quickly. ## Links and Resources + A list of resources I found helpful in my spike: -- [CSS Solution Analysis for Polaris Foundations](https://docs.google.com/spreadsheets/d/1rxrRTlbNWiLVu-Q5IK7xh5O1FmWcjyAS2XN7jiPrhYM/edit#gid=0) - - An in depth analysis of different CSS approaches. A few years old, and notably, current Tailwind has addressed nearly all its issues (multiple themes and build-time performance). -- [Tailwind](https://tailwindcss.com/) -- [shadcn/ui](https://ui.shadcn.com/) -- [Radix](https://www.radix-ui.com/) -- [Chakra](https://chakra-ui.com/) -- [Mantine](https://mantine.dev/) -- [HeadlessUI](https://headlessui.com/) -- [AriaKit](https://ariakit.org/) -- [Flowbite](https://flowbite.com/) -- [Flowbite-React](https://www.flowbite-react.com/) + +- [CSS Solution Analysis for Polaris Foundations](https://docs.google.com/spreadsheets/d/1rxrRTlbNWiLVu-Q5IK7xh5O1FmWcjyAS2XN7jiPrhYM/edit#gid=0) + - An in depth analysis of different CSS approaches. A few years old, and notably, current Tailwind has addressed nearly all its issues (multiple themes and build-time performance). +- [Tailwind](https://tailwindcss.com/) +- [shadcn/ui](https://ui.shadcn.com/) +- [Radix](https://www.radix-ui.com/) +- [Chakra](https://chakra-ui.com/) +- [Mantine](https://mantine.dev/) +- [HeadlessUI](https://headlessui.com/) +- [AriaKit](https://ariakit.org/) +- [Flowbite](https://flowbite.com/) +- [Flowbite-React](https://www.flowbite-react.com/) diff --git a/packages/ui/alert/package.json b/packages/ui/alert/package.json new file mode 100644 index 000000000..07d4e222d --- /dev/null +++ b/packages/ui/alert/package.json @@ -0,0 +1,4 @@ +{ + "main": "dist/ui-alert.cjs.js", + "module": "dist/ui-alert.esm.js" +} diff --git a/packages/ui/avatar/package.json b/packages/ui/avatar/package.json new file mode 100644 index 000000000..beb541505 --- /dev/null +++ b/packages/ui/avatar/package.json @@ -0,0 +1,4 @@ +{ + "main": "dist/ui-avatar.cjs.js", + "module": "dist/ui-avatar.esm.js" +} diff --git a/packages/ui/badge/package.json b/packages/ui/badge/package.json new file mode 100644 index 000000000..180d027b2 --- /dev/null +++ b/packages/ui/badge/package.json @@ -0,0 +1,4 @@ +{ + "main": "dist/ui-badge.cjs.js", + "module": "dist/ui-badge.esm.js" +} diff --git a/packages/ui/button/package.json b/packages/ui/button/package.json new file mode 100644 index 000000000..1081a7e87 --- /dev/null +++ b/packages/ui/button/package.json @@ -0,0 +1,4 @@ +{ + "main": "dist/ui-button.cjs.js", + "module": "dist/ui-button.esm.js" +} diff --git a/packages/ui/calandar/package.json b/packages/ui/calandar/package.json new file mode 100644 index 000000000..513cc6c82 --- /dev/null +++ b/packages/ui/calandar/package.json @@ -0,0 +1,4 @@ +{ + "main": "dist/ui-calandar.cjs.js", + "module": "dist/ui-calandar.esm.js" +} diff --git a/packages/ui/card/package.json b/packages/ui/card/package.json new file mode 100644 index 000000000..9fe9a69b3 --- /dev/null +++ b/packages/ui/card/package.json @@ -0,0 +1,4 @@ +{ + "main": "dist/ui-card.cjs.js", + "module": "dist/ui-card.esm.js" +} diff --git a/packages/ui/checkbox/package.json b/packages/ui/checkbox/package.json new file mode 100644 index 000000000..19785872b --- /dev/null +++ b/packages/ui/checkbox/package.json @@ -0,0 +1,4 @@ +{ + "main": "dist/ui-checkbox.cjs.js", + "module": "dist/ui-checkbox.esm.js" +} diff --git a/packages/ui/collapsible/package.json b/packages/ui/collapsible/package.json new file mode 100644 index 000000000..7f5b38106 --- /dev/null +++ b/packages/ui/collapsible/package.json @@ -0,0 +1,4 @@ +{ + "main": "dist/ui-collapsible.cjs.js", + "module": "dist/ui-collapsible.esm.js" +} diff --git a/packages/ui/customRenderers/confidence/confidence/package.json b/packages/ui/customRenderers/confidence/confidence/package.json new file mode 100644 index 000000000..d7811aacd --- /dev/null +++ b/packages/ui/customRenderers/confidence/confidence/package.json @@ -0,0 +1,4 @@ +{ + "main": "dist/ui-customRenderers-confidence-confidence.cjs.js", + "module": "dist/ui-customRenderers-confidence-confidence.esm.js" +} diff --git a/packages/ui/customRenderers/fileUpload/fileUpload/package.json b/packages/ui/customRenderers/fileUpload/fileUpload/package.json new file mode 100644 index 000000000..115896971 --- /dev/null +++ b/packages/ui/customRenderers/fileUpload/fileUpload/package.json @@ -0,0 +1,4 @@ +{ + "main": "dist/ui-customRenderers-fileUpload-fileUpload.cjs.js", + "module": "dist/ui-customRenderers-fileUpload-fileUpload.esm.js" +} diff --git a/packages/ui/dialog/package.json b/packages/ui/dialog/package.json new file mode 100644 index 000000000..6c62c6c49 --- /dev/null +++ b/packages/ui/dialog/package.json @@ -0,0 +1,4 @@ +{ + "main": "dist/ui-dialog.cjs.js", + "module": "dist/ui-dialog.esm.js" +} diff --git a/packages/ui/dropdown-menu/package.json b/packages/ui/dropdown-menu/package.json new file mode 100644 index 000000000..19a93e342 --- /dev/null +++ b/packages/ui/dropdown-menu/package.json @@ -0,0 +1,4 @@ +{ + "main": "dist/ui-dropdown-menu.cjs.js", + "module": "dist/ui-dropdown-menu.esm.js" +} diff --git a/packages/ui/form/package.json b/packages/ui/form/package.json new file mode 100644 index 000000000..2d77a663e --- /dev/null +++ b/packages/ui/form/package.json @@ -0,0 +1,4 @@ +{ + "main": "dist/ui-form.cjs.js", + "module": "dist/ui-form.esm.js" +} diff --git a/packages/ui/hooks/package.json b/packages/ui/hooks/package.json new file mode 100644 index 000000000..ba1eb5669 --- /dev/null +++ b/packages/ui/hooks/package.json @@ -0,0 +1,4 @@ +{ + "main": "dist/ui-hooks.cjs.js", + "module": "dist/ui-hooks.esm.js" +} diff --git a/packages/ui/hover-card/package.json b/packages/ui/hover-card/package.json new file mode 100644 index 000000000..ef697bc22 --- /dev/null +++ b/packages/ui/hover-card/package.json @@ -0,0 +1,4 @@ +{ + "main": "dist/ui-hover-card.cjs.js", + "module": "dist/ui-hover-card.esm.js" +} diff --git a/packages/ui/icon/package.json b/packages/ui/icon/package.json new file mode 100644 index 000000000..a76332af5 --- /dev/null +++ b/packages/ui/icon/package.json @@ -0,0 +1,4 @@ +{ + "main": "dist/ui-icon.cjs.js", + "module": "dist/ui-icon.esm.js" +} diff --git a/packages/ui/input/package.json b/packages/ui/input/package.json new file mode 100644 index 000000000..420fbd174 --- /dev/null +++ b/packages/ui/input/package.json @@ -0,0 +1,4 @@ +{ + "main": "dist/ui-input.cjs.js", + "module": "dist/ui-input.esm.js" +} diff --git a/packages/ui/label/package.json b/packages/ui/label/package.json new file mode 100644 index 000000000..4c9190dc6 --- /dev/null +++ b/packages/ui/label/package.json @@ -0,0 +1,4 @@ +{ + "main": "dist/ui-label.cjs.js", + "module": "dist/ui-label.esm.js" +} diff --git a/packages/ui/package.json b/packages/ui/package.json index ac4bc0110..561b6096c 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -4,9 +4,163 @@ "version": "0.0.0", "main": "dist/ui.cjs.js", "module": "dist/ui.esm.js", + "exports": { + "./card": { + "module": "./card/dist/ui-card.esm.js", + "default": "./card/dist/ui-card.cjs.js" + }, + "./form": { + "module": "./form/dist/ui-form.esm.js", + "default": "./form/dist/ui-form.cjs.js" + }, + "./icon": { + "module": "./icon/dist/ui-icon.esm.js", + "default": "./icon/dist/ui-icon.cjs.js" + }, + "./tabs": { + "module": "./tabs/dist/ui-tabs.esm.js", + "default": "./tabs/dist/ui-tabs.cjs.js" + }, + "./alert": { + "module": "./alert/dist/ui-alert.esm.js", + "default": "./alert/dist/ui-alert.cjs.js" + }, + "./badge": { + "module": "./badge/dist/ui-badge.esm.js", + "default": "./badge/dist/ui-badge.cjs.js" + }, + "./input": { + "module": "./input/dist/ui-input.esm.js", + "default": "./input/dist/ui-input.cjs.js" + }, + "./label": { + "module": "./label/dist/ui-label.esm.js", + "default": "./label/dist/ui-label.cjs.js" + }, + "./toast": { + "module": "./toast/dist/ui-toast.esm.js", + "default": "./toast/dist/ui-toast.cjs.js" + }, + "./avatar": { + "module": "./avatar/dist/ui-avatar.esm.js", + "default": "./avatar/dist/ui-avatar.cjs.js" + }, + "./button": { + "module": "./button/dist/ui-button.esm.js", + "default": "./button/dist/ui-button.cjs.js" + }, + "./dialog": { + "module": "./dialog/dist/ui-dialog.esm.js", + "default": "./dialog/dist/ui-dialog.cjs.js" + }, + "./select": { + "module": "./select/dist/ui-select.esm.js", + "default": "./select/dist/ui-select.cjs.js" + }, + "./popover": { + "module": "./popover/dist/ui-popover.esm.js", + "default": "./popover/dist/ui-popover.cjs.js" + }, + "./toaster": { + "module": "./toaster/dist/ui-toaster.esm.js", + "default": "./toaster/dist/ui-toaster.cjs.js" + }, + "./tooltip": { + "module": "./tooltip/dist/ui-tooltip.esm.js", + "default": "./tooltip/dist/ui-tooltip.cjs.js" + }, + "./calandar": { + "module": "./calandar/dist/ui-calandar.esm.js", + "default": "./calandar/dist/ui-calandar.cjs.js" + }, + "./checkbox": { + "module": "./checkbox/dist/ui-checkbox.esm.js", + "default": "./checkbox/dist/ui-checkbox.cjs.js" + }, + "./textarea": { + "module": "./textarea/dist/ui-textarea.esm.js", + "default": "./textarea/dist/ui-textarea.cjs.js" + }, + "./separator": { + "module": "./separator/dist/ui-separator.esm.js", + "default": "./separator/dist/ui-separator.cjs.js" + }, + "./use-toast": { + "module": "./use-toast/dist/ui-use-toast.esm.js", + "default": "./use-toast/dist/ui-use-toast.cjs.js" + }, + "./hooks": { + "module": "./hooks/dist/ui-hooks.esm.js", + "default": "./hooks/dist/ui-hooks.cjs.js" + }, + "./hover-card": { + "module": "./hover-card/dist/ui-hover-card.esm.js", + "default": "./hover-card/dist/ui-hover-card.cjs.js" + }, + "./collapsible": { + "module": "./collapsible/dist/ui-collapsible.esm.js", + "default": "./collapsible/dist/ui-collapsible.cjs.js" + }, + "./dropdown-menu": { + "module": "./dropdown-menu/dist/ui-dropdown-menu.esm.js", + "default": "./dropdown-menu/dist/ui-dropdown-menu.cjs.js" + }, + "./customRenderers/confidence/confidence": { + "module": "./customRenderers/confidence/confidence/dist/ui-customRenderers-confidence-confidence.esm.js", + "default": "./customRenderers/confidence/confidence/dist/ui-customRenderers-confidence-confidence.cjs.js" + }, + "./customRenderers/fileUpload/fileUpload": { + "module": "./customRenderers/fileUpload/fileUpload/dist/ui-customRenderers-fileUpload-fileUpload.esm.js", + "default": "./customRenderers/fileUpload/fileUpload/dist/ui-customRenderers-fileUpload-fileUpload.cjs.js" + }, + "./package.json": "./package.json", + "./customRenderers/confidence": { + "module": "./customRenderers/confidence/confidence/dist/ui-customRenderers-confidence-confidence.esm.js", + "default": "./customRenderers/confidence/confidence/dist/ui-customRenderers-confidence-confidence.cjs.js" + }, + "./customRenderers/fileUpload": { + "module": "./customRenderers/fileUpload/fileUpload/dist/ui-customRenderers-fileUpload-fileUpload.esm.js", + "default": "./customRenderers/fileUpload/fileUpload/dist/ui-customRenderers-fileUpload-fileUpload.cjs.js" + }, + "./tailwind.config.js": { + "module": "./tailwind.config.js", + "default": "./tailwind.config.js" + }, + "./styles.css": { + "module": "./styles.css", + "default": "./styles.css" + } + }, "files": [ "dist", - "styles.css" + "styles.css", + "alert", + "avatar", + "badge", + "button", + "calandar", + "card", + "checkbox", + "collapsible", + "dialog", + "dropdown-menu", + "form", + "hover-card", + "icon", + "index", + "input", + "label", + "popover", + "select", + "separator", + "tabs", + "textarea", + "toast", + "toaster", + "tooltip", + "use-toast", + "hooks", + "customRenderers" ], "scripts": { "type-check": "tsc" @@ -56,7 +210,58 @@ "react": "^18.2.0", "react-hook-form": "^7.46.1", "tsconfig": "workspace:*", - "typescript": "^4.9.4", + "typescript": "^5.3.3", "zod": "^3.21.4" + }, + "preconstruct": { + "exports": { + "extra": { + "./customRenderers/confidence": { + "module": "./customRenderers/confidence/confidence/dist/ui-customRenderers-confidence-confidence.esm.js", + "default": "./customRenderers/confidence/confidence/dist/ui-customRenderers-confidence-confidence.cjs.js" + }, + "./customRenderers/fileUpload": { + "module": "./customRenderers/fileUpload/fileUpload/dist/ui-customRenderers-fileUpload-fileUpload.esm.js", + "default": "./customRenderers/fileUpload/fileUpload/dist/ui-customRenderers-fileUpload-fileUpload.cjs.js" + }, + "./tailwind.config.js": { + "module": "./tailwind.config.js", + "default": "./tailwind.config.js" + }, + "./styles.css": { + "module": "./styles.css", + "default": "./styles.css" + } + } + }, + "entrypoints": [ + "alert.tsx", + "avatar.tsx", + "badge.tsx", + "button.tsx", + "calandar.tsx", + "card.tsx", + "checkbox.tsx", + "collapsible.tsx", + "dialog.tsx", + "dropdown-menu.tsx", + "form.tsx", + "hover-card.tsx", + "icon.tsx", + "input.tsx", + "label.tsx", + "popover.tsx", + "select.tsx", + "separator.tsx", + "tabs.tsx", + "textarea.tsx", + "toast.tsx", + "toaster.tsx", + "tooltip.tsx", + "use-toast.tsx", + "hooks/index.ts", + "customRenderers/confidence/confidence.tsx", + "customRenderers/fileUpload/fileUpload.tsx" + ] } } diff --git a/packages/ui/popover/package.json b/packages/ui/popover/package.json new file mode 100644 index 000000000..6d810b0e6 --- /dev/null +++ b/packages/ui/popover/package.json @@ -0,0 +1,4 @@ +{ + "main": "dist/ui-popover.cjs.js", + "module": "dist/ui-popover.esm.js" +} diff --git a/packages/ui/select/package.json b/packages/ui/select/package.json new file mode 100644 index 000000000..e3370ae0e --- /dev/null +++ b/packages/ui/select/package.json @@ -0,0 +1,4 @@ +{ + "main": "dist/ui-select.cjs.js", + "module": "dist/ui-select.esm.js" +} diff --git a/packages/ui/separator/package.json b/packages/ui/separator/package.json new file mode 100644 index 000000000..f244fc904 --- /dev/null +++ b/packages/ui/separator/package.json @@ -0,0 +1,4 @@ +{ + "main": "dist/ui-separator.cjs.js", + "module": "dist/ui-separator.esm.js" +} diff --git a/packages/ui/src/index.tsx b/packages/ui/src/index.tsx deleted file mode 100644 index 2c851a98b..000000000 --- a/packages/ui/src/index.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* util/src/index.tsx */ - -/* Components */ -export * as Icon from "./icon"; -export * from "./alert"; -export * from "./avatar"; -export * from "./badge"; -export * from "./button"; -export * from "./card"; -export * from "./checkbox"; -export * from "./collapsible"; -export * from "./checkbox"; -export * from "./dialog"; -export * from "./dropdown-menu"; -export * from "./form"; -export * from "./hover-card"; -export * from "./input"; -export * from "./label"; -export * from "./popover"; -export * from "./select"; -export * from "./separator"; -export * from "./tabs"; -export * from "./textarea"; -export * from "./toast"; -export * from "./toaster"; -export * from "./tooltip"; -export * from "./use-toast"; - -/* Renderers */ -export * from "./customRenderers/confidence/confidence"; -export * from "./customRenderers/fileUpload/fileUpload"; - -/* Hooks */ -export * from "./hooks"; diff --git a/packages/ui/tabs/package.json b/packages/ui/tabs/package.json new file mode 100644 index 000000000..27b89bdf0 --- /dev/null +++ b/packages/ui/tabs/package.json @@ -0,0 +1,4 @@ +{ + "main": "dist/ui-tabs.cjs.js", + "module": "dist/ui-tabs.esm.js" +} diff --git a/packages/ui/textarea/package.json b/packages/ui/textarea/package.json new file mode 100644 index 000000000..965ab6525 --- /dev/null +++ b/packages/ui/textarea/package.json @@ -0,0 +1,4 @@ +{ + "main": "dist/ui-textarea.cjs.js", + "module": "dist/ui-textarea.esm.js" +} diff --git a/packages/ui/toast/package.json b/packages/ui/toast/package.json new file mode 100644 index 000000000..56c4eb416 --- /dev/null +++ b/packages/ui/toast/package.json @@ -0,0 +1,4 @@ +{ + "main": "dist/ui-toast.cjs.js", + "module": "dist/ui-toast.esm.js" +} diff --git a/packages/ui/toaster/package.json b/packages/ui/toaster/package.json new file mode 100644 index 000000000..b010e9efa --- /dev/null +++ b/packages/ui/toaster/package.json @@ -0,0 +1,4 @@ +{ + "main": "dist/ui-toaster.cjs.js", + "module": "dist/ui-toaster.esm.js" +} diff --git a/packages/ui/tooltip/package.json b/packages/ui/tooltip/package.json new file mode 100644 index 000000000..c9dd2a863 --- /dev/null +++ b/packages/ui/tooltip/package.json @@ -0,0 +1,4 @@ +{ + "main": "dist/ui-tooltip.cjs.js", + "module": "dist/ui-tooltip.esm.js" +} diff --git a/packages/ui/use-toast/package.json b/packages/ui/use-toast/package.json new file mode 100644 index 000000000..ce5a012ae --- /dev/null +++ b/packages/ui/use-toast/package.json @@ -0,0 +1,4 @@ +{ + "main": "dist/ui-use-toast.cjs.js", + "module": "dist/ui-use-toast.esm.js" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3235da88f..37020f032 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,8 +24,8 @@ importers: specifier: ^2.26.2 version: 2.26.2 '@preconstruct/cli': - specifier: ^2.8.1 - version: 2.8.1 + specifier: ^2.8.3 + version: 2.8.3 husky: specifier: ^8.0.3 version: 8.0.3 @@ -547,8 +547,8 @@ importers: specifier: workspace:* version: link:../../tsconfig typescript: - specifier: ^4.9.4 - version: 4.9.4 + specifier: ^5.3.3 + version: 5.3.3 zod: specifier: ^3.21.4 version: 3.21.4 @@ -1201,14 +1201,6 @@ packages: tslib: 2.6.1 dev: false - /@babel/code-frame@7.22.10: - resolution: {integrity: sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/highlight': 7.22.13 - chalk: 2.4.2 - dev: true - /@babel/code-frame@7.22.13: resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==} engines: {node: '>=6.9.0'} @@ -3079,11 +3071,11 @@ packages: '@nodelib/fs.scandir': 2.1.5 fastq: 1.15.0 - /@preconstruct/cli@2.8.1: - resolution: {integrity: sha512-PX5w+au06iY/QaT+9RLmRlIfavRCRoMTC/krwtNrgPEnubR9e6P+QlywrKmwiEvkzbR9AEzGnRZL8uNRDDMzrQ==} + /@preconstruct/cli@2.8.3: + resolution: {integrity: sha512-4PNEPcp8REUdqZIjtpXF1fqECuHt+pIS6k0PluSRcgX0KwPtfSw407Y2B/ItndgtRD3rKHXI6cKkwh/6Mc4TXg==} hasBin: true dependencies: - '@babel/code-frame': 7.22.10 + '@babel/code-frame': 7.22.13 '@babel/core': 7.22.17 '@babel/helper-module-imports': 7.22.15 '@babel/runtime': 7.22.10 @@ -3095,6 +3087,7 @@ packages: '@rollup/plugin-replace': 2.4.2(rollup@2.79.1) builtin-modules: 3.3.0 chalk: 4.1.2 + ci-info: 3.8.0 dataloader: 2.2.2 detect-indent: 6.1.0 enquirer: 2.4.1 @@ -3102,7 +3095,6 @@ packages: fast-deep-equal: 2.0.1 fast-glob: 3.3.1 fs-extra: 9.1.0 - is-ci: 2.0.0 is-reference: 1.2.1 jest-worker: 26.6.2 magic-string: 0.30.3 @@ -6492,10 +6484,6 @@ packages: engines: {node: '>=10'} dev: true - /ci-info@2.0.0: - resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} - dev: true - /ci-info@3.8.0: resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==} engines: {node: '>=8'} @@ -7806,13 +7794,6 @@ packages: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} - /is-ci@2.0.0: - resolution: {integrity: sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==} - hasBin: true - dependencies: - ci-info: 2.0.0 - dev: true - /is-ci@3.0.1: resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} hasBin: true @@ -10885,6 +10866,12 @@ packages: engines: {node: '>=14.17'} hasBin: true + /typescript@5.3.3: + resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} + engines: {node: '>=14.17'} + hasBin: true + dev: true + /unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} dependencies: From fd3884961274bb5b09bfb303c121716ab8f2625b Mon Sep 17 00:00:00 2001 From: qweliant Date: Tue, 13 Feb 2024 14:10:48 -0500 Subject: [PATCH 02/12] 0;9uadd new emails --- .../evaluations/app/actions/manage/actions.ts | 4 +- integrations/evaluations/lib/emails.ts | 402 ++++++++++++++++-- 2 files changed, 362 insertions(+), 44 deletions(-) diff --git a/integrations/evaluations/app/actions/manage/actions.ts b/integrations/evaluations/app/actions/manage/actions.ts index 7de372b5c..08978cd16 100644 --- a/integrations/evaluations/app/actions/manage/actions.ts +++ b/integrations/evaluations/app/actions/manage/actions.ts @@ -9,7 +9,7 @@ import { cookie } from "~/lib/request"; import { isInvited } from "~/lib/types"; import { scheduleNoReplyNotificationEmail, - scheduleReminderEmail, + scheduleInvitationReminderEmail, sendInviteEmail, } from "../../../lib/emails"; import { InviteFormEvaluator } from "./types"; @@ -71,7 +71,7 @@ export const save = async ( // Immediately send the invite email. await sendInviteEmail(instanceId, pubId, evaluator); // Scehdule a reminder email to person who was invited to evaluate. - await scheduleReminderEmail(instanceId, instanceConfig, pubId, evaluator); + await scheduleInvitationReminderEmail(instanceId, instanceConfig, pubId, evaluator); // Schedule no-reply notification email to person who invited the // evaluator. await scheduleNoReplyNotificationEmail( diff --git a/integrations/evaluations/lib/emails.ts b/integrations/evaluations/lib/emails.ts index ea3918e86..f90ce9ad8 100644 --- a/integrations/evaluations/lib/emails.ts +++ b/integrations/evaluations/lib/emails.ts @@ -29,6 +29,21 @@ export function calculateDeadline( } } +export function getDeadline( + instanceConfig: InstanceConfig, + evaluator: EvaluatorWhoAccepted & EvaluatorWithInvite +): Date { + return evaluator.deadline + ? new Date(evaluator.deadline) + : calculateDeadline( + { + deadlineLength: instanceConfig.deadlineLength, + deadlineUnit: instanceConfig.deadlineUnit, + }, + new Date(evaluator.acceptedAt) + ); +} + const notificationFooter = '

This is an automated email sent from Unjournal. Please contact contact@unjournal.org with any questions.

'; @@ -41,6 +56,7 @@ const makeNoReplyJobKey = (instanceId: string, pubId: string, evaluator: Evaluat const makeNoSubmitJobKey = (instanceId: string, pubId: string, evaluator: EvaluatorWithInvite) => `send-email-${instanceId}-${pubId}-${evaluator.userId}-no-submit`; +// sent to the community manager export const scheduleNoReplyNotificationEmail = async ( instanceId: string, instanceConfig: InstanceConfig, @@ -139,6 +155,158 @@ export const unscheduleNoSubmitNotificationEmail = ( return client.unscheduleEmail(instanceId, jobKey); }; +export const scheduleReminderEmail = async ( + instanceId: string, + instanceConfig: InstanceConfig, + pubId: string, + evaluator: EvaluatorWithInvite +) => { + const jobKey = makeReminderJobKey(instanceId, pubId, evaluator); + const runAt = new Date(evaluator.invitedAt); + runAt.setMinutes(runAt.getMinutes() + DAYS_TO_REMIND_EVALUATOR * 24 * 60); + + await client.scheduleEmail( + instanceId, + { + to: { + userId: evaluator.userId, + }, + subject: `Reminder: {{users.invitor.firstName}} {{users.invitor.lastName}} invited you to evaluate "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}" for The Unjournal`, + message: evaluator.emailTemplate.message, + include: { + users: { + invitor: evaluator.invitedBy, + }, + pubs: { + submission: pubId, + }, + }, + extra: { + accept_link: `Accept`, + decline_link: `Decline`, + info_link: `More Information`, + }, + }, + { jobKey, runAt } + ); +}; + +export const sendRequestedInfoNotification = ( + instanceId: string, + instanceConfig: InstanceConfig, + pubId: string, + evaluator: EvaluatorWithInvite +) => { + return client.sendEmail(instanceId, { + to: { + userId: evaluator.invitedBy, + }, + subject: `[Unjournal] More Information Request for "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}"`, + message: `

An invited evaluator, {{users.evaluator.firstName}} {{users.evaluator.lastName}}, for "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}", has requested more information. You may contact them at {{users.evaluator.email}}.

+${notificationFooter}`, + include: { + pubs: { + submission: pubId, + }, + users: { + evaluator: evaluator.userId, + }, + }, + }); +}; + +export const sendAcceptedNotificationEmail = ( + instanceId: string, + instanceConfig: InstanceConfig, + pubId: string, + evaluator: EvaluatorWithInvite +) => { + return client.sendEmail(instanceId, { + to: { + userId: evaluator.invitedBy, + }, + subject: `[Unjournal] Accepted evaluation for "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}"`, + message: `

An invited evaluator, {{users.evaluator.firstName}} {{users.evaluator.lastName}}, has agreed to evaluate "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}". You may review the status of this and other invitations on the {{extra.manage_link}}.

+${notificationFooter}`, + include: { + pubs: { + submission: pubId, + }, + users: { + evaluator: evaluator.userId, + }, + }, + extra: { + manage_link: `Invite Evaluators page`, + }, + }); +}; + +export const sendDeclinedNotificationEmail = async ( + instanceId: string, + instanceConfig: InstanceConfig, + pubId: string, + evaluator: EvaluatorWithInvite +) => { + return client.sendEmail(instanceId, { + to: { + userId: evaluator.invitedBy, + }, + subject: `[Unjournal] Invited evaluator declines to evaluate "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}"`, + message: `

An invited evaluator, {{users.evaluator.firstName}} {{users.evaluator.lastName}}, has declined to evaluate "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}". You may review the status of this and other invitations on the {{extra.manage_link}}.

+${notificationFooter}`, + include: { + pubs: { + submission: pubId, + }, + users: { + evaluator: evaluator.userId, + }, + }, + extra: { + manage_link: `Invite Evaluators page`, + }, + }); +}; + +export const sendSubmittedNotificationEmail = async ( + instanceId: string, + instanceConfig: InstanceConfig, + pubId: string, + evaluator: EvaluatorWhoEvaluated +) => { + return client.sendEmail(instanceId, { + to: { + userId: evaluator.invitedBy, + }, + subject: `[Unjournal] Evaluation submitted for "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}"`, + message: `

An evaluator, {{users.evaluator.firstName}} {{users.evaluator.lastName}}, has submitted an evaluation for "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}". The submitted evaluation Pub can be viewed here.

+

You may review the status of this and other invitations on the {{extra.manage_link}}.

+${notificationFooter}`, + include: { + pubs: { + submission: pubId, + }, + users: { + evaluator: evaluator.userId, + }, + }, + extra: { + manage_link: `Invite Evaluators page`, + }, + }); +}; + +// sent to the evaluator + +/** + * + * Sends an email to the evaluator with the invitation to evaluate the pub. + * @param instanceId + * @param pubId + * @param evaluator + * @returns Promise that resolves to the result of the email send operation. + */ export const sendInviteEmail = async ( instanceId: string, pubId: string, @@ -166,7 +334,14 @@ export const sendInviteEmail = async ( }); }; -export const scheduleReminderEmail = async ( +/** + * Sends an email to the evaluator as a reminder to accept the invitation to evaluate the pub. + * @param instanceId + * @param instanceConfig + * @param pubId + * @param evaluator + */ +export const scheduleInvitationReminderEmail = async ( instanceId: string, instanceConfig: InstanceConfig, pubId: string, @@ -202,6 +377,13 @@ export const scheduleReminderEmail = async ( ); }; +/** + * Cancels the scheduled reminder email for the evaluator to accept the invitation to evaluate the pub. + * @param instanceId + * @param pubId + * @param evaluator + * @returns + */ export const unscheduleReminderEmail = ( instanceId: string, pubId: string, @@ -211,21 +393,21 @@ export const unscheduleReminderEmail = ( return client.unscheduleEmail(instanceId, jobKey); }; +/** + * Sends an email to the evaluator to inform them that their invitation to evaluate the pub has been accepted. + * @param instanceId + * @param instanceConfig + * @param pubId + * @param evaluator + * @returns + */ export const sendAcceptedEmail = async ( instanceId: string, instanceConfig: InstanceConfig, pubId: string, evaluator: EvaluatorWhoAccepted ) => { - const deadline = evaluator.deadline - ? new Date(evaluator.deadline) - : calculateDeadline( - { - deadlineLength: instanceConfig.deadlineLength, - deadlineUnit: instanceConfig.deadlineUnit, - }, - new Date(evaluator.acceptedAt) - ); + const deadline = getDeadline(instanceConfig, evaluator); await client.sendEmail(instanceId, { to: { userId: evaluator.userId, @@ -261,108 +443,244 @@ export const sendAcceptedEmail = async ( }); }; -export const sendRequestedInfoNotification = ( +// export const sendPromptEvalBonusReminderEmail = () => {}; +// export const sendFinalPromptEvalBonusReminderEmail = () => {}; +// export const sendEvalutaionReminderEmail = () => {}; +// export const sendFinalEvaluationReminderEmail = () => {}; +// export const sendFollowUpToEvaluationReminderEmail = () => {}; +// export const sendNoticeOfNoSubmitEmail = () => {}; + +// Send prompt evaluation bonus reminder email +export const sendPromptEvalBonusReminderEmail = async ( instanceId: string, instanceConfig: InstanceConfig, pubId: string, - evaluator: EvaluatorWithInvite + evaluator: EvaluatorWhoAccepted ) => { + const deadline = getDeadline(instanceConfig, evaluator); + const reminderDeadline = new Date(deadline.getTime() - 14 * (1000 * 60 * 60 * 24)); return client.sendEmail(instanceId, { to: { - userId: evaluator.invitedBy, + userId: evaluator.userId, }, - subject: `[Unjournal] More Information Request for "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}"`, - message: `

An invited evaluator, {{users.evaluator.firstName}} {{users.evaluator.lastName}}, for "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}", has requested more information. You may contact them at {{users.evaluator.email}}.

-${notificationFooter}`, + subject: `[Unjournal] Reminder to evaluate "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}" for prompt evaluation bonus`, + message: `

Hi {{user.firstName}},

+

Thanks again for agreeing to evaluate "{{pubs.submission.values["${ + instanceConfig.titleFieldSlug + }"]}}" for The Unjournal.

+

This note is a reminder to submit your evaluation by ${new Date( + reminderDeadline.getTime() - 14 * (1000 * 60 * 60 * 24) + ).toLocaleDateString()} to receive a $100 “prompt evaluation bonus,” in addition to your baseline compensation. Please note that after ${new Date( + deadline.getTime() + ).toLocaleDateString()} we will consider re-assigning the evaluation, and later submissions may not be eligible for the full baseline compensation.

+

Please submit your evaluation and rating, as well as any specific considerations, using this evaluation form. The form includes instructions and information about the paper/project.

+

If you have any questions, do not hesitate to reach out to me at {{users.invitor.email}}.

+

Once your evaluation has been submitted and reviewed, we will follow up with details about payment and next steps.

+

Thanks and best wishes,

+

{{users.invitor.firstName}} {{users.invitor.lastName}}

+

Unjournal.org

`, include: { pubs: { submission: pubId, }, users: { - evaluator: evaluator.userId, + invitor: evaluator.invitedBy, }, }, + extra: { + evaluate_link: `{{instance.actions.evaluate}}?instanceId={{instance.id}}&pubId={{pubs.submission.id}}&token={{user.token}}`, + }, }); }; -export const sendAcceptedNotificationEmail = ( +// Send final prompt evaluation bonus reminder email +export const sendFinalPromptEvalBonusReminderEmail = async ( instanceId: string, instanceConfig: InstanceConfig, pubId: string, - evaluator: EvaluatorWithInvite + evaluator: EvaluatorWhoAccepted ) => { + const deadline = getDeadline(instanceConfig, evaluator); + const reminderDeadline = new Date(deadline.getTime() - 14 * (1000 * 60 * 60 * 24)); return client.sendEmail(instanceId, { to: { - userId: evaluator.invitedBy, + userId: evaluator.userId, }, - subject: `[Unjournal] Accepted evaluation for "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}"`, - message: `

An invited evaluator, {{users.evaluator.firstName}} {{users.evaluator.lastName}}, has agreed to evaluate "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}". You may review the status of this and other invitations on the {{extra.manage_link}}.

-${notificationFooter}`, + subject: `[Unjournal] Final Reminder: Submit evaluation for prompt bonus "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}"`, + message: `

Hi {{user.firstName}},

+

This is a final reminder to submit your evaluation for "{{pubs.submission.values["${ + instanceConfig.titleFieldSlug + }"]}}" by the deadline ${new Date( + reminderDeadline.getTime() + ).toLocaleDateString()} to receive the $100 “prompt evaluation bonus.”

+

If you haven't already, please submit your evaluation and rating, as well as any specific considerations, using this evaluation form. The form includes instructions and information about the paper/project.

+

If you have any questions, do not hesitate to reach out to me at {{users.invitor.email}}.

+

Once your evaluation has been submitted and reviewed, we will follow up with details about payment and next steps.

+

Thanks and best wishes,

+

{{users.invitor.firstName}} {{users.invitor.lastName}}

+

Unjournal.org

`, include: { pubs: { submission: pubId, }, users: { - evaluator: evaluator.userId, + invitor: evaluator.invitedBy, }, }, extra: { - manage_link: `Invite Evaluators page`, + evaluate_link: `{{instance.actions.evaluate}}?instanceId={{instance.id}}&pubId={{pubs.submission.id}}&token={{user.token}}`, }, }); }; -export const sendDeclinedNotificationEmail = async ( +// Send evaluation reminder email +export const sendEvaluationReminderEmail = async ( instanceId: string, instanceConfig: InstanceConfig, pubId: string, - evaluator: EvaluatorWithInvite + evaluator: EvaluatorWhoAccepted ) => { + // Calculate the deadline for the reminder email + const deadline = getDeadline(instanceConfig, evaluator); + return client.sendEmail(instanceId, { to: { - userId: evaluator.invitedBy, + userId: evaluator.userId, }, - subject: `[Unjournal] Invited evaluator declines to evaluate "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}"`, - message: `

An invited evaluator, {{users.evaluator.firstName}} {{users.evaluator.lastName}}, has declined to evaluate "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}". You may review the status of this and other invitations on the {{extra.manage_link}}.

-${notificationFooter}`, + subject: `[Unjournal] Reminder to evaluate "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}" by next week`, + message: `

Hi {{user.firstName}},

+

Thank you again for agreeing to evaluate "{{pubs.submission.values["${ + instanceConfig.titleFieldSlug + }"]}}" for The Unjournal.

+

This note is a reminder that your evaluation should be submitted by ${new Date( + deadline.getTime() + ).toLocaleDateString()} (next week). Please note that after that date we will consider re-assigning the evaluation, and later submissions may not be eligible for the full baseline compensation.

+

Please submit your evaluation and rating, as well as any specific considerations, using this evaluation form. The form includes instructions and information about the paper/project.

+

If you have any questions, do not hesitate to reach out to me at {{users.invitor.email}}.

+

Once your evaluation has been submitted and reviewed, we will follow up with details about payment and next steps.

+

Thanks and best wishes,

+

{{users.invitor.firstName}} {{users.invitor.lastName}}

+

Unjournal.org

`, include: { pubs: { submission: pubId, }, users: { - evaluator: evaluator.userId, + invitor: evaluator.invitedBy, }, }, extra: { - manage_link: `Invite Evaluators page`, + evaluate_link: `{{instance.actions.evaluate}}?instanceId={{instance.id}}&pubId={{pubs.submission.id}}&token={{user.token}}`, }, }); }; -export const sendSubmittedNotificationEmail = async ( +// Send final evaluation reminder email +export const sendFinalEvaluationReminderEmail = async ( instanceId: string, instanceConfig: InstanceConfig, pubId: string, - evaluator: EvaluatorWhoEvaluated + evaluator: EvaluatorWhoAccepted ) => { return client.sendEmail(instanceId, { to: { - userId: evaluator.invitedBy, + userId: evaluator.userId, }, - subject: `[Unjournal] Evaluation submitted for "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}"`, - message: `

An evaluator, {{users.evaluator.firstName}} {{users.evaluator.lastName}}, has submitted an evaluation for "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}". The submitted evaluation Pub can be viewed here.

-

You may review the status of this and other invitations on the {{extra.manage_link}}.

-${notificationFooter}`, + subject: `[Unjournal] Final Reminder: Evaluation due tomorrow`, + message: `

Hi {{user.firstName}},

+

This note is a final reminder that your evaluation for "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}" is due tomorrow. Please make sure to submit your evaluation by the deadline.

+

If you haven't already, please submit your evaluation and rating, as well as any specific considerations, using this evaluation form. The form includes instructions and information about the paper/project.

+

If you have any questions, do not hesitate to reach out to me at {{users.invitor.email}}.

+

Once your evaluation has been submitted and reviewed, we will follow up with details about payment and next steps.

+

Thanks and best wishes,

+

{{users.invitor.firstName}} {{users.invitor.lastName}}

+

Unjournal.org

`, include: { pubs: { submission: pubId, }, users: { - evaluator: evaluator.userId, + invitor: evaluator.invitedBy, }, }, extra: { - manage_link: `Invite Evaluators page`, + evaluate_link: `{{instance.actions.evaluate}}?instanceId={{instance.id}}&pubId={{pubs.submission.id}}&token={{user.token}}`, + }, + }); +}; + +// Send follow-up to evaluation reminder email +export const sendFollowUpToFinalEvaluationReminderEmail = async ( + instanceId: string, + instanceConfig: InstanceConfig, + pubId: string, + evaluator: EvaluatorWhoAccepted +) => { + const deadline = getDeadline(instanceConfig, evaluator); + return client.sendEmail(instanceId, { + to: { + userId: evaluator.userId, + }, + subject: `[Unjournal] Follow-up: Evaluation overdue, to be reassigned`, + message: `

Hi {{user.firstName}},

+

This note is a reminder that your evaluation for "{{pubs.submission.values["${ + instanceConfig.titleFieldSlug + }"]}}" is overdue. We are now planning to reassign the evaluation to another evaluator.

+

If you have completed the evaluation but forgot to submit it, please submit your evaluation and rating today using this evaluation form. If we don't hear from you by the end of ${new Date( + deadline.getTime() + ).toLocaleDateString()}, we will remove you from this assignment and you will no longer be eligible for compensation.

+

If you have any questions, do not hesitate to reach out to me at {{users.invitor.email}}.

+

Thanks and best wishes,

+

{{users.invitor.firstName}} {{users.invitor.lastName}}

+

Unjournal.org

`, + include: { + pubs: { + submission: pubId, + }, + users: { + invitor: evaluator.invitedBy, + }, + }, + extra: { + evaluate_link: `{{instance.actions.evaluate}}?instanceId={{instance.id}}&pubId={{pubs.submission.id}}&token={{user.token}}`, + }, + }); +}; + +// Send notice of no submit email +export const sendNoticeOfNoSubmitEmail = async ( + instanceId: string, + instanceConfig: InstanceConfig, + pubId: string, + evaluator: EvaluatorWhoAccepted +) => { + const deadline = getDeadline(instanceConfig, evaluator); + return client.sendEmail(instanceId, { + to: { + userId: evaluator.userId, + }, + subject: `[Unjournal] Evaluation not submitted for "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}"`, + message: `

Hi {{user.firstName}},

+

This is to inform you that you have not submitted an evaluation for "{{pubs.submission.values["${ + instanceConfig.titleFieldSlug + }"]}}", which was due on ${new Date(deadline.getTime()).toLocaleDateString()}.

+

If you have completed the evaluation but forgot to submit it, please submit your evaluation and rating today using this evaluation form. If we don't hear from you by the end of ${new Date( + deadline.getTime() + ).toLocaleDateString()}, we will remove you from this assignment and you will no longer be eligible for compensation.

+

If you have any questions, do not hesitate to reach out to me at {{users.invitor.email}}.

+

Thanks and best wishes,

+

{{users.invitor.firstName}} {{users.invitor.lastName}}

+

Unjournal.org

`, + include: { + pubs: { + submission: pubId, + }, + users: { + invitor: evaluator.invitedBy, + }, + }, + extra: { + evaluate_link: `{{instance.actions.evaluate}}?instanceId={{instance.id}}&pubId={{pubs.submission.id}}&token={{user.token}}`, }, }); }; From b63e0845d26d909380abc2698629609fe94c6491 Mon Sep 17 00:00:00 2001 From: qweliant Date: Tue, 13 Feb 2024 15:33:24 -0500 Subject: [PATCH 03/12] todo add runAt --- .../app/actions/evaluate/evaluate.tsx | 10 +--- .../app/actions/respond/actions.ts | 1 - integrations/evaluations/lib/emails.ts | 58 +------------------ 3 files changed, 3 insertions(+), 66 deletions(-) diff --git a/integrations/evaluations/app/actions/evaluate/evaluate.tsx b/integrations/evaluations/app/actions/evaluate/evaluate.tsx index baa483e46..8f997d51c 100644 --- a/integrations/evaluations/app/actions/evaluate/evaluate.tsx +++ b/integrations/evaluations/app/actions/evaluate/evaluate.tsx @@ -101,15 +101,7 @@ export function Evaluate(props: Props) { const submissionUrl = pub.values["unjournal:url"] as string; const submissionTitle = pub.values[props.instanceConfig.titleFieldSlug] as string; const submissionAbstract = pub.values["unjournal:description"] as string; - const deadline = props.evaluator.deadline - ? new Date(props.evaluator.deadline) - : calculateDeadline( - { - deadlineLength: props.instanceConfig.deadlineLength, - deadlineUnit: props.instanceConfig.deadlineUnit, - }, - new Date(props.evaluator.acceptedAt) - ); + const deadline = getDeadline(props.instanceConfig, props.evaluator); return ( <> diff --git a/integrations/evaluations/app/actions/respond/actions.ts b/integrations/evaluations/app/actions/respond/actions.ts index 357dc1ba6..99f2d0ca5 100644 --- a/integrations/evaluations/app/actions/respond/actions.ts +++ b/integrations/evaluations/app/actions/respond/actions.ts @@ -39,7 +39,6 @@ export const accept = async (instanceId: string, pubId: string) => { ...evaluator, status: "accepted", acceptedAt: new Date().toString(), - deadline: new Date(Date.now()), }; const deadline = calculateDeadline( { diff --git a/integrations/evaluations/lib/emails.ts b/integrations/evaluations/lib/emails.ts index f90ce9ad8..2b6fcc5d7 100644 --- a/integrations/evaluations/lib/emails.ts +++ b/integrations/evaluations/lib/emails.ts @@ -29,10 +29,7 @@ export function calculateDeadline( } } -export function getDeadline( - instanceConfig: InstanceConfig, - evaluator: EvaluatorWhoAccepted & EvaluatorWithInvite -): Date { +export function getDeadline(instanceConfig: InstanceConfig, evaluator: EvaluatorWhoAccepted): Date { return evaluator.deadline ? new Date(evaluator.deadline) : calculateDeadline( @@ -109,15 +106,7 @@ export const scheduleNoSubmitNotificationEmail = async ( evaluator: EvaluatorWhoAccepted ) => { const jobKey = makeNoSubmitJobKey(instanceId, pubId, evaluator); - const deadline = evaluator.deadline - ? new Date(evaluator.deadline) - : calculateDeadline( - { - deadlineLength: instanceConfig.deadlineLength, - deadlineUnit: instanceConfig.deadlineUnit, - }, - new Date(evaluator.acceptedAt) - ); + const deadline = getDeadline(instanceConfig, evaluator); const runAt = deadline; await client.scheduleEmail( @@ -155,42 +144,6 @@ export const unscheduleNoSubmitNotificationEmail = ( return client.unscheduleEmail(instanceId, jobKey); }; -export const scheduleReminderEmail = async ( - instanceId: string, - instanceConfig: InstanceConfig, - pubId: string, - evaluator: EvaluatorWithInvite -) => { - const jobKey = makeReminderJobKey(instanceId, pubId, evaluator); - const runAt = new Date(evaluator.invitedAt); - runAt.setMinutes(runAt.getMinutes() + DAYS_TO_REMIND_EVALUATOR * 24 * 60); - - await client.scheduleEmail( - instanceId, - { - to: { - userId: evaluator.userId, - }, - subject: `Reminder: {{users.invitor.firstName}} {{users.invitor.lastName}} invited you to evaluate "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}" for The Unjournal`, - message: evaluator.emailTemplate.message, - include: { - users: { - invitor: evaluator.invitedBy, - }, - pubs: { - submission: pubId, - }, - }, - extra: { - accept_link: `Accept`, - decline_link: `Decline`, - info_link: `More Information`, - }, - }, - { jobKey, runAt } - ); -}; - export const sendRequestedInfoNotification = ( instanceId: string, instanceConfig: InstanceConfig, @@ -443,13 +396,6 @@ export const sendAcceptedEmail = async ( }); }; -// export const sendPromptEvalBonusReminderEmail = () => {}; -// export const sendFinalPromptEvalBonusReminderEmail = () => {}; -// export const sendEvalutaionReminderEmail = () => {}; -// export const sendFinalEvaluationReminderEmail = () => {}; -// export const sendFollowUpToEvaluationReminderEmail = () => {}; -// export const sendNoticeOfNoSubmitEmail = () => {}; - // Send prompt evaluation bonus reminder email export const sendPromptEvalBonusReminderEmail = async ( instanceId: string, From 448ec8805d11b1b8311d86d5c3e29dd4bee5bb09 Mon Sep 17 00:00:00 2001 From: qweliant Date: Wed, 14 Feb 2024 14:08:02 -0500 Subject: [PATCH 04/12] update name --- integrations/evaluations/app/actions/respond/actions.ts | 7 ++++--- integrations/evaluations/lib/emails.ts | 3 +-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/integrations/evaluations/app/actions/respond/actions.ts b/integrations/evaluations/app/actions/respond/actions.ts index 99f2d0ca5..4d81b6794 100644 --- a/integrations/evaluations/app/actions/respond/actions.ts +++ b/integrations/evaluations/app/actions/respond/actions.ts @@ -8,7 +8,7 @@ import { sendDeclinedNotificationEmail, sendRequestedInfoNotification, unscheduleNoReplyNotificationEmail, - unscheduleReminderEmail, + unscheduleInvitationReminderEmail, calculateDeadline, } from "~/lib/emails"; import { getInstanceConfig, getInstanceState, setInstanceState } from "~/lib/instance"; @@ -50,13 +50,14 @@ export const accept = async (instanceId: string, pubId: string) => { evaluator.deadline = deadline; await setInstanceState(instanceId, pubId, instanceState); // Unschedule reminder email. - await unscheduleReminderEmail(instanceId, pubId, evaluator); + await unscheduleInvitationReminderEmail(instanceId, pubId, evaluator); // Unschedule no-reply notification email. await unscheduleNoReplyNotificationEmail(instanceId, pubId, evaluator); // Immediately send accepted notification email. await sendAcceptedNotificationEmail(instanceId, instanceConfig, pubId, evaluator); // Immediately send accepted email to evaluator. await sendAcceptedEmail(instanceId, instanceConfig, pubId, evaluator); + // Schedule no-submit notification email. await scheduleNoSubmitNotificationEmail(instanceId, instanceConfig, pubId, evaluator); return { success: true }; @@ -84,7 +85,7 @@ export const decline = async (instanceId: string, pubId: string) => { evaluator = instanceState[user.id] = { ...evaluator, status: "declined" }; await setInstanceState(instanceId, pubId, instanceState); // Unschedule reminder email. - await unscheduleReminderEmail(instanceId, pubId, evaluator); + await unscheduleInvitationReminderEmail(instanceId, pubId, evaluator); // Unschedule no-reply notification email. await unscheduleNoReplyNotificationEmail(instanceId, pubId, evaluator); // Immediately send declined notification email. diff --git a/integrations/evaluations/lib/emails.ts b/integrations/evaluations/lib/emails.ts index 2b6fcc5d7..48efe69ce 100644 --- a/integrations/evaluations/lib/emails.ts +++ b/integrations/evaluations/lib/emails.ts @@ -337,7 +337,7 @@ export const scheduleInvitationReminderEmail = async ( * @param evaluator * @returns */ -export const unscheduleReminderEmail = ( +export const unscheduleInvitationReminderEmail = ( instanceId: string, pubId: string, evaluator: EvaluatorWithInvite @@ -486,7 +486,6 @@ export const sendEvaluationReminderEmail = async ( pubId: string, evaluator: EvaluatorWhoAccepted ) => { - // Calculate the deadline for the reminder email const deadline = getDeadline(instanceConfig, evaluator); return client.sendEmail(instanceId, { From 5e26f646815766ffc6df2d1fb05547a852c2d217 Mon Sep 17 00:00:00 2001 From: qweliant Date: Wed, 14 Feb 2024 17:28:52 -0500 Subject: [PATCH 05/12] handle unscheduling --- .../app/actions/evaluate/actions.ts | 10 ++- .../app/actions/respond/actions.ts | 39 ++++++++++-- integrations/evaluations/lib/emails.ts | 63 +++++++++++++++++-- packages/sdk/src/client.ts | 1 - 4 files changed, 100 insertions(+), 13 deletions(-) diff --git a/integrations/evaluations/app/actions/evaluate/actions.ts b/integrations/evaluations/app/actions/evaluate/actions.ts index 9182a2802..1a71159e1 100644 --- a/integrations/evaluations/app/actions/evaluate/actions.ts +++ b/integrations/evaluations/app/actions/evaluate/actions.ts @@ -3,7 +3,11 @@ import { PubValues } from "@pubpub/sdk"; import { revalidatePath } from "next/cache"; import { expect } from "utils"; -import { sendSubmittedNotificationEmail, unscheduleNoSubmitNotificationEmail } from "~/lib/emails"; +import { + sendSubmittedNotificationEmail, + unscheduleAllDeadlineReminderEmails, + unscheduleNoSubmitNotificationEmail, +} from "~/lib/emails"; import { getInstanceConfig, getInstanceState, setInstanceState } from "~/lib/instance"; import { client } from "~/lib/pubpub"; import { cookie } from "~/lib/request"; @@ -37,8 +41,10 @@ export const submit = async (instanceId: string, pubId: string, values: PubValue evaluationPubId: pub.id, }; await setInstanceState(instanceId, pubId, instanceState); - // Unschedule no-submit notification email. + // Unschedule no-submit notification email for manager. await unscheduleNoSubmitNotificationEmail(instanceId, pubId, evaluator); + // unschedule dealine reminder emails. + await unscheduleAllDeadlineReminderEmails(instanceId, pubId, evaluator); // Immediately send submitted notification email. await sendSubmittedNotificationEmail(instanceId, instanceConfig, pubId, evaluator); revalidatePath("/"); diff --git a/integrations/evaluations/app/actions/respond/actions.ts b/integrations/evaluations/app/actions/respond/actions.ts index 4d81b6794..911934a75 100644 --- a/integrations/evaluations/app/actions/respond/actions.ts +++ b/integrations/evaluations/app/actions/respond/actions.ts @@ -10,6 +10,12 @@ import { unscheduleNoReplyNotificationEmail, unscheduleInvitationReminderEmail, calculateDeadline, + schedulePromptEvalBonusReminderEmail, + scheduleFinalPromptEvalBonusReminderEmail, + scheduleEvaluationReminderEmail, + scheduleFinalEvaluationReminderEmail, + scheduleFollowUpToFinalEvaluationReminderEmail, + sendNoticeOfNoSubmitEmail, } from "~/lib/emails"; import { getInstanceConfig, getInstanceState, setInstanceState } from "~/lib/instance"; import { cookie } from "~/lib/request"; @@ -49,17 +55,40 @@ export const accept = async (instanceId: string, pubId: string) => { ); evaluator.deadline = deadline; await setInstanceState(instanceId, pubId, instanceState); - // Unschedule reminder email. + // Unschedule reminder email to evaluator. await unscheduleInvitationReminderEmail(instanceId, pubId, evaluator); - // Unschedule no-reply notification email. + // Unschedule no-reply notification email to community manager. await unscheduleNoReplyNotificationEmail(instanceId, pubId, evaluator); - // Immediately send accepted notification email. + // Immediately send accepted notification email to community manager. await sendAcceptedNotificationEmail(instanceId, instanceConfig, pubId, evaluator); // Immediately send accepted email to evaluator. await sendAcceptedEmail(instanceId, instanceConfig, pubId, evaluator); - - // Schedule no-submit notification email. + // Schedule no-submit notification email to community manager. await scheduleNoSubmitNotificationEmail(instanceId, instanceConfig, pubId, evaluator); + + // schedule prompt evaluation email to evaluator. + await schedulePromptEvalBonusReminderEmail(instanceId, instanceConfig, pubId, evaluator); + //schedule final prompt eval email to evaluator + await scheduleFinalPromptEvalBonusReminderEmail( + instanceId, + instanceConfig, + pubId, + evaluator + ); + //schedule eval reminder email to evaluator + await scheduleEvaluationReminderEmail(instanceId, instanceConfig, pubId, evaluator); + //schedule final eval reminder email to evaluator + await scheduleFinalEvaluationReminderEmail(instanceId, instanceConfig, pubId, evaluator); + //schedule follow up to final eval reminder email to evaluator + await scheduleFollowUpToFinalEvaluationReminderEmail( + instanceId, + instanceConfig, + pubId, + evaluator + ); + // schedule no-submit notification email to evalutaor + await sendNoticeOfNoSubmitEmail(instanceId, instanceConfig, pubId, evaluator); + return { success: true }; } catch (error) { return { error: error.message }; diff --git a/integrations/evaluations/lib/emails.ts b/integrations/evaluations/lib/emails.ts index 48efe69ce..c47184571 100644 --- a/integrations/evaluations/lib/emails.ts +++ b/integrations/evaluations/lib/emails.ts @@ -5,6 +5,7 @@ import { EvaluatorWithInvite, InstanceConfig, } from "~/lib/types"; +import { SendEmailResponseBody } from "../../../packages/contracts"; const DAYS_TO_ACCEPT_INVITE = 10; const DAYS_TO_REMIND_EVALUATOR = 5; @@ -53,6 +54,42 @@ const makeNoReplyJobKey = (instanceId: string, pubId: string, evaluator: Evaluat const makeNoSubmitJobKey = (instanceId: string, pubId: string, evaluator: EvaluatorWithInvite) => `send-email-${instanceId}-${pubId}-${evaluator.userId}-no-submit`; +const makePromptEvalBonusReminderJobKey = ( + instanceId: string, + pubId: string, + evaluator: EvaluatorWhoAccepted +) => `send-email-${instanceId}-${pubId}-${evaluator.userId}-prompt-eval-bonus-reminder`; + +const makeFinalPromptEvalBonusReminderJobKey = ( + instanceId: string, + pubId: string, + evaluator: EvaluatorWhoAccepted +) => `send-email-${instanceId}-${pubId}-${evaluator.userId}-final-prompt-eval-bonus-reminder`; + +const makeEvalReminderJobKey = ( + instanceId: string, + pubId: string, + evaluator: EvaluatorWhoAccepted +) => `send-email-${instanceId}-${pubId}-${evaluator.userId}-eval-reminder`; + +const makeFinalEvalReminderJobKey = ( + instanceId: string, + pubId: string, + evaluator: EvaluatorWhoAccepted +) => `send-email-${instanceId}-${pubId}-${evaluator.userId}-final-eval-reminder`; + +const makeFollowUpToFinalEvalReminderJobKey = ( + instanceId: string, + pubId: string, + evaluator: EvaluatorWhoAccepted +) => `send-email-${instanceId}-${pubId}-${evaluator.userId}-follow-up-to-final-eval-reminder`; + +const makeNoticeOfNoSubmitJobKey = ( + instanceId: string, + pubId: string, + evaluator: EvaluatorWhoAccepted +) => `send-email-${instanceId}-${pubId}-${evaluator.userId}-no-submit-notice`; + // sent to the community manager export const scheduleNoReplyNotificationEmail = async ( instanceId: string, @@ -397,7 +434,7 @@ export const sendAcceptedEmail = async ( }; // Send prompt evaluation bonus reminder email -export const sendPromptEvalBonusReminderEmail = async ( +export const schedulePromptEvalBonusReminderEmail = async ( instanceId: string, instanceConfig: InstanceConfig, pubId: string, @@ -440,7 +477,7 @@ export const sendPromptEvalBonusReminderEmail = async ( }; // Send final prompt evaluation bonus reminder email -export const sendFinalPromptEvalBonusReminderEmail = async ( +export const scheduleFinalPromptEvalBonusReminderEmail = async ( instanceId: string, instanceConfig: InstanceConfig, pubId: string, @@ -480,7 +517,7 @@ export const sendFinalPromptEvalBonusReminderEmail = async ( }; // Send evaluation reminder email -export const sendEvaluationReminderEmail = async ( +export const scheduleEvaluationReminderEmail = async ( instanceId: string, instanceConfig: InstanceConfig, pubId: string, @@ -521,7 +558,7 @@ export const sendEvaluationReminderEmail = async ( }; // Send final evaluation reminder email -export const sendFinalEvaluationReminderEmail = async ( +export const scheduleFinalEvaluationReminderEmail = async ( instanceId: string, instanceConfig: InstanceConfig, pubId: string, @@ -555,7 +592,7 @@ export const sendFinalEvaluationReminderEmail = async ( }; // Send follow-up to evaluation reminder email -export const sendFollowUpToFinalEvaluationReminderEmail = async ( +export const scheduleFollowUpToFinalEvaluationReminderEmail = async ( instanceId: string, instanceConfig: InstanceConfig, pubId: string, @@ -629,3 +666,19 @@ export const sendNoticeOfNoSubmitEmail = async ( }, }); }; +// unschedules all the deadline reminder emails +export const unscheduleAllDeadlineReminderEmails = async ( + instanceId: string, + pubId: string, + evaluator: EvaluatorWhoAccepted +) => { + const jobKeys = [ + makePromptEvalBonusReminderJobKey(instanceId, pubId, evaluator), + makeFinalPromptEvalBonusReminderJobKey(instanceId, pubId, evaluator), + makeEvalReminderJobKey(instanceId, pubId, evaluator), + makeFinalEvalReminderJobKey(instanceId, pubId, evaluator), + makeFollowUpToFinalEvalReminderJobKey(instanceId, pubId, evaluator), + makeNoticeOfNoSubmitJobKey(instanceId, pubId, evaluator), + ]; + return Promise.all(jobKeys.map((jobKey) => client.unscheduleEmail(instanceId, jobKey))); +}; diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts index 622ac58e1..4049295f2 100644 --- a/packages/sdk/src/client.ts +++ b/packages/sdk/src/client.ts @@ -9,7 +9,6 @@ import { ScheduleEmailResponseBody, SendEmailRequestBody, SendEmailResponseBody, - UpdatePubRequestBody, UpdatePubResponseBody, User, api, From 86951904c9ea8236abd16174d2dd2ddf242bc68d Mon Sep 17 00:00:00 2001 From: qweliant Date: Thu, 15 Feb 2024 15:50:38 -0500 Subject: [PATCH 06/12] add run key --- integrations/evaluations/lib/emails.ts | 115 ++++++++++++++++++------- 1 file changed, 83 insertions(+), 32 deletions(-) diff --git a/integrations/evaluations/lib/emails.ts b/integrations/evaluations/lib/emails.ts index c47184571..9ba01f0a9 100644 --- a/integrations/evaluations/lib/emails.ts +++ b/integrations/evaluations/lib/emails.ts @@ -5,7 +5,6 @@ import { EvaluatorWithInvite, InstanceConfig, } from "~/lib/types"; -import { SendEmailResponseBody } from "../../../packages/contracts"; const DAYS_TO_ACCEPT_INVITE = 10; const DAYS_TO_REMIND_EVALUATOR = 5; @@ -287,8 +286,7 @@ ${notificationFooter}`, }); }; -// sent to the evaluator - +// emails sent to the evaluator /** * * Sends an email to the evaluator with the invitation to evaluate the pub. @@ -325,7 +323,7 @@ export const sendInviteEmail = async ( }; /** - * Sends an email to the evaluator as a reminder to accept the invitation to evaluate the pub. + * Schedules an email to the evaluator as a reminder to accept the invitation to evaluate the pub. * @param instanceId * @param instanceConfig * @param pubId @@ -433,7 +431,14 @@ export const sendAcceptedEmail = async ( }); }; -// Send prompt evaluation bonus reminder email +/** + * Schedules a reminder email to an evaluator for prompt evaluation bonus. + * @param instanceId - The ID of the instance. + * @param instanceConfig - The configuration of the instance. + * @param pubId - The ID of the publication. + * @param evaluator - The evaluator who accepted the evaluation. + * @returns A promise that resolves when the email is sent. + */ export const schedulePromptEvalBonusReminderEmail = async ( instanceId: string, instanceConfig: InstanceConfig, @@ -441,42 +446,51 @@ export const schedulePromptEvalBonusReminderEmail = async ( evaluator: EvaluatorWhoAccepted ) => { const deadline = getDeadline(instanceConfig, evaluator); - const reminderDeadline = new Date(deadline.getTime() - 14 * (1000 * 60 * 60 * 24)); - return client.sendEmail(instanceId, { - to: { - userId: evaluator.userId, - }, - subject: `[Unjournal] Reminder to evaluate "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}" for prompt evaluation bonus`, - message: `

Hi {{user.firstName}},

+ const reminderDeadline = new Date(deadline.getTime() - 21 * (1000 * 60 * 60 * 24)); + const jobKey = makePromptEvalBonusReminderJobKey(instanceId, pubId, evaluator); + const runAt = reminderDeadline; + return client.scheduleEmail( + instanceId, + { + to: { + userId: evaluator.userId, + }, + subject: `[Unjournal] Reminder to evaluate "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}" for prompt evaluation bonus`, + message: `

Hi {{user.firstName}},

Thanks again for agreeing to evaluate "{{pubs.submission.values["${ instanceConfig.titleFieldSlug }"]}}" for The Unjournal.

-

This note is a reminder to submit your evaluation by ${new Date( - reminderDeadline.getTime() - 14 * (1000 * 60 * 60 * 24) - ).toLocaleDateString()} to receive a $100 “prompt evaluation bonus,” in addition to your baseline compensation. Please note that after ${new Date( - deadline.getTime() - ).toLocaleDateString()} we will consider re-assigning the evaluation, and later submissions may not be eligible for the full baseline compensation.

+

This note is a reminder to submit your evaluation by ${reminderDeadline.toLocaleDateString()} to receive a $100 “prompt evaluation bonus,” in addition to your baseline compensation. Please note that after ${deadline.toLocaleDateString()} we will consider re-assigning the evaluation, and later submissions may not be eligible for the full baseline compensation.

Please submit your evaluation and rating, as well as any specific considerations, using this evaluation form. The form includes instructions and information about the paper/project.

If you have any questions, do not hesitate to reach out to me at {{users.invitor.email}}.

Once your evaluation has been submitted and reviewed, we will follow up with details about payment and next steps.

Thanks and best wishes,

{{users.invitor.firstName}} {{users.invitor.lastName}}

Unjournal.org

`, - include: { - pubs: { - submission: pubId, + include: { + pubs: { + submission: pubId, + }, + users: { + invitor: evaluator.invitedBy, + }, }, - users: { - invitor: evaluator.invitedBy, + extra: { + evaluate_link: `{{instance.actions.evaluate}}?instanceId={{instance.id}}&pubId={{pubs.submission.id}}&token={{user.token}}`, }, }, - extra: { - evaluate_link: `{{instance.actions.evaluate}}?instanceId={{instance.id}}&pubId={{pubs.submission.id}}&token={{user.token}}`, - }, - }); + { jobKey, runAt } + ); }; -// Send final prompt evaluation bonus reminder email +/** + * Schedules a final reminder email to an evaluator for prompt evaluation bonus. + * @param instanceId + * @param instanceConfig + * @param pubId + * @param evaluator + * @returns + */ export const scheduleFinalPromptEvalBonusReminderEmail = async ( instanceId: string, instanceConfig: InstanceConfig, @@ -516,7 +530,14 @@ export const scheduleFinalPromptEvalBonusReminderEmail = async ( }); }; -// Send evaluation reminder email +/** + * Schedules a reminder email to an evaluator to submit their evaluation. + * @param instanceId + * @param instanceConfig + * @param pubId + * @param evaluator + * @returns + */ export const scheduleEvaluationReminderEmail = async ( instanceId: string, instanceConfig: InstanceConfig, @@ -557,7 +578,14 @@ export const scheduleEvaluationReminderEmail = async ( }); }; -// Send final evaluation reminder email +/** + * Schedules a final reminder email to an evaluator to submit their evaluation. + * @param instanceId + * @param instanceConfig + * @param pubId + * @param evaluator + * @returns + */ export const scheduleFinalEvaluationReminderEmail = async ( instanceId: string, instanceConfig: InstanceConfig, @@ -591,7 +619,14 @@ export const scheduleFinalEvaluationReminderEmail = async ( }); }; -// Send follow-up to evaluation reminder email +/** + * Schedules a follow-up to evaluation reminder email to an evaluator. + * @param instanceId + * @param instanceConfig + * @param pubId + * @param evaluator + * @returns + */ export const scheduleFollowUpToFinalEvaluationReminderEmail = async ( instanceId: string, instanceConfig: InstanceConfig, @@ -629,7 +664,14 @@ export const scheduleFollowUpToFinalEvaluationReminderEmail = async ( }); }; -// Send notice of no submit email +/** + * Schedules a notice of no submit email to an evaluator. + * @param instanceId + * @param instanceConfig + * @param pubId + * @param evaluator + * @returns + */ export const sendNoticeOfNoSubmitEmail = async ( instanceId: string, instanceConfig: InstanceConfig, @@ -666,7 +708,16 @@ export const sendNoticeOfNoSubmitEmail = async ( }, }); }; -// unschedules all the deadline reminder emails + +/** + * Unschedules all the deadline reminder emails. + * `client.unscheduleEmail()` returns no-op for emails that have been + * sent, allowing us to call it without checking if an error + * @param instanceId + * @param pubId + * @param evaluator + * @returns + */ export const unscheduleAllDeadlineReminderEmails = async ( instanceId: string, pubId: string, From 073580f0ccd4596d61160a54c416d4634ab0cd17 Mon Sep 17 00:00:00 2001 From: qweliant Date: Thu, 15 Feb 2024 18:33:43 -0500 Subject: [PATCH 07/12] add job keys and runAts --- integrations/evaluations/lib/emails.ts | 191 ++++++++++++++----------- 1 file changed, 111 insertions(+), 80 deletions(-) diff --git a/integrations/evaluations/lib/emails.ts b/integrations/evaluations/lib/emails.ts index 9ba01f0a9..37ec5acaf 100644 --- a/integrations/evaluations/lib/emails.ts +++ b/integrations/evaluations/lib/emails.ts @@ -460,7 +460,9 @@ export const schedulePromptEvalBonusReminderEmail = async (

Thanks again for agreeing to evaluate "{{pubs.submission.values["${ instanceConfig.titleFieldSlug }"]}}" for The Unjournal.

-

This note is a reminder to submit your evaluation by ${reminderDeadline.toLocaleDateString()} to receive a $100 “prompt evaluation bonus,” in addition to your baseline compensation. Please note that after ${deadline.toLocaleDateString()} we will consider re-assigning the evaluation, and later submissions may not be eligible for the full baseline compensation.

+

This note is a reminder to submit your evaluation by ${reminderDeadline.toLocaleDateString()} to receive a $100 “prompt evaluation bonus,” in addition to your baseline compensation. Please note that after ${new Date( + deadline + ).toLocaleDateString()} we will consider re-assigning the evaluation, and later submissions may not be eligible for the full baseline compensation.

Please submit your evaluation and rating, as well as any specific considerations, using this evaluation form. The form includes instructions and information about the paper/project.

If you have any questions, do not hesitate to reach out to me at {{users.invitor.email}}.

Once your evaluation has been submitted and reviewed, we will follow up with details about payment and next steps.

@@ -499,35 +501,39 @@ export const scheduleFinalPromptEvalBonusReminderEmail = async ( ) => { const deadline = getDeadline(instanceConfig, evaluator); const reminderDeadline = new Date(deadline.getTime() - 14 * (1000 * 60 * 60 * 24)); - return client.sendEmail(instanceId, { - to: { - userId: evaluator.userId, - }, - subject: `[Unjournal] Final Reminder: Submit evaluation for prompt bonus "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}"`, - message: `

Hi {{user.firstName}},

+ const jobKey = makeFinalPromptEvalBonusReminderJobKey(instanceId, pubId, evaluator); + const runAt = reminderDeadline; + return client.scheduleEmail( + instanceId, + { + to: { + userId: evaluator.userId, + }, + subject: `[Unjournal] Final Reminder: Submit evaluation for prompt bonus "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}"`, + message: `

Hi {{user.firstName}},

This is a final reminder to submit your evaluation for "{{pubs.submission.values["${ instanceConfig.titleFieldSlug - }"]}}" by the deadline ${new Date( - reminderDeadline.getTime() - ).toLocaleDateString()} to receive the $100 “prompt evaluation bonus.”

+ }"]}}" by the deadline ${reminderDeadline.toLocaleDateString()} to receive the $100 “prompt evaluation bonus.”

If you haven't already, please submit your evaluation and rating, as well as any specific considerations, using this evaluation form. The form includes instructions and information about the paper/project.

If you have any questions, do not hesitate to reach out to me at {{users.invitor.email}}.

Once your evaluation has been submitted and reviewed, we will follow up with details about payment and next steps.

Thanks and best wishes,

{{users.invitor.firstName}} {{users.invitor.lastName}}

Unjournal.org

`, - include: { - pubs: { - submission: pubId, + include: { + pubs: { + submission: pubId, + }, + users: { + invitor: evaluator.invitedBy, + }, }, - users: { - invitor: evaluator.invitedBy, + extra: { + evaluate_link: `{{instance.actions.evaluate}}?instanceId={{instance.id}}&pubId={{pubs.submission.id}}&token={{user.token}}`, }, }, - extra: { - evaluate_link: `{{instance.actions.evaluate}}?instanceId={{instance.id}}&pubId={{pubs.submission.id}}&token={{user.token}}`, - }, - }); + { jobKey, runAt } + ); }; /** @@ -545,13 +551,17 @@ export const scheduleEvaluationReminderEmail = async ( evaluator: EvaluatorWhoAccepted ) => { const deadline = getDeadline(instanceConfig, evaluator); + const jobKey = makeEvalReminderJobKey(instanceId, pubId, evaluator); + const runAt = new Date(deadline.getTime() - 7 * (1000 * 60 * 60 * 24)); - return client.sendEmail(instanceId, { - to: { - userId: evaluator.userId, - }, - subject: `[Unjournal] Reminder to evaluate "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}" by next week`, - message: `

Hi {{user.firstName}},

+ return client.scheduleEmail( + instanceId, + { + to: { + userId: evaluator.userId, + }, + subject: `[Unjournal] Reminder to evaluate "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}" by next week`, + message: `

Hi {{user.firstName}},

Thank you again for agreeing to evaluate "{{pubs.submission.values["${ instanceConfig.titleFieldSlug }"]}}" for The Unjournal.

@@ -564,18 +574,20 @@ export const scheduleEvaluationReminderEmail = async (

Thanks and best wishes,

{{users.invitor.firstName}} {{users.invitor.lastName}}

Unjournal.org

`, - include: { - pubs: { - submission: pubId, + include: { + pubs: { + submission: pubId, + }, + users: { + invitor: evaluator.invitedBy, + }, }, - users: { - invitor: evaluator.invitedBy, + extra: { + evaluate_link: `{{instance.actions.evaluate}}?instanceId={{instance.id}}&pubId={{pubs.submission.id}}&token={{user.token}}`, }, }, - extra: { - evaluate_link: `{{instance.actions.evaluate}}?instanceId={{instance.id}}&pubId={{pubs.submission.id}}&token={{user.token}}`, - }, - }); + { jobKey, runAt } + ); }; /** @@ -592,12 +604,17 @@ export const scheduleFinalEvaluationReminderEmail = async ( pubId: string, evaluator: EvaluatorWhoAccepted ) => { - return client.sendEmail(instanceId, { - to: { - userId: evaluator.userId, - }, - subject: `[Unjournal] Final Reminder: Evaluation due tomorrow`, - message: `

Hi {{user.firstName}},

+ const deadline = getDeadline(instanceConfig, evaluator); + const jobKey = makeFinalEvalReminderJobKey(instanceId, pubId, evaluator); + const runAt = new Date(deadline.getTime() - 1 * (1000 * 60 * 60 * 24)); + return client.scheduleEmail( + instanceId, + { + to: { + userId: evaluator.userId, + }, + subject: `[Unjournal] Final Reminder: Evaluation due tomorrow`, + message: `

Hi {{user.firstName}},

This note is a final reminder that your evaluation for "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}" is due tomorrow. Please make sure to submit your evaluation by the deadline.

If you haven't already, please submit your evaluation and rating, as well as any specific considerations, using this evaluation form. The form includes instructions and information about the paper/project.

If you have any questions, do not hesitate to reach out to me at {{users.invitor.email}}.

@@ -605,18 +622,20 @@ export const scheduleFinalEvaluationReminderEmail = async (

Thanks and best wishes,

{{users.invitor.firstName}} {{users.invitor.lastName}}

Unjournal.org

`, - include: { - pubs: { - submission: pubId, + include: { + pubs: { + submission: pubId, + }, + users: { + invitor: evaluator.invitedBy, + }, }, - users: { - invitor: evaluator.invitedBy, + extra: { + evaluate_link: `{{instance.actions.evaluate}}?instanceId={{instance.id}}&pubId={{pubs.submission.id}}&token={{user.token}}`, }, }, - extra: { - evaluate_link: `{{instance.actions.evaluate}}?instanceId={{instance.id}}&pubId={{pubs.submission.id}}&token={{user.token}}`, - }, - }); + { jobKey, runAt } + ); }; /** @@ -634,34 +653,40 @@ export const scheduleFollowUpToFinalEvaluationReminderEmail = async ( evaluator: EvaluatorWhoAccepted ) => { const deadline = getDeadline(instanceConfig, evaluator); - return client.sendEmail(instanceId, { - to: { - userId: evaluator.userId, - }, - subject: `[Unjournal] Follow-up: Evaluation overdue, to be reassigned`, - message: `

Hi {{user.firstName}},

+ const jobKey = makeFollowUpToFinalEvalReminderJobKey(instanceId, pubId, evaluator); + const runAt = new Date(deadline.getTime() + 6 * (1000 * 60 * 60 * 24)); + return client.scheduleEmail( + instanceId, + { + to: { + userId: evaluator.userId, + }, + subject: `[Unjournal] Follow-up: Evaluation overdue, to be reassigned`, + message: `

Hi {{user.firstName}},

This note is a reminder that your evaluation for "{{pubs.submission.values["${ instanceConfig.titleFieldSlug }"]}}" is overdue. We are now planning to reassign the evaluation to another evaluator.

If you have completed the evaluation but forgot to submit it, please submit your evaluation and rating today using this evaluation form. If we don't hear from you by the end of ${new Date( - deadline.getTime() + deadline.getTime() + 7 * (1000 * 60 * 60 * 24) ).toLocaleDateString()}, we will remove you from this assignment and you will no longer be eligible for compensation.

If you have any questions, do not hesitate to reach out to me at {{users.invitor.email}}.

Thanks and best wishes,

{{users.invitor.firstName}} {{users.invitor.lastName}}

Unjournal.org

`, - include: { - pubs: { - submission: pubId, + include: { + pubs: { + submission: pubId, + }, + users: { + invitor: evaluator.invitedBy, + }, }, - users: { - invitor: evaluator.invitedBy, + extra: { + evaluate_link: `{{instance.actions.evaluate}}?instanceId={{instance.id}}&pubId={{pubs.submission.id}}&token={{user.token}}`, }, }, - extra: { - evaluate_link: `{{instance.actions.evaluate}}?instanceId={{instance.id}}&pubId={{pubs.submission.id}}&token={{user.token}}`, - }, - }); + { jobKey, runAt } + ); }; /** @@ -679,12 +704,16 @@ export const sendNoticeOfNoSubmitEmail = async ( evaluator: EvaluatorWhoAccepted ) => { const deadline = getDeadline(instanceConfig, evaluator); - return client.sendEmail(instanceId, { - to: { - userId: evaluator.userId, - }, - subject: `[Unjournal] Evaluation not submitted for "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}"`, - message: `

Hi {{user.firstName}},

+ const jobKey = makeNoticeOfNoSubmitJobKey(instanceId, pubId, evaluator); + const runAt = new Date(deadline.getTime() + 8 * (1000 * 60 * 60 * 24)); + return client.scheduleEmail( + instanceId, + { + to: { + userId: evaluator.userId, + }, + subject: `[Unjournal] Evaluation not submitted for "{{pubs.submission.values["${instanceConfig.titleFieldSlug}"]}}"`, + message: `

Hi {{user.firstName}},

This is to inform you that you have not submitted an evaluation for "{{pubs.submission.values["${ instanceConfig.titleFieldSlug }"]}}", which was due on ${new Date(deadline.getTime()).toLocaleDateString()}.

@@ -695,18 +724,20 @@ export const sendNoticeOfNoSubmitEmail = async (

Thanks and best wishes,

{{users.invitor.firstName}} {{users.invitor.lastName}}

Unjournal.org

`, - include: { - pubs: { - submission: pubId, + include: { + pubs: { + submission: pubId, + }, + users: { + invitor: evaluator.invitedBy, + }, }, - users: { - invitor: evaluator.invitedBy, + extra: { + evaluate_link: `{{instance.actions.evaluate}}?instanceId={{instance.id}}&pubId={{pubs.submission.id}}&token={{user.token}}`, }, }, - extra: { - evaluate_link: `{{instance.actions.evaluate}}?instanceId={{instance.id}}&pubId={{pubs.submission.id}}&token={{user.token}}`, - }, - }); + { jobKey, runAt } + ); }; /** From fad7445f3e3ba43fb99868ca10b0d4d3a7af8593 Mon Sep 17 00:00:00 2001 From: qweliant Date: Tue, 20 Feb 2024 09:52:30 -0500 Subject: [PATCH 08/12] test emails --- integrations/evaluations/lib/emails.ts | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/integrations/evaluations/lib/emails.ts b/integrations/evaluations/lib/emails.ts index 37ec5acaf..d7e28acd2 100644 --- a/integrations/evaluations/lib/emails.ts +++ b/integrations/evaluations/lib/emails.ts @@ -448,7 +448,8 @@ export const schedulePromptEvalBonusReminderEmail = async ( const deadline = getDeadline(instanceConfig, evaluator); const reminderDeadline = new Date(deadline.getTime() - 21 * (1000 * 60 * 60 * 24)); const jobKey = makePromptEvalBonusReminderJobKey(instanceId, pubId, evaluator); - const runAt = reminderDeadline; + // const runAt = reminderDeadline; + const runAt = new Date(Date.now()); return client.scheduleEmail( instanceId, { @@ -502,7 +503,9 @@ export const scheduleFinalPromptEvalBonusReminderEmail = async ( const deadline = getDeadline(instanceConfig, evaluator); const reminderDeadline = new Date(deadline.getTime() - 14 * (1000 * 60 * 60 * 24)); const jobKey = makeFinalPromptEvalBonusReminderJobKey(instanceId, pubId, evaluator); - const runAt = reminderDeadline; + // const runAt = reminderDeadline; + const runAt = new Date(Date.now()); + return client.scheduleEmail( instanceId, { @@ -552,7 +555,8 @@ export const scheduleEvaluationReminderEmail = async ( ) => { const deadline = getDeadline(instanceConfig, evaluator); const jobKey = makeEvalReminderJobKey(instanceId, pubId, evaluator); - const runAt = new Date(deadline.getTime() - 7 * (1000 * 60 * 60 * 24)); + // const runAt = new Date(deadline.getTime() - 7 * (1000 * 60 * 60 * 24)); + const runAt = new Date(Date.now()); return client.scheduleEmail( instanceId, @@ -606,7 +610,9 @@ export const scheduleFinalEvaluationReminderEmail = async ( ) => { const deadline = getDeadline(instanceConfig, evaluator); const jobKey = makeFinalEvalReminderJobKey(instanceId, pubId, evaluator); - const runAt = new Date(deadline.getTime() - 1 * (1000 * 60 * 60 * 24)); + // const runAt = new Date(deadline.getTime() - 1 * (1000 * 60 * 60 * 24)); + const runAt = new Date(Date.now()); + return client.scheduleEmail( instanceId, { @@ -654,7 +660,9 @@ export const scheduleFollowUpToFinalEvaluationReminderEmail = async ( ) => { const deadline = getDeadline(instanceConfig, evaluator); const jobKey = makeFollowUpToFinalEvalReminderJobKey(instanceId, pubId, evaluator); - const runAt = new Date(deadline.getTime() + 6 * (1000 * 60 * 60 * 24)); + // const runAt = new Date(deadline.getTime() + 6 * (1000 * 60 * 60 * 24)); + const runAt = new Date(Date.now()); + return client.scheduleEmail( instanceId, { @@ -705,7 +713,9 @@ export const sendNoticeOfNoSubmitEmail = async ( ) => { const deadline = getDeadline(instanceConfig, evaluator); const jobKey = makeNoticeOfNoSubmitJobKey(instanceId, pubId, evaluator); - const runAt = new Date(deadline.getTime() + 8 * (1000 * 60 * 60 * 24)); + // const runAt = new Date(deadline.getTime() + 8 * (1000 * 60 * 60 * 24)); + const runAt = new Date(Date.now()); + return client.scheduleEmail( instanceId, { From c077d6a1d02a722d5bbca6db33243ed4ac7135ea Mon Sep 17 00:00:00 2001 From: qweliant Date: Tue, 27 Feb 2024 15:40:10 -0500 Subject: [PATCH 09/12] rmv date.now add JSdoc for emails bc it helps diferentiate them --- integrations/evaluations/lib/emails.ts | 79 ++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 11 deletions(-) diff --git a/integrations/evaluations/lib/emails.ts b/integrations/evaluations/lib/emails.ts index d7e28acd2..c103faabb 100644 --- a/integrations/evaluations/lib/emails.ts +++ b/integrations/evaluations/lib/emails.ts @@ -89,7 +89,15 @@ const makeNoticeOfNoSubmitJobKey = ( evaluator: EvaluatorWhoAccepted ) => `send-email-${instanceId}-${pubId}-${evaluator.userId}-no-submit-notice`; -// sent to the community manager +// emails sent to the evaluation manager +/** + * Schedules an email to the evaluation manager to notify them that an invited evaluator has not responded to the invitation. + * @param instanceId + * @param instanceConfig + * @param pubId + * @param evaluator + * @returns + */ export const scheduleNoReplyNotificationEmail = async ( instanceId: string, instanceConfig: InstanceConfig, @@ -126,6 +134,13 @@ ${notificationFooter}`, ); }; +/** + * Unschedules the no reply notification email for the evaluation manager. + * @param instanceId + * @param pubId + * @param evaluator + * @returns + */ export const unscheduleNoReplyNotificationEmail = ( instanceId: string, pubId: string, @@ -135,6 +150,14 @@ export const unscheduleNoReplyNotificationEmail = ( return client.unscheduleEmail(instanceId, jobKey); }; +/** + * Schedules an email to the evaluation manager to notify them that an evaluator has not submitted their evaluation. + * @param instanceId + * @param instanceConfig + * @param pubId + * @param evaluator + * @returns + */ export const scheduleNoSubmitNotificationEmail = async ( instanceId: string, instanceConfig: InstanceConfig, @@ -171,6 +194,13 @@ ${notificationFooter}`, ); }; +/** + * Unschedules the no submit notification email for the evaluation manager. + * @param instanceId + * @param pubId + * @param evaluator + * @returns + */ export const unscheduleNoSubmitNotificationEmail = ( instanceId: string, pubId: string, @@ -180,6 +210,14 @@ export const unscheduleNoSubmitNotificationEmail = ( return client.unscheduleEmail(instanceId, jobKey); }; +/** + * Sends an email to the evaluation manager to notify them that an evaluator has requested more information. + * @param instanceId + * @param instanceConfig + * @param pubId + * @param evaluator + * @returns + */ export const sendRequestedInfoNotification = ( instanceId: string, instanceConfig: InstanceConfig, @@ -204,6 +242,14 @@ ${notificationFooter}`, }); }; +/** + * Sends an email to the evaluation manager to notify them that an evaluator has accepted the invitation to evaluate the pub. + * @param instanceId + * @param instanceConfig + * @param pubId + * @param evaluator + * @returns + */ export const sendAcceptedNotificationEmail = ( instanceId: string, instanceConfig: InstanceConfig, @@ -231,6 +277,14 @@ ${notificationFooter}`, }); }; +/** + * Sends an email to the evaluation manager to notify them that an evaluator has declined the invitation to evaluate the pub. + * @param instanceId + * @param instanceConfig + * @param pubId + * @param evaluator + * @returns + */ export const sendDeclinedNotificationEmail = async ( instanceId: string, instanceConfig: InstanceConfig, @@ -258,6 +312,14 @@ ${notificationFooter}`, }); }; +/** + * Sends an email to the evaluation manager to notify them that an evaluator has submitted their evaluation. + * @param instanceId + * @param instanceConfig + * @param pubId + * @param evaluator + * @returns + */ export const sendSubmittedNotificationEmail = async ( instanceId: string, instanceConfig: InstanceConfig, @@ -503,8 +565,7 @@ export const scheduleFinalPromptEvalBonusReminderEmail = async ( const deadline = getDeadline(instanceConfig, evaluator); const reminderDeadline = new Date(deadline.getTime() - 14 * (1000 * 60 * 60 * 24)); const jobKey = makeFinalPromptEvalBonusReminderJobKey(instanceId, pubId, evaluator); - // const runAt = reminderDeadline; - const runAt = new Date(Date.now()); + const runAt = reminderDeadline; return client.scheduleEmail( instanceId, @@ -555,8 +616,7 @@ export const scheduleEvaluationReminderEmail = async ( ) => { const deadline = getDeadline(instanceConfig, evaluator); const jobKey = makeEvalReminderJobKey(instanceId, pubId, evaluator); - // const runAt = new Date(deadline.getTime() - 7 * (1000 * 60 * 60 * 24)); - const runAt = new Date(Date.now()); + const runAt = new Date(deadline.getTime() - 7 * (1000 * 60 * 60 * 24)); return client.scheduleEmail( instanceId, @@ -610,8 +670,7 @@ export const scheduleFinalEvaluationReminderEmail = async ( ) => { const deadline = getDeadline(instanceConfig, evaluator); const jobKey = makeFinalEvalReminderJobKey(instanceId, pubId, evaluator); - // const runAt = new Date(deadline.getTime() - 1 * (1000 * 60 * 60 * 24)); - const runAt = new Date(Date.now()); + const runAt = new Date(deadline.getTime() - 1 * (1000 * 60 * 60 * 24)); return client.scheduleEmail( instanceId, @@ -660,8 +719,7 @@ export const scheduleFollowUpToFinalEvaluationReminderEmail = async ( ) => { const deadline = getDeadline(instanceConfig, evaluator); const jobKey = makeFollowUpToFinalEvalReminderJobKey(instanceId, pubId, evaluator); - // const runAt = new Date(deadline.getTime() + 6 * (1000 * 60 * 60 * 24)); - const runAt = new Date(Date.now()); + const runAt = new Date(deadline.getTime() + 6 * (1000 * 60 * 60 * 24)); return client.scheduleEmail( instanceId, @@ -713,8 +771,7 @@ export const sendNoticeOfNoSubmitEmail = async ( ) => { const deadline = getDeadline(instanceConfig, evaluator); const jobKey = makeNoticeOfNoSubmitJobKey(instanceId, pubId, evaluator); - // const runAt = new Date(deadline.getTime() + 8 * (1000 * 60 * 60 * 24)); - const runAt = new Date(Date.now()); + const runAt = new Date(deadline.getTime() + 8 * (1000 * 60 * 60 * 24)); return client.scheduleEmail( instanceId, From dace64e258856f6de01b4d3a04af662f7d2fe977 Mon Sep 17 00:00:00 2001 From: qweliant Date: Tue, 27 Feb 2024 16:55:42 -0500 Subject: [PATCH 10/12] unschedule on evaluator --- .../evaluations/app/actions/manage/actions.ts | 13 +++++++++--- integrations/evaluations/lib/emails.ts | 20 +++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/integrations/evaluations/app/actions/manage/actions.ts b/integrations/evaluations/app/actions/manage/actions.ts index 08978cd16..79f5624c0 100644 --- a/integrations/evaluations/app/actions/manage/actions.ts +++ b/integrations/evaluations/app/actions/manage/actions.ts @@ -6,12 +6,14 @@ import { expect } from "utils"; import { getInstanceConfig, getInstanceState, setInstanceState } from "~/lib/instance"; import { client } from "~/lib/pubpub"; import { cookie } from "~/lib/request"; -import { isInvited } from "~/lib/types"; +import { EvaluatorWhoAccepted, EvaluatorWithInvite, isInvited } from "~/lib/types"; import { - scheduleNoReplyNotificationEmail, scheduleInvitationReminderEmail, + scheduleNoReplyNotificationEmail, sendInviteEmail, -} from "../../../lib/emails"; + unscheduleAllDeadlineReminderEmails, + unscheduleAllManagerEmails, +} from "~/lib/emails"; import { InviteFormEvaluator } from "./types"; export const save = async ( @@ -118,6 +120,10 @@ export const remove = async (instanceId: string, pubId: string, userId: string) const evaluation = pub.children.find( (child) => child.values[instanceConfig.evaluatorFieldSlug] === userId ); + await unscheduleAllDeadlineReminderEmails(instanceId, pubId, { + userId, + } as EvaluatorWhoAccepted); + await unscheduleAllManagerEmails(instanceId, pubId, { userId } as EvaluatorWithInvite); // TODO: When an evaluator is removed, we should unschedule reminder // email and notification email(s). if (evaluation !== undefined) { @@ -127,6 +133,7 @@ export const remove = async (instanceId: string, pubId: string, userId: string) delete instanceState[userId]; await setInstanceState(instanceId, pubId, instanceState); } + return { success: true }; } catch (error) { return { error: error.message }; diff --git a/integrations/evaluations/lib/emails.ts b/integrations/evaluations/lib/emails.ts index c103faabb..cd4d6a4f6 100644 --- a/integrations/evaluations/lib/emails.ts +++ b/integrations/evaluations/lib/emails.ts @@ -831,3 +831,23 @@ export const unscheduleAllDeadlineReminderEmails = async ( ]; return Promise.all(jobKeys.map((jobKey) => client.unscheduleEmail(instanceId, jobKey))); }; + +/** + * Unschedules all emails. + * @param instanceId + * @param pubId + * @param evaluator + * @returns + */ +export const unscheduleAllManagerEmails = async ( + instanceId: string, + pubId: string, + evaluator: EvaluatorWithInvite +) => { + const jobKeys = [ + makeReminderJobKey(instanceId, pubId, evaluator), + makeNoReplyJobKey(instanceId, pubId, evaluator), + makeNoSubmitJobKey(instanceId, pubId, evaluator), + ]; + return Promise.all(jobKeys.map((jobKey) => client.unscheduleEmail(instanceId, jobKey))); +} \ No newline at end of file From bfacd4199e2a07ee48638cf6c906f41a40ec43af Mon Sep 17 00:00:00 2001 From: qweliant Date: Wed, 28 Feb 2024 09:52:41 -0500 Subject: [PATCH 11/12] rmv test, use instanace stae for evaluator --- .../evaluations/app/actions/manage/actions.ts | 22 +++++++++++++------ integrations/evaluations/lib/emails.ts | 8 +++---- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/integrations/evaluations/app/actions/manage/actions.ts b/integrations/evaluations/app/actions/manage/actions.ts index 79f5624c0..89d7bd141 100644 --- a/integrations/evaluations/app/actions/manage/actions.ts +++ b/integrations/evaluations/app/actions/manage/actions.ts @@ -6,7 +6,13 @@ import { expect } from "utils"; import { getInstanceConfig, getInstanceState, setInstanceState } from "~/lib/instance"; import { client } from "~/lib/pubpub"; import { cookie } from "~/lib/request"; -import { EvaluatorWhoAccepted, EvaluatorWithInvite, isInvited } from "~/lib/types"; +import { + EvaluatorWhoAccepted, + EvaluatorWithInvite, + assertHasAccepted, + assertIsInvited, + isInvited, +} from "~/lib/types"; import { scheduleInvitationReminderEmail, scheduleNoReplyNotificationEmail, @@ -120,16 +126,18 @@ export const remove = async (instanceId: string, pubId: string, userId: string) const evaluation = pub.children.find( (child) => child.values[instanceConfig.evaluatorFieldSlug] === userId ); - await unscheduleAllDeadlineReminderEmails(instanceId, pubId, { - userId, - } as EvaluatorWhoAccepted); - await unscheduleAllManagerEmails(instanceId, pubId, { userId } as EvaluatorWithInvite); - // TODO: When an evaluator is removed, we should unschedule reminder - // email and notification email(s). + if (evaluation !== undefined) { await client.deletePub(instanceId, evaluation.id); } if (instanceState !== undefined) { + let evaluator = expect( + instanceState[userId], + `User was not invited to evaluate pub ${pubId}` + ); + assertHasAccepted(evaluator); + await unscheduleAllDeadlineReminderEmails(instanceId, pubId, evaluator); + await unscheduleAllManagerEmails(instanceId, pubId, evaluator); delete instanceState[userId]; await setInstanceState(instanceId, pubId, instanceState); } diff --git a/integrations/evaluations/lib/emails.ts b/integrations/evaluations/lib/emails.ts index cd4d6a4f6..57d324301 100644 --- a/integrations/evaluations/lib/emails.ts +++ b/integrations/evaluations/lib/emails.ts @@ -510,8 +510,8 @@ export const schedulePromptEvalBonusReminderEmail = async ( const deadline = getDeadline(instanceConfig, evaluator); const reminderDeadline = new Date(deadline.getTime() - 21 * (1000 * 60 * 60 * 24)); const jobKey = makePromptEvalBonusReminderJobKey(instanceId, pubId, evaluator); - // const runAt = reminderDeadline; - const runAt = new Date(Date.now()); + const runAt = reminderDeadline; + return client.scheduleEmail( instanceId, { @@ -833,7 +833,7 @@ export const unscheduleAllDeadlineReminderEmails = async ( }; /** - * Unschedules all emails. + * Unschedules all emails. * @param instanceId * @param pubId * @param evaluator @@ -850,4 +850,4 @@ export const unscheduleAllManagerEmails = async ( makeNoSubmitJobKey(instanceId, pubId, evaluator), ]; return Promise.all(jobKeys.map((jobKey) => client.unscheduleEmail(instanceId, jobKey))); -} \ No newline at end of file +}; From d3d1b998f407f4f25c48646a4c32efb1d7d8f46e Mon Sep 17 00:00:00 2001 From: Qwelian D Tanner Date: Mon, 4 Mar 2024 18:07:49 -0500 Subject: [PATCH 12/12] this fixes an import bug (#253) Co-authored-by: qweliant --- integrations/evaluations/app/actions/evaluate/evaluate.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrations/evaluations/app/actions/evaluate/evaluate.tsx b/integrations/evaluations/app/actions/evaluate/evaluate.tsx index 8f997d51c..9c675f979 100644 --- a/integrations/evaluations/app/actions/evaluate/evaluate.tsx +++ b/integrations/evaluations/app/actions/evaluate/evaluate.tsx @@ -11,7 +11,7 @@ import { Process } from "~/lib/components/Process"; import { Research } from "~/lib/components/Research"; import { EvaluatorWhoAccepted, InstanceConfig } from "~/lib/types"; import { submit, upload } from "./actions"; -import { calculateDeadline } from "~/lib/emails"; +import { getDeadline } from "~/lib/emails"; import { useToast } from "ui/use-toast"; import { useLocalStorage } from "ui/hooks"; import { Form } from "ui/form";