mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-05 10:16:41 +00:00
I18n orgId/settings (#21)
* New translation keys in en-US locale * New translation keys in de-DE locale * New translation keys in fr-FR locale * New translation keys in it-IT locale * New translation keys in pl-PL locale * New translation keys in pt-PT locale * New translation keys in tr-TR locale * Add translation keys if settings/resources/resourceId/authentication * New translation keys in en-US locale * New translation keys in de-DE locale * New translation keys in fr-FR locale * New translation keys in it-IT locale * New translation keys in pl-PL locale * New translation keys in pt-PT locale * New translation keys in tr-TR locale * Add translation keys if settings/resources/resourceId/general * Small naming fix * New translation keys in en-US locale * New translation keys in de-DE locale * New translation keys in fr-FR locale * New translation keys in it-IT locale * New translation keys in pl-PL locale * New translation keys in pt-PT locale * New translation keys in tr-TR locale * Add translation keys if settings/access/roles * New translation keys in en-US locale * New translation keys in de-DE locale * New translation keys in fr-FR locale * New translation keys in it-IT locale * New translation keys in pl-PL locale * New translation keys in pt-PT locale * New translation keys in tr-TR locale * Add translation keys in orgId/settings * Fixes after merge * Fixes after merge * Fixes after merge * Small fix * Fix build
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface AccessPageHeaderAndNavProps {
|
||||
children: React.ReactNode;
|
||||
@@ -12,20 +13,21 @@ export default function AccessPageHeaderAndNav({
|
||||
children,
|
||||
hasInvitations
|
||||
}: AccessPageHeaderAndNavProps) {
|
||||
const t = useTranslations();
|
||||
const navItems = [
|
||||
{
|
||||
title: "Users",
|
||||
title: t('users'),
|
||||
href: `/{orgId}/settings/access/users`
|
||||
},
|
||||
{
|
||||
title: "Roles",
|
||||
title: t('roles'),
|
||||
href: `/{orgId}/settings/access/roles`
|
||||
}
|
||||
];
|
||||
|
||||
if (hasInvitations) {
|
||||
navItems.push({
|
||||
title: "Invitations",
|
||||
title: t('invite'),
|
||||
href: `/{orgId}/settings/access/invitations`
|
||||
});
|
||||
}
|
||||
@@ -33,8 +35,8 @@ export default function AccessPageHeaderAndNav({
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title="Manage Users & Roles"
|
||||
description="Invite users and add them to roles to manage access to your organization"
|
||||
title={t('accessUsersRoles')}
|
||||
description={t('accessUsersRolesDescription')}
|
||||
/>
|
||||
|
||||
<HorizontalTabs items={navItems}>
|
||||
|
||||
@@ -148,7 +148,7 @@ export default function InvitationsTable({
|
||||
dialog={
|
||||
<div className="space-y-4">
|
||||
<p>
|
||||
{t('inviteQuestionRemove', {email: selectedInvitation?.email})}
|
||||
{t('inviteQuestionRemove', {email: selectedInvitation?.email || ''})}
|
||||
</p>
|
||||
<p>
|
||||
{t('inviteMessageRemove')}
|
||||
|
||||
@@ -177,7 +177,7 @@ export default function RegenerateInvitationForm({
|
||||
{!inviteLink ? (
|
||||
<div>
|
||||
<p>
|
||||
{t('inviteQuestionRegenerate', {email: invitation?.email})}
|
||||
{t('inviteQuestionRegenerate', {email: invitation?.email || ''})}
|
||||
</p>
|
||||
<div className="flex items-center space-x-2 mt-4">
|
||||
<Checkbox
|
||||
|
||||
@@ -67,7 +67,7 @@ export default async function InvitationsPage(props: InvitationsPageProps) {
|
||||
id: invite.inviteId,
|
||||
email: invite.email,
|
||||
expiresAt: new Date(Number(invite.expiresAt)).toISOString(),
|
||||
role: invite.roleName || "Unknown Role",
|
||||
role: invite.roleName || t('accessRoleUnknown'),
|
||||
roleId: invite.roleId
|
||||
};
|
||||
});
|
||||
|
||||
@@ -31,6 +31,7 @@ import { CreateRoleBody, CreateRoleResponse } from "@server/routers/role";
|
||||
import { formatAxiosError } from "@app/lib/api";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type CreateRoleFormProps = {
|
||||
open: boolean;
|
||||
@@ -38,8 +39,10 @@ type CreateRoleFormProps = {
|
||||
afterCreate?: (res: CreateRoleResponse) => Promise<void>;
|
||||
};
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string({ message: "Name is required" }).max(32),
|
||||
name: z.string({ message: t('accessRoleNameRequired') }).max(32),
|
||||
description: z.string().max(255).optional()
|
||||
});
|
||||
|
||||
@@ -76,10 +79,10 @@ export default function CreateRoleForm({
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to create role",
|
||||
title: t('accessRoleErrorCreate'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while creating the role."
|
||||
t('accessRoleErrorCreateDescription')
|
||||
)
|
||||
});
|
||||
});
|
||||
@@ -87,8 +90,8 @@ export default function CreateRoleForm({
|
||||
if (res && res.status === 201) {
|
||||
toast({
|
||||
variant: "default",
|
||||
title: "Role created",
|
||||
description: "The role has been successfully created."
|
||||
title: t('accessRoleCreated'),
|
||||
description: t('accessRoleCreatedDescription')
|
||||
});
|
||||
|
||||
if (open) {
|
||||
@@ -115,10 +118,9 @@ export default function CreateRoleForm({
|
||||
>
|
||||
<CredenzaContent>
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>Create Role</CredenzaTitle>
|
||||
<CredenzaTitle>{t('accessRoleCreate')}</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
Create a new role to group users and manage their
|
||||
permissions.
|
||||
{t('accessRoleCreateDescription')}
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
@@ -133,7 +135,7 @@ export default function CreateRoleForm({
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Role Name</FormLabel>
|
||||
<FormLabel>{t('accessRoleName')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
@@ -146,7 +148,7 @@ export default function CreateRoleForm({
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormLabel>{t('description')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
@@ -159,7 +161,7 @@ export default function CreateRoleForm({
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
<Button variant="outline">{t('close')}</Button>
|
||||
</CredenzaClose>
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -167,7 +169,7 @@ export default function CreateRoleForm({
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
>
|
||||
Create Role
|
||||
{t('accessRoleCreateSubmit')}
|
||||
</Button>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
|
||||
@@ -38,6 +38,7 @@ import { RoleRow } from "./RolesTable";
|
||||
import { formatAxiosError } from "@app/lib/api";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type CreateRoleFormProps = {
|
||||
open: boolean;
|
||||
@@ -46,8 +47,10 @@ type CreateRoleFormProps = {
|
||||
afterDelete?: () => void;
|
||||
};
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
const formSchema = z.object({
|
||||
newRoleId: z.string({ message: "New role is required" })
|
||||
newRoleId: z.string({ message: t('accessRoleErrorNewRequired') })
|
||||
});
|
||||
|
||||
export default function DeleteRoleForm({
|
||||
@@ -73,10 +76,10 @@ export default function DeleteRoleForm({
|
||||
console.error(e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to fetch roles",
|
||||
title: t('accessRoleErrorFetch'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while fetching the roles"
|
||||
t('accessRoleErrorFetchDescription')
|
||||
)
|
||||
});
|
||||
});
|
||||
@@ -112,10 +115,10 @@ export default function DeleteRoleForm({
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to remove role",
|
||||
title: t('accessRoleErrorRemove'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while removing the role."
|
||||
t('accessRoleErrorRemoveDescription')
|
||||
)
|
||||
});
|
||||
});
|
||||
@@ -123,8 +126,8 @@ export default function DeleteRoleForm({
|
||||
if (res && res.status === 200) {
|
||||
toast({
|
||||
variant: "default",
|
||||
title: "Role removed",
|
||||
description: "The role has been successfully removed."
|
||||
title: t('accessRoleRemoved'),
|
||||
description: t('accessRoleRemovedDescription')
|
||||
});
|
||||
|
||||
if (open) {
|
||||
@@ -151,22 +154,19 @@ export default function DeleteRoleForm({
|
||||
>
|
||||
<CredenzaContent>
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>Remove Role</CredenzaTitle>
|
||||
<CredenzaTitle>{t('accessRoleRemove')}</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
Remove a role from the organization
|
||||
{t('accessRoleRemoveDescription')}
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4">
|
||||
<p>
|
||||
You're about to delete the{" "}
|
||||
<b>{roleToDelete.name}</b> role. You cannot
|
||||
undo this action.
|
||||
{t('accessRoleQuestionRemove', {name: roleToDelete.name})}
|
||||
</p>
|
||||
<p>
|
||||
Before deleting this role, please select a
|
||||
new role to transfer existing members to.
|
||||
{t('accessRoleRequiredRemove')}
|
||||
</p>
|
||||
</div>
|
||||
<Form {...form}>
|
||||
@@ -180,7 +180,7 @@ export default function DeleteRoleForm({
|
||||
name="newRoleId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Role</FormLabel>
|
||||
<FormLabel>{t('role')}</FormLabel>
|
||||
<Select
|
||||
onValueChange={
|
||||
field.onChange
|
||||
@@ -189,7 +189,7 @@ export default function DeleteRoleForm({
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select role" />
|
||||
<SelectValue placeholder={t('accessRoleSelect')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
@@ -215,7 +215,7 @@ export default function DeleteRoleForm({
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
<Button variant="outline">{t('close')}</Button>
|
||||
</CredenzaClose>
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -223,7 +223,7 @@ export default function DeleteRoleForm({
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
>
|
||||
Remove Role
|
||||
{t('accessRoleRemoveSubmit')}
|
||||
</Button>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
|
||||
@@ -24,7 +24,7 @@ export function UsersDataTable<TData, TValue>({
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
title="Users"
|
||||
title={t('users')}
|
||||
searchPlaceholder={t('accessUsersSearch')}
|
||||
searchColumn="email"
|
||||
onAdd={inviteUser}
|
||||
|
||||
@@ -181,7 +181,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
||||
variant="ghost"
|
||||
className="opacity-0 cursor-default"
|
||||
>
|
||||
Placeholder
|
||||
{t('placeholder')}
|
||||
</Button>
|
||||
)}
|
||||
{!userRow.isOwner && (
|
||||
@@ -192,7 +192,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
||||
variant={"outlinePrimary"}
|
||||
className="ml-2"
|
||||
>
|
||||
Manage
|
||||
{t('manage')}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
@@ -210,10 +210,10 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to remove user",
|
||||
title: t('userErrorOrgRemove'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while removing the user."
|
||||
t('userErrorOrgRemoveDescription')
|
||||
)
|
||||
});
|
||||
});
|
||||
@@ -221,8 +221,8 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
||||
if (res && res.status === 200) {
|
||||
toast({
|
||||
variant: "default",
|
||||
title: "User removed",
|
||||
description: `The user ${selectedUser.email} has been removed from the organization.`
|
||||
title: t('userOrgRemoved'),
|
||||
description: t('userOrgRemovedDescription', {email: selectedUser.email || ''})
|
||||
});
|
||||
|
||||
setUsers((prev) =>
|
||||
@@ -244,29 +244,19 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
||||
dialog={
|
||||
<div className="space-y-4">
|
||||
<p>
|
||||
Are you sure you want to remove{" "}
|
||||
<b>
|
||||
{selectedUser?.email ||
|
||||
selectedUser?.name ||
|
||||
selectedUser?.username}
|
||||
</b>{" "}
|
||||
from the organization?
|
||||
{t('userQuestionOrgRemove', {email: selectedUser?.email || selectedUser?.name || selectedUser?.username || ''})}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Once removed, this user will no longer have access
|
||||
to the organization. You can always re-invite them
|
||||
later, but they will need to accept the invitation
|
||||
again.
|
||||
{t('userMessageOrgRemove')}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
To confirm, please type the name of the of the user
|
||||
below.
|
||||
{t('userMessageOrgConfirm')}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
buttonText="Confirm Remove User"
|
||||
buttonText={t('userRemoveOrgConfirm')}
|
||||
onConfirm={removeUser}
|
||||
string={
|
||||
selectedUser?.email ||
|
||||
@@ -274,7 +264,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
||||
selectedUser?.username ||
|
||||
""
|
||||
}
|
||||
title="Remove User from Organization"
|
||||
title={t('userRemoveOrg')}
|
||||
/>
|
||||
|
||||
<UsersDataTable
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
import { formatAxiosError } from "@app/lib/api";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const formSchema = z.object({
|
||||
username: z.string(),
|
||||
@@ -64,6 +65,8 @@ export default function AccessControlsPage() {
|
||||
}
|
||||
});
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchRoles() {
|
||||
const res = await api
|
||||
@@ -72,10 +75,10 @@ export default function AccessControlsPage() {
|
||||
console.error(e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to fetch roles",
|
||||
title: t('accessRoleErrorFetch'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while fetching the roles"
|
||||
t('accessRoleErrorFetchDescription')
|
||||
)
|
||||
});
|
||||
});
|
||||
@@ -100,10 +103,10 @@ export default function AccessControlsPage() {
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to add user to role",
|
||||
title: t('accessRoleErrorAdd'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while adding user to the role."
|
||||
t('accessRoleErrorAddDescription')
|
||||
)
|
||||
});
|
||||
});
|
||||
@@ -111,8 +114,8 @@ export default function AccessControlsPage() {
|
||||
if (res && res.status === 200) {
|
||||
toast({
|
||||
variant: "default",
|
||||
title: "User saved",
|
||||
description: "The user has been updated."
|
||||
title: t('userSaved'),
|
||||
description: t('userSavedDescription')
|
||||
});
|
||||
}
|
||||
|
||||
@@ -123,10 +126,9 @@ export default function AccessControlsPage() {
|
||||
<SettingsContainer>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>Access Controls</SettingsSectionTitle>
|
||||
<SettingsSectionTitle>{t('accessControls')}</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Manage what this user can access and do in the
|
||||
organization
|
||||
{t('accessControlsDescription')}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
|
||||
@@ -143,14 +145,14 @@ export default function AccessControlsPage() {
|
||||
name="roleId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Role</FormLabel>
|
||||
<FormLabel>{t('role')}</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select role" />
|
||||
<SelectValue placeholder={t('accessRoleSelect')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
@@ -180,7 +182,7 @@ export default function AccessControlsPage() {
|
||||
disabled={loading}
|
||||
form="access-controls-form"
|
||||
>
|
||||
Save Access Controls
|
||||
{t('accessControlsSubmit')}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import Link from "next/link";
|
||||
import { cache } from "react";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface UserLayoutProps {
|
||||
children: React.ReactNode;
|
||||
@@ -26,6 +27,8 @@ export default async function UserLayoutProps(props: UserLayoutProps) {
|
||||
|
||||
const { children } = props;
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
let user = null;
|
||||
try {
|
||||
const getOrgUser = cache(async () =>
|
||||
@@ -42,7 +45,7 @@ export default async function UserLayoutProps(props: UserLayoutProps) {
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
title: "Access Controls",
|
||||
title: t('accessControls'),
|
||||
href: "/{orgId}/settings/access/users/{userId}/access-controls"
|
||||
}
|
||||
];
|
||||
@@ -51,7 +54,7 @@ export default async function UserLayoutProps(props: UserLayoutProps) {
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title={`${user?.email}`}
|
||||
description="Manage the settings on this user"
|
||||
description={t('userDescription2')}
|
||||
/>
|
||||
<OrgUserProvider orgUser={user}>
|
||||
<HorizontalTabs items={navItems}>
|
||||
|
||||
@@ -44,6 +44,7 @@ import { formatAxiosError } from "@app/lib/api";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { Checkbox } from "@app/components/ui/checkbox";
|
||||
import { ListIdpsResponse } from "@server/routers/idp";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type UserType = "internal" | "oidc";
|
||||
|
||||
@@ -59,28 +60,30 @@ interface IdpOption {
|
||||
type: string;
|
||||
}
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
const internalFormSchema = z.object({
|
||||
email: z.string().email({ message: "Invalid email address" }),
|
||||
validForHours: z.string().min(1, { message: "Please select a duration" }),
|
||||
roleId: z.string().min(1, { message: "Please select a role" })
|
||||
email: z.string().email({ message: t('emailInvalid') }),
|
||||
validForHours: z.string().min(1, { message: t('inviteValidityDuration') }),
|
||||
roleId: z.string().min(1, { message: t('accessRoleSelectPlease') })
|
||||
});
|
||||
|
||||
const externalFormSchema = z.object({
|
||||
username: z.string().min(1, { message: "Username is required" }),
|
||||
username: z.string().min(1, { message: t('usernameRequired') }),
|
||||
email: z
|
||||
.string()
|
||||
.email({ message: "Invalid email address" })
|
||||
.email({ message: t('emailInvalid') })
|
||||
.optional()
|
||||
.or(z.literal("")),
|
||||
name: z.string().optional(),
|
||||
roleId: z.string().min(1, { message: "Please select a role" }),
|
||||
idpId: z.string().min(1, { message: "Please select an identity provider" })
|
||||
roleId: z.string().min(1, { message: t('accessRoleSelectPlease') }),
|
||||
idpId: z.string().min(1, { message: t('idpSelectPlease') })
|
||||
});
|
||||
|
||||
const formatIdpType = (type: string) => {
|
||||
switch (type.toLowerCase()) {
|
||||
case "oidc":
|
||||
return "Generic OAuth2/OIDC provider.";
|
||||
return t('idpGenericOidc');
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
@@ -103,13 +106,13 @@ export default function Page() {
|
||||
const [dataLoaded, setDataLoaded] = useState(false);
|
||||
|
||||
const validFor = [
|
||||
{ hours: 24, name: "1 day" },
|
||||
{ hours: 48, name: "2 days" },
|
||||
{ hours: 72, name: "3 days" },
|
||||
{ hours: 96, name: "4 days" },
|
||||
{ hours: 120, name: "5 days" },
|
||||
{ hours: 144, name: "6 days" },
|
||||
{ hours: 168, name: "7 days" }
|
||||
{ hours: 24, name: t('day', {count: 1}) },
|
||||
{ hours: 48, name: t('day', {count: 2}) },
|
||||
{ hours: 72, name: t('day', {count: 3}) },
|
||||
{ hours: 96, name: t('day', {count: 4}) },
|
||||
{ hours: 120, name: t('day', {count: 5}) },
|
||||
{ hours: 144, name: t('day', {count: 6}) },
|
||||
{ hours: 168, name: t('day', {count: 7}) }
|
||||
];
|
||||
|
||||
const internalForm = useForm<z.infer<typeof internalFormSchema>>({
|
||||
@@ -155,10 +158,10 @@ export default function Page() {
|
||||
console.error(e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to fetch roles",
|
||||
title: t('accessRoleErrorFetch'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while fetching the roles"
|
||||
t('accessRoleErrorFetchDescription')
|
||||
)
|
||||
});
|
||||
});
|
||||
@@ -178,10 +181,10 @@ export default function Page() {
|
||||
console.error(e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to fetch identity providers",
|
||||
title: t('idpErrorFetch'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while fetching identity providers"
|
||||
t('idpErrorFetchDescription')
|
||||
)
|
||||
});
|
||||
});
|
||||
@@ -218,17 +221,16 @@ export default function Page() {
|
||||
if (e.response?.status === 409) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "User Already Exists",
|
||||
description:
|
||||
"This user is already a member of the organization."
|
||||
title: t('userErrorExists'),
|
||||
description: t('userErrorExistsDescription')
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to invite user",
|
||||
title: t('inviteError'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while inviting the user"
|
||||
t('inviteErrorDescription')
|
||||
)
|
||||
});
|
||||
}
|
||||
@@ -238,8 +240,8 @@ export default function Page() {
|
||||
setInviteLink(res.data.data.inviteLink);
|
||||
toast({
|
||||
variant: "default",
|
||||
title: "User invited",
|
||||
description: "The user has been successfully invited."
|
||||
title: t('userInvited'),
|
||||
description: t('userInvitedDescription')
|
||||
});
|
||||
|
||||
setExpiresInDays(parseInt(values.validForHours) / 24);
|
||||
@@ -265,10 +267,10 @@ export default function Page() {
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to create user",
|
||||
title: t('userErrorCreate'),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while creating the user"
|
||||
t('userErrorCreateDescription')
|
||||
)
|
||||
});
|
||||
});
|
||||
@@ -276,8 +278,8 @@ export default function Page() {
|
||||
if (res && res.status === 201) {
|
||||
toast({
|
||||
variant: "default",
|
||||
title: "User created",
|
||||
description: "The user has been successfully created."
|
||||
title: t('userCreated'),
|
||||
description: t('userCreatedDescription')
|
||||
});
|
||||
router.push(`/${orgId}/settings/access/users`);
|
||||
}
|
||||
@@ -288,13 +290,13 @@ export default function Page() {
|
||||
const userTypes: ReadonlyArray<UserTypeOption> = [
|
||||
{
|
||||
id: "internal",
|
||||
title: "Internal User",
|
||||
description: "Invite a user to join your organization directly."
|
||||
title: t('userTypeInternal'),
|
||||
description: t('userTypeInternalDescription')
|
||||
},
|
||||
{
|
||||
id: "oidc",
|
||||
title: "External User",
|
||||
description: "Create a user with an external identity provider."
|
||||
title: t('userTypeExternal'),
|
||||
description: t('userTypeExternalDescription')
|
||||
}
|
||||
];
|
||||
|
||||
@@ -302,8 +304,8 @@ export default function Page() {
|
||||
<>
|
||||
<div className="flex justify-between">
|
||||
<HeaderTitle
|
||||
title="Create User"
|
||||
description="Follow the steps below to create a new user"
|
||||
title={t('accessUserCreate')}
|
||||
description={t('accessUserCreateDescription')}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -311,7 +313,7 @@ export default function Page() {
|
||||
router.push(`/${orgId}/settings/access/users`);
|
||||
}}
|
||||
>
|
||||
See All Users
|
||||
{t('userSeeAll')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -320,10 +322,10 @@ export default function Page() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
User Type
|
||||
{t('userTypeTitle')}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Determine how you want to create the user
|
||||
{t('userTypeDescription')}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -349,10 +351,10 @@ export default function Page() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
User Information
|
||||
{t('userInfo')}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Enter the details for the new user
|
||||
{t('userSettingsDescription')}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -373,7 +375,7 @@ export default function Page() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Email
|
||||
{t('email')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -402,8 +404,7 @@ export default function Page() {
|
||||
htmlFor="send-email"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Send invite email to
|
||||
user
|
||||
{t('inviteEmailSent')}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
@@ -416,7 +417,7 @@ export default function Page() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Valid For
|
||||
{t('inviteValid')}
|
||||
</FormLabel>
|
||||
<Select
|
||||
onValueChange={
|
||||
@@ -428,7 +429,7 @@ export default function Page() {
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select duration" />
|
||||
<SelectValue placeholder={t('selectDuration')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
@@ -463,7 +464,7 @@ export default function Page() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Role
|
||||
{t('role')}
|
||||
</FormLabel>
|
||||
<Select
|
||||
onValueChange={
|
||||
@@ -472,7 +473,7 @@ export default function Page() {
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select role" />
|
||||
<SelectValue placeholder={t('accessRoleSelect')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
@@ -503,37 +504,16 @@ export default function Page() {
|
||||
<div className="max-w-md space-y-4">
|
||||
{sendEmail && (
|
||||
<p>
|
||||
An email has
|
||||
been sent to the
|
||||
user with the
|
||||
access link
|
||||
below. They must
|
||||
access the link
|
||||
to accept the
|
||||
invitation.
|
||||
{t('inviteEmailSentDescription')}
|
||||
</p>
|
||||
)}
|
||||
{!sendEmail && (
|
||||
<p>
|
||||
The user has
|
||||
been invited.
|
||||
They must access
|
||||
the link below
|
||||
to accept the
|
||||
invitation.
|
||||
{t('inviteSentDescription')}
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
The invite will
|
||||
expire in{" "}
|
||||
<b>
|
||||
{expiresInDays}{" "}
|
||||
{expiresInDays ===
|
||||
1
|
||||
? "day"
|
||||
: "days"}
|
||||
</b>
|
||||
.
|
||||
{t('inviteExpiresIn', {days: expiresInDays})}
|
||||
</p>
|
||||
<CopyTextBox
|
||||
text={inviteLink}
|
||||
@@ -554,20 +534,16 @@ export default function Page() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
Identity Provider
|
||||
{t('idpTitle')}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Select the identity provider for the
|
||||
external user
|
||||
{t('idpSelect')}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
{idps.length === 0 ? (
|
||||
<p className="text-muted-foreground">
|
||||
No identity providers are
|
||||
configured. Please configure an
|
||||
identity provider before creating
|
||||
external users.
|
||||
{t('idpNotConfigured')}
|
||||
</p>
|
||||
) : (
|
||||
<Form {...externalForm}>
|
||||
@@ -621,10 +597,10 @@ export default function Page() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
User Information
|
||||
{t('userSettings')}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Enter the details for the new user
|
||||
{t('userSettingsDescription')}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -645,7 +621,7 @@ export default function Page() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Username
|
||||
{t('username')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -653,15 +629,7 @@ export default function Page() {
|
||||
/>
|
||||
</FormControl>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
This must
|
||||
match the
|
||||
unique
|
||||
username
|
||||
that exists
|
||||
in the
|
||||
selected
|
||||
identity
|
||||
provider.
|
||||
{t('usernameUniq')}
|
||||
</p>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -676,8 +644,7 @@ export default function Page() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Email
|
||||
(Optional)
|
||||
{t('emailOptional')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -697,8 +664,7 @@ export default function Page() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Name
|
||||
(Optional)
|
||||
{t('nameOptional')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -718,7 +684,7 @@ export default function Page() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Role
|
||||
{t('role')}
|
||||
</FormLabel>
|
||||
<Select
|
||||
onValueChange={
|
||||
@@ -727,7 +693,7 @@ export default function Page() {
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select role" />
|
||||
<SelectValue placeholder={t('accessRoleSelect')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
@@ -771,7 +737,7 @@ export default function Page() {
|
||||
router.push(`/${orgId}/settings/access/users`);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
{userType && dataLoaded && (
|
||||
<Button
|
||||
@@ -783,7 +749,7 @@ export default function Page() {
|
||||
(userType === "internal" && inviteLink !== null)
|
||||
}
|
||||
>
|
||||
Create User
|
||||
{t('accessUserCreate')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { verifySession } from "@app/lib/auth/verifySession";
|
||||
import AccessPageHeaderAndNav from "../AccessPageHeaderAndNav";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type UsersPageProps = {
|
||||
params: Promise<{ orgId: string }>;
|
||||
@@ -24,6 +25,8 @@ export default async function UsersPage(props: UsersPageProps) {
|
||||
const getUser = cache(verifySession);
|
||||
const user = await getUser();
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
let users: ListUsersResponse["users"] = [];
|
||||
let hasInvitations = false;
|
||||
|
||||
@@ -77,15 +80,13 @@ export default async function UsersPage(props: UsersPageProps) {
|
||||
email: user.email,
|
||||
type: user.type,
|
||||
idpId: user.idpId,
|
||||
idpName: user.idpName || "Internal",
|
||||
status: "Confirmed",
|
||||
role: user.isOwner ? "Owner" : user.roleName || "Member",
|
||||
idpName: user.idpName || t('idpNameInternal'),
|
||||
status: t('userConfirmed'),
|
||||
role: user.isOwner ? t('accessRoleOwner') : user.roleName || t('accessRoleMember'),
|
||||
isOwner: user.isOwner || false
|
||||
};
|
||||
});
|
||||
|
||||
const t = await getTranslations();
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
|
||||
Reference in New Issue
Block a user