Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/common/components/Form/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface InputProps<T extends FieldValues>
PropsWithTestId {
control: Control<T>;
label?: string;
name: Path<T>;
name: string;
supportingText?: string;
}

Expand All @@ -41,7 +41,7 @@ const Input = <T extends FieldValues>({
testId = 'input',
...props
}: InputProps<T>): JSX.Element => {
const { field, fieldState } = useController({ control, name });
const { field, fieldState } = useController({ control, name: name as Path<T> });
const isDisabled = props.disabled || props.readOnly;

return (
Expand Down
4 changes: 2 additions & 2 deletions src/common/components/Form/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export interface SelectProps<T extends FieldValues>
InputHTMLAttributes<HTMLSelectElement> {
control: Control<T>;
label?: string;
name: Path<T>;
name: string;
supportingText?: string;
}

Expand All @@ -47,7 +47,7 @@ const Select = <T extends FieldValues>({
testId = 'select',
...props
}: SelectProps<T>): JSX.Element => {
const { field, fieldState } = useController({ control, name });
const { field, fieldState } = useController({ control, name: name as Path<T> });
const isDisabled = props.disabled || props.readOnly;

return (
Expand Down
9 changes: 5 additions & 4 deletions src/common/components/Form/Toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export interface ToggleProps<T extends FieldValues> extends BaseComponentProps {
control: Control<T>;
disabled?: boolean;
label?: string;
name: Path<T>;
name: string;
required?: boolean;
supportingText?: string;
}
Expand All @@ -45,7 +45,7 @@ const Toggle = <T extends FieldValues>({
supportingText,
testId = 'toggle',
}: ToggleProps<T>): JSX.Element => {
const { field, fieldState } = useController({ control, name });
const { field, fieldState } = useController({ control, name: name as Path<T> });

const handleClick = () => {
field.onChange(!field.value);
Expand All @@ -59,16 +59,17 @@ const Toggle = <T extends FieldValues>({
</Label>
)}
<Button
id={name}
variant="text"
onClick={handleClick}
disabled={disabled}
aria-label={label}
testId={`${testId}-button`}
>
{field.value ? (
<FAIcon icon="toggleOn" size="2xl" fixedWidth />
<FAIcon icon="toggleOn" size="2xl" fixedWidth testId={`${testId}-icon-on`} />
) : (
<FAIcon icon="toggleOff" size="2xl" fixedWidth />
<FAIcon icon="toggleOff" size="2xl" fixedWidth testId={`${testId}-icon-off`} />
)}
</Button>
<FieldError message={fieldState.error?.message} testId={`${testId}-error`} />
Expand Down
22 changes: 18 additions & 4 deletions src/common/components/Form/__stories__/Input.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,34 @@
import type { Meta, StoryObj } from '@storybook/react';
import { FieldValues, useForm } from 'react-hook-form';
import { useForm } from 'react-hook-form';
import { InferType, object, string } from 'yup';
import { yupResolver } from '@hookform/resolvers/yup';

import { default as MyInput } from '../Input';
import { InputProps } from '../Input';

const formSchema = object({
color: string().required('Required'),
});

type FormValues = InferType<typeof formSchema>;

/**
* A wrapper for the `Input` component. Provides the RHF form `control`
* to the `Input` component.
*/
const Input = (props: Omit<InputProps<FieldValues>, 'control'>) => {
const form = useForm();
const Input = (props: Omit<InputProps<FormValues>, 'control'>) => {
const form = useForm({
defaultValues: {
color: '',
},
mode: 'all',
resolver: yupResolver(formSchema),
});

const onSubmit = () => {};

return (
<form className="w-96" onSubmit={form.handleSubmit(onSubmit)}>
<form className="w-96" onSubmit={form.handleSubmit(onSubmit)} noValidate>
<MyInput control={form.control} {...props} />
</form>
);
Expand Down
22 changes: 18 additions & 4 deletions src/common/components/Form/__stories__/Select.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,34 @@
import type { Meta, StoryObj } from '@storybook/react';
import { FieldValues, useForm } from 'react-hook-form';
import { useForm } from 'react-hook-form';
import { InferType, object, string } from 'yup';
import { yupResolver } from '@hookform/resolvers/yup';

import { default as MySelect } from '../Select';
import { SelectProps } from '../Select';

const formSchema = object({
color: string().required('Required. ').oneOf(['blue', 'green'], 'Must be blue or green. '),
});

type FormValues = InferType<typeof formSchema>;

/**
* A wrapper for the `Select` component. Provides the RHF form `control`
* to the `Select` component.
*/
const Select = (props: Omit<SelectProps<FieldValues>, 'control'>) => {
const form = useForm();
const Select = (props: Omit<SelectProps<FormValues>, 'control'>) => {
const form = useForm({
defaultValues: {
color: '',
},
mode: 'all',
resolver: yupResolver(formSchema),
});

const onSubmit = () => {};

return (
<form className="w-96" onSubmit={form.handleSubmit(onSubmit)}>
<form className="w-96" onSubmit={form.handleSubmit(onSubmit)} noValidate>
<MySelect control={form.control} {...props}></MySelect>
</form>
);
Expand Down
20 changes: 17 additions & 3 deletions src/common/components/Form/__stories__/Toggle.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
import type { Meta, StoryObj } from '@storybook/react';
import { FieldValues, useForm } from 'react-hook-form';
import { useForm } from 'react-hook-form';
import { boolean, InferType, object } from 'yup';
import { yupResolver } from '@hookform/resolvers/yup';

import { default as MyToggle } from '../Toggle';
import { ToggleProps } from '../Toggle';

const formSchema = object({
isEnabledNotifications: boolean(),
});

type FormValues = InferType<typeof formSchema>;

/**
* A wrapper for the `Toggle` component. Provides the RHF form `control`
* to the `Input` component.
*/
const Toggle = (props: Omit<ToggleProps<FieldValues>, 'control'>) => {
const form = useForm();
const Toggle = (props: Omit<ToggleProps<FormValues>, 'control'>) => {
const form = useForm({
defaultValues: {
isEnabledNotifications: false,
},
mode: 'all',
resolver: yupResolver(formSchema),
});

const onSubmit = () => {};

Expand Down
12 changes: 11 additions & 1 deletion src/common/components/Router/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import TasksPage from 'pages/Tasks/TasksPage';
import TaskListLayout from 'pages/Tasks/components/TaskListLayout';
import TaskDetailLayout from 'pages/Tasks/components/TaskDetailLayout';
import TaskAdd from 'pages/Tasks/components/Add/TaskAdd';
import TaskEdit from 'pages/Tasks/components/Edit/TaskEdit';

/**
* The React Router configuration. An array of `RouteObject`.
Expand Down Expand Up @@ -123,7 +124,16 @@ export const routes: RouteObject[] = [
},
{
path: ':taskId',
element: <TaskDetailLayout />,
children: [
{
index: true,
element: <TaskDetailLayout />,
},
{
path: 'edit',
element: <TaskEdit />,
},
],
},
],
},
Expand Down
2 changes: 1 addition & 1 deletion src/common/utils/i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"creatingReactApps": "Creating React apps just got a lot simpler",
"errors": {
"generic": "Uh oh",
"unable-to-find-short": "Not found",
"unable-to-find-short": "Not found.",
"unable-to-find": "We are unable to find information matching your request.",
"unable-to-process": "We are experiencing problems processing your request.",
"unable-to-retrieve": "We are unable to retrieve the requested information at this time."
Expand Down
4 changes: 3 additions & 1 deletion src/common/utils/i18n/locales/en/tasks.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"addTask": "Add a new task",
"editTask": "Edit task",
"label": {
"title": "Title",
"completed": "Is complete"
Expand All @@ -16,5 +17,6 @@
"status-of-tasks": "Status of Tasks",
"task": "Task",
"tasks": "Tasks",
"tasks-empty": "Nothing to do. Create a task to get started."
"tasks-empty": "Nothing to do. Create a task to get started.",
"updatedTask": "Task updated."
}
2 changes: 1 addition & 1 deletion src/common/utils/i18n/locales/es/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"creatingReactApps": "Crear aplicaciones React ahora es mucho m谩s sencillo",
"errors": {
"generic": "Uh oh",
"unable-to-find-short": "No se encontr贸 nada",
"unable-to-find-short": "No se pudo encontrar.",
"unable-to-find": "No podemos encontrar informaci贸n que coincida con su solicitud.",
"unable-to-process": "Estamos experimentando problemas al procesar su solicitud.",
"unable-to-retrieve": "No podemos recuperar la informaci贸n solicitada en este momento."
Expand Down
4 changes: 3 additions & 1 deletion src/common/utils/i18n/locales/es/tasks.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"addTask": "Crear una nueva tarea",
"editTask": "Editar tarea",
"label": {
"title": "T铆tulo",
"completed": "Est谩 completo"
Expand All @@ -16,5 +17,6 @@
"status-of-tasks": "Estado de las Tareas",
"task": "Tarea",
"tasks": "Tareas",
"tasks-empty": "No hay nada que hacer. Haz una tarea para empezar."
"tasks-empty": "No hay nada que hacer. Haz una tarea para empezar.",
"updatedTask": "Tarea actualizada."
}
2 changes: 1 addition & 1 deletion src/common/utils/i18n/locales/fr/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"creatingReactApps": "La cr茅ation d'applications React est devenue beaucoup plus simple",
"errors": {
"generic": "Uh oh",
"unable-to-find-short": "Not found",
"unable-to-find-short": "Impossible de trouver.",
"unable-to-find": "Nous ne parvenons pas 脿 trouver d'informations correspondant 脿 votre demande.",
"unable-to-process": "Nous rencontrons des probl猫mes lors du traitement de votre demande.",
"unable-to-retrieve": "Nous ne sommes pas en mesure de r茅cup茅rer les informations demand茅es pour le moment."
Expand Down
4 changes: 3 additions & 1 deletion src/common/utils/i18n/locales/fr/tasks.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"addTask": "Cr茅er une nouvelle t芒che",
"editTask": "Modifier la t芒che",
"label": {
"title": "Titre",
"completed": "Est complet"
Expand All @@ -16,5 +17,6 @@
"status-of-tasks": "Statut des T芒ches",
"task": "T芒che",
"tasks": "T芒ches",
"tasks-empty": "Rien 脿 faire. Cr茅ez une t芒che pour commencer."
"tasks-empty": "Rien 脿 faire. Cr茅ez une t芒che pour commencer.",
"updatedTask": "T芒che mise 脿 jour."
}
2 changes: 1 addition & 1 deletion src/pages/Auth/Signin/components/SigninForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ const SigninForm = ({ className, testId = 'form-signin' }: BaseComponentProps):
control={control}
name="username"
label="Username"
supportingText="Use any username from {JSON}Placeholder, e.g. Bret or Samantha."
supportingText="Use any username from {JSON}Placeholder, e.g. Kamren or Samantha."
className="mb-4"
autoFocus
autoComplete="off"
Expand Down
4 changes: 3 additions & 1 deletion src/pages/Tasks/api/useUpdateTask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,13 @@ export const useUpdateTask = () => {
return useMutation({
mutationFn: updateTask,
onSuccess: (data, variables) => {
// update cached query data
// update the cached task list
queryClient.setQueryData<Task[]>(
[QueryKey.Tasks, { userId: variables.task.userId }],
(cachedTasks) => (cachedTasks ? [...reject(cachedTasks, { id: data.id }), data] : [data]),
);
// update the cache for this task
queryClient.setQueryData<Task>([QueryKey.Tasks, data.id], () => data);
},
});
};
53 changes: 45 additions & 8 deletions src/pages/Tasks/components/Add/TaskAdd.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';

import { useToasts } from 'common/hooks/useToasts';
import { useGetCurrentUser } from 'common/api/useGetCurrentUser';
import { useCreateTask } from 'pages/Tasks/api/useCreateTask';
import { BaseComponentProps } from 'common/utils/types';
import TaskForm from '../Form/TaskForm';
import TaskForm, { TaskFormValues } from '../Form/TaskForm';
import Alert from 'common/components/Alert/Alert';
import FAIcon from 'common/components/Icon/FAIcon';

/**
* Properties for the `TaskAdd` component.
Expand All @@ -19,30 +23,63 @@ export interface TaskAddProps extends BaseComponentProps {}
* @returns JSX
*/
const TaskAdd = ({ className, testId = 'task-add' }: TaskAddProps): JSX.Element => {
const [taskCreateError, setTaskCreateError] = useState('');
const { t } = useTranslation();
const navigate = useNavigate();
const { createToast } = useToasts();
const { data: user } = useGetCurrentUser();
const { mutate: createTask } = useCreateTask();

const onTaskCreated = () => {
createToast({ text: t('createdTask', { ns: 'tasks' }), isAutoDismiss: true });
/**
* Form cancellation callback function.
*/
const onFormCancel = () => {
navigate(-1);
};

/**
* Form submission callback function.
* @param data - The submitted form data.
* @returns A Promise which resolves empty when the mutation function completes.
*/
const onFormSubmit = (data: TaskFormValues): Promise<void> => {
return new Promise<void>((resolve) => {
createTask(
{ task: data },
{
onSuccess: () => {
createToast({ text: t('createdTask', { ns: 'tasks' }), isAutoDismiss: true });
navigate(-1);
},
onError: (err) => {
setTaskCreateError(err.message);
},
onSettled: () => {
resolve();
},
},
);
});
};

return (
<div className={className} data-testid={testId}>
{/* heading */}
<h2 className="mb-8 border-b border-neutral-500/10 pb-1 text-lg font-bold">
{t('addTask', { ns: 'tasks' })}
</h2>

{/* error state */}
{!!taskCreateError && (
<Alert variant="error" className="mb-4 rounded-none" testId={`${testId}-error-create`}>
<FAIcon icon="circleExclamation" size="lg" />
{`${t('errors.unable-to-process')} ${taskCreateError}`}
</Alert>
)}

{/* form */}
{!!user && (
<TaskForm
task={{ userId: user.id }}
onCancel={() => navigate(-1)}
onSubmit={onTaskCreated}
/>
<TaskForm task={{ userId: user.id }} onCancel={onFormCancel} onSubmit={onFormSubmit} />
)}
</div>
);
Expand Down
Loading