mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-26 14:56:39 +00:00
Add translation keys in settings/access/invitations
This commit is contained in:
@@ -22,7 +22,7 @@ export function InvitationsDataTable<TData, TValue>({
|
|||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={data}
|
data={data}
|
||||||
title="Invitations"
|
title={t('invite')}
|
||||||
searchPlaceholder={t('inviteSearch')}
|
searchPlaceholder={t('inviteSearch')}
|
||||||
searchColumn="email"
|
searchColumn="email"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { useOrgContext } from "@app/hooks/useOrgContext";
|
|||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export type InvitationRow = {
|
export type InvitationRow = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -39,6 +40,8 @@ export default function InvitationsTable({
|
|||||||
const [selectedInvitation, setSelectedInvitation] =
|
const [selectedInvitation, setSelectedInvitation] =
|
||||||
useState<InvitationRow | null>(null);
|
useState<InvitationRow | null>(null);
|
||||||
|
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
const { org } = useOrgContext();
|
const { org } = useOrgContext();
|
||||||
|
|
||||||
@@ -51,7 +54,7 @@ export default function InvitationsTable({
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
<span className="sr-only">Open menu</span>
|
<span className="sr-only">{t('openMenu')}</span>
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@@ -62,7 +65,7 @@ export default function InvitationsTable({
|
|||||||
setSelectedInvitation(invitation);
|
setSelectedInvitation(invitation);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>Regenerate Invitation</span>
|
<span>{t('inviteRegenerate')}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -71,7 +74,7 @@ export default function InvitationsTable({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="text-red-500">
|
<span className="text-red-500">
|
||||||
Remove Invitation
|
{t('inviteRemove')}
|
||||||
</span>
|
</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
@@ -112,17 +115,16 @@ export default function InvitationsTable({
|
|||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: "Failed to remove invitation",
|
title: t('inviteRemoveError'),
|
||||||
description:
|
description: t('inviteRemoveErrorDescription')
|
||||||
"An error occurred while removing the invitation."
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res && res.status === 200) {
|
if (res && res.status === 200) {
|
||||||
toast({
|
toast({
|
||||||
variant: "default",
|
variant: "default",
|
||||||
title: "Invitation removed",
|
title: t('inviteRemoved'),
|
||||||
description: `The invitation for ${selectedInvitation.email} has been removed.`
|
description: t('inviteRemovedDescription', {email: selectedInvitation.email})
|
||||||
});
|
});
|
||||||
|
|
||||||
setInvitations((prev) =>
|
setInvitations((prev) =>
|
||||||
@@ -146,23 +148,20 @@ export default function InvitationsTable({
|
|||||||
dialog={
|
dialog={
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<p>
|
<p>
|
||||||
Are you sure you want to remove the invitation for{" "}
|
{t('inviteQuestionRemove', {email: selectedInvitation?.email ?? ''})}
|
||||||
<b>{selectedInvitation?.email}</b>?
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Once removed, this invitation will no longer be
|
{t('inviteMessageRemove')}
|
||||||
valid. You can always re-invite the user later.
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
To confirm, please type the email address of the
|
{t('inviteMessageConfirm')}
|
||||||
invitation below.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
buttonText="Confirm Remove Invitation"
|
buttonText={t('inviteRemoveConfirm')}
|
||||||
onConfirm={removeInvitation}
|
onConfirm={removeInvitation}
|
||||||
string={selectedInvitation?.email ?? ""}
|
string={selectedInvitation?.email ?? ""}
|
||||||
title="Remove Invitation"
|
title={t('inviteRemove')}
|
||||||
/>
|
/>
|
||||||
<RegenerateInvitationForm
|
<RegenerateInvitationForm
|
||||||
open={isRegenerateModalOpen}
|
open={isRegenerateModalOpen}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
SelectValue
|
SelectValue
|
||||||
} from "@app/components/ui/select";
|
} from "@app/components/ui/select";
|
||||||
import { Label } from "@app/components/ui/label";
|
import { Label } from "@app/components/ui/label";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
type RegenerateInvitationFormProps = {
|
type RegenerateInvitationFormProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -56,14 +57,16 @@ export default function RegenerateInvitationForm({
|
|||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
const { org } = useOrgContext();
|
const { org } = useOrgContext();
|
||||||
|
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
const validForOptions = [
|
const validForOptions = [
|
||||||
{ hours: 24, name: "1 day" },
|
{ hours: 24, name: t('day', { count: 1 }) },
|
||||||
{ hours: 48, name: "2 days" },
|
{ hours: 48, name: t('day', { count: 2 }) },
|
||||||
{ hours: 72, name: "3 days" },
|
{ hours: 72, name: t('day', { count: 3 }) },
|
||||||
{ hours: 96, name: "4 days" },
|
{ hours: 96, name: t('day', { count: 4 }) },
|
||||||
{ hours: 120, name: "5 days" },
|
{ hours: 120, name: t('day', { count: 5 }) },
|
||||||
{ hours: 144, name: "6 days" },
|
{ hours: 144, name: t('day', { count: 6 }) },
|
||||||
{ hours: 168, name: "7 days" }
|
{ hours: 168, name: t('day', { count: 7 }) }
|
||||||
];
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -79,9 +82,8 @@ export default function RegenerateInvitationForm({
|
|||||||
if (!org?.org.orgId) {
|
if (!org?.org.orgId) {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: "Organization ID Missing",
|
title: t('orgMissing'),
|
||||||
description:
|
description: t('orgMissingMessage'),
|
||||||
"Unable to regenerate invitation without an organization ID.",
|
|
||||||
duration: 5000
|
duration: 5000
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -105,15 +107,15 @@ export default function RegenerateInvitationForm({
|
|||||||
if (sendEmail) {
|
if (sendEmail) {
|
||||||
toast({
|
toast({
|
||||||
variant: "default",
|
variant: "default",
|
||||||
title: "Invitation Regenerated",
|
title: t('inviteRegenerated'),
|
||||||
description: `A new invitation has been sent to ${invitation.email}.`,
|
description: t('inviteSent', {email: invitation.email}),
|
||||||
duration: 5000
|
duration: 5000
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
toast({
|
toast({
|
||||||
variant: "default",
|
variant: "default",
|
||||||
title: "Invitation Regenerated",
|
title: t('inviteRegenerated'),
|
||||||
description: `A new invitation has been generated for ${invitation.email}.`,
|
description: t('inviteGenerate', {email: invitation.email}),
|
||||||
duration: 5000
|
duration: 5000
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -130,24 +132,22 @@ export default function RegenerateInvitationForm({
|
|||||||
if (error.response?.status === 409) {
|
if (error.response?.status === 409) {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: "Duplicate Invite",
|
title: t('inviteDuplicateError'),
|
||||||
description: "An invitation for this user already exists.",
|
description: t('inviteDuplicateErrorDescription'),
|
||||||
duration: 5000
|
duration: 5000
|
||||||
});
|
});
|
||||||
} else if (error.response?.status === 429) {
|
} else if (error.response?.status === 429) {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: "Rate Limit Exceeded",
|
title: t('inviteRateLimitError'),
|
||||||
description:
|
description: t('inviteRateLimitErrorDescription'),
|
||||||
"You have exceeded the limit of 3 regenerations per hour. Please try again later.",
|
|
||||||
duration: 5000
|
duration: 5000
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: "Failed to Regenerate Invitation",
|
title: t('inviteRegenerateError'),
|
||||||
description:
|
description: t('inviteRegenerateErrorDescription'),
|
||||||
"An error occurred while regenerating the invitation.",
|
|
||||||
duration: 5000
|
duration: 5000
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -168,18 +168,16 @@ export default function RegenerateInvitationForm({
|
|||||||
>
|
>
|
||||||
<CredenzaContent>
|
<CredenzaContent>
|
||||||
<CredenzaHeader>
|
<CredenzaHeader>
|
||||||
<CredenzaTitle>Regenerate Invitation</CredenzaTitle>
|
<CredenzaTitle>{t('inviteRegenerate')}</CredenzaTitle>
|
||||||
<CredenzaDescription>
|
<CredenzaDescription>
|
||||||
Revoke previous invitation and create a new one
|
{t('inviteRegenerateDescription')}
|
||||||
</CredenzaDescription>
|
</CredenzaDescription>
|
||||||
</CredenzaHeader>
|
</CredenzaHeader>
|
||||||
<CredenzaBody>
|
<CredenzaBody>
|
||||||
{!inviteLink ? (
|
{!inviteLink ? (
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
Are you sure you want to regenerate the
|
{t('inviteQuestionRegenerate', {email: invitation?.email ?? ''})}
|
||||||
invitation for <b>{invitation?.email}</b>? This
|
|
||||||
will revoke the previous invitation.
|
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center space-x-2 mt-4">
|
<div className="flex items-center space-x-2 mt-4">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -190,12 +188,12 @@ export default function RegenerateInvitationForm({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="send-email">
|
<label htmlFor="send-email">
|
||||||
Send email notification to the user
|
{t('inviteSentEmail')}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 space-y-2">
|
<div className="mt-4 space-y-2">
|
||||||
<Label>
|
<Label>
|
||||||
Validity Period
|
{t('inviteValidityPeriod')}
|
||||||
</Label>
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
value={validHours.toString()}
|
value={validHours.toString()}
|
||||||
@@ -204,7 +202,7 @@ export default function RegenerateInvitationForm({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select validity period" />
|
<SelectValue placeholder={t('inviteValidityPeriodSelect')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{validForOptions.map((option) => (
|
{validForOptions.map((option) => (
|
||||||
@@ -222,9 +220,7 @@ export default function RegenerateInvitationForm({
|
|||||||
) : (
|
) : (
|
||||||
<div className="space-y-4 max-w-md">
|
<div className="space-y-4 max-w-md">
|
||||||
<p>
|
<p>
|
||||||
The invitation has been regenerated. The user
|
{t('inviteRegenerateMessage')}
|
||||||
must access the link below to accept the
|
|
||||||
invitation.
|
|
||||||
</p>
|
</p>
|
||||||
<CopyTextBox text={inviteLink} wrapText={false} />
|
<CopyTextBox text={inviteLink} wrapText={false} />
|
||||||
</div>
|
</div>
|
||||||
@@ -240,7 +236,7 @@ export default function RegenerateInvitationForm({
|
|||||||
onClick={handleRegenerate}
|
onClick={handleRegenerate}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
>
|
>
|
||||||
Regenerate
|
{t('inviteRegenerateButton')}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user