diff --git a/apps/formbricks-com/components/dummyUI/AddNoCodeEventModalDummy.tsx b/apps/formbricks-com/components/dummyUI/AddNoCodeEventModalDummy.tsx index dbdb8d82ab..6b716e1d3c 100644 --- a/apps/formbricks-com/components/dummyUI/AddNoCodeEventModalDummy.tsx +++ b/apps/formbricks-com/components/dummyUI/AddNoCodeEventModalDummy.tsx @@ -11,7 +11,7 @@ import { SelectValue, } from "@formbricks/ui"; import { CursorArrowRaysIcon } from "@heroicons/react/24/solid"; -import Modal from "../shared/Modal"; +import { Modal } from "@formbricks/ui"; interface EventDetailModalProps { open: boolean; diff --git a/apps/formbricks-com/components/home/VideoWalkThrough.tsx b/apps/formbricks-com/components/home/VideoWalkThrough.tsx index 10259215e3..44df80ec94 100644 --- a/apps/formbricks-com/components/home/VideoWalkThrough.tsx +++ b/apps/formbricks-com/components/home/VideoWalkThrough.tsx @@ -1,5 +1,5 @@ import { ResponsiveVideo } from "@formbricks/ui"; -import Modal from "../shared/Modal"; +import { Modal } from "@formbricks/ui"; interface VideoWalkThroughProps { open: boolean; diff --git a/apps/formbricks-com/components/shared/Modal.tsx b/apps/formbricks-com/components/shared/Modal.tsx deleted file mode 100644 index 2e565bd7f6..0000000000 --- a/apps/formbricks-com/components/shared/Modal.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { Dialog, Transition } from "@headlessui/react"; -import { XMarkIcon } from "@heroicons/react/24/solid"; -import { Fragment } from "react"; -import clsx from "clsx"; - -type Modal = { - open: boolean; - setOpen: (v: boolean) => void; - children: React.ReactNode; - title?: string; - noPadding?: boolean; - closeOnOutsideClick?: boolean; -}; - -const Modal: React.FC = ({ - open, - setOpen, - children, - title, - noPadding, - closeOnOutsideClick = true, -}) => { - return ( - <> - - closeOnOutsideClick && setOpen(false)}> - -
- - -
-
- - -
- -
- {title &&

{title}

} - - {children} -
-
-
-
-
-
- - ); -}; - -export default Modal; diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionSettingsTab.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionSettingsTab.tsx index da87f38a60..0b6d95a87f 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionSettingsTab.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionSettingsTab.tsx @@ -1,6 +1,6 @@ "use client"; -import DeleteDialog from "@/components/shared/DeleteDialog"; +import { DeleteDialog } from "@formbricks/ui"; import type { NoCodeConfig } from "@formbricks/types/events"; import { Button, Input, Label } from "@formbricks/ui"; import { TrashIcon } from "@heroicons/react/24/outline"; diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/AddNoCodeActionModal.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/AddNoCodeActionModal.tsx index ab39bb7279..e1cc56dd89 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/AddNoCodeActionModal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/AddNoCodeActionModal.tsx @@ -1,6 +1,6 @@ "use client"; -import Modal from "@/components/shared/Modal"; +import { Modal } from "@formbricks/ui"; import { Button, Input, Label } from "@formbricks/ui"; import { CursorArrowRaysIcon } from "@heroicons/react/24/solid"; import { useState } from "react"; diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/actions.ts b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/actions.ts index fc739ff3e0..8182aafaea 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/actions.ts @@ -1,6 +1,6 @@ "use server"; -import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { authOptions } from "@formbricks/lib/authOptions"; import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; import { createActionClass, deleteActionClass, updateActionClass } from "@formbricks/lib/actionClass/service"; import { canUserAccessActionClass } from "@formbricks/lib/actionClass/auth"; diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/UploadAttributesModal.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/UploadAttributesModal.tsx index ec977ef6d8..6f5bc1fd09 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/UploadAttributesModal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/UploadAttributesModal.tsx @@ -1,4 +1,4 @@ -import Modal from "@/components/shared/Modal"; +import { Modal } from "@formbricks/ui"; interface UploadAttributesModalProps { open: boolean; diff --git a/apps/web/app/(app)/environments/[environmentId]/actions.ts b/apps/web/app/(app)/environments/[environmentId]/actions.ts index bd1091ffab..cd0101cea6 100644 --- a/apps/web/app/(app)/environments/[environmentId]/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/actions.ts @@ -1,6 +1,6 @@ "use server"; -import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { authOptions } from "@formbricks/lib/authOptions"; import { prisma } from "@formbricks/database"; import { SHORT_SURVEY_BASE_URL, SURVEY_BASE_URL } from "@formbricks/lib/constants"; import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; diff --git a/apps/web/app/(app)/environments/[environmentId]/components/AddProductModal.tsx b/apps/web/app/(app)/environments/[environmentId]/components/AddProductModal.tsx index 2a0dab6026..b8d2b5719f 100644 --- a/apps/web/app/(app)/environments/[environmentId]/components/AddProductModal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/components/AddProductModal.tsx @@ -1,7 +1,7 @@ "use client"; import { createProductAction } from "@/app/(app)/environments/[environmentId]/actions"; -import Modal from "@/components/shared/Modal"; +import { Modal } from "@formbricks/ui"; import { Button, Input, Label } from "@formbricks/ui"; import { PlusCircleIcon } from "@heroicons/react/24/outline"; import { useRouter } from "next/navigation"; diff --git a/apps/web/app/(app)/environments/[environmentId]/components/Navigation.tsx b/apps/web/app/(app)/environments/[environmentId]/components/Navigation.tsx index 77fe750264..7c93ea1adc 100644 --- a/apps/web/app/(app)/environments/[environmentId]/components/Navigation.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/components/Navigation.tsx @@ -304,7 +304,8 @@ export default function Navigation({
- {session.user.image ? ( + + {/* {session.user.image ? ( ) : ( - )} + )} */}

@@ -328,7 +329,7 @@ export default function Navigation({ Signed in as - {session?.user?.name.length > 30 ? ( + {session?.user?.name && session?.user?.name.length > 30 ? ( diff --git a/apps/web/app/(app)/environments/[environmentId]/components/UrlShortenerModal.tsx b/apps/web/app/(app)/environments/[environmentId]/components/UrlShortenerModal.tsx index 9f828720a9..f0927d83c2 100644 --- a/apps/web/app/(app)/environments/[environmentId]/components/UrlShortenerModal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/components/UrlShortenerModal.tsx @@ -1,4 +1,4 @@ -import Modal from "@/components/shared/Modal"; +import { Modal } from "@formbricks/ui"; import { Button, Input, Label } from "@formbricks/ui"; import { LinkIcon } from "@heroicons/react/24/outline"; import clsx from "clsx"; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/AddIntegrationModal.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/AddIntegrationModal.tsx index f35afe0b9b..1ea6d8b08b 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/AddIntegrationModal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/AddIntegrationModal.tsx @@ -10,7 +10,7 @@ import { useState, useEffect } from "react"; import { useForm } from "react-hook-form"; import toast from "react-hot-toast"; import Image from "next/image"; -import Modal from "@/components/shared/Modal"; +import { Modal } from "@formbricks/ui"; import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; import { ChevronDownIcon } from "@heroicons/react/24/solid"; import { upsertIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/actions"; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/Home.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/Home.tsx index dfa76520bb..f4c222ae79 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/Home.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/Home.tsx @@ -1,7 +1,7 @@ "use client"; import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/actions"; -import DeleteDialog from "@/components/shared/DeleteDialog"; +import { DeleteDialog } from "@formbricks/ui"; import EmptySpaceFiller from "@/components/shared/EmptySpaceFiller"; import { timeSince } from "@formbricks/lib/time"; import { TEnvironment } from "@formbricks/types/v1/environment"; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/AddWebhookModal.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/AddWebhookModal.tsx index 089e4cb349..160c5697eb 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/AddWebhookModal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/AddWebhookModal.tsx @@ -2,7 +2,7 @@ import SurveyCheckboxGroup from "@/app/(app)/environments/[environmentId]/integr import TriggerCheckboxGroup from "@/app/(app)/environments/[environmentId]/integrations/webhooks/TriggerCheckboxGroup"; import { triggers } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/HardcodedTriggers"; import { testEndpoint } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/testEndpoint"; -import Modal from "@/components/shared/Modal"; +import { Modal } from "@formbricks/ui"; import { createWebhookAction } from "./actions"; import { TPipelineTrigger } from "@formbricks/types/v1/pipelines"; import { TSurvey } from "@formbricks/types/v1/surveys"; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/WebhookSettingsTab.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/WebhookSettingsTab.tsx index c72aa3a116..0c15484cbb 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/WebhookSettingsTab.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/WebhookSettingsTab.tsx @@ -1,6 +1,6 @@ "use client"; -import DeleteDialog from "@/components/shared/DeleteDialog"; +import { DeleteDialog } from "@formbricks/ui"; import { Button, Input, Label } from "@formbricks/ui"; import { TrashIcon } from "@heroicons/react/24/outline"; import clsx from "clsx"; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/actions.ts b/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/actions.ts index 9c5fe1f0fd..a35c103c37 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/actions.ts @@ -1,6 +1,6 @@ "use server"; -import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { authOptions } from "@formbricks/lib/authOptions"; import { createWebhook, deleteWebhook, updateWebhook } from "@formbricks/lib/webhook/service"; import { TWebhook, TWebhookInput } from "@formbricks/types/v1/webhooks"; import { getServerSession } from "next-auth"; diff --git a/apps/web/app/(app)/environments/[environmentId]/layout.tsx b/apps/web/app/(app)/environments/[environmentId]/layout.tsx index 0cdf5abcd7..33551265b2 100644 --- a/apps/web/app/(app)/environments/[environmentId]/layout.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/layout.tsx @@ -2,7 +2,7 @@ import EnvironmentsNavbar from "@/app/(app)/environments/[environmentId]/compone import ToasterClient from "@/components/ToasterClient"; import { getServerSession } from "next-auth"; import { redirect } from "next/navigation"; -import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { authOptions } from "@formbricks/lib/authOptions"; import FormbricksClient from "../../FormbricksClient"; import { ResponseFilterProvider } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext"; import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; diff --git a/apps/web/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponseSection.tsx b/apps/web/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponseSection.tsx index 969d094116..beadc5551f 100644 --- a/apps/web/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponseSection.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponseSection.tsx @@ -1,31 +1,41 @@ import ResponseTimeline from "@/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponseTimeline"; -import { getSurveys } from "@formbricks/lib/survey/service"; +import { authOptions } from "@formbricks/lib/authOptions"; import { getResponsesByPersonId } from "@formbricks/lib/response/service"; +import { getSurveys } from "@formbricks/lib/survey/service"; import { TEnvironment } from "@formbricks/types/v1/environment"; -import { TResponseWithSurvey } from "@formbricks/types/v1/responses"; import { TSurvey } from "@formbricks/types/v1/surveys"; +import { TTag } from "@formbricks/types/v1/tags"; +import { getServerSession } from "next-auth"; export default async function ResponseSection({ environment, personId, + environmentTags, }: { environment: TEnvironment; personId: string; + environmentTags: TTag[]; }) { const responses = await getResponsesByPersonId(personId); const surveyIds = responses?.map((response) => response.surveyId) || []; const surveys: TSurvey[] = surveyIds.length === 0 ? [] : (await getSurveys(environment.id)) ?? []; - const responsesWithSurvey: TResponseWithSurvey[] = - responses?.reduce((acc: TResponseWithSurvey[], response) => { - const thisSurvey = surveys.find((survey) => survey?.id === response.surveyId); - if (thisSurvey) { - acc.push({ - ...response, - survey: thisSurvey, - }); - } - return acc; - }, []) || []; + const session = await getServerSession(authOptions); - return ; + if (!session) { + throw new Error("No session found"); + } + + return ( + <> + {responses && ( + + )} + + ); } diff --git a/apps/web/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponseTimeline.tsx b/apps/web/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponseTimeline.tsx index 36f0f12143..357ac849e8 100644 --- a/apps/web/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponseTimeline.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponseTimeline.tsx @@ -1,23 +1,37 @@ "use client"; import ResponseFeed from "@/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponsesFeed"; +import { TResponse } from "@formbricks/types/v1/responses"; +import { TSurvey } from "@formbricks/types/v1/surveys"; import { TEnvironment } from "@formbricks/types/v1/environment"; -import { TResponseWithSurvey } from "@formbricks/types/v1/responses"; import { ArrowsUpDownIcon } from "@heroicons/react/24/outline"; -import { useState } from "react"; +import { useState, useEffect } from "react"; +import { TTag } from "@formbricks/types/v1/tags"; +import { TProfile } from "@formbricks/types/v1/profile"; export default function ResponseTimeline({ + surveys, + profile, environment, responses, + environmentTags, }: { + surveys: TSurvey[]; + profile: TProfile; + responses: TResponse[]; environment: TEnvironment; - responses: TResponseWithSurvey[]; + environmentTags: TTag[]; }) { - const [responsesAscending, setResponsesAscending] = useState(true); + const [responsesAscending, setResponsesAscending] = useState(false); + const [sortedResponses, setSortedResponses] = useState(responses); const toggleSortResponses = () => { setResponsesAscending(!responsesAscending); }; + useEffect(() => { + setSortedResponses(responsesAscending ? [...responses].reverse() : responses); + }, [responsesAscending]); + return (

@@ -30,7 +44,13 @@ export default function ResponseTimeline({
- +
); } diff --git a/apps/web/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponsesFeed.tsx b/apps/web/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponsesFeed.tsx index b6d82d226a..cac070acf8 100644 --- a/apps/web/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponsesFeed.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponsesFeed.tsx @@ -1,102 +1,48 @@ -import { formatDistance } from "date-fns"; import EmptySpaceFiller from "@/components/shared/EmptySpaceFiller"; -import SurveyStatusIndicator from "@/components/shared/SurveyStatusIndicator"; -import { TResponseWithSurvey } from "@formbricks/types/v1/responses"; -import Link from "next/link"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui"; import { TEnvironment } from "@formbricks/types/v1/environment"; +import { TProfile } from "@formbricks/types/v1/profile"; +import { TResponse } from "@formbricks/types/v1/responses"; +import { TSurvey } from "@formbricks/types/v1/surveys"; +import { TTag } from "@formbricks/types/v1/tags"; +import SingleResponseCard from "@formbricks/ui/SingleResponseCard"; -export default function ResponseFeed({ +export default async function ResponseFeed({ responses, - sortByDate, environment, + surveys, + profile, + environmentTags, }: { - responses: TResponseWithSurvey[]; - sortByDate: boolean; + responses: TResponse[]; environment: TEnvironment; + surveys: TSurvey[]; + profile: TProfile; + environmentTags: TTag[]; }) { return ( <> {responses.length === 0 ? ( ) : ( -
- {responses - .slice() - .sort((a, b) => - sortByDate - ? new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() - : new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() - ) - .map((response: TResponseWithSurvey, responseIdx) => ( -
  • -
    - {responseIdx !== responses.length - 1 ? ( -
    -
  • - ))} -
    + responses.map((response, idx) => { + const survey = surveys.find((survey) => { + return survey.id === response.surveyId; + }); + return ( +
    + {survey && ( + + )} +
    + ); + }) )} ); diff --git a/apps/web/app/(app)/environments/[environmentId]/people/[personId]/DeletePersonButton.tsx b/apps/web/app/(app)/environments/[environmentId]/people/[personId]/DeletePersonButton.tsx index 5b0de95cbe..62a9c53bd4 100644 --- a/apps/web/app/(app)/environments/[environmentId]/people/[personId]/DeletePersonButton.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/people/[personId]/DeletePersonButton.tsx @@ -1,7 +1,7 @@ "use client"; import { deletePersonAction } from "@/app/(app)/environments/[environmentId]/people/[personId]/actions"; -import DeleteDialog from "@/components/shared/DeleteDialog"; +import { DeleteDialog } from "@formbricks/ui"; import { TrashIcon } from "lucide-react"; import { useRouter } from "next/navigation"; import { useState } from "react"; diff --git a/apps/web/app/(app)/environments/[environmentId]/people/[personId]/HeadingSection.tsx b/apps/web/app/(app)/environments/[environmentId]/people/[personId]/HeadingSection.tsx index 5cfa9dcf2d..0adb092697 100644 --- a/apps/web/app/(app)/environments/[environmentId]/people/[personId]/HeadingSection.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/people/[personId]/HeadingSection.tsx @@ -1,5 +1,6 @@ import GoBackButton from "@/components/shared/GoBackButton"; import { DeletePersonButton } from "./DeletePersonButton"; +import { getPersonIdentifier } from "@formbricks/lib/people/helpers"; import { getPerson } from "@formbricks/lib/person/service"; interface HeadingSectionProps { @@ -18,7 +19,7 @@ export default async function HeadingSection({ environmentId, personId }: Headin

    - {person.attributes.email || person.id} + {getPersonIdentifier(person)}

    diff --git a/apps/web/app/(app)/environments/[environmentId]/people/[personId]/page.tsx b/apps/web/app/(app)/environments/[environmentId]/people/[personId]/page.tsx index 9d8e2df966..641d45d3b4 100644 --- a/apps/web/app/(app)/environments/[environmentId]/people/[personId]/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/people/[personId]/page.tsx @@ -5,10 +5,12 @@ import AttributesSection from "@/app/(app)/environments/[environmentId]/people/[ import ResponseSection from "@/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponseSection"; import HeadingSection from "@/app/(app)/environments/[environmentId]/people/[personId]/HeadingSection"; import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants"; +import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service"; import { getEnvironment } from "@formbricks/lib/environment/service"; export default async function PersonPage({ params }) { const environment = await getEnvironment(params.environmentId); + const environmentTags = await getTagsByEnvironmentId(params.environmentId); if (!environment) { throw new Error("Environment not found"); } @@ -20,7 +22,11 @@ export default async function PersonPage({ params }) {
    - +
    diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/AddApiKeyModal.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/AddApiKeyModal.tsx index b6f2142097..4f9f3e1af3 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/AddApiKeyModal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/AddApiKeyModal.tsx @@ -1,6 +1,6 @@ "use client"; -import Modal from "@/components/shared/Modal"; +import { Modal } from "@formbricks/ui"; import { Button, Input, Label } from "@formbricks/ui"; import { ExclamationTriangleIcon } from "@heroicons/react/24/solid"; import { useForm } from "react-hook-form"; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/EditApiKeys.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/EditApiKeys.tsx index 1a4fbd04d7..5651d4c3f3 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/EditApiKeys.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/EditApiKeys.tsx @@ -1,6 +1,6 @@ "use client"; -import DeleteDialog from "@/components/shared/DeleteDialog"; +import { DeleteDialog } from "@formbricks/ui"; import { capitalizeFirstLetter } from "@/lib/utils"; import { timeSince } from "@formbricks/lib/time"; import { TApiKey } from "@formbricks/types/v1/apiKeys"; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/actions.ts b/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/actions.ts index 1db54a5815..de92bc2698 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/actions.ts @@ -1,6 +1,6 @@ "use server"; -import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { authOptions } from "@formbricks/lib/authOptions"; import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; import { deleteApiKey, createApiKey } from "@formbricks/lib/apiKey/service"; import { canUserAccessApiKey } from "@formbricks/lib/apiKey/auth"; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/billing/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/billing/page.tsx index 748debd6e6..ee6594ed88 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/billing/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/billing/page.tsx @@ -3,7 +3,7 @@ export const revalidate = REVALIDATION_INTERVAL; import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants"; import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; -import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { authOptions } from "@formbricks/lib/authOptions"; import { getTeamByEnvironmentId } from "@formbricks/lib/team/service"; import { getServerSession } from "next-auth"; import { notFound } from "next/navigation"; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/members/AddMemberModal.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/members/AddMemberModal.tsx index 52f486edb5..c30f292387 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/members/AddMemberModal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/members/AddMemberModal.tsx @@ -1,6 +1,6 @@ "use client"; -import Modal from "@/components/shared/Modal"; +import { Modal } from "@formbricks/ui"; import { Button, Input, diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/members/DeleteTeam.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/members/DeleteTeam.tsx index 0671808fd1..38610c1240 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/members/DeleteTeam.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/members/DeleteTeam.tsx @@ -1,7 +1,7 @@ "use client"; import { deleteTeamAction } from "@/app/(app)/environments/[environmentId]/settings/members/actions"; -import DeleteDialog from "@/components/shared/DeleteDialog"; +import { DeleteDialog } from "@formbricks/ui"; import { TTeam } from "@formbricks/types/v1/teams"; import { Button, Input } from "@formbricks/ui"; import { useRouter } from "next/navigation"; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/members/EditMemberships/MemberActions.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/members/EditMemberships/MemberActions.tsx index b0aa596b29..ad72bedf27 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/members/EditMemberships/MemberActions.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/members/EditMemberships/MemberActions.tsx @@ -7,7 +7,7 @@ import { deleteMembershipAction, resendInviteAction, } from "@/app/(app)/environments/[environmentId]/settings/members/actions"; -import DeleteDialog from "@/components/shared/DeleteDialog"; +import { DeleteDialog } from "@formbricks/ui"; import { TInvite } from "@formbricks/types/v1/invites"; import { TMember } from "@formbricks/types/v1/memberships"; import { TTeam } from "@formbricks/types/v1/teams"; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/members/ShareInviteModal.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/members/ShareInviteModal.tsx index 845c3e73ae..205eb0b5e6 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/members/ShareInviteModal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/members/ShareInviteModal.tsx @@ -1,6 +1,6 @@ "use client"; -import Modal from "@/components/shared/Modal"; +import { Modal } from "@formbricks/ui"; import { Button } from "@formbricks/ui"; import { CheckIcon } from "@heroicons/react/24/outline"; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/members/actions.ts b/apps/web/app/(app)/environments/[environmentId]/settings/members/actions.ts index a5086ba087..9bf16b2288 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/members/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/settings/members/actions.ts @@ -1,6 +1,6 @@ "use server"; -import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { authOptions } from "@formbricks/lib/authOptions"; import { createInviteToken } from "@formbricks/lib/jwt"; import { AuthenticationError, AuthorizationError, ValidationError } from "@formbricks/types/v1/errors"; import { diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/members/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/members/page.tsx index 4039652774..1c7cfb80f3 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/members/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/members/page.tsx @@ -1,5 +1,5 @@ import TeamActions from "@/app/(app)/environments/[environmentId]/settings/members/EditMemberships/TeamActions"; -import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { authOptions } from "@formbricks/lib/authOptions"; import { getMembershipsByUserId, getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service"; import { getTeamByEnvironmentId } from "@formbricks/lib/team/service"; import { Skeleton } from "@formbricks/ui"; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/notifications/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/notifications/page.tsx index cc75c2b8c4..cb3836456d 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/notifications/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/notifications/page.tsx @@ -1,4 +1,4 @@ -import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { authOptions } from "@formbricks/lib/authOptions"; import SettingsCard from "@/app/(app)/environments/[environmentId]/settings/SettingsCard"; import { prisma } from "@formbricks/database"; import { NotificationSettings } from "@formbricks/types/users"; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/product/DeleteProduct.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/product/DeleteProduct.tsx index b785cbd545..1de54b6055 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/product/DeleteProduct.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/product/DeleteProduct.tsx @@ -3,7 +3,7 @@ import { getTeamByEnvironmentId } from "@formbricks/lib/team/service"; import { TProduct } from "@formbricks/types/v1/product"; import DeleteProductRender from "@/app/(app)/environments/[environmentId]/settings/product/DeleteProductRender"; import { getServerSession } from "next-auth"; -import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { authOptions } from "@formbricks/lib/authOptions"; import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service"; type DeleteProductProps = { diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/product/DeleteProductRender.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/product/DeleteProductRender.tsx index 25ae93cb6b..aaf2b3e244 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/product/DeleteProductRender.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/product/DeleteProductRender.tsx @@ -1,7 +1,7 @@ "use client"; import { deleteProductAction } from "@/app/(app)/environments/[environmentId]/settings/product/actions"; -import DeleteDialog from "@/components/shared/DeleteDialog"; +import { DeleteDialog } from "@formbricks/ui"; import { truncate } from "@/lib/utils"; import { TProduct } from "@formbricks/types/v1/product"; import { Button } from "@formbricks/ui"; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/profile/DeleteAccount.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/profile/DeleteAccount.tsx index 49ae6e6f85..593f52788e 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/profile/DeleteAccount.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/profile/DeleteAccount.tsx @@ -1,6 +1,6 @@ "use client"; -import DeleteDialog from "@/components/shared/DeleteDialog"; +import { DeleteDialog } from "@formbricks/ui"; import AvatarPlaceholder from "@/images/avatar-placeholder.png"; import { formbricksLogout } from "@/lib/formbricks"; import { Button, Input, ProfileAvatar } from "@formbricks/ui"; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/profile/EditAvatar.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/profile/EditAvatar.tsx index 71cd2f305c..1cba424d1c 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/profile/EditAvatar.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/profile/EditAvatar.tsx @@ -1,14 +1,12 @@ "use client"; -import AvatarPlaceholder from "@/images/avatar-placeholder.png"; import { Button, ProfileAvatar } from "@formbricks/ui"; -import Image from "next/image"; import { Session } from "next-auth"; export function EditAvatar({ session }: { session: Session | null }) { return (
    - {session?.user?.image ? ( + {/* {session?.user?.image ? ( ) : ( - )} + )} */} + -
    -
    -
    -
    - {data.responses.map((response, idx) => ( -
    -

    {response.question}

    - {typeof response.answer !== "object" ? ( - response.type === QuestionType.Rating ? ( -
    - -
    - ) : ( -

    - {response.answer} -

    - ) - ) : ( -

    - {response.answer.join(", ")} -

    - )} -
    - ))} -
    - - ({ tagId: tag.id, tagName: tag.name }))} - key={data.tags.map((tag) => tag.id).join("-")} - environmentTags={environmentTags} - /> - - -
    - - - ); -} diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.tsx index aa65e47d0c..22455dbc50 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.tsx @@ -1,6 +1,6 @@ export const revalidate = REVALIDATION_INTERVAL; -import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { authOptions } from "@formbricks/lib/authOptions"; import ResponsePage from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage"; import { getAnalysisData } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/data"; import { getServerSession } from "next-auth"; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions.ts index 994ea7cca2..ce434fab47 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions.ts @@ -1,7 +1,7 @@ "use server"; import { getServerSession } from "next-auth"; -import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { authOptions } from "@formbricks/lib/authOptions"; import { sendEmbedSurveyPreviewEmail } from "@formbricks/lib/emails/emails"; import { AuthenticationError } from "@formbricks/types/v1/errors"; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary.tsx index 8c2b9943df..15a9c28636 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/MultipleChoiceSummary.tsx @@ -4,7 +4,7 @@ import { PersonAvatar, ProgressBar } from "@formbricks/ui"; import { InboxStackIcon } from "@heroicons/react/24/solid"; import { useMemo } from "react"; import Link from "next/link"; -import { truncate } from "@/lib/utils"; +import { getPersonIdentifier } from "@formbricks/lib/people/helpers"; import { TSurveyMultipleChoiceMultiQuestion, TSurveyMultipleChoiceSingleQuestion, @@ -57,20 +57,15 @@ export default function MultipleChoiceSummary({ }; } - function findEmail(person) { - return person.attributes?.email || null; - } - const addOtherChoice = (response, value) => { for (const key in resultsDict) { if (resultsDict[key].id === "other" && value !== "") { - const email = response.person && findEmail(response.person); - const displayIdentifier = email || truncate(response.personId, 16); + const displayIdentifier = getPersonIdentifier(response.person); resultsDict[key].otherValues?.push({ value, person: { id: response.personId, - email: displayIdentifier, + email: typeof displayIdentifier === "string" ? displayIdentifier : undefined, }, }); resultsDict[key].count += 1; @@ -196,7 +191,7 @@ export default function MultipleChoiceSummary({
    {otherValue.person.id && } - {otherValue.person.email} + {getPersonIdentifier(otherValue.person)}
    )} diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/OpenTextSummary.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/OpenTextSummary.tsx index a92e2a1a77..20f68d4714 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/OpenTextSummary.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/OpenTextSummary.tsx @@ -1,5 +1,5 @@ +import { getPersonIdentifier } from "@formbricks/lib/people/helpers"; import Headline from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/Headline"; -import { truncate } from "@/lib/utils"; import { timeSince } from "@formbricks/lib/time"; import type { QuestionSummary } from "@formbricks/types/responses"; import { TSurveyOpenTextQuestion } from "@formbricks/types/v1/surveys"; @@ -13,10 +13,6 @@ interface OpenTextSummaryProps { environmentId: string; } -function findEmail(person) { - return person.attributes?.email || null; -} - export default function OpenTextSummary({ questionSummary, environmentId }: OpenTextSummaryProps) { const questionTypeInfo = questionTypes.find((type) => type.id === questionSummary.question.type); @@ -43,8 +39,7 @@ export default function OpenTextSummary({ questionSummary, environmentId }: Open
    Time
    {questionSummary.responses.map((response) => { - const email = response.person && findEmail(response.person); - const displayIdentifier = email || (response.person && truncate(response.person.id, 16)) || null; + const displayIdentifier = getPersonIdentifier(response.person!); return (
    - + {(survey.type === "link" || environment.widgetSetupCompleted) && ( + + )} {survey.status === "inProgress" && "In-progress"} {survey.status === "paused" && "Paused"} diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyMenuBar.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyMenuBar.tsx index eb604942cd..dfa9f00f69 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyMenuBar.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyMenuBar.tsx @@ -1,7 +1,7 @@ "use client"; import AlertDialog from "@/components/shared/AlertDialog"; -import DeleteDialog from "@/components/shared/DeleteDialog"; +import { DeleteDialog } from "@formbricks/ui"; import SurveyStatusDropdown from "@/components/shared/SurveyStatusDropdown"; import { QuestionType } from "@formbricks/types/questions"; import type { Survey } from "@formbricks/types/surveys"; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions.ts index 97a0304f2e..06bc594762 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions.ts @@ -3,7 +3,7 @@ import { TSurvey } from "@formbricks/types/v1/surveys"; import { deleteSurvey, updateSurvey } from "@formbricks/lib/survey/service"; import { canUserAccessSurvey } from "@formbricks/lib/survey/auth"; -import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { authOptions } from "@formbricks/lib/authOptions"; import { getServerSession } from "next-auth"; import { AuthorizationError } from "@formbricks/types/v1/errors"; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/actions.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/actions.ts index 31240985e5..40f6dcd691 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/actions.ts @@ -1,6 +1,6 @@ "use server"; -import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { authOptions } from "@formbricks/lib/authOptions"; import { getServerSession } from "next-auth"; import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; import { createSurvey } from "@formbricks/lib/survey/service"; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/templates/actions.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/templates/actions.ts index 33cb980756..f964574a59 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/templates/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/templates/actions.ts @@ -1,7 +1,7 @@ "use server"; import { createSurvey } from "@formbricks/lib/survey/service"; -import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { authOptions } from "@formbricks/lib/authOptions"; import { getServerSession } from "next-auth"; import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; import { AuthorizationError } from "@formbricks/types/v1/errors"; diff --git a/apps/web/app/(app)/layout.tsx b/apps/web/app/(app)/layout.tsx index 92dc1a7199..76e1c6198e 100644 --- a/apps/web/app/(app)/layout.tsx +++ b/apps/web/app/(app)/layout.tsx @@ -1,6 +1,6 @@ import FormbricksClient from "@/app/(app)/FormbricksClient"; import { PHProvider, PostHogPageview } from "@/app/PostHogClient"; -import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { authOptions } from "@formbricks/lib/authOptions"; import { getServerSession } from "next-auth"; import { redirect } from "next/navigation"; import { Suspense } from "react"; diff --git a/apps/web/app/(app)/onboarding/actions.ts b/apps/web/app/(app)/onboarding/actions.ts index d97cca6585..1f7e1d163c 100644 --- a/apps/web/app/(app)/onboarding/actions.ts +++ b/apps/web/app/(app)/onboarding/actions.ts @@ -1,6 +1,6 @@ "use server"; -import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { authOptions } from "@formbricks/lib/authOptions"; import { updateProduct } from "@formbricks/lib/product/service"; import { updateProfile } from "@formbricks/lib/profile/service"; import { TProductUpdateInput } from "@formbricks/types/v1/product"; diff --git a/apps/web/app/(app)/onboarding/page.tsx b/apps/web/app/(app)/onboarding/page.tsx index bf9d39247a..960a464f38 100644 --- a/apps/web/app/(app)/onboarding/page.tsx +++ b/apps/web/app/(app)/onboarding/page.tsx @@ -1,6 +1,6 @@ export const revalidate = REVALIDATION_INTERVAL; -import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { authOptions } from "@formbricks/lib/authOptions"; import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants"; import { getFirstEnvironmentByUserId } from "@formbricks/lib/environment/service"; import { getProductByEnvironmentId } from "@formbricks/lib/product/service"; diff --git a/apps/web/app/(auth)/invite/page.tsx b/apps/web/app/(auth)/invite/page.tsx index 473168ce96..e8b494a6cb 100644 --- a/apps/web/app/(auth)/invite/page.tsx +++ b/apps/web/app/(auth)/invite/page.tsx @@ -1,6 +1,6 @@ import { sendInviteAcceptedEmail } from "@/lib/email"; import { verifyInviteToken } from "@formbricks/lib/jwt"; -import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { authOptions } from "@formbricks/lib/authOptions"; import { getServerSession } from "next-auth"; import { prisma } from "@formbricks/database"; import { diff --git a/apps/web/app/(redirects)/products/[productId]/route.ts b/apps/web/app/(redirects)/products/[productId]/route.ts index 730cddadd8..2d81b0b840 100644 --- a/apps/web/app/(redirects)/products/[productId]/route.ts +++ b/apps/web/app/(redirects)/products/[productId]/route.ts @@ -1,4 +1,4 @@ -import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { authOptions } from "@formbricks/lib/authOptions"; import { hasTeamAccess } from "@/lib/api/apiHelper"; import { getEnvironments } from "@formbricks/lib/environment/service"; import { getProduct } from "@formbricks/lib/product/service"; diff --git a/apps/web/app/(redirects)/teams/[teamId]/route.ts b/apps/web/app/(redirects)/teams/[teamId]/route.ts index d392b2582c..b1d1aac59c 100644 --- a/apps/web/app/(redirects)/teams/[teamId]/route.ts +++ b/apps/web/app/(redirects)/teams/[teamId]/route.ts @@ -1,4 +1,4 @@ -import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { authOptions } from "@formbricks/lib/authOptions"; import { hasTeamAccess } from "@/lib/api/apiHelper"; import { getEnvironments } from "@formbricks/lib/environment/service"; import { getProducts } from "@formbricks/lib/product/service"; diff --git a/apps/web/app/api/google-sheet/route.ts b/apps/web/app/api/google-sheet/route.ts index 9e3864a54d..306deefcf5 100644 --- a/apps/web/app/api/google-sheet/route.ts +++ b/apps/web/app/api/google-sheet/route.ts @@ -6,7 +6,7 @@ import { } from "@formbricks/lib/constants"; import { google } from "googleapis"; import { NextRequest, NextResponse } from "next/server"; -import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { authOptions } from "@formbricks/lib/authOptions"; import { getServerSession } from "next-auth"; const scopes = [ diff --git a/apps/web/app/api/internal/csv-conversion/route.ts b/apps/web/app/api/internal/csv-conversion/route.ts index 96eeb42546..b6331e1e8a 100755 --- a/apps/web/app/api/internal/csv-conversion/route.ts +++ b/apps/web/app/api/internal/csv-conversion/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import { AsyncParser } from "@json2csv/node"; import { getServerSession } from "next-auth"; -import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { authOptions } from "@formbricks/lib/authOptions"; import { responses } from "@/lib/api/response"; export async function POST(request: NextRequest) { diff --git a/apps/web/app/api/internal/excel-conversion/route.ts b/apps/web/app/api/internal/excel-conversion/route.ts index d6a1263e04..d9cff765e1 100755 --- a/apps/web/app/api/internal/excel-conversion/route.ts +++ b/apps/web/app/api/internal/excel-conversion/route.ts @@ -1,4 +1,4 @@ -import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { authOptions } from "@formbricks/lib/authOptions"; import { responses } from "@/lib/api/response"; import { getServerSession } from "next-auth"; import { NextRequest, NextResponse } from "next/server"; diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 9e23d1606d..675d1f7ee7 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,5 +1,5 @@ import ClientLogout from "@/app/ClientLogout"; -import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { authOptions } from "@formbricks/lib/authOptions"; import { getFirstEnvironmentByUserId } from "@formbricks/lib/environment/service"; import type { Session } from "next-auth"; import { getServerSession } from "next-auth"; diff --git a/apps/web/components/shared/AlertDialog.tsx b/apps/web/components/shared/AlertDialog.tsx index 8c463b202f..bb9925078a 100644 --- a/apps/web/components/shared/AlertDialog.tsx +++ b/apps/web/components/shared/AlertDialog.tsx @@ -1,6 +1,6 @@ "use client"; -import Modal from "@/components/shared/Modal"; +import { Modal } from "@formbricks/ui"; import { Button } from "@formbricks/ui"; interface AlertDialogProps { diff --git a/apps/web/components/shared/CustomDialog.tsx b/apps/web/components/shared/CustomDialog.tsx index 0bec931d45..de48332792 100644 --- a/apps/web/components/shared/CustomDialog.tsx +++ b/apps/web/components/shared/CustomDialog.tsx @@ -1,6 +1,6 @@ "use client"; -import Modal from "@/components/shared/Modal"; +import { Modal } from "@formbricks/ui"; import { Button } from "@formbricks/ui"; interface CustomDialogProps { diff --git a/apps/web/components/shared/ModalWithTabs.tsx b/apps/web/components/shared/ModalWithTabs.tsx index 78caefa72c..4ac45f4b17 100644 --- a/apps/web/components/shared/ModalWithTabs.tsx +++ b/apps/web/components/shared/ModalWithTabs.tsx @@ -1,4 +1,4 @@ -import Modal from "@/components/shared/Modal"; +import { Modal } from "@formbricks/ui"; import { useEffect, useState } from "react"; interface ModalWithTabsProps { diff --git a/apps/web/components/shared/SurveyStatusDropdown.tsx b/apps/web/components/shared/SurveyStatusDropdown.tsx index d639df310d..510966de00 100644 --- a/apps/web/components/shared/SurveyStatusDropdown.tsx +++ b/apps/web/components/shared/SurveyStatusDropdown.tsx @@ -1,7 +1,7 @@ "use client"; import { updateSurveyAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions"; -import SurveyStatusIndicator from "@/components/shared/SurveyStatusIndicator"; +import { SurveyStatusIndicator } from "@formbricks/ui"; import { TEnvironment } from "@formbricks/types/v1/environment"; import { TSurvey } from "@formbricks/types/v1/surveys"; import { @@ -35,7 +35,9 @@ export default function SurveyStatusDropdown({ <> {survey.status === "draft" ? (
    - + {(survey.type === "link" || environment.widgetSetupCompleted) && ( + + )} {survey.status === "draft" &&

    Draft

    }
    ) : ( @@ -69,11 +71,9 @@ export default function SurveyStatusDropdown({
    - + {(survey.type === "link" || environment.widgetSetupCompleted) && ( + + )} {survey.status === "inProgress" && "In-progress"} {survey.status === "paused" && "Paused"} diff --git a/apps/web/components/team/CreateTeamModal.tsx b/apps/web/components/team/CreateTeamModal.tsx index b6d48ddf81..3a9e6a940d 100644 --- a/apps/web/components/team/CreateTeamModal.tsx +++ b/apps/web/components/team/CreateTeamModal.tsx @@ -1,5 +1,5 @@ import { createTeamAction } from "@/app/(app)/environments/[environmentId]/actions"; -import Modal from "@/components/shared/Modal"; +import { Modal } from "@formbricks/ui"; import { Button, Input, Label } from "@formbricks/ui"; import { PlusCircleIcon } from "@heroicons/react/24/outline"; import { useRouter } from "next/navigation"; diff --git a/apps/web/lib/api/apiHelper.ts b/apps/web/lib/api/apiHelper.ts index 75f158f8e5..89efd9d1cb 100644 --- a/apps/web/lib/api/apiHelper.ts +++ b/apps/web/lib/api/apiHelper.ts @@ -1,5 +1,5 @@ -import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; import { prisma } from "@formbricks/database"; +import { authOptions } from "@formbricks/lib/authOptions"; import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; import { createHash } from "crypto"; import { NextApiRequest, NextApiResponse } from "next"; @@ -31,36 +31,6 @@ export const hasEnvironmentAccess = async ( return true; }; -export const getPlan = async (req, res) => { - if (req.headers["x-api-key"]) { - const apiKey = req.headers["x-api-key"].toString(); - const apiKeyData = await prisma.apiKey.findUnique({ - where: { - hashedKey: hashApiKey(apiKey), - }, - select: { - environment: { - select: { - product: { - select: { - team: { - select: { - plan: true, - }, - }, - }, - }, - }, - }, - }, - }); - return apiKeyData?.environment.product.team.plan || "free"; - } else { - const user = await getSessionUser(req, res); - return user && user.teams?.length > 0 ? user.teams[0].plan : "free"; - } -}; - export const hasApiEnvironmentAccess = async (apiKey, environmentId) => { // write function to check if the API Key has access to the environment const apiKeyData = await prisma.apiKey.findUnique({ diff --git a/apps/web/pages/api/v1/environments/[environmentId]/surveys/[surveyId]/responses/index.ts b/apps/web/pages/api/v1/environments/[environmentId]/surveys/[surveyId]/responses/index.ts index 4a4e4b2359..f086e7fdc3 100644 --- a/apps/web/pages/api/v1/environments/[environmentId]/surveys/[surveyId]/responses/index.ts +++ b/apps/web/pages/api/v1/environments/[environmentId]/surveys/[surveyId]/responses/index.ts @@ -1,7 +1,5 @@ -import { getPlan, hasEnvironmentAccess } from "@/lib/api/apiHelper"; +import { hasEnvironmentAccess } from "@/lib/api/apiHelper"; import { prisma } from "@formbricks/database"; -import { RESPONSES_LIMIT_FREE } from "@formbricks/lib/constants"; -import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; import type { NextApiRequest, NextApiResponse } from "next"; @@ -73,16 +71,6 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse) }, }); - if (IS_FORMBRICKS_CLOUD) { - const plan = await getPlan(req, res); - if (plan === "free" && responses.length > RESPONSES_LIMIT_FREE) { - return res.json({ - count: responses.length, - responses: responses.slice(responses.length - RESPONSES_LIMIT_FREE, responses.length), // get last 30 from array - reachedLimit: true, - }); - } - } return res.json({ count: responses.length, responses, reachedLimit: false }); } diff --git a/packages/lib/authOptions.ts b/packages/lib/authOptions.ts new file mode 100644 index 0000000000..d48f523284 --- /dev/null +++ b/packages/lib/authOptions.ts @@ -0,0 +1,366 @@ +import { env } from "@/env.mjs"; +import { verifyPassword } from "@/lib/auth"; +import { prisma } from "@formbricks/database"; +import { EMAIL_VERIFICATION_DISABLED, INTERNAL_SECRET, WEBAPP_URL } from "./constants"; +import { verifyToken } from "./jwt"; +import { getProfileByEmail } from "./profile/service"; +import type { IdentityProvider } from "@prisma/client"; +import type { NextAuthOptions } from "next-auth"; +import CredentialsProvider from "next-auth/providers/credentials"; +import GitHubProvider from "next-auth/providers/github"; +import GoogleProvider from "next-auth/providers/google"; + +export const authOptions: NextAuthOptions = { + providers: [ + CredentialsProvider({ + id: "credentials", + // The name to display on the sign in form (e.g. "Sign in with...") + name: "Credentials", + // The credentials is used to generate a suitable form on the sign in page. + // You can specify whatever fields you are expecting to be submitted. + // e.g. domain, username, password, 2FA token, etc. + // You can pass any HTML attribute to the tag through the object. + credentials: { + email: { + label: "Email Address", + type: "email", + placeholder: "Your email address", + }, + password: { + label: "Password", + type: "password", + placeholder: "Your password", + }, + }, + async authorize(credentials, _req) { + let user; + try { + user = await prisma.user.findUnique({ + where: { + email: credentials?.email, + }, + }); + } catch (e) { + console.error(e); + throw Error("Internal server error. Please try again later"); + } + + if (!user || !credentials) { + throw new Error("No user matches the provided credentials"); + } + if (!user.password) { + throw new Error("No user matches the provided credentials"); + } + + const isValid = await verifyPassword(credentials.password, user.password); + + if (!isValid) { + throw new Error("No user matches the provided credentials"); + } + + return { + id: user.id, + email: user.email, + firstname: user.firstname, + lastname: user.firstname, + emailVerified: user.emailVerified, + }; + }, + }), + CredentialsProvider({ + id: "token", + // The name to display on the sign in form (e.g. "Sign in with...") + name: "Token", + // The credentials is used to generate a suitable form on the sign in page. + // You can specify whatever fields you are expecting to be submitted. + // e.g. domain, username, password, 2FA token, etc. + // You can pass any HTML attribute to the tag through the object. + credentials: { + token: { + label: "Verification Token", + type: "string", + }, + }, + async authorize(credentials, _req) { + let user; + try { + if (!credentials?.token) { + throw new Error("Token not found"); + } + const { id } = await verifyToken(credentials?.token); + user = await prisma.user.findUnique({ + where: { + id: id, + }, + }); + } catch (e) { + console.error(e); + throw new Error("Either a user does not match the provided token or the token is invalid"); + } + + if (!user) { + throw new Error("Either a user does not match the provided token or the token is invalid"); + } + + if (user.emailVerified) { + throw new Error("Email already verified"); + } + + user = await prisma.user.update({ + where: { + id: user.id, + }, + data: { emailVerified: new Date().toISOString() }, + }); + + return { + id: user.id, + email: user.email, + firstname: user.firstname, + lastname: user.firstname, + emailVerified: user.emailVerified, + }; + }, + }), + GitHubProvider({ + clientId: env.GITHUB_ID || "", + clientSecret: env.GITHUB_SECRET || "", + }), + GoogleProvider({ + clientId: env.GOOGLE_CLIENT_ID || "", + clientSecret: env.GOOGLE_CLIENT_SECRET || "", + allowDangerousEmailAccountLinking: true, + }), + ], + callbacks: { + async jwt({ token }) { + const existingUser = await getProfileByEmail(token?.email!); + + if (!existingUser) { + return token; + } + + const additionalAttributs = { + id: existingUser.id, + createdAt: existingUser.createdAt, + onboardingCompleted: existingUser.onboardingCompleted, + name: existingUser.name, + }; + + return { + ...token, + ...additionalAttributs, + }; + }, + async session({ session, token }) { + // @ts-ignore + session.user.id = token?.id; + // @ts-ignore + session.user.createdAt = token?.createdAt ? new Date(token?.createdAt).toISOString() : undefined; + // @ts-ignore + session.user.onboardingCompleted = token?.onboardingCompleted; + // @ts-ignore + session.user.name = token.name || ""; + + return session; + }, + async signIn({ user, account }: any) { + if (account.provider === "credentials" || account.provider === "token") { + if (!user.emailVerified && !EMAIL_VERIFICATION_DISABLED) { + return `/auth/verification-requested?email=${encodeURIComponent(user.email)}`; + } + return true; + } + + if (!user.email || !user.name || account.type !== "oauth") { + return false; + } + + if (account.provider) { + const provider = account.provider.toLowerCase() as IdentityProvider; + // check if accounts for this provider / account Id already exists + const existingUserWithAccount = await prisma.user.findFirst({ + include: { + accounts: { + where: { + provider: account.provider, + }, + }, + }, + where: { + identityProvider: provider, + identityProviderAccountId: account.providerAccountId, + }, + }); + + if (existingUserWithAccount) { + // User with this provider found + // check if email still the same + if (existingUserWithAccount.email === user.email) { + return true; + } + + // user seemed to change his email within the provider + // check if user with this email already exist + // if not found just update user with new email address + // if found throw an error (TODO find better solution) + const otherUserWithEmail = await prisma.user.findFirst({ + where: { email: user.email }, + }); + + if (!otherUserWithEmail) { + await prisma.user.update({ + where: { id: existingUserWithAccount.id }, + data: { email: user.email }, + }); + return true; + } + return "/auth/login?error=Looks%20like%20you%20updated%20your%20email%20somewhere%20else.%0AA%20user%20with%20this%20new%20email%20exists%20already."; + } + + // There is no existing account for this identity provider / account id + // check if user account with this email already exists + // if user already exists throw error and request password login + const existingUserWithEmail = await prisma.user.findFirst({ + where: { email: user.email }, + }); + + if (existingUserWithEmail) { + return "/auth/login?error=A%20user%20with%20this%20email%20exists%20already."; + } + + const createdUser = await prisma.user.create({ + data: { + name: user.name, + email: user.email, + emailVerified: new Date(Date.now()), + onboardingCompleted: false, + identityProvider: provider, + identityProviderAccountId: user.id as string, + accounts: { + create: [{ ...account }], + }, + memberships: { + create: [ + { + accepted: true, + role: "owner", + team: { + create: { + name: `${user.name}'s Team`, + products: { + create: [ + { + name: "My Product", + environments: { + create: [ + { + type: "production", + eventClasses: { + create: [ + { + name: "New Session", + description: "Gets fired when a new session is created", + type: "automatic", + }, + { + name: "Exit Intent (Desktop)", + description: "A user on Desktop leaves the website with the cursor.", + type: "automatic", + }, + { + name: "50% Scroll", + description: "A user scrolled 50% of the current page", + type: "automatic", + }, + ], + }, + attributeClasses: { + create: [ + { + name: "userId", + description: "The internal ID of the person", + type: "automatic", + }, + { + name: "email", + description: "The email of the person", + type: "automatic", + }, + ], + }, + }, + { + type: "development", + eventClasses: { + create: [ + { + name: "New Session", + description: "Gets fired when a new session is created", + type: "automatic", + }, + { + name: "Exit Intent (Desktop)", + description: "A user on Desktop leaves the website with the cursor.", + type: "automatic", + }, + { + name: "50% Scroll", + description: "A user scrolled 50% of the current page", + type: "automatic", + }, + ], + }, + attributeClasses: { + create: [ + { + name: "userId", + description: "The internal ID of the person", + type: "automatic", + }, + { + name: "email", + description: "The email of the person", + type: "automatic", + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + include: { + memberships: true, + }, + }); + + const teamId = createdUser.memberships?.[0]?.teamId; + if (teamId) { + fetch(`${WEBAPP_URL}/api/v1/teams/${teamId}/add_demo_product`, { + method: "POST", + headers: { + "x-api-key": INTERNAL_SECRET, + }, + }); + } + + return true; + } + + return true; + }, + }, + pages: { + signIn: "/auth/login", + signOut: "/auth/logout", + error: "/auth/login", // Error code passed in query string as ?error= + }, +}; diff --git a/packages/lib/emails/emails.ts b/packages/lib/emails/emails.ts index 30ea314177..7477f51ce9 100644 --- a/packages/lib/emails/emails.ts +++ b/packages/lib/emails/emails.ts @@ -90,9 +90,9 @@ export const sendPasswordResetNotifyEmail = async (user: TEmailUser) => { export const sendInviteMemberEmail = async ( inviteId: string, - inviterName: string, - inviteeName: string, - email: string + email: string, + inviterName: string | null, + inviteeName: string | null ) => { const token = createInviteToken(inviteId, email, { expiresIn: "7d", diff --git a/packages/lib/invite/service.ts b/packages/lib/invite/service.ts index 5e4f6f1085..0420f4bec8 100644 --- a/packages/lib/invite/service.ts +++ b/packages/lib/invite/service.ts @@ -118,8 +118,8 @@ export const inviteUser = async ({ teamId, }: { teamId: string; - invitee: { name: string; email: string; role: TMembershipRole }; - currentUser: { id: string; name: string }; + invitee: { name: string | null; email: string; role: TMembershipRole }; + currentUser: { id: string; name: string | null }; }) => { const { name, email, role } = invitee; const { id: currentUserId, name: currentUserName } = currentUser; @@ -157,7 +157,7 @@ export const inviteUser = async ({ }, }); - await sendInviteMemberEmail(invite.id, currentUserName, name, email); + await sendInviteMemberEmail(invite.id, email, currentUserName, name); return invite; }; diff --git a/packages/lib/people/helpers.ts b/packages/lib/people/helpers.ts new file mode 100644 index 0000000000..e0a38c95dc --- /dev/null +++ b/packages/lib/people/helpers.ts @@ -0,0 +1,5 @@ +import { TPerson } from "@formbricks/types/v1/people"; + +export const getPersonIdentifier = (person: TPerson): string | number | null => { + return person?.attributes?.userId || person?.attributes?.email || person?.id || null; +}; diff --git a/packages/lib/responseNote/service.ts b/packages/lib/responseNote/service.ts index b600fa5428..09ae3665b4 100644 --- a/packages/lib/responseNote/service.ts +++ b/packages/lib/responseNote/service.ts @@ -21,6 +21,30 @@ const select = { }, }; +export const createResponseNote = async ( + responseId: string, + userId: string, + text: string +): Promise => { + try { + const responseNote = await prisma.responseNote.create({ + data: { + responseId: responseId, + userId: userId, + text: text, + }, + select, + }); + return responseNote; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError("Database operation failed"); + } + + throw error; + } +}; + export const updateResponseNote = async (responseNoteId: string, text: string): Promise => { try { const updatedResponseNote = await prisma.responseNote.update({ diff --git a/packages/lib/tsconfig.json b/packages/lib/tsconfig.json index 4f24eab9ea..b05575e078 100644 --- a/packages/lib/tsconfig.json +++ b/packages/lib/tsconfig.json @@ -5,6 +5,7 @@ "compilerOptions": { "baseUrl": ".", "paths": { + "@/*": ["../../apps/web/*"], "@prisma/client/*": ["@formbricks/database/client/*"] } } diff --git a/packages/tailwind-config/tailwind.config.js b/packages/tailwind-config/tailwind.config.js index 5e8a4421b1..53610dddd9 100644 --- a/packages/tailwind-config/tailwind.config.js +++ b/packages/tailwind-config/tailwind.config.js @@ -6,7 +6,8 @@ module.exports = { "./components/**/*.{js,ts,jsx,tsx}", "./lib/**/*.{js,ts,jsx,tsx}", // include packages if not transpiling - "../../packages/ui/components/**/*.{js,ts,jsx,tsx}", + "../../packages/ui/**/*.{js,ts,jsx,tsx}", + "!../../packages/ui/node_modules/**/*.{js,ts,jsx,tsx}", ], theme: { extend: { diff --git a/packages/types/next-auth.d.ts b/packages/types/next-auth.d.ts index 25f6f2f27a..a6e46e6974 100644 --- a/packages/types/next-auth.d.ts +++ b/packages/types/next-auth.d.ts @@ -1,22 +1,11 @@ import NextAuth from "next-auth"; +import { TProfile } from "./v1/profile"; declare module "next-auth" { /** * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context */ interface Session { - user: { - id: string; - createdAt: string; - teams: { - id: string; - plan: string; - role: string; - }[]; - email: string; - name: string; - onboardingCompleted: boolean; - image?: StaticImageData; - }; + user: TProfile; } } diff --git a/apps/web/components/shared/DeleteDialog.tsx b/packages/ui/DeleteDialog/index.tsx similarity index 89% rename from apps/web/components/shared/DeleteDialog.tsx rename to packages/ui/DeleteDialog/index.tsx index 186e56f116..95d0f043e2 100644 --- a/apps/web/components/shared/DeleteDialog.tsx +++ b/packages/ui/DeleteDialog/index.tsx @@ -1,7 +1,7 @@ "use client"; -import Modal from "@/components/shared/Modal"; -import { Button } from "@formbricks/ui"; +import { Modal } from "../Modal"; +import Button from "../components/Button"; interface DeleteDialogProps { open: boolean; @@ -16,7 +16,7 @@ interface DeleteDialogProps { disabled?: boolean; } -export default function DeleteDialog({ +export function DeleteDialog({ open, setOpen, deleteWhat, diff --git a/apps/web/components/shared/Modal.tsx b/packages/ui/Modal/index.tsx similarity index 98% rename from apps/web/components/shared/Modal.tsx rename to packages/ui/Modal/index.tsx index 912d0c1730..d661a7cf8b 100644 --- a/apps/web/components/shared/Modal.tsx +++ b/packages/ui/Modal/index.tsx @@ -16,7 +16,7 @@ type Modal = { closeOnOutsideClick?: boolean; }; -const Modal: React.FC = ({ +export const Modal: React.FC = ({ open, setOpen, children, @@ -81,5 +81,3 @@ const Modal: React.FC = ({ ); }; - -export default Modal; diff --git a/packages/ui/SingleResponseCard/actions.ts b/packages/ui/SingleResponseCard/actions.ts new file mode 100644 index 0000000000..f886042e11 --- /dev/null +++ b/packages/ui/SingleResponseCard/actions.ts @@ -0,0 +1,41 @@ +"use server"; + +import { getServerSession } from "next-auth"; +import { AuthorizationError } from "@formbricks/types/v1/errors"; +import { authOptions } from "@formbricks/lib/authOptions"; +import { deleteResponse } from "@formbricks/lib/response/service"; +import { canUserAccessResponse } from "@formbricks/lib/response/auth"; +import { + updateResponseNote, + resolveResponseNote, + createResponseNote, +} from "@formbricks/lib/responseNote/service"; + +export const deleteResponseAction = async (responseId: string) => { + const session = await getServerSession(authOptions); + if (!session) throw new AuthorizationError("Not authorized"); + const isAuthorized = await canUserAccessResponse(session.user!.id, responseId); + if (!isAuthorized) throw new AuthorizationError("Not authorized"); + + return await deleteResponse(responseId); +}; + +export const updateResponseNoteAction = async (responseNoteId: string, text: string) => { + const session = await getServerSession(authOptions); + if (!session) throw new AuthorizationError("Not authorized"); + await updateResponseNote(responseNoteId, text); +}; + +export const resolveResponseNoteAction = async (responseNoteId: string) => { + const session = await getServerSession(authOptions); + if (!session) throw new AuthorizationError("Not authorized"); + await resolveResponseNote(responseNoteId); +}; + +export const createResponseNoteAction = async (responseId: string, userId: string, text: string) => { + const session = await getServerSession(authOptions); + if (!session) throw new AuthorizationError("Not authorized"); + const authotized = await canUserAccessResponse(session.user!.id, responseId); + if (!authotized) throw new AuthorizationError("Not authorized"); + return await createResponseNote(responseId, userId, text); +}; diff --git a/packages/ui/SingleResponseCard/components/QuestionSkip.tsx b/packages/ui/SingleResponseCard/components/QuestionSkip.tsx new file mode 100644 index 0000000000..5eabcb1b59 --- /dev/null +++ b/packages/ui/SingleResponseCard/components/QuestionSkip.tsx @@ -0,0 +1,78 @@ +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../.."; +import { ChevronDoubleDownIcon, XCircleIcon } from "@heroicons/react/20/solid"; +import { TSurveyQuestion } from "@formbricks/types/v1/surveys"; + +interface QuestionSkipProps { + skippedQuestions: string[] | undefined; + status: string; + questions: TSurveyQuestion[]; +} + +export default function QuestionSkip({ skippedQuestions, status, questions }: QuestionSkipProps) { + return ( + <> + {skippedQuestions && ( +
    + {status === "skipped" && ( +
    +
    + {skippedQuestions.length > 1 && ( + + + + + + +

    Respondent skipped these questions.

    +
    +
    +
    + )} +
    +
    + {skippedQuestions && + skippedQuestions.map((questionId) => { + return ( +

    + {questions.find((question) => question.id === questionId)!.headline} +

    + ); + })} +
    +
    + )} + {status === "aborted" && ( +
    +
    +
    + +
    +
    +
    +

    Survey Closed

    + {skippedQuestions && + skippedQuestions.map((questionId) => { + return ( +

    + {questions.find((question) => question.id === questionId)!.headline} +

    + ); + })} +
    +
    + )} +
    + )} + + ); +} diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/RatingResponse.tsx b/packages/ui/SingleResponseCard/components/RatingResponse.tsx similarity index 96% rename from apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/RatingResponse.tsx rename to packages/ui/SingleResponseCard/components/RatingResponse.tsx index bd1e70236b..37699228b5 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/RatingResponse.tsx +++ b/packages/ui/SingleResponseCard/components/RatingResponse.tsx @@ -9,14 +9,14 @@ import { SmilingFaceWithSmilingEyes, TiredFace, WearyFace, -} from "@/components/Smileys"; +} from "./Smileys"; import { StarIcon } from "@heroicons/react/24/solid"; interface RatingResponseProps { scale?: "number" | "star" | "smiley"; range?: number; - answer: string; + answer: string | number | string[]; } export const RatingResponse: React.FC = ({ scale, range, answer }) => { @@ -27,7 +27,7 @@ export const RatingResponse: React.FC = ({ scale, range, an // show number of stars according to answer value const stars: any = []; for (let i = 0; i < range; i++) { - if (i < parseInt(answer)) { + if (i < parseInt(answer.toString())) { stars.push(); } else { stars.push(); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseNote.tsx b/packages/ui/SingleResponseCard/components/ResponseNote.tsx similarity index 93% rename from apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseNote.tsx rename to packages/ui/SingleResponseCard/components/ResponseNote.tsx index cbdfcaf495..6db3ff5604 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseNote.tsx +++ b/packages/ui/SingleResponseCard/components/ResponseNote.tsx @@ -1,41 +1,28 @@ "use client"; -import { - resolveResponseNoteAction, - updateResponseNoteAction, -} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/actions"; -import { useProfile } from "@/lib/profile"; -import { addResponseNote } from "@/lib/responseNotes/responsesNotes"; import { cn } from "@formbricks/lib/cn"; import { timeSince } from "@formbricks/lib/time"; +import { TProfile } from "@formbricks/types/v1/profile"; import { TResponseNote } from "@formbricks/types/v1/responses"; -import { Button, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui"; import { CheckIcon, PencilIcon, PlusIcon } from "@heroicons/react/24/solid"; import clsx from "clsx"; import { Maximize2Icon, Minimize2Icon } from "lucide-react"; import { useRouter } from "next/navigation"; import { FormEvent, useEffect, useMemo, useRef, useState } from "react"; import toast from "react-hot-toast"; +import { Button, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../.."; +import { resolveResponseNoteAction, updateResponseNoteAction, createResponseNoteAction } from "../actions"; interface ResponseNotesProps { + profile: TProfile; responseId: string; notes: TResponseNote[]; - environmentId: string; - surveyId: string; isOpen: boolean; setIsOpen: (isOpen: boolean) => void; } -export default function ResponseNotes({ - responseId, - notes, - environmentId, - surveyId, - isOpen, - setIsOpen, -}: ResponseNotesProps) { +export default function ResponseNotes({ profile, responseId, notes, isOpen, setIsOpen }: ResponseNotesProps) { const router = useRouter(); - const { profile } = useProfile(); const [noteText, setNoteText] = useState(""); const [isCreatingNote, setIsCreatingNote] = useState(false); const [isUpdatingNote, setIsUpdatingNote] = useState(false); @@ -47,7 +34,7 @@ export default function ResponseNotes({ e.preventDefault(); setIsCreatingNote(true); try { - await addResponseNote(environmentId, surveyId, responseId, noteText); + await createResponseNoteAction(responseId, profile.id, noteText); router.refresh(); setIsCreatingNote(false); setNoteText(""); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTagsWrapper.tsx b/packages/ui/SingleResponseCard/components/ResponseTagsWrapper.tsx similarity index 96% rename from apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTagsWrapper.tsx rename to packages/ui/SingleResponseCard/components/ResponseTagsWrapper.tsx index 201b544b27..537ab140b6 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTagsWrapper.tsx +++ b/packages/ui/SingleResponseCard/components/ResponseTagsWrapper.tsx @@ -3,10 +3,10 @@ import TagsCombobox from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/TagsCombobox"; import React, { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; -import { Tag } from "./Tag"; +import { Tag } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/Tag"; import { ExclamationCircleIcon, Cog6ToothIcon } from "@heroicons/react/24/solid"; import { useRouter } from "next/navigation"; -import { Button } from "@formbricks/ui"; +import { Button } from "../.."; import { TTag } from "@formbricks/types/v1/tags"; import { createTagToResponeAction, diff --git a/apps/web/components/Smileys.tsx b/packages/ui/SingleResponseCard/components/Smileys.tsx similarity index 100% rename from apps/web/components/Smileys.tsx rename to packages/ui/SingleResponseCard/components/Smileys.tsx diff --git a/packages/ui/SingleResponseCard/index.tsx b/packages/ui/SingleResponseCard/index.tsx new file mode 100644 index 0000000000..b45798e550 --- /dev/null +++ b/packages/ui/SingleResponseCard/index.tsx @@ -0,0 +1,344 @@ +"use client"; + +import { RatingResponse } from "./components/RatingResponse"; +import ResponseNotes from "./components/ResponseNote"; +import ResponseTagsWrapper from "./components/ResponseTagsWrapper"; +import { deleteResponseAction } from "./actions"; +import { DeleteDialog } from "../DeleteDialog"; +import QuestionSkip from "./components/QuestionSkip"; +import { SurveyStatusIndicator } from "../SurveyStatusIndicator"; +import { timeSince } from "@formbricks/lib/time"; +import { QuestionType } from "@formbricks/types/questions"; +import { TResponse } from "@formbricks/types/v1/responses"; +import { TSurvey } from "@formbricks/types/v1/surveys"; +import { PersonAvatar, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from ".."; +import { TrashIcon } from "@heroicons/react/24/outline"; +import { CheckCircleIcon } from "@heroicons/react/24/solid"; +import clsx from "clsx"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { ReactNode, useState } from "react"; +import toast from "react-hot-toast"; +import { getPersonIdentifier } from "@formbricks/lib/people/helpers"; +import { TTag } from "@formbricks/types/v1/tags"; +import { TEnvironment } from "@formbricks/types/v1/environment"; +import { TProfile } from "@formbricks/types/v1/profile"; + +export interface SingleResponseCardProps { + survey: TSurvey; + response: TResponse; + profile: TProfile; + pageType: string; + environmentTags: TTag[]; + environment: TEnvironment; +} + +interface TooltipRendererProps { + shouldRender: boolean; + tooltipContent: ReactNode; + children: ReactNode; +} + +function TooltipRenderer(props: TooltipRendererProps) { + const { children, shouldRender, tooltipContent } = props; + if (shouldRender) { + return ( + + + {children} + {tooltipContent} + + + ); + } + + return <>{children}; +} + +export default function SingleResponseCard({ + survey, + response, + profile, + pageType, + environmentTags, + environment, +}: SingleResponseCardProps) { + const environmentId = survey.environmentId; + const router = useRouter(); + const displayIdentifier = response.person ? getPersonIdentifier(response.person) : null; + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const [isOpen, setIsOpen] = useState(false); + const isSubmissionFresh = isSubmissionTimeLessThan5Minutes(response.updatedAt); + let skippedQuestions: string[][] = []; + let temp: string[] = []; + + function isValidValue(value: any) { + return ( + (typeof value === "string" && value.trim() !== "") || + (Array.isArray(value) && value.length > 0) || + typeof value === "number" + ); + } + + if (response.finished) { + survey.questions.forEach((question) => { + if (!response.data[question.id]) { + temp.push(question.id); + } else { + if (temp.length > 0) { + skippedQuestions.push([...temp]); + temp = []; + } + } + }); + } else { + for (let index = survey.questions.length - 1; index >= 0; index--) { + const question = survey.questions[index]; + if (!response.data[question.id]) { + if (skippedQuestions.length === 0) { + temp.push(question.id); + } else if (skippedQuestions.length > 0 && !isValidValue(response.data[question.id])) { + temp.push(question.id); + } + } else { + if (temp.length > 0) { + temp.reverse(); + skippedQuestions.push([...temp]); + temp = []; + } + } + } + } + // Handle the case where the last entries are empty + if (temp.length > 0) { + skippedQuestions.push(temp); + } + + function handleArray(data: string | number | string[]): string { + if (Array.isArray(data)) { + return data.join(", "); + } else { + return String(data); + } + } + + const handleDeleteSubmission = async () => { + setIsDeleting(true); + try { + await deleteResponseAction(response.id); + router.refresh(); + toast.success("Submission deleted successfully."); + setDeleteDialogOpen(false); + } catch (error) { + if (error instanceof Error) toast.error(error.message); + } finally { + setIsDeleting(false); + } + }; + + const renderTooltip = Boolean( + (response.personAttributes && Object.keys(response.personAttributes).length > 0) || + (response.meta?.userAgent && Object.keys(response.meta.userAgent).length > 0) + ); + + function isSubmissionTimeLessThan5Minutes(submissionTimeISOString: Date) { + const submissionTime: Date = new Date(submissionTimeISOString); + const currentTime: Date = new Date(); + const timeDifference: number = (currentTime.getTime() - submissionTime.getTime()) / (1000 * 60); // Convert milliseconds to minutes + return timeDifference < 5; + } + + const tooltipContent = ( + <> + {response.singleUseId && ( +
    +

    SingleUse ID:

    + {response.singleUseId} +
    + )} + {response.personAttributes && Object.keys(response.personAttributes).length > 0 && ( +
    +

    Person attributes:

    + {Object.keys(response.personAttributes).map((key) => ( +

    + {key}:{" "} + {response.personAttributes && response.personAttributes[key]} +

    + ))} +
    + )} + + {response.meta?.userAgent && Object.keys(response.meta.userAgent).length > 0 && ( +
    + {response.personAttributes && Object.keys(response.personAttributes).length > 0 && ( +
    + )} +

    Device info:

    + {response.meta?.userAgent?.browser &&

    Browser: {response.meta.userAgent.browser}

    } + {response.meta?.userAgent?.os &&

    OS: {response.meta.userAgent.os}

    } + {response.meta?.userAgent && ( +

    + Device:{" "} + {response.meta.userAgent.device ? response.meta.userAgent.device : "PC / Generic device"} +

    + )} +
    + )} + + ); + const deleteSubmissionToolTip = <>This submission is in progress.; + + return ( +
    +
    +
    +
    + {pageType === "response" && ( +
    + {response.person?.id ? ( + + + + +

    + {displayIdentifier} +

    + + ) : ( +
    + + + +

    Anonymous

    +
    + )} +
    + )} + + {pageType === "people" && ( +
    + {(survey.type === "link" || environment.widgetSetupCompleted) && ( + + )} + + {survey.name} + +
    + )} + +
    + + + { + if (!isSubmissionFresh) { + setDeleteDialogOpen(true); + } + }} + className={`h-4 w-4 ${ + isSubmissionFresh + ? "cursor-not-allowed text-gray-400" + : "text-slate-500 hover:text-red-700" + } `} + /> + +
    +
    +
    +
    + {survey.questions.map((question) => { + const skipped = skippedQuestions.find((skippedQuestionElement) => + skippedQuestionElement.includes(question.id) + ); + + // If found, remove it from the list + if (skipped) { + skippedQuestions = skippedQuestions.filter((item) => item !== skipped); + } + + return ( +
    + {isValidValue(response.data[question.id]) ? ( +

    {question.headline}

    + ) : ( + 0 && + !skippedQuestions[skippedQuestions.length - 1].includes(question.id)) + ? "skipped" + : "aborted" + } + /> + )} + {typeof response.data[question.id] !== "object" ? ( + question.type === QuestionType.Rating ? ( +
    + +
    + ) : ( +

    + {response.data[question.id]} +

    + ) + ) : ( +

    + {handleArray(response.data[question.id])} +

    + )} +
    + ); + })} + {response.finished && ( +
    + +

    Completed

    +
    + )} +
    + + ({ tagId: tag.id, tagName: tag.name }))} + environmentTags={environmentTags} + /> + + +
    + {pageType === "response" && ( + + )} +
    + ); +} diff --git a/apps/web/components/shared/SurveyStatusIndicator.tsx b/packages/ui/SurveyStatusIndicator/index.tsx similarity index 86% rename from apps/web/components/shared/SurveyStatusIndicator.tsx rename to packages/ui/SurveyStatusIndicator/index.tsx index e1053ad55b..f26e722dad 100644 --- a/apps/web/components/shared/SurveyStatusIndicator.tsx +++ b/packages/ui/SurveyStatusIndicator/index.tsx @@ -1,33 +1,14 @@ "use client"; -import { TEnvironment } from "@formbricks/types/v1/environment"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui"; -import { ArchiveBoxIcon, CheckIcon, PauseIcon, ExclamationTriangleIcon } from "@heroicons/react/24/solid"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from ".."; +import { ArchiveBoxIcon, CheckIcon, PauseIcon } from "@heroicons/react/24/solid"; interface SurveyStatusIndicatorProps { status: string; tooltip?: boolean; - environment: TEnvironment; - type: string; } -export default function SurveyStatusIndicator({ - status, - tooltip, - environment, - type, -}: SurveyStatusIndicatorProps) { - if (!environment.widgetSetupCompleted) { - if (type === "web") { - return ( -
    - -
    - ); - } else { - return null; - } - } +export function SurveyStatusIndicator({ status, tooltip }: SurveyStatusIndicatorProps) { if (tooltip) { return ( diff --git a/packages/ui/index.tsx b/packages/ui/index.tsx index 4969e91a50..3790f76514 100644 --- a/packages/ui/index.tsx +++ b/packages/ui/index.tsx @@ -74,6 +74,10 @@ export { Switch } from "./components/Switch"; export { TabBar } from "./components/TabBar"; export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./components/Tooltip"; export { AddVariablesDropdown, Editor } from "./components/editor"; +export { RatingResponse } from "./SingleResponseCard/components/RatingResponse"; +export { DeleteDialog } from "./DeleteDialog"; +export { Modal } from "./Modal"; +export { SurveyStatusIndicator } from "./SurveyStatusIndicator"; export { QuestionTypeSelector } from "./components/QuestionTypeSelector"; /* Icons */ diff --git a/packages/ui/package.json b/packages/ui/package.json index 4eee2a3e8c..fd288ab2d5 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -2,18 +2,14 @@ "name": "@formbricks/ui", "private": true, "sideEffects": false, - "version": "0.0.0", + "version": "1.0.0", "main": "./index.tsx", - "exports": { - ".": "./index.tsx", - "./components/icon": "./components/icon/index.ts" - }, - "types": "./index.tsx", "scripts": { "clean": "rimraf .turbo node_modules dist" }, "devDependencies": { "@formbricks/tsconfig": "workspace:*", + "@formbricks/types": "workspace:*", "concurrently": "^8.2.1", "eslint-config-formbricks": "workspace:*", "postcss": "^8.4.31", @@ -49,6 +45,7 @@ "react-confetti": "^6.1.0", "react-day-picker": "^8.8.2", "react-dom": "^18.2.0", + "react-hot-toast": "^2.4.1", "react-radio-group": "^3.0.3", "react-use": "^17.4.0" } diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index 3bcd54a8cd..a4d91a4ca3 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -1,5 +1,5 @@ { "extends": "@formbricks/tsconfig/react-library.json", "include": ["."], - "exclude": ["dist", "build", "node_modules"] + "exclude": ["build", "node_modules"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55256031e0..68771a842d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,7 +28,7 @@ importers: version: 3.13.0 turbo: specifier: latest - version: 1.10.12 + version: 1.10.13 apps/demo: dependencies: @@ -459,7 +459,7 @@ importers: version: 9.0.0(eslint@8.50.0) eslint-config-turbo: specifier: latest - version: 1.10.12(eslint@8.50.0) + version: 1.8.8(eslint@8.50.0) eslint-plugin-react: specifier: 7.33.2 version: 7.33.2(eslint@8.50.0) @@ -792,6 +792,9 @@ importers: react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) + react-hot-toast: + specifier: ^2.4.1 + version: 2.4.1(csstype@3.1.1)(react-dom@18.2.0)(react@18.2.0) react-radio-group: specifier: ^3.0.3 version: 3.0.3(react-dom@18.2.0)(react@18.2.0) @@ -802,6 +805,9 @@ importers: '@formbricks/tsconfig': specifier: workspace:* version: link:../tsconfig + '@formbricks/types': + specifier: workspace:* + version: link:../types concurrently: specifier: ^8.2.1 version: 8.2.1 @@ -10229,7 +10235,7 @@ packages: normalize-path: 3.0.0 readdirp: 3.6.0 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 /chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} @@ -12512,13 +12518,13 @@ packages: resolution: {integrity: sha512-NB/L/1Y30qyJcG5xZxCJKW/+bqyj+llbcCwo9DEz8bESIP0SLTOQ8T1DWCCFc+wJ61AMEstj4511PSScqMMfCw==} dev: true - /eslint-config-turbo@1.10.12(eslint@8.50.0): - resolution: {integrity: sha512-z3jfh+D7UGYlzMWGh+Kqz++hf8LOE96q3o5R8X4HTjmxaBWlLAWG+0Ounr38h+JLR2TJno0hU9zfzoPNkR9BdA==} + /eslint-config-turbo@1.8.8(eslint@8.50.0): + resolution: {integrity: sha512-+yT22sHOT5iC1sbBXfLIdXfbZuiv9bAyOXsxTxFCWelTeFFnANqmuKB3x274CFvf7WRuZ/vYP/VMjzU9xnFnxA==} peerDependencies: eslint: '>6.6.0' dependencies: eslint: 8.50.0 - eslint-plugin-turbo: 1.10.12(eslint@8.50.0) + eslint-plugin-turbo: 1.8.8(eslint@8.50.0) dev: true /eslint-import-resolver-node@0.3.9: @@ -12724,12 +12730,11 @@ packages: semver: 6.3.1 string.prototype.matchall: 4.0.8 - /eslint-plugin-turbo@1.10.12(eslint@8.50.0): - resolution: {integrity: sha512-uNbdj+ohZaYo4tFJ6dStRXu2FZigwulR1b3URPXe0Q8YaE7thuekKNP+54CHtZPH9Zey9dmDx5btAQl9mfzGOw==} + /eslint-plugin-turbo@1.8.8(eslint@8.50.0): + resolution: {integrity: sha512-zqyTIvveOY4YU5jviDWw9GXHd4RiKmfEgwsjBrV/a965w0PpDwJgEUoSMB/C/dU310Sv9mF3DSdEjxjJLaw6rA==} peerDependencies: eslint: '>6.6.0' dependencies: - dotenv: 16.0.3 eslint: 8.50.0 dev: true @@ -13696,19 +13701,11 @@ packages: dev: true optional: true - /fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - requiresBuild: true - optional: true - /fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] requiresBuild: true - dev: true optional: true /fstream@1.0.12: @@ -15868,7 +15865,7 @@ packages: micromatch: 4.0.5 walker: 1.0.8 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true /jest-leak-detector@29.7.0: @@ -22204,7 +22201,7 @@ packages: engines: {node: '>=10.0.0'} hasBin: true optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: false /rollup@2.79.1: @@ -22212,7 +22209,7 @@ packages: engines: {node: '>=10.0.0'} hasBin: true optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true /rollup@3.29.4: @@ -22220,7 +22217,7 @@ packages: engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true /rtl-css-js@1.16.1: @@ -24216,65 +24213,64 @@ packages: dependencies: safe-buffer: 5.2.1 - /turbo-darwin-64@1.10.12: - resolution: {integrity: sha512-vmDfGVPl5/aFenAbOj3eOx3ePNcWVUyZwYr7taRl0ZBbmv2TzjRiFotO4vrKCiTVnbqjQqAFQWY2ugbqCI1kOQ==} + /turbo-darwin-64@1.10.13: + resolution: {integrity: sha512-vmngGfa2dlYvX7UFVncsNDMuT4X2KPyPJ2Jj+xvf5nvQnZR/3IeDEGleGVuMi/hRzdinoxwXqgk9flEmAYp0Xw==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-darwin-arm64@1.10.12: - resolution: {integrity: sha512-3JliEESLNX2s7g54SOBqqkqJ7UhcOGkS0ywMr5SNuvF6kWVTbuUq7uBU/sVbGq8RwvK1ONlhPvJne5MUqBCTCQ==} + /turbo-darwin-arm64@1.10.13: + resolution: {integrity: sha512-eMoJC+k7gIS4i2qL6rKmrIQGP6Wr9nN4odzzgHFngLTMimok2cGLK3qbJs5O5F/XAtEeRAmuxeRnzQwTl/iuAw==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-linux-64@1.10.12: - resolution: {integrity: sha512-siYhgeX0DidIfHSgCR95b8xPee9enKSOjCzx7EjTLmPqPaCiVebRYvbOIYdQWRqiaKh9yfhUtFmtMOMScUf1gg==} + /turbo-linux-64@1.10.13: + resolution: {integrity: sha512-0CyYmnKTs6kcx7+JRH3nPEqCnzWduM0hj8GP/aodhaIkLNSAGAa+RiYZz6C7IXN+xUVh5rrWTnU2f1SkIy7Gdg==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-linux-arm64@1.10.12: - resolution: {integrity: sha512-K/ZhvD9l4SslclaMkTiIrnfcACgos79YcAo4kwc8bnMQaKuUeRpM15sxLpZp3xDjDg8EY93vsKyjaOhdFG2UbA==} + /turbo-linux-arm64@1.10.13: + resolution: {integrity: sha512-0iBKviSGQQlh2OjZgBsGjkPXoxvRIxrrLLbLObwJo3sOjIH0loGmVIimGS5E323soMfi/o+sidjk2wU1kFfD7Q==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-windows-64@1.10.12: - resolution: {integrity: sha512-7FSgSwvktWDNOqV65l9AbZwcoueAILeE4L7JvjauNASAjjbuzXGCEq5uN8AQU3U5BOFj4TdXrVmO2dX+lLu8Zg==} + /turbo-windows-64@1.10.13: + resolution: {integrity: sha512-S5XySRfW2AmnTeY1IT+Jdr6Goq7mxWganVFfrmqU+qqq3Om/nr0GkcUX+KTIo9mPrN0D3p5QViBRzulwB5iuUQ==} cpu: [x64] os: [win32] requiresBuild: true dev: true optional: true - /turbo-windows-arm64@1.10.12: - resolution: {integrity: sha512-gCNXF52dwom1HLY9ry/cneBPOKTBHhzpqhMylcyvJP0vp9zeMQQkt6yjYv+6QdnmELC92CtKNp2FsNZo+z0pyw==} + /turbo-windows-arm64@1.10.13: + resolution: {integrity: sha512-nKol6+CyiExJIuoIc3exUQPIBjP9nIq5SkMJgJuxsot2hkgGrafAg/izVDRDrRduQcXj2s8LdtxJHvvnbI8hEQ==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /turbo@1.10.12: - resolution: {integrity: sha512-WM3+jTfQWnB9W208pmP4oeehZcC6JQNlydb/ZHMRrhmQa+htGhWLCzd6Q9rLe0MwZLPpSPFV2/bN5egCLyoKjQ==} + /turbo@1.10.13: + resolution: {integrity: sha512-vOF5IPytgQPIsgGtT0n2uGZizR2N3kKuPIn4b5p5DdeLoI0BV7uNiydT7eSzdkPRpdXNnO8UwS658VaI4+YSzQ==} hasBin: true - requiresBuild: true optionalDependencies: - turbo-darwin-64: 1.10.12 - turbo-darwin-arm64: 1.10.12 - turbo-linux-64: 1.10.12 - turbo-linux-arm64: 1.10.12 - turbo-windows-64: 1.10.12 - turbo-windows-arm64: 1.10.12 + turbo-darwin-64: 1.10.13 + turbo-darwin-arm64: 1.10.13 + turbo-linux-64: 1.10.13 + turbo-linux-arm64: 1.10.13 + turbo-windows-64: 1.10.13 + turbo-windows-arm64: 1.10.13 dev: true /tw-to-css@0.0.11: @@ -25172,7 +25168,7 @@ packages: rollup: 3.29.4 terser: 5.21.0 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true /vm-browserify@1.1.2: