feat: change global role on administration page (#61)

* feat: change global role on administration page

* feat: change global role on administration page test code todo

* feat: change global role on administration page test code

* feat: change global role on administration page

* feat: change global role on administration page

* feat: change global role on administration page

* feat: change global role on administration page

* feat: change global role on administration page
This commit is contained in:
Takeshi Kimata
2024-09-29 20:26:18 +09:00
committed by GitHub
parent d0fcd5e77b
commit f7cc3d918f
13 changed files with 2746 additions and 30 deletions

View File

@@ -3,9 +3,15 @@ import React from 'react';
import { useState, useEffect, useContext } from 'react';
import { UserType, AdminMessages } from '@/types/user';
import { TokenContext } from '@/utils/TokenProvider';
import { ToastContext } from '@/utils/ToastProvider';
import { useRouter } from '@/src/navigation';
import UsersTable from './UsersTable';
import Config from '@/config/config';
import { LocaleCodeType } from '@/types/locale';
import { updateUserRole } from '@/utils/usersControl';
import { Button } from '@nextui-org/react';
import { roles } from '@/config/selection';
import DeleteConfirmDialog from '@/components/DeleteConfirmDialog';
const apiServer = Config.apiServer;
type Props = {
@@ -37,33 +43,96 @@ async function fetchUsers(jwt: string) {
}
export default function AdminPage({ messages, locale }: Props) {
const context = useContext(TokenContext);
const router = useRouter();
const tokenContext = useContext(TokenContext);
const toastContext = useContext(ToastContext);
const [users, setUsers] = useState<UserType[]>([]);
const [myself, setMyself] = useState<UserType | null>(null);
// Quit confirm dialog
const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false);
const closeConfirmDialog = () => {
setIsConfirmDialogOpen(false);
};
useEffect(() => {
async function fetchDataEffect() {
if (!context.isSignedIn()) {
if (!tokenContext.isAdmin()) {
return;
}
try {
const data = await fetchUsers(context.token.access_token);
const data = await fetchUsers(tokenContext.token.access_token);
setUsers(data);
if (tokenContext.token.user) {
setMyself(tokenContext.token.user);
}
} catch (error: any) {
console.error('Error in effect:', error.message);
}
}
fetchDataEffect();
}, [context]);
}, [tokenContext]);
const handleChangeRole = async (userEdit: UserType, role: number) => {
if (!tokenContext.isAdmin()) {
console.error('you are not admin');
return;
}
if (userEdit.id) {
const data = await updateUserRole(tokenContext.token.access_token, userEdit.id, role);
if (data.user) {
toastContext.showToast(messages.roleChanged, 'dark');
setUsers((prevUsers) => {
return prevUsers.map((user) => {
if (user.id === userEdit.id) {
return { ...user, role: role };
}
return user;
});
});
}
}
};
const onQuitConfirm = async () => {
if (myself && myself.id) {
const userRoleIndex = roles.findIndex((entry) => entry.uid === 'user');
const data = await updateUserRole(tokenContext.token.access_token, myself.id, userRoleIndex);
if (data && data.user) {
toastContext.showToast(messages.lostAdminAuth, 'dark');
router.push(`/`, { locale: locale });
} else {
toastContext.showToast(messages.atLeast, 'error');
}
}
};
return (
<div className="container mx-auto max-w-3xl pt-16 px-6 flex-grow">
<div className="w-full p-3 flex items-center justify-between">
<h3 className="font-bold">{messages.userManagement}</h3>
<>
<div className="container mx-auto max-w-3xl pt-16 px-6 flex-grow">
<div className="w-full p-3 flex items-center justify-between">
<h3 className="font-bold">{messages.userManagement}</h3>
</div>
<UsersTable users={users} myself={myself} onChangeRole={handleChangeRole} messages={messages} />
<Button className="mt-4" color="danger" variant="bordered" onPress={() => setIsConfirmDialogOpen(true)}>
{messages.quitAdmin}
</Button>
</div>
<UsersTable users={users} messages={messages} />
</div>
<DeleteConfirmDialog
isOpen={isConfirmDialogOpen}
onCancel={closeConfirmDialog}
onConfirm={onQuitConfirm}
closeText={messages.close}
confirmText={messages.quitConfirm}
deleteText={messages.quit}
/>
</>
);
}

View File

