remove stuff
This commit is contained in:
parent
24724c1f90
commit
b43552c5eb
53 changed files with 26 additions and 2316 deletions
|
@ -1,2 +1 @@
|
|||
REACT_APP_API_URL=http://stackspin_proxy:8081/api/v1
|
||||
REACT_APP_HYDRA_PUBLIC_URL=https://sso.init.stackspin.net
|
|
@ -1,277 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import _ from 'lodash';
|
||||
import { TrashIcon } from '@heroicons/react/outline';
|
||||
import { useFieldArray, useForm, useWatch } from 'react-hook-form';
|
||||
import { Banner, Modal, ConfirmationModal } from 'src/components';
|
||||
import { Input, Select } from 'src/components/Form';
|
||||
import { User, UserRole, useUsers } from 'src/services/users';
|
||||
import { useAuth } from 'src/services/auth';
|
||||
import { appAccessList, initialUserForm } from './consts';
|
||||
import { UserModalProps } from './types';
|
||||
|
||||
export const UserModal = ({ open, onClose, userId, setUserId }: UserModalProps) => {
|
||||
const [deleteModal, setDeleteModal] = useState(false);
|
||||
const [isAdminRoleSelected, setAdminRoleSelected] = useState(true);
|
||||
const [isPersonalModal, setPersonalModal] = useState(false);
|
||||
const {
|
||||
user,
|
||||
loadUser,
|
||||
loadPersonalInfo,
|
||||
editUserById,
|
||||
editPersonalInfo,
|
||||
createNewUser,
|
||||
userModalLoading,
|
||||
deleteUserById,
|
||||
clearSelectedUser,
|
||||
} = useUsers();
|
||||
const { currentUser, isAdmin } = useAuth();
|
||||
|
||||
const { control, reset, handleSubmit } = useForm<User>({
|
||||
defaultValues: initialUserForm,
|
||||
});
|
||||
|
||||
const { fields, update } = useFieldArray({
|
||||
control,
|
||||
name: 'app_roles',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (userId) {
|
||||
const currentUserId = currentUser?.id;
|
||||
if (currentUserId === userId) {
|
||||
setPersonalModal(true);
|
||||
loadPersonalInfo();
|
||||
} else {
|
||||
loadUser(userId);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [userId, open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!_.isEmpty(user)) {
|
||||
reset(user);
|
||||
}
|
||||
|
||||
return () => {
|
||||
reset(initialUserForm);
|
||||
};
|
||||
}, [user, reset, open]);
|
||||
|
||||
const dashboardRole = useWatch({
|
||||
control,
|
||||
name: 'app_roles.0.role',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const isAdminDashboardRoleSelected = dashboardRole === UserRole.Admin;
|
||||
setAdminRoleSelected(isAdminDashboardRoleSelected);
|
||||
if (isAdminDashboardRoleSelected) {
|
||||
fields.forEach((field, index) => update(index, { name: field.name, role: UserRole.Admin }));
|
||||
} else {
|
||||
fields.forEach((field, index) => update(index, { name: field.name, role: UserRole.User }));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dashboardRole]);
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
if (isPersonalModal) {
|
||||
await handleSubmit((data) => editPersonalInfo(data))();
|
||||
} else if (userId) {
|
||||
await handleSubmit((data) => editUserById(data))();
|
||||
} else {
|
||||
await handleSubmit((data) => createNewUser(data))();
|
||||
}
|
||||
} catch (e: any) {
|
||||
// Continue
|
||||
}
|
||||
|
||||
onClose();
|
||||
clearSelectedUser();
|
||||
setUserId(null);
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: any) => {
|
||||
if (e.key === 'Enter' || e.key === 'NumpadEnter') {
|
||||
handleSave();
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
clearSelectedUser();
|
||||
setUserId(null);
|
||||
};
|
||||
|
||||
const deleteModalOpen = () => setDeleteModal(true);
|
||||
const deleteModalClose = () => setDeleteModal(false);
|
||||
|
||||
const handleDelete = () => {
|
||||
if (userId) {
|
||||
deleteUserById(userId);
|
||||
}
|
||||
|
||||
clearSelectedUser();
|
||||
setUserId(null);
|
||||
handleClose();
|
||||
deleteModalClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
onClose={handleClose}
|
||||
open={open}
|
||||
onSave={handleSave}
|
||||
isLoading={userModalLoading}
|
||||
leftActions={
|
||||
userId &&
|
||||
user.email !== currentUser?.email && (
|
||||
<button
|
||||
onClick={deleteModalOpen}
|
||||
type="button"
|
||||
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
|
||||
</button>
|
||||
)
|
||||
}
|
||||
useCancelButton
|
||||
>
|
||||
<div className="bg-white px-4">
|
||||
<div className="space-y-10 divide-y divide-gray-200">
|
||||
<div>
|
||||
<div>
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">{userId ? 'Edit user' : 'Add new user'}</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} required={false} />
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-3">
|
||||
<Input
|
||||
control={control}
|
||||
name="email"
|
||||
label="Email"
|
||||
type="email"
|
||||
onKeyPress={handleKeyPress}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{isAdmin && (
|
||||
<>
|
||||
<div className="sm:col-span-3">
|
||||
{fields
|
||||
.filter((field) => field.name === 'dashboard')
|
||||
.map((item, index) => (
|
||||
<Select
|
||||
key={item.name}
|
||||
control={control}
|
||||
name={`app_roles.${index}.role`}
|
||||
label="Role"
|
||||
options={[
|
||||
{ value: UserRole.User, name: 'User' },
|
||||
{ value: UserRole.Admin, name: 'Admin' },
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
</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">
|
||||
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>
|
||||
</div>
|
||||
|
||||
{isAdmin && !userModalLoading && (
|
||||
<div>
|
||||
<div className="mt-8">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">App Access</h3>
|
||||
</div>
|
||||
|
||||
{isAdminRoleSelected && (
|
||||
<div className="sm:col-span-6">
|
||||
<Banner
|
||||
title="Admin users automatically have admin-level access to all apps."
|
||||
titleSm="Admin user"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isAdminRoleSelected && (
|
||||
<div>
|
||||
<div className="flow-root mt-6">
|
||||
<ul className="-my-5 divide-y divide-gray-200">
|
||||
{fields.map((item, index) => {
|
||||
if (item.name === 'dashboard') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="py-4" key={item.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={_.find(appAccessList, ['name', item.name!])?.image}
|
||||
alt={item.name ?? 'Image'}
|
||||
/>
|
||||
<h3 className="ml-4 text-md leading-6 font-medium text-gray-900">
|
||||
{_.find(appAccessList, ['name', item.name!])?.label}
|
||||
</h3>
|
||||
</div>
|
||||
<div>
|
||||
<Select
|
||||
key={item.id}
|
||||
control={control}
|
||||
name={`app_roles.${index}.role`}
|
||||
disabled={isAdminRoleSelected}
|
||||
options={[
|
||||
{ value: UserRole.NoAccess, name: 'No Access' },
|
||||
{ value: UserRole.User, name: 'User' },
|
||||
{ value: UserRole.Admin, name: 'Admin' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<ConfirmationModal
|
||||
onDeleteAction={handleDelete}
|
||||
open={deleteModal}
|
||||
onClose={deleteModalClose}
|
||||
title="Delete user"
|
||||
body="Are you sure you want to delete this user? All of the user data will be permanently removed. This action cannot be undone."
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,64 +0,0 @@
|
|||
import { UserRole } from 'src/services/users';
|
||||
|
||||
export const appAccessList = [
|
||||
{
|
||||
name: 'wekan',
|
||||
image: '/assets/wekan.svg',
|
||||
label: 'Wekan',
|
||||
},
|
||||
{
|
||||
name: 'wordpress',
|
||||
image: '/assets/wordpress.svg',
|
||||
label: 'Wordpress',
|
||||
},
|
||||
{
|
||||
name: 'nextcloud',
|
||||
image: '/assets/nextcloud.svg',
|
||||
label: 'Nextcloud',
|
||||
},
|
||||
{
|
||||
name: 'zulip',
|
||||
image: '/assets/zulip.svg',
|
||||
label: 'Zulip',
|
||||
},
|
||||
];
|
||||
|
||||
export const allAppAccessList = [
|
||||
{
|
||||
name: 'dashboard',
|
||||
image: '/assets/logo-small.svg',
|
||||
label: 'Dashboard',
|
||||
},
|
||||
...appAccessList,
|
||||
];
|
||||
|
||||
export const initialAppRoles = [
|
||||
{
|
||||
name: 'dashboard',
|
||||
role: UserRole.User,
|
||||
},
|
||||
{
|
||||
name: 'wekan',
|
||||
role: UserRole.User,
|
||||
},
|
||||
{
|
||||
name: 'wordpress',
|
||||
role: UserRole.User,
|
||||
},
|
||||
{
|
||||
name: 'nextcloud',
|
||||
role: UserRole.User,
|
||||
},
|
||||
{
|
||||
name: 'zulip',
|
||||
role: UserRole.User,
|
||||
},
|
||||
];
|
||||
|
||||
export const initialUserForm = {
|
||||
id: '',
|
||||
name: '',
|
||||
email: '',
|
||||
app_roles: initialAppRoles,
|
||||
status: '',
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
export { UserModal } from './UserModal';
|
|
@ -1,6 +0,0 @@
|
|||
export type UserModalProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
userId: string | null;
|
||||
setUserId: any;
|
||||
};
|
|
@ -4,5 +4,4 @@ export { Table } from './Table';
|
|||
export { Banner } from './Banner';
|
||||
export { Tabs } from './Tabs';
|
||||
export { Modal, ConfirmationModal, StepsModal } from './Modal';
|
||||
export { UserModal } from './UserModal';
|
||||
export { ProgressSteps } from './ProgressSteps';
|
||||
|
|
|
@ -1,51 +1,12 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import axios from 'axios';
|
||||
import { Provider } from 'react-redux';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { PersistGate } from 'redux-persist/integration/react';
|
||||
|
||||
import { isValid } from './services/api';
|
||||
import { configureStore } from './redux';
|
||||
import { AuthActionTypes } from './services/auth/redux';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
|
||||
const { store, persistor } = configureStore();
|
||||
|
||||
axios.interceptors.request.use(
|
||||
(config) => {
|
||||
const { auth } = store.getState();
|
||||
if (isValid(auth)) {
|
||||
config.headers.Authorization = `Bearer ${auth.token}`;
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
// when we receive 401 error, we sign out the logged in user
|
||||
axios.interceptors.response.use(
|
||||
(response: any) => response,
|
||||
(error: any) => {
|
||||
if (error.response !== undefined && error.response.status === 401) {
|
||||
store.dispatch({ type: AuthActionTypes.SIGN_OUT, payload: error });
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</PersistGate>
|
||||
</Provider>,
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>,
|
||||
document.getElementById('root'),
|
||||
);
|
||||
|
|
|
@ -13,7 +13,7 @@ export const AppIframe: React.FC<any> = ({ app }: { app: any }) => {
|
|||
overflow="hidden"
|
||||
scrolling="no"
|
||||
title={app.name}
|
||||
url={app.url}
|
||||
url={app.externalUrl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal } from 'src/components';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export const DashboardCard: React.FC<any> = ({ app }: { app: any }) => {
|
||||
const [readMoreVisible, setReadMoreVisible] = useState(false);
|
||||
|
@ -17,40 +18,33 @@ export const DashboardCard: React.FC<any> = ({ app }: { app: any }) => {
|
|||
.catch(() => {});
|
||||
}, [app.markdownSrc]);
|
||||
|
||||
const url = `/${app.internalUrl}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-white overflow-hidden shadow rounded-lg divide-y divide-gray-100 mb-4 md:mb-0" key={app.name}>
|
||||
<div className="px-4 py-5 sm:p-6">
|
||||
<div className="mr-4 flex items-center">
|
||||
<img
|
||||
className="h-16 w-16 rounded-md overflow-hidden mr-4 flex-shrink-0"
|
||||
src={app.assetSrc}
|
||||
alt="Nextcloud"
|
||||
/>
|
||||
<Link
|
||||
to={url}
|
||||
// className="mx-1 inline-flex items-center px-2.5 py-1.5 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||
>
|
||||
<div
|
||||
className="bg-white overflow-hidden shadow rounded-lg divide-y divide-gray-100 mb-4 md:mb-0"
|
||||
key={app.name}
|
||||
>
|
||||
<div className="px-4 py-5 sm:p-6">
|
||||
<div className="mr-4 flex items-center">
|
||||
<img
|
||||
className="h-16 w-16 rounded-md overflow-hidden mr-4 flex-shrink-0"
|
||||
src={app.assetSrc}
|
||||
alt="Nextcloud"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl leading-8 font-bold">{app.name}</h2>
|
||||
<div>
|
||||
<h2 className="text-xl leading-8 font-bold">{app.name}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-2.5 py-2.5 sm:px-4 flex justify-end">
|
||||
<a
|
||||
href={app.internalUrl}
|
||||
rel="noreferrer"
|
||||
className="mx-1 inline-flex items-center px-2.5 py-1.5 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||
>
|
||||
Im Dashboard öffnen
|
||||
</a>
|
||||
<a
|
||||
href={app.externalUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="mx-1 inline-flex items-center px-2.5 py-1.5 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||
>
|
||||
neues Fenster
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Modal open={readMoreVisible} onClose={onReadMoreCloseClick} title={app.name}>
|
||||
<ReactMarkdown className="prose">{content}</ReactMarkdown>
|
||||
|
|
|
@ -1,4 +1,2 @@
|
|||
export { Login } from './login';
|
||||
export { Dashboard } from './dashboard';
|
||||
export { Apps, AppSingle } from './apps';
|
||||
export { Users } from './users';
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { LockClosedIcon } from '@heroicons/react/solid';
|
||||
|
||||
import { performApiCall } from 'src/services/api';
|
||||
import { showToast, ToastType } from 'src/common/util/show-toast';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export function Login() {
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const { data } = await performApiCall({
|
||||
path: '/login',
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (data.authorizationUrl) {
|
||||
window.location.href = data.authorizationUrl;
|
||||
}
|
||||
} catch (e: any) {
|
||||
showToast('Something went wrong', ToastType.Error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div className="flex justify-center">
|
||||
<img className="lg:block" src="assets/logo.svg" alt="Stackspin" />
|
||||
<h2 className="mt-6 text-center text-xl font-bold text-gray-900 sr-only">Sign in</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
type="button"
|
||||
className={clsx(
|
||||
'group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-primary-dark hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500',
|
||||
)}
|
||||
>
|
||||
<span className="absolute left-0 inset-y-0 flex items-center pl-3">
|
||||
<LockClosedIcon className="h-5 w-5 text-white group-hover:text-primary-light" aria-hidden="true" />
|
||||
</span>
|
||||
Sign in
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { showToast, ToastType } from 'src/common/util/show-toast';
|
||||
import { useAuth } from 'src/services/auth';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export function LoginCallback() {
|
||||
const currentURL = window.location.href;
|
||||
const indexOfQuestionMark = currentURL.indexOf('?');
|
||||
const params = currentURL.slice(indexOfQuestionMark);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { logIn } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
async function logInUser() {
|
||||
if (params.length > 2) {
|
||||
const res = await logIn(params);
|
||||
|
||||
// @ts-ignore
|
||||
if (!res.ok) {
|
||||
navigate('/login');
|
||||
showToast('Something went wrong, please try logging in again.', ToastType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logInUser();
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [params]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div className="flex flex-col justify-center items-center">
|
||||
<div className="flex justify-center items-center border border-transparent text-base font-medium rounded-md text-white transition ease-in-out duration-150">
|
||||
<svg
|
||||
className="animate-spin h-10 w-10 text-primary"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle className="opacity-50" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path
|
||||
className="opacity-100"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-lg text-primary-600 mt-2">Logging You in, just a moment.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export { Login } from './Login';
|
|
@ -1,186 +0,0 @@
|
|||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import React, { useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import { SearchIcon, PlusIcon, ViewGridAddIcon } from '@heroicons/react/solid';
|
||||
import { CogIcon, TrashIcon } from '@heroicons/react/outline';
|
||||
import { useUsers } from 'src/services/users';
|
||||
import { Table } from 'src/components';
|
||||
import { debounce } from 'lodash';
|
||||
import { useAuth } from 'src/services/auth';
|
||||
|
||||
import { UserModal } from '../../components/UserModal';
|
||||
import { MultipleUsersModal } from './components';
|
||||
|
||||
export const Users: React.FC = () => {
|
||||
const [selectedRowsIds, setSelectedRowsIds] = useState({});
|
||||
const [configureModal, setConfigureModal] = useState(false);
|
||||
const [multipleUsersModal, setMultipleUsersModal] = useState(false);
|
||||
const [userId, setUserId] = useState(null);
|
||||
const [search, setSearch] = useState('');
|
||||
const { users, loadUsers, userTableLoading } = useUsers();
|
||||
const { isAdmin } = useAuth();
|
||||
|
||||
const handleSearch = (event: any) => {
|
||||
setSearch(event.target.value);
|
||||
};
|
||||
|
||||
const debouncedSearch = useCallback(debounce(handleSearch, 250), []);
|
||||
|
||||
useEffect(() => {
|
||||
loadUsers();
|
||||
|
||||
return () => {
|
||||
debouncedSearch.cancel();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const filterSearch = useMemo(() => {
|
||||
return users.filter((item: any) => item.email?.toLowerCase().includes(search.toLowerCase()));
|
||||
}, [search, users]);
|
||||
|
||||
const configureModalOpen = (id: any) => {
|
||||
setUserId(id);
|
||||
setConfigureModal(true);
|
||||
};
|
||||
|
||||
const configureModalClose = () => setConfigureModal(false);
|
||||
|
||||
const multipleUsersModalClose = () => setMultipleUsersModal(false);
|
||||
|
||||
const columns: any = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
Header: 'Name',
|
||||
accessor: 'name',
|
||||
width: 'auto',
|
||||
},
|
||||
{
|
||||
Header: 'Email',
|
||||
accessor: 'email',
|
||||
width: 'auto',
|
||||
},
|
||||
{
|
||||
Header: 'Status',
|
||||
accessor: 'status',
|
||||
width: 'auto',
|
||||
},
|
||||
{
|
||||
Header: ' ',
|
||||
Cell: (props: any) => {
|
||||
const { row } = props;
|
||||
|
||||
if (isAdmin) {
|
||||
return (
|
||||
<div className="text-right lg:opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={() => configureModalOpen(row.original.id)}
|
||||
type="button"
|
||||
className="inline-flex items-center px-4 py-2 border border-gray-200 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||
>
|
||||
<CogIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
|
||||
Configure
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
width: 'auto',
|
||||
},
|
||||
],
|
||||
[isAdmin],
|
||||
);
|
||||
|
||||
const selectedRows = useCallback((rows: Record<string, boolean>) => {
|
||||
setSelectedRowsIds(rows);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<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>
|
||||
|
||||
{isAdmin && (
|
||||
<div className="mt-3 sm:mt-0 sm:ml-4">
|
||||
<button
|
||||
onClick={() => configureModalOpen(null)}
|
||||
type="button"
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-700 hover:bg-primary-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-800 mx-5 "
|
||||
>
|
||||
<PlusIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
|
||||
Add new user
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMultipleUsersModal(true)}
|
||||
type="button"
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-700 hover:bg-primary-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-800"
|
||||
>
|
||||
<ViewGridAddIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
|
||||
Add new users
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between w-100 my-3 items-center mb-5 ">
|
||||
<div className="flex items-center">
|
||||
<div className="inline-block">
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 sr-only">
|
||||
Search candidates
|
||||
</label>
|
||||
<div className="mt-1 flex rounded-md shadow-sm">
|
||||
<div className="relative flex items-stretch flex-grow focus-within:z-10">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<SearchIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="email"
|
||||
id="email"
|
||||
className="focus:ring-primary-500 focus:border-primary-500 block w-full rounded-md pl-10 sm:text-sm border-gray-200"
|
||||
placeholder="Search Users"
|
||||
onChange={debouncedSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedRowsIds && Object.keys(selectedRowsIds).length !== 0 && (
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
onClick={() => {}}
|
||||
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"
|
||||
>
|
||||
<TrashIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<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 overflow-hidden">
|
||||
<Table
|
||||
data={filterSearch as any}
|
||||
columns={columns}
|
||||
getSelectedRowIds={selectedRows}
|
||||
loading={userTableLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{configureModal && (
|
||||
<UserModal open={configureModal} onClose={configureModalClose} userId={userId} setUserId={setUserId} />
|
||||
)}
|
||||
{multipleUsersModal && <MultipleUsersModal open={multipleUsersModal} onClose={multipleUsersModalClose} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,248 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import _ from 'lodash';
|
||||
import { useFieldArray, useForm, useWatch } from 'react-hook-form';
|
||||
|
||||
import { Banner, StepsModal, ProgressSteps } from 'src/components';
|
||||
import { Select, TextArea } from 'src/components/Form';
|
||||
import { MultipleUsersData, UserRole, useUsers } from 'src/services/users';
|
||||
import { allAppAccessList } from 'src/components/UserModal/consts';
|
||||
import { ProgressStepInfo, ProgressStepStatus } from 'src/components/ProgressSteps/types';
|
||||
import { initialMultipleUsersForm, MultipleUsersModalProps } from './types';
|
||||
|
||||
export const MultipleUsersModal = ({ open, onClose }: MultipleUsersModalProps) => {
|
||||
const [steps, setSteps] = useState<ProgressStepInfo[]>([]);
|
||||
const [isAdminRoleSelected, setAdminRoleSelected] = useState(false);
|
||||
const { createUsers, userModalLoading } = useUsers();
|
||||
|
||||
const { control, handleSubmit } = useForm<MultipleUsersData>({
|
||||
defaultValues: initialMultipleUsersForm,
|
||||
});
|
||||
|
||||
const { fields, update } = useFieldArray({
|
||||
control,
|
||||
name: 'appRoles',
|
||||
});
|
||||
|
||||
const dashboardRole = useWatch({
|
||||
control,
|
||||
name: 'appRoles.0.role',
|
||||
});
|
||||
|
||||
const csvDataWatch = useWatch({
|
||||
control,
|
||||
name: 'csvUserData',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const isAdminDashboardRoleSelected = dashboardRole === UserRole.Admin;
|
||||
setAdminRoleSelected(isAdminDashboardRoleSelected);
|
||||
if (isAdminDashboardRoleSelected) {
|
||||
fields.forEach((field, index) => update(index, { name: field.name, role: UserRole.Admin }));
|
||||
} else {
|
||||
fields.forEach((field, index) => update(index, { name: field.name, role: UserRole.User }));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dashboardRole]);
|
||||
|
||||
const renderUsersCsvDataInput = () => {
|
||||
return (
|
||||
<div>
|
||||
<div className="mt-8">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">CSV data</h3>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<TextArea
|
||||
control={control}
|
||||
name="csvUserData"
|
||||
placeholder={`Please paste users in CSV format: email, name\nuser1@example.com,User One\nuser2@example.com,User Two`}
|
||||
rows={15}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderAppAccess = () => {
|
||||
return (
|
||||
<div>
|
||||
<div className="mt-8">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">App Access</h3>
|
||||
</div>
|
||||
|
||||
{isAdminRoleSelected && (
|
||||
<div className="sm:col-span-6">
|
||||
<Banner title="Admin users automatically have admin-level access to all apps." titleSm="Admin user" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<div className="flow-root mt-6">
|
||||
<ul className="-my-5 divide-y divide-gray-200">
|
||||
{fields
|
||||
.filter((field) => field.name === 'dashboard')
|
||||
.map((item, index) => (
|
||||
<li className="py-4" key={item.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={_.find(allAppAccessList, ['name', item.name!])?.image}
|
||||
alt={item.name ?? 'Image'}
|
||||
/>
|
||||
<h3 className="ml-4 text-md leading-6 font-medium text-gray-900">
|
||||
{_.find(allAppAccessList, ['name', item.name!])?.label}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="sm:col-span-2">
|
||||
<Select
|
||||
key={item.id}
|
||||
control={control}
|
||||
name={`appRoles.${index}.role`}
|
||||
options={[
|
||||
{ value: UserRole.User, name: 'User' },
|
||||
{ value: UserRole.Admin, name: 'Admin' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
{!isAdminRoleSelected &&
|
||||
fields.map((item, index) => {
|
||||
if (item.name === 'dashboard') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="py-4" key={item.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={_.find(allAppAccessList, ['name', item.name!])?.image}
|
||||
alt={item.name ?? 'Image'}
|
||||
/>
|
||||
<h3 className="ml-4 text-md leading-6 font-medium text-gray-900">
|
||||
{_.find(allAppAccessList, ['name', item.name!])?.label}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="sm:col-span-2">
|
||||
<Select
|
||||
key={item.id}
|
||||
control={control}
|
||||
name={`appRoles.${index}.role`}
|
||||
disabled={isAdminRoleSelected}
|
||||
options={[
|
||||
{ value: UserRole.NoAccess, name: 'No Access' },
|
||||
{ value: UserRole.User, name: 'User' },
|
||||
{ value: UserRole.Admin, name: 'Admin' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSteps([
|
||||
{
|
||||
id: 'Step 1',
|
||||
name: 'Enter CSV user data',
|
||||
status: ProgressStepStatus.Current,
|
||||
},
|
||||
{
|
||||
id: 'Step 2',
|
||||
name: 'Define app access roles',
|
||||
status: ProgressStepStatus.Upcoming,
|
||||
},
|
||||
]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open]);
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
await handleSubmit((data) => createUsers(data))();
|
||||
} catch (e: any) {
|
||||
// Continue
|
||||
}
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
const getActiveStepIndex = () => _.findIndex(steps, { status: ProgressStepStatus.Current });
|
||||
|
||||
const updateStepsStatus = (nextIndex: number) => {
|
||||
const updatedSteps = [...steps];
|
||||
_.forEach(updatedSteps, (step, index) => {
|
||||
if (index < nextIndex) {
|
||||
step.status = ProgressStepStatus.Complete;
|
||||
} else if (index === nextIndex) {
|
||||
step.status = ProgressStepStatus.Current;
|
||||
} else {
|
||||
step.status = ProgressStepStatus.Upcoming;
|
||||
}
|
||||
});
|
||||
setSteps(updatedSteps);
|
||||
};
|
||||
|
||||
const handleStepClick = (stepId: string) => {
|
||||
const activeStepIndex = _.findIndex(steps, { id: stepId });
|
||||
updateStepsStatus(activeStepIndex);
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
const nextIndex = getActiveStepIndex() + 1;
|
||||
updateStepsStatus(nextIndex);
|
||||
};
|
||||
|
||||
const handlePrevious = () => {
|
||||
const nextIndex = getActiveStepIndex() - 1;
|
||||
updateStepsStatus(nextIndex);
|
||||
};
|
||||
|
||||
const activeStepIndex = getActiveStepIndex();
|
||||
const showSave = !_.some(steps, { status: ProgressStepStatus.Upcoming });
|
||||
const showPrevious = _.some(steps, { status: ProgressStepStatus.Complete });
|
||||
|
||||
return (
|
||||
<StepsModal
|
||||
onClose={handleClose}
|
||||
open={open}
|
||||
onSave={handleSave}
|
||||
onNext={handleNext}
|
||||
onPrevious={handlePrevious}
|
||||
showPreviousButton={showPrevious}
|
||||
isLoading={userModalLoading}
|
||||
useCancelButton
|
||||
showSaveButton={showSave}
|
||||
saveButtonDisabled={_.isEmpty(csvDataWatch)}
|
||||
>
|
||||
<div className="bg-white px-4">
|
||||
<div className="space-y-10 divide-y divide-gray-200">
|
||||
<div>
|
||||
<div>
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">Add new users</h3>
|
||||
</div>
|
||||
<div className="sm:px-6 pt-6">
|
||||
<ProgressSteps steps={steps} onStepClick={handleStepClick}>
|
||||
{activeStepIndex === 0 ? renderUsersCsvDataInput() : renderAppAccess()}
|
||||
</ProgressSteps>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</StepsModal>
|
||||
);
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
export { MultipleUsersModal } from './MultipleUsersModal';
|
|
@ -1,10 +0,0 @@
|
|||
import { initialAppRoles } from 'src/components/UserModal/consts';
|
||||
|
||||
export type MultipleUsersModalProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const initialMultipleUsersForm = {
|
||||
appRoles: initialAppRoles,
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
export { MultipleUsersModal } from './MultipleUsersModal';
|
|
@ -1 +0,0 @@
|
|||
export { Users } from './Users';
|
|
@ -1,2 +0,0 @@
|
|||
export { configureStore } from './store';
|
||||
export * from './types';
|
|
@ -1,35 +0,0 @@
|
|||
import { createStore, compose, applyMiddleware, combineReducers } from 'redux';
|
||||
import thunkMiddleware from 'redux-thunk';
|
||||
import { persistStore, persistReducer } from 'redux-persist';
|
||||
import storage from 'redux-persist/lib/storage';
|
||||
|
||||
import { reducer as authReducer } from 'src/services/auth';
|
||||
|
||||
import usersReducer from 'src/services/users/redux/reducers';
|
||||
import { State } from './types';
|
||||
|
||||
const persistConfig = {
|
||||
key: 'root',
|
||||
storage,
|
||||
whitelist: ['auth'],
|
||||
};
|
||||
|
||||
const appReducer = combineReducers<State>({
|
||||
auth: authReducer,
|
||||
users: usersReducer,
|
||||
});
|
||||
|
||||
const persistedReducer = persistReducer(persistConfig, appReducer);
|
||||
|
||||
const middlewares = [thunkMiddleware];
|
||||
|
||||
// @ts-ignore
|
||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
|
||||
export const configureStore = () => {
|
||||
const store = createStore(persistedReducer, composeEnhancers(applyMiddleware(...middlewares)));
|
||||
|
||||
const persistor = persistStore(store);
|
||||
|
||||
return { store, persistor };
|
||||
};
|
|
@ -1,11 +0,0 @@
|
|||
import { Store } from 'redux';
|
||||
|
||||
import { AuthState } from 'src/services/auth/redux';
|
||||
import { UsersState } from 'src/services/users/redux';
|
||||
|
||||
export interface AppStore extends Store, State {}
|
||||
|
||||
export interface State {
|
||||
auth: AuthState;
|
||||
users: UsersState;
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import axios from 'axios';
|
||||
|
||||
import { api } from './config';
|
||||
import { ApiConfig } from './types';
|
||||
|
||||
/**
|
||||
* Function which creates an API call for given api config
|
||||
*/
|
||||
export const createApiCall = (apiConfig: ApiConfig) => async () => {
|
||||
const hostname = apiConfig.hostname || api.hostname;
|
||||
const { path } = apiConfig;
|
||||
const method = _.get(apiConfig, 'method', 'GET');
|
||||
const contentType =
|
||||
_.includes(['POST', 'PUT', 'PATCH'], method) && !apiConfig.formData
|
||||
? {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
: {};
|
||||
const passedHeaders = _.get(apiConfig, 'headers', {});
|
||||
const headers = {
|
||||
...contentType,
|
||||
...passedHeaders,
|
||||
};
|
||||
const axiosConfig = {
|
||||
method,
|
||||
headers,
|
||||
url: `${hostname}${path}`,
|
||||
timeout: 30000,
|
||||
data: apiConfig.formData || apiConfig.body,
|
||||
};
|
||||
|
||||
return axios(axiosConfig);
|
||||
};
|
||||
|
||||
/**
|
||||
* Performs an API call for given api config and returns a promise
|
||||
*/
|
||||
export const performApiCall = (apiConfig: ApiConfig) => {
|
||||
const apiCall = createApiCall(apiConfig);
|
||||
return apiCall();
|
||||
};
|
|
@ -1,3 +0,0 @@
|
|||
export const api = {
|
||||
hostname: process.env.REACT_APP_API_URL,
|
||||
};
|
|
@ -1,5 +0,0 @@
|
|||
export * from './redux';
|
||||
|
||||
export { api } from './config';
|
||||
|
||||
export { createApiCall, performApiCall } from './apiCall';
|
|
@ -1,186 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import urlcat from 'urlcat';
|
||||
|
||||
import { createApiCall } from '../apiCall';
|
||||
import { StartAction, SuccessAction, FailureAction } from './types';
|
||||
|
||||
import { ApiConfig } from '../types';
|
||||
|
||||
/**
|
||||
* Function which creates a thunk action for given api config and associated action types
|
||||
* @param {string|Record<string, unknown>} apiConfig if string it is interprated as path in api endpoint
|
||||
* @param {string[]} actionTypes action types for start, success and failure
|
||||
*/
|
||||
export const createApiAction = (apiConfig: ApiConfig, actionTypes: string[]) => async (dispatch: any) => {
|
||||
const apiCall = createApiCall(apiConfig);
|
||||
|
||||
const startAction = (): StartAction => ({
|
||||
type: actionTypes[0],
|
||||
payload: null,
|
||||
});
|
||||
|
||||
const successAction = (payload: any): SuccessAction => ({
|
||||
type: actionTypes[1],
|
||||
payload,
|
||||
});
|
||||
|
||||
const failureAction = (error: string): FailureAction => ({
|
||||
type: actionTypes[2],
|
||||
payload: { error },
|
||||
});
|
||||
|
||||
dispatch(startAction());
|
||||
|
||||
try {
|
||||
const response = await apiCall();
|
||||
|
||||
let res;
|
||||
const additionalData = apiConfig.additionalData || {};
|
||||
|
||||
if (!_.isEmpty(additionalData)) {
|
||||
res = await dispatch(successAction({ ...response.data, ...additionalData }));
|
||||
} else {
|
||||
res = await dispatch(successAction(response.data));
|
||||
}
|
||||
|
||||
return { ok: true, res };
|
||||
} catch (e) {
|
||||
const error = _.get(e, 'response.data', {
|
||||
errorMessage: e.message || 'Undefined error, please try again.',
|
||||
});
|
||||
|
||||
await dispatch(failureAction(error));
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
errorMessage: error.message,
|
||||
status: e?.response?.status,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates opinionated CRUD actions
|
||||
*/
|
||||
export function createCrudApiActions<T>(
|
||||
basePath: string,
|
||||
startActionType: string,
|
||||
failureActionType: string,
|
||||
fetchActionType: string,
|
||||
addActionType: string,
|
||||
updateActionType: string,
|
||||
deleteActionType: string,
|
||||
transformItemForApi: (data: T) => any,
|
||||
pathTemplates: { load?: string; add?: string; change?: string } = {
|
||||
load: '/',
|
||||
add: '/',
|
||||
change: '/:id',
|
||||
},
|
||||
): any {
|
||||
const { load, add, change } = pathTemplates;
|
||||
const fetchItems = (
|
||||
pageNumber?: number,
|
||||
pageSize?: number,
|
||||
params: Record<string, unknown> = {},
|
||||
template: string = load!,
|
||||
) =>
|
||||
createApiAction(
|
||||
{
|
||||
path: urlcat(basePath, template, { pageNumber, pageSize, ...params }),
|
||||
method: 'GET',
|
||||
},
|
||||
[startActionType, fetchActionType, failureActionType],
|
||||
);
|
||||
|
||||
const addItem = (item: T, params: Record<string, unknown> = {}) =>
|
||||
createApiAction(
|
||||
{
|
||||
path: urlcat(basePath, add!, { ...params }),
|
||||
method: 'POST',
|
||||
body: transformItemForApi(item),
|
||||
},
|
||||
[startActionType, addActionType, failureActionType],
|
||||
);
|
||||
|
||||
const updateItem = (item: T, params: Record<string, unknown> = {}) =>
|
||||
createApiAction(
|
||||
{
|
||||
// @ts-ignore
|
||||
path: urlcat(basePath, change!, { id: item.id, ...params }),
|
||||
method: 'PUT',
|
||||
body: transformItemForApi(item),
|
||||
},
|
||||
[startActionType, updateActionType, failureActionType],
|
||||
);
|
||||
|
||||
const deleteItem = (itemId: number, params: Record<string, unknown> = {}) =>
|
||||
createApiAction(
|
||||
{
|
||||
path: urlcat(basePath, change!, { id: itemId, ...params }),
|
||||
method: 'DELETE',
|
||||
},
|
||||
[startActionType, deleteActionType, failureActionType],
|
||||
);
|
||||
|
||||
return [fetchItems, addItem, updateItem, deleteItem];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates opinionated CRUD actions
|
||||
*/
|
||||
export function createCrudApiActionsWithoutPaging<T extends { id?: number | string | null }>(
|
||||
basePath: string,
|
||||
startActionType: string,
|
||||
failureActionType: string,
|
||||
fetchActionType: string,
|
||||
addActionType: string,
|
||||
updateActionType: string,
|
||||
deleteActionType: string,
|
||||
transformItemForApi: (data: T) => any,
|
||||
pathTemplates: { load?: string; add?: string; change?: string } = {
|
||||
load: '/',
|
||||
add: '/',
|
||||
change: '/:id',
|
||||
},
|
||||
): any {
|
||||
const { load, add, change } = pathTemplates;
|
||||
const fetchItems = (params: Record<string, unknown> = {}, template: string = load!) =>
|
||||
createApiAction(
|
||||
{
|
||||
path: urlcat(basePath, template, params),
|
||||
method: 'GET',
|
||||
},
|
||||
[startActionType, fetchActionType, failureActionType],
|
||||
);
|
||||
|
||||
const addItem = (item: T, params: Record<string, unknown> = {}) =>
|
||||
createApiAction(
|
||||
{
|
||||
path: urlcat(basePath, add!, { ...params }),
|
||||
method: 'POST',
|
||||
body: transformItemForApi(item),
|
||||
},
|
||||
[startActionType, addActionType, failureActionType],
|
||||
);
|
||||
|
||||
const updateItem = (item: T, params: Record<string, unknown> = {}) =>
|
||||
createApiAction(
|
||||
{
|
||||
path: urlcat(basePath, change!, { id: item.id, ...params }),
|
||||
method: 'PUT',
|
||||
body: transformItemForApi(item),
|
||||
},
|
||||
[startActionType, updateActionType, failureActionType],
|
||||
);
|
||||
|
||||
const deleteItem = (itemId: number, params: Record<string, unknown> = {}) =>
|
||||
createApiAction(
|
||||
{
|
||||
path: urlcat(basePath, change!, { id: itemId, ...params }),
|
||||
method: 'DELETE',
|
||||
},
|
||||
[startActionType, deleteActionType, failureActionType],
|
||||
);
|
||||
|
||||
return [fetchItems, addItem, updateItem, deleteItem];
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
export {
|
||||
createApiReducer,
|
||||
createCrudApiReducer,
|
||||
createCrudApiReducerWithoutPaging,
|
||||
chainReducers,
|
||||
INITIAL_API_STATUS,
|
||||
INITIAL_API_STATE,
|
||||
} from './reducers';
|
||||
|
||||
export { createApiAction, createCrudApiActions, createCrudApiActionsWithoutPaging } from './actions';
|
||||
|
||||
export { isEmpty, isLoading, isValid, isError, getError, getErrorMessage } from './traits';
|
||||
|
||||
export * from './types';
|
|
@ -1,176 +0,0 @@
|
|||
import { StartAction, SuccessAction, FailureAction, ApiStatus, ApiState } from './types';
|
||||
|
||||
export const INITIAL_API_STATUS: ApiStatus = {
|
||||
isEmpty: true,
|
||||
isLoading: false,
|
||||
isValid: false,
|
||||
isError: false,
|
||||
dateTime: Date.now(),
|
||||
error: null,
|
||||
errorMessage: null,
|
||||
};
|
||||
|
||||
export const INITIAL_API_STATE: ApiState = {
|
||||
_status: INITIAL_API_STATUS,
|
||||
items: [],
|
||||
};
|
||||
|
||||
type AllActions = StartAction | SuccessAction | FailureAction;
|
||||
|
||||
/**
|
||||
* Use this function to create any reducer which handles api communication
|
||||
* @param {Array} actionTypes Array of action types associated with fetchThunk e.g. USER_REQUEST,
|
||||
* USER_SUCCESS and USER_ERROR
|
||||
* @param {function} dataAdapter function which will be called to transform response body
|
||||
* to format you want to use in state
|
||||
* @param {function} errorAdapter function which will be called to transform error
|
||||
* to format you want to use in state
|
||||
* @returns {function} reducer handles api requests, saves response body as data and status
|
||||
*/
|
||||
export const createApiReducer =
|
||||
(
|
||||
actionTypes: string[],
|
||||
dataAdapter: (data: any, state?: any) => any = (data: any) => data,
|
||||
errorAdapter = (data: any) => data,
|
||||
) =>
|
||||
(state = INITIAL_API_STATE, action: AllActions) => {
|
||||
const [REQUEST, SUCCESS, ERROR] = actionTypes;
|
||||
|
||||
switch (action.type) {
|
||||
case REQUEST:
|
||||
return {
|
||||
...state,
|
||||
_status: {
|
||||
...state._status,
|
||||
isLoading: true,
|
||||
dateTime: Date.now(),
|
||||
},
|
||||
};
|
||||
case SUCCESS:
|
||||
return {
|
||||
_status: {
|
||||
isEmpty: false,
|
||||
isLoading: false,
|
||||
isValid: true,
|
||||
isError: false,
|
||||
dateTime: Date.now(),
|
||||
error: null,
|
||||
errorMessage: null,
|
||||
},
|
||||
...dataAdapter(action.payload, state),
|
||||
};
|
||||
case ERROR:
|
||||
return {
|
||||
...state,
|
||||
_status: {
|
||||
isEmpty: false,
|
||||
isLoading: false,
|
||||
isValid: false,
|
||||
isError: true,
|
||||
error: action.payload,
|
||||
errorMessage: errorAdapter(action.payload),
|
||||
},
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This is higher order reducer
|
||||
* Use this if you want your state is combination of results of multiple reducers
|
||||
* under the same key, e.g. you can get the same data from multiple endpoints
|
||||
*/
|
||||
export const chainReducers =
|
||||
(initialState: any, ...args: any[]) =>
|
||||
(state = initialState, action: any) =>
|
||||
args.reduce((newState, reducer) => reducer(newState, action), state);
|
||||
|
||||
/**
|
||||
* Creates an opinionated CRUD reducer
|
||||
*/
|
||||
export function createCrudApiReducer<T>(
|
||||
startActionType: string,
|
||||
failureActionType: string,
|
||||
fetchActionType: string,
|
||||
addActionType: string,
|
||||
updateActionType: string,
|
||||
deleteActionType: string,
|
||||
transformItem: (data: any) => T,
|
||||
keyField = 'id',
|
||||
) {
|
||||
const fetchReducer = createApiReducer(
|
||||
[startActionType, fetchActionType, failureActionType],
|
||||
(data) => ({ _meta: data.meta, items: (data.data || data).map(transformItem) }),
|
||||
(data) => data,
|
||||
);
|
||||
|
||||
const addReducer = createApiReducer(
|
||||
[startActionType, addActionType, failureActionType],
|
||||
(data, state) => ({ items: [...state.items, transformItem(data)] }),
|
||||
(data) => data,
|
||||
);
|
||||
|
||||
const updateReducer = createApiReducer(
|
||||
[startActionType, updateActionType, failureActionType],
|
||||
(data, state) => ({
|
||||
items: state.items.map((i: any) => (i[keyField] === data[keyField] ? transformItem(data) : i)),
|
||||
}),
|
||||
(data) => data,
|
||||
);
|
||||
|
||||
const deleteReducer = createApiReducer(
|
||||
[startActionType, deleteActionType, failureActionType],
|
||||
(data, state) => ({
|
||||
items: state.items.filter((i: any) => i[keyField] !== data[keyField]),
|
||||
}),
|
||||
(data) => data,
|
||||
);
|
||||
|
||||
return chainReducers(INITIAL_API_STATE, fetchReducer, addReducer, updateReducer, deleteReducer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an opinionated CRUD reducer without paging
|
||||
*/
|
||||
export function createCrudApiReducerWithoutPaging<T>(
|
||||
startActionType: string,
|
||||
failureActionType: string,
|
||||
fetchActionType: string,
|
||||
addActionType: string,
|
||||
updateActionType: string,
|
||||
deleteActionType: string,
|
||||
transformItem: (data: any) => T,
|
||||
keyField = 'id',
|
||||
) {
|
||||
const fetchReducer = createApiReducer(
|
||||
[startActionType, fetchActionType, failureActionType],
|
||||
(data) => ({ items: data.map(transformItem) }),
|
||||
(data) => data,
|
||||
);
|
||||
|
||||
const addReducer = createApiReducer(
|
||||
[startActionType, addActionType, failureActionType],
|
||||
(data, state) => ({ items: [...state.items, transformItem(data)] }),
|
||||
(data) => data,
|
||||
);
|
||||
|
||||
const updateReducer = createApiReducer(
|
||||
[startActionType, updateActionType, failureActionType],
|
||||
(data, state) => ({
|
||||
items: state.items.map((i: any) => (i[keyField] === data[keyField] ? transformItem(data) : i)),
|
||||
}),
|
||||
(data) => data,
|
||||
);
|
||||
|
||||
const deleteReducer = createApiReducer(
|
||||
[startActionType, deleteActionType, failureActionType],
|
||||
(data, state) => ({
|
||||
items: state.items.filter((i: any) => i[keyField] !== data[keyField]),
|
||||
}),
|
||||
(data) => data,
|
||||
);
|
||||
|
||||
return chainReducers(INITIAL_API_STATE, fetchReducer, addReducer, updateReducer, deleteReducer);
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
export const isEmpty = (data: any): boolean => {
|
||||
return _.get(data, '_status.isEmpty', true);
|
||||
};
|
||||
|
||||
export const isLoading = (data: any): boolean => {
|
||||
return _.get(data, '_status.isLoading', false);
|
||||
};
|
||||
|
||||
export const isValid = (data: any): boolean => {
|
||||
return _.get(data, '_status.isValid', false);
|
||||
};
|
||||
|
||||
export const isError = (data: any): boolean => {
|
||||
return _.get(data, '_status.isError', false);
|
||||
};
|
||||
|
||||
export const getError = (data: any): any => {
|
||||
return _.get(data, '_status.error', null);
|
||||
};
|
||||
|
||||
export const getErrorMessage = (data: any): string | null => {
|
||||
return _.get(data, '_status.errorMessage', null);
|
||||
};
|
||||
|
||||
export const getTotalPages = (data: any): number | undefined => {
|
||||
return _.get(data, '_meta.totalPages', undefined);
|
||||
};
|
|
@ -1,29 +0,0 @@
|
|||
export interface StartAction {
|
||||
type: string;
|
||||
payload: any;
|
||||
}
|
||||
|
||||
export interface SuccessAction {
|
||||
type: string;
|
||||
payload: any;
|
||||
}
|
||||
|
||||
export interface FailureAction {
|
||||
type: string;
|
||||
payload: { error: string };
|
||||
}
|
||||
|
||||
export interface ApiStatus {
|
||||
isEmpty: boolean;
|
||||
isLoading: boolean;
|
||||
isValid: boolean;
|
||||
isError: boolean;
|
||||
dateTime: number;
|
||||
error: any;
|
||||
errorMessage: string | null;
|
||||
}
|
||||
|
||||
export interface ApiState<T = any> {
|
||||
_status: ApiStatus;
|
||||
items: T[];
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
import { Method } from 'axios';
|
||||
|
||||
export interface ApiConfig {
|
||||
hostname?: string;
|
||||
path: string;
|
||||
method?: Method;
|
||||
headers?: any;
|
||||
formData?: any;
|
||||
body?: any;
|
||||
additionalData?: any;
|
||||
}
|
||||
|
||||
export interface SimpleSnackBarConfig {
|
||||
serviceName: any;
|
||||
}
|
||||
|
||||
export interface SnackBarConfigWithMessages {
|
||||
getMessage: {
|
||||
success(response: any): string;
|
||||
error(error: any): string;
|
||||
};
|
||||
}
|
||||
|
||||
export type SnackBarConfig = SnackBarConfigWithMessages | SimpleSnackBarConfig;
|
||||
|
||||
export enum SortDirection {
|
||||
Ascending = 'asc',
|
||||
Descending = 'desc',
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
import { performApiCall } from 'src/services/api';
|
||||
|
||||
export const sendPasswordResetLink = async (email: string): Promise<boolean> => {
|
||||
try {
|
||||
await performApiCall({
|
||||
path: `/auth/forgot-password`,
|
||||
method: 'POST',
|
||||
body: { email },
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const resetPassword = async (email: string, code: string, password: string): Promise<boolean> => {
|
||||
try {
|
||||
await performApiCall({
|
||||
path: `/auth/reset-password`,
|
||||
method: 'POST',
|
||||
body: { email, code, password },
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
export const verifyEmail = async (email: string, activationCode: string): Promise<boolean> => {
|
||||
try {
|
||||
await performApiCall({
|
||||
path: `/auth/activation`,
|
||||
method: 'POST',
|
||||
body: { email, activationCode },
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
export { useAuth } from './use-auth';
|
|
@ -1,29 +0,0 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { getAuthToken, getCurrentUser, getIsAdmin, signIn, signOut } from '../redux';
|
||||
|
||||
export function useAuth() {
|
||||
const dispatch = useDispatch();
|
||||
const currentUser = useSelector(getCurrentUser);
|
||||
const authToken = useSelector(getAuthToken);
|
||||
const isAdmin = useSelector(getIsAdmin);
|
||||
|
||||
const logIn = useCallback(
|
||||
(params) => {
|
||||
return dispatch(signIn(params));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const logOut = useCallback(() => {
|
||||
return dispatch(signOut());
|
||||
}, [dispatch]);
|
||||
|
||||
return {
|
||||
authToken,
|
||||
currentUser,
|
||||
isAdmin,
|
||||
logIn,
|
||||
logOut,
|
||||
};
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export { useAuth } from './hooks';
|
||||
export { getAuth, reducer, signIn, signOut, AuthActionTypes, getIsAuthLoading } from './redux';
|
||||
export * from './types';
|
|
@ -1,32 +0,0 @@
|
|||
import { createApiAction } from 'src/services/api';
|
||||
import { SuccessAction } from 'src/services/api/redux/types';
|
||||
|
||||
export enum AuthActionTypes {
|
||||
SIGN_IN_START = 'auth/sign_in_start',
|
||||
SIGN_IN_SUCCESS = 'auth/sign_in_success',
|
||||
SIGN_IN_FAILURE = 'auth/sign_in_failure',
|
||||
SIGN_OUT = 'auth/SIGN_OUT',
|
||||
UPDATE_AUTH_USER = 'auth/update_auth_user',
|
||||
REGISTRATION_START = 'auth/registration_start',
|
||||
REGISTRATION_FAILURE = 'auth/registration_failure',
|
||||
}
|
||||
|
||||
const signOutAction = (): SuccessAction => ({
|
||||
type: AuthActionTypes.SIGN_OUT,
|
||||
payload: null,
|
||||
});
|
||||
|
||||
export const signIn = (params: string) =>
|
||||
createApiAction(
|
||||
{
|
||||
path: `/hydra/callback${params}`,
|
||||
method: 'GET',
|
||||
},
|
||||
[AuthActionTypes.SIGN_IN_START, AuthActionTypes.SIGN_IN_SUCCESS, AuthActionTypes.SIGN_IN_FAILURE],
|
||||
);
|
||||
|
||||
export function signOut() {
|
||||
return (dispatch: any) => {
|
||||
dispatch(signOutAction());
|
||||
};
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
export { signIn, signOut, AuthActionTypes } from './actions';
|
||||
export { default as reducer } from './reducers';
|
||||
export { getAuth, getIsAuthLoading, getAuthToken, getCurrentUser, getIsAdmin } from './selectors';
|
||||
export * from './types';
|
|
@ -1,47 +0,0 @@
|
|||
import { createApiReducer, chainReducers, INITIAL_API_STATUS } from 'src/services/api';
|
||||
|
||||
import { User } from 'src/services/users';
|
||||
import { AuthState } from './types';
|
||||
import { AuthActionTypes } from './actions';
|
||||
import { transformAuthUser } from '../transformations';
|
||||
|
||||
const initialCurrentUserState: User = {
|
||||
email: '',
|
||||
name: '',
|
||||
id: '',
|
||||
app_roles: [],
|
||||
status: '',
|
||||
preferredUsername: '',
|
||||
};
|
||||
|
||||
const initialState: AuthState = {
|
||||
token: null,
|
||||
userInfo: initialCurrentUserState,
|
||||
_status: INITIAL_API_STATUS,
|
||||
};
|
||||
|
||||
const authLocalReducer = (state: any = initialState, action: any) => {
|
||||
switch (action.type) {
|
||||
case AuthActionTypes.UPDATE_AUTH_USER:
|
||||
return {
|
||||
...state,
|
||||
userInfo: action.payload,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const auth = createApiReducer(
|
||||
[AuthActionTypes.SIGN_IN_START, AuthActionTypes.SIGN_IN_SUCCESS, AuthActionTypes.SIGN_IN_FAILURE],
|
||||
(data) => transformAuthUser(data),
|
||||
(data) => data.error.message,
|
||||
);
|
||||
|
||||
const signOut = createApiReducer(
|
||||
['', AuthActionTypes.SIGN_OUT, ''],
|
||||
() => initialState,
|
||||
() => initialState,
|
||||
);
|
||||
|
||||
export default chainReducers(initialState, auth, signOut, authLocalReducer);
|
|
@ -1,28 +0,0 @@
|
|||
import { State } from 'src/redux';
|
||||
|
||||
import { isLoading } from 'src/services/api';
|
||||
import { UserRole } from 'src/services/users';
|
||||
|
||||
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 getIsAdmin = (state: State) => {
|
||||
// check since old users wont have this
|
||||
if (state.auth.userInfo) {
|
||||
if (!state.auth.userInfo.app_roles) {
|
||||
return false;
|
||||
}
|
||||
const isAdmin = state.auth.userInfo.app_roles.find(
|
||||
(role) => role.name === 'dashboard' && role.role === UserRole.Admin,
|
||||
);
|
||||
return !!isAdmin;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const getIsAuthLoading = (state: State) => isLoading(getAuth(state));
|
||||
|
||||
export const getToken = (state: State) => state.auth.token;
|
|
@ -1,7 +0,0 @@
|
|||
import { ApiStatus } from 'src/services/api/redux/types';
|
||||
|
||||
import { Auth } from '../types';
|
||||
|
||||
export interface AuthState extends Auth {
|
||||
_status: ApiStatus;
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
import { User } from '../users';
|
||||
import { Auth } from './types';
|
||||
import { transformAppRoles } from '../users/transformations';
|
||||
|
||||
const transformUser = (response: any): User => {
|
||||
return {
|
||||
id: response.id ?? '',
|
||||
app_roles: response.app_roles ? response.app_roles.map(transformAppRoles) : [],
|
||||
email: response.email ?? '',
|
||||
name: response.name ?? '',
|
||||
preferredUsername: response.preferredUsername ?? '',
|
||||
status: response.state ?? '',
|
||||
};
|
||||
};
|
||||
|
||||
export const transformAuthUser = (response: any): Auth => {
|
||||
return {
|
||||
token: response.accessToken,
|
||||
userInfo: response.userInfo ? transformUser(response.userInfo) : null,
|
||||
};
|
||||
};
|
|
@ -1,6 +0,0 @@
|
|||
import { User } from '../users';
|
||||
|
||||
export interface Auth {
|
||||
token: string | null;
|
||||
userInfo: User | null;
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
import { performApiCall } from 'src/services/api';
|
||||
|
||||
import { User } from './types';
|
||||
import { transformUser } from './transformations';
|
||||
|
||||
export const fetchMemberDetails = async (memberId: number): Promise<User> => {
|
||||
const res = await performApiCall({
|
||||
path: `/members/${memberId}`,
|
||||
});
|
||||
return transformUser(res.data);
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
export { useUsers } from './use-users';
|
|
@ -1,74 +0,0 @@
|
|||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
getUsers,
|
||||
fetchUsers,
|
||||
fetchUserById,
|
||||
fetchPersonalInfo,
|
||||
updateUserById,
|
||||
updatePersonalInfo,
|
||||
createUser,
|
||||
deleteUser,
|
||||
clearCurrentUser,
|
||||
createBatchUsers,
|
||||
} from '../redux';
|
||||
import { getUserById, getUserModalLoading, getUserslLoading } from '../redux/selectors';
|
||||
|
||||
export function useUsers() {
|
||||
const dispatch = useDispatch();
|
||||
const users = useSelector(getUsers);
|
||||
const user = useSelector(getUserById);
|
||||
const userModalLoading = useSelector(getUserModalLoading);
|
||||
const userTableLoading = useSelector(getUserslLoading);
|
||||
|
||||
function loadUsers() {
|
||||
return dispatch(fetchUsers());
|
||||
}
|
||||
|
||||
function loadUser(id: string) {
|
||||
return dispatch(fetchUserById(id));
|
||||
}
|
||||
|
||||
function loadPersonalInfo() {
|
||||
return dispatch(fetchPersonalInfo());
|
||||
}
|
||||
|
||||
function clearSelectedUser() {
|
||||
return dispatch(clearCurrentUser());
|
||||
}
|
||||
|
||||
function editUserById(data: any) {
|
||||
return dispatch(updateUserById(data));
|
||||
}
|
||||
|
||||
function editPersonalInfo(data: any) {
|
||||
return dispatch(updatePersonalInfo(data));
|
||||
}
|
||||
|
||||
function createNewUser(data: any) {
|
||||
return dispatch(createUser(data));
|
||||
}
|
||||
|
||||
function createUsers(data: any) {
|
||||
return dispatch(createBatchUsers(data));
|
||||
}
|
||||
|
||||
function deleteUserById(id: string) {
|
||||
return dispatch(deleteUser(id));
|
||||
}
|
||||
|
||||
return {
|
||||
users,
|
||||
user,
|
||||
loadUser,
|
||||
loadUsers,
|
||||
loadPersonalInfo,
|
||||
editUserById,
|
||||
editPersonalInfo,
|
||||
userModalLoading,
|
||||
userTableLoading,
|
||||
createNewUser,
|
||||
deleteUserById,
|
||||
clearSelectedUser,
|
||||
createUsers,
|
||||
};
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
export * from './types';
|
||||
|
||||
export { reducer } from './redux';
|
||||
|
||||
export { useUsers } from './hooks';
|
||||
|
||||
export { fetchMemberDetails } from './api';
|
|
@ -1,255 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import { Dispatch } from 'redux';
|
||||
import { showToast, ToastType } from 'src/common/util/show-toast';
|
||||
import { State } from 'src/redux/types';
|
||||
import { performApiCall } from 'src/services/api';
|
||||
import { AuthActionTypes } from 'src/services/auth';
|
||||
import {
|
||||
transformBatchResponse,
|
||||
transformRequestMultipleUsers,
|
||||
transformRequestUser,
|
||||
transformUser,
|
||||
} from '../transformations';
|
||||
|
||||
export enum UserActionTypes {
|
||||
FETCH_USERS = 'users/fetch_users',
|
||||
FETCH_USER = 'users/fetch_user',
|
||||
UPDATE_USER = 'users/update_user',
|
||||
CREATE_USER = 'users/create_user',
|
||||
DELETE_USER = 'users/delete_user',
|
||||
SET_USER_MODAL_LOADING = 'users/user_modal_loading',
|
||||
SET_USERS_LOADING = 'users/users_loading',
|
||||
CREATE_BATCH_USERS = 'users/create_batch_users',
|
||||
}
|
||||
|
||||
export const setUsersLoading = (isLoading: boolean) => (dispatch: Dispatch<any>) => {
|
||||
dispatch({
|
||||
type: UserActionTypes.SET_USERS_LOADING,
|
||||
payload: isLoading,
|
||||
});
|
||||
};
|
||||
|
||||
export const setUserModalLoading = (isLoading: boolean) => (dispatch: Dispatch<any>) => {
|
||||
dispatch({
|
||||
type: UserActionTypes.SET_USER_MODAL_LOADING,
|
||||
payload: isLoading,
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchUsers = () => async (dispatch: Dispatch<any>) => {
|
||||
dispatch(setUsersLoading(true));
|
||||
|
||||
try {
|
||||
const { data } = await performApiCall({
|
||||
path: '/users',
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: UserActionTypes.FETCH_USERS,
|
||||
payload: data.map(transformUser),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
dispatch(setUsersLoading(false));
|
||||
};
|
||||
|
||||
export const fetchUserById = (id: string) => async (dispatch: Dispatch<any>) => {
|
||||
dispatch(setUserModalLoading(true));
|
||||
|
||||
try {
|
||||
const { data } = await performApiCall({
|
||||
path: `/users/${id}`,
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: UserActionTypes.FETCH_USER,
|
||||
payload: transformUser(data),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
dispatch(setUserModalLoading(false));
|
||||
};
|
||||
|
||||
export const fetchPersonalInfo = () => async (dispatch: Dispatch<any>) => {
|
||||
dispatch(setUserModalLoading(true));
|
||||
|
||||
try {
|
||||
const { data } = await performApiCall({
|
||||
path: '/me',
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: UserActionTypes.FETCH_USER,
|
||||
payload: transformUser(data),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
dispatch(setUserModalLoading(false));
|
||||
};
|
||||
|
||||
export const updateUserById = (user: any) => async (dispatch: Dispatch<any>, getState: any) => {
|
||||
dispatch(setUserModalLoading(true));
|
||||
|
||||
const state: State = getState();
|
||||
|
||||
try {
|
||||
const { data } = await performApiCall({
|
||||
path: `/users/${user.id}`,
|
||||
method: 'PUT',
|
||||
body: transformRequestUser(user),
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: UserActionTypes.UPDATE_USER,
|
||||
payload: transformUser(data),
|
||||
});
|
||||
|
||||
if (state.auth.userInfo?.id === user.id) {
|
||||
dispatch({
|
||||
type: AuthActionTypes.UPDATE_AUTH_USER,
|
||||
payload: transformUser(data),
|
||||
});
|
||||
}
|
||||
|
||||
showToast('User updated successfully.', ToastType.Success);
|
||||
|
||||
dispatch(fetchUsers());
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
dispatch(setUserModalLoading(false));
|
||||
};
|
||||
|
||||
export const updatePersonalInfo = (user: any) => async (dispatch: Dispatch<any>) => {
|
||||
dispatch(setUserModalLoading(true));
|
||||
|
||||
try {
|
||||
const { data } = await performApiCall({
|
||||
path: '/me',
|
||||
method: 'PUT',
|
||||
body: transformRequestUser(user),
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: UserActionTypes.UPDATE_USER,
|
||||
payload: transformUser(data),
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: AuthActionTypes.UPDATE_AUTH_USER,
|
||||
payload: transformUser(data),
|
||||
});
|
||||
|
||||
showToast('Personal information updated successfully.', ToastType.Success);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
dispatch(setUserModalLoading(false));
|
||||
};
|
||||
|
||||
export const createUser = (user: any) => async (dispatch: Dispatch<any>) => {
|
||||
dispatch(setUserModalLoading(true));
|
||||
|
||||
try {
|
||||
const { data } = await performApiCall({
|
||||
path: '/users',
|
||||
method: 'POST',
|
||||
body: transformRequestUser(user),
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: UserActionTypes.CREATE_USER,
|
||||
payload: transformUser(data),
|
||||
});
|
||||
|
||||
showToast('User created successfully.', ToastType.Success);
|
||||
|
||||
dispatch(fetchUsers());
|
||||
} catch (err: any) {
|
||||
dispatch(setUserModalLoading(false));
|
||||
showToast(`${err}`, ToastType.Error);
|
||||
throw err;
|
||||
}
|
||||
|
||||
dispatch(setUserModalLoading(false));
|
||||
};
|
||||
|
||||
export const deleteUser = (id: string) => async (dispatch: Dispatch<any>) => {
|
||||
dispatch(setUserModalLoading(true));
|
||||
|
||||
try {
|
||||
await performApiCall({
|
||||
path: `/users/${id}`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: UserActionTypes.DELETE_USER,
|
||||
payload: {},
|
||||
});
|
||||
|
||||
showToast('User deleted successfully.', ToastType.Success);
|
||||
|
||||
dispatch(fetchUsers());
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
dispatch(setUserModalLoading(false));
|
||||
};
|
||||
|
||||
export const createBatchUsers = (users: any) => async (dispatch: Dispatch<any>) => {
|
||||
dispatch(setUserModalLoading(true));
|
||||
|
||||
try {
|
||||
const { data } = await performApiCall({
|
||||
path: '/users-batch',
|
||||
method: 'POST',
|
||||
body: transformRequestMultipleUsers(users),
|
||||
});
|
||||
|
||||
const responseData = transformBatchResponse(data);
|
||||
|
||||
dispatch({
|
||||
type: UserActionTypes.CREATE_BATCH_USERS,
|
||||
payload: responseData,
|
||||
});
|
||||
|
||||
// show information about created users
|
||||
if (!_.isEmpty(responseData.success)) {
|
||||
showToast(responseData.success.message, ToastType.Success, Infinity);
|
||||
}
|
||||
if (!_.isEmpty(responseData.existing)) {
|
||||
showToast(responseData.existing.message, ToastType.Error, Infinity);
|
||||
}
|
||||
if (!_.isEmpty(responseData.failed)) {
|
||||
showToast(responseData.failed.message, ToastType.Error, Infinity);
|
||||
}
|
||||
|
||||
dispatch(fetchUsers());
|
||||
} catch (err: any) {
|
||||
dispatch(setUserModalLoading(false));
|
||||
showToast(`${err}`, ToastType.Error);
|
||||
throw err;
|
||||
}
|
||||
|
||||
dispatch(setUserModalLoading(false));
|
||||
};
|
||||
|
||||
export const clearCurrentUser = () => (dispatch: Dispatch<any>) => {
|
||||
dispatch({
|
||||
type: UserActionTypes.DELETE_USER,
|
||||
payload: {},
|
||||
});
|
||||
};
|
|
@ -1,4 +0,0 @@
|
|||
export * from './actions';
|
||||
export { default as reducer } from './reducers';
|
||||
export { getUsers } from './selectors';
|
||||
export * from './types';
|
|
@ -1,46 +0,0 @@
|
|||
import { UserActionTypes } from './actions';
|
||||
|
||||
const initialUsersState: any = {
|
||||
users: [],
|
||||
user: {},
|
||||
userModalLoading: false,
|
||||
usersLoading: false,
|
||||
};
|
||||
|
||||
const usersReducer = (state: any = initialUsersState, action: any) => {
|
||||
switch (action.type) {
|
||||
case UserActionTypes.FETCH_USERS:
|
||||
return {
|
||||
...state,
|
||||
users: action.payload,
|
||||
};
|
||||
case UserActionTypes.SET_USER_MODAL_LOADING:
|
||||
return {
|
||||
...state,
|
||||
userModalLoading: action.payload,
|
||||
};
|
||||
case UserActionTypes.SET_USERS_LOADING:
|
||||
return {
|
||||
...state,
|
||||
usersLoading: action.payload,
|
||||
};
|
||||
case UserActionTypes.FETCH_USER:
|
||||
case UserActionTypes.UPDATE_USER:
|
||||
case UserActionTypes.CREATE_USER:
|
||||
case UserActionTypes.CREATE_BATCH_USERS:
|
||||
return {
|
||||
...state,
|
||||
isModalVisible: false,
|
||||
user: action.payload,
|
||||
};
|
||||
case UserActionTypes.DELETE_USER:
|
||||
return {
|
||||
...state,
|
||||
user: {},
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default usersReducer;
|
|
@ -1,6 +0,0 @@
|
|||
import { State } from 'src/redux';
|
||||
|
||||
export const getUsers = (state: State) => state.users.users;
|
||||
export const getUserById = (state: State) => state.users.user;
|
||||
export const getUserModalLoading = (state: State) => state.users.userModalLoading;
|
||||
export const getUserslLoading = (state: State) => state.users.usersLoading;
|
|
@ -1,26 +0,0 @@
|
|||
import { ApiStatus } from 'src/services/api/redux';
|
||||
|
||||
import { User } from '../types';
|
||||
|
||||
export interface CurrentUserState extends User {
|
||||
_status: ApiStatus;
|
||||
}
|
||||
|
||||
export interface UsersState {
|
||||
currentUser: CurrentUserState;
|
||||
users: User[];
|
||||
user: User;
|
||||
userModalLoading: boolean;
|
||||
usersLoading: boolean;
|
||||
}
|
||||
|
||||
export interface CurrentUserUpdateAPI {
|
||||
id: number;
|
||||
phoneNumber?: string;
|
||||
email?: string;
|
||||
language?: string;
|
||||
country?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
password?: string;
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import { AppRoles, MultipleUsersData, User, UserRole } from './types';
|
||||
|
||||
const transformRoleById = (roleId: any): UserRole => {
|
||||
switch (roleId) {
|
||||
case 1:
|
||||
return UserRole.Admin;
|
||||
case 2:
|
||||
return UserRole.User;
|
||||
case 3:
|
||||
return UserRole.NoAccess;
|
||||
default:
|
||||
return UserRole.NoAccess;
|
||||
}
|
||||
};
|
||||
|
||||
const transformRoleIdByRole = (role: UserRole | null): number | null => {
|
||||
switch (role) {
|
||||
case UserRole.Admin:
|
||||
return 1;
|
||||
case UserRole.User:
|
||||
return 2;
|
||||
case UserRole.NoAccess:
|
||||
return 3;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const transformAppRoles = (data: any): AppRoles => {
|
||||
const userRole = transformRoleById(data.role_id);
|
||||
|
||||
return {
|
||||
name: data.name ?? '',
|
||||
role: userRole,
|
||||
};
|
||||
};
|
||||
|
||||
export const transformRequestAppRoles = (data: AppRoles): any => {
|
||||
const resolvedRequestRole = transformRoleIdByRole(data.role) ?? null;
|
||||
|
||||
return {
|
||||
name: data.name ?? '',
|
||||
role_id: resolvedRequestRole,
|
||||
};
|
||||
};
|
||||
|
||||
export const transformUser = (response: any): User => {
|
||||
return {
|
||||
id: response.id ?? '',
|
||||
app_roles: response.traits.app_roles ? response.traits.app_roles.map(transformAppRoles) : [],
|
||||
email: response.traits.email ?? '',
|
||||
name: response.traits.name ?? '',
|
||||
preferredUsername: response.preferredUsername ?? '',
|
||||
status: response.state ?? '',
|
||||
};
|
||||
};
|
||||
|
||||
export const transformRequestUser = (data: Pick<User, 'app_roles' | 'name' | 'email'>) => {
|
||||
return {
|
||||
app_roles: data.app_roles.map(transformRequestAppRoles),
|
||||
email: data.email ?? '',
|
||||
name: data.name ?? '',
|
||||
};
|
||||
};
|
||||
|
||||
const extractUsersFromCsv = (csvData: string) => {
|
||||
const csvRows = csvData.split('\n');
|
||||
|
||||
return _.map(csvRows, (row) => {
|
||||
const values = row.split(',');
|
||||
const email = values[0].trim();
|
||||
const name = !_.isNil(values[1]) ? values[1].trim() : '';
|
||||
return { email, name, app_roles: [] };
|
||||
});
|
||||
};
|
||||
|
||||
export const transformRequestMultipleUsers = (data: MultipleUsersData) => {
|
||||
const batchUsers = extractUsersFromCsv(data.csvUserData);
|
||||
return {
|
||||
users: _.map(batchUsers, (user) =>
|
||||
transformRequestUser({ app_roles: data.appRoles, name: user.name, email: user.email } as User),
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export const transformBatchResponse = (response: any): any => {
|
||||
return {
|
||||
success: response.success,
|
||||
existing: response.existing,
|
||||
failed: response.failed,
|
||||
};
|
||||
};
|
|
@ -1,36 +0,0 @@
|
|||
export interface User {
|
||||
id: string;
|
||||
app_roles: AppRoles[];
|
||||
email: string;
|
||||
name: string;
|
||||
preferredUsername: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface FormUser extends User {
|
||||
password?: string;
|
||||
confirmPassword?: string;
|
||||
}
|
||||
|
||||
export enum UserRole {
|
||||
NoAccess = 'no_access',
|
||||
Admin = 'admin',
|
||||
User = 'user',
|
||||
}
|
||||
|
||||
export interface AppRoles {
|
||||
name: string | null;
|
||||
role: UserRole | null;
|
||||
}
|
||||
|
||||
export interface UserApiRequest {
|
||||
id: number | null;
|
||||
email: string;
|
||||
name: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface MultipleUsersData {
|
||||
csvUserData: string;
|
||||
appRoles: AppRoles[];
|
||||
}
|
Loading…
Reference in a new issue