Skip to content

Commit

Permalink
feat: task display & edit
Browse files Browse the repository at this point in the history
  • Loading branch information
aseerkt committed Feb 18, 2024
1 parent 73070a0 commit 74edac2
Show file tree
Hide file tree
Showing 26 changed files with 766 additions and 133 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4",
Expand Down
33 changes: 33 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions prisma/migrations/20240216145819_add_author_for_task/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
Warnings:
- Added the required column `reporterId` to the `tasks` table without a default value. This is not possible if the table is not empty.
- Added the required column `updatedAt` to the `tasks` table without a default value. This is not possible if the table is not empty.
*/
-- DropForeignKey
ALTER TABLE "collaborators" DROP CONSTRAINT "collaborators_projectId_fkey";

-- AlterTable
ALTER TABLE "tasks" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "reporterId" INTEGER NOT NULL,
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL;

-- AddForeignKey
ALTER TABLE "collaborators" ADD CONSTRAINT "collaborators_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_reporterId_fkey" FOREIGN KEY ("reporterId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
2 changes: 1 addition & 1 deletion prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ model Collaborator {
userId Int
role CollaboratorRole @default(WRITE)
project Project @relation(fields: [projectId], references: [id])
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id])
@@id([projectId, userId])
Expand Down
14 changes: 14 additions & 0 deletions src/app/api/tasks/[taskId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,20 @@ export async function GET(
priority: true,
description: true,
isActive: true,
createdAt: true,
updatedAt: true,
reporter: {
select: {
id: true,
username: true,
},
},
assignee: {
select: {
id: true,
username: true,
},
},
column: {
select: {
id: true,
Expand Down
43 changes: 40 additions & 3 deletions src/app/projects/new/CreateProjectForm.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
'use client';

import { InputField, TextAreaField } from '@/components/form';
import { InputField, RadioGroupField, TextAreaField } from '@/components/form';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import { Form } from '@/components/ui/form';
import { toast } from '@/components/ui/use-toast';
import { convertToGithubRepoName } from '@/lib/utils';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { createProject } from './actions';
Expand All @@ -14,18 +16,24 @@ export default function CreateProjectForm() {
defaultValues: {
name: '',
description: '',
isPublic: 'true',
},
resolver: zodResolver(createProjectSchema),
});
const {
handleSubmit,
control,
watch,
formState: { isSubmitting },
} = form;

const onSubmit = handleSubmit(async (values) => {
try {
await createProject(values);
await createProject({
...values,
name: convertToGithubRepoName(values.name),
isPublic: values.isPublic === 'true',
});
} catch (error) {
toast({
title: 'Something went wrong',
Expand All @@ -34,16 +42,45 @@ export default function CreateProjectForm() {
}
});

const projectName = convertToGithubRepoName(watch('name'));
const isPublic = watch('isPublic') === 'true';

return (
<Form {...form}>
<form onSubmit={onSubmit}>
<InputField label='Project name' name='name' control={control} />
{projectName && (
<Alert variant='success' className='mb-4'>
<AlertTitle>
Your new project will be created as <b>{projectName}</b>
</AlertTitle>
<AlertDescription>
The project name can only contain ASCII letters, digits, and the
characters ., -, and _.
</AlertDescription>
</Alert>
)}
<TextAreaField
label='Description'
name='description'
control={control}
rows={5}
/>
<RadioGroupField
name='isPublic'
control={control}
label='Visibility'
options={[
{ label: 'Public', value: 'true' },
{ label: 'Private', value: 'false' },
]}
helperText={
isPublic
? 'Anyone on the internet can see this project. You choose who can write.'
: 'You choose who can see and write to this project.'
}
/>
<Button type='submit' disabled={isSubmitting}>
<Button className='mt-5' type='submit' disabled={isSubmitting}>
Create
</Button>
</form>
Expand Down
10 changes: 8 additions & 2 deletions src/app/projects/new/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@ import { getAuthSesssion } from '@/lib/authUtils';
import prisma from '@/lib/prisma';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { CreateProjectValues } from './schema';

export const createProject = async (values: CreateProjectValues) => {
interface CreateProjectPayload {
name: string;
description: string;
isPublic: boolean;
}

export const createProject = async (values: CreateProjectPayload) => {
const session = await getAuthSesssion();
if (!session) {
throw new Error('Not Authenticated');
Expand All @@ -17,6 +22,7 @@ export const createProject = async (values: CreateProjectValues) => {
name: values.name,
description: values.description,
authorId: session?.user.id,
isPublic: values.isPublic,
columns: {
createMany: {
data: [
Expand Down
4 changes: 2 additions & 2 deletions src/app/projects/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ export const metadata: Metadata = {

export default function CreateProjectPage() {
return (
<Card className='mx-auto mt-10 max-w-[500px] w-full '>
<Card className='mx-auto mt-10 max-w-[700px] w-full '>
<CardHeader>
<CardTitle>Create project</CardTitle>
<CardDescription></CardDescription>
<CardDescription>A project contains all the tasks</CardDescription>
</CardHeader>
<CardContent>
<CreateProjectForm />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
import { debounce } from 'lodash';
import { RefObject, useEffect, useId, useState } from 'react';
import { useImmer } from 'use-immer';
import AddTask from '../AddTask';
import { AddTask } from '../(task)';
import { useProjectAccess } from '../ProjectContext';
import { BoardData } from '../types';
import ColumnCard from './ColumnCard';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client';

import { InputField, MdEditorField, SelectField } from '@/components/form';
import { Button } from '@/components/ui/button';
import {
Expand All @@ -15,9 +17,10 @@ import { TaskPriority } from '@prisma/client';
import { DialogClose } from '@radix-ui/react-dialog';
import { PlusCircleIcon } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { priorityOptions, taskPriorityBgColors } from '../constants';
import { AddTaskSchema, addTaskSchema } from '../schemas';
import { AssigneeAutocomplete } from './AssigneeAutocomplete';
import { createTask } from './actions';
import { priorityOptions, taskPriorityBgColors } from './constants';
import { addTaskSchema } from './schemas';

interface AddTaskProps {
projectId: number;
Expand All @@ -30,8 +33,12 @@ export default function AddTask({
columnId,
onClose,
}: AddTaskProps) {
const form = useForm({
defaultValues: { title: '', priority: TaskPriority.LOW, description: '' },
const form = useForm<AddTaskSchema>({
defaultValues: {
title: '',
priority: TaskPriority.LOW,
description: '',
},
disabled: !columnId,
resolver: zodResolver(addTaskSchema),
});
Expand All @@ -42,6 +49,7 @@ export default function AddTask({
projectId,
columnId: columnId!,
...values,
assigneeId: values.assignee?.id,
});
onClose();
} catch (error) {
Expand Down Expand Up @@ -87,6 +95,12 @@ export default function AddTask({
control={form.control}
label='Short description'
/>
<AssigneeAutocomplete
name='assignee'
control={form.control}
projectId={projectId}
label='Assignee'
/>
</form>
<DialogFooter className='justify-end'>
<DialogClose asChild>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { AsyncAutocomplete } from '@/components/form';
import { useSession } from 'next-auth/react';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { Control } from 'react-hook-form';

interface AssigneeAutocompleteProps {
projectId: number;
name: string;
control: Control<any>;
label?: string;
}

interface Assignee {
id: number;
username: string;
name: string;
}

export function AssigneeAutocomplete({
projectId,
name,
control,
label,
}: AssigneeAutocompleteProps) {
const session = useSession();
const [assigneeOptions, setAssigneeOptions] = useState<Assignee[]>([
session.data!.user,
]);

const fetchAssignees = (username?: string) => {
const searchParams = new URLSearchParams();
if (username?.length) {
searchParams.set('username', username);
}
const currentUser = session.data!.user;
fetch(
`${
window.location.origin
}/api/projects/${projectId}/collaborators?${searchParams.toString()}`
)
.then((res) => res.json())
.then((result) =>
setAssigneeOptions(
[currentUser].concat(...(Array.isArray(result) ? result : []))
)
);
};

useEffect(() => {
fetchAssignees();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<AsyncAutocomplete
name={name}
control={control}
options={assigneeOptions}
onSearch={fetchAssignees}
getOptionValue={(option) => ({
id: option.id,
username: option.username,
})}
getSearchValue={(option) => option.username}
renderOption={(option) => (
<div className='p-2 flex items-center gap-2 hover:bg-gray-100 cursor-pointer rounded-sm'>
<b className='font-semibold'>{option.username}</b>
<span className='text-gray-500'>{option.name}</span>
</div>
)}
renderValue={(value) => (
<Link href={`/users/${value.username}`}>{value.username}</Link>
)}
optionKey='username'
valueKey='username'
label={label}
placeholder='Select assignee'
autocompletePlaceholder='Search by username'
noResultPlaceholder='No assignees found'
/>
);
}
Loading

0 comments on commit 74edac2

Please sign in to comment.