@@ -1,15 +1,31 @@
import { useState, useMemo, useCallback } from 'react';
import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, SortDescriptor } from '@nextui-org/react';
import { useState, useMemo } from 'react';
import {
Button,
Table,
TableHeader,
TableColumn,
TableBody,
TableRow,
TableCell,
SortDescriptor,
DropdownTrigger,
Dropdown,
DropdownMenu,
DropdownItem,
} from '@nextui-org/react';
import { ChevronDown } from 'lucide-react';
import { UserType, AdminMessages } from '@/types/user';
import { roles } from '@/config/selection';
import Avatar from 'boring-avatars';
type Props = {
users: UserType[];
myself: UserType | null;
onChangeRole: (userEdit: UserType, role: number) => void;
messages: AdminMessages;
};
export default function UsersTable({ users, messages }: Props) {
export default function UsersTable({ users, myself, onChangeRole, messages }: Props) {
const headerColumns = [
{ name: messages.avatar, uid: 'avatar', sortable: false },
{ name: messages.id, uid: 'id', sortable: true },
@@ -33,7 +49,15 @@ export default function UsersTable({ users, messages }: Props) {
});
}, [sortDescriptor, users]);
const renderCell = useCallback((user: UserType, columnKey: string) => {
const isMyself = (myself: UserType | null, user: UserType) => {
if (myself && myself.id === user.id) {
return true;
} else {
return false;
}
};
const renderCell = (user: UserType, columnKey: string) => {
const cellValue = user[columnKey as keyof UserType];
switch (columnKey) {
@@ -53,12 +77,32 @@ export default function UsersTable({ users, messages }: Props) {
case 'name':
return cellValue;
case 'role':
return <span>{messages[roles[cellValue as number].uid]}</span>;
return (
<Dropdown>
<DropdownTrigger>
<Button
size="sm"
isDisabled={isMyself(myself, user)}
variant="light"
endContent={<ChevronDown size={16} />}
>
<span className="w-20">{messages[roles[cellValue as number].uid]}</span>
</Button>
</DropdownTrigger>
<DropdownMenu aria-label="global role actions">
{roles.map((role, index) => (
<DropdownItem key={index} onPress={() => onChangeRole(user, index)}>
{messages[role.uid]}
</DropdownItem>
))}
</DropdownMenu>
</Dropdown>
);
default:
return cellValue;
}
}, []);
};
const classNames = useMemo(
() => ({

View File

@@ -2,11 +2,11 @@ import { useTranslations } from 'next-intl';
import AdminPage from './AdminPage';
import { PageType } from '@/types/base';
import { LocaleCodeType } from '@/types/locale';
import { AdminMessages } from '@/types/user';
export default function Page({ params }: PageType) {
const t = useTranslations('Admin');
const messages = {
admin: t('admin'),
const messages: AdminMessages = {
userManagement: t('user_management'),
avatar: t('avatar'),
id: t('id'),
@@ -16,6 +16,13 @@ export default function Page({ params }: PageType) {
administrator: t('administrator'),
user: t('user'),
noUsersFound: t('no_users_found'),
quitAdmin: t('quit_admin'),
quit: t('quit'),
quitConfirm: t('quit_confirm'),
close: t('close'),
roleChanged: t('role_changed'),
lostAdminAuth: t('lost_admin_auth'),
atLeast: t('at_least'),
};
return (

View File

@@ -53,7 +53,7 @@ export default function CaseEditor({
priorityMessages,
locale,
}: Props) {
const toakenContext = useContext(TokenContext);
const tokenContext = useContext(TokenContext);
const toastContext = useContext(ToastContext);
const [testCase, setTestCase] = useState<CaseType>(defaultTestCase);
const [isTitleInvalid, setIsTitleInvalid] = useState<boolean>(false);
@@ -194,11 +194,11 @@ export default function CaseEditor({
useEffect(() => {
async function fetchDataEffect() {
if (!toakenContext.isSignedIn()) {
if (!tokenContext.isSignedIn()) {
return;
}
try {
const data = await fetchCase(toakenContext.token.access_token, Number(caseId));
const data = await fetchCase(tokenContext.token.access_token, Number(caseId));
data.Steps.forEach((step: StepType) => {
step.editState = 'notChanged';
});
@@ -209,7 +209,7 @@ export default function CaseEditor({
}
fetchDataEffect();
}, [toakenContext]);
}, [tokenContext]);
return (
<>
@@ -232,14 +232,14 @@ export default function CaseEditor({
<Button
startContent={<Save size={16} />}
size="sm"
isDisabled={!toakenContext.isProjectDeveloper(Number(projectId))}
isDisabled={!tokenContext.isProjectDeveloper(Number(projectId))}
color="primary"
isLoading={isUpdating}
onPress={async () => {
setIsUpdating(true);
await updateCase(toakenContext.token.access_token, testCase);
await updateCase(tokenContext.token.access_token, testCase);
if (testCase.Steps) {
await updateSteps(toakenContext.token.access_token, Number(caseId), testCase.Steps);
await updateSteps(tokenContext.token.access_token, Number(caseId), testCase.Steps);
}
toastContext.showToast(messages.updatedTestCase, 'dark');
@@ -387,7 +387,7 @@ export default function CaseEditor({
<Button
startContent={<Plus size={16} />}
size="sm"
isDisabled={!toakenContext.isProjectDeveloper(Number(projectId))}
isDisabled={!tokenContext.isProjectDeveloper(Number(projectId))}
color="primary"
className="ms-3"
onPress={() => onPlusClick(1)}
@@ -397,7 +397,7 @@ export default function CaseEditor({
</div>
{testCase.Steps && (
<CaseStepsEditor
isDisabled={!toakenContext.isProjectDeveloper(Number(projectId))}
isDisabled={!tokenContext.isProjectDeveloper(Number(projectId))}
steps={testCase.Steps}
onStepUpdate={(stepId, changeStep) => {
if (testCase.Steps) {
@@ -425,7 +425,7 @@ export default function CaseEditor({
<h6 className="font-bold">{messages.attachments}</h6>
{testCase.Attachments && (
<CaseAttachmentsEditor
isDisabled={!toakenContext.isProjectDeveloper(Number(projectId))}
isDisabled={!tokenContext.isProjectDeveloper(Number(projectId))}
attachments={testCase.Attachments}
onAttachmentDownload={(attachmentId: number, downloadFileName: string) =>
fetchDownloadAttachment(attachmentId, downloadFileName)