From 1abcdb766e9b7d4caa6cef4a8e3451abc5da1e0b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 09:33:38 -0400 Subject: [PATCH 1/3] chore(deps): bump @types/three from 0.177.0 to 0.180.0 (#1494) Bumps [@types/three](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/three) from 0.177.0 to 0.180.0. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/three) --- updated-dependencies: - dependency-name: "@types/three" dependency-version: 0.180.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/app/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/app/package.json b/apps/app/package.json index 06f65ef17..a3f25721d 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -48,7 +48,7 @@ "@trigger.dev/sdk": "4", "@trycompai/db": "^1.3.4", "@types/canvas-confetti": "^1.9.0", - "@types/three": "^0.177.0", + "@types/three": "^0.180.0", "@uploadthing/react": "^7.3.0", "@upstash/ratelimit": "^2.0.5", "@vercel/sdk": "^1.7.1", From 5b406a2d77ba5ac4a314a83b7ad262c3005a489f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 09:33:50 -0400 Subject: [PATCH 2/3] chore(deps): bump @radix-ui/react-accordion from 1.2.11 to 1.2.12 (#1492) Bumps [@radix-ui/react-accordion](https://github.com/radix-ui/primitives) from 1.2.11 to 1.2.12. - [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md) - [Commits](https://github.com/radix-ui/primitives/commits) --- updated-dependencies: - dependency-name: "@radix-ui/react-accordion" dependency-version: 1.2.12 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- packages/ui/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/package.json b/packages/ui/package.json index cb81f1d38..08086883d 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -8,7 +8,7 @@ "tailwind-merge" ], "dependencies": { - "@radix-ui/react-accordion": "1.2.11", + "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-alert-dialog": "1.1.14", "@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-checkbox": "1.3.2", From 2f203be5386426c1ccf711ec002fa9f9859c02c0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 10:03:37 -0400 Subject: [PATCH 3/3] [dev] [Marfuen] mariano/create-task (#1495) * feat: implement task creation functionality with validation and UI integration - Added a new action for creating tasks with input validation using Zod. - Implemented a CreateTaskSheet component for the task creation UI, integrating form handling and submission. - Updated TaskList and TaskFilterHeader components to include the new task creation functionality and controls retrieval. - Enhanced the TasksPage to fetch controls for task assignment. These changes improve task management capabilities within the application. * refactor: optimize task sheet component by replacing useMemo with useCallback - Changed the filter function in CreateTaskSheet from useMemo to useCallback to improve performance and prevent unnecessary re-renders. - Simplified the computation of selected options by removing the memoization, as it is now computed inline based on field.value. These changes enhance the efficiency of the CreateTaskSheet component in the task creation process. --------- Co-authored-by: Mariano Fuentes --- .../src/actions/tasks/create-task-action.ts | 79 +++++ .../tasks/components/CreateTaskSheet.tsx | 328 ++++++++++++++++++ .../tasks/components/TaskFilterHeader.tsx | 87 +++-- .../[orgId]/tasks/components/TaskList.tsx | 6 + apps/app/src/app/(app)/[orgId]/tasks/page.tsx | 30 +- 5 files changed, 494 insertions(+), 36 deletions(-) create mode 100644 apps/app/src/actions/tasks/create-task-action.ts create mode 100644 apps/app/src/app/(app)/[orgId]/tasks/components/CreateTaskSheet.tsx diff --git a/apps/app/src/actions/tasks/create-task-action.ts b/apps/app/src/actions/tasks/create-task-action.ts new file mode 100644 index 000000000..b04552813 --- /dev/null +++ b/apps/app/src/actions/tasks/create-task-action.ts @@ -0,0 +1,79 @@ +'use server'; + +import { authActionClient } from '@/actions/safe-action'; +import { db, Departments, TaskFrequency } from '@db'; +import { revalidatePath } from 'next/cache'; +import { headers } from 'next/headers'; +import { z } from 'zod'; + +const createTaskSchema = z.object({ + title: z.string().min(1, { + message: 'Title is required', + }), + description: z.string().min(1, { + message: 'Description is required', + }), + assigneeId: z.string().nullable().optional(), + frequency: z.nativeEnum(TaskFrequency).nullable().optional(), + department: z.nativeEnum(Departments).nullable().optional(), + controlIds: z.array(z.string()).optional(), +}); + +export const createTaskAction = authActionClient + .inputSchema(createTaskSchema) + .metadata({ + name: 'create-task', + track: { + event: 'create-task', + channel: 'server', + }, + }) + .action(async ({ parsedInput, ctx }) => { + const { title, description, assigneeId, frequency, department, controlIds } = parsedInput; + const { + session: { activeOrganizationId }, + user, + } = ctx; + + if (!user.id || !activeOrganizationId) { + throw new Error('Invalid user input'); + } + + try { + const task = await db.task.create({ + data: { + title, + description, + assigneeId: assigneeId || null, + organizationId: activeOrganizationId, + status: 'todo', + order: 0, + frequency: frequency || null, + department: department || null, + ...(controlIds && + controlIds.length > 0 && { + controls: { + connect: controlIds.map((id) => ({ id })), + }, + }), + }, + }); + + // Revalidate the path based on the header + const headersList = await headers(); + let path = headersList.get('x-pathname') || headersList.get('referer') || ''; + path = path.replace(/\/[a-z]{2}\//, '/'); + revalidatePath(path); + + return { + success: true, + task, + }; + } catch (error) { + console.error('Failed to create task:', error); + return { + success: false, + error: 'Failed to create task', + }; + } + }); diff --git a/apps/app/src/app/(app)/[orgId]/tasks/components/CreateTaskSheet.tsx b/apps/app/src/app/(app)/[orgId]/tasks/components/CreateTaskSheet.tsx new file mode 100644 index 000000000..7d0a1b5fe --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/tasks/components/CreateTaskSheet.tsx @@ -0,0 +1,328 @@ +'use client'; + +import { createTaskAction } from '@/actions/tasks/create-task-action'; +import { SelectAssignee } from '@/components/SelectAssignee'; +import { Button } from '@comp/ui/button'; +import { Drawer, DrawerContent, DrawerTitle } from '@comp/ui/drawer'; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@comp/ui/form'; +import { useMediaQuery } from '@comp/ui/hooks'; +import { Input } from '@comp/ui/input'; +import MultipleSelector, { Option } from '@comp/ui/multiple-selector'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@comp/ui/select'; +import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@comp/ui/sheet'; +import { Textarea } from '@comp/ui/textarea'; +import { Departments, Member, TaskFrequency, User } from '@db'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { ArrowRightIcon, X } from 'lucide-react'; +import { useAction } from 'next-safe-action/hooks'; +import { useQueryState } from 'nuqs'; +import { useCallback, useMemo } from 'react'; +import { useForm } from 'react-hook-form'; +import { toast } from 'sonner'; +import { z } from 'zod'; +import { taskDepartments, taskFrequencies } from '../[taskId]/components/constants'; + +const createTaskSchema = z.object({ + title: z.string().min(1, { + message: 'Title is required', + }), + description: z.string().min(1, { + message: 'Description is required', + }), + assigneeId: z.string().nullable().optional(), + frequency: z.nativeEnum(TaskFrequency).nullable().optional(), + department: z.nativeEnum(Departments).nullable().optional(), + controlIds: z.array(z.string()).optional(), +}); + +export function CreateTaskSheet({ + members, + controls, +}: { + members: (Member & { user: User })[]; + controls: { id: string; name: string }[]; +}) { + const isDesktop = useMediaQuery('(min-width: 768px)'); + const [createTaskOpen, setCreateTaskOpen] = useQueryState('create-task'); + const isOpen = Boolean(createTaskOpen); + + const handleOpenChange = (open: boolean) => { + setCreateTaskOpen(open ? 'true' : null); + }; + + const createTask = useAction(createTaskAction, { + onSuccess: () => { + toast.success('Task created successfully'); + setCreateTaskOpen(null); + form.reset(); + }, + onError: (error) => { + toast.error(error.error?.serverError || 'Failed to create task'); + }, + }); + + const form = useForm>({ + resolver: zodResolver(createTaskSchema), + defaultValues: { + title: '', + description: '', + assigneeId: null, + frequency: null, + department: null, + controlIds: [], + }, + }); + + const onSubmit = useCallback( + (data: z.infer) => { + createTask.execute(data); + }, + [createTask], + ); + + // Memoize control options to prevent re-renders + const controlOptions = useMemo( + () => + controls.map((control) => ({ + value: control.id, + label: control.name, + })), + [controls], + ); + + // Memoize filter function to prevent re-renders + const filterFunction = useCallback( + (value: string, search: string) => { + // Find the option with this value (control ID) + const option = controlOptions.find((opt) => opt.value === value); + if (!option) return 0; + + // Check if the control name (label) contains the search string + return option.label.toLowerCase().includes(search.toLowerCase()) ? 1 : 0; + }, + [controlOptions], + ); + + // Memoize select handlers + const handleFrequencyChange = useCallback((value: string, onChange: (value: any) => void) => { + onChange(value === 'none' ? null : value); + }, []); + + const handleDepartmentChange = useCallback((value: string, onChange: (value: any) => void) => { + onChange(value === 'none' ? null : value); + }, []); + + const handleControlsChange = useCallback((options: Option[], onChange: (value: any) => void) => { + onChange(options.map((option) => option.value)); + }, []); + + const taskForm = ( +
+ + ( + + Task Title + + + + + + )} + /> + + ( + + Description + +