diff --git a/apps/storybook/package.json b/apps/storybook/package.json index 0dca3377ff..cace03b330 100644 --- a/apps/storybook/package.json +++ b/apps/storybook/package.json @@ -12,18 +12,18 @@ }, "devDependencies": { "@chromatic-com/storybook": "^5.0.1", - "@storybook/addon-a11y": "10.2.14", - "@storybook/addon-links": "10.2.14", - "@storybook/addon-onboarding": "10.2.14", - "@storybook/react-vite": "10.2.14", + "@storybook/addon-a11y": "10.2.15", + "@storybook/addon-links": "10.2.15", + "@storybook/addon-onboarding": "10.2.15", + "@storybook/react-vite": "10.2.15", "@typescript-eslint/eslint-plugin": "8.56.1", "@tailwindcss/vite": "4.2.1", "@typescript-eslint/parser": "8.56.1", "@vitejs/plugin-react": "5.1.4", "eslint-plugin-react-refresh": "0.4.26", "eslint-plugin-storybook": "10.2.14", - "storybook": "10.2.14", + "storybook": "10.2.15", "vite": "7.3.1", - "@storybook/addon-docs": "10.2.14" + "@storybook/addon-docs": "10.2.15" } } diff --git a/apps/web/app/(app)/(onboarding)/types/onboarding.ts b/apps/web/app/(app)/(onboarding)/types/onboarding.ts index ead024f3ba..65b1b2aaba 100644 --- a/apps/web/app/(app)/(onboarding)/types/onboarding.ts +++ b/apps/web/app/(app)/(onboarding)/types/onboarding.ts @@ -1,7 +1,7 @@ import { z } from "zod"; export const ZOrganizationTeam = z.object({ - id: z.string().cuid2(), + id: z.cuid2(), name: z.string(), }); diff --git a/apps/web/app/(app)/environments/[environmentId]/actions.ts b/apps/web/app/(app)/environments/[environmentId]/actions.ts index 59459dada8..76cffb9490 100644 --- a/apps/web/app/(app)/environments/[environmentId]/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/actions.ts @@ -25,7 +25,7 @@ const ZCreateProjectAction = z.object({ data: ZProjectUpdateInput, }); -export const createProjectAction = authenticatedActionClient.schema(ZCreateProjectAction).action( +export const createProjectAction = authenticatedActionClient.inputSchema(ZCreateProjectAction).action( withAuditLogging( "created", "project", @@ -97,7 +97,7 @@ const ZGetOrganizationsForSwitcherAction = z.object({ * Called on-demand when user opens the organization switcher. */ export const getOrganizationsForSwitcherAction = authenticatedActionClient - .schema(ZGetOrganizationsForSwitcherAction) + .inputSchema(ZGetOrganizationsForSwitcherAction) .action(async ({ ctx, parsedInput }) => { await checkAuthorizationUpdated({ userId: ctx.user.id, @@ -122,7 +122,7 @@ const ZGetProjectsForSwitcherAction = z.object({ * Called on-demand when user opens the project switcher. */ export const getProjectsForSwitcherAction = authenticatedActionClient - .schema(ZGetProjectsForSwitcherAction) + .inputSchema(ZGetProjectsForSwitcherAction) .action(async ({ ctx, parsedInput }) => { await checkAuthorizationUpdated({ userId: ctx.user.id, diff --git a/apps/web/app/(app)/environments/[environmentId]/components/MainNavigation.tsx b/apps/web/app/(app)/environments/[environmentId]/components/MainNavigation.tsx index 907ce8caf3..aadb6a7113 100644 --- a/apps/web/app/(app)/environments/[environmentId]/components/MainNavigation.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/components/MainNavigation.tsx @@ -11,6 +11,7 @@ import { RocketIcon, UserCircleIcon, UserIcon, + WorkflowIcon, } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; @@ -114,6 +115,13 @@ export const MainNavigation = ({ pathname?.includes("/segments") || pathname?.includes("/attributes"), }, + { + name: t("common.workflows"), + href: `/environments/${environment.id}/workflows`, + icon: WorkflowIcon, + isActive: pathname?.includes("/workflows"), + isHidden: !isFormbricksCloud, + }, { name: t("common.configuration"), href: `/environments/${environment.id}/workspace/general`, @@ -121,7 +129,7 @@ export const MainNavigation = ({ isActive: pathname?.includes("/project"), }, ], - [t, environment.id, pathname] + [t, environment.id, pathname, isFormbricksCloud] ); const dropdownNavigation = [ diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/actions.ts b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/actions.ts index b2043a4d0c..8b7334fcd5 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/actions.ts @@ -12,7 +12,7 @@ const ZUpdateNotificationSettingsAction = z.object({ }); export const updateNotificationSettingsAction = authenticatedActionClient - .schema(ZUpdateNotificationSettingsAction) + .inputSchema(ZUpdateNotificationSettingsAction) .action( withAuditLogging( "updated", diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/actions.ts b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/actions.ts index 5949d1638c..b2d5548012 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/actions.ts @@ -63,7 +63,7 @@ async function handleEmailUpdate({ return payload; } -export const updateUserAction = authenticatedActionClient.schema(ZUserPersonalInfoUpdateInput).action( +export const updateUserAction = authenticatedActionClient.inputSchema(ZUserPersonalInfoUpdateInput).action( withAuditLogging( "updated", "user", diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/actions.ts b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/actions.ts index 91246bca6c..942a279f05 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/actions.ts @@ -17,7 +17,7 @@ const ZUpdateOrganizationNameAction = z.object({ }); export const updateOrganizationNameAction = authenticatedActionClient - .schema(ZUpdateOrganizationNameAction) + .inputSchema(ZUpdateOrganizationNameAction) .action( withAuditLogging( "updated", @@ -55,28 +55,36 @@ const ZDeleteOrganizationAction = z.object({ organizationId: ZId, }); -export const deleteOrganizationAction = authenticatedActionClient.schema(ZDeleteOrganizationAction).action( - withAuditLogging( - "deleted", - "organization", - async ({ ctx, parsedInput }: { ctx: AuthenticatedActionClientCtx; parsedInput: Record }) => { - const isMultiOrgEnabled = await getIsMultiOrgEnabled(); - if (!isMultiOrgEnabled) throw new OperationNotAllowedError("Organization deletion disabled"); +export const deleteOrganizationAction = authenticatedActionClient + .inputSchema(ZDeleteOrganizationAction) + .action( + withAuditLogging( + "deleted", + "organization", + async ({ + ctx, + parsedInput, + }: { + ctx: AuthenticatedActionClientCtx; + parsedInput: Record; + }) => { + const isMultiOrgEnabled = await getIsMultiOrgEnabled(); + if (!isMultiOrgEnabled) throw new OperationNotAllowedError("Organization deletion disabled"); - await checkAuthorizationUpdated({ - userId: ctx.user.id, - organizationId: parsedInput.organizationId, - access: [ - { - type: "organization", - roles: ["owner"], - }, - ], - }); - ctx.auditLoggingCtx.organizationId = parsedInput.organizationId; - const oldObject = await getOrganization(parsedInput.organizationId); - ctx.auditLoggingCtx.oldObject = oldObject; - return await deleteOrganization(parsedInput.organizationId); - } - ) -); + await checkAuthorizationUpdated({ + userId: ctx.user.id, + organizationId: parsedInput.organizationId, + access: [ + { + type: "organization", + roles: ["owner"], + }, + ], + }); + ctx.auditLoggingCtx.organizationId = parsedInput.organizationId; + const oldObject = await getOrganization(parsedInput.organizationId); + ctx.auditLoggingCtx.oldObject = oldObject; + return await deleteOrganization(parsedInput.organizationId); + } + ) + ); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions.ts index a248082239..d9b62357e7 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions.ts @@ -23,7 +23,7 @@ const ZGetResponsesAction = z.object({ }); export const getResponsesAction = authenticatedActionClient - .schema(ZGetResponsesAction) + .inputSchema(ZGetResponsesAction) .action(async ({ ctx, parsedInput }) => { await checkAuthorizationUpdated({ userId: ctx.user.id, @@ -57,7 +57,7 @@ const ZGetSurveySummaryAction = z.object({ }); export const getSurveySummaryAction = authenticatedActionClient - .schema(ZGetSurveySummaryAction) + .inputSchema(ZGetSurveySummaryAction) .action(async ({ ctx, parsedInput }) => { await checkAuthorizationUpdated({ userId: ctx.user.id, @@ -85,7 +85,7 @@ const ZGetResponseCountAction = z.object({ }); export const getResponseCountAction = authenticatedActionClient - .schema(ZGetResponseCountAction) + .inputSchema(ZGetResponseCountAction) .action(async ({ ctx, parsedInput }) => { await checkAuthorizationUpdated({ userId: ctx.user.id, @@ -110,12 +110,12 @@ export const getResponseCountAction = authenticatedActionClient const ZGetDisplaysWithContactAction = z.object({ surveyId: ZId, - limit: z.number().int().min(1).max(100), - offset: z.number().int().nonnegative(), + limit: z.int().min(1).max(100), + offset: z.int().nonnegative(), }); export const getDisplaysWithContactAction = authenticatedActionClient - .schema(ZGetDisplaysWithContactAction) + .inputSchema(ZGetDisplaysWithContactAction) .action(async ({ ctx, parsedInput }) => { await checkAuthorizationUpdated({ userId: ctx.user.id, 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 815cdd2c9f..8ab355a92f 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 @@ -22,7 +22,7 @@ const ZSendEmbedSurveyPreviewEmailAction = z.object({ }); export const sendEmbedSurveyPreviewEmailAction = authenticatedActionClient - .schema(ZSendEmbedSurveyPreviewEmailAction) + .inputSchema(ZSendEmbedSurveyPreviewEmailAction) .action(async ({ ctx, parsedInput }) => { const organizationId = await getOrganizationIdFromSurveyId(parsedInput.surveyId); const organizationLogoUrl = await getOrganizationLogoUrl(organizationId); @@ -69,7 +69,7 @@ const ZResetSurveyAction = z.object({ projectId: ZId, }); -export const resetSurveyAction = authenticatedActionClient.schema(ZResetSurveyAction).action( +export const resetSurveyAction = authenticatedActionClient.inputSchema(ZResetSurveyAction).action( withAuditLogging( "updated", "survey", @@ -123,7 +123,7 @@ const ZGetEmailHtmlAction = z.object({ }); export const getEmailHtmlAction = authenticatedActionClient - .schema(ZGetEmailHtmlAction) + .inputSchema(ZGetEmailHtmlAction) .action(async ({ ctx, parsedInput }) => { await checkAuthorizationUpdated({ userId: ctx.user.id, @@ -152,7 +152,7 @@ const ZGeneratePersonalLinksAction = z.object({ }); export const generatePersonalLinksAction = authenticatedActionClient - .schema(ZGeneratePersonalLinksAction) + .inputSchema(ZGeneratePersonalLinksAction) .action(async ({ ctx, parsedInput }) => { const isContactsEnabled = await getIsContactsEnabled(); if (!isContactsEnabled) { @@ -231,7 +231,7 @@ const ZUpdateSingleUseLinksAction = z.object({ }); export const updateSingleUseLinksAction = authenticatedActionClient - .schema(ZUpdateSingleUseLinksAction) + .inputSchema(ZUpdateSingleUseLinksAction) .action(async ({ ctx, parsedInput }) => { await checkAuthorizationUpdated({ userId: ctx.user.id, diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/surveySummary.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/surveySummary.ts index 66b3de58b0..788aa33d79 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/surveySummary.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/surveySummary.ts @@ -1095,7 +1095,7 @@ export const getResponsesForSummary = reactCache( [limit, ZOptionalNumber], [offset, ZOptionalNumber], [filterCriteria, ZResponseFilterCriteria.optional()], - [cursor, z.string().cuid2().optional()] + [cursor, z.cuid2().optional()] ); const queryLimit = limit ?? RESPONSES_PER_PAGE; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions.ts index 91100b25e3..67666a471d 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions.ts @@ -28,7 +28,7 @@ const ZGetResponsesDownloadUrlAction = z.object({ }); export const getResponsesDownloadUrlAction = authenticatedActionClient - .schema(ZGetResponsesDownloadUrlAction) + .inputSchema(ZGetResponsesDownloadUrlAction) .action(async ({ ctx, parsedInput }) => { await checkAuthorizationUpdated({ userId: ctx.user.id, @@ -58,7 +58,7 @@ const ZGetSurveyFilterDataAction = z.object({ }); export const getSurveyFilterDataAction = authenticatedActionClient - .schema(ZGetSurveyFilterDataAction) + .inputSchema(ZGetSurveyFilterDataAction) .action(async ({ ctx, parsedInput }) => { const survey = await getSurvey(parsedInput.surveyId); @@ -121,7 +121,7 @@ const checkSurveyFollowUpsPermission = async (organizationId: string): Promise { + const { t } = useTranslation(); + const [step, setStep] = useState("prompt"); + const [promptValue, setPromptValue] = useState(""); + const [detailsValue, setDetailsValue] = useState(""); + const [responseId, setResponseId] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleGenerateWorkflow = async () => { + if (promptValue.trim().length < 100 || isSubmitting) return; + setIsSubmitting(true); + + try { + const res = await fetch(`${FORMBRICKS_HOST}/api/v2/client/${ENVIRONMENT_ID}/responses`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + surveyId: SURVEY_ID, + finished: false, + data: { + workflow: promptValue.trim(), + useremail: userEmail, + orgname: organizationName, + billingplan: billingPlan, + }, + }), + }); + + if (res.ok) { + const json = await res.json(); + setResponseId(json.data?.id ?? null); + } + + setStep("followup"); + } catch { + setStep("followup"); + } finally { + setIsSubmitting(false); + } + }; + + const handleSubmitFeedback = async () => { + if (isSubmitting) return; + setIsSubmitting(true); + + if (responseId) { + try { + await fetch(`${FORMBRICKS_HOST}/api/v1/client/${ENVIRONMENT_ID}/responses/${responseId}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + finished: true, + data: { + details: detailsValue.trim(), + }, + }), + }); + } catch { + // silently fail + } + } + + setIsSubmitting(false); + setStep("thankyou"); + }; + + const handleSkipFeedback = async () => { + if (!responseId) { + setStep("thankyou"); + return; + } + + try { + await fetch(`${FORMBRICKS_HOST}/api/v1/client/${ENVIRONMENT_ID}/responses/${responseId}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + finished: true, + data: {}, + }), + }); + } catch { + // silently fail + } + + setStep("thankyou"); + }; + + if (step === "prompt") { + return ( +
+
+
+
+ +
+

{t("workflows.heading")}

+

{t("workflows.subheading")}

+
+ +
+