Skip to content

Commit

Permalink
[NGO Admin] Move the form for adding individual observers into a modal (
Browse files Browse the repository at this point in the history
#791)

* fix: make dropdown menus scrollable

* fix: truncate overflowing table columns

* Squashed commit of the following:

commit 742f250
Author: imdeaconu <[email protected]>
Date:   Wed Sep 11 19:54:55 2024 +0300

    add read notification checkmark

commit ea11fa0
Author: imdeaconu <[email protected]>
Date:   Wed Sep 11 19:54:30 2024 +0300

    add read notification column

* Squashed commit of the following:

commit d8833dc
Author: imdeaconu <[email protected]>
Date:   Fri Sep 13 13:29:31 2024 +0300

    WIP: add selector functionality

commit 3608c0e
Author: imdeaconu <[email protected]>
Date:   Fri Sep 13 10:00:05 2024 +0300

    WIP: create new tags input

* chore: remove unused import

* chore: delete duplicated / unused classes

* feature: add searching to MonitoringObserversTagFilter

* chore: update config files

* Revert "[NGO Admin] Rewrite the tag selector component  (#675)"

This reverts commit 2ad0e90.

* Merge branch 'main' of https://github.com/commitglobal/votemonitor into commitglobal-main

* move the create observer form into a modal

* remove create observer route
  • Loading branch information
imdeaconu authored Oct 22, 2024
1 parent 1c85478 commit efa3813
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 174 deletions.
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
import { authApi } from '@/common/auth-api';
import Layout from '@/components/layout/Layout';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogTitle } from '@/components/ui/dialog';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Separator } from '@/components/ui/separator';
import TagsSelectFormField from '@/components/ui/tag-selector';
import { toast } from '@/components/ui/use-toast';
import { useCurrentElectionRoundStore } from '@/context/election-round.store';
import { useMonitoringObserversTags } from '@/hooks/tags-queries';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useNavigate } from '@tanstack/react-router';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { MonitorObserverBackButton } from './MonitoringObserverBackButton';
import { monitoringObserversKeys } from '../hooks/monitoring-observers-queries';
import { monitoringObserversKeys } from '../../hooks/monitoring-observers-queries';

