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
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import {
Button,
Dialog,
Field,
Flex,
Label,
Select,
Text,
TextArea,
toastManager
} from '@raystack/apsara';
Expand All @@ -30,11 +29,21 @@ import { useTerminology } from '../../../../hooks/useTerminology';
import { handleConnectError } from '~/utils/error';

const inviteSchema = z.object({
role: z.string(),
role: z.string().min(1, { message: 'Role is required' }),
emails: z
.string()
.transform(value => value.split(',').map(str => str.trim()))
.pipe(z.array(z.string().email()))
.min(1, { message: 'Email is required' })
.transform(value =>
value
.split(',')
.map(str => str.trim())
.filter(str => str.length > 0)
)
.pipe(
z
.array(z.string().email({ message: 'Enter valid email address(es)' }))
.min(1, { message: 'Email is required' })
)
});

type InviteSchemaType = z.infer<typeof inviteSchema>;
Expand Down Expand Up @@ -109,6 +118,7 @@ export const InviteUsersDialog = ({ onOpenChange }: InviteUsersDialogProps) => {

const isSubmitting = methods?.formState?.isSubmitting;
const errors = methods?.formState?.errors;

return (
<Dialog open onOpenChange={onOpenChange}>
<Dialog.Content width={600}>
Expand All @@ -119,10 +129,14 @@ export const InviteUsersDialog = ({ onOpenChange }: InviteUsersDialogProps) => {
</Dialog.Header>
<Dialog.Body className={styles['invite-users-dialog-body']}>
<Flex direction="column" gap={7}>
<Flex direction="column" gap={2}>
<Label className={styles['invite-users-dialog-label']}>
Emails
</Label>
<Field
label="Emails"
error={
errors?.emails?.message ||
(Array.isArray(errors?.emails)
? errors.emails.find(e => e?.message)?.message
: undefined)
}>
<Controller
name="emails"
control={methods.control}
Expand All @@ -138,17 +152,9 @@ export const InviteUsersDialog = ({ onOpenChange }: InviteUsersDialogProps) => {
);
}}
/>
{errors?.emails?.message || errors?.emails?.length ? (
<Text size="mini" className={styles['form-error-message']}>
{errors?.emails?.message || errors?.emails?.[0]?.message}
</Text>
) : null}
</Flex>
</Field>

<Flex direction="column" gap={2}>
<Label className={styles['invite-users-dialog-label']}>
Role
</Label>
<Field label="Role" error={errors?.role?.message}>
<Controller
name="role"
control={methods.control}
Expand Down Expand Up @@ -177,13 +183,7 @@ export const InviteUsersDialog = ({ onOpenChange }: InviteUsersDialogProps) => {
);
}}
/>

{errors?.role?.message && (
<Text size="mini" className={styles['form-error-message']}>
{errors?.role?.message}
</Text>
)}
</Flex>
</Field>
</Flex>
</Dialog.Body>
<Dialog.Footer>
Expand Down
8 changes: 0 additions & 8 deletions web/sdk/admin/views/users/list/invite-users.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,6 @@
gap: var(--rs-space-7);
}

.invite-users-dialog-label {
font-weight: var(--rs-font-weight-medium);
}

.form-error-message {
color: var(--rs-color-foreground-danger-primary);
}

.invite-users-emails-textarea {
resize: vertical;
width: 100%;
Expand Down
204 changes: 101 additions & 103 deletions web/sdk/admin/views/users/list/invite-users.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { useMemo, useState, useEffect } from "react";
import {
Button,
Dialog,
Field,
Flex,
Label,
Select,
Text,
TextArea,
Skeleton,
toastManager,
} from "@raystack/apsara";
import { PlusIcon } from "@radix-ui/react-icons";
Expand All @@ -15,7 +15,6 @@ import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { SCOPES, DEFAULT_ROLES } from "../../../utils/constants";
import styles from "./invite-users.module.css";
import Skeleton from "react-loading-skeleton";
import { useMutation, useQuery } from "@connectrpc/connect-query";
import {
AdminServiceQueries,
Expand All @@ -28,16 +27,29 @@ import { create } from "@bufbuild/protobuf";
import { handleConnectError } from "~/utils/error";
import { useTerminology } from "../../../hooks/useTerminology";

const inviteSchema = z.object({
role: z.string(),
organizationId: z.string(),
emails: z
.string()
.transform(value => value.split(",").map(str => str.trim()))
.pipe(z.array(z.string().email())),
});
const createInviteSchema = (t: ReturnType<typeof useTerminology>) =>
z.object({
role: z.string().min(1, { message: "Role is required" }),
organizationId: z.string().min(1, {
message: `${t.organization({ case: "capital" })} is required`,
}),
emails: z
.string()
.min(1, { message: "Email is required" })
.transform(value =>
value
.split(",")
.map(str => str.trim())
.filter(str => str.length > 0)
)
.pipe(
z
.array(z.string().email({ message: "Enter valid email address(es)" }))
.min(1, { message: "Email is required" })
),
});

type InviteSchemaType = z.infer<typeof inviteSchema>;
type InviteSchemaType = z.infer<ReturnType<typeof createInviteSchema>>;

export const InviteUser = () => {
const t = useTerminology();
Expand Down Expand Up @@ -67,6 +79,8 @@ export const InviteUser = () => {
}
);

const isLoading = isRolesLoading || isOrganizationsLoading;

useEffect(() => {
if (organizationsError) {
console.error("Failed to fetch organizations:", organizationsError);
Expand All @@ -84,6 +98,8 @@ export const InviteUser = () => {
[roles],
);

const inviteSchema = useMemo(() => createInviteSchema(t), [t]);

const {
formState: { errors, isSubmitting },
handleSubmit,
Expand Down Expand Up @@ -149,46 +165,46 @@ export const InviteUser = () => {
</Dialog.Header>
<Dialog.Body className={styles["invite-users-dialog-body"]}>
<Flex direction="column" gap={7}>
<Flex direction="column" gap={2}>
<Label className={styles["invite-users-dialog-label"]}>
Emails
</Label>
<Controller
name="emails"
control={control}
render={({ field }) => {
const { value, ...rest } = field;
return (
<TextArea
{...rest}
value={Array.isArray(value) ? value.join(", ") : (value ?? "") as string}
placeholder="abc@example.com, xyz@example.com"
className={styles["invite-users-emails-textarea"]}
/>
);
}}
/>
{(errors?.emails?.message || errors?.emails?.length) && (
<Text size="mini" className={styles["form-error-message"]}>
{errors?.emails?.message || errors?.emails?.[0]?.message}
</Text>
<Field
label="Emails"
error={
errors?.emails?.message ||
(Array.isArray(errors?.emails)
? errors.emails.find(e => e?.message)?.message
: undefined)
}>
{isLoading ? (
Comment thread
coderabbitai[bot] marked this conversation as resolved.
<Skeleton height="80px" />
) : (
<Controller
name="emails"
control={control}
render={({ field }) => {
const { value, ...rest } = field;
return (
<TextArea
{...rest}
value={Array.isArray(value) ? value.join(", ") : (value ?? "") as string}
placeholder="abc@example.com, xyz@example.com"
className={styles["invite-users-emails-textarea"]}
/>
);
}}
/>
)}
</Flex>
</Field>

<Flex direction="column" gap={2}>
<Label className={styles["invite-users-dialog-label"]}>
Invite as
</Label>
<Controller
name="role"
defaultValue={defaultRoleId}
disabled={isRolesLoading}
control={control}
render={({ field, fieldState: { error } }) => {
const { ref, ...rest } = field;
if (isRolesLoading) return <Skeleton height={33} />;
Comment thread
rohanchkrabrty marked this conversation as resolved.
return (
<>
<Field label="Invite as" error={errors?.role?.message}>
{isLoading ? (
<Skeleton height="36px" />
) : (
<Controller
name="role"
defaultValue={defaultRoleId}
control={control}
render={({ field }) => {
const { ref, ...rest } = field;
return (
<Select
{...rest}
onValueChange={value => field.onChange(value)}>
Expand All @@ -203,72 +219,54 @@ export const InviteUser = () => {
))}
</Select.Content>
</Select>
{error && (
<Text
size="mini"
className={styles["form-error-message"]}>
{error?.message}
</Text>
)}
</>
);
}}
/>
</Flex>
);
}}
/>
)}
</Field>

<Flex direction="column" gap={2}>
<Label className={styles["invite-users-dialog-label"]}>
{t.organization({ case: "capital" })}
</Label>
<Controller
<Field
label={t.organization({ case: "capital" })}
error={errors?.organizationId?.message}>
{isLoading ? (<Skeleton height="36px" />) : (<Controller
name="organizationId"
disabled={isOrganizationsLoading}
control={control}
render={({ field, fieldState: { error } }) => {
render={({ field }) => {
const { ref, ...rest } = field;
if (isOrganizationsLoading) return <Skeleton height={33} />;
return (
<>
<Select
{...rest}
autocomplete
onValueChange={value => field.onChange(value)}>
<Select.Trigger ref={ref}>
<Select.Value
placeholder={`Select ${t.organization({ case: "capital" })}`}
/>
</Select.Trigger>
<Select.Content
searchPlaceholder={`Search ${t.organization({ case: "lower" })}`}
style={{
maxHeight: 280,
overflowY: "auto",
}}>
{organizations?.map(org => (
<Select.Item key={org.id} value={org.id ?? ""}>
{org.title || org.name}
</Select.Item>
))}
</Select.Content>
</Select>
{error && (
<Text
size="mini"
className={styles["form-error-message"]}>
{error?.message}
</Text>
)}
</>
<Select
{...rest}
autocomplete
onValueChange={value => field.onChange(value)}>
<Select.Trigger ref={ref}>
<Select.Value
placeholder={`Select ${t.organization({ case: "capital" })}`}
/>
</Select.Trigger>
<Select.Content
searchPlaceholder={`Search ${t.organization({ case: "lower" })}`}
style={{
maxHeight: 280,
overflowY: "auto",
}}>
{organizations?.map(org => (
<Select.Item key={org.id} value={org.id ?? ""}>
{org.title || org.name}
</Select.Item>
))}
</Select.Content>
</Select>
);
}}
/>
</Flex>
/>)}
</Field>
</Flex>
</Dialog.Body>
<Dialog.Footer>
<Button
data-test-id="users-list-invite-user-submit-btn"
type="submit"
disabled={isLoading}
loading={isSubmitting}
loaderText="Sending...">
Send invite
Expand Down
Loading
Loading