Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BugFix] label 및 sprint 이름 중복시 예외처리 추가 및 toast 추가 #152

Merged
merged 16 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from 15 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
2 changes: 2 additions & 0 deletions apps/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,13 @@
"framer-motion": "^11.11.17",
"lexorank": "^1.0.5",
"lucide-react": "^0.454.0",
"next-themes": "^0.4.3",
"react": "^18.3.1",
"react-day-picker": "8.10.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.2",
"socket.io-client": "^4.8.1",
"sonner": "^1.7.0",
"tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8"
Expand Down
2 changes: 1 addition & 1 deletion apps/client/src/components/ui/date-range-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export function DateRangePicker({ className, date, onChange }: DateRangePickerPr
<Button
id="date"
variant="outline"
className={cn('w-[300px] justify-start text-left font-normal', !dateState && '')}
className={cn('max-w-xs justify-start text-left font-normal', !dateState && '')}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{getDateRangeText()}
Expand Down
27 changes: 27 additions & 0 deletions apps/client/src/components/ui/sonner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useTheme } from 'next-themes';
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분은 shadcn/ui 의 sonner 를 사용한 부분입니다.
리뷰 대상이 아닙니다.

import { Toaster as Sonner } from 'sonner';

type ToasterProps = React.ComponentProps<typeof Sonner>;

function Toaster({ ...props }: ToasterProps) {
const { theme = 'system' } = useTheme();

return (
<Sonner
theme={theme as ToasterProps['theme']}
className="toaster group"
toastOptions={{
classNames: {
toast:
'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
description: 'group-[.toast]:text-muted-foreground',
actionButton: 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
cancelButton: 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
},
}}
{...props}
/>
);
}

