Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions apps/app/src/actions/floating.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"use server";

import { addYears } from "date-fns";
import { createSafeActionClient } from "next-safe-action";
import { cookies } from "next/headers";
import { z } from "zod";

const schema = z.object({
floatingOpen: z.boolean(),
});

export const updateFloatingState = createSafeActionClient()
.schema(schema)
.action(async ({ parsedInput }) => {
const cookieStore = await cookies();

cookieStore.set({
name: "floating-onboarding-checklist",
value: JSON.stringify(parsedInput.floatingOpen),
expires: addYears(new Date(), 1),
});

return { success: true };
});
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export default async function Layout({

const cookieStore = await cookies();
const isCollapsed = cookieStore.get("sidebar-collapsed")?.value === "true";
const floatingOpen =
cookieStore.get("floating-onboarding-checklist")?.value !== "false";

if (!session?.session?.userId) {
return redirect("/auth");
Expand Down Expand Up @@ -88,12 +90,13 @@ export default async function Layout({
<div className="hidden md:flex">
{!("error" in onboardingStatus) &&
onboardingStatus.completedItems <
onboardingStatus.totalItems && (
onboardingStatus.totalItems && (
<FloatingOnboardingChecklist
orgId={currentOrganization.id}
completedItems={onboardingStatus.completedItems}
totalItems={onboardingStatus.totalItems}
checklistItems={onboardingStatus.checklistItems}
floatingOpen={floatingOpen}
/>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"use server";

import { authActionClient } from "@/actions/safe-action";
import { db } from "@comp/db";
import { z } from "zod";

const isFriendlyAvailableSchema = z.object({
friendlyUrl: z.string(),
orgId: z.string(),
});

export const isFriendlyAvailable = authActionClient
.schema(isFriendlyAvailableSchema)
.metadata({
name: "check-friendly-url",
track: {
event: "check-friendly-url",
channel: "server",
},
})
.action(async ({ parsedInput, ctx }) => {
const { friendlyUrl, orgId } = parsedInput;

if (!ctx.session.activeOrganizationId) {
throw new Error("No active organization");
}

const url = await db.trust.findUnique({
where: {
friendlyUrl,
},
select: {
friendlyUrl: true,
organizationId: true,
},
});

if (url) {
if (url.organizationId === orgId) {
return { isAvailable: true };
}
return { isAvailable: false };
}

return { isAvailable: true };
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { z } from "zod";
const trustPortalSwitchSchema = z.object({
enabled: z.boolean(),
contactEmail: z.string().email().optional().or(z.literal("")),
friendlyUrl: z.string().optional(),
});

export const trustPortalSwitchAction = authActionClient
Expand All @@ -22,7 +23,7 @@ export const trustPortalSwitchAction = authActionClient
},
})
.action(async ({ parsedInput, ctx }) => {
const { enabled, contactEmail } = parsedInput;
const { enabled, contactEmail, friendlyUrl } = parsedInput;
const { activeOrganizationId } = ctx.session;

if (!activeOrganizationId) {
Expand All @@ -31,15 +32,19 @@ export const trustPortalSwitchAction = authActionClient

try {
await db.trust.upsert({
where: { organizationId: activeOrganizationId },
where: {
organizationId: activeOrganizationId,
},
update: {
status: enabled ? "published" : "draft",
contactEmail: contactEmail === "" ? null : contactEmail,
friendlyUrl: friendlyUrl === "" ? null : friendlyUrl,
},
create: {
organizationId: activeOrganizationId,
status: enabled ? "published" : "draft",
contactEmail: contactEmail === "" ? null : contactEmail,
friendlyUrl: friendlyUrl === "" ? null : friendlyUrl,
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,14 @@ import { Button } from "@comp/ui/button"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@comp/ui/select"
import { updateTrustPortalFrameworks } from "../actions/update-trust-portal-frameworks"
import { SOC2, ISO27001, GDPR } from "./logos"
import { isFriendlyAvailable } from "../actions/is-friendly-available"
import { useDebounce } from "@/hooks/useDebounce"
import { useState, useEffect, useRef, useCallback } from "react"

const trustPortalSwitchSchema = z.object({
enabled: z.boolean(),
contactEmail: z.string().email().or(z.literal("")).optional(),
friendlyUrl: z.string().optional(),
soc2: z.boolean(),
iso27001: z.boolean(),
gdpr: z.boolean(),
Expand All @@ -42,6 +46,7 @@ export function TrustPortalSwitch({
soc2Status,
iso27001Status,
gdprStatus,
friendlyUrl,
}: {
enabled: boolean
slug: string
Expand All @@ -55,6 +60,7 @@ export function TrustPortalSwitch({
soc2Status: "started" | "in_progress" | "compliant"
iso27001Status: "started" | "in_progress" | "compliant"
gdprStatus: "started" | "in_progress" | "compliant"
friendlyUrl: string | null
}) {
const t = useI18n()

Expand All @@ -67,6 +73,8 @@ export function TrustPortalSwitch({
},
})

const checkFriendlyUrl = useAction(isFriendlyAvailable)

const form = useForm<z.infer<typeof trustPortalSwitchSchema>>({
resolver: zodResolver(trustPortalSwitchSchema),
defaultValues: {
Expand All @@ -78,18 +86,113 @@ export function TrustPortalSwitch({
soc2Status: soc2Status ?? "started",
iso27001Status: iso27001Status ?? "started",
gdprStatus: gdprStatus ?? "started",
friendlyUrl: friendlyUrl ?? undefined,
},
})

const onSubmit = async (data: z.infer<typeof trustPortalSwitchSchema>) => {
await trustPortalSwitch.execute(data)
}

const portalUrl = domainVerified ? `https://${domain}` : `https://trust.trycomp.ai/${slug}`
const portalUrl = domainVerified ? `https://${domain}` : `https://trust.inc/${slug}`

// --- Auto-save helpers ---
const lastSaved = useRef<{ [key: string]: string | boolean }>({
contactEmail: contactEmail ?? "",
friendlyUrl: friendlyUrl ?? "",
enabled: enabled,
})

// Save handler
const autoSave = useCallback(
async (field: string, value: any) => {
const current = form.getValues()
if (lastSaved.current[field] !== value) {
const data = { ...current, [field]: value }
await onSubmit(data)
lastSaved.current[field] = value
}
},
[form, onSubmit]
)

// --- Field Handlers ---
// Contact Email
const [contactEmailValue, setContactEmailValue] = useState(form.getValues("contactEmail") || "")
const debouncedContactEmail = useDebounce(contactEmailValue, 500)
// Debounced auto-save
useEffect(() => {
if (
debouncedContactEmail !== undefined &&
debouncedContactEmail !== lastSaved.current.contactEmail
) {
form.setValue("contactEmail", debouncedContactEmail)
autoSave("contactEmail", debouncedContactEmail)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedContactEmail])
// On blur immediate save
const handleContactEmailBlur = useCallback(
(e: React.FocusEvent<HTMLInputElement>) => {
const value = e.target.value
form.setValue("contactEmail", value)
autoSave("contactEmail", value)
},
[form, autoSave]
)

// Friendly URL
const [friendlyUrlValue, setFriendlyUrlValue] = useState(form.getValues("friendlyUrl") || "")
const debouncedFriendlyUrl = useDebounce(friendlyUrlValue, 500)
const [friendlyUrlStatus, setFriendlyUrlStatus] = useState<"idle" | "checking" | "available" | "unavailable">("idle")
// Check availability on debounce
useEffect(() => {
if (!debouncedFriendlyUrl || debouncedFriendlyUrl === (friendlyUrl ?? "")) {
setFriendlyUrlStatus("idle")
return
}
setFriendlyUrlStatus("checking")
checkFriendlyUrl.execute({ friendlyUrl: debouncedFriendlyUrl, orgId })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedFriendlyUrl, orgId, friendlyUrl])
useEffect(() => {
if (checkFriendlyUrl.status === "executing") return
if (checkFriendlyUrl.result?.data?.isAvailable === true) {
setFriendlyUrlStatus("available")
// Auto-save if available and changed
if (debouncedFriendlyUrl !== lastSaved.current.friendlyUrl) {
form.setValue("friendlyUrl", debouncedFriendlyUrl)
autoSave("friendlyUrl", debouncedFriendlyUrl)
}
} else if (checkFriendlyUrl.result?.data?.isAvailable === false) {
setFriendlyUrlStatus("unavailable")
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [checkFriendlyUrl.status, checkFriendlyUrl.result])
// On blur immediate save if available
const handleFriendlyUrlBlur = useCallback(
(e: React.FocusEvent<HTMLInputElement>) => {
const value = e.target.value
if (friendlyUrlStatus === "available" && value !== lastSaved.current.friendlyUrl) {
form.setValue("friendlyUrl", value)
autoSave("friendlyUrl", value)
}
},
[form, autoSave, friendlyUrlStatus]
)

// Enabled switch immediate save
const handleEnabledChange = useCallback(
(val: boolean) => {
form.setValue("enabled", val)
autoSave("enabled", val)
},
[form, autoSave]
)

return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<form className="space-y-4">
<Card className="overflow-hidden">
<CardHeader className="pb-4">
<div className="flex items-center justify-between">
Expand All @@ -114,7 +217,7 @@ export function TrustPortalSwitch({
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
onCheckedChange={handleEnabledChange}
disabled={trustPortalSwitch.status === "executing"}
/>
</FormControl>
Expand All @@ -126,19 +229,64 @@ export function TrustPortalSwitch({
<CardContent className="space-y-6 pt-0">
{form.watch("enabled") && (
<div className="pt-2">
<h3 className="text-sm font-medium mb-4">Information Requests</h3>
<div className="rounded-md border p-4">
<h3 className="text-sm font-medium mb-4">Trust Portal Settings</h3>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-x-6 border rounded-md p-4">
<FormField
control={form.control}
name="friendlyUrl"
render={({ field }) => (
<FormItem className="w-full">
<FormLabel>
Custom URL
</FormLabel>
<FormControl>
<div>
<div className="relative flex items-center w-full">
<Input
{...field}
value={friendlyUrlValue}
onChange={e => {
field.onChange(e)
setFriendlyUrlValue(e.target.value)
}}
onBlur={handleFriendlyUrlBlur}
placeholder="my-org"
autoComplete="off"
autoCapitalize="none"
autoCorrect="off"
spellCheck="false"
prefix="trust.inc/"
/>
</div>
{friendlyUrlValue && (
<div className="text-xs mt-1 min-h-[18px]">
{friendlyUrlStatus === "checking" && t("settings.trust_portal.friendly_url.checking")}
{friendlyUrlStatus === "available" && <span className="text-green-600">{t("settings.trust_portal.friendly_url.available")}</span>}
{friendlyUrlStatus === "unavailable" && <span className="text-red-600">{t("settings.trust_portal.friendly_url.unavailable")}</span>}
</div>
)}
</div>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="contactEmail"
render={({ field }) => (
<FormItem className="flex items-center justify-between">
<FormItem className="w-full">
<FormLabel>
Contact Email
</FormLabel>
<FormControl>
<Input
{...field}
value={contactEmailValue}
onChange={e => {
field.onChange(e)
setContactEmailValue(e.target.value)
}}
onBlur={handleContactEmailBlur}
placeholder="contact@example.com"
className="w-auto"
autoComplete="off"
Expand Down Expand Up @@ -259,10 +407,6 @@ export function TrustPortalSwitch({
) : (
<p className="text-xs text-muted-foreground">Trust portal is currently disabled.</p>
)}
<Button type="submit" disabled={trustPortalSwitch.status === "executing"}>
{trustPortalSwitch.status === "executing" ? <Loader2 className="h-4 w-4 animate-spin mr-1" /> : null}
{t("common.actions.save")}
</Button>
</CardFooter>
</Card>
</form>
Expand Down
Loading
Loading