mirror of
https://github.com/dataelement/bisheng.git
synced 2026-08-28 17:14:44 +08:00
refactor(permission): one level menu and one dialog shell across the surfaces
The member list, the create-page draft and the two "新增授权" dialogs had each grown their own version of the same two things — so the level control looked different depending on where you opened it, and the draft picker was a look-alike of the share dialog rather than the same dialog. - PermissionLevelMenu extracts the list's dropdown (levels, separator, destructive 移除) and the draft editor adopts it, dropping its RelationSelect + separate delete icon button. Removal now always lives inside the menu, never as a button beside it; a row with neither permission renders the plain label at the same width, so the column never jitters. - permissionDialogStyles holds the share dialog's chrome verbatim — shell sizing (80vh card / full-screen under 768px), subject tabs, 包含子部门 toggle, footer captions — and the draft picker consumes it instead of re-approximating it. - Empty authorization lists get their own copy (new key, three languages) instead of borrowing the search-result empty text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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,19 +12,13 @@ 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.
|
||||
@@ -596,61 +590,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>
|
||||
);
|
||||
|
||||
@@ -26,7 +26,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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
|
||||
+1
-1
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user