mirror of
https://github.com/dataelement/bisheng.git
synced 2026-08-28 17:14:44 +08:00
Feat/2.6.0 0811 (#2293)
## What 简要描述做了什么改动。 ## Why 为什么需要这个改动? ## How 实现方式、设计决策(如有)。 ## Test - [ ] 本地测试通过 - [ ] 114 测试服务器验证通过 ## Related - Issue/ticket:
This commit is contained in:
@@ -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": "确认创建",
|
||||
|
||||
+8
-4
@@ -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)}
|
||||
/>
|
||||
|
||||
+2
-2
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user