improvements to create user

This commit is contained in:
miloschwartz
2026-09-11 11:39:33 -04:00
parent e73ea120e3
commit 33c1eb1426
3 changed files with 218 additions and 208 deletions
+3 -3
View File
@@ -676,11 +676,11 @@
"accessUserCreateDescription": "Follow the steps below to create a new user", "accessUserCreateDescription": "Follow the steps below to create a new user",
"userSeeAll": "See All Users", "userSeeAll": "See All Users",
"userTypeTitle": "User Type", "userTypeTitle": "User Type",
"userTypeDescription": "Determine how you want to create the user", "userTypeDescription": "Select the identity provider to use for this user",
"userSettings": "User Information", "userSettings": "User Information",
"userSettingsDescription": "Enter the details for the new user", "userSettingsDescription": "Enter the general details for the new user",
"inviteEmailSent": "Send invite email to user", "inviteEmailSent": "Send invite email to user",
"inviteValid": "Invite Valid For (days)", "inviteValid": "Invite Valid For",
"selectDuration": "Select duration", "selectDuration": "Select duration",
"selectResource": "Select Resource", "selectResource": "Select Resource",
"filterByResource": "Filter By Resource", "filterByResource": "Filter By Resource",
@@ -1,24 +1,20 @@
"use client"; "use client";
import CopyToClipboard from "@app/components/CopyToClipboard";
import { import {
SettingsContainer, Credenza,
SettingsSection, CredenzaBody,
SettingsSectionBody, CredenzaContent,
SettingsSectionDescription, CredenzaDescription,
SettingsSectionForm, CredenzaFooter,
SettingsSectionHeader, CredenzaHeader,
SettingsSectionTitle CredenzaTitle
} from "@app/components/Settings"; } from "@app/components/Credenza";
import { StrategyOption, StrategySelect } from "@app/components/StrategySelect"; import { StrategySelect } from "@app/components/StrategySelect";
import HeaderTitle from "@app/components/SettingsSectionTitle"; import HeaderTitle from "@app/components/SettingsSectionTitle";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import { import { useActionState, useRef, useState, startTransition } from "react";
useActionState,
useRef,
useState,
startTransition
} from "react";
import { import {
Form, Form,
FormControl, FormControl,
@@ -42,7 +38,6 @@ import { AxiosResponse } from "axios";
import { useEffect } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
import CopyTextBox from "@app/components/CopyTextBox";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { ListRolesResponse } from "@server/routers/role"; import { ListRolesResponse } from "@server/routers/role";
import { formatAxiosError } from "@app/lib/api"; import { formatAxiosError } from "@app/lib/api";
@@ -55,7 +50,15 @@ import IdpTypeIcon from "@app/components/IdpTypeIcon";
import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { tierMatrix } from "@server/lib/billing/tierMatrix";
import OrgRolesTagField from "@app/components/OrgRolesTagField"; import OrgRolesTagField from "@app/components/OrgRolesTagField";
import CopyToClipboard from "@app/components/CopyToClipboard"; import {
SettingsContainer,
SettingsSection,
SettingsSectionBody,
SettingsSectionDescription,
SettingsSectionForm,
SettingsSectionHeader,
SettingsSectionTitle
} from "@app/components/Settings";
type UserType = "internal" | "oidc"; type UserType = "internal" | "oidc";
@@ -96,6 +99,7 @@ export default function Page() {
"internal" "internal"
); );
const [inviteLink, setInviteLink] = useState<string | null>(null); const [inviteLink, setInviteLink] = useState<string | null>(null);
const [isInviteDialogOpen, setIsInviteDialogOpen] = useState(false);
const [expiresInDays, setExpiresInDays] = useState(1); const [expiresInDays, setExpiresInDays] = useState(1);
const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]); const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]);
@@ -246,9 +250,9 @@ export default function Page() {
build === "saas" || env.app.identityProviderMode === "org"; build === "saas" || env.app.identityProviderMode === "org";
const res = await api const res = await api
.get< .get<AxiosResponse<ListIdpsResponse>>(
AxiosResponse<ListIdpsResponse> useOrgIdps ? `/org/${orgId}/idp` : "/idp"
>(useOrgIdps ? `/org/${orgId}/idp` : "/idp") )
.catch((e) => { .catch((e) => {
console.error(e); console.error(e);
toast({ toast({
@@ -350,13 +354,13 @@ export default function Page() {
if (res && res.status === 200) { if (res && res.status === 200) {
setInviteLink(res.data.data.inviteLink); setInviteLink(res.data.data.inviteLink);
setExpiresInDays(parseInt(values.validForHours) / 24);
setIsInviteDialogOpen(true);
toast({ toast({
variant: "default", variant: "default",
title: t("userInvited"), title: t("userInvited"),
description: t("userInvitedDescription") description: t("userInvitedDescription")
}); });
setExpiresInDays(parseInt(values.validForHours) / 24);
} }
} }
@@ -460,6 +464,7 @@ export default function Page() {
setSendEmail(env.email.emailEnabled); setSendEmail(env.email.emailEnabled);
internalForm.reset(); internalForm.reset();
setInviteLink(null); setInviteLink(null);
setIsInviteDialogOpen(false);
setExpiresInDays(1); setExpiresInDays(1);
} else { } else {
googleAzureForm.reset(); googleAzureForm.reset();
@@ -486,7 +491,7 @@ export default function Page() {
<div> <div>
<SettingsContainer> <SettingsContainer>
{!inviteLink && userOptions.length > 1 ? ( {userOptions.length > 1 ? (
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>
@@ -508,177 +513,132 @@ export default function Page() {
) : null} ) : null}
{selectedOption === "internal" && dataLoaded && ( {selectedOption === "internal" && dataLoaded && (
<> <SettingsSection>
{!inviteLink ? ( <SettingsSectionHeader>
<SettingsSection> <SettingsSectionTitle>
<SettingsSectionHeader> {t("userSettings")}
<SettingsSectionTitle> </SettingsSectionTitle>
{t("userSettings")} <SettingsSectionDescription>
</SettingsSectionTitle> {t("userSettingsDescription")}
<SettingsSectionDescription> </SettingsSectionDescription>
{t("userSettingsDescription")} </SettingsSectionHeader>
</SettingsSectionDescription> <SettingsSectionBody>
</SettingsSectionHeader> <SettingsSectionForm>
<SettingsSectionBody> <Form {...internalForm}>
<SettingsSectionForm> <form
<Form {...internalForm}> onSubmit={(e) => {
<form e.preventDefault();
onSubmit={(e) => { startTransition(() => {
e.preventDefault(); submitInternalAction();
startTransition(() => { });
submitInternalAction(); }}
}); className="space-y-4"
}} id="create-user-form"
className="space-y-4" >
id="create-user-form" <FormField
> control={internalForm.control}
<FormField name="email"
control={ render={({ field }) => (
internalForm.control <FormItem>
} <FormLabel>
name="email" {t("email")}
render={({ field }) => ( </FormLabel>
<FormItem> <FormControl>
<FormLabel> <Input {...field} />
{t("email")} </FormControl>
</FormLabel> <FormMessage />
<FormControl> </FormItem>
<Input )}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={
internalForm.control
}
name="validForHours"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"inviteValid"
)}
</FormLabel>
<Select
onValueChange={
field.onChange
}
defaultValue={
field.value
}
>
<FormControl>
<SelectTrigger className="w-full">
<SelectValue
placeholder={t(
"selectDuration"
)}
/>
</SelectTrigger>
</FormControl>
<SelectContent>
{validFor.map(
(
option
) => (
<SelectItem
key={
option.hours
}
value={option.hours.toString()}
>
{
option.name
}
</SelectItem>
)
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<OrgRolesTagField
form={internalForm}
name="roles"
orgId={orgId as string}
supportsMultipleRolesPerUser={
supportsMultipleRolesPerUser
}
showMultiRolePaywallMessage={
showMultiRolePaywallMessage
}
paywallMessage={
invitePaywallMessage
}
/>
{env.email.emailEnabled && (
<div className="flex items-center space-x-2">
<Checkbox
id="send-email"
checked={
sendEmail
}
onCheckedChange={(
e
) =>
setSendEmail(
e as boolean
)
}
/>
<label
htmlFor="send-email"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{t(
"inviteEmailSent"
)}
</label>
</div>
)}
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
</SettingsSection>
) : (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("userInvited")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{sendEmail
? t(
"inviteEmailSentDescription"
)
: t("inviteSentDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<div className="space-y-4">
<p>
{t("inviteExpiresIn", {
days: expiresInDays
})}
</p>
<CopyToClipboard
text={inviteLink}
/> />
</div>
</SettingsSectionBody> {env.email.emailEnabled && (
</SettingsSection> <div className="flex items-center space-x-2">
)} <Checkbox
</> id="send-email"
checked={sendEmail}
onCheckedChange={(e) =>
setSendEmail(
e as boolean
)
}
/>
<label
htmlFor="send-email"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{t("inviteEmailSent")}
</label>
</div>
)}
<FormField
control={internalForm.control}
name="validForHours"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("inviteValid")}
</FormLabel>
<Select
onValueChange={
field.onChange
}
defaultValue={
field.value
}
>
<FormControl>
<SelectTrigger className="w-full">
<SelectValue
placeholder={t(
"selectDuration"
)}
/>
</SelectTrigger>
</FormControl>
<SelectContent>
{validFor.map(
(
option
) => (
<SelectItem
key={
option.hours
}
value={option.hours.toString()}
>
{
option.name
}
</SelectItem>
)
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<OrgRolesTagField
form={internalForm}
name="roles"
orgId={orgId as string}
supportsMultipleRolesPerUser={
supportsMultipleRolesPerUser
}
showMultiRolePaywallMessage={
showMultiRolePaywallMessage
}
paywallMessage={
invitePaywallMessage
}
/>
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
</SettingsSection>
)} )}
{selectedOption && {selectedOption &&
@@ -898,24 +858,61 @@ export default function Page() {
<div className="flex justify-end space-x-2 mt-8"> <div className="flex justify-end space-x-2 mt-8">
{selectedOption && dataLoaded && ( {selectedOption && dataLoaded && (
<Button <Button
type={inviteLink ? "button" : "submit"} type="submit"
form={inviteLink ? undefined : "create-user-form"} form="create-user-form"
loading={loading} loading={loading}
disabled={loading} disabled={loading}
onClick={
inviteLink
? () =>
router.push(
`/${orgId}/settings/access/users`
)
: undefined
}
> >
{inviteLink ? t("done") : t("accessUserCreate")} {t("accessUserCreate")}
</Button> </Button>
)} )}
</div> </div>
</div> </div>
<Credenza
open={isInviteDialogOpen}
onOpenChange={(open) => {
setIsInviteDialogOpen(open);
if (!open) {
setInviteLink(null);
}
}}
>
<CredenzaContent>
<CredenzaHeader>
<CredenzaTitle>{t("userInvited")}</CredenzaTitle>
<CredenzaDescription>
{sendEmail
? t("inviteEmailSentDescription")
: t("inviteSentDescription")}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<div className="space-y-4">
<p>
{t("inviteExpiresIn", {
days: expiresInDays
})}
</p>
{inviteLink && (
<CopyToClipboard
text={inviteLink}
isLink={true}
/>
)}
</div>
</CredenzaBody>
<CredenzaFooter>
<Button
onClick={() =>
router.push(`/${orgId}/settings/access/users`)
}
>
{t("done")}
</Button>
</CredenzaFooter>
</CredenzaContent>
</Credenza>
</> </>
); );
} }
+14 -1
View File
@@ -171,7 +171,20 @@ export default function UsersTable({
<Icon className="ml-2 h-4 w-4" /> <Icon className="ml-2 h-4 w-4" />
</Button> </Button>
); );
} },
cell: ({ row }) => (
<span className="inline-flex gap-1 items-center">
{row.original.displayUsername}{" "}
{row.original.id === user.userId && (
<>
<span className="text-muted-foreground">
&middot;
</span>{" "}
<span className="text-primary">you</span>
</>
)}
</span>
)
}, },
{ {
accessorKey: "idpName", accessorKey: "idpName",