Merge branch 'feat/logged-in-user-data' into 'main'
Feat/logged in user data See merge request stackspin/dashboard!21
This commit is contained in:
commit
ae61e7bf94
15 changed files with 351 additions and 141 deletions
|
@ -1,10 +1,11 @@
|
|||
import React, { Fragment } from 'react';
|
||||
import React, { Fragment, useState } from 'react';
|
||||
import { Disclosure, Menu, Transition } from '@headlessui/react';
|
||||
import { MenuIcon, XIcon } from '@heroicons/react/outline';
|
||||
import { useAuth } from 'src/services/auth';
|
||||
import Gravatar from 'react-gravatar';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import clsx from 'clsx';
|
||||
import { CurrentUserModal } from './components/CurrentUserModal';
|
||||
|
||||
const navigation = [
|
||||
{ name: 'Dashboard', to: '/dashboard' },
|
||||
|
@ -19,130 +20,138 @@ function classNames(...classes: any[]) {
|
|||
interface HeaderProps {}
|
||||
|
||||
const Header: React.FC<HeaderProps> = () => {
|
||||
const { logOut } = useAuth();
|
||||
const [currentUserModal, setCurrentUserModal] = useState(false);
|
||||
const { logOut, currentUser } = useAuth();
|
||||
|
||||
const { pathname } = useLocation();
|
||||
|
||||
return (
|
||||
<Disclosure as="nav" className="bg-white shadow relative z-10">
|
||||
{({ open }) => (
|
||||
<div className="relative">
|
||||
<div className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8">
|
||||
<div className="relative flex justify-between h-16">
|
||||
<div className="absolute inset-y-0 left-0 flex items-center sm:hidden">
|
||||
{/* Mobile menu button */}
|
||||
<Disclosure.Button className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary-500">
|
||||
<span className="sr-only">Open main menu</span>
|
||||
{open ? (
|
||||
<XIcon className="block h-6 w-6" aria-hidden="true" />
|
||||
) : (
|
||||
<MenuIcon className="block h-6 w-6" aria-hidden="true" />
|
||||
)}
|
||||
</Disclosure.Button>
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-center sm:items-stretch sm:justify-start">
|
||||
<div className="flex-shrink-0 flex items-center">
|
||||
<img className="block lg:hidden" src="/assets/logo-small.svg" alt="Stackspin" />
|
||||
<img className="hidden lg:block" src="/assets/logo.svg" alt="Stackspin" />
|
||||
</div>
|
||||
<div className="hidden sm:ml-6 sm:flex sm:space-x-8">
|
||||
{/* Current: "border-primary-500 text-gray-900", Default: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700" */}
|
||||
{navigation.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
to={item.to}
|
||||
className={clsx(
|
||||
'border-primary-50 text-gray-900 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium',
|
||||
{
|
||||
'border-primary-500 text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 text-sm font-medium':
|
||||
pathname.includes(item.to),
|
||||
},
|
||||
)}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0">
|
||||
{/* Profile dropdown */}
|
||||
<Menu as="div" className="ml-3 relative">
|
||||
<div>
|
||||
<Menu.Button className="bg-white rounded-full flex text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary">
|
||||
<span className="sr-only">Open user menu</span>
|
||||
<span className="inline-flex items-center justify-center h-8 w-8 rounded-full bg-gray-500 overflow-hidden">
|
||||
<Gravatar email="" size={32} rating="pg" protocol="https://" />
|
||||
</span>
|
||||
</Menu.Button>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<a
|
||||
onClick={() => logOut()}
|
||||
className={classNames(
|
||||
active ? 'bg-gray-100 cursor-pointer' : '',
|
||||
'block px-4 py-2 text-sm text-gray-700 cursor-pointer',
|
||||
)}
|
||||
>
|
||||
Configure profile
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<a
|
||||
onClick={() => logOut()}
|
||||
className={classNames(
|
||||
active ? 'bg-gray-100 cursor-pointer' : '',
|
||||
'block px-4 py-2 text-sm text-gray-700 cursor-pointer',
|
||||
)}
|
||||
>
|
||||
Sign out
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
const currentUserModalOpen = () => {
|
||||
setCurrentUserModal(true);
|
||||
};
|
||||
const currentUserModalClose = () => setCurrentUserModal(false);
|
||||
|
||||
<Disclosure.Panel className="sm:hidden">
|
||||
<div className="pt-2 pb-4 space-y-1">
|
||||
{navigation.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
to={item.to}
|
||||
// getProps={({ isCurrent }) => {
|
||||
// // the object returned here is passed to the
|
||||
// // anchor element's props
|
||||
// return {
|
||||
// className: isCurrent
|
||||
// ? 'bg-primary-50 border-primary-400 text-primary-700 block pl-3 pr-4 py-2 border-l-4 text-base font-medium'
|
||||
// : 'border-transparent text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700 block pl-3 pr-4 py-2 border-l-4 text-base font-medium',
|
||||
// 'aria-current': isCurrent ? 'page' : undefined,
|
||||
// };
|
||||
// }}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
return (
|
||||
<>
|
||||
<Disclosure as="nav" className="bg-white shadow relative z-10">
|
||||
{({ open }) => (
|
||||
<div className="relative">
|
||||
<div className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8">
|
||||
<div className="relative flex justify-between h-16">
|
||||
<div className="absolute inset-y-0 left-0 flex items-center sm:hidden">
|
||||
{/* Mobile menu button */}
|
||||
<Disclosure.Button className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary-500">
|
||||
<span className="sr-only">Open main menu</span>
|
||||
{open ? (
|
||||
<XIcon className="block h-6 w-6" aria-hidden="true" />
|
||||
) : (
|
||||
<MenuIcon className="block h-6 w-6" aria-hidden="true" />
|
||||
)}
|
||||
</Disclosure.Button>
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-center sm:items-stretch sm:justify-start">
|
||||
<Link to="/" className="flex-shrink-0 flex items-center">
|
||||
<img className="block lg:hidden" src="/assets/logo-small.svg" alt="Stackspin" />
|
||||
<img className="hidden lg:block" src="/assets/logo.svg" alt="Stackspin" />
|
||||
</Link>
|
||||
<div className="hidden sm:ml-6 sm:flex sm:space-x-8">
|
||||
{/* Current: "border-primary-500 text-gray-900", Default: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700" */}
|
||||
{navigation.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
to={item.to}
|
||||
className={clsx(
|
||||
'border-primary-50 text-gray-900 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium',
|
||||
{
|
||||
'border-primary-500 text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 text-sm font-medium':
|
||||
pathname.includes(item.to),
|
||||
},
|
||||
)}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0">
|
||||
{/* Profile dropdown */}
|
||||
<Menu as="div" className="ml-3 relative">
|
||||
<div>
|
||||
<Menu.Button className="bg-white rounded-full flex text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary">
|
||||
<span className="sr-only">Open user menu</span>
|
||||
<span className="inline-flex items-center justify-center h-8 w-8 rounded-full bg-gray-500 overflow-hidden">
|
||||
<Gravatar email={currentUser.email || ''} size={32} rating="pg" protocol="https://" />
|
||||
</span>
|
||||
</Menu.Button>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<a
|
||||
onClick={() => currentUserModalOpen()}
|
||||
className={classNames(
|
||||
active ? 'bg-gray-100 cursor-pointer' : '',
|
||||
'block px-4 py-2 text-sm text-gray-700 cursor-pointer',
|
||||
)}
|
||||
>
|
||||
Configure profile
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<a
|
||||
onClick={() => logOut()}
|
||||
className={classNames(
|
||||
active ? 'bg-gray-100 cursor-pointer' : '',
|
||||
'block px-4 py-2 text-sm text-gray-700 cursor-pointer',
|
||||
)}
|
||||
>
|
||||
Sign out
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</div>
|
||||
)}
|
||||
</Disclosure>
|
||||
|
||||
<Disclosure.Panel className="sm:hidden">
|
||||
<div className="pt-2 pb-4 space-y-1">
|
||||
{navigation.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
to={item.to}
|
||||
className={clsx(
|
||||
'border-transparent text-gray-500 hover:text-gray-700 block pl-3 pr-4 py-2 border-l-4 text-base font-medium',
|
||||
{
|
||||
'bg-primary-50 border-primary-400 text-primary-700 block pl-3 pr-4 py-2': pathname.includes(
|
||||
item.to,
|
||||
),
|
||||
},
|
||||
)}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</div>
|
||||
)}
|
||||
</Disclosure>
|
||||
|
||||
<CurrentUserModal open={currentUserModal} onClose={currentUserModalClose} user={currentUser} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,152 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Modal, Banner } from 'src/components';
|
||||
import { Input } from 'src/components/Form';
|
||||
import { useUsers } from 'src/services/users';
|
||||
import { CurrentUser } from 'src/services/auth';
|
||||
import { appAccessList } from './consts';
|
||||
import { UserModalProps } from './types';
|
||||
|
||||
export const CurrentUserModal = ({ open, onClose, user }: UserModalProps) => {
|
||||
const { editUserById, userModalLoading } = useUsers();
|
||||
|
||||
const { control, reset, handleSubmit } = useForm<CurrentUser>({
|
||||
defaultValues: {
|
||||
name: null,
|
||||
email: null,
|
||||
id: null,
|
||||
preferredUsername: null,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
reset(user);
|
||||
}
|
||||
|
||||
return () => {
|
||||
reset({ name: null, email: null, id: null });
|
||||
};
|
||||
}, [user, reset]);
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
if (user) {
|
||||
await handleSubmit((data) => editUserById(data))();
|
||||
}
|
||||
|
||||
onClose();
|
||||
} catch (e: any) {
|
||||
// Continue
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: any) => {
|
||||
if (e.key === 'Enter' || e.key === 'NumpadEnter') {
|
||||
handleSave();
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal onClose={handleClose} open={open} onSave={handleSave} isLoading={userModalLoading} useCancelButton>
|
||||
<div className="bg-white px-4">
|
||||
<div className="space-y-4 divide-y divide-gray-200">
|
||||
<div>
|
||||
<div>
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">Profile</h3>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
|
||||
<div className="sm:col-span-3">
|
||||
<Input control={control} name="name" label="Name" onKeyPress={handleKeyPress} />
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-3">
|
||||
<Input control={control} name="email" label="Email" type="email" onKeyPress={handleKeyPress} />
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-6">
|
||||
<Banner title="Editing user status, roles and app access coming soon." titleSm="Comming soon!" />
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-3 opacity-40 cursor-default pointer-events-none select-none">
|
||||
{/* <Select control={control} name="status" label="Status" options={['Active', 'Inactive']} /> */}
|
||||
<label htmlFor="status" className="block text-sm font-medium text-gray-700">
|
||||
Status
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<select
|
||||
id="status"
|
||||
name="status"
|
||||
className="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||
>
|
||||
<option>Active</option>
|
||||
<option>Inactive</option>
|
||||
<option>Banned</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-3 opacity-40 cursor-default pointer-events-none select-none">
|
||||
<label htmlFor="status" className="block text-sm font-medium text-gray-700">
|
||||
Role
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<select
|
||||
id="status"
|
||||
name="status"
|
||||
className="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||
>
|
||||
<option>User</option>
|
||||
<option>Admin</option>
|
||||
<option>Super Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="opacity-40 cursor-default pointer-events-none select-none">
|
||||
<div className="mt-4">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">App Access</h3>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flow-root mt-6">
|
||||
<ul className="-my-5 divide-y divide-gray-200 ">
|
||||
{appAccessList.map((app: any) => {
|
||||
return (
|
||||
<li className="py-4" key={app.name}>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex-shrink-0 flex-1 flex items-center">
|
||||
<img className="h-10 w-10 rounded-md overflow-hidden" src={app.image} alt={app.name} />
|
||||
<h3 className="ml-4 text-md leading-6 font-medium text-gray-900">{app.name}</h3>
|
||||
</div>
|
||||
<div>
|
||||
<select
|
||||
id={app.name}
|
||||
name={app.name}
|
||||
className="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||
>
|
||||
<option>User</option>
|
||||
<option>Admin</option>
|
||||
<option>Super Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
18
src/components/Header/components/CurrentUserModal/consts.ts
Normal file
18
src/components/Header/components/CurrentUserModal/consts.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
export const appAccessList = [
|
||||
{
|
||||
image: '/assets/wekan.svg',
|
||||
name: 'Wekan',
|
||||
},
|
||||
{
|
||||
image: '/assets/wordpress.svg',
|
||||
name: 'Wordpress',
|
||||
},
|
||||
{
|
||||
image: '/assets/nextcloud.svg',
|
||||
name: 'NextCloud',
|
||||
},
|
||||
{
|
||||
image: '/assets/zulip.svg',
|
||||
name: 'Zulip',
|
||||
},
|
||||
];
|
|
@ -0,0 +1 @@
|
|||
export { CurrentUserModal } from './CurrentUserModal';
|
|
@ -0,0 +1,7 @@
|
|||
import { CurrentUser } from 'src/services/auth';
|
||||
|
||||
export type UserModalProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
user: CurrentUser;
|
||||
};
|
1
src/components/Header/components/index.ts
Normal file
1
src/components/Header/components/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { CurrentUserModal } from './CurrentUserModal';
|
|
@ -40,14 +40,12 @@ axios.interceptors.response.use(
|
|||
);
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<Provider store={store}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
</React.StrictMode>,
|
||||
<Provider store={store}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</PersistGate>
|
||||
</Provider>,
|
||||
document.getElementById('root'),
|
||||
);
|
||||
|
|
|
@ -66,7 +66,7 @@ export const Users: React.FC = () => {
|
|||
const { row } = props;
|
||||
|
||||
return (
|
||||
<div className="text-right opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<div className="text-right lg:opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={() => configureModalOpen(row.original.id)}
|
||||
type="button"
|
||||
|
@ -90,7 +90,7 @@ export const Users: React.FC = () => {
|
|||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="max-w-7xl mx-auto py-4 sm:px-6 lg:px-8 h-full flex-grow">
|
||||
<div className="max-w-7xl mx-auto py-4 px-3 sm:px-6 lg:px-8 h-full flex-grow">
|
||||
<div className="pb-5 mt-6 border-b border-gray-200 sm:flex sm:items-center sm:justify-between">
|
||||
<h1 className="text-3xl leading-6 font-bold text-gray-900">Users</h1>
|
||||
<div className="mt-3 sm:mt-0 sm:ml-4">
|
||||
|
@ -144,9 +144,9 @@ export const Users: React.FC = () => {
|
|||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div className="-my-2 sm:-mx-6 lg:-mx-8">
|
||||
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
||||
<div className="shadow border-b border-gray-200 sm:rounded-lg">
|
||||
<div className="shadow border-b border-gray-200 sm:rounded-lg overflow-hidden">
|
||||
<Table data={filterSearch as any} columns={columns} getSelectedRowIds={selectedRows} selectable />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -5,11 +5,13 @@ import { Modal, Banner } from 'src/components';
|
|||
import { Input } from 'src/components/Form';
|
||||
import { useUsers } from 'src/services/users';
|
||||
import { CurrentUserState } from 'src/services/users/redux';
|
||||
import { useAuth } from 'src/services/auth';
|
||||
import { appAccessList } from './consts';
|
||||
import { UserModalProps } from './types';
|
||||
|
||||
export const UserModal = ({ open, onClose, userId }: UserModalProps) => {
|
||||
const { user, loadUser, editUserById, createNewUser, userModalLoading, deleteUserById } = useUsers();
|
||||
const { currentUser } = useAuth();
|
||||
|
||||
const { control, reset, handleSubmit } = useForm<CurrentUserState>({
|
||||
defaultValues: {
|
||||
|
@ -78,11 +80,12 @@ export const UserModal = ({ open, onClose, userId }: UserModalProps) => {
|
|||
onSave={handleSave}
|
||||
isLoading={userModalLoading}
|
||||
leftActions={
|
||||
userId && (
|
||||
userId &&
|
||||
user.email !== currentUser.email && (
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
type="button"
|
||||
className="inline-flex items-center px-4 py-2 text-sm font-medium rounded-md text-red-700 bg-red-50 hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
|
||||
className="mb-4 sm:mb-0 inline-flex items-center px-4 py-2 text-sm font-medium rounded-md text-red-700 bg-red-50 hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
|
||||
>
|
||||
<TrashIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
|
||||
Delete
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { getAuthToken, signIn, signOut } from '../redux';
|
||||
import { getAuthToken, getCurrentUser, signIn, signOut } from '../redux';
|
||||
|
||||
export function useAuth() {
|
||||
const dispatch = useDispatch();
|
||||
const currentUser = useSelector(getCurrentUser);
|
||||
const authToken = useSelector(getAuthToken);
|
||||
|
||||
const logIn = useCallback(
|
||||
|
@ -19,6 +20,7 @@ export function useAuth() {
|
|||
|
||||
return {
|
||||
authToken,
|
||||
currentUser,
|
||||
logIn,
|
||||
logOut,
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export { signIn, signOut, AuthActionTypes } from './actions';
|
||||
export { default as reducer } from './reducers';
|
||||
export { getAuth, getIsAuthLoading, getAuthToken } from './selectors';
|
||||
export { getAuth, getIsAuthLoading, getAuthToken, getCurrentUser } from './selectors';
|
||||
export * from './types';
|
||||
|
|
|
@ -2,15 +2,24 @@ import { createApiReducer, chainReducers, INITIAL_API_STATUS } from 'src/service
|
|||
|
||||
import { AuthState } from './types';
|
||||
import { AuthActionTypes } from './actions';
|
||||
import { CurrentUser } from '../types';
|
||||
|
||||
const initialCurrentUserState: CurrentUser = {
|
||||
email: null,
|
||||
name: null,
|
||||
preferredUsername: null,
|
||||
id: null,
|
||||
};
|
||||
|
||||
const initialState: AuthState = {
|
||||
token: null,
|
||||
userInfo: initialCurrentUserState,
|
||||
_status: INITIAL_API_STATUS,
|
||||
};
|
||||
|
||||
const auth = createApiReducer(
|
||||
[AuthActionTypes.SIGN_IN_START, AuthActionTypes.SIGN_IN_SUCCESS, AuthActionTypes.SIGN_IN_FAILURE],
|
||||
(data) => ({ token: data.accessToken }),
|
||||
(data) => ({ token: data.accessToken, userInfo: data.userInfo }),
|
||||
(data) => data.error.message,
|
||||
);
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@ export const getAuth = (state: State) => state.auth;
|
|||
|
||||
export const getAuthToken = (state: State) => state.auth.token;
|
||||
|
||||
export const getCurrentUser = (state: State) => state.auth.userInfo;
|
||||
|
||||
export const getIsAuthLoading = (state: State) => isLoading(getAuth(state));
|
||||
|
||||
export const getToken = (state: State) => state.auth.token;
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
export interface Auth {
|
||||
token: string | null;
|
||||
userInfo: CurrentUser;
|
||||
}
|
||||
|
||||
export interface CurrentUser {
|
||||
email: string | null;
|
||||
name: string | null;
|
||||
preferredUsername: string | null;
|
||||
id: string | null;
|
||||
}
|
||||
|
|
|
@ -70,7 +70,7 @@ export const updateUserById = (user: any) => async (dispatch: Dispatch<any>) =>
|
|||
payload: transformResponseUser(data),
|
||||
});
|
||||
|
||||
showToast('User updated sucessfully.', ToastType.Success);
|
||||
showToast('User updated successfully.', ToastType.Success);
|
||||
|
||||
dispatch(fetchUsers());
|
||||
} catch (err) {
|
||||
|
@ -95,7 +95,7 @@ export const createUser = (user: any) => async (dispatch: Dispatch<any>) => {
|
|||
payload: transformResponseUser(data),
|
||||
});
|
||||
|
||||
showToast('User created sucessfully.', ToastType.Success);
|
||||
showToast('User created successfully.', ToastType.Success);
|
||||
|
||||
dispatch(fetchUsers());
|
||||
} catch (err: any) {
|
||||
|
@ -121,7 +121,7 @@ export const deleteUser = (id: string) => async (dispatch: Dispatch<any>) => {
|
|||
payload: {},
|
||||
});
|
||||
|
||||
showToast('User deleted sucessfully.', ToastType.Success);
|
||||
showToast('User deleted successfully.', ToastType.Success);
|
||||
|
||||
dispatch(fetchUsers());
|
||||
} catch (err) {
|
||||
|
|
Loading…
Reference in a new issue