Feat/2.6.0 0811 (#2293)

## What

简要描述做了什么改动。

## Why

为什么需要这个改动?

## How

实现方式、设计决策(如有)。

## Test

- [ ] 本地测试通过
- [ ] 114 测试服务器验证通过

## Related

- Issue/ticket:
This commit is contained in:
GuoQing Zhang
2026-08-13 11:34:44 +08:00
committed by GitHub
16 changed files with 325 additions and 177 deletions
@@ -1726,9 +1726,6 @@
}
},
"src/components/permission/PermissionListTab.tsx": {
"@typescript-eslint/no-explicit-any": {
"count": 1
},
"no-restricted-syntax": {
"count": 4
}
@@ -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,
@@ -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">
@@ -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>
);
})}
@@ -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"];
@@ -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)}
>
@@ -65,7 +71,7 @@ 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")}
@@ -73,16 +79,19 @@ export function PermissionDraftPanel({
)}
</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
@@ -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";
@@ -105,28 +113,41 @@ 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)}
/>
@@ -134,10 +155,10 @@ export function PermissionDraftPickerDialog({
</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}
@@ -145,13 +166,15 @@ export function PermissionDraftPickerDialog({
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}
@@ -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>
);
}
@@ -12,58 +12,14 @@ import type {
RevokeItem,
} from "~/api/permission";
import { Avatar, AvatarName } from "~/components/ui/Avatar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "~/components/ui/DropdownMenu";
import { Tooltip, TooltipContent, TooltipTrigger } from "~/components/ui/Tooltip2";
import { Building2, ChevronDown, RotateCcw, Search, User, Users } from "lucide-react";
import { Building2, RotateCcw, Search, User, Users } from "lucide-react";
import { LoadingIcon } from "~/components/ui/icon/Loading";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useLocalize } from "~/hooks";
import { cn } from "~/utils";
import { PermissionLevelMenu } from "./PermissionLevelMenu";
import { RelationModelOption } from "./RelationSelect";
// Tooltip that only shows when the wrapped element's text is truncated.
function TruncatedTooltip({
content,
className,
as: Tag = "span",
children,
}: {
content: string;
className?: string;
as?: "span" | "p" | "div";
children: React.ReactNode;
}) {
const ref = useRef<HTMLElement | null>(null);
const [open, setOpen] = useState(false);
const handleOpenChange = (next: boolean) => {
if (!next) {
setOpen(false);
return;
}
const el = ref.current;
if (el && (el.scrollWidth > el.clientWidth || el.scrollHeight > el.clientHeight)) {
setOpen(true);
}
};
return (
<Tooltip open={open} onOpenChange={handleOpenChange}>
<TooltipTrigger asChild>
<Tag ref={ref as React.LegacyRef<any>} className={className}>{children}</Tag>
</TooltipTrigger>
<TooltipContent side="top" className="z-[120] max-w-xs break-all">
{content}
</TooltipContent>
</Tooltip>
);
}
import { TruncatedTooltip } from "./TruncatedTooltip";
const SUBJECT_ICONS = {
user: User,
@@ -596,61 +552,20 @@ export function PermissionListTab({
</TruncatedTooltip>
<div className="flex w-[136px] shrink-0 items-center justify-end gap-1">
{(canModifyEntry && (!isOwner || canManageOwnerEntry)) || canDeleteEntrySubject ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="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]"
>
<span className="truncate">{getPermissionLabel(entry)}</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"
>
{canModifyEntry && (!isOwner || canManageOwnerEntry) && entryModelOptions.map((model) => {
const active = model.id === currentModelId;
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={() => {
void handleModify(entry, model.id);
}}
>
{model.name}
</DropdownMenuItem>
);
})}
{canModifyEntry && (!isOwner || canManageOwnerEntry) && canDeleteEntrySubject && (
<DropdownMenuSeparator className="my-1 bg-[#EBECF0]" />
)}
{canDeleteEntrySubject && (
<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={() => {
void handleDeleteSubject(entry);
}}
>
{localize("com_permission.remove")}
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
) : (
<span className="inline-flex h-8 w-[96px] shrink-0 items-center justify-end whitespace-nowrap px-2 text-[14px] leading-[22px] text-[#999999]">
{getPermissionLabel(entry)}
</span>
)}
<PermissionLevelMenu
label={getPermissionLabel(entry)}
options={entryModelOptions}
activeId={currentModelId}
canChangeLevel={canModifyEntry && (!isOwner || canManageOwnerEntry)}
onChange={(modelId) => {
void handleModify(entry, modelId);
}}
onRemove={canDeleteEntrySubject
? () => {
void handleDeleteSubject(entry);
}
: undefined}
/>
</div>
</div>
);
@@ -0,0 +1,48 @@
import type { ReactNode } from "react";
import { useRef, useState } from "react";
import { Tooltip, TooltipContent, TooltipTrigger } from "~/components/ui/Tooltip2";
interface TruncatedTooltipProps {
/** Full text, shown only when the rendered element is actually clipped. */
content: string;
className?: string;
as?: "span" | "p" | "div";
children: ReactNode;
}
/**
* Tooltip that stays silent unless the wrapped text is truncated — so rows that
* fit never fire a hover popup.
*/
export function TruncatedTooltip({
content,
className,
as: Tag = "span",
children,
}: TruncatedTooltipProps) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- polymorphic `as`: no single element type covers span/p/div refs
const ref = useRef<any>(null);
const [open, setOpen] = useState(false);
const handleOpenChange = (next: boolean) => {
if (!next) {
setOpen(false);
return;
}
const el = ref.current;
if (el && (el.scrollWidth > el.clientWidth || el.scrollHeight > el.clientHeight)) {
setOpen(true);
}
};
return (
<Tooltip open={open} onOpenChange={handleOpenChange}>
<TooltipTrigger asChild>
<Tag ref={ref} className={className}>{children}</Tag>
</TooltipTrigger>
<TooltipContent side="top" className="z-[120] max-w-xs break-all">
{content}
</TooltipContent>
</Tooltip>
);
}
@@ -3,6 +3,7 @@ import * as RadioGroup from "@radix-ui/react-radio-group";
import { Outlined } from "bisheng-icons";
import type { ComponentType, ReactNode } from "react";
import { Switch } from "~/components/ui/Switch";
import { TruncatedTooltip } from "./TruncatedTooltip";
export type SettingsSectionKind = "basic" | "advanced" | "permission";
@@ -26,7 +27,7 @@ export function SettingsSectionHeader({
}: SettingsSectionHeaderProps) {
const Icon = SECTION_ICONS[kind];
return (
<div className="flex h-8 items-center gap-2 rounded-md bg-fill-2 px-3 text-body-sm font-medium text-text-1">
<div className="flex h-8 items-center gap-2 rounded-md bg-fill-1 px-3 text-body-sm font-medium text-text-1">
<Icon className="size-4 text-blue-500" />
<span>{title}</span>
</div>
@@ -75,7 +76,7 @@ export function AccessModeSelector({
{options.map((option) => (
<label
key={option.value}
className="flex min-h-12 cursor-pointer items-center gap-2 rounded-xl border border-border-base px-3 text-body text-text-1 has-[[data-disabled]]:cursor-not-allowed has-[[data-disabled]]:opacity-60"
className="flex min-h-12 cursor-pointer items-center gap-2 rounded-xl border border-border-base px-3 text-body text-text-1 transition-colors hover:bg-fill-1 has-[[data-state=checked]]:bg-blue-500/[0.07] has-[[data-disabled]]:cursor-not-allowed has-[[data-disabled]]:opacity-60 has-[[data-disabled]]:hover:bg-transparent"
>
<RadioGroup.Item
value={option.value}
@@ -83,10 +84,13 @@ export function AccessModeSelector({
>
<RadioGroup.Indicator className="size-1 rounded-full bg-fill-1" />
</RadioGroup.Item>
<span className="font-medium">{option.label}</span>
<span className="min-w-0 truncate text-[#999999]">
<span className="shrink-0 whitespace-nowrap font-medium">{option.label}</span>
<TruncatedTooltip
content={option.description}
className="min-w-0 truncate text-[#999999]"
>
{option.description}
</span>
</TruncatedTooltip>
</label>
))}
</RadioGroup.Root>
@@ -0,0 +1,42 @@
/**
* Shared chrome for the permission dialogs.
*
* Extracted verbatim from KnowledgeSpaceShareDialog, which shipped the original
* "新增授权" dialog. The unified-permission draft picker reuses these so both
* dialogs stay pixel-identical instead of drifting into two look-alikes.
*/
/** Dialog shell: fixed 80vh card on desktop, full-screen sheet under 768px. */
export const PERMISSION_DIALOG_CONTENT_CLASS =
"!flex h-[80vh] max-h-[800px] w-[calc(100vw-80px)] max-w-[800px] min-w-0 flex-col gap-0 overflow-hidden p-5 max-[768px]:fixed max-[768px]:inset-0 max-[768px]:h-[100dvh] max-[768px]:max-h-[100dvh] max-[768px]:w-full max-[768px]:max-w-none max-[768px]:translate-x-0 max-[768px]:translate-y-0 max-[768px]:rounded-none max-[768px]:p-4";
/** Subject-type switcher: bordered pill group, brand-tinted active segment. */
export const SUBJECT_TAB_LIST_CLASS =
"w-fit shrink-0 rounded-md border border-[#ECECEC] bg-white p-[3px] shadow-none";
export const SUBJECT_TAB_TRIGGER_CLASS =
"min-w-0 rounded-[4px] px-3 py-0.5 text-[14px] font-normal leading-[22px] text-[#818181] shadow-none data-[state=active]:bg-[rgb(var(--brand-500)/0.15)] data-[state=active]:font-medium data-[state=active]:text-blue-500 data-[state=active]:shadow-none";
/**
* Same switcher rendered with plain buttons instead of Radix Tabs — used where
* the active segment is driven by external state. Wrap them in
* `inline-flex items-center justify-center ${SUBJECT_TAB_LIST_CLASS}`.
*/
export const SUBJECT_TAB_BUTTON_CLASS =
"min-w-0 rounded-[4px] px-3 py-0.5 text-[14px] leading-[22px] transition-colors";
export const SUBJECT_TAB_BUTTON_ACTIVE_CLASS =
"bg-[rgb(var(--brand-500)/0.15)] font-medium text-blue-500";
export const SUBJECT_TAB_BUTTON_INACTIVE_CLASS = "font-normal text-[#818181]";
/** "包含子部门" toggle sitting next to the tab group. */
export const INCLUDE_CHILDREN_LABEL_CLASS =
"flex shrink-0 cursor-pointer items-center gap-2 text-[14px] leading-[22px] text-[#212121]";
export const INCLUDE_CHILDREN_CHECKBOX_CLASS =
"border-[#D9D9D9] data-[state=checked]:border-primary data-[state=indeterminate]:border-primary";
/** Muted caption used by the footer labels ("已选用户:", "统一授权:"). */
export const PERMISSION_FOOTER_LABEL_CLASS =
"shrink-0 text-[14px] font-normal leading-[22px] text-[#999999]";
@@ -1867,6 +1867,7 @@
"add_authorization": "Add permission",
"advanced_settings": "Advanced settings",
"authorization": "Permissions",
"authorization_empty": "No authorized subjects for this type yet",
"basic_settings": "Basic settings",
"cancel": "Cancel",
"confirm_create": "Create",
@@ -1790,6 +1790,7 @@
"add_authorization": "権限を追加",
"advanced_settings": "詳細設定",
"authorization": "権限",
"authorization_empty": "現在のタイプに権限対象はまだありません",
"basic_settings": "基本設定",
"cancel": "キャンセル",
"confirm_create": "作成を確定",
@@ -1796,6 +1796,7 @@
"add_authorization": "新增授权",
"advanced_settings": "高级设置",
"authorization": "授权",
"authorization_empty": "当前类型下暂无授权对象",
"basic_settings": "基础设置",
"cancel": "取消",
"confirm_create": "确认创建",
@@ -1,4 +1,5 @@
import { Outlined } from "bisheng-icons";
// bisheng-icons has no boxed plus; SquarePlus matches the sibling "add condition" button.
import { SquarePlus } from "lucide-react";
import type { ComponentProps } from "react";
import { NotificationSeverity } from "~/common";
import {
@@ -183,7 +184,7 @@ export function ChannelBusinessSettings({
/>
{form.createSubChannel && (
<div
className="overflow-hidden rounded-lg border border-border-base bg-white"
className="divide-y divide-border-base overflow-hidden rounded-lg border border-border-base bg-white"
data-testid="sub-channel-list"
>
{form.subChannels.map((subChannel) => (
@@ -219,9 +220,12 @@ export function ChannelBusinessSettings({
<button
type="button"
onClick={form.handleAddSubChannel}
className="flex h-12 w-full items-center gap-2 bg-fill-2 px-4 text-body text-text-2 hover:bg-fill-3"
className="flex h-12 w-full items-center gap-3 bg-[#F8F8F8] px-4 text-left text-body text-text-1 transition-colors hover:bg-fill-2"
>
<Outlined.Plus className="size-4" />
<SquarePlus
className="size-4 shrink-0 text-text-2"
strokeWidth={1.5}
/>
{settings.localize("com_subscription.add_sub_channel")}
</button>
)}
@@ -348,6 +348,7 @@ export function ChannelSettingsPage() {
</div>
<SettingsFooter
centered
cancelLabel={settings.localize("com_unified_permission.cancel")}
submitLabel={settings.localize(
settings.isEditMode
@@ -1,4 +1,14 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import {
INCLUDE_CHILDREN_CHECKBOX_CLASS,
INCLUDE_CHILDREN_LABEL_CLASS,
PERMISSION_DIALOG_CONTENT_CLASS,
SUBJECT_TAB_BUTTON_ACTIVE_CLASS,
SUBJECT_TAB_BUTTON_CLASS,
SUBJECT_TAB_BUTTON_INACTIVE_CLASS,
SUBJECT_TAB_LIST_CLASS,
SUBJECT_TAB_TRIGGER_CLASS,
} from "~/components/permission/permissionDialogStyles";
import { PermissionGrantTab } from "~/components/permission/PermissionGrantTab";
import { PermissionListTab } from "~/components/permission/PermissionListTab";
import {
@@ -113,12 +123,12 @@ export function KnowledgeSpaceShareDialog({
className="flex min-h-0 flex-1 flex-col"
>
<div className="flex items-center justify-between gap-3">
<TabsList className="w-fit shrink-0 rounded-md border border-[#ECECEC] bg-white p-[3px] shadow-none">
<TabsList className={SUBJECT_TAB_LIST_CLASS}>
{SUBJECT_TABS.map((tab) => (
<TabsTrigger
key={tab.value}
value={tab.value}
className="min-w-0 rounded-[4px] px-3 py-0.5 text-[14px] font-normal leading-[22px] text-[#818181] shadow-none data-[state=active]:bg-[rgb(var(--brand-500)/0.15)] data-[state=active]:font-medium data-[state=active]:text-blue-500 data-[state=active]:shadow-none"
className={SUBJECT_TAB_TRIGGER_CLASS}
>
{localize(tab.labelKey)}
</TabsTrigger>
@@ -160,7 +170,7 @@ export function KnowledgeSpaceShareDialog({
return (
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="!flex h-[80vh] max-h-[800px] w-[calc(100vw-80px)] max-w-[800px] min-w-0 flex-col gap-0 overflow-hidden p-5 max-[768px]:fixed max-[768px]:inset-0 max-[768px]:h-[100dvh] max-[768px]:max-h-[100dvh] max-[768px]:w-full max-[768px]:max-w-none max-[768px]:translate-x-0 max-[768px]:translate-y-0 max-[768px]:rounded-none max-[768px]:p-4">
<DialogContent className={PERMISSION_DIALOG_CONTENT_CLASS}>
<DialogHeader className="shrink-0 text-left">
<DialogTitle className="text-left">{dialogTitle}</DialogTitle>
</DialogHeader>
@@ -172,7 +182,7 @@ export function KnowledgeSpaceShareDialog({
</Dialog>
<Dialog open={grantDialogOpen} onOpenChange={setGrantDialogOpen}>
<DialogContent className="!flex h-[80vh] max-h-[800px] w-[calc(100vw-80px)] max-w-[800px] min-w-0 flex-col gap-0 overflow-hidden p-5 max-[768px]:fixed max-[768px]:inset-0 max-[768px]:h-[100dvh] max-[768px]:max-h-[100dvh] max-[768px]:w-full max-[768px]:max-w-none max-[768px]:translate-x-0 max-[768px]:translate-y-0 max-[768px]:rounded-none max-[768px]:p-4">
<DialogContent className={PERMISSION_DIALOG_CONTENT_CLASS}>
<DialogHeader className="shrink-0 text-left">
<DialogTitle className="text-left">
{localize("com_permission.tab_grant")} - {resourceName}
@@ -181,16 +191,16 @@ export function KnowledgeSpaceShareDialog({
<div className="user-manger mt-4 flex min-h-0 flex-1 flex-col overflow-hidden">
<div className="flex items-center gap-3">
<div className="inline-flex w-fit shrink-0 items-center justify-center rounded-md border border-[#ECECEC] bg-white p-[3px]">
<div className={`inline-flex items-center justify-center ${SUBJECT_TAB_LIST_CLASS}`}>
{SUBJECT_TABS.map((tab) => (
<button
key={tab.value}
type="button"
className={[
"min-w-0 rounded-[4px] px-3 py-0.5 text-[14px] leading-[22px] transition-colors",
SUBJECT_TAB_BUTTON_CLASS,
grantSubjectType === tab.value
? "bg-[rgb(var(--brand-500)/0.15)] font-medium text-blue-500"
: "font-normal text-[#818181]",
? SUBJECT_TAB_BUTTON_ACTIVE_CLASS
: SUBJECT_TAB_BUTTON_INACTIVE_CLASS,
].join(" ")}
onClick={() => setGrantSubjectType(tab.value)}
>
@@ -200,9 +210,9 @@ export function KnowledgeSpaceShareDialog({
</div>
{grantSubjectType === "department" && (
<label className="flex shrink-0 cursor-pointer items-center gap-2 text-[14px] leading-[22px] text-[#212121]">
<label className={INCLUDE_CHILDREN_LABEL_CLASS}>
<Checkbox
className="border-[#D9D9D9] data-[state=checked]:border-primary data-[state=indeterminate]:border-primary"
className={INCLUDE_CHILDREN_CHECKBOX_CLASS}
checked={grantIncludeChildren}
onCheckedChange={(value) => setGrantIncludeChildren(value === true)}
/>
@@ -230,7 +230,7 @@ export function KnowledgeSpaceSettingsPage() {
return (
<main
className="flex h-full min-h-0 bg-white p-2"
className="flex h-full min-h-0 bg-white"
data-testid="knowledge-space-settings-page"
>
<div className="flex min-h-0 w-full flex-1 flex-col rounded-xl bg-white px-4 pt-4">
@@ -553,7 +553,7 @@ export function KnowledgeSpaceSettingsPage() {
</div>
<SettingsFooter
centered={settings.mode === "create"}
centered
cancelLabel={localize("com_unified_permission.cancel")}
submitLabel={localize(
settings.mode === "create"