ui(admin): fix users list

This commit is contained in:
Matthew Penner 2023-01-25 11:14:39 -07:00
parent 5063db7d95
commit 4b82ca1042
No known key found for this signature in database
5 changed files with 126 additions and 92 deletions

View file

@ -4,7 +4,7 @@ import { useState } from 'react';
import Checkbox from '@/components/elements/inputs/Checkbox'; import Checkbox from '@/components/elements/inputs/Checkbox';
import { Dropdown } from '@/components/elements/dropdown'; import { Dropdown } from '@/components/elements/dropdown';
import { Dialog } from '@/components/elements/dialog'; import { Dialog } from '@/components/elements/dialog';
import { User } from '@definitions/admin'; import type { User } from '@definitions/admin';
interface Props { interface Props {
user: User; user: User;
@ -12,7 +12,7 @@ interface Props {
onRowChange: (user: User, selected: boolean) => void; onRowChange: (user: User, selected: boolean) => void;
} }
const UserTableRow = ({ user, selected, onRowChange }: Props) => { function UserTableRow({ user, selected, onRowChange }: Props) {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
return ( return (
@ -56,12 +56,14 @@ const UserTableRow = ({ user, selected, onRowChange }: Props) => {
</span> </span>
)} )}
</td> </td>
<td className={'whitespace-nowrap px-6 py-4'}> <td className="whitespace-nowrap px-6 py-4">
<Dropdown> <Dropdown>
<Dropdown.Button className={'px-2'}> <Dropdown.Button className="px-2">
<DotsVerticalIcon /> <DotsVerticalIcon />
</Dropdown.Button> </Dropdown.Button>
<Dropdown.Item icon={<PencilIcon />}>Edit</Dropdown.Item> <Dropdown.Item to={`/admin/users/${user.id}`} icon={<PencilIcon />}>
Edit
</Dropdown.Item>
<Dropdown.Item icon={<SupportIcon />}>Reset Password</Dropdown.Item> <Dropdown.Item icon={<SupportIcon />}>Reset Password</Dropdown.Item>
<Dropdown.Item icon={<LockOpenIcon />} disabled={!user.isUsingTwoFactor}> <Dropdown.Item icon={<LockOpenIcon />} disabled={!user.isUsingTwoFactor}>
Disable 2-FA Disable 2-FA
@ -76,6 +78,6 @@ const UserTableRow = ({ user, selected, onRowChange }: Props) => {
</tr> </tr>
</> </>
); );
}; }
export default UserTableRow; export default UserTableRow;

View file