export default function CreateMonitoringObserver() {
export interface CreateMonitoringObserverDialogProps {
open: boolean;
onOpenChange: (open: any) => void;
}

function CreateMonitoringObserverDialog({ open, onOpenChange }: CreateMonitoringObserverDialogProps) {
const { t } = useTranslation('translation', { keyPrefix: 'observers.addObserver' });
const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId);
const { data: availableTags } = useMonitoringObserversTags(currentElectionRoundId);
const navigate = useNavigate();
const queryClient = useQueryClient();
const newObserverSchema = z.object({
firstName: z.string(),
Expand All @@ -47,18 +47,15 @@ export default function CreateMonitoringObserver() {
return authApi.post(`/election-rounds/${electionRoundId}/monitoring-observers`, { observers: [values] });
},

onSuccess: (_, {electionRoundId}) => {
onSuccess: (_, { electionRoundId }) => {
toast({
title: 'Success',
description: t('onSuccess'),
});

queryClient.invalidateQueries({ queryKey: monitoringObserversKeys.all(electionRoundId) });

navigate({
to: '/monitoring-observers/$tab',
params: { tab: 'list' },
});
form.reset({});
onOpenChange(false);
},
onError: () => {
toast({
Expand All @@ -68,16 +65,19 @@ export default function CreateMonitoringObserver() {
});
},
});

return (
<Layout title={''} backButton={<MonitorObserverBackButton />} enableBreadcrumbs={false}>
<Card className='w-[800px] pt-0'>
<CardHeader className='flex gap-2 flex-column'>
<div className='flex flex-row items-center justify-between'>
<CardTitle className='text-xl'>{t('title')}</CardTitle>
</div>
<Separator />
</CardHeader>
<CardContent>
<Dialog open={open} onOpenChange={onOpenChange} modal={true}>
<DialogContent
className='min-w-[650px] min-h-[350px]'
onInteractOutside={(e) => {
e.preventDefault();
}}
onEscapeKeyDown={(e) => {
e.preventDefault();
}}>
<DialogTitle className='mb-3.5'>{t('title')}</DialogTitle>
<div className='flex flex-col gap-3'>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'>
<FormField
Expand Down Expand Up @@ -147,29 +147,22 @@ export default function CreateMonitoringObserver() {
)}
/>

<div className='flex justify-between'>
<div className='flex gap-2'>
<Button
variant='outline'
type='button'
onClick={() => {
void navigate({
to: '/monitoring-observers/$tab',
params: { tab: 'list' },
});
}}>
<DialogFooter>
<DialogClose asChild>
<Button className='text-purple-900 border border-purple-900 border-input bg-background hover:bg-purple-50 hover:text-purple-600'>
Cancel
</Button>

<Button title={t('addBtnText')} type='submit' className='px-6'>
{t('addBtnText')}
</Button>
</div>
</div>
</DialogClose>
<Button title={t('addBtnText')} type='submit' className='px-6'>
{t('addBtnText')}
</Button>
</DialogFooter>
</form>
</Form>
</CardContent>
</Card>
</Layout>
</div>
</DialogContent>
</Dialog>
);
}

export default CreateMonitoringObserverDialog;
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { Separator } from '@/components/ui/separator';
import { useDialog } from '@/components/ui/use-dialog';
import { Cog8ToothIcon, EllipsisVerticalIcon, FunnelIcon, PaperAirplaneIcon } from '@heroicons/react/24/outline';
import { useMutation } from '@tanstack/react-query';
import { Link, useNavigate, useRouter } from '@tanstack/react-router';
import { useNavigate, useRouter } from '@tanstack/react-router';
import { CellContext, ColumnDef } from '@tanstack/react-table';
import { useEffect, useMemo, useState } from 'react';

Expand All @@ -38,98 +38,101 @@ import { MonitoringObserver, MonitoringObserverStatus } from '../../models/monit
import ImportMonitoringObserversDialog from '../MonitoringObserversList/ImportMonitoringObserversDialog';
import ImportMonitoringObserversErrorsDialog from '../MonitoringObserversList/ImportMonitoringObserversErrorsDialog';
import ConfirmResendInvitationDialog from './ConfirmResendInvitationDialog';
import CreateMonitoringObserverDialog from './CreateMonitoringObserverDialog';

function MonitoringObserversList() {
const navigate = useNavigate();
const router = useRouter();
const search = Route.useSearch();
const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId);

const monitoringObserverColDefs: ColumnDef<MonitoringObserver>[] = useMemo(()=>{
const monitoringObserverColDefs: ColumnDef<MonitoringObserver>[] = useMemo(() => {
return [
{
header: ({ column }) => <DataTableColumnHeader title='Name' column={column} />,
accessorKey: 'name',
enableSorting: true,
enableGlobalFilter: true,
cell: ({
row: {
original: { firstName, lastName },
},
}) => (
<p>
{firstName} {lastName}
</p>
),
},
{
header: ({ column }) => <DataTableColumnHeader title='Email' column={column} />,
accessorKey: 'email',
enableSorting: true,
},
{
header: ({ column }) => <DataTableColumnHeader title='Observer tags' column={column} />,
accessorKey: 'tags',
cell: ({
row: {
original: { tags },
},
}) => <TableTagList tags={tags} />,
},
{
header: ({ column }) => <DataTableColumnHeader title='Phone' column={column} />,
accessorKey: 'phoneNumber',
enableSorting: true,
},
{
header: ({ column }) => <DataTableColumnHeader title='Observer status' column={column} />,
accessorKey: 'status',
enableSorting: true,
cell: ({
row: {
original: { status },
},
}) => <Badge className={'badge-' + status}>{status}</Badge>,
},
{
header: ({ column }) => <DataTableColumnHeader title='Latest activity at' column={column} />,
accessorKey: 'latestActivityAt',
enableSorting: true,
cell: ({
row: {
original: { latestActivityAt },
},
}) => <p>{latestActivityAt ? format(latestActivityAt, DateTimeFormat) : '-'}</p>,
},
{
header: '',
accessorKey: 'action',
enableSorting: true,
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger>
<EllipsisVerticalIcon className='w-[24px] h-[24px] text-purple-600' />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => navigateToObserver(row.original.id)}>View</DropdownMenuItem>
<DropdownMenuItem onClick={() => navigateToEdit(row.original.id)}>Edit</DropdownMenuItem>
<DropdownMenuItem
disabled={row.original.status !== MonitoringObserverStatus.Pending}
onClick={() => handleResendInviteToObserver(row.original.id)}>
Resend invitation email
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
},
];}, [currentElectionRoundId]);
{
header: ({ column }) => <DataTableColumnHeader title='Name' column={column} />,
accessorKey: 'name',
enableSorting: true,
enableGlobalFilter: true,
cell: ({
row: {
original: { firstName, lastName },
},
}) => (
<p>
{firstName} {lastName}
</p>
),
},
{
header: ({ column }) => <DataTableColumnHeader title='Email' column={column} />,
accessorKey: 'email',
enableSorting: true,
},
{
header: ({ column }) => <DataTableColumnHeader title='Observer tags' column={column} />,
accessorKey: 'tags',
cell: ({
row: {
original: { tags },
},
}) => <TableTagList tags={tags} />,
},
{
header: ({ column }) => <DataTableColumnHeader title='Phone' column={column} />,
accessorKey: 'phoneNumber',
enableSorting: true,
},
{
header: ({ column }) => <DataTableColumnHeader title='Observer status' column={column} />,
accessorKey: 'status',
enableSorting: true,
cell: ({
row: {
original: { status },
},
}) => <Badge className={'badge-' + status}>{status}</Badge>,
},
{
header: ({ column }) => <DataTableColumnHeader title='Latest activity at' column={column} />,
accessorKey: 'latestActivityAt',
enableSorting: true,
cell: ({
row: {
original: { latestActivityAt },
},
}) => <p>{latestActivityAt ? format(latestActivityAt, DateTimeFormat) : '-'}</p>,
},
{
header: '',
accessorKey: 'action',
enableSorting: true,
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger>
<EllipsisVerticalIcon className='w-[24px] h-[24px] text-purple-600' />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => navigateToObserver(row.original.id)}>View</DropdownMenuItem>
<DropdownMenuItem onClick={() => navigateToEdit(row.original.id)}>Edit</DropdownMenuItem>
<DropdownMenuItem
disabled={row.original.status !== MonitoringObserverStatus.Pending}
onClick={() => handleResendInviteToObserver(row.original.id)}>
Resend invitation email
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
},
];
}, [currentElectionRoundId]);

