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
4 changes: 0 additions & 4 deletions apps/app/src/app/(app)/[orgId]/settings/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,6 @@ export default async function Layout({ children }: { children: React.ReactNode }
path: `/${orgId}/settings`,
label: 'General',
},
{
path: `/${orgId}/settings/trust-portal`,
label: 'Trust Portal',
},
{
path: `/${orgId}/settings/context-hub`,
label: 'Context',
Expand Down

This file was deleted.

30 changes: 30 additions & 0 deletions apps/app/src/app/(app)/[orgId]/trust/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { SecondaryMenu } from '@comp/ui/secondary-menu';

export default async function Layout({
children,
params
}: {
children: React.ReactNode;
params: Promise<{ orgId: string }>;
}) {
const { orgId } = await params;

return (
<div className="m-auto max-w-[1200px] py-8">
<SecondaryMenu
items={[
{
path: `/${orgId}/trust`,
label: 'Access & Grants',
},
{
path: `/${orgId}/trust/portal-settings`,
label: 'Portal Settings',
},
]}
/>
<div>{children}</div>
</div>
);
}

20 changes: 9 additions & 11 deletions apps/app/src/app/(app)/[orgId]/trust/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,20 @@ export default async function TrustAccessPage({ params }: { params: Promise<{ or
const { orgId } = await params;

return (
<div className="mx-auto max-w-[1200px] py-8">
<PageCore>
<div className="space-y-4">
<div>
<h1 className="text-2xl font-semibold">Trust Access Management</h1>
<p className="text-muted-foreground">Manage data access requests and grants</p>
</div>
<TrustAccessRequestsClient orgId={orgId} />
<PageCore>
<div className="space-y-4">
<div>
<h1 className="text-2xl font-semibold">Access & Grants</h1>
<p className="text-muted-foreground">Manage data access requests and grants</p>
</div>
</PageCore>
</div>
<TrustAccessRequestsClient orgId={orgId} />
</div>
</PageCore>
);
}

export async function generateMetadata(): Promise<Metadata> {
return {
title: 'Trust Access Management',
title: 'Access & Grants',
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,8 @@ export const checkDnsRecordAction = authActionClient
},
});

revalidatePath(`/${activeOrgId}/settings/trust-portal`);
revalidatePath(`/${activeOrgId}/trust`);
revalidatePath(`/${activeOrgId}/trust/portal-settings`);
revalidateTag(`organization_${activeOrgId}`);

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ export const customDomainAction = authActionClient
},
});

revalidatePath(`/${activeOrganizationId}/settings/trust-portal`);
revalidatePath(`/${activeOrganizationId}/trust`);
revalidatePath(`/${activeOrganizationId}/trust/portal-settings`);
revalidateTag(`organization_${activeOrganizationId}`);

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ export const trustPortalSwitchAction = authActionClient
},
});

revalidatePath(`/${activeOrganizationId}/settings/trust-portal`);
revalidatePath(`/${activeOrganizationId}/trust`);
revalidatePath(`/${activeOrganizationId}/trust/portal-settings`);
revalidateTag(`organization_${activeOrganizationId}`);

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export async function updateTrustPortalFrameworks({
},
});

revalidatePath(`/${orgId}/settings/trust-portal`);
revalidatePath(`/${orgId}/trust`);
revalidatePath(`/${orgId}/trust/portal-settings`);
revalidateTag(`organization_${orgId}`);
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
SOC2Type2,
} from './logos';

