mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-22 12:10:35 +02:00
improve role required feedback on user
This commit is contained in:
+2
-1
@@ -615,7 +615,8 @@
|
|||||||
"idpNameInternal": "Internal",
|
"idpNameInternal": "Internal",
|
||||||
"emailInvalid": "Invalid email address",
|
"emailInvalid": "Invalid email address",
|
||||||
"inviteValidityDuration": "Please select a duration",
|
"inviteValidityDuration": "Please select a duration",
|
||||||
"accessRoleSelectPlease": "Please select a role",
|
"accessRoleSelectPlease": "A user must belong to at least one role.",
|
||||||
|
"accessRoleRequired": "Role required",
|
||||||
"removeOwnAdminRoleConfirmTitle": "Remove your administrator access?",
|
"removeOwnAdminRoleConfirmTitle": "Remove your administrator access?",
|
||||||
"removeOwnAdminRoleConfirmDescription": "You will no longer have administrator permissions in this organization after saving. Another administrator can restore access if needed.",
|
"removeOwnAdminRoleConfirmDescription": "You will no longer have administrator permissions in this organization after saving. Another administrator can restore access if needed.",
|
||||||
"removeOwnAdminRoleConfirmButton": "Remove My Administrator Access",
|
"removeOwnAdminRoleConfirmButton": "Remove My Administrator Access",
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export default function AccessControlsPage() {
|
|||||||
if (values.roles.length === 0) {
|
if (values.roles.length === 0) {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: t("accessRoleErrorAdd"),
|
title: t("accessRoleRequired"),
|
||||||
description: t("accessRoleSelectPlease")
|
description: t("accessRoleSelectPlease")
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -173,15 +173,13 @@ export default function AccessControlsPage() {
|
|||||||
if (values.roles.length === 0) {
|
if (values.roles.length === 0) {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: t("accessRoleErrorAdd"),
|
title: t("accessRoleRequired"),
|
||||||
description: t("accessRoleSelectPlease")
|
description: t("accessRoleSelectPlease")
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const willHaveAdminRole = values.roles.some(
|
const willHaveAdminRole = values.roles.some((r) => r.isAdmin === true);
|
||||||
(r) => r.isAdmin === true
|
|
||||||
);
|
|
||||||
|
|
||||||
const isRemovingOwnAdmin =
|
const isRemovingOwnAdmin =
|
||||||
sessionUser.userId === user.userId &&
|
sessionUser.userId === user.userId &&
|
||||||
@@ -226,7 +224,9 @@ export default function AccessControlsPage() {
|
|||||||
<SettingsSectionForm>
|
<SettingsSectionForm>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={(e) => void handleAccessControlsSubmit(e)}
|
onSubmit={(e) =>
|
||||||
|
void handleAccessControlsSubmit(e)
|
||||||
|
}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
id="access-controls-form"
|
id="access-controls-form"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
import { useRef } from "react";
|
||||||
import type { FieldValues, Path, UseFormReturn } from "react-hook-form";
|
import type { FieldValues, Path, UseFormReturn } from "react-hook-form";
|
||||||
import { RolesSelector, type SelectedRole } from "./roles-selector";
|
import { RolesSelector, type SelectedRole } from "./roles-selector";
|
||||||
|
|
||||||
@@ -41,6 +42,46 @@ export default function OrgRolesTagField<TFieldValues extends FieldValues>({
|
|||||||
disabled
|
disabled
|
||||||
}: OrgRolesTagFieldProps<TFieldValues>) {
|
}: OrgRolesTagFieldProps<TFieldValues>) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
const isPopoverOpenRef = useRef(false);
|
||||||
|
const lastValidRolesRef = useRef<SelectedRole[]>(
|
||||||
|
(form.getValues(name) as SelectedRole[]) ?? []
|
||||||
|
);
|
||||||
|
|
||||||
|
function validateRolesSelection() {
|
||||||
|
const current = form.getValues(name) as SelectedRole[];
|
||||||
|
|
||||||
|
if (current.length === 0 && lastValidRolesRef.current.length > 0) {
|
||||||
|
form.setValue(name, lastValidRolesRef.current as never, {
|
||||||
|
shouldDirty: true
|
||||||
|
});
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: t("accessRoleRequired"),
|
||||||
|
description: t("accessRoleSelectPlease")
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current.length > 0) {
|
||||||
|
lastValidRolesRef.current = current;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePopoverOpenChange(open: boolean) {
|
||||||
|
isPopoverOpenRef.current = open;
|
||||||
|
|
||||||
|
if (open) {
|
||||||
|
const current = form.getValues(name) as SelectedRole[];
|
||||||
|
if (current.length > 0) {
|
||||||
|
lastValidRolesRef.current = current;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
validateRolesSelection();
|
||||||
|
}
|
||||||
|
|
||||||
function setRoleTags(nextValue: SelectedRole[]) {
|
function setRoleTags(nextValue: SelectedRole[]) {
|
||||||
const prev = form.getValues(name) as SelectedRole[];
|
const prev = form.getValues(name) as SelectedRole[];
|
||||||
@@ -61,39 +102,44 @@ export default function OrgRolesTagField<TFieldValues extends FieldValues>({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (next.length === 0) {
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: t("accessRoleErrorAdd"),
|
|
||||||
description: t("accessRoleSelectPlease")
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
form.setValue(name, next as never, { shouldDirty: true });
|
form.setValue(name, next as never, { shouldDirty: true });
|
||||||
|
|
||||||
|
if (next.length > 0 && !isPopoverOpenRef.current) {
|
||||||
|
lastValidRolesRef.current = next;
|
||||||
|
} else if (!isPopoverOpenRef.current) {
|
||||||
|
validateRolesSelection();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={name}
|
name={name}
|
||||||
render={({ field }) => (
|
render={({ field }) => {
|
||||||
<FormItem className="flex flex-col items-start">
|
const selectedRoles = (field.value ?? []) as SelectedRole[];
|
||||||
<FormLabel>{label ?? t("roles")}</FormLabel>
|
if (!isPopoverOpenRef.current && selectedRoles.length > 0) {
|
||||||
<FormControl>
|
lastValidRolesRef.current = selectedRoles;
|
||||||
<RolesSelector
|
}
|
||||||
orgId={orgId}
|
|
||||||
selectedRoles={field.value ?? []}
|
return (
|
||||||
onSelectRoles={setRoleTags}
|
<FormItem className="flex flex-col items-start">
|
||||||
disabled={disabled}
|
<FormLabel>{label ?? t("roles")}</FormLabel>
|
||||||
/>
|
<FormControl>
|
||||||
</FormControl>
|
<RolesSelector
|
||||||
{showMultiRolePaywallMessage && (
|
orgId={orgId}
|
||||||
<FormDescription>{paywallMessage}</FormDescription>
|
selectedRoles={selectedRoles}
|
||||||
)}
|
onSelectRoles={setRoleTags}
|
||||||
<FormMessage />
|
onPopoverOpenChange={handlePopoverOpenChange}
|
||||||
</FormItem>
|
disabled={disabled}
|
||||||
)}
|
/>
|
||||||
|
</FormControl>
|
||||||
|
{showMultiRolePaywallMessage && (
|
||||||
|
<FormDescription>{paywallMessage}</FormDescription>
|
||||||
|
)}
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,11 +17,13 @@ export interface MultiSelectInputProps<
|
|||||||
> extends MultiSelectTagsProps<T> {
|
> extends MultiSelectTagsProps<T> {
|
||||||
buttonText?: string;
|
buttonText?: string;
|
||||||
lockedIds?: Set<string>;
|
lockedIds?: Set<string>;
|
||||||
|
onPopoverOpenChange?: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MultiSelectTagInput<T extends TagValue>({
|
export function MultiSelectTagInput<T extends TagValue>({
|
||||||
buttonText,
|
buttonText,
|
||||||
lockedIds,
|
lockedIds,
|
||||||
|
onPopoverOpenChange,
|
||||||
...props
|
...props
|
||||||
}: MultiSelectInputProps<T>) {
|
}: MultiSelectInputProps<T>) {
|
||||||
const selectedValues = new Set(props.value.map((v) => v.id));
|
const selectedValues = new Set(props.value.map((v) => v.id));
|
||||||
@@ -33,6 +35,7 @@ export function MultiSelectTagInput<T extends TagValue>({
|
|||||||
// clear input when popover is closed
|
// clear input when popover is closed
|
||||||
props.onSearch("");
|
props.onSearch("");
|
||||||
}
|
}
|
||||||
|
onPopoverOpenChange?.(open);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export type RolesSelectorProps = {
|
|||||||
orgId: string;
|
orgId: string;
|
||||||
selectedRoles?: SelectedRole[];
|
selectedRoles?: SelectedRole[];
|
||||||
onSelectRoles: (roles: SelectedRole[]) => void;
|
onSelectRoles: (roles: SelectedRole[]) => void;
|
||||||
|
onPopoverOpenChange?: (open: boolean) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
restrictAdminRole?: boolean;
|
restrictAdminRole?: boolean;
|
||||||
mapRolesByName?: boolean;
|
mapRolesByName?: boolean;
|
||||||
@@ -23,6 +24,7 @@ export function RolesSelector({
|
|||||||
orgId,
|
orgId,
|
||||||
selectedRoles = [],
|
selectedRoles = [],
|
||||||
onSelectRoles,
|
onSelectRoles,
|
||||||
|
onPopoverOpenChange,
|
||||||
disabled,
|
disabled,
|
||||||
restrictAdminRole,
|
restrictAdminRole,
|
||||||
mapRolesByName,
|
mapRolesByName,
|
||||||
@@ -77,6 +79,7 @@ export function RolesSelector({
|
|||||||
options={rolesShown}
|
options={rolesShown}
|
||||||
value={selectedRoles}
|
value={selectedRoles}
|
||||||
onChange={onSelectRoles}
|
onChange={onSelectRoles}
|
||||||
|
onPopoverOpenChange={onPopoverOpenChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
lockedIds={lockedIds}
|
lockedIds={lockedIds}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user