From 339e4c8a1fe4138a5510ee349c4c6d4c7eeddfbf Mon Sep 17 00:00:00 2001 From: twwu Date: Mon, 27 Apr 2026 16:14:39 +0800 Subject: [PATCH] feat: implement AddRuleTargetsModal for role and member assignment in access rules management --- .../add-rule-targets-modal/index.tsx | 361 ++++++++++++++++++ .../access-rules-page/index.tsx | 82 ++-- 2 files changed, 417 insertions(+), 26 deletions(-) create mode 100644 web/app/components/header/account-setting/access-rules-page/add-rule-targets-modal/index.tsx diff --git a/web/app/components/header/account-setting/access-rules-page/add-rule-targets-modal/index.tsx b/web/app/components/header/account-setting/access-rules-page/add-rule-targets-modal/index.tsx new file mode 100644 index 0000000000..357bd698b6 --- /dev/null +++ b/web/app/components/header/account-setting/access-rules-page/add-rule-targets-modal/index.tsx @@ -0,0 +1,361 @@ +'use client' + +import type { Member } from '@/models/common' +import { Avatar } from '@langgenius/dify-ui/avatar' +import { Button } from '@langgenius/dify-ui/button' +import { cn } from '@langgenius/dify-ui/cn' +import { + Dialog, + DialogCloseButton, + DialogContent, + DialogDescription, + DialogTitle, +} from '@langgenius/dify-ui/dialog' +import { ScrollArea } from '@langgenius/dify-ui/scroll-area' +import { useCallback, useMemo, useState } from 'react' +import Checkbox from '@/app/components/base/checkbox' +import Input from '@/app/components/base/input' +import { useMembers } from '@/service/use-common' + +export type AssignableRoleOption = { + id: string + name: string + description?: string +} + +export type AssignableMemberOption = { + id: string + name: string + email: string + avatarUrl?: string | null +} + +type TabKey = 'roles' | 'members' + +type AddRuleTargetsModalBaseProps = { + ruleName?: string + initialRoleIds?: string[] + initialMemberIds?: string[] + onClose: () => void + onSubmit: (selection: { roleIds: string[], memberIds: string[] }) => void +} + +export type AddRuleTargetsModalProps = AddRuleTargetsModalBaseProps & { + open: boolean +} + +const TABS: Array<{ key: TabKey, label: string }> = [ + { key: 'roles', label: 'ROLES' }, + { key: 'members', label: 'MEMBERS' }, +] + +// TODO: replace with roles fetched from the permissions API once available. +const MOCK_ROLE_OPTIONS: AssignableRoleOption[] = [ + { id: 'admin', name: 'Admin', description: 'Full workspace management' }, + { id: 'editor', name: 'Editor', description: 'Create and edit resources' }, + { id: 'member', name: 'Member', description: 'Basic access' }, + { id: 'auditor', name: 'Auditor', description: 'View logs and audit trails' }, + { id: 'tester', name: 'Tester', description: 'Test in sandbox' }, +] + +const toMemberOption = (member: Member): AssignableMemberOption => ({ + id: member.id, + name: member.name, + email: member.email, + avatarUrl: member.avatar_url ?? member.avatar ?? null, +}) + +const AddRuleTargetsModalBody = ({ + ruleName, + initialRoleIds = [], + initialMemberIds = [], + onClose, + onSubmit, +}: AddRuleTargetsModalBaseProps) => { + const { data: membersData, isLoading: membersLoading } = useMembers() + + const roles = MOCK_ROLE_OPTIONS + + const members = useMemo(() => { + const accounts = membersData?.accounts ?? [] + return accounts + .filter(account => account.status !== 'banned' && account.status !== 'closed') + .map(toMemberOption) + }, [membersData]) + const [activeTab, setActiveTab] = useState('roles') + const [keyword, setKeyword] = useState('') + const [selectedRoleIds, setSelectedRoleIds] = useState(initialRoleIds) + const [selectedMemberIds, setSelectedMemberIds] = useState(initialMemberIds) + + const trimmed = keyword.trim().toLowerCase() + + const filteredRoles = useMemo(() => { + if (!trimmed) + return roles + return roles.filter( + role => + role.name.toLowerCase().includes(trimmed) + || role.description?.toLowerCase().includes(trimmed), + ) + }, [roles, trimmed]) + + const filteredMembers = useMemo(() => { + if (!trimmed) + return members + return members.filter( + member => + member.name.toLowerCase().includes(trimmed) + || member.email.toLowerCase().includes(trimmed), + ) + }, [members, trimmed]) + + const handleSwitchTab = useCallback((tab: TabKey) => { + setActiveTab(tab) + setKeyword('') + }, []) + + const toggleRole = useCallback((id: string) => { + setSelectedRoleIds(prev => + prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id], + ) + }, []) + + const toggleMember = useCallback((id: string) => { + setSelectedMemberIds(prev => + prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id], + ) + }, []) + + const handleConfirm = useCallback(() => { + onSubmit({ roleIds: selectedRoleIds, memberIds: selectedMemberIds }) + onClose() + }, [onClose, onSubmit, selectedMemberIds, selectedRoleIds]) + + const description = ruleName + ? `Select roles or members to grant the "${ruleName}" access level by default.` + : 'Select roles or members to grant this access level by default.' + + const summary = (() => { + const parts: string[] = [] + parts.push(`${selectedRoleIds.length} ${selectedRoleIds.length === 1 ? 'role' : 'roles'}`) + parts.push(`${selectedMemberIds.length} ${selectedMemberIds.length === 1 ? 'member' : 'members'} selected`) + return parts.join(', ') + })() + + return ( + +
+ +
+ + Add Roles or Members + + + {description} + +
+
+ +
+
+ {TABS.map((tab) => { + const active = activeTab === tab.key + return ( + + ) + })} +
+
+ +
+ setKeyword(e.target.value)} + onClear={() => setKeyword('')} + placeholder={ + activeTab === 'roles' ? 'Search roles...' : 'Search members...' + } + /> +
+ + + {activeTab === 'roles' && ( + filteredRoles.length === 0 + ? ( +
+ No matching roles +
+ ) + : ( +
    + {filteredRoles.map((role) => { + const checked = selectedRoleIds.includes(role.id) + const handleToggle = () => toggleRole(role.id) + return ( +
  • +
    { + if (e.key === ' ' || e.key === 'Enter') { + e.preventDefault() + handleToggle() + } + }} + > + +
    +
    + {role.name} +
    + {role.description && ( +
    + {role.description} +
    + )} +
    +
    +
  • + ) + })} +
+ ) + )} + {activeTab === 'members' && ( + membersLoading + ? ( +
+ Loading members... +
+ ) + : filteredMembers.length === 0 + ? ( +
+ No matching members +
+ ) + : ( +
    + {filteredMembers.map((member) => { + const checked = selectedMemberIds.includes(member.id) + const handleToggle = () => toggleMember(member.id) + return ( +
  • +
    { + if (e.key === ' ' || e.key === 'Enter') { + e.preventDefault() + handleToggle() + } + }} + > + + +
    +
    + {member.name} +
    +
    + {member.email} +
    +
    +
    +
  • + ) + })} +
+ ) + )} +
+ +
+
+ {summary} +
+
+ + +
+
+
+ ) +} + +const AddRuleTargetsModal = ({ + open, + ruleName, + initialRoleIds, + initialMemberIds, + onClose, + onSubmit, +}: AddRuleTargetsModalProps) => { + return ( + { + if (!nextOpen) + onClose() + }} + > + + + ) +} + +export default AddRuleTargetsModal diff --git a/web/app/components/header/account-setting/access-rules-page/index.tsx b/web/app/components/header/account-setting/access-rules-page/index.tsx index 786f9f2a70..67a3dd05cd 100644 --- a/web/app/components/header/account-setting/access-rules-page/index.tsx +++ b/web/app/components/header/account-setting/access-rules-page/index.tsx @@ -1,8 +1,9 @@ 'use client' import type { AccessRule } from './access-rule-row' -import { useCallback } from 'react' +import { useCallback, useState } from 'react' import AccessRuleSection from './access-rule-section' +import AddRuleTargetsModal from './add-rule-targets-modal' // todo: replace with API data when backend is ready const APP_ACCESS_RULES: AccessRule[] = [ @@ -96,35 +97,64 @@ const KNOWLEDGE_BASE_ACCESS_RULES: AccessRule[] = [ ] const AccessRulesPage = () => { + const [addingRule, setAddingRule] = useState(null) + + const handleAddRole = useCallback((rule: AccessRule) => { + setAddingRule(rule) + }, []) + + const closeAddModal = useCallback(() => { + setAddingRule(null) + }, []) + + const handleAddSubmit = useCallback( + (_selection: { roleIds: string[], memberIds: string[] }) => { + // TODO: wire up to API when backend is ready. + }, + [], + ) + const noop = useCallback(() => { - // TODO: wire up to API when backend is ready + // TODO: wire up to API when backend is ready. }, []) return ( -
- - -
+ <> +
+ + +
+ {addingRule && ( + role.id)} + initialMemberIds={[]} + onClose={closeAddModal} + onSubmit={handleAddSubmit} + /> + )} + ) }