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
3 changes: 0 additions & 3 deletions src/frontend/client/eslint-suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -1726,9 +1726,6 @@
}
},
"src/components/permission/PermissionListTab.tsx": {
"@typescript-eslint/no-explicit-any": {
"count": 1
},
"no-restricted-syntax": {
"count": 4
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { Button } from "@bisheng/ui";
import { Outlined } from "bisheng-icons";
import { useLocalize } from "~/hooks";
import { Tooltip, TooltipContent, TooltipTrigger } from "~/components/ui/Tooltip2";
import { RelationSelect } from "./RelationSelect";
import { PermissionLevelMenu } from "./PermissionLevelMenu";
import type { RelationModelOption } from "./RelationSelect";
import {
getPermissionDraftRowKey,
Expand Down Expand Up @@ -58,6 +55,10 @@ export function PermissionDraftEditor({
&& capabilities.canChangeRelation
&& relationModels.length > 0;
const canRemove = !row.immutableCreator && capabilities.canRemove;
const activeModelId = row.modelId ?? row.relation;
const relationLabel =
capabilities.relationModels.find((model) => model.id === activeModelId)?.name
?? localize(`com_permission.level_${row.relation}`);

return (
<div key={rowKey} className="flex min-h-11 items-center gap-3 py-2">
Expand All @@ -68,34 +69,19 @@ export function PermissionDraftEditor({
<span className="min-w-0 truncate text-body text-text-1">{row.subjectName}</span>
</div>
{row.immutableCreator ? (
<span className="px-2 text-body text-[#999999]">{localize("creator")}</span>
<span className="inline-flex h-8 w-[96px] shrink-0 items-center justify-end whitespace-nowrap px-2 text-[14px] leading-[22px] text-[#999999]">
{localize("creator")}
</span>
) : (
<RelationSelect
value={row.modelId ?? row.relation}
onChange={(modelId) => handleRelationChange(row, modelId)}
<PermissionLevelMenu
label={relationLabel}
options={relationModels}
disabled={!canChangeRelation}
className="w-32"
activeId={row.modelId ?? row.relation}
canChangeLevel={canChangeRelation}
onChange={(modelId) => handleRelationChange(row, modelId)}
onRemove={canRemove ? () => handleRemove(row) : undefined}
/>
)}
{canRemove && (
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
color="danger"
variant="text"
size="small"
iconOnly
aria-label={localize("com_permission.remove")}
onClick={() => handleRemove(row)}
>
<Outlined.Delete className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>{localize("com_permission.remove")}</TooltipContent>
</Tooltip>
)}
</div>
);
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import type { SubjectType } from "~/api/permission";
import { EmptyStateIllustration } from "~/components/illustrations";
import { useLocalize } from "~/hooks";
import { PermissionDraftEditor, type PermissionDraftEditorCapabilities } from "./PermissionDraftEditor";
import {
SUBJECT_TAB_BUTTON_ACTIVE_CLASS,
SUBJECT_TAB_BUTTON_CLASS,
SUBJECT_TAB_BUTTON_INACTIVE_CLASS,
SUBJECT_TAB_LIST_CLASS,
} from "./permissionDialogStyles";
import { getPermissionDraftRowKey, type PermissionDraftRow } from "./usePermissionDraft";

const SUBJECT_TYPES: SubjectType[] = ["user", "department", "user_group"];
Expand Down Expand Up @@ -43,15 +49,15 @@ export function PermissionDraftPanel({
{localize("com_unified_permission.authorization")}
</div>
<div className="flex items-center justify-between gap-3">
<div className="inline-flex rounded-md bg-fill-2 p-[3px]">
<div className={`inline-flex items-center justify-center ${SUBJECT_TAB_LIST_CLASS}`}>
{SUBJECT_TYPES.map((type) => (
<button
key={type}
type="button"
className={`rounded px-3 py-0.5 text-body ${
className={`${SUBJECT_TAB_BUTTON_CLASS} ${
activeSubjectType === type
? "bg-blue-500/[0.15] font-medium text-blue-500"
: "text-text-3"
? SUBJECT_TAB_BUTTON_ACTIVE_CLASS
: SUBJECT_TAB_BUTTON_INACTIVE_CLASS
}`}
onClick={() => onActiveSubjectTypeChange(type)}
>
Expand All @@ -65,24 +71,27 @@ export function PermissionDraftPanel({
color="primary"
variant="filled"
size="small"
className="h-7"
className="h-7 px-3"
onClick={onAddAuthorization}
>
{localize("com_unified_permission.add_authorization")}
</Button>
)}
</div>
<div
className="h-[400px] overflow-y-auto rounded-xl border border-border-base bg-white px-3"
className="h-[400px] overflow-y-auto rounded-xl bg-white pl-2"
data-testid="authorization-list-body"
>
{visibleRows.length === 0 ? (
<div className="flex h-full items-center justify-center">
<div className="flex h-full flex-col items-center justify-center gap-2">
<EmptyStateIllustration
role="img"
aria-label={localize("com_subscription.no_data")}
className="size-[120px]"
/>
<p className="text-body text-text-3">
{localize("com_unified_permission.authorization_empty")}
</p>
</div>
) : (
<PermissionDraftEditor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ import {
} from "~/components/ui/Dialog";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/Tabs";
import { useLocalize } from "~/hooks";
import {
INCLUDE_CHILDREN_CHECKBOX_CLASS,
INCLUDE_CHILDREN_LABEL_CLASS,
PERMISSION_DIALOG_CONTENT_CLASS,
PERMISSION_FOOTER_LABEL_CLASS,
SUBJECT_TAB_LIST_CLASS,
SUBJECT_TAB_TRIGGER_CLASS,
} from "./permissionDialogStyles";
import { RelationSelect, type RelationModelOption } from "./RelationSelect";
import { SubjectSearchDepartment } from "./SubjectSearchDepartment";
import { SubjectSearchUser } from "./SubjectSearchUser";
Expand Down Expand Up @@ -105,53 +113,68 @@ export function PermissionDraftPickerDialog({

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="flex h-[min(680px,80vh)] max-w-[720px] flex-col overflow-hidden bg-surface-primary">
<DialogHeader>
<DialogTitle>{localize("com_unified_permission.add_authorization")}</DialogTitle>
<DialogContent className={PERMISSION_DIALOG_CONTENT_CLASS}>
<DialogHeader className="shrink-0 text-left">
<DialogTitle className="text-left">
{localize("com_unified_permission.add_authorization")}
</DialogTitle>
</DialogHeader>
<Tabs
value={subjectType}
onValueChange={handleSubjectTypeChange}
className="flex min-h-0 flex-1 flex-col"
className="mt-4 flex min-h-0 flex-1 flex-col overflow-hidden"
>
<div className="flex items-center justify-between gap-3">
<TabsList>
<TabsTrigger value="user">{localize("com_permission.subject_user")}</TabsTrigger>
<TabsTrigger value="department" disabled={!canAddNonUserSubjects}>
<div className="flex items-center gap-3">
<TabsList className={SUBJECT_TAB_LIST_CLASS}>
<TabsTrigger value="user" className={SUBJECT_TAB_TRIGGER_CLASS}>
{localize("com_permission.subject_user")}
</TabsTrigger>
<TabsTrigger
value="department"
className={SUBJECT_TAB_TRIGGER_CLASS}
disabled={!canAddNonUserSubjects}
>
{localize("com_permission.subject_department")}
</TabsTrigger>
<TabsTrigger value="user_group" disabled={!canAddNonUserSubjects}>
<TabsTrigger
value="user_group"
className={SUBJECT_TAB_TRIGGER_CLASS}
disabled={!canAddNonUserSubjects}
>
{localize("com_permission.subject_user_group")}
</TabsTrigger>
</TabsList>
{subjectType === "department" && (
<label className="flex items-center gap-2 text-body text-text-2">
<label className={INCLUDE_CHILDREN_LABEL_CLASS}>
<Checkbox
className={INCLUDE_CHILDREN_CHECKBOX_CLASS}
checked={includeChildren}
onCheckedChange={(checked) => setIncludeChildren(checked === true)}
/>
{localize("com_permission.include_children")}
</label>
)}
</div>
<TabsContent value="user" className="mt-4 min-h-0 flex-1">
<TabsContent value="user" className="mt-3 min-h-0 flex-1 overflow-hidden p-0">
<SubjectSearchUser {...searchProps} grantUsersApi={searchApi?.grantUsersApi} />
</TabsContent>
<TabsContent value="department" className="mt-4 min-h-0 flex-1">
<TabsContent value="department" className="mt-3 min-h-0 flex-1 overflow-hidden p-0">
<SubjectSearchDepartment
{...searchProps}
includeChildren={includeChildren}
grantDepartmentChildrenApi={searchApi?.grantDepartmentChildrenApi}
grantDepartmentSearchApi={searchApi?.grantDepartmentSearchApi}
/>
</TabsContent>
<TabsContent value="user_group" className="mt-4 min-h-0 flex-1">
<TabsContent value="user_group" className="mt-3 min-h-0 flex-1 overflow-hidden p-0">
<SubjectSearchUserGroup {...searchProps} grantUserGroupsApi={searchApi?.grantUserGroupsApi} />
</TabsContent>
</Tabs>
<DialogFooter className="items-center sm:justify-between">
<div className="flex items-center gap-2 text-body-sm text-text-3">
<span>{localize("com_permission.uniform_grant")}</span>
<DialogFooter className="mt-3 shrink-0 items-center border-t pt-3 sm:justify-between">
<div className="flex items-center gap-2">
<span className={PERMISSION_FOOTER_LABEL_CLASS}>
{localize("com_permission.uniform_grant")}
</span>
<RelationSelect
value={activeModel?.id ?? ""}
onChange={setSelectedModelId}
Expand Down
106 changes: 106 additions & 0 deletions src/frontend/client/src/components/permission/PermissionLevelMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "~/components/ui/DropdownMenu";
import { ChevronDown } from "lucide-react";
import { useLocalize } from "~/hooks";
import { cn } from "~/utils";
import type { RelationModelOption } from "./RelationSelect";

interface PermissionLevelMenuProps {
/** Current level shown on the trigger. */
label: string;
options: RelationModelOption[];
activeId?: string;
/** When false the level items are hidden — the menu then only offers removal. */
canChangeLevel: boolean;
onChange: (modelId: string) => void;
/** Renders the destructive "移除" item. Omit to hide it. */
onRemove?: () => void;
className?: string;
}

/**
* Level dropdown shared by the member-management list and the create-page
* authorization draft. Removal lives inside the menu — never as a separate
* icon button next to it.
*/
export function PermissionLevelMenu({
label,
options,
activeId,
canChangeLevel,
onChange,
onRemove,
className,
}: PermissionLevelMenuProps) {
const localize = useLocalize();
const showLevels = canChangeLevel && options.length > 0;

if (!showLevels && !onRemove) {
return (
<span
className={cn(
"inline-flex h-8 w-[96px] shrink-0 items-center justify-end whitespace-nowrap px-2 text-[14px] leading-[22px] text-[#999999]",
className,
)}
>
{label}
</span>
);
}

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className={cn(
"inline-flex h-8 w-[96px] items-center justify-end gap-1 rounded-md px-2 text-[14px] leading-[22px] text-[#999999] transition-colors hover:bg-[#F7F7F7]",
className,
)}
>
<span className="truncate">{label}</span>
<ChevronDown className="size-3.5 shrink-0 text-[#999999]" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="z-[120] max-h-[240px] w-[100px] overflow-x-hidden overflow-y-auto overscroll-none rounded-lg border-0 bg-white p-1 shadow-[0px_6px_20px_1px_rgba(117,145,212,0.12)] scrollbar-hide [&::-webkit-scrollbar]:!w-0 [&::-webkit-scrollbar]:!h-0"
>
{showLevels && options.map((model) => {
const active = model.id === activeId;
return (
<DropdownMenuItem
key={model.id}
className={cn(
"rounded-md px-2 py-[5px] text-[14px] leading-[22px]",
active
? "bg-blue-500/[0.07] text-blue-500 data-[highlighted]:bg-blue-500/[0.07] data-[highlighted]:text-blue-500"
: "text-[#212121] data-[highlighted]:bg-[#F7F7F7] data-[highlighted]:text-[#212121]",
)}
onSelect={() => onChange(model.id)}
>
{model.name}
</DropdownMenuItem>
);
})}
{showLevels && onRemove && (
<DropdownMenuSeparator className="my-1 bg-[#EBECF0]" />
)}
{onRemove && (
<DropdownMenuItem
aria-label={localize("com_permission.remove")}
className="rounded-md px-2 py-[5px] text-[14px] leading-[22px] text-[#F53F3F] data-[highlighted]:bg-[#FFF2F0] data-[highlighted]:text-[#F53F3F]"
onSelect={() => onRemove()}
>
{localize("com_permission.remove")}
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
);
}
Loading
Loading