diff --git a/web/src/components/LanguageSelector/LanguageSelector.tsx b/web/src/components/LanguageSelector/LanguageSelector.tsx index b3a6350bf..5e8662ceb 100644 --- a/web/src/components/LanguageSelector/LanguageSelector.tsx +++ b/web/src/components/LanguageSelector/LanguageSelector.tsx @@ -1,11 +1,16 @@ import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import i18n from '@/i18n'; +import { useRouter } from '@tanstack/react-router'; import { FC } from 'react'; +import { useTranslation } from 'react-i18next'; import { Label } from '../ui/label'; export const LanguageSelector: FC = () => { + const { i18n } = useTranslation(); // not passing any namespace will use the defaultNS (by default set to 'translation') + const router = useRouter(); + const changeLanguage = (lng: string) => { i18n.changeLanguage(lng); + router.invalidate(); }; const options = [ diff --git a/web/src/components/layout/Layout.tsx b/web/src/components/layout/Layout.tsx index 6e6c7f9cc..b737988d3 100644 --- a/web/src/components/layout/Layout.tsx +++ b/web/src/components/layout/Layout.tsx @@ -1,12 +1,13 @@ -import type { ReactNode } from 'react'; import type { FunctionComponent } from '@/common/types'; -import Breadcrumbs from './Breadcrumbs/Breadcrumbs'; +import type { ReactNode } from 'react'; import BackButton from './Breadcrumbs/BackButton'; +import Breadcrumbs from './Breadcrumbs/Breadcrumbs'; interface LayoutProps { title?: string; subtitle?: string; enableBreadcrumbs?: boolean; + enableBackButton?: boolean; breadcrumbs?: ReactNode; backButton?: ReactNode; actions?: ReactNode; @@ -21,6 +22,7 @@ const Layout = ({ breadcrumbs, children, enableBreadcrumbs = true, + enableBackButton, }: LayoutProps): FunctionComponent => { return ( <> @@ -29,6 +31,7 @@ const Layout = ({ {enableBreadcrumbs && (breadcrumbs || )}

{enableBreadcrumbs && (backButton || )} + {enableBackButton && (backButton || )} {title}

{subtitle ??

{subtitle}

} diff --git a/web/src/features/forms/components/Dashboard/Dashboard.tsx b/web/src/features/forms/components/Dashboard/Dashboard.tsx index efc7d2d08..e7f5f1c6b 100644 --- a/web/src/features/forms/components/Dashboard/Dashboard.tsx +++ b/web/src/features/forms/components/Dashboard/Dashboard.tsx @@ -6,7 +6,7 @@ import { DataTableColumnHeader } from '@/components/ui/DataTable/DataTableColumn import { QueryParamsDataTable } from '@/components/ui/DataTable/QueryParamsDataTable'; import { useConfirm } from '@/components/ui/alert-dialog-provider'; import { Badge } from '@/components/ui/badge'; -import { buttonVariants } from '@/components/ui/button'; +import { Button, buttonVariants } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { DropdownMenu, @@ -31,9 +31,10 @@ import { EllipsisVerticalIcon, FunnelIcon, PhotoIcon, + PlusIcon, } from '@heroicons/react/24/outline'; import { useMutation } from '@tanstack/react-query'; -import { useNavigate, useRouter } from '@tanstack/react-router'; +import { Link, useNavigate, useRouter } from '@tanstack/react-router'; import { ColumnDef, createColumnHelper, Row } from '@tanstack/react-table'; import { useDebounce } from '@uidotdev/usehooks'; import { format } from 'date-fns'; @@ -41,7 +42,6 @@ import { useMemo, useState, type ReactElement } from 'react'; import { FormBase, FormStatus } from '../../models/form'; import { formsKeys, useForms } from '../../queries'; import AddTranslationsDialog, { useAddTranslationsDialog } from './AddTranslationsDialog'; -import CreateForm from './CreateForm'; import { FormFilters } from './FormFilters/FormFilters'; import EditFormAccessDialog, { useEditFormAccessDialog } from './EditFormAccessDialog'; import { useElectionRoundDetails } from '@/features/election-event/hooks/election-event-hooks'; @@ -725,9 +725,12 @@ export default function FormsDashboard(): ReactElement { {i18n.t('electionEvent.observerForms.cardTitle')}
- - - + + +
diff --git a/web/src/features/forms/components/EditForm/EditForm.tsx b/web/src/features/forms/components/EditForm/EditForm.tsx index dfeceed8b..dc0a566a5 100644 --- a/web/src/features/forms/components/EditForm/EditForm.tsx +++ b/web/src/features/forms/components/EditForm/EditForm.tsx @@ -1,4 +1,4 @@ -import { QuestionType, ZFormType, type FunctionComponent } from '@/common/types'; +import { QuestionType, ZFormType } from '@/common/types'; import FormQuestionsEditor from '@/components/questionsEditor/FormQuestionsEditor'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Form } from '@/components/ui/form'; @@ -25,17 +25,12 @@ import { Button, buttonVariants } from '@/components/ui/button'; import { LanguageBadge } from '@/components/ui/language-badge'; import { toast } from '@/components/ui/use-toast'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; -import { - cn, - ensureTranslatedStringCorrectness, - isNilOrWhitespace, - isNotNilOrWhitespace -} from '@/lib/utils'; +import { cn, ensureTranslatedStringCorrectness, isNilOrWhitespace, isNotNilOrWhitespace } from '@/lib/utils'; import { queryClient } from '@/main'; import { Route } from '@/routes/forms_.$formId.edit'; import { useMutation } from '@tanstack/react-query'; import { useBlocker, useNavigate, useRouter } from '@tanstack/react-router'; -import { useEffect, useState } from 'react'; +import { FC, useEffect, useState } from 'react'; import { UpdateFormRequest } from '../../models/form'; import { formDetailsQueryOptions, formsKeys } from '../../queries'; import { @@ -49,7 +44,6 @@ import { ZEditQuestionType, ZTranslatedString, } from '../../types'; -import { FormDetailsBreadcrumbs } from '../FormDetailsBreadcrumbs/FormDetailsBreadcrumbs'; import EditFormDetails from './EditFormDetails'; export const ZEditFormType = z @@ -215,7 +209,11 @@ export const ZEditFormType = z export type EditFormType = z.infer; -export default function EditForm(): FunctionComponent { +interface EditFormProps { + currentTab?: string; +} + +const EditForm: FC = ({ currentTab }) => { const { formId } = Route.useParams(); const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId); const { data: formData } = useSuspenseQuery(formDetailsQueryOptions(currentElectionRoundId, formId)); @@ -383,7 +381,7 @@ export default function EditForm(): FunctionComponent { description: ensureTranslatedStringCorrectness(formData.description, formData.languages), formType: formData.formType, questions: editQuestions, - icon: formData.icon ?? '' + icon: formData.icon ?? '', }, mode: 'all', }); @@ -423,7 +421,7 @@ export default function EditForm(): FunctionComponent { }); }, - onSuccess: async (_, { shouldExitEditor,electionRoundId }) => { + onSuccess: async (_, { shouldExitEditor, electionRoundId }) => { toast({ title: 'Success', description: 'Form updated successfully', @@ -474,14 +472,21 @@ export default function EditForm(): FunctionComponent { } }, [form.formState.isSubmitSuccessful, form.reset]); + const [activeTab, setActiveTab] = useState(currentTab ?? 'form-details'); + return ( } - breadcrumbs={} + enableBreadcrumbs={false} title={`${code} - ${name[languageCode]}`}>
- + ); -} +}; + +export default EditForm; diff --git a/web/src/features/forms/components/FormBuilder/components/CreateFormPage.tsx b/web/src/features/forms/components/FormBuilder/components/CreateFormPage.tsx new file mode 100644 index 000000000..a769067fa --- /dev/null +++ b/web/src/features/forms/components/FormBuilder/components/CreateFormPage.tsx @@ -0,0 +1,204 @@ +import { authApi } from '@/common/auth-api'; +import { ZFormType } from '@/common/types'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Separator } from '@/components/ui/separator'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Textarea } from '@/components/ui/textarea'; +import LanguageSelect from '@/containers/LanguageSelect'; +import { useCurrentElectionRoundStore } from '@/context/election-round.store'; +import { useElectionRoundDetails } from '@/features/election-event/hooks/election-event-hooks'; +import { FormBase, NewFormRequest } from '@/features/forms/models/form'; +import { formsKeys } from '@/features/forms/queries'; +import { cn, mapFormType, newTranslatedString } from '@/lib/utils'; +import { queryClient } from '@/main'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useMutation } from '@tanstack/react-query'; +import { useNavigate } from '@tanstack/react-router'; +import { FC } from 'react'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; + +export const CreateFormPage: FC = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId); + const { data: electionRound } = useElectionRoundDetails(currentElectionRoundId); + + const newFormFormSchema = z.object({ + code: z.string().nonempty('Form code is required'), + name: z.string().nonempty('Form name is required'), + description: z.string().optional(), + defaultLanguage: z.string().nonempty(), + formType: ZFormType.catch(ZFormType.Values.Opening), + }); + + const form = useForm>({ + resolver: zodResolver(newFormFormSchema), + }); + + function onSubmit(values: z.infer) { + const name = newTranslatedString([values.defaultLanguage], values.defaultLanguage, values.name); + const description = newTranslatedString([values.defaultLanguage], values.defaultLanguage, values.description ?? ''); + + const newForm: NewFormRequest = { + ...values, + description, + name, + languages: [values.defaultLanguage], + }; + + newFormMutation.mutate({ electionRoundId: currentElectionRoundId, newForm }); + } + + const newFormMutation = useMutation({ + mutationFn: ({ electionRoundId, newForm }: { electionRoundId: string; newForm: NewFormRequest }) => { + return authApi.post(`/election-rounds/${electionRoundId}/forms`, newForm); + }, + + onSuccess: ({ data: form }) => { + queryClient.invalidateQueries({ queryKey: formsKeys.all(currentElectionRoundId) }); + navigate({ to: `/forms/$formId/edit`, params: { formId: form.id }, search: { tab: 'questions' } }); + }, + }); + + return ( + + + + + + Form details + + + Questions + + + + + +
+ Form details +
+ +
+ +
+
+ ( + + {t('form.field.name')} + + + + )} + /> + + ( + + {t('form.field.code')} + + + + )} + /> + + ( + + {t('form.field.formType')} + + + )} + /> + + ( + + {t('form.field.defaultLanguage')} + + + + )} + /> +
+
+ ( + + {t('form.field.description')} +