diff --git a/src/frontend/client/eslint-suppressions.json b/src/frontend/client/eslint-suppressions.json index b3ad68c14e..e914f6c21e 100644 --- a/src/frontend/client/eslint-suppressions.json +++ b/src/frontend/client/eslint-suppressions.json @@ -1726,9 +1726,6 @@ } }, "src/components/permission/PermissionListTab.tsx": { - "@typescript-eslint/no-explicit-any": { - "count": 1 - }, "no-restricted-syntax": { "count": 4 } diff --git a/src/frontend/client/src/components/permission/PermissionDraftEditor.tsx b/src/frontend/client/src/components/permission/PermissionDraftEditor.tsx index 353a738d36..baba3f4bf4 100644 --- a/src/frontend/client/src/components/permission/PermissionDraftEditor.tsx +++ b/src/frontend/client/src/components/permission/PermissionDraftEditor.tsx @@ -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 (
@@ -68,34 +69,19 @@ export function PermissionDraftEditor({ {row.subjectName}
{row.immutableCreator ? ( - {localize("creator")} + + {localize("creator")} + ) : ( - handleRelationChange(row, modelId)} + handleRelationChange(row, modelId)} + onRemove={canRemove ? () => handleRemove(row) : undefined} /> )} - {canRemove && ( - - - - - {localize("com_permission.remove")} - - )} ); })} diff --git a/src/frontend/client/src/components/permission/PermissionDraftPanel.tsx b/src/frontend/client/src/components/permission/PermissionDraftPanel.tsx index 6010c933d5..361659a4c6 100644 --- a/src/frontend/client/src/components/permission/PermissionDraftPanel.tsx +++ b/src/frontend/client/src/components/permission/PermissionDraftPanel.tsx @@ -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")}
-
+
{SUBJECT_TYPES.map((type) => (
{visibleRows.length === 0 ? ( -
+
+

+ {localize("com_unified_permission.authorization_empty")} +

) : ( - - - {localize("com_unified_permission.add_authorization")} + + + + {localize("com_unified_permission.add_authorization")} + -
- - {localize("com_permission.subject_user")} - +
+ + + {localize("com_permission.subject_user")} + + {localize("com_permission.subject_department")} - + {localize("com_permission.subject_user_group")} {subjectType === "department" && ( -
- + - + - + - -
- {localize("com_permission.uniform_grant")} + +
+ + {localize("com_permission.uniform_grant")} +
); diff --git a/src/frontend/client/src/components/permission/TruncatedTooltip.tsx b/src/frontend/client/src/components/permission/TruncatedTooltip.tsx new file mode 100644 index 0000000000..f2a523b812 --- /dev/null +++ b/src/frontend/client/src/components/permission/TruncatedTooltip.tsx @@ -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(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 ( + + + {children} + + + {content} + + + ); +} diff --git a/src/frontend/client/src/components/permission/UnifiedPermissionControls.tsx b/src/frontend/client/src/components/permission/UnifiedPermissionControls.tsx index ac75958c47..bb59908221 100644 --- a/src/frontend/client/src/components/permission/UnifiedPermissionControls.tsx +++ b/src/frontend/client/src/components/permission/UnifiedPermissionControls.tsx @@ -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 ( -
+
{title}
@@ -75,7 +76,7 @@ export function AccessModeSelector({ {options.map((option) => ( ))} diff --git a/src/frontend/client/src/components/permission/permissionDialogStyles.ts b/src/frontend/client/src/components/permission/permissionDialogStyles.ts new file mode 100644 index 0000000000..429c313f33 --- /dev/null +++ b/src/frontend/client/src/components/permission/permissionDialogStyles.ts @@ -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]"; diff --git a/src/frontend/client/src/locales/en/translation.json b/src/frontend/client/src/locales/en/translation.json index f7ef802cad..20d7824b52 100644 --- a/src/frontend/client/src/locales/en/translation.json +++ b/src/frontend/client/src/locales/en/translation.json @@ -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", diff --git a/src/frontend/client/src/locales/ja/translation.json b/src/frontend/client/src/locales/ja/translation.json index 5e72ae11bb..8018677f15 100644 --- a/src/frontend/client/src/locales/ja/translation.json +++ b/src/frontend/client/src/locales/ja/translation.json @@ -1790,6 +1790,7 @@ "add_authorization": "権限を追加", "advanced_settings": "詳細設定", "authorization": "権限", + "authorization_empty": "現在のタイプに権限対象はまだありません", "basic_settings": "基本設定", "cancel": "キャンセル", "confirm_create": "作成を確定", diff --git a/src/frontend/client/src/locales/zh-Hans/translation.json b/src/frontend/client/src/locales/zh-Hans/translation.json index ebed23da2c..b50722f3c5 100644 --- a/src/frontend/client/src/locales/zh-Hans/translation.json +++ b/src/frontend/client/src/locales/zh-Hans/translation.json @@ -1796,6 +1796,7 @@ "add_authorization": "新增授权", "advanced_settings": "高级设置", "authorization": "授权", + "authorization_empty": "当前类型下暂无授权对象", "basic_settings": "基础设置", "cancel": "取消", "confirm_create": "确认创建", diff --git a/src/frontend/client/src/pages/Subscription/ChannelSettings/ChannelBusinessSettings.tsx b/src/frontend/client/src/pages/Subscription/ChannelSettings/ChannelBusinessSettings.tsx index 90e0f6beb3..7c633bb40f 100644 --- a/src/frontend/client/src/pages/Subscription/ChannelSettings/ChannelBusinessSettings.tsx +++ b/src/frontend/client/src/pages/Subscription/ChannelSettings/ChannelBusinessSettings.tsx @@ -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 && (
{form.subChannels.map((subChannel) => ( @@ -219,9 +220,12 @@ export function ChannelBusinessSettings({ )} diff --git a/src/frontend/client/src/pages/Subscription/ChannelSettings/ChannelSettingsPage.tsx b/src/frontend/client/src/pages/Subscription/ChannelSettings/ChannelSettingsPage.tsx index 6cc72c6fcb..dbead94088 100644 --- a/src/frontend/client/src/pages/Subscription/ChannelSettings/ChannelSettingsPage.tsx +++ b/src/frontend/client/src/pages/Subscription/ChannelSettings/ChannelSettingsPage.tsx @@ -348,6 +348,7 @@ export function ChannelSettingsPage() {
- + {SUBJECT_TABS.map((tab) => ( {localize(tab.labelKey)} @@ -160,7 +170,7 @@ export function KnowledgeSpaceShareDialog({ return ( <> - + {dialogTitle} @@ -172,7 +182,7 @@ export function KnowledgeSpaceShareDialog({ - + {localize("com_permission.tab_grant")} - {resourceName} @@ -181,16 +191,16 @@ export function KnowledgeSpaceShareDialog({
-
+
{SUBJECT_TABS.map((tab) => (
{grantSubjectType === "department" && ( -