@ -1,5 +1,6 @@
import { LockOpenIcon, PlusIcon, SupportIcon, TrashIcon } from '@heroicons/react/solid'; import { LockOpenIcon, PlusIcon, SupportIcon, TrashIcon } from '@heroicons/react/solid';
import { Fragment, useEffect, useState } from 'react'; import { Fragment, useEffect, useState } from 'react';
import { NavLink } from 'react-router-dom';
import { useGetUsers } from '@/api/admin/users'; import { useGetUsers } from '@/api/admin/users';
import type { UUID } from '@/api/definitions'; import type { UUID } from '@/api/definitions';
@ -16,7 +17,7 @@ import { Shape } from '@/components/elements/button/types';
const filters = ['id', 'uuid', 'external_id', 'username', 'email'] as const; const filters = ['id', 'uuid', 'external_id', 'username', 'email'] as const;
const UsersContainer = () => { function UsersContainer() {
const [search, setSearch] = useDebouncedState('', 500); const [search, setSearch] = useDebouncedState('', 500);
const [selected, setSelected] = useState<UUID[]>([]); const [selected, setSelected] = useState<UUID[]>([]);
const { data: users } = useGetUsers( const { data: users } = useGetUsers(
@ -42,13 +43,16 @@ const UsersContainer = () => {
return ( return (
<div> <div>
<div className={'mb-4 flex justify-end'}> <div className="mb-4 flex justify-end">
<Button className={'shadow focus:ring-offset-2 focus:ring-offset-neutral-800'}> <NavLink to="/admin/users/new">
Add User <PlusIcon className={'ml-2 h-5 w-5'} /> <Button className="shadow focus:ring-offset-2 focus:ring-offset-neutral-800">
</Button> Add User <PlusIcon className="ml-2 h-5 w-5" />
</Button>
</NavLink>
</div> </div>
<div className={'relative flex items-center rounded-t bg-neutral-700 px-4 py-2'}>
<div className={'mr-6'}> <div className="relative flex items-center rounded-t bg-neutral-700 px-4 py-2">
<div className="mr-6">
<Checkbox <Checkbox
checked={selectAllChecked} checked={selectAllChecked}
disabled={!users?.items.length} disabled={!users?.items.length}
@ -56,22 +60,18 @@ const UsersContainer = () => {
onChange={onSelectAll} onChange={onSelectAll}
/> />
</div> </div>
<div className={'flex-1'}> <div className="flex-1">
<InputField <InputField
type={'text'} type="text"
name={'filter'} name="filter"
placeholder={'Begin typing to filter...'} placeholder="Begin typing to filter..."
className={'w-56 focus:w-96'} className="w-56 focus:w-96"
onChange={e => setSearch(e.currentTarget.value)} onChange={e => setSearch(e.currentTarget.value)}
/> />
</div> </div>
<Transition.Fade as={Fragment} show={selected.length > 0} duration={'duration-75'}> <Transition.Fade as={Fragment} show={selected.length > 0} duration="duration-75">
<div <div className="absolute top-0 left-0 flex h-full w-full items-center justify-end space-x-4 rounded-t bg-neutral-700 px-4">
className={ <div className="flex-1">
'absolute top-0 left-0 flex h-full w-full items-center justify-end space-x-4 rounded-t bg-neutral-700 px-4'
}
>
<div className={'flex-1'}>
<Checkbox <Checkbox
checked={selectAllChecked} checked={selectAllChecked}
indeterminate={selected.length !== users?.items.length} indeterminate={selected.length !== users?.items.length}
@ -79,26 +79,26 @@ const UsersContainer = () => {
/> />
</div> </div>
<Button.Text shape={Shape.IconSquare}> <Button.Text shape={Shape.IconSquare}>
<SupportIcon className={'h-4 w-4'} /> <SupportIcon className="h-4 w-4" />
</Button.Text> </Button.Text>
<Button.Text shape={Shape.IconSquare}> <Button.Text shape={Shape.IconSquare}>
<LockOpenIcon className={'h-4 w-4'} /> <LockOpenIcon className="h-4 w-4" />
</Button.Text> </Button.Text>
<Button.Text shape={Shape.IconSquare}> <Button.Text shape={Shape.IconSquare}>
<TrashIcon className={'h-4 w-4'} /> <TrashIcon className="h-4 w-4" />
</Button.Text> </Button.Text>
</div> </div>
</Transition.Fade> </Transition.Fade>
</div> </div>
<table className={'min-w-full rounded bg-neutral-700'}> <table className="min-w-full rounded bg-neutral-700">
<thead className={'bg-neutral-900'}> <thead className="bg-neutral-900">
<tr> <tr>
<th scope={'col'} className={'w-8'} /> <th scope="col" className="w-8" />
<th scope={'col'} className={'w-full px-6 py-2 text-left'}> <th scope="col" className="w-full px-6 py-2 text-left">
Email Email
</th> </th>
<th scope={'col'} /> <th scope="col" />
<th scope={'col'} /> <th scope="col" />
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -111,10 +111,10 @@ const UsersContainer = () => {
/> />
))} ))}
</tbody> </tbody>
{users && <TFootPaginated span={4} pagination={users.pagination} />} {users ? <TFootPaginated span={4} pagination={users.pagination} /> : null}
</table> </table>
</div> </div>
); );
}; }
export default UsersContainer; export default UsersContainer;

View file

