Repository: copilot-platforms/tasks-app
Pull request #705: OUT-1558, OUT-1559 | Add "Create subtask" button and new subtask form
Author: login: arpandhakal
Created: 2025-03-21T11:01:43Z | Merged: 2025-03-25T12:35:40Z

## Changes

- [x] designed a new component : SubTaskWrapper.
- [x] and its sub components : AddSubTaskBtn, NewTaskCard.
- [x] managed states to store form data for sub task creation.
- [x] added validation for sub tasks field.
- [x] added template icon and template selection functionality in create sub task form.
- [x] manage template end option added to template secletor in sub task creation form.
- [x] added a function to apply templates to fill up title, description, workflowstate from template to sub task form.
- [x] sub task creation API implementation.

## Testing Criteria

- [LOOM](https://www.loom.com/share/43c6d6cf1a8d4432acdb07da1d26b433)
- [LOOM-sub-task-creation](https://www.loom.com/share/8d9d78571a594834a73106e1b2285f87)

## Notes

- Remaining task : sub task creation API implementation and optimistic updates. Some design changes needed to do (ref : LOOM)





Diff:
diff --git a/src/app/detail/[task_id]/[user_type]/page.tsx b/src/app/detail/[task_id]/[user_type]/page.tsx
index 4ffdc8283..b77a5fdff 100644
--- a/src/app/detail/[task_id]/[user_type]/page.tsx
+++ b/src/app/detail/[task_id]/[user_type]/page.tsx
@@ -20,6 +20,7 @@ import { MenuBoxContainer } from '@/app/detail/ui/MenuBoxContainer'
 import { ResponsiveStack } from '@/app/detail/ui/ResponsiveStack'
 import { Sidebar, SidebarSkeleton } from '@/app/detail/ui/Sidebar'
 import { StyledBox, StyledTiptapDescriptionWrapper, TaskDetailsContainer } from '@/app/detail/ui/styledComponent'
+import { Subtasks } from '@/app/detail/ui/Subtasks'
 import { TaskEditor } from '@/app/detail/ui/TaskEditor'
 import { ToggleButtonContainer } from '@/app/detail/ui/ToggleButtonContainer'
 import { ToggleController } from '@/app/detail/ui/ToggleController'
@@ -157,7 +158,7 @@ export default async function TaskDetailPage({
                     userType={params.user_type}
                   />
                 </StyledTiptapDescriptionWrapper>
-
+                <Subtasks />
                 <ActivityWrapper task_id={task_id} token={token} tokenPayload={tokenPayload} />
               </TaskDetailsContainer>
             </CustomScrollBar>
diff --git a/src/app/detail/ui/ActivityWrapper.tsx b/src/app/detail/ui/ActivityWrapper.tsx
index bdf7c14c4..5942c2291 100644
--- a/src/app/detail/ui/ActivityWrapper.tsx
+++ b/src/app/detail/ui/ActivityWrapper.tsx
@@ -186,7 +186,7 @@ export const ActivityWrapper = ({
 
   return (
     <Box width="100%">
-      <Stack direction="column" alignItems="left" p="16px 0px" rowGap={4}>
+      <Stack direction="column" alignItems="left" p="24px 0px" rowGap={4}>
         <Typography variant="lg">Activity</Typography>
         {isLoading ? (
           <Stack direction="column" rowGap={5}>
diff --git a/src/app/detail/ui/NewTaskCard.tsx b/src/app/detail/ui/NewTaskCard.tsx
new file mode 100644
index 000000000..6469fe180
--- /dev/null
+++ b/src/app/detail/ui/NewTaskCard.tsx
@@ -0,0 +1,455 @@
+import { handleCreate } from '@/app/actions'
+import { CopilotAvatar } from '@/components/atoms/CopilotAvatar'
+import { MiniLoader } from '@/components/atoms/MiniLoader'
+import AttachmentLayout from '@/components/AttachmentLayout'
+import { IconBtn } from '@/components/buttons/IconBtn'
+import { ManageTemplatesEndOption } from '@/components/buttons/ManageTemplatesEndOptions'
+import { PrimaryBtn } from '@/components/buttons/PrimaryBtn'
+import { SecondaryBtn } from '@/components/buttons/SecondaryBtn'
+import { DatePickerComponent } from '@/components/inputs/DatePickerComponent'
+import Selector, { SelectorType } from '@/components/inputs/Selector'
+import { WorkflowStateSelector } from '@/components/inputs/Selector-WorkflowState'
+import { StyledTextField } from '@/components/inputs/TextField'
+import { MAX_UPLOAD_LIMIT } from '@/constants/attachments'
+import { useHandleSelectorComponent } from '@/hooks/useHandleSelectorComponent'
+import { AssigneePlaceholderSmall, TempalteIconMd, TemplateIconSm } from '@/icons'
+import { selectAuthDetails } from '@/redux/features/authDetailsSlice'
+import { selectTaskBoard } from '@/redux/features/taskBoardSlice'
+import { selectCreateTemplate } from '@/redux/features/templateSlice'
+import { DateString } from '@/types/date'
+import { CreateTaskRequestSchema } from '@/types/dto/tasks.dto'
+import { WorkflowStateResponse } from '@/types/dto/workflowStates.dto'
+import {
+  AssigneeType,
+  CreateTaskErrors,
+  HandleSelectorComponentModes,
+  IAssigneeCombined,
+  ITemplate,
+} from '@/types/interfaces'
+import { getAssigneeName } from '@/utils/assignee'
+import { getAssigneeTypeCorrected } from '@/utils/getAssigneeTypeCorrected'
+import { deleteEditorAttachmentsHandler, uploadImageHandler } from '@/utils/inlineImage'
+import { NoAssigneeExtraOptions } from '@/utils/noAssignee'
+import { trimAllTags } from '@/utils/trimTags'
+import { setDebouncedFilteredAssignees } from '@/utils/users'
+import { Box, Stack, Typography } from '@mui/material'
+import dayjs from 'dayjs'
+import { useCallback, useEffect, useState } from 'react'
+import { useSelector } from 'react-redux'
+import { Tapwrite } from 'tapwrite'
+import { string, z } from 'zod'
+
+interface IErrors {
+  [CreateTaskErrors.ASSIGNEE]: boolean
+}
+
+interface SubTaskFields {
+  title: string
+  description: string
+  workflowStateId: string
+  assigneeId: string | null
+  dueDate: DateString | null
+  errors: IErrors
+  assigneeType?: AssigneeType | null
+}
+
+export const NewTaskCard = ({ handleClose }: { handleClose: () => void }) => {
+  const { workflowStates, assignee, token, filterOptions, previewMode, activeTask } = useSelector(selectTaskBoard)
+  const { templates } = useSelector(selectCreateTemplate)
+  const [activeDebounceTimeoutId, setActiveDebounceTimeoutId] = useState<NodeJS.Timeout | null>(null)
+  const [loading, setLoading] = useState(false)
+
+  const [isEditorReadonly, setIsEditorReadonly] = useState(false)
+
+  const { tokenPayload } = useSelector(selectAuthDetails)
+  const [subTaskFields, setSubTaskFields] = useState<SubTaskFields>({
+    title: '',
+    description: '',
+    workflowStateId: '',
+    assigneeId: '',
+    dueDate: null,
+    errors: {
+      [CreateTaskErrors.ASSIGNEE]: false,
+    },
+    assigneeType: null,
+  })
+
+  const [filteredAssignees, setFilteredAssignees] = useState(assignee)
+
+  const [isUploading, setIsUploading] = useState(false)
+
+  const clearSubTaskFields = () => {
+    setSubTaskFields((prev) => ({
+      ...prev,
+      title: '',
+      description: '',
+      errors: {
+        [CreateTaskErrors.ASSIGNEE]: false,
+      },
+    }))
+  }
+
+  const handleFieldChange = (field: keyof SubTaskFields, value: string | DateString | IErrors | null) => {
+    setSubTaskFields((prev) => ({
+      ...prev,
+      [field]: value,
+    }))
+  }
+
+  const uploadFn =
+    token && tokenPayload?.workspaceId
+      ? (file: File) => uploadImageHandler(file, token, tokenPayload.workspaceId, null)
+      : undefined
+
+  const todoWorkflowState = workflowStates.find((el) => el.key === 'todo') || workflowStates[0]
+
+  useEffect(() => {
+    handleFieldChange('workflowStateId', todoWorkflowState.id)
+  }, [todoWorkflowState])
+
+  const { renderingItem: _statusValue, updateRenderingItem: updateStatusValue } = useHandleSelectorComponent({
+    item: todoWorkflowState,
+    type: SelectorType.STATUS_SELECTOR,
+  })
+
+  const statusValue = _statusValue as WorkflowStateResponse
+
+  const { renderingItem: _assigneeValue, updateRenderingItem: updateAssigneeValue } = useHandleSelectorComponent({
+    item: null,
+    type: SelectorType.ASSIGNEE_SELECTOR,
+  })
+  const assigneeValue = _assigneeValue as IAssigneeCombined
+
+  const { renderingItem: _templateValue, updateRenderingItem: updateTemplateValue } = useHandleSelectorComponent({
+    item: undefined,
+    type: SelectorType.TEMPLATE_SELECTOR,
+  })
+  const templateValue = _templateValue as ITemplate
+
+  const [inputStatusValue, setInputStatusValue] = useState('')
+
+  const handleUploadStatusChange = (uploading: boolean) => {
+    setIsUploading(uploading)
+  }
+
+  const applyTemplate = useCallback(
+    (id: string, templateTitle: string) => {
+      const controller = new AbortController()
+
+      const fetchTemplate = async () => {
+        try {
+          setIsEditorReadonly?.(true)
+
+          if (!subTaskFields.title.trim()) {
+            setSubTaskFields((prev) => ({
+              ...prev,
+              title: templateTitle,
+            }))
+          } else {
+            setSubTaskFields((prev) => ({
+              ...prev,
+              title: prev.title + ' ' + templateTitle,
+            }))
+          }
+          const resp = await fetch(`/api/tasks/templates/${id}/apply?token=${token}`, {
+            signal: controller.signal,
+          })
+          const { data: template } = await resp.json()
+          setIsEditorReadonly?.(false)
+
+          updateStatusValue(workflowStates.find((state) => state.id === template.workflowStateId))
+          setSubTaskFields((prev) => ({
+            ...prev,
+            workflowStateId: template.workflowStateId,
+          }))
+          const trimmedAppliedDescription = template.description && trimAllTags(template.description)
+          const trimmedDescription = trimAllTags(subTaskFields.description)
+          if (trimmedAppliedDescription == trimmedDescription || trimmedDescription === '<p></p>') {
+            setSubTaskFields((prev) => ({
+              ...prev,
+              description: template.body,
+            }))
+          } else {
+            setSubTaskFields((prev) => ({
+              ...prev,
+              description: prev.description + template.body,
+            }))
+          }
+        } catch (error) {
+          console.error('error applying template')
+        } finally {
+          setIsEditorReadonly?.(false)
+        }
+      }
+      fetchTemplate()
+      return controller
+    },
+    [token, setIsEditorReadonly, workflowStates, subTaskFields.title, subTaskFields.description],
+  )
+
+  const applyTemplateHandler = (newValue: ITemplate) => {
+    if (!newValue || !token) return
+    const controller = applyTemplate(newValue.id, newValue.title)
+    return () => {
+      controller.abort()
+    }
+  }
+
+  const handleTaskCreation = async () => {
+    if (subTaskFields.title && subTaskFields.assigneeId && subTaskFields.assigneeType) {
+      const formattedDueDate = subTaskFields.dueDate && dayjs(new Date(subTaskFields.dueDate)).format('YYYY-MM-DD')
+
+      const payload = {
+        title: subTaskFields.title,
+        body: subTaskFields.description,
+        workflowStateId: subTaskFields.workflowStateId,
+        assigneeType: subTaskFields.assigneeType,
+        assigneeId: subTaskFields.assigneeId,
+        dueDate: formattedDueDate,
+        parentId: activeTask?.id,
+      }
+
+      clearSubTaskFields()
+      await handleCreate(token as string, CreateTaskRequestSchema.parse(payload))
+    }
+  }
+
+  return (
+    <Stack
+      direction="column"
+      sx={{
+        display: 'flex',
+        padding: '12px 0px',
+        alignItems: 'flex-start',
+        alignSelf: 'stretch',
+        borderRadius: '4px',
+        border: (theme) => `1px solid ${theme.color.borders.border}`,
+        boxShadow: '0px 6px 20px 0px rgba(0,0,0, 0.07)',
+      }}
+    >
+      <Stack
+        direction="column"
+        sx={{ display: 'flex', padding: '0px 12px 12px', alignItems: 'center', gap: '4px', alignSelf: 'stretch' }}
+      >
+        <Stack direction="row" sx={{ display: 'flex', alignItems: 'flex-end', gap: '4px', alignSelf: 'stretch' }}>
+          <StyledTextField
+            type="text"
+            multiline
+            borderLess
+            sx={{
+              width: '100%',
+              '& .MuiInputBase-input': {
+                fontSize: '16px',
+                lineHeight: '24px',
+                color: (theme) => theme.color.gray[600],
+                fontWeight: 500,
+              },
+              '& .MuiInputBase-input.Mui-disabled': {
+                WebkitTextFillColor: (theme) => theme.color.gray[600],
+              },
+              '& .MuiInputBase-root': {
+                padding: '0px 0px',
+              },
+            }}
+            placeholder="Task name"
+            value={subTaskFields.title}
+            onChange={(event) => {
+              handleFieldChange('title', event.target.value)
+            }}
+            inputProps={{ maxLength: 255 }}
+            onKeyDown={(e) => {
+              if (e.key === 'Enter') {
+                e.preventDefault() //prevent users from breaking line
+              }
+            }}
+            disabled={isEditorReadonly}
+          />
+
+          <Selector
+            padding="5px"
+            inputStatusValue={inputStatusValue}
+            setInputStatusValue={setInputStatusValue}
+            getSelectedValue={(_newValue) => {
+              const newValue = _newValue as ITemplate
+              updateTemplateValue(newValue)
+              applyTemplateHandler(newValue)
+            }}
+            startIcon={<TempalteIconMd />}
+            options={templates || []}
+            placeholder="Search..."
+            value={templateValue}
+            selectorType={SelectorType.TEMPLATE_SELECTOR}
+            endOption={<ManageTemplatesEndOption />}
+            endOptionHref={`/manage-templates?token=${token}`}
+            listAutoHeightMax="147px"
+            disableOutline
+            responsiveNoHide
+            buttonWidth="auto"
+            useClickHandler
+          />
+        </Stack>
+
+        <Box sx={{ height: '100%', width: '100%' }}>
+          <Tapwrite
+            content={subTaskFields.description}
+            getContent={(content) => handleFieldChange('description', content)}
+            placeholder="Add description.."
+            editorClass="tapwrite-task-editor"
+            uploadFn={uploadFn}
+            deleteEditorAttachments={(url) => deleteEditorAttachmentsHandler(url, token ?? '', null, null)}
+            attachmentLayout={(props) => (
+              <AttachmentLayout {...props} isComment={true} onUploadStatusChange={handleUploadStatusChange} />
+            )}
+            maxUploadLimit={MAX_UPLOAD_LIMIT}
+            parentContainerStyle={{ gap: '0px' }}
+            readonly={isEditorReadonly}
+          />
+        </Box>
+      </Stack>
+      <Stack
+        direction="row"
+        columnGap={'24px'}
+        rowGap={'12px'}
+        sx={{
+          display: 'flex',
+          padding: '0px 12px',
+          alignItems: 'center',
+
+          alignSelf: 'stretch',
+          justifyContent: 'space-between',
+          flexWrap: 'wrap',
+        }}
+      >
+        <Stack
+          direction="row"
+          sx={{
+            display: 'flex',
+            alignItems: 'center',
+            gap: '8px',
+            alignSelf: 'stretch',
+            flexWrap: 'wrap',
+          }}
+        >
+          <WorkflowStateSelector
+            option={workflowStates}
+            value={statusValue}
+            getValue={(value) => {
+              updateStatusValue(value)
+              handleFieldChange('workflowStateId', value.id)
+            }}
+            padding={'4px 8px'}
+          />
+          <Selector
+            padding={'4px 8px'}
+            inputStatusValue={inputStatusValue}
+            setInputStatusValue={setInputStatusValue}
+            placeholder="Set assignee"
+            getSelectedValue={(_newValue) => {
+              handleFieldChange('errors', {
+                assignee: false,
+              })
+              const newValue = _newValue as IAssigneeCombined
+
+              updateAssigneeValue(newValue)
+              handleFieldChange('assigneeType', getAssigneeTypeCorrected(newValue))
+              handleFieldChange('assigneeId', newValue?.id)
+            }}
+            startIcon={assigneeValue ? <CopilotAvatar currentAssignee={assigneeValue} /> : <AssigneePlaceholderSmall />}
+            onClick={() => {
+              if (activeDebounceTimeoutId) {
+                clearTimeout(activeDebounceTimeoutId)
+              }
+              setLoading(true)
+              setFilteredAssignees(assignee)
+              setLoading(false)
+            }}
+            options={loading ? [] : filteredAssignees}
+            value={assigneeValue}
+            extraOption={NoAssigneeExtraOptions}
+            extraOptionRenderer={(setAnchorEl, anchorEl, props) => {
+              return <>{loading && <MiniLoader />}</>
+            }}
+            selectorType={SelectorType.ASSIGNEE_SELECTOR}
+            handleInputChange={async (newInputValue: string) => {
+              if (!newInputValue) {
+                setFilteredAssignees(assignee)
+                return
+              }
+              setDebouncedFilteredAssignees(
+                activeDebounceTimeoutId,
+                setActiveDebounceTimeoutId,
+                setLoading,
+                setFilteredAssignees,
+                z.string().parse(token),
+                newInputValue,
+              )
+            }}
+            filterOption={(x: unknown) => x}
+            buttonHeight="auto"
+            buttonContent={
+              <Typography
+                variant="bodySm"
+                sx={{
+                  color: (theme) => (assigneeValue ? theme.color.gray[600] : theme.color.gray[550]),
+                  textOverflow: 'ellipsis',
+                  whiteSpace: 'nowrap',
+
+                  overflow: 'hidden',
+                  fontSize: '12px',
+                  maxWidth: '120px',
+                }}
+              >
+                {getAssigneeName(assigneeValue as IAssigneeCombined, 'Assignee')}
+              </Typography>
+            }
+            error={subTaskFields.errors.assignee}
+            errorPlaceholder=""
+          />
+          <DatePickerComponent
+            padding={'4px 8px'}
+            getDate={(value) => handleFieldChange('dueDate', value)}
+            isButton={true}
+          />
+        </Stack>
+        <Stack
+          direction="row"
+          sx={{
+            display: 'flex',
+            alignItems: 'center',
+            justifyContent: 'flex-end',
+            gap: '16px',
+            alignSelf: 'stretch',
+
+            marginLeft: 'auto',
+          }}
+        >
+          <SecondaryBtn
+            padding={'4px 8px'}
+            handleClick={handleClose}
+            buttonContent={
+              <Typography variant="sm" sx={{ color: (theme) => theme.color.gray[700] }}>
+                Discard
+              </Typography>
+            }
+          />
+          <PrimaryBtn
+            padding={'4px 8px'}
+            handleClick={() => {
+              const hasAssigneeError = !subTaskFields.assigneeId
+
+              if (hasAssigneeError) {
+                handleFieldChange('errors', {
+                  assignee: true,
+                })
+              } else {
+                handleTaskCreation()
+              }
+            }}
+            buttonText="Create"
+            disabled={!subTaskFields.title.trim() || isUploading || isEditorReadonly}
+          />
+        </Stack>
+      </Stack>
+    </Stack>
+  )
+}
diff --git a/src/app/detail/ui/Subtasks.tsx b/src/app/detail/ui/Subtasks.tsx
new file mode 100644
index 000000000..ad697107d
--- /dev/null
+++ b/src/app/detail/ui/Subtasks.tsx
@@ -0,0 +1,25 @@
+'use client'
+
+import { useState } from 'react'
+import { Stack } from '@mui/material'
+
+import { NewTaskCard } from '@/app/detail/ui/NewTaskCard'
+import { GhostBtn } from '@/components/buttons/GhostBtn'
+import { GrayAddMediumIcon } from '@/icons'
+
+export const Subtasks = () => {
+  const [openTaskForm, setOpenTaskForm] = useState(false)
+
+  const handleFormCancel = () => {
+    setOpenTaskForm(false)
+  }
+  const handleFormOpen = () => {
+    setOpenTaskForm(!openTaskForm)
+  }
+  return (
+    <Stack direction="column" rowGap={'8px'} width="100%" sx={{ padding: '24px 0px 0px' }}>
+      <GhostBtn buttonText="Create subtask" handleClick={handleFormOpen} startIcon={<GrayAddMediumIcon />} />
+      {openTaskForm && <NewTaskCard handleClose={handleFormCancel} />}
+    </Stack>
+  )
+}
diff --git a/src/app/ui/NewTaskForm.tsx b/src/app/ui/NewTaskForm.tsx
index 268d75a9d..11936610e 100644
--- a/src/app/ui/NewTaskForm.tsx
+++ b/src/app/ui/NewTaskForm.tsx
@@ -1,6 +1,7 @@
 import { CopilotAvatar } from '@/components/atoms/CopilotAvatar'
 import { MiniLoader } from '@/components/atoms/MiniLoader'
 import AttachmentLayout from '@/components/AttachmentLayout'
+import { ManageTemplatesEndOption } from '@/components/buttons/ManageTemplatesEndOptions'
 import { PrimaryBtn } from '@/components/buttons/PrimaryBtn'
 import { SecondaryBtn } from '@/components/buttons/SecondaryBtn'
 import { DatePickerComponent } from '@/components/inputs/DatePickerComponent'
@@ -10,7 +11,7 @@ import { StyledTextField } from '@/components/inputs/TextField'
 import { MAX_UPLOAD_LIMIT } from '@/constants/attachments'
 import { AppMargin, SizeofAppMargin } from '@/hoc/AppMargin'
 import { useHandleSelectorComponent } from '@/hooks/useHandleSelectorComponent'
-import { ArrowRightIcon, AssigneePlaceholderSmall, CloseIcon, TemplateIconSm } from '@/icons'
+import { AssigneePlaceholderSmall, CloseIcon, TemplateIconSm } from '@/icons'
 import { selectAuthDetails } from '@/redux/features/authDetailsSlice'
 import {
   selectCreateTask,
@@ -373,35 +374,6 @@ const NewTaskFooter = ({
     },
     [token, setIsEditorReadonly, workflowStates, title, description, appliedDescription, appliedTitle],
   )
-  const ManageTemplatesEndOption = () => {
-    return (
-      <Stack
-        id="manage-templates-btn"
-        key={'Manage templates'}
-        direction="row"
-        pl="16px"
-        py="6px"
-        justifyContent="space-between"
-        sx={{
-          borderTop: (theme) => `1px solid ${theme.color.borders.borderDisabled}`,
-          borderBottom: (theme) => `0px solid ${theme.color.borders.borderDisabled}`,
-          cursor: 'pointer',
-          lineHeight: '21px',
-          ':hover': (theme) => ({
-            zIndex: '999',
-            bgcolor: theme.color.background.bgHover,
-          }),
-        }}
-      >
-        <Typography variant="sm">
-          <Box display="flex" gap="4px" alignItems="center" sx={{ color: (theme) => theme.color.gray[600] }}>
-            Manage templates
-            <ArrowRightIcon />
-          </Box>
-        </Typography>
-      </Stack>
-    )
-  }
 
   const applyTemplateHandler = (newValue: ITemplate) => {
     if (!newValue || !token) return
@@ -454,7 +426,7 @@ const NewTaskFooter = ({
                 handleClick={() => handleClose()}
                 buttonContent={
                   <Typography variant="sm" sx={{ color: (theme) => theme.color.gray[700] }}>
-                    Cancel
+                    Discard
                   </Typography>
                 }
               />
diff --git a/src/components/buttons/GhostBtn.tsx b/src/components/buttons/GhostBtn.tsx
new file mode 100644
index 000000000..675d220d0
--- /dev/null
+++ b/src/components/buttons/GhostBtn.tsx
@@ -0,0 +1,50 @@
+'use client'
+
+import { Box, Stack, Typography } from '@mui/material'
+import { ReactNode } from 'react'
+
+export const GhostBtn = ({
+  buttonText,
+  handleClick,
+  startIcon,
+}: {
+  buttonText: string
+  handleClick: () => void
+  startIcon?: ReactNode
+}) => {
+  return (
+    <Box
+      sx={{
+        display: 'flex',
+        justifyContent: 'flex-start',
+        alignItems: 'center',
+      }}
+    >
+      <Stack
+        direction="row"
+        columnGap={'6px'}
+        alignItems={'center'}
+        sx={{
+          padding: '5px',
+          ':hover': {
+            backgroundColor: (theme) => theme.color.gray[100],
+          },
+          borderRadius: '4px',
+          userSelect: 'none',
+        }}
+        onClick={handleClick}
+      >
+        {startIcon && startIcon}
+        <Typography
+          sx={{
+            color: (theme) => theme.color.text.textSecondary,
+          }}
+          lineHeight="22px"
+          variant="md"
+        >
+          {buttonText}
+        </Typography>
+      </Stack>
+    </Box>
+  )
+}
diff --git a/src/components/buttons/ManageTemplatesEndOptions.tsx b/src/components/buttons/ManageTemplatesEndOptions.tsx
new file mode 100644
index 000000000..26a5d95ba
--- /dev/null
+++ b/src/components/buttons/ManageTemplatesEndOptions.tsx
@@ -0,0 +1,32 @@
+import { ArrowRightIcon } from '@/icons'
+import { Box, Stack, Typography } from '@mui/material'
+
+export const ManageTemplatesEndOption = () => {
+  return (
+    <Stack
+      id="manage-templates-btn"
+      key={'Manage templates'}
+      direction="row"
+      pl="16px"
+      py="6px"
+      justifyContent="space-between"
+      sx={{
+        borderTop: (theme) => `1px solid ${theme.color.borders.borderDisabled}`,
+        borderBottom: (theme) => `0px solid ${theme.color.borders.borderDisabled}`,
+        cursor: 'pointer',
+        lineHeight: '21px',
+        ':hover': (theme) => ({
+          zIndex: '999',
+          bgcolor: theme.color.background.bgHover,
+        }),
+      }}
+    >
+      <Typography variant="sm">
+        <Box display="flex" gap="4px" alignItems="center" sx={{ color: (theme) => theme.color.gray[600] }}>
+          Manage templates
+          <ArrowRightIcon />
+        </Box>
+      </Typography>
+    </Stack>
+  )
+}
diff --git a/src/components/buttons/PrimaryBtn.tsx b/src/components/buttons/PrimaryBtn.tsx
index ef0049f0c..3a7f03325 100644
--- a/src/components/buttons/PrimaryBtn.tsx
+++ b/src/components/buttons/PrimaryBtn.tsx
@@ -8,12 +8,14 @@ export const PrimaryBtn = ({
   handleClick,
   buttonBackground,
   disabled,
+  padding,
 }: {
   startIcon?: ReactNode
   buttonText: string
   handleClick: () => void
   buttonBackground?: string
   disabled?: boolean
+  padding?: string
 }) => {
   return (
     <Button
@@ -27,6 +29,7 @@ export const PrimaryBtn = ({
         '&:hover': { backgroundColor: buttonBackground || theme.color.gray[600], boxShadow: 'none' },
         borderRadius: '4px',
         cursor: 'pointer',
+        padding: padding ?? 'none',
       })}
       onClick={() => handleClick()}
       disableRipple
diff --git a/src/components/inputs/Selector.tsx b/src/components/inputs/Selector.tsx
index 8f74ce58c..887e1e8fc 100644
--- a/src/components/inputs/Selector.tsx
+++ b/src/components/inputs/Selector.tsx
@@ -37,7 +37,7 @@ interface Prop<T extends keyof SelectorOptionsType> {
   value: unknown
   selectorType: T
   options: SelectorOptionsType[T][] | IExtraOption[]
-  buttonContent: ReactNode
+  buttonContent?: ReactNode
   inputStatusValue: string
   setInputStatusValue: React.Dispatch<React.SetStateAction<string>>
   disabled?: boolean
@@ -64,6 +64,7 @@ interface Prop<T extends keyof SelectorOptionsType> {
   useClickHandler?: boolean
   cursor?: Property.Cursor
   currentOption?: SelectorOptionsType[T] //option which shall be at the top of the selector without any grouping
+  errorPlaceholder?: string
 }
 
 export default function Selector<T extends keyof SelectorOptionsType>({
@@ -95,6 +96,7 @@ export default function Selector<T extends keyof SelectorOptionsType>({
   useClickHandler,
   cursor,
   currentOption,
+  errorPlaceholder = 'Required',
 }: Prop<T>) {
   const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
 
@@ -225,26 +227,28 @@ export default function Selector<T extends keyof SelectorOptionsType>({
               justifyContent: { xs: 'flex-start', sm: 'flex-start' },
               cursor: disabled ? 'auto' : (cursor ?? 'pointer'),
               borderRadius: '4px',
-              padding: '8px 8px',
+              padding: padding ? padding : '8px 8px',
               ':hover': {
                 backgroundColor: disabled ? 'none' : (theme) => theme.color.gray[100],
               },
             }}
           >
             <Box>{startIcon}</Box>
-            <Typography
-              variant="bodySm"
-              sx={{
-                textOverflow: 'ellipsis',
-                whiteSpace: 'nowrap',
-                overflow: 'hidden',
-                maxWidth: '150px',
-                display: { xs: responsiveNoHide ? 'block' : 'none', sm: 'block' },
-                userSelect: 'none',
-              }}
-            >
-              {buttonContent}
-            </Typography>
+            {buttonContent && (
+              <Typography
+                variant="bodySm"
+                sx={{
+                  textOverflow: 'ellipsis',
+                  whiteSpace: 'nowrap',
+                  overflow: 'hidden',
+                  maxWidth: '150px',
+                  display: { xs: responsiveNoHide ? 'block' : 'none', sm: 'block' },
+                  userSelect: 'none',
+                }}
+              >
+                {buttonContent}
+              </Typography>
+            )}
           </Stack>
         ) : (
           <>
@@ -258,7 +262,7 @@ export default function Selector<T extends keyof SelectorOptionsType>({
               error={error}
               endIcon={endIcon}
             />
-            {error && <StyledHelperText> Required</StyledHelperText>}
+            {error && errorPlaceholder && <StyledHelperText> Required</StyledHelperText>}
           </>
         )}
       </Box>
diff --git a/src/icons/add-gray-medium.svg b/src/icons/add-gray-medium.svg
new file mode 100644
index 000000000..f60b08d72
--- /dev/null
+++ b/src/icons/add-gray-medium.svg
@@ -0,0 +1,6 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Icon (approved)">
+<path id="Plus_path" fill-rule="evenodd" clip-rule="evenodd" d="M6.00059 1.125C6.28906 1.125 6.52291 1.35885 6.52291 1.64732V10.3527C6.52291 10.6411 6.28906 10.875 6.00059 10.875C5.71212 10.875 5.47827 10.6411 5.47827 10.3527V1.64732C5.47827 1.35885 5.71212 1.125 6.00059 1.125Z" fill="#6B6F76"/>
+<path id="Plus_path_2" fill-rule="evenodd" clip-rule="evenodd" d="M1.12622 6.00084C1.12622 5.71237 1.36007 5.47852 1.64854 5.47852H10.3539C10.6424 5.47852 10.8762 5.71237 10.8762 6.00084C10.8762 6.28931 10.6424 6.52316 10.3539 6.52316H1.64854C1.36007 6.52316 1.12622 6.28931 1.12622 6.00084Z" fill="#6B6F76"/>
+</g>
+</svg>
diff --git a/src/icons/index.ts b/src/icons/index.ts
index 0d495cd5c..6a6bef989 100644
--- a/src/icons/index.ts
+++ b/src/icons/index.ts
@@ -17,6 +17,7 @@ export { default as EditIcon } from './edit.svg'
 export { default as BoardViewIcon } from './board-view.svg'
 export { default as ListViewIcon } from './list-view.svg'
 export { default as TemplateIconSm } from './template-sm.svg'
+export { default as TempalteIconMd } from './template.md.svg'
 export { default as TemplateIconLg } from './template-lg.svg'
 export { default as ArrowLinkIcon } from './arrow-link.svg'
 export { default as AviIcon } from './avi.svg'
@@ -53,6 +54,7 @@ export { default as AddLargeIcon } from './add-large.svg'
 export { default as TasksListIcon } from './tasks-list.svg'
 export { default as CrossIconSmall } from './cross-small.svg'
 export { default as GrayAddIcon } from './add-gray.svg'
+export { default as GrayAddMediumIcon } from './add-gray-medium.svg'
 export { default as ArrowUpIcon } from './arrow-up.svg'
 export { default as DownloadBtn } from './download.svg'
 export { default as DownloadIconBlack } from './download-black.svg'
diff --git a/src/icons/template.md.svg b/src/icons/template.md.svg
new file mode 100644
index 000000000..ca7a1da9b
--- /dev/null
+++ b/src/icons/template.md.svg
@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Icons">
+<path id="Vector" d="M3 2.5C2.725 2.5 2.5 2.725 2.5 3V13C2.5 13.275 2.725 13.5 3 13.5H10V11C10 10.4469 10.4469 10 11 10H13.5V3C13.5 2.725 13.275 2.5 13 2.5H3ZM10 15H3C1.89687 15 1 14.1031 1 13V3C1 1.89687 1.89687 1 3 1H13C14.1031 1 15 1.89687 15 3V10V10.1719C15 10.7031 14.7906 11.2125 14.4156 11.5875L11.5875 14.4156C11.2125 14.7906 10.7031 15 10.1719 15H10Z" fill="#6B6F76"/>
+</g>
+</svg>


Reviews:
user: login: rrojan
body: There isn't supposed to be a horizontal separator here in the design. Can you confirm whether this is intentional

<img width="662" alt="image" src="https://github.com/user-attachments/assets/3db7576e-97fc-4697-9226-01d19406626c" />

There are only two dots here
<img width="206" alt="image" src="https://github.com/user-attachments/assets/cd132e8c-62c6-454f-907a-4dc3ae553345" />

The selector box for assignees can stretch a bit for long names. Is this intentional?

https://github.com/user-attachments/assets/1c86c7e5-7945-41c1-b810-2ba8d1a90854





state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T13:00:27Z
user: login: rrojan
body: LGTM 🏆 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T12:33:47Z

Review comments:
diff_hunk: @@ -0,0 +1,24 @@
+'use client'
+
+import { NewTaskCard } from '@/app/detail/ui/NewTaskCard'
+import { AddSubTaskBtn } from '@/components/buttons/AddSubTaskBtn'
+import { GrayAddMediumIcon } from '@/icons'
+import { Stack } from '@mui/material'
+import { useState } from 'react'
+
+export const SubtasksWrapper = () => {
path: src/app/detail/ui/SubtasksWrapper.tsx
user: login: rrojan
body: Might not be semantic to call this a `Wrapper` since this component does not wrap anything
created_at: 2025-03-24T12:52:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,24 @@
+'use client'
+
+import { NewTaskCard } from '@/app/detail/ui/NewTaskCard'
+import { AddSubTaskBtn } from '@/components/buttons/AddSubTaskBtn'
+import { GrayAddMediumIcon } from '@/icons'
+import { Stack } from '@mui/material'
+import { useState } from 'react'
path: src/app/detail/ui/SubtasksWrapper.tsx
user: login: rrojan
body: Please be sure to organize imports on every file you change
created_at: 2025-03-24T12:53:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,50 @@
+'use client'
+
+import { Box, Stack, Typography } from '@mui/material'
+import { ReactNode } from 'react'
+
+export const AddSubTaskBtn = ({
path: src/components/buttons/AddSubTaskBtn.tsx
user: login: rrojan
body: We could change this to a more generic name so it can be reused
created_at: 2025-03-24T12:53:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -27,6 +29,7 @@ export const PrimaryBtn = ({
         '&:hover': { backgroundColor: buttonBackground || theme.color.gray[600], boxShadow: 'none' },
         borderRadius: '4px',
         cursor: 'pointer',
+        padding: padding ? padding : 'none',
path: src/components/buttons/PrimaryBtn.tsx
user: login: rrojan
body: ```suggestion
        padding: padding ?? 'none',
```
created_at: 2025-03-24T12:54:10Z
author_association: CONTRIBUTOR

PR comments:
user: login: linear[bot]
created_at: 2025-03-21T11:01:46Z
body: <p><a href="https://linear.app/copilotplatforms/issue/OUT-1558/add-create-subtask-button-and-new-subtask-form">OUT-1558 Add "Create subtask" button and new subtask form</a></p>
author_association: NONE
user: login: vercel[bot]
created_at: 2025-03-21T11:01:47Z
body: [vc]: #dbLowjwdnv891UAPahX/cMSZhIURiv+6qqjG9dklilk=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ0YXNrcy1hcHAiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoidGFza3MtYXBwLWdpdC1vdXQtMTU1OC1jb3BpbG90LXBsYXRmb3Jtcy52ZXJjZWwuYXBwIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9jb3BpbG90LXBsYXRmb3Jtcy90YXNrcy1hcHAvSDhGS0hEekxRMmRhb0VlSnl1YzJEOHdxNmpDNCIsInByZXZpZXdVcmwiOiJ0YXNrcy1hcHAtZ2l0LW91dC0xNTU4LWNvcGlsb3QtcGxhdGZvcm1zLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJyb290RGlyZWN0b3J5IjpudWxsfV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **tasks-app** | ✅ Ready ([Inspect](https://vercel.com/copilot-platforms/tasks-app/H8FKHDzLQ2daoEeJyuc2D8wq6jC4)) | [Visit Preview](https://tasks-app-git-out-1558-copilot-platforms.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/tasks-app-git-out-1558-copilot-platforms.vercel.app?via=pr-comment-feedback-link) | Mar 25, 2025 0:35am |


author_association: CONTRIBUTOR
user: login: linear[bot]
created_at: 2025-03-24T10:17:29Z
body: <p><a href="https://linear.app/copilotplatforms/issue/OUT-1559/add-support-for-apply-template-icon-button-in-create-subtask-form">OUT-1559 Add support for "Apply template" icon button in Create Subtask Form</a></p>
author_association: NONE