feat: preview default roles permissions (#22621)

Co-authored-by: Alex van Andel <me@alexvanandel.com>
This commit is contained in:
sean-brydon
2025-07-18 12:20:19 +00:00
committed by GitHub
co-authored by Alex van Andel
parent e1d7631215
commit dc5aef0dd4
6 changed files with 64 additions and 22 deletions
@@ -16,6 +16,7 @@ interface AdvancedPermissionGroupProps {
resource: Resource;
selectedPermissions: string[];
onChange: (permissions: string[]) => void;
disabled?: boolean;
}
const INTERNAL_DATAACCESS_KEY = "_resource";
@@ -24,6 +25,7 @@ export function AdvancedPermissionGroup({
resource,
selectedPermissions,
onChange,
disabled,
}: AdvancedPermissionGroupProps) {
const { t } = useLocale();
const { toggleSinglePermission, toggleResourcePermissionLevel } = usePermissions();
@@ -47,7 +49,9 @@ export function AdvancedPermissionGroup({
const handleToggleAll = (e: React.MouseEvent) => {
e.stopPropagation(); // Stop event from triggering parent click
onChange(toggleResourcePermissionLevel(resource, isAllSelected ? "none" : "all", selectedPermissions));
if (!disabled) {
onChange(toggleResourcePermissionLevel(resource, isAllSelected ? "none" : "all", selectedPermissions));
}
};
// Helper function to check if read permission is auto-enabled
@@ -77,6 +81,7 @@ export function AdvancedPermissionGroup({
checked={isAllSelected}
onCheckedChange={() => handleToggleAll}
onClick={handleToggleAll}
disabled={disabled}
/>
<span className="text-default text-sm font-medium leading-none">
{t(resourceConfig._resource?.i18nKey || "")}
@@ -107,9 +112,12 @@ export function AdvancedPermissionGroup({
checked={isChecked}
className="mr-2"
onCheckedChange={(checked) => {
onChange(toggleSinglePermission(permission, !!checked, selectedPermissions));
if (!disabled) {
onChange(toggleSinglePermission(permission, !!checked, selectedPermissions));
}
}}
onClick={(e) => e.stopPropagation()} // Stop checkbox clicks from affecting parent
disabled={disabled}
/>
<div
className="flex items-center gap-2"
@@ -7,9 +7,10 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
export interface RoleColorPickerProps {
value: string;
onChange: (color: string) => void;
disabled?: boolean;
}
export default function RoleColorPicker({ value, onChange }: RoleColorPickerProps) {
export default function RoleColorPicker({ value, onChange, disabled }: RoleColorPickerProps) {
const { t } = useLocale();
const [, setCustomColor] = useState(value);
const colorInputRef = useRef<HTMLInputElement>(null);
@@ -21,7 +22,9 @@ export default function RoleColorPicker({ value, onChange }: RoleColorPickerProp
};
const handleColorSquareClick = () => {
colorInputRef.current?.click();
if (!disabled) {
colorInputRef.current?.click();
}
};
return (
@@ -29,7 +32,8 @@ export default function RoleColorPicker({ value, onChange }: RoleColorPickerProp
<button
type="button"
onClick={handleColorSquareClick}
className="bg-default text-default border-default hover:bg-muted hover:text-emphasis focus:bg-subtle focus-visible:shadow-outline-gray-focused shadow-outline-gray-rested enabled:hover:shadow-outline-gray-hover enabled:active:shadow-outline-gray-active flex h-8 w-8 items-center justify-center gap-2 rounded-[10px] border text-sm transition-shadow duration-200 focus-visible:outline-none focus-visible:ring-0">
disabled={disabled}
className="bg-default text-default border-default hover:bg-muted hover:text-emphasis focus:bg-subtle focus-visible:shadow-outline-gray-focused shadow-outline-gray-rested enabled:hover:shadow-outline-gray-hover enabled:active:shadow-outline-gray-active flex h-8 w-8 items-center justify-center gap-2 rounded-[10px] border text-sm transition-shadow duration-200 focus-visible:outline-none focus-visible:ring-0 disabled:cursor-not-allowed disabled:opacity-50">
<div
className="h-5 w-5 rounded border shadow-sm"
style={{ backgroundColor: value }}
@@ -62,6 +62,7 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
const { t } = useLocale();
const router = useRouter();
const isEditing = Boolean(role);
const isSystemRole = role?.type === "SYSTEM";
const [searchQuery, setSearchQuery] = useState("");
const defaultValues = useMemo(
@@ -171,7 +172,9 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent>
<SheetHeader>
<SheetTitle>{isEditing ? t("edit_role") : t("create_role")}</SheetTitle>
<SheetTitle>
{isSystemRole ? t("view_role") : isEditing ? t("edit_role") : t("create_role")}
</SheetTitle>
</SheetHeader>
<Form form={form} handleSubmit={onSubmit}>
<div className="space-y-4 py-5">
@@ -181,11 +184,13 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
label={t("role_name")}
{...form.register("name")}
placeholder={t("role_name_placeholder")}
disabled={isSystemRole}
/>
</div>
<RoleColorPicker
value={color}
onChange={(value) => form.setValue("color", value, { shouldDirty: true })}
disabled={isSystemRole}
/>
</div>
@@ -208,6 +213,7 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
placeholder={t("search_permissions")}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
disabled={isSystemRole}
/>
</div>
@@ -217,6 +223,7 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
resource={resource as Resource}
selectedPermissions={permissions}
onChange={(newPermissions) => form.setValue("permissions", newPermissions)}
disabled={isSystemRole}
/>
))}
</div>
@@ -239,6 +246,7 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
resource={resource}
permissions={permissions}
onChange={(newPermissions) => form.setValue("permissions", newPermissions)}
disabled={isSystemRole}
/>
))}
</div>
@@ -247,18 +255,27 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
</div>
</div>
<SheetFooter>
<Button
type="button"
color="secondary"
onClick={() => onOpenChange(false)}
disabled={createMutation.isPending || updateMutation.isPending}>
{t("cancel")}
</Button>
<Button type="submit" loading={createMutation.isPending || updateMutation.isPending}>
{isEditing ? t("save") : t("create")}
</Button>
</SheetFooter>
{!isSystemRole && (
<SheetFooter>
<Button
type="button"
color="secondary"
onClick={() => onOpenChange(false)}
disabled={createMutation.isPending || updateMutation.isPending}>
{t("cancel")}
</Button>
<Button type="submit" loading={createMutation.isPending || updateMutation.isPending}>
{isEditing ? t("save") : t("create")}
</Button>
</SheetFooter>
)}
{isSystemRole && (
<SheetFooter>
<Button type="button" color="secondary" onClick={() => onOpenChange(false)}>
{t("close")}
</Button>
</SheetFooter>
)}
</Form>
</SheetContent>
</Sheet>
@@ -81,8 +81,10 @@ export function RolesList({
role={role}
key={role.id}
onClick={() => {
// Cant edit system roles
// For system roles, open in view-only mode
if (role.type === "SYSTEM") {
setSelectedRoleId(role.id);
setIsOpen(true);
return;
}
@@ -136,7 +138,9 @@ function RoleItem({
<div
className={classNames(
"border-subtle flex p-3",
canUpdate && role.type !== "SYSTEM" && "hover:bg-subtle cursor-pointer"
(canUpdate && role.type !== "SYSTEM") || role.type === "SYSTEM"
? "hover:bg-subtle cursor-pointer"
: ""
)}
onClick={onClick}>
<div className="flex w-full items-center gap-3 truncate">
@@ -12,9 +12,15 @@ interface SimplePermissionItemProps {
resource: string;
permissions: string[];
onChange: (permissions: string[]) => void;
disabled?: boolean;
}
export function SimplePermissionItem({ resource, permissions, onChange }: SimplePermissionItemProps) {
export function SimplePermissionItem({
resource,
permissions,
onChange,
disabled,
}: SimplePermissionItemProps) {
const { t } = useLocale();
const { getResourcePermissionLevel, toggleResourcePermissionLevel } = usePermissions();
@@ -37,10 +43,12 @@ export function SimplePermissionItem({ resource, permissions, onChange }: Simple
</span>
<ToggleGroup
onValueChange={(val) => {
if (val) onChange(toggleResourcePermissionLevel(resource, val as PermissionLevel, permissions));
if (val && !disabled)
onChange(toggleResourcePermissionLevel(resource, val as PermissionLevel, permissions));
}}
value={getResourcePermissionLevel(resource, permissions)}
options={options}
disabled={disabled}
/>
</div>
);
@@ -683,6 +683,7 @@
"send_invite_email": "Send an invite email",
"role": "Role",
"edit_role": "Edit Role",
"view_role": "View Role",
"edit_team": "Edit team",
"editing_user": "Editing user",
"editing_org": "Editing organization",