@ -1,27 +1,29 @@
import { ElementType, forwardRef, useMemo } from 'react';
import * as React from 'react';
import { Menu, Transition } from '@headlessui/react'; import { Menu, Transition } from '@headlessui/react';
import styles from './style.module.css';
import classNames from 'classnames'; import classNames from 'classnames';
import DropdownItem from '@/components/elements/dropdown/DropdownItem'; import type { ElementType, ReactNode } from 'react';
import DropdownButton from '@/components/elements/dropdown/DropdownButton'; import { Children as ReactChildren } from 'react';
import { forwardRef, useMemo } from 'react';
import { DropdownButton } from '@/components/elements/dropdown/DropdownButton';
import { DropdownItem } from '@/components/elements/dropdown/DropdownItem';
import styles from './style.module.css';
interface Props { interface Props {
as?: ElementType; as?: ElementType;
children: React.ReactNode; children: ReactNode;
} }
const DropdownGap = ({ invisible }: { invisible?: boolean }) => ( const DropdownGap = ({ invisible }: { invisible?: boolean }) => (
<div className={classNames('m-2 border', { 'border-neutral-700': !invisible, 'border-transparent': invisible })} /> <div className={classNames('m-2 border', { 'border-neutral-700': !invisible, 'border-transparent': invisible })} />
); );
type TypedChild = (React.ReactChild | React.ReactFragment | React.ReactPortal) & { type TypedChild = ReactNode & {
type?: JSX.Element; type?: JSX.Element;
}; };
const Dropdown = forwardRef<typeof Menu, Props>(({ as, children }, ref) => { const Dropdown = forwardRef<typeof Menu, Props>(({ as, children }, ref) => {
const [Button, items] = useMemo(() => { const [Button, items] = useMemo(() => {
const list = React.Children.toArray(children) as unknown as TypedChild[]; const list = ReactChildren.toArray(children) as unknown as TypedChild[];
return [ return [
list.filter(child => child.type === DropdownButton), list.filter(child => child.type === DropdownButton),
@ -34,18 +36,18 @@ const Dropdown = forwardRef<typeof Menu, Props>(({ as, children }, ref) => {
} }
return ( return (
<Menu as={as || 'div'} className={styles.menu} ref={ref}> <Menu as={as ?? 'div'} className={styles.menu} ref={ref}>
{Button} {Button}
<Transition <Transition
enter={'transition duration-100 ease-out'} enter="transition duration-100 ease-out"
enterFrom={'transition scale-95 opacity-0'} enterFrom="transition scale-95 opacity-0"
enterTo={'transform scale-100 opacity-100'} enterTo="transform scale-100 opacity-100"
leave={'transition duration-75 ease-out'} leave="transition duration-75 ease-out"
leaveFrom={'transform scale-100 opacity-100'} leaveFrom="transform scale-100 opacity-100"
leaveTo={'transform scale-95 opacity-0'} leaveTo="transform scale-95 opacity-0"
> >
<Menu.Items className={classNames(styles.items_container, 'w-56')}> <Menu.Items className={classNames(styles.items_container, 'w-56')}>
<div className={'px-1 py-1'}>{items}</div> <div className="px-1 py-1">{items}</div>
</Menu.Items> </Menu.Items>
</Transition> </Transition>
</Menu> </Menu>

View file

@ -1,24 +1,29 @@
import classNames from 'classnames';
import styles from '@/components/elements/dropdown/style.module.css';
import { ChevronDownIcon } from '@heroicons/react/solid';
import { Menu } from '@headlessui/react'; import { Menu } from '@headlessui/react';
import * as React from 'react'; import { ChevronDownIcon } from '@heroicons/react/solid';
import classNames from 'classnames';
import type { ReactNode } from 'react';
import styles from './style.module.css';
interface Props { interface Props {
className?: string; className?: string;
animate?: boolean; animate?: boolean;
children: React.ReactNode; children: ReactNode;
} }
export default ({ className, animate = true, children }: Props) => ( function DropdownButton({ className, animate = true, children }: Props) {
<Menu.Button className={classNames(styles.button, className || 'px-4')}> return (
{typeof children === 'string' ? ( <Menu.Button className={classNames(styles.button, className ?? 'px-4')}>
<> {typeof children === 'string' ? (
<span className={'mr-2'}>{children}</span> <>
<ChevronDownIcon aria-hidden={'true'} data-animated={animate.toString()} /> <span className="mr-2">{children}</span>
</> <ChevronDownIcon aria-hidden="true" data-animated={animate.toString()} />
) : ( </>
children ) : (
)} children
</Menu.Button> )}
); </Menu.Button>
);
}
export { DropdownButton };

View file

@ -1,43 +1,68 @@
import { forwardRef } from 'react';
import * as React from 'react';
import { Menu } from '@headlessui/react'; import { Menu } from '@headlessui/react';
import styles from './style.module.css';
import classNames from 'classnames'; import classNames from 'classnames';
import type { MouseEvent, ReactNode, Ref } from 'react';
import { forwardRef } from 'react';
import type { NavLinkProps } from 'react-router-dom';
import { NavLink } from 'react-router-dom';
import styles from './style.module.css';
interface Props { interface Props {
children: React.ReactNode | ((opts: { active: boolean; disabled: boolean }) => JSX.Element); children: ReactNode | ((opts: { active: boolean; disabled: boolean }) => JSX.Element);
danger?: boolean; danger?: boolean;
disabled?: boolean; disabled?: boolean;
className?: string; className?: string;
icon?: JSX.Element; icon?: JSX.Element;
onClick?: (e: React.MouseEvent) => void; onClick?: (e: MouseEvent) => void;
} }
const DropdownItem = forwardRef<HTMLAnchorElement, Props>( const DropdownItem = forwardRef<HTMLAnchorElement | HTMLButtonElement, Props & Partial<Omit<NavLinkProps, 'children'>>>(
({ disabled, danger, className, onClick, children, icon: IconComponent }, ref) => { ({ disabled, danger, className, onClick, children, icon: IconComponent, ...props }, ref) => {
return ( return (
<Menu.Item disabled={disabled}> <Menu.Item disabled={disabled}>
{({ disabled, active }) => ( {({ disabled, active }) => (
<a <>
ref={ref} {'to' in props && props.to !== undefined ? (
href={'#'} <NavLink
className={classNames( {...props}
styles.menu_item, to={props.to}
{ ref={ref as unknown as Ref<HTMLAnchorElement>}
[styles.danger]: danger, className={classNames(
[styles.disabled]: disabled, styles.menu_item,
}, {
className, [styles.danger]: danger,
[styles.disabled]: disabled,
},
className,
)}
onClick={onClick}
>
{IconComponent}
{typeof children === 'function' ? children({ disabled, active }) : children}
</NavLink>
) : (
<button
type="button"
ref={ref as unknown as Ref<HTMLButtonElement>}
className={classNames(
styles.menu_item,
{
[styles.danger]: danger,
[styles.disabled]: disabled,
},
className,
)}
onClick={onClick}
>
{IconComponent}
{typeof children === 'function' ? children({ disabled, active }) : children}
</button>
)} )}
onClick={onClick} </>
>
{IconComponent}
{typeof children === 'function' ? children({ disabled, active }) : children}
</a>
)} )}
</Menu.Item> </Menu.Item>
); );
}, },
); );
export default DropdownItem; export { DropdownItem };