const [searchText, setSearchText] = useState(search.searchText);
const debouncedSearch = useDebounce(search, 300);
const debouncedSearchText = useDebounce(searchText, 300);

const [importErrorsFileId, setImportErrorsFileId] = useState<string | undefined>();
const [monitoringObserverId, setMonitoringObserverId] = useState<string | undefined>();
const createMonitoringObserverDialog = useDialog();
const importMonitoringObserversDialog = useDialog();
const importMonitoringObserverErrorsDialog = useDialog();
const confirmResendInvitesDialog = useDialog();
Expand All @@ -155,7 +158,7 @@ function MonitoringObserversList() {
const params = [
['status', debouncedSearch.monitoringObserverStatus],
['tags', debouncedSearch.tags],
['searchText', debouncedSearch.searchText]
['searchText', debouncedSearch.searchText],
].filter(([_, value]) => value);

return Object.fromEntries(params);
Expand Down Expand Up @@ -184,7 +187,7 @@ function MonitoringObserversList() {
});
},

onSuccess: (_, {electionRoundId}) => {
onSuccess: (_, { electionRoundId }) => {
queryClient.invalidateQueries({ queryKey: monitoringObserversKeys.all(electionRoundId) });
router.invalidate();

Expand Down Expand Up @@ -251,19 +254,13 @@ function MonitoringObserversList() {
<div className='flex flex-row items-center justify-between px-6'>
<CardTitle className='text-xl'>Monitoring observers list</CardTitle>
<div className='flex flex-row-reverse gap-4 table-actions flex-row-'>
<Link to='/monitoring-observers/new-observer'>
<Button>
<Plus className='mr-2' width={18} height={18} />
{i18n.t('observers.addObserver.addBtnText')}
</Button>
</Link>

{!!importErrorsFileId && (
<ImportMonitoringObserversErrorsDialog
fileId={importErrorsFileId}
{...importMonitoringObserverErrorsDialog.dialogProps}
/>
)}

<ImportMonitoringObserversDialog
{...importMonitoringObserversDialog.dialogProps}
onImportError={(fileId) => {
Expand Down Expand Up @@ -291,6 +288,12 @@ function MonitoringObserversList() {
</svg>
Import observer list
</Button>
<Button variant='secondary' onClick={() => createMonitoringObserverDialog.trigger()}>
<Plus className='mr-2' width={18} height={18} />
{i18n.t('observers.addObserver.addBtnText')}
</Button>
<CreateMonitoringObserverDialog {...createMonitoringObserverDialog.dialogProps} />

<Button
className='text-purple-900 bg-background hover:bg-purple-50 hover:text-purple-500'
onClick={exportMonitoringObservers}>
Expand Down
Loading

0 comments on commit efa3813

Please sign in to comment.