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,8 +513,6 @@ export default function Page() {
) : null} ) : null}
{selectedOption === "internal" && dataLoaded && ( {selectedOption === "internal" && dataLoaded && (
<>
{!inviteLink ? (
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>
@@ -533,9 +536,7 @@ export default function Page() {
id="create-user-form" id="create-user-form"
> >
<FormField <FormField
control={ control={internalForm.control}
internalForm.control
}
name="email" name="email"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
@@ -543,26 +544,40 @@ export default function Page() {
{t("email")} {t("email")}
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input <Input {...field} />
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField {env.email.emailEnabled && (
control={ <div className="flex items-center space-x-2">
internalForm.control <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" name="validForHours"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
{t( {t("inviteValid")}
"inviteValid"
)}
</FormLabel> </FormLabel>
<Select <Select
onValueChange={ onValueChange={
@@ -619,66 +634,11 @@ export default function Page() {
invitePaywallMessage 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>
</Form> </Form>
</SettingsSectionForm> </SettingsSectionForm>
</SettingsSectionBody> </SettingsSectionBody>
</SettingsSection> </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>
</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",