Skip to content

Commit 90fa35c

Browse files
authored
[NGO Admin] Add form wizard (#811)
* fix: make dropdown menus scrollable * fix: truncate overflowing table columns * Squashed commit of the following: commit 742f250 Author: imdeaconu <imdeaconu@gmail.com> Date: Wed Sep 11 19:54:55 2024 +0300 add read notification checkmark commit ea11fa0 Author: imdeaconu <imdeaconu@gmail.com> Date: Wed Sep 11 19:54:30 2024 +0300 add read notification column * Squashed commit of the following: commit d8833dc Author: imdeaconu <imdeaconu@gmail.com> Date: Fri Sep 13 13:29:31 2024 +0300 WIP: add selector functionality commit 3608c0e Author: imdeaconu <imdeaconu@gmail.com> 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 * WIP: fix language selector * WIPȘ add forrm wizard start page * WIP: update FormBuilder * WIP: add FormBuilder screens and routes * WIP: move FormBuilder into the forms directory * WIP: add new form page instead of modal * WIP: fix Dashboard page imports * WIP: update fform edit route * Revert "resync branch" (#10) * Revert "Revert "resync branch" (#10)" (#11) This reverts commit aeefd9a. * fix Dashboard * readd updated on tooltip * WIP: add form template tables * WIP: fix Dashboard page * WIP: add form template preview * update preview template dialog * create new form from template preview * WIP: update hook for creating forms from teplates * WIP: create reusable components * WIP: hide form from form and add business logic * WIP: fix imports * WIP: fix imports and citizen reporting for createNewForm * fix unused import
1 parent a8d59bf commit 90fa35c

File tree

22 files changed

+1225
-32
lines changed

22 files changed

+1225
-32
lines changed

web/src/components/LanguageSelector/LanguageSelector.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
2-
import i18n from '@/i18n';
2+
import { useRouter } from '@tanstack/react-router';
33
import { FC } from 'react';
4+
import { useTranslation } from 'react-i18next';
45
import { Label } from '../ui/label';
56

67
export const LanguageSelector: FC = () => {
8+
const { i18n } = useTranslation(); // not passing any namespace will use the defaultNS (by default set to 'translation')
9+
const router = useRouter();
10+
711
const changeLanguage = (lng: string) => {
812
i18n.changeLanguage(lng);
13+
router.invalidate();
914
};
1015

1116
const options = [

web/src/components/layout/Layout.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import type { ReactNode } from 'react';
21
import type { FunctionComponent } from '@/common/types';
3-
import Breadcrumbs from './Breadcrumbs/Breadcrumbs';
2+
import type { ReactNode } from 'react';
43
import BackButton from './Breadcrumbs/BackButton';
4+
import Breadcrumbs from './Breadcrumbs/Breadcrumbs';
55

66
interface LayoutProps {
77
title?: string;
88
subtitle?: string;
99
enableBreadcrumbs?: boolean;
10+
enableBackButton?: boolean;
1011
breadcrumbs?: ReactNode;
1112
backButton?: ReactNode;
1213
actions?: ReactNode;
@@ -21,6 +22,7 @@ const Layout = ({
2122
breadcrumbs,
2223
children,
2324
enableBreadcrumbs = true,
25+
enableBackButton,
2426
}: LayoutProps): FunctionComponent => {
2527
return (
2628
<>
@@ -29,6 +31,7 @@ const Layout = ({
2931
{enableBreadcrumbs && (breadcrumbs || <Breadcrumbs />)}
3032
<h1 className='flex flex-row items-center gap-3 text-3xl font-bold tracking-tight text-gray-900'>
3133
{enableBreadcrumbs && (backButton || <BackButton />)}
34+
{enableBackButton && (backButton || <BackButton />)}
3235
{title}
3336
</h1>
3437
{subtitle ?? <h3 className='text-lg font-light'>{subtitle}</h3>}

web/src/features/forms/components/Dashboard/Dashboard.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { DataTableColumnHeader } from '@/components/ui/DataTable/DataTableColumn
66
import { QueryParamsDataTable } from '@/components/ui/DataTable/QueryParamsDataTable';
77
import { useConfirm } from '@/components/ui/alert-dialog-provider';
88
import { Badge } from '@/components/ui/badge';
9-
import { buttonVariants } from '@/components/ui/button';
9+
import { Button, buttonVariants } from '@/components/ui/button';
1010
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
1111
import {
1212
DropdownMenu,
@@ -31,17 +31,17 @@ import {
3131
EllipsisVerticalIcon,
3232
FunnelIcon,
3333
PhotoIcon,
34+
PlusIcon,
3435
} from '@heroicons/react/24/outline';
3536
import { useMutation } from '@tanstack/react-query';
36-
import { useNavigate, useRouter } from '@tanstack/react-router';
37+
import { Link, useNavigate, useRouter } from '@tanstack/react-router';
3738
import { ColumnDef, createColumnHelper, Row } from '@tanstack/react-table';
3839
import { useDebounce } from '@uidotdev/usehooks';
3940
import { format } from 'date-fns';
4041
import { useMemo, useState, type ReactElement } from 'react';
4142
import { FormBase, FormStatus } from '../../models/form';
4243
import { formsKeys, useForms } from '../../queries';
4344
import AddTranslationsDialog, { useAddTranslationsDialog } from './AddTranslationsDialog';
44-
import CreateForm from './CreateForm';
4545
import { FormFilters } from './FormFilters/FormFilters';
4646
import EditFormAccessDialog, { useEditFormAccessDialog } from './EditFormAccessDialog';
4747
import { useElectionRoundDetails } from '@/features/election-event/hooks/election-event-hooks';
@@ -725,9 +725,12 @@ export default function FormsDashboard(): ReactElement {
725725
{i18n.t('electionEvent.observerForms.cardTitle')}
726726
</div>
727727
<div>
728-
<CreateDialog title={i18n.t('electionEvent.observerForms.createDialogTitle')}>
729-
<CreateForm />
730-
</CreateDialog>
728+
<Link to='/forms/new'>
729+
<Button title='Create form' variant='default'>
730+
<PlusIcon className='w-5 h-5 mr-2 -ml-1.5' />
731+
<span>Create form</span>
732+
</Button>
733+
</Link>
731734
</div>
732735
</CardTitle>
733736
<Separator />

web/src/features/forms/components/EditForm/EditForm.tsx

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { QuestionType, ZFormType, type FunctionComponent } from '@/common/types';
1+
import { QuestionType, ZFormType } from '@/common/types';
22
import FormQuestionsEditor from '@/components/questionsEditor/FormQuestionsEditor';
33
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
44
import { Form } from '@/components/ui/form';
@@ -25,17 +25,12 @@ import { Button, buttonVariants } from '@/components/ui/button';
2525
import { LanguageBadge } from '@/components/ui/language-badge';
2626
import { toast } from '@/components/ui/use-toast';
2727
import { useCurrentElectionRoundStore } from '@/context/election-round.store';
28-
import {
29-
cn,
30-
ensureTranslatedStringCorrectness,
31-
isNilOrWhitespace,
32-
isNotNilOrWhitespace
33-
} from '@/lib/utils';
28+
import { cn, ensureTranslatedStringCorrectness, isNilOrWhitespace, isNotNilOrWhitespace } from '@/lib/utils';
3429
import { queryClient } from '@/main';
3530
import { Route } from '@/routes/forms_.$formId.edit';
3631
import { useMutation } from '@tanstack/react-query';
3732
import { useBlocker, useNavigate, useRouter } from '@tanstack/react-router';
38-
import { useEffect, useState } from 'react';
33+
import { FC, useEffect, useState } from 'react';
3934
import { UpdateFormRequest } from '../../models/form';
4035
import { formDetailsQueryOptions, formsKeys } from '../../queries';
4136
import {
@@ -49,7 +44,6 @@ import {
4944
ZEditQuestionType,
5045
ZTranslatedString,
5146
} from '../../types';
52-
import { FormDetailsBreadcrumbs } from '../FormDetailsBreadcrumbs/FormDetailsBreadcrumbs';
5347
import EditFormDetails from './EditFormDetails';
5448

5549
export const ZEditFormType = z
@@ -215,7 +209,11 @@ export const ZEditFormType = z
215209

216210
export type EditFormType = z.infer<typeof ZEditFormType>;
217211

218-
export default function EditForm(): FunctionComponent {
212+
interface EditFormProps {
213+
currentTab?: string;
214+
}
215+
216+
const EditForm: FC<EditFormProps> = ({ currentTab }) => {
219217
const { formId } = Route.useParams();
220218
const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId);
221219
const { data: formData } = useSuspenseQuery(formDetailsQueryOptions(currentElectionRoundId, formId));
@@ -383,7 +381,7 @@ export default function EditForm(): FunctionComponent {
383381
description: ensureTranslatedStringCorrectness(formData.description, formData.languages),
384382
formType: formData.formType,
385383
questions: editQuestions,
386-
icon: formData.icon ?? ''
384+
icon: formData.icon ?? '',
387385
},
388386
mode: 'all',
389387
});
@@ -423,7 +421,7 @@ export default function EditForm(): FunctionComponent {
423421
});
424422
},
425423

426-
onSuccess: async (_, { shouldExitEditor,electionRoundId }) => {
424+
onSuccess: async (_, { shouldExitEditor, electionRoundId }) => {
427425
toast({
428426
title: 'Success',
429427
description: 'Form updated successfully',
@@ -474,14 +472,21 @@ export default function EditForm(): FunctionComponent {
474472
}
475473
}, [form.formState.isSubmitSuccessful, form.reset]);
476474

475+
const [activeTab, setActiveTab] = useState(currentTab ?? 'form-details');
476+
477477
return (
478478
<Layout
479+
enableBackButton
479480
backButton={<NavigateBack to='/election-event/$tab' params={{ tab: 'observer-forms' }} />}
480-
breadcrumbs={<FormDetailsBreadcrumbs formCode={code} formName={name[languageCode] ?? ''} />}
481+
enableBreadcrumbs={false}
481482
title={`${code} - ${name[languageCode]}`}>
482483
<Form {...form}>
483484
<form onSubmit={form.handleSubmit(saveForm)} className='flex flex-col flex-1'>
484-
<Tabs className='flex flex-col flex-1' defaultValue='form-details'>
485+
<Tabs
486+
value={activeTab}
487+
onValueChange={setActiveTab}
488+
className='flex flex-col flex-1'
489+
defaultValue='form-details'>
485490
<TabsList className='grid grid-cols-2 bg-gray-200 w-[400px] mb-4'>
486491
<TabsTrigger
487492
value='form-details'
@@ -556,4 +561,6 @@ export default function EditForm(): FunctionComponent {
556561
</Form>
557562
</Layout>
558563
);
559-
}
564+
};
565+
566+
export default EditForm;
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import { authApi } from '@/common/auth-api';
2+
import { ZFormType } from '@/common/types';
3+
import { Button } from '@/components/ui/button';
4+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
5+
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
6+
import { Input } from '@/components/ui/input';
7+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
8+
import { Separator } from '@/components/ui/separator';
9+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
10+
import { Textarea } from '@/components/ui/textarea';
11+
import LanguageSelect from '@/containers/LanguageSelect';
12+
import { useCurrentElectionRoundStore } from '@/context/election-round.store';
13+
import { useElectionRoundDetails } from '@/features/election-event/hooks/election-event-hooks';
14+
import { FormBase, NewFormRequest } from '@/features/forms/models/form';
15+
import { formsKeys } from '@/features/forms/queries';
16+
import { cn, mapFormType, newTranslatedString } from '@/lib/utils';
17+
import { queryClient } from '@/main';
18+
import { zodResolver } from '@hookform/resolvers/zod';
19+
import { useMutation } from '@tanstack/react-query';
20+
import { useNavigate } from '@tanstack/react-router';
21+
import { FC } from 'react';
22+
import { useForm } from 'react-hook-form';
23+
import { useTranslation } from 'react-i18next';
24+
import { z } from 'zod';
25+
26+
export const CreateFormPage: FC = () => {
27+
const { t } = useTranslation();
28+
const navigate = useNavigate();
29+
const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId);
30+
const { data: electionRound } = useElectionRoundDetails(currentElectionRoundId);
31+
32+
const newFormFormSchema = z.object({
33+
code: z.string().nonempty('Form code is required'),
34+
name: z.string().nonempty('Form name is required'),
35+
description: z.string().optional(),
36+
defaultLanguage: z.string().nonempty(),
37+
formType: ZFormType.catch(ZFormType.Values.Opening),
38+
});
39+
40+
const form = useForm<z.infer<typeof newFormFormSchema>>({
41+
resolver: zodResolver(newFormFormSchema),
42+
});
43+
44+
function onSubmit(values: z.infer<typeof newFormFormSchema>) {
45+
const name = newTranslatedString([values.defaultLanguage], values.defaultLanguage, values.name);
46+
const description = newTranslatedString([values.defaultLanguage], values.defaultLanguage, values.description ?? '');
47+
48+
const newForm: NewFormRequest = {
49+
...values,
50+
description,
51+
name,
52+
languages: [values.defaultLanguage],
53+
};
54+
55+
newFormMutation.mutate({ electionRoundId: currentElectionRoundId, newForm });
56+
}
57+
58+
const newFormMutation = useMutation({
59+
mutationFn: ({ electionRoundId, newForm }: { electionRoundId: string; newForm: NewFormRequest }) => {
60+
return authApi.post<FormBase>(`/election-rounds/${electionRoundId}/forms`, newForm);
61+
},
62+
63+
onSuccess: ({ data: form }) => {
64+
queryClient.invalidateQueries({ queryKey: formsKeys.all(currentElectionRoundId) });
65+
navigate({ to: `/forms/$formId/edit`, params: { formId: form.id }, search: { tab: 'questions' } });
66+
},
67+
});
68+
69+
return (
70+
<Form {...form}>
71+
<form onSubmit={form.handleSubmit(onSubmit)}>
72+
<Tabs className='flex flex-col flex-1' defaultValue='form-details'>
73+
<TabsList className='grid grid-cols-2 bg-gray-200 w-[400px] mb-4'>
74+
<TabsTrigger
75+
value='form-details'
76+
className={cn({
77+
'border-b-4 border-red-400': form.getFieldState('name').invalid || form.getFieldState('code').invalid,
78+
})}>
79+
Form details
80+
</TabsTrigger>
81+
<TabsTrigger value='questions' disabled>
82+
Questions
83+
</TabsTrigger>
84+
</TabsList>
85+
<TabsContent value='form-details'>
86+
<Card className='pt-0'>
87+
<CardHeader className='flex gap-2 flex-column'>
88+
<div className='flex flex-row items-center justify-between'>
89+
<CardTitle className='text-xl'>Form details</CardTitle>
90+
</div>
91+
<Separator />
92+
</CardHeader>
93+
<CardContent className='flex flex-col items-baseline gap-6'>
94+
<div className='md:inline-flex md:space-x-6 w-full'>
95+
<div className='space-y-4 md:w-1/2'>
96+
<FormField
97+
control={form.control}
98+
name='name'
99+
render={({ field, fieldState }) => (
100+
<FormItem>
101+
<FormLabel>{t('form.field.name')}</FormLabel>
102+
<Input placeholder={t('form.placeholder.name')} {...field} {...fieldState} />
103+
<FormMessage />
104+
</FormItem>
105+
)}
106+
/>
107+
108+
<FormField
109+
control={form.control}
110+
name='code'
111+
render={({ field, fieldState }) => (
112+
<FormItem>
113+
<FormLabel>{t('form.field.code')}</FormLabel>
114+
<Input placeholder={t('form.placeholder.code')} {...field} {...fieldState} />
115+
<FormMessage />
116+
</FormItem>
117+
)}
118+
/>
119+
120+
<FormField
121+
control={form.control}
122+
name='formType'
123+
render={({ field }) => (
124+
<FormItem>
125+
<FormLabel>{t('form.field.formType')}</FormLabel>
126+
<Select onValueChange={field.onChange} defaultValue={field.value}>
127+
<FormControl>
128+
<SelectTrigger>
129+
<SelectValue placeholder='Select a form type' />
130+
</SelectTrigger>
131+
</FormControl>
132+
<SelectContent>
133+
<SelectItem value={ZFormType.Values.Opening}>
134+
{mapFormType(ZFormType.Values.Opening)}
135+
</SelectItem>
136+
<SelectItem value={ZFormType.Values.Voting}>
137+
{mapFormType(ZFormType.Values.Voting)}
138+
</SelectItem>
139+
<SelectItem value={ZFormType.Values.ClosingAndCounting}>
140+
{mapFormType(ZFormType.Values.ClosingAndCounting)}
141+
</SelectItem>
142+
{electionRound?.isMonitoringNgoForCitizenReporting && (
143+
<SelectItem value={ZFormType.Values.CitizenReporting}>
144+
{mapFormType(ZFormType.Values.CitizenReporting)}
145+
</SelectItem>
146+
)}
147+
<SelectItem value={ZFormType.Values.IncidentReporting}>
148+
{mapFormType(ZFormType.Values.IncidentReporting)}
149+
</SelectItem>
150+
<SelectItem value={ZFormType.Values.Other}>
151+
{mapFormType(ZFormType.Values.Other)}
152+
</SelectItem>
153+
</SelectContent>
154+
</Select>
155+
</FormItem>
156+
)}
157+
/>
158+
159+
<FormField
160+
control={form.control}
161+
name='defaultLanguage'
162+
render={({ field, fieldState }) => (
163+
<FormItem>
164+
<FormLabel>{t('form.field.defaultLanguage')}</FormLabel>
165+
<LanguageSelect languageCode={field.value} onLanguageSelected={field.onChange} />
166+
<FormMessage />
167+
</FormItem>
168+
)}
169+
/>
170+
</div>
171+
<div className='md:w-1/2'>
172+
<FormField
173+
control={form.control}
174+
name='description'
175+
render={({ field }) => (
176+
<FormItem>
177+
<FormLabel>{t('form.field.description')}</FormLabel>
178+
<Textarea
179+
resizable={false}
180+
rows={10}
181+
cols={100}
182+
{...field}
183+
placeholder={t('form.placeholder.description')}
184+
/>
185+
</FormItem>
186+
)}
187+
/>
188+
</div>
189+
</div>
190+
</CardContent>
191+
</Card>
192+
</TabsContent>
193+
</Tabs>
194+
<footer className='fixed left-0 bottom-0 h-[64px] w-full bg-white'>
195+
<div className='container flex items-center justify-end h-full gap-4'>
196+
<Button title='Next' type='submit' variant='default' disabled={!form.formState.isValid}>
197+
Next
198+
</Button>
199+
</div>
200+
</footer>
201+
</form>
202+
</Form>
203+
);
204+
};

0 commit comments

Comments
 (0)