Skip to content

Commit

Permalink
✨ Add role and status filter for team list (#291)
Browse files Browse the repository at this point in the history
  • Loading branch information
foysalit authored Feb 22, 2025
1 parent 3876678 commit c53770e
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 21 deletions.
52 changes: 34 additions & 18 deletions components/config/Member.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
import { ActionButton } from '@/common/buttons'
import { usePermission } from '@/shell/ConfigurationContext'
import { useState } from 'react'
import { ToolsOzoneTeamDefs } from '@atproto/api'
import { PlusIcon } from '@heroicons/react/24/outline'
import { MemberEditor } from 'components/team/MemberEditor'
import { MemberList } from 'components/team/MemberList'
import { useMemberList } from 'components/team/useMemberList'
import { useState } from 'react'

import { ActionButton } from '@/common/buttons'
import { usePermission } from '@/shell/ConfigurationContext'
import { MemberEditor } from '@/team/MemberEditor'
import { MemberList } from '@/team/MemberList'
import RolePicker from '@/team/RolePicker'
import { StatusPicker } from '@/team/StatusPicker'
import { useMemberList } from '@/team/useMemberList'

export function MemberConfig() {
const [editingMember, setEditingMember] =
useState<ToolsOzoneTeamDefs.Member | null>(null)
const [showMemberCreateForm, setShowMemberCreateForm] = useState(false)
const { fetchNextPage, data, hasNextPage, isInitialLoading } = useMemberList()
const {
fetchNextPage,
data,
hasNextPage,
isInitialLoading,
disabled,
setDisabled,
roles,
setRoles,
} = useMemberList()
const hideEditorForm = () => {
if (editingMember) {
setEditingMember(null)
Expand All @@ -23,20 +35,24 @@ export function MemberConfig() {

return (
<div className="pt-4">
<div className="flex flex-row justify-between mb-4">
<div className="flex flex-col md:flex-row justify-between items-center mb-4 gap-2">
<h4 className="font-medium text-gray-700 dark:text-gray-100">
Manage Members
</h4>
{!showMemberCreateForm && !editingMember && canManageTeam && (
<ActionButton
size="sm"
appearance="primary"
onClick={() => setShowMemberCreateForm((current) => !current)}
>
<PlusIcon className="h-3 w-3 mr-1" />
<span className="text-xs">Add New</span>
</ActionButton>
)}
<div className="flex flex-row justify-end items-center gap-2">
{!showMemberCreateForm && !editingMember && canManageTeam && (
<ActionButton
size="sm"
appearance="primary"
onClick={() => setShowMemberCreateForm((current) => !current)}
>
<PlusIcon className="h-3 w-3 mr-1" />
<span className="text-xs">Add</span>
</ActionButton>
)}
<StatusPicker onSelect={setDisabled} selected={disabled} />
<RolePicker size="sm" values={roles} onChange={setRoles} />
</div>
</div>
{(showMemberCreateForm || !!editingMember) && canManageTeam && (
<MemberEditor
Expand Down
110 changes: 110 additions & 0 deletions components/team/RolePicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
'use client'

import { useState, Fragment } from 'react'
import { Combobox, Transition } from '@headlessui/react'
import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/24/solid'
import { getRoleText, MemberRoleNames } from './helpers'

interface RolePickerProps {
values?: string[]
onChange?: (selectedRoles: string[]) => void
size?: 'sm' | 'md' | 'lg'
}

const roles = Object.keys(MemberRoleNames)

const sizeClasses = {
sm: 'px-2 py-0.5 text-xs',
md: 'px-3 py-2 text-sm',
lg: 'px-4 py-3 text-base',
}

export default function RolePicker({
values = [],
onChange,
size = 'md',
}: RolePickerProps) {
const [selectedRoles, setSelectedRoles] = useState<string[]>(values)

return (
<Combobox
value={selectedRoles}
onChange={(roles) => {
setSelectedRoles(roles)
onChange?.(roles)
}}
multiple
>
<div className="relative w-full">
{/* Dropdown Button */}
<Combobox.Button
className={`relative w-full cursor-pointer rounded bg-white dark:bg-slate-800 dark:hover:bg-slate-700 text-left shadow-sm border border-gray-300 dark:border-teal-500 flex items-center justify-between focus:outline-none focus-visible:ring-2 focus-visible:ring-white/75 focus-visible:ring-offset-2 focus-visible:ring-offset-teal-300 sm:text-sm dark:text-gray-100 ${sizeClasses[size]}`}
>
<span className={sizeClasses[size]}>
{selectedRoles.length > 0
? selectedRoles.map(getRoleText).join(', ')
: 'Select Roles'}
</span>
<ChevronUpDownIcon
className="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</Combobox.Button>

{/* Dropdown Menu */}
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Combobox.Options className="absolute z-20 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white dark:bg-slate-700 py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm">
{roles.length === 0 ? (
<div className="p-2 text-gray-500 dark:text-gray-300">
No roles available
</div>
) : (
roles.map((role) => (
<Combobox.Option
key={role}
value={role}
className={({ active }) =>
`relative cursor-pointer select-none py-2 pl-10 pr-4 ${
active
? 'bg-gray-100 dark:bg-slate-600 text-gray-900 dark:text-gray-200'
: 'text-gray-900 dark:text-gray-200'
}`
}
>
{({ selected }) => (
<>
{selected && (
<span
className={`absolute inset-y-0 left-0 flex items-center pl-3 ${
selected ? 'text-indigo-900' : 'text-indigo-600'
}`}
>
<CheckIcon
className="h-5 w-5 dark:text-gray-50"
aria-hidden="true"
/>
</span>
)}
<span
className={`block truncate ${
selected ? 'font-medium' : 'font-normal'
}`}
>
{getRoleText(role)}
</span>
</>
)}
</Combobox.Option>
))
)}
</Combobox.Options>
</Transition>
</div>
</Combobox>
)
}
44 changes: 44 additions & 0 deletions components/team/StatusPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Dropdown } from '@/common/Dropdown'
import { ChevronDownIcon } from '@heroicons/react/24/solid'

export const StatusPicker = ({
selected,
onSelect,
}: {
selected?: boolean
onSelect: (disabled?: boolean) => void
}) => {
const selectedText =
selected === undefined ? 'All' : selected ? 'Disabled' : 'Enabled'
return (
<Dropdown
className="inline-flex justify-center items-center rounded border border-gray-300 dark:border-teal-500 bg-white dark:bg-slate-800 dark:text-gray-100 dark:focus:border-teal-500 dark px-2 py-0.5 text-xs text-gray-700 shadow-sm hover:bg-gray-50 dark:hover:bg-slate-700"
items={[
{
id: 'default',
text: 'All',
onClick: () => onSelect(),
},
{
id: 'enabled',
text: 'Enabled',
onClick: () => onSelect(false),
},
{
id: 'disabled',
text: 'Disabled',
onClick: () => onSelect(true),
},
]}
>
<span className="px-1 py-0.5 inline-flex items-center gap-2">
{selectedText}

<ChevronDownIcon
className="h-3 w-3 text-violet-200 hover:text-violet-100"
aria-hidden="true"
/>
</span>
</Dropdown>
)
}
21 changes: 18 additions & 3 deletions components/team/useMemberList.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,35 @@
import { useLabelerAgent } from '@/shell/ConfigurationContext'
import { ToolsOzoneTeamDefs } from '@atproto/api'
import { useInfiniteQuery, useQuery } from '@tanstack/react-query'
import { useState } from 'react'
import { MemberRoleNames } from './helpers'

export const useMemberList = () => {
const labelerAgent = useLabelerAgent()
return useInfiniteQuery({
queryKey: ['memberList'],
const [roles, setRoles] = useState<string[]>(Object.keys(MemberRoleNames))
const [disabled, setDisabled] = useState<boolean | undefined>(false)

const results = useInfiniteQuery({
queryKey: ['memberList', { disabled, roles }],
queryFn: async ({ pageParam }) => {
const { data } = await labelerAgent.tools.ozone.team.listMembers({
limit: 25,
roles,
disabled,
limit: 50,
cursor: pageParam,
})
return data
},
getNextPageParam: (lastPage) => lastPage.cursor,
})

return {
...results,
disabled,
setDisabled,
roles,
setRoles,
}
}

export const useFullMemberList = () => {
Expand Down
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@/workspace/*": ["components/workspace/*"],
"@/sets/*": ["components/sets/*"],
"@/setting/*": ["components/setting/*"],
"@/team/*": ["components/team/*"],
}
},
"include": [
Expand Down

0 comments on commit c53770e

Please sign in to comment.