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 api/tasks/tasks.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { TApiMethodsRecord } from '../common/common.types'
import {
AddTaskToWatchListDto,
AssignTaskToUserReqDto,
CrateTaskDto,
CrateTaskReqDto,
GetTaskReqDto,
GetTasksDto,
GetWatchListTaskDto,
Expand All @@ -24,7 +24,7 @@ export const TasksApi = {

createTask: {
key: ['tasksApi.createTask'],
fn: async (task: CrateTaskDto): Promise<TTask> => {
fn: async (task: CrateTaskReqDto): Promise<TTask> => {
const { data } = await apiClient.post<TTask>(`/v1/tasks`, task)
return data
},
Expand Down
6 changes: 0 additions & 6 deletions api/tasks/tasks.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,3 @@ export enum TASK_PRIORITY_ENUM {
MEDIUM = 'MEDIUM',
HIGH = 'HIGH',
}

export const NUM_TASK_PRIORITY_TO_TASK_ENUM: Record<number, TASK_PRIORITY_ENUM> = {
1: TASK_PRIORITY_ENUM.LOW,
2: TASK_PRIORITY_ENUM.MEDIUM,
3: TASK_PRIORITY_ENUM.HIGH,
}
7 changes: 5 additions & 2 deletions api/tasks/tasks.types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { USER_TYPE_ENUM } from '../common/common-enum'
import { TMinimalUser, TTaskAssignee } from '../common/common.types'
import { TASK_PRIORITY_ENUM, TASK_STATUS_ENUM } from './tasks.enum'

Expand Down Expand Up @@ -44,13 +45,15 @@ export type GetTasksDto = {
tasks: TTask[]
}

export type CrateTaskDto = {
export type CrateTaskReqDto = {
title: string
description?: string
priority?: TASK_PRIORITY_ENUM
status?: TASK_STATUS_ENUM
labels?: string[]
dueAt?: string
assignee_id: string
user_type: USER_TYPE_ENUM
}

export type UpdateTaskDto = {
Expand Down Expand Up @@ -82,7 +85,7 @@ export type TWatchListTask = {
userId: string
title: string
description?: string
priority?: number
priority?: TASK_PRIORITY_ENUM
status: TASK_STATUS_ENUM
isAcknowledged: boolean | null
isDeleted: boolean | null
Expand Down
2 changes: 1 addition & 1 deletion components/create-edit-todo-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ type CreateModeProps = BaseProps & {

type EditModeProps = BaseProps & {
mode: 'edit'
defaultData: TTodoFormData
defaultData: Partial<TTodoFormData>
}

type CreateEditTodoDialogProps = CreateModeProps | EditModeProps
Expand Down
78 changes: 59 additions & 19 deletions components/create-edit-todo-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ import { UserAndTeamSearch } from './user-and-team-search'
const todoFormSchema = z.object({
taskId: z.string().optional(),
title: z.string().min(1, 'Title is required'),
description: z.string().min(1, 'Description is required'),
description: z.string().optional(),
dueDate: z.string().min(1, 'Due date is required'),
priority: z.enum(TASK_PRIORITY_ENUM).optional(),
status: z.enum(TASK_STATUS_ENUM).optional(),
labels: z.array(z.string()).optional(),
assigneeId: z.string().optional(),
userType: z.enum(USER_TYPE_ENUM).optional(),
assigneeId: z.string().min(1, 'Assignee is required'),
userType: z.enum(USER_TYPE_ENUM, { message: 'Assignee is required' }),
})

export type TTodoFormData = z.infer<typeof todoFormSchema>
Expand All @@ -42,6 +42,7 @@ type FormInputProps = {
htmlFor?: string
icon?: LucideIcon
required?: boolean
errorMessage?: string
children: React.ReactNode
direction?: 'row' | 'column'
}
Expand All @@ -52,6 +53,7 @@ const FormInput = ({
htmlFor,
icon: Icon,
required,
errorMessage,
direction = 'row',
}: FormInputProps) => {
return (
Expand All @@ -68,6 +70,7 @@ const FormInput = ({
</div>

{children}
{errorMessage && <p className="w-full text-sm text-red-500">{errorMessage}</p>}
</div>
)
}
Expand All @@ -81,10 +84,11 @@ type SubmitButtonProps = {

const SubmitButton = ({ text, isLoading, isDisabled, watch }: SubmitButtonProps) => {
const title = watch('title')
const description = watch('description')
const dueDate = watch('dueDate')
const userType = watch('userType')
const assigneeId = watch('assigneeId')

const isButtonDisabled = !title || !description || !dueDate || isLoading || isDisabled
const isButtonDisabled = !title || !dueDate || !assigneeId || !userType || isLoading || isDisabled

return (
<Button type="submit" disabled={isButtonDisabled}>
Expand Down Expand Up @@ -119,11 +123,13 @@ export const CreateEditTodoForm = ({
resolver: zodResolver(todoFormSchema),
defaultValues: {
title: initialData?.title || '',
description: initialData?.description || '',
description: initialData?.description || undefined,
dueDate: initialData?.dueDate || '',
priority: initialData?.priority || TASK_PRIORITY_ENUM.LOW,
status: initialData?.status || TASK_STATUS_ENUM.TODO,
labels: initialData?.labels || [],
assigneeId: initialData?.assigneeId || undefined,
userType: initialData?.userType || undefined,
},
})

Expand All @@ -143,32 +149,48 @@ export const CreateEditTodoForm = ({
return (
<form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-4">
{/* Title */}
<FormInput required label="Title" htmlFor="title" direction="column">
<FormInput
required
label="Title"
htmlFor="title"
direction="column"
errorMessage={errors.title?.message}
>
<Input
id="title"
type="text"
placeholder="e.g Cool new title for my todo"
{...register('title')}
/>
{errors.title && <p className="text-sm text-red-500">{errors.title.message}</p>}
</FormInput>

{/* Description */}
<FormInput required label="Description" htmlFor="description" direction="column">
<FormInput
label="Description"
htmlFor="description"
direction="column"
errorMessage={errors.description?.message}
>
<Input
id="description"
type="text"
placeholder="e.g Nothing is cool in here"
{...register('description')}
/>
{errors.description && <p className="text-sm text-red-500">{errors.description.message}</p>}
</FormInput>

{/* Assignee */}
<Controller
control={control}
name="assigneeId"
render={({ field }) => (
<FormInput required label="Assignee" htmlFor="assigneeId" direction="column">
<FormInput
required
label="Assignee"
htmlFor="assigneeId"
direction="column"
errorMessage={errors.assigneeId?.message}
>
<UserAndTeamSearch
placeholder="Select assignee"
value={field.value}
Expand All @@ -191,16 +213,19 @@ export const CreateEditTodoForm = ({
control={control}
name="dueDate"
render={({ field }) => (
<FormInput required label="Due Date" htmlFor="dueDate" icon={CalendarIcon}>
<FormInput
required
label="Due Date"
htmlFor="dueDate"
icon={CalendarIcon}
errorMessage={errors.dueDate?.message}
>
<div className="flex-1">
<DatePickerSelect
isDateDisabled={(date) => isPastDate(date)}
value={field.value ? new Date(field.value) : undefined}
onChange={(date) => field.onChange(date?.toISOString())}
/>
{errors.dueDate && (
<p className="mt-1 text-sm text-red-500">{errors.dueDate.message}</p>
)}
</div>
</FormInput>
)}
Expand All @@ -211,7 +236,12 @@ export const CreateEditTodoForm = ({
control={control}
name="priority"
render={({ field }) => (
<FormInput label="Priority" htmlFor="priority" icon={PlayIcon}>
<FormInput
label="Priority"
htmlFor="priority"
icon={PlayIcon}
errorMessage={errors.priority?.message}
>
<Select
value={field.value}
onValueChange={(value) => field.onChange(value as TASK_PRIORITY_ENUM)}
Expand All @@ -234,7 +264,12 @@ export const CreateEditTodoForm = ({
control={control}
name="status"
render={({ field }) => (
<FormInput label="Status" htmlFor="status" icon={CircleDotIcon}>
<FormInput
label="Status"
htmlFor="status"
icon={CircleDotIcon}
errorMessage={errors.status?.message}
>
<Select
value={field.value}
onValueChange={(value) => field.onChange(value as TASK_STATUS_ENUM)}
Expand All @@ -258,7 +293,12 @@ export const CreateEditTodoForm = ({
control={control}
name="labels"
render={({ field }) => (
<FormInput label="Labels" htmlFor="labels" icon={TagIcon}>
<FormInput
label="Labels"
htmlFor="labels"
icon={TagIcon}
errorMessage={errors.labels?.message}
>
<SelectLabels
labelData={labels}
value={field.value ?? []}
Expand All @@ -280,8 +320,8 @@ export const CreateEditTodoForm = ({

<SubmitButton
watch={watch}
isDisabled={mode === 'edit' ? !isDirty : false}
isLoading={isSubmitting}
isDisabled={mode === 'edit' ? !isDirty : false}
text={isSubmitting ? buttonLoadingText : buttonText}
/>
</div>
Expand Down
92 changes: 40 additions & 52 deletions components/edit-task-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,70 +20,58 @@ export const EditTodoButton = ({ todo }: EditTodoButtonProps) => {

const [showEditTaskForm, setShowEditTaskForm] = useState(false)

const updateTaskMutation = useMutation({
mutationFn: TasksApi.updateTask.fn,
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: TasksApi.getTasks.key() })
toast.success('Todo updated successfully')
setShowEditTaskForm(false)
},
onError: () => {
toast.error('Failed to update todo')
},
})

const updateTaskMutation = useMutation({ mutationFn: TasksApi.updateTask.fn })
const assignTaskToUserMutation = useMutation({ mutationFn: TasksApi.assignTaskToUser.fn })
const assignTaskToUserOrTeamMutation = useMutation({
mutationFn: TaskAssignmentApi.assignTaskToUserOrTeam.fn,
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: TasksApi.getTasks.key() })
toast.success('Todo assigned successfully')
},
onError: () => {
toast.error('Failed to assign todo')
},
})

const assignTaskToUserMutation = useMutation({
mutationFn: TasksApi.assignTaskToUser.fn,
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: TasksApi.getTasks.key() })
toast.success('Todo assigned successfully')
},
onError: () => {
toast.error('Failed to assign todo')
},
})

const isMutationPending =
updateTaskMutation.isPending ||
assignTaskToUserOrTeamMutation.isPending ||
assignTaskToUserMutation.isPending

const handleSubmission = (todoDetails: TTodoFormData) => {
const handleSubmission = async (todoDetails: TTodoFormData) => {
let updateSucceeded = false
const updateDetails = TodoUtil.getUpdateTodoDetails(todoDetails, todo)

if (Object.keys(updateDetails).length > 0) {
updateTaskMutation.mutate({
id: todo.id,
...updateDetails,
})
}
try {
if (Object.keys(updateDetails).length > 0) {
await updateTaskMutation.mutateAsync({
id: todo.id,
...updateDetails,
})
updateSucceeded = true
}

/**
* If the task has not been to any user then we call the post api to assign the task to a user or team.
* If the task has been assigned to a user then we call the patch api to update the task assignee to a different user.
* */
if (!todo.assignee && todoDetails.assigneeId && todoDetails.userType) {
await assignTaskToUserOrTeamMutation.mutateAsync({
task_id: todo.id,
assignee_id: todoDetails.assigneeId,
user_type: todoDetails.userType,
})
} else if (todo.assignee && todoDetails.assigneeId) {
await assignTaskToUserMutation.mutateAsync({
task_id: todo.id,
assignee_id: todoDetails.assigneeId,
})
}

void queryClient.invalidateQueries({ queryKey: TasksApi.getTasks.key() })
toast.success('Todo updated successfully')
setShowEditTaskForm(false)
} catch (error) {
console.debug(error)
if (updateSucceeded) {
toast.error('Failed to assign todo, please try again')
return
}

/**
* If the task has not been to any user then we call the post api to assign the task to a user or team.
* If the task has been assigned to a user then we call the patch api to update the task assignee to a different user.
* */
if (!todo.assignee && todoDetails.assigneeId && todoDetails.userType) {
assignTaskToUserOrTeamMutation.mutate({
task_id: todo.id,
assignee_id: todoDetails.assigneeId,
user_type: todoDetails.userType,
})
} else if (todo.assignee && todoDetails.assigneeId) {
assignTaskToUserMutation.mutate({
task_id: todo.id,
assignee_id: todoDetails.assigneeId,
})
toast.error('Failed to update todo, please try again')
}
}

Expand Down
3 changes: 2 additions & 1 deletion lib/todo-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,13 @@ export class TodoUtil {
return updateDetails
}

static getDefaultTodoFormData = (todo: TTask): TTodoFormData => {
static getDefaultTodoFormData = (todo: TTask): Partial<TTodoFormData> => {
return {
title: todo.title,
priority: todo.priority,
dueDate: todo.dueAt || '',
description: todo.description || '',
assigneeId: todo.assignee?.id ?? '',
labels: todo.labels?.map((l) => l.id) ?? [],
}
}
Expand Down
Loading