ui(admin): fix users list
This commit is contained in:
parent
5063db7d95
commit
4b82ca1042
5 changed files with 126 additions and 92 deletions
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
Loading…
Reference in a new issue