// Client-side form schema (includes all fields for form state)
const trustPortalSwitchSchema = z.object({
enabled: z.boolean(),
contactEmail: z.string().email().or(z.literal('')).optional(),
Expand All @@ -56,6 +57,13 @@ const trustPortalSwitchSchema = z.object({
iso9001Status: z.enum(['started', 'in_progress', 'compliant']),
});

// Server action input schema (only fields that the server accepts)
type TrustPortalSwitchActionInput = {
enabled: boolean;
contactEmail?: string | '';
friendlyUrl?: string;
};

const FRAMEWORK_KEY_TO_API_SLUG: Record<string, string> = {
iso27001: 'iso_27001',
iso42001: 'iso_42001',
Expand Down Expand Up @@ -272,6 +280,10 @@ export function TrustPortalSwitch({
},
});

// Use ref to store latest trustPortalSwitch to avoid stale closures
const trustPortalSwitchRef = useRef(trustPortalSwitch);
trustPortalSwitchRef.current = trustPortalSwitch;

const checkFriendlyUrl = useAction(isFriendlyAvailable);

const form = useForm<z.infer<typeof trustPortalSwitchSchema>>({
Expand Down Expand Up @@ -302,10 +314,10 @@ export function TrustPortalSwitch({
});

const onSubmit = useCallback(
async (data: z.infer<typeof trustPortalSwitchSchema>) => {
await trustPortalSwitch.execute(data);
async (data: TrustPortalSwitchActionInput) => {
await trustPortalSwitchRef.current.execute(data);
},
[], // Remove trustPortalSwitch from dependencies to prevent infinite loop
[], // Safe to use empty array because we use ref
);

const portalUrl = domainVerified ? `https://${domain}` : `https://trust.inc/${slug}`;
Expand All @@ -316,28 +328,51 @@ export function TrustPortalSwitch({
enabled: enabled,
});

const savingRef = useRef<{ [key: string]: boolean }>({
contactEmail: false,
friendlyUrl: false,
enabled: false,
});

const autoSave = useCallback(
async (field: string, value: any) => {
// Prevent concurrent saves for the same field
if (savingRef.current[field]) {
return;
}

const current = form.getValues();
if (lastSaved.current[field] !== value) {
const data = { ...current, [field]: value };
await onSubmit(data);
lastSaved.current[field] = value;
savingRef.current[field] = true;
try {
// Only send fields that trustPortalSwitchAction accepts
// Server schema only accepts: enabled, contactEmail, friendlyUrl
const data: TrustPortalSwitchActionInput = {
enabled: field === 'enabled' ? value : current.enabled,
contactEmail: field === 'contactEmail' ? value : current.contactEmail ?? '',
friendlyUrl: field === 'friendlyUrl' ? value : current.friendlyUrl ?? undefined,
};
await onSubmit(data);
lastSaved.current[field] = value;
} finally {
savingRef.current[field] = false;
}
}
},
[form, onSubmit],
);

const [contactEmailValue, setContactEmailValue] = useState(form.getValues('contactEmail') || '');
const debouncedContactEmail = useDebounce(contactEmailValue, 500);
const debouncedContactEmail = useDebounce(contactEmailValue, 800);

useEffect(() => {
if (
debouncedContactEmail !== undefined &&
debouncedContactEmail !== lastSaved.current.contactEmail
debouncedContactEmail !== lastSaved.current.contactEmail &&
!savingRef.current.contactEmail
) {
form.setValue('contactEmail', debouncedContactEmail);
autoSave('contactEmail', debouncedContactEmail);
void autoSave('contactEmail', debouncedContactEmail);
}
}, [debouncedContactEmail, autoSave, form]);

Expand All @@ -351,30 +386,62 @@ export function TrustPortalSwitch({
);

const [friendlyUrlValue, setFriendlyUrlValue] = useState(form.getValues('friendlyUrl') || '');
const debouncedFriendlyUrl = useDebounce(friendlyUrlValue, 500);
const debouncedFriendlyUrl = useDebounce(friendlyUrlValue, 700);
const [friendlyUrlStatus, setFriendlyUrlStatus] = useState<
'idle' | 'checking' | 'available' | 'unavailable'
>('idle');
const lastCheckedUrlRef = useRef<string>('');
const processingResultRef = useRef<string>('');

useEffect(() => {
if (!debouncedFriendlyUrl || debouncedFriendlyUrl === (friendlyUrl ?? '')) {
setFriendlyUrlStatus('idle');
lastCheckedUrlRef.current = '';
processingResultRef.current = '';
return;
}

// Only check if we haven't already checked this exact value
if (lastCheckedUrlRef.current === debouncedFriendlyUrl) {
return;
}

lastCheckedUrlRef.current = debouncedFriendlyUrl;
processingResultRef.current = '';
setFriendlyUrlStatus('checking');
checkFriendlyUrl.execute({ friendlyUrl: debouncedFriendlyUrl, orgId });
}, [debouncedFriendlyUrl, orgId, friendlyUrl]);

useEffect(() => {
if (checkFriendlyUrl.status === 'executing') return;
if (checkFriendlyUrl.result?.data?.isAvailable === true) {

const result = checkFriendlyUrl.result?.data;
const checkedUrl = lastCheckedUrlRef.current;

// Only process if this result matches the currently checked URL
if (checkedUrl !== debouncedFriendlyUrl || !checkedUrl) {
return;
}

// Prevent processing the same result multiple times
if (processingResultRef.current === checkedUrl) {
return;
}

if (result?.isAvailable === true) {
setFriendlyUrlStatus('available');
processingResultRef.current = checkedUrl;

if (debouncedFriendlyUrl !== lastSaved.current.friendlyUrl) {
if (
debouncedFriendlyUrl !== lastSaved.current.friendlyUrl &&
!savingRef.current.friendlyUrl
) {
form.setValue('friendlyUrl', debouncedFriendlyUrl);
autoSave('friendlyUrl', debouncedFriendlyUrl);
void autoSave('friendlyUrl', debouncedFriendlyUrl);
}
} else if (checkFriendlyUrl.result?.data?.isAvailable === false) {
} else if (result?.isAvailable === false) {
setFriendlyUrlStatus('unavailable');
processingResultRef.current = checkedUrl;
}
}, [checkFriendlyUrl.status, checkFriendlyUrl.result, debouncedFriendlyUrl, form, autoSave]);

Expand All @@ -400,42 +467,40 @@ export function TrustPortalSwitch({
return (
<Form {...form}>
<form className="space-y-4">
<Card className="overflow-hidden">
<CardHeader className="pb-4">
<div className="flex items-center justify-between">
<div className="space-y-1">
<CardTitle className="flex items-center gap-2">
<div className="space-y-6">
<div className="flex items-center justify-between pb-4">
<div className="space-y-1">
<h2 className="text-lg font-medium flex items-center gap-2">
<Link
href={portalUrl}
target="_blank"
className="text-primary hover:underline flex items-center gap-2"
>
Trust Portal
<Link
href={portalUrl}
target="_blank"
className="text-muted-foreground hover:text-foreground text-sm"
>
<ExternalLink className="h-4 w-4" />
</Link>
</CardTitle>
<p className="text-muted-foreground text-sm">
Create a public trust portal for your organization.
</p>
</div>
<FormField
control={form.control}
name="enabled"
render={({ field }) => (
<FormItem className="flex items-center space-y-0 space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={handleEnabledChange}
disabled={trustPortalSwitch.status === 'executing'}
/>
</FormControl>
</FormItem>
)}
/>
<ExternalLink className="h-4 w-4" />
</Link>
</h2>
<p className="text-muted-foreground text-sm">
Create a public trust portal for your organization.
</p>
</div>
</CardHeader>
<CardContent className="space-y-6 pt-0">
<FormField
control={form.control}
name="enabled"
render={({ field }) => (
<FormItem className="flex items-center space-y-0 space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={handleEnabledChange}
disabled={trustPortalSwitch.status === 'executing'}
/>
</FormControl>
</FormItem>
)}
/>
</div>
<div className="space-y-6">
{form.watch('enabled') && (
<div className="pt-2">
<h3 className="mb-4 text-sm font-medium">Trust Portal Settings</h3>
Expand Down Expand Up @@ -831,8 +896,8 @@ export function TrustPortalSwitch({
</div>
</div>
)}
</CardContent>
</Card>
</div>
</div>
</form>
</Form>
);
Expand Down
Loading
Loading