diff --git a/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx b/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx index 9ab9e93fa..717d7f211 100644 --- a/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx +++ b/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx @@ -1,44 +1,40 @@ "use client"; +import IdpTypeBadge from "@app/components/IdpTypeBadge"; +import OrgRolesTagField from "@app/components/OrgRolesTagField"; +import { + SettingsContainer, + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionFooter, + SettingsSectionForm, + SettingsSectionHeader, + SettingsSectionTitle +} from "@app/components/Settings"; +import { Button } from "@app/components/ui/button"; +import { Checkbox } from "@app/components/ui/checkbox"; import { Form, FormControl, FormField, FormItem, - FormLabel, - FormMessage + FormLabel } from "@app/components/ui/form"; -import { Checkbox } from "@app/components/ui/checkbox"; -import OrgRolesTagField from "@app/components/OrgRolesTagField"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { userOrgUserContext } from "@app/hooks/useOrgUserContext"; +import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { toast } from "@app/hooks/useToast"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { AxiosResponse } from "axios"; -import { useEffect, useState } from "react"; +import { build } from "@server/build"; +import { tierMatrix } from "@server/lib/billing/tierMatrix"; +import { UserType } from "@server/types/UserTypes"; +import { useTranslations } from "next-intl"; +import { useParams } from "next/navigation"; +import { useActionState, useEffect } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; -import { ListRolesResponse } from "@server/routers/role"; -import { userOrgUserContext } from "@app/hooks/useOrgUserContext"; -import { useParams } from "next/navigation"; -import { Button } from "@app/components/ui/button"; -import { - SettingsContainer, - SettingsSection, - SettingsSectionHeader, - SettingsSectionTitle, - SettingsSectionDescription, - SettingsSectionBody, - SettingsSectionForm, - SettingsSectionFooter -} from "@app/components/Settings"; -import { formatAxiosError } from "@app/lib/api"; -import { createApiClient } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { useTranslations } from "next-intl"; -import IdpTypeBadge from "@app/components/IdpTypeBadge"; -import { UserType } from "@server/types/UserTypes"; -import { usePaidStatus } from "@app/hooks/usePaidStatus"; -import { tierMatrix } from "@server/lib/billing/tierMatrix"; -import { build } from "@server/build"; const accessControlsFormSchema = z.object({ username: z.string(), @@ -59,12 +55,6 @@ export default function AccessControlsPage() { const { orgId } = useParams(); - const [loading, setLoading] = useState(false); - const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]); - const [activeRoleTagIndex, setActiveRoleTagIndex] = useState( - null - ); - const t = useTranslations(); const { isPaidUser } = usePaidStatus(); const isPaid = isPaidUser(tierMatrix.fullRbac); @@ -97,44 +87,21 @@ export default function AccessControlsPage() { text: r.name })) ); - }, [user.userId, currentRoleIds.join(",")]); - - useEffect(() => { - async function fetchRoles() { - const res = await api - .get>(`/org/${orgId}/roles`) - .catch((e) => { - console.error(e); - toast({ - variant: "destructive", - title: t("accessRoleErrorFetch"), - description: formatAxiosError( - e, - t("accessRoleErrorFetchDescription") - ) - }); - }); - - if (res?.status === 200) { - setRoles(res.data.data.roles); - } - } - - fetchRoles(); form.setValue("autoProvisioned", user.autoProvisioned || false); - }, []); - - const allRoleOptions = roles.map((role) => ({ - id: role.roleId.toString(), - text: role.name - })); + }, [user.userId, user.autoProvisioned, currentRoleIds.join(",")]); const paywallMessage = build === "saas" ? t("singleRolePerUserPlanNotice") : t("singleRolePerUserEditionNotice"); - async function onSubmit(values: z.infer) { + const [, action, isSubmitting] = useActionState(onSubmit, null); + async function onSubmit() { + const isValid = await form.trigger(); + if (!isValid) return; + + const values = form.getValues(); + if (values.roles.length === 0) { toast({ variant: "destructive", @@ -144,7 +111,6 @@ export default function AccessControlsPage() { return; } - setLoading(true); try { const roleIds = values.roles.map((r) => parseInt(r.id, 10)); const updateRoleRequest = supportsMultipleRolesPerUser @@ -184,7 +150,6 @@ export default function AccessControlsPage() { ) }); } - setLoading(false); } return ( @@ -203,7 +168,7 @@ export default function AccessControlsPage() {
@@ -226,9 +191,7 @@ export default function AccessControlsPage() { {user.idpAutoProvision && ( @@ -277,8 +237,8 @@ export default function AccessControlsPage() { - - - - { - form.setValue( - "clients", - machines - ); - }} - /> - - */} )} diff --git a/src/components/OrgRolesTagField.tsx b/src/components/OrgRolesTagField.tsx index dcd679663..bc8e5a0b5 100644 --- a/src/components/OrgRolesTagField.tsx +++ b/src/components/OrgRolesTagField.tsx @@ -8,51 +8,42 @@ import { FormLabel, FormMessage } from "@app/components/ui/form"; -import { Tag, TagInput } from "@app/components/tags/tag-input"; + import { toast } from "@app/hooks/useToast"; import { useTranslations } from "next-intl"; -import type { Dispatch, SetStateAction } from "react"; -import type { FieldValues, Path, UseFormReturn } from "react-hook-form"; -export type RoleTag = { - id: string; - text: string; -}; +import type { FieldValues, Path, UseFormReturn } from "react-hook-form"; +import { RolesSelector, type SelectedRole } from "./roles-selector"; type OrgRolesTagFieldProps = { - form: Pick, "control" | "getValues" | "setValue">; + form: Pick< + UseFormReturn, + "control" | "getValues" | "setValue" + >; + orgId: string; /** Field in the form that holds Tag[] (role tags). Default: `"roles"`. */ name?: Path; - label: string; - placeholder: string; - allRoleOptions: Tag[]; + label?: string; supportsMultipleRolesPerUser: boolean; showMultiRolePaywallMessage: boolean; paywallMessage: string; - loading?: boolean; - activeTagIndex: number | null; - setActiveTagIndex: Dispatch>; + disabled?: boolean; }; export default function OrgRolesTagField({ form, name = "roles" as Path, label, - placeholder, - allRoleOptions, + orgId, supportsMultipleRolesPerUser, showMultiRolePaywallMessage, paywallMessage, - loading = false, - activeTagIndex, - setActiveTagIndex + disabled }: OrgRolesTagFieldProps) { const t = useTranslations(); - function setRoleTags(updater: Tag[] | ((prev: Tag[]) => Tag[])) { - const prev = form.getValues(name) as Tag[]; - const nextValue = - typeof updater === "function" ? updater(prev) : updater; + function setRoleTags(nextValue: SelectedRole[]) { + const prev = form.getValues(name) as SelectedRole[]; const next = supportsMultipleRolesPerUser ? nextValue : nextValue.length > 1 @@ -88,22 +79,13 @@ export default function OrgRolesTagField({ name={name} render={({ field }) => ( - {label} + {label ?? t("roles")} - {showMultiRolePaywallMessage && ( diff --git a/src/components/multi-select/multi-select-content.tsx b/src/components/multi-select/multi-select-content.tsx index 00a1cf870..0ea54d897 100644 --- a/src/components/multi-select/multi-select-content.tsx +++ b/src/components/multi-select/multi-select-content.tsx @@ -21,6 +21,7 @@ export type MultiSelectTagsProps = { onChange: (newValue: Array) => void; onSearch: (query: string) => void; ref?: Ref; + disabled?: boolean; }; export function MultiSelectContent({ @@ -40,8 +41,7 @@ export function MultiSelectContent({ value={searchQuery} onValueChange={onSearch} /> - {/* FIXME: why isn't this list scrolling ????? */} - + {emptyPlaceholder} {options.map((option) => ( diff --git a/src/components/multi-select/multi-select-tag-input.tsx b/src/components/multi-select/multi-select-tag-input.tsx index dfca035a7..6b3e0fb0f 100644 --- a/src/components/multi-select/multi-select-tag-input.tsx +++ b/src/components/multi-select/multi-select-tag-input.tsx @@ -1,17 +1,16 @@ +import { buttonVariants } from "@app/components/ui/button"; import { Popover, PopoverContent, PopoverTrigger } from "@app/components/ui/popover"; -import { Button, buttonVariants } from "@app/components/ui/button"; import { cn } from "@app/lib/cn"; import { ChevronDownIcon, XIcon } from "lucide-react"; import { - type TagValue, type MultiSelectTagsProps, + type TagValue, MultiSelectContent } from "./multi-select-content"; -import { useState } from "react"; export interface MultiSelectInputProps< T extends TagValue @@ -36,7 +35,8 @@ export function MultiSelectTagInput({ }), "justify-between w-full inline-flex", "text-muted-foreground pl-1.5 cursor-text", - "hover:bg-transparent hover:text-muted-foreground" + "hover:bg-transparent hover:text-muted-foreground", + props.disabled && "pointer-events-none opacity-50" )} > ({ {option.text}