export { Toaster };
82 changes: 56 additions & 26 deletions apps/client/src/features/project/label/components/CreateLabel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useForm } from 'react-hook-form';
import { Plus, Shuffle } from 'lucide-react';
import { zodResolver } from '@hookform/resolvers/zod';
import { UseMutationResult } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { Input } from '@/components/ui/input.tsx';
import { Button } from '@/components/ui/button.tsx';
import {
Expand All @@ -14,13 +16,27 @@ import {
import { ColorInput } from '@/features/project/label/components/ColorInput.tsx';
import { generateRandomColor } from '@/features/project/label/generateRandomColor.ts';
import { labelFormSchema, LabelFormValues } from '@/features/project/label/labelSchema.ts';
import { BaseResponse } from '@/features/types.ts';
import { CreateLabelDto } from '@/features/project/types.ts';
import { useToast } from '@/lib/useToast.tsx';

interface CreateLabelProps {
onCreate: (data: LabelFormValues) => void;
createMutation: UseMutationResult<BaseResponse, AxiosError, CreateLabelDto>;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

외부에서 한번 mutation 을 wrapping 해서 사용했었는데,
onSuccess, onError 등을 내부에서 사용하는 편이 보기 좋아보여서, 이렇게 수정했습니다.

}

export function CreateLabel({ onCreate }: CreateLabelProps) {
const createForm = useForm<LabelFormValues>({
export function CreateLabel({ createMutation }: CreateLabelProps) {
const toast = useToast();

const { mutate } = createMutation;

const {
register,
handleSubmit,
setValue,
watch,
setError,
formState: { errors },
} = useForm<LabelFormValues>({
resolver: zodResolver(labelFormSchema),
defaultValues: {
name: '',
Expand All @@ -29,17 +45,37 @@ export function CreateLabel({ onCreate }: CreateLabelProps) {
},
});

const handleRandomColor = () => {
createForm.setValue('color', generateRandomColor(), { shouldValidate: true });
const onCreate = (data: LabelFormValues) => {
mutate(
{
name: data.name.trim(),
description: data.description.trim(),
color: data.color,
},
{ onSuccess, onError }
);
};

const handleSubmit = (data: LabelFormValues) => {
onCreate(data);
createForm.reset({
name: '',
description: '',
color: generateRandomColor(),
});
const onSuccess = () => {
toast.success('Label created successfully');
setValue('name', '');
setValue('description', '');
setValue('color', generateRandomColor());
};

const onError = (error: AxiosError) => {
if (error?.response?.status === 409) {
setError('name', {
message: 'Label with this name already exists',
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 에러 메세지들은 나중에 하나의 객체로 분리하면 더 좋을 것 같습니다.

});
return;
}

toast.error('Failed to create label');
};

const handleRandomColor = () => {
setValue('color', generateRandomColor(), { shouldValidate: true });
};

return (
Expand All @@ -49,23 +85,19 @@ export function CreateLabel({ onCreate }: CreateLabelProps) {
<CardDescription>Add a new label to your project.</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={createForm.handleSubmit(handleSubmit)} className="space-y-4">
<form onSubmit={handleSubmit(onCreate)} className="space-y-4">
<div className="flex gap-4">
<div className="flex-1">
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
Name
<Input
{...createForm.register('name')}
{...register('name')}
placeholder="Label name"
className="mt-1 h-10"
id="name"
/>
</label>
{createForm.formState.errors.name && (
<p className="mt-1 text-sm text-red-500">
{createForm.formState.errors.name.message}
</p>
)}
{errors.name && <p className="mt-1 text-sm text-red-500">{errors.name.message}</p>}
</div>
<div className="flex">
<div className="mt-1 flex min-w-[200px] items-end gap-2">
Expand All @@ -75,8 +107,8 @@ export function CreateLabel({ onCreate }: CreateLabelProps) {
>
Color
<ColorInput
value={createForm.watch('color')}
onChange={(value) => createForm.setValue('color', value)}
value={watch('color')}
onChange={(value) => setValue('color', value)}
className="flex-1"
/>
</label>
Expand All @@ -96,16 +128,14 @@ export function CreateLabel({ onCreate }: CreateLabelProps) {
<label htmlFor="description" className="block text-sm font-medium text-gray-700">
Description
<Input
{...createForm.register('description')}
{...register('description')}
placeholder="Label description"
className="mt-1"
id="description"
/>
</label>
{createForm.formState.errors.description && (
<p className="mt-1 text-sm text-red-500">
{createForm.formState.errors.description.message}
</p>
{errors.description && (
<p className="mt-1 text-sm text-red-500">{errors.description.message}</p>
)}
</div>
<Button type="submit" className="w-full bg-black hover:bg-black/80">
Expand Down
116 changes: 97 additions & 19 deletions apps/client/src/features/project/label/components/LabelList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { Pencil, X, Shuffle } from 'lucide-react';
import { zodResolver } from '@hookform/resolvers/zod';
import { AxiosError } from 'axios';
import { UseMutationResult } from '@tanstack/react-query';
import { Input } from '@/components/ui/input.tsx';
import { Button } from '@/components/ui/button.tsx';
import {
Expand All @@ -14,32 +16,100 @@ import {
import { ColorInput } from '@/features/project/label/components/ColorInput.tsx';
import { generateRandomColor } from '@/features/project/label/generateRandomColor.ts';
import { labelFormSchema, LabelFormValues } from '@/features/project/label/labelSchema.ts';
import { Label } from '@/features/types.ts';
import { BaseResponse, Label } from '@/features/types.ts';
import { UpdateLabelDto } from '@/features/project/types.ts';
import { useToast } from '@/lib/useToast.tsx';

interface LabelListProps {
labels: Label[];
onUpdate: (labelId: number, data: Partial<Label>) => void;
onDelete: (labelId: number) => void;
updateMutation: UseMutationResult<
BaseResponse,
AxiosError,
{
labelId: number;
updateLabelDto: UpdateLabelDto;
}
>;
deleteMutation: UseMutationResult<BaseResponse, AxiosError, number>;
}

export function LabelList({ labels, onUpdate, onDelete }: LabelListProps) {
export function LabelList({ labels, updateMutation, deleteMutation }: LabelListProps) {
const toast = useToast();

const [editingId, setEditingId] = useState<number | null>(null);

const editForm = useForm<LabelFormValues>({
const { mutate: updateLabel } = updateMutation;

const { mutate: deleteLabel } = deleteMutation;

const {
handleSubmit,
register,
watch,
setValue,
setError,
reset,
formState: { errors },
} = useForm<LabelFormValues>({
resolver: zodResolver(labelFormSchema),
});

const startEditing = (label: Label) => {
setEditingId(label.id);
editForm.reset({
reset({
name: label.name,
description: label.description,
color: label.color,
});
};

const onUpdate = (labelId: number, data: LabelFormValues) => {
updateLabel(
{
labelId,
updateLabelDto: {
name: data.name.trim(),
description: data.description.trim(),
color: data.color,
},
},
{ onSuccess: onUpdateSuccess, onError: onUpdateError }
);
};

const onDelete = (labelId: number) => {
deleteLabel(labelId, {
onSuccess: onDeleteSuccess,
onError: onDeleteError,
});
};

const onUpdateSuccess = () => {
toast.success('Label updated successfully');
setEditingId(null);
};

const onUpdateError = (error: AxiosError) => {
if (error?.response?.status === 409) {
setError('name', {
message: 'Label with this name already exists',
});
return;
}

toast.error('Label update failed');
};

const onDeleteSuccess = () => {
toast.success('Label deleted successfully');
};

const onDeleteError = () => {
toast.error('Label deletion failed');
};

const handleRandomColor = () => {
editForm.setValue('color', generateRandomColor(), { shouldValidate: true });
setValue('color', generateRandomColor(), { shouldValidate: true });
};

return (
Expand All @@ -58,21 +128,29 @@ export function LabelList({ labels, onUpdate, onDelete }: LabelListProps) {
{editingId === label.id ? (
<form
className="flex w-full flex-col gap-4"
onSubmit={editForm.handleSubmit((data) => {
onSubmit={handleSubmit((data) => {
onUpdate(label.id, data);
setEditingId(null);
})}
>
<div className="flex items-center gap-4">
<Input
{...editForm.register('name')}
className="h-10 flex-1"
placeholder="Name"
/>
<div className="flex min-w-[200px] gap-2">
<div className="flex items-start gap-4">
<div className="flex-1">
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
Name
<Input
{...register('name')}
placeholder="Label name"
className="mt-1 h-10"
id="name"
/>
</label>
{errors.name && (
<p className="mt-1 text-sm text-red-500">{errors.name.message}</p>
)}
</div>
<div className="mt-6 flex min-w-[200px] gap-2">
<ColorInput
value={editForm.watch('color')}
onChange={(value) => editForm.setValue('color', value)}
value={watch('color')}
onChange={(value) => setValue('color', value)}
className="flex-1"
/>
<Button
Expand All @@ -88,7 +166,7 @@ export function LabelList({ labels, onUpdate, onDelete }: LabelListProps) {
</div>
<div className="flex gap-4">
<Input
{...editForm.register('description')}
{...register('description')}
className="flex-1"
placeholder="Description"
/>
Expand Down
Loading
Loading