diff --git a/apps/web/app/hooks/features/useOrganizationProjects.ts b/apps/web/app/hooks/features/useOrganizationProjects.ts index 121c4dc8e..fdf19e3b5 100644 --- a/apps/web/app/hooks/features/useOrganizationProjects.ts +++ b/apps/web/app/hooks/features/useOrganizationProjects.ts @@ -2,13 +2,15 @@ import { editOrganizationProjectSettingAPI, editOrganizationProjectAPI, getOrganizationProjectAPI, - getOrganizationProjectsAPI + getOrganizationProjectsAPI, + createOrganizationProjectAPI } from '@app/services/client/api'; import { userState } from '@app/stores'; import { useCallback } from 'react'; import { useAtom } from 'jotai'; import { useQuery } from '../useQuery'; import { organizationProjectsState } from '@/app/stores/organization-projects'; +import { getOrganizationIdCookie, getTenantIdCookie } from '@/app/helpers'; export function useOrganizationProjects() { const [user] = useAtom(userState); @@ -26,6 +28,9 @@ export function useOrganizationProjects() { const { loading: getOrganizationProjectsLoading, queryCall: getOrganizationProjectsQueryCall } = useQuery(getOrganizationProjectsAPI); + const { loading: createOrganizationProjectLoading, queryCall: createOrganizationProjectQueryCall } = + useQuery(createOrganizationProjectAPI); + const editOrganizationProjectSetting = useCallback( (id: string, data: any) => { if (user?.tenantId) { @@ -69,6 +74,24 @@ export function useOrganizationProjects() { } }, [getOrganizationProjectsQueryCall, setOrganizationProjects]); + const createOrganizationProject = useCallback( + async (data: { name: string }) => { + try { + const organizationId = getOrganizationIdCookie(); + const tenantId = getTenantIdCookie(); + + const res = await createOrganizationProjectQueryCall({ ...data, organizationId, tenantId }); + + setOrganizationProjects([...organizationProjects, res.data]); + + return res.data; + } catch (error) { + console.error(error); + } + }, + [createOrganizationProjectQueryCall, organizationProjects, setOrganizationProjects] + ); + return { editOrganizationProjectSetting, editOrganizationProjectSettingLoading, @@ -79,5 +102,7 @@ export function useOrganizationProjects() { getOrganizationProjects, getOrganizationProjectsLoading, organizationProjects, + createOrganizationProject, + createOrganizationProjectLoading }; } diff --git a/apps/web/app/services/client/api/projects.ts b/apps/web/app/services/client/api/projects.ts index a4f08467f..a69d540cf 100644 --- a/apps/web/app/services/client/api/projects.ts +++ b/apps/web/app/services/client/api/projects.ts @@ -1,12 +1,7 @@ -import { IProject } from '@app/interfaces'; +import { IProject, IProjectCreate } from '@app/interfaces'; import { post } from '../axios'; -type Params = { - name: string; - tenantId: string; - organizationId: string; -}; +export function createOrganizationProjectAPI(data: IProjectCreate) { -export function createOrganizationProjectAPI(params: Params) { - return post(`/organization-projects`, params); + return post(`/organization-projects`, data); } diff --git a/apps/web/components/pages/task/details-section/blocks/task-secondary-info.tsx b/apps/web/components/pages/task/details-section/blocks/task-secondary-info.tsx index ecf7bc426..37707b6ce 100644 --- a/apps/web/components/pages/task/details-section/blocks/task-secondary-info.tsx +++ b/apps/web/components/pages/task/details-section/blocks/task-secondary-info.tsx @@ -1,4 +1,4 @@ -import { useModal, useOrganizationProjects, useTeamTasks } from '@app/hooks'; +import { useModal, useOrganizationProjects, useOrganizationTeams, useTeamTasks } from '@app/hooks'; import { IProject, ITaskVersionCreate, ITeamTask } from '@app/interfaces'; import { detailedTaskState } from '@app/stores'; import { PlusIcon } from '@heroicons/react/20/solid'; @@ -21,11 +21,13 @@ import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'; import { useAtomValue } from 'jotai'; import TaskRow from '../components/task-row'; import { useTranslations } from 'next-intl'; -import { ChevronDownIcon, Square4OutlineIcon } from 'assets/svg'; +import { AddIcon, ChevronDownIcon, Square4OutlineIcon, TrashIcon } from 'assets/svg'; import { Listbox, Transition } from '@headlessui/react'; import { clsxm } from '@/app/utils'; import { organizationProjectsState } from '@/app/stores/organization-projects'; import ProjectIcon from '@components/ui/svgs/project-icon'; +import { ScrollArea, ScrollBar } from '@components/ui/scroll-bar'; +import { CreateProjectModal } from '@/lib/features/project/create-project-modal'; type StatusType = 'version' | 'epic' | 'status' | 'label' | 'size' | 'priority'; @@ -104,23 +106,6 @@ const TaskSecondaryInfo = () => { - {/* Epic */} - {task && task.issueType === 'Story' && ( - - { - onTaskSelect({ - id: d - } as ITeamTask); - }} - className="lg:min-w-[170px] text-black" - forDetails={true} - sidebarUI={true} - taskStatusClassName="text-[0.625rem] w-[7.6875rem] h-[2.35rem] max-w-[7.6875rem] rounded 3xl:text-xs" - defaultValue={task.parentId || ''} - /> - - )} {/* Epic */} {task && task.issueType === 'Story' && ( @@ -139,27 +124,8 @@ const TaskSecondaryInfo = () => { )} - {task && } {task && } - {/* Task Status */} - - - - - {/* Task Status */} { - {/* Task Labels */} - - - - {tags.length > 0 && ( - -
- {tags.map((tag, i) => { - return ( - - - - ); - })} -
-
- )} {/* Task Labels */} { {/* Task project */} {task && ( - + )} @@ -344,15 +282,16 @@ export default TaskSecondaryInfo; */ export function ProjectDropDown(props: ITaskProjectDropdownProps) { const { task, controlled = false, onChange, styles } = props; - + const { openModal, isOpen, closeModal } = useModal(); const organizationProjects = useAtomValue(organizationProjectsState); const { getOrganizationProjects } = useOrganizationProjects(); const { updateTask, updateLoading } = useTeamTasks(); + const { teams } = useOrganizationTeams(); const t = useTranslations(); useEffect(() => { getOrganizationProjects(); - }, [getOrganizationProjects]); + }, [getOrganizationProjects, teams]); const [selected, setSelected] = useState(); @@ -395,99 +334,128 @@ export function ProjectDropDown(props: ITaskProjectDropdownProps) { }, [task, updateTask]); return ( -
- { - if (controlled && onChange) { - onChange(project); - } else { - handleUpdateProject(project); - } - - setSelected(project); - }} + <> +
- {({ open }) => { - return ( - <> - { + if (controlled && onChange) { + onChange(project); + } else { + handleUpdateProject(project); + } + + setSelected(project); + }} + > + {({ open }) => { + return ( + <> + - {selected && ( -
- -
- )} - {updateLoading ? ( - - ) : ( -

- {selected?.name ?? 'Project'} -

- )} - + {selected && ( +
+ +
+ )} + {updateLoading ? ( + + ) : ( +

+ {selected?.name ?? 'Project'} +

)} - aria-hidden="true" - /> -
- - - - - {organizationProjects.map((item, i) => { - return ( - -
  • - {item.name} -
  • -
    - ); - })} - {!controlled && ( - - )} -
    -
    -
    - - ); + aria-hidden="true" + /> +
    + + + + + +
    + {organizationProjects.map((item, i) => { + return ( + +
  • + {' '} + {item.name} +
  • +
    + ); + })} +
    + {!controlled && ( + + )} + +
    +
    + +
    +
    +
    +
    + + ); + }} + +
    + { + setSelected(project); + onChange?.(project); + if (!controlled) { + handleUpdateProject(project); + } }} -
    -
    + open={isOpen} + closeModal={closeModal} + /> + ); } diff --git a/apps/web/components/ui/svgs/project-icon.tsx b/apps/web/components/ui/svgs/project-icon.tsx index e15ec9d40..1512e0648 100644 --- a/apps/web/components/ui/svgs/project-icon.tsx +++ b/apps/web/components/ui/svgs/project-icon.tsx @@ -1,6 +1,14 @@ -export default function ProjectIcon() { +import { IIconProps } from '@/app/interfaces'; + +export default function ProjectIcon(props: Partial) { return ( - + void; + onSuccess?: (project: IProject) => void; +} +/** + * A modal that allow to create a new project + * + * @param {Object} props - The props Object + * @param {boolean} props.open - If true open the modal otherwise close the modal + * @param {() => void} props.closeModal - A function to close the modal + * + * @returns {JSX.Element} The modal element + */ +export function CreateProjectModal(props: ICreateProjectModalProps) { + const t = useTranslations(); + const { open, closeModal, onSuccess } = props; + const { createOrganizationProject, createOrganizationProjectLoading } = useOrganizationProjects(); + const [name, setName] = useState(''); + + // Cleanup + useEffect(() => { + return () => { + setName(''); + }; + }, []); + + const handleCreateProject = useCallback(async () => { + try { + if (name.trim() === '') { + return; + } + const data = await createOrganizationProject({ name }); + + if (data) { + onSuccess?.(data); + } + + closeModal(); + } catch (error) { + console.error(error); + } + }, [closeModal, createOrganizationProject, name, onSuccess]); + + return ( + + +
    + + {t('common.CREATE_PROJECT')} + + +
    + setName(e.target.value)} + placeholder={'Please enter the project name'} + required + className="w-full" + wrapperClassName=" h-full border border-blue-500" + noWrapper + /> +
    + +
    + + +
    +
    + +
    + ); +} diff --git a/apps/web/lib/features/task/task-status.tsx b/apps/web/lib/features/task/task-status.tsx index f8598bf3d..6bd82e0a3 100644 --- a/apps/web/lib/features/task/task-status.tsx +++ b/apps/web/lib/features/task/task-status.tsx @@ -1079,7 +1079,7 @@ export function StatusDropdown({ {items.map((item, i) => { const item_value = item?.value || item?.name; diff --git a/apps/web/locales/ar.json b/apps/web/locales/ar.json index c64c8e161..b46388feb 100644 --- a/apps/web/locales/ar.json +++ b/apps/web/locales/ar.json @@ -249,7 +249,8 @@ "FILTER_CUSTOM_RANGE": "نطاق مخصص", "CLEAR_FILTER": "مسح الفلتر", "CLEAR": "مسح", - "APPLY_FILTER": "تطبيق الفلتر" + "APPLY_FILTER": "تطبيق الفلتر", + "CREATE_NEW": "إنشاء جديد" }, "sidebar": { "DASHBOARD": "لوحة التحكم", diff --git a/apps/web/locales/bg.json b/apps/web/locales/bg.json index a504c9f88..f67870cdc 100644 --- a/apps/web/locales/bg.json +++ b/apps/web/locales/bg.json @@ -267,7 +267,8 @@ "FILTER_CUSTOM_RANGE": "Потребителски диапазон", "CLEAR_FILTER": "Изчисти филтъра", "CLEAR": "Изчисти", - "APPLY_FILTER": "Приложи филтър" + "APPLY_FILTER": "Приложи филтър", + "CREATE_NEW": "Създай нов" }, "hotkeys": { "HELP": "Помощ", diff --git a/apps/web/locales/de.json b/apps/web/locales/de.json index a29130ced..4eb10c048 100644 --- a/apps/web/locales/de.json +++ b/apps/web/locales/de.json @@ -267,7 +267,8 @@ "FILTER_CUSTOM_RANGE": "Benutzerdefinierter Bereich", "CLEAR_FILTER": "Filter löschen", "CLEAR": "Löschen", - "APPLY_FILTER": "Filter anwenden" + "APPLY_FILTER": "Filter anwenden", + "CREATE_NEW": "Neu erstellen" }, "hotkeys": { "HELP": "Hilfe", diff --git a/apps/web/locales/en.json b/apps/web/locales/en.json index 7ad4c6701..f9b336cb5 100644 --- a/apps/web/locales/en.json +++ b/apps/web/locales/en.json @@ -267,7 +267,8 @@ "FILTER_CUSTOM_RANGE": "Custom Range", "CLEAR_FILTER": "Clear Filter", "CLEAR": "Clear", - "APPLY_FILTER": "Apply Filter" + "APPLY_FILTER": "Apply Filter", + "CREATE_NEW": "Create New" }, "hotkeys": { "HELP": "Help", diff --git a/apps/web/locales/es.json b/apps/web/locales/es.json index 072a30752..48a1fb500 100644 --- a/apps/web/locales/es.json +++ b/apps/web/locales/es.json @@ -267,7 +267,8 @@ "FILTER_CUSTOM_RANGE": "Rango personalizado", "CLEAR_FILTER": "Borrar filtro", "CLEAR": "Borrar", - "APPLY_FILTER": "Aplicar filtro" + "APPLY_FILTER": "Aplicar filtro", + "CREATE_NEW": "Crear nuevo" }, "hotkeys": { "HELP": "Ayuda", diff --git a/apps/web/locales/fr.json b/apps/web/locales/fr.json index 03e6d72ae..df60e6395 100644 --- a/apps/web/locales/fr.json +++ b/apps/web/locales/fr.json @@ -267,7 +267,8 @@ "FILTER_CUSTOM_RANGE": "Plage personnalisée", "CLEAR_FILTER": "Effacer le filtre", "CLEAR": "Effacer", - "APPLY_FILTER": "Appliquer le filtre" + "APPLY_FILTER": "Appliquer le filtre", + "CREATE_NEW": "Créer nouveau" }, "hotkeys": { "HELP": "Aide", diff --git a/apps/web/locales/he.json b/apps/web/locales/he.json index 009eaf700..4eb9b0848 100644 --- a/apps/web/locales/he.json +++ b/apps/web/locales/he.json @@ -267,7 +267,8 @@ "FILTER_CUSTOM_RANGE": "טווח מותאם אישית", "CLEAR_FILTER": "נקה סינון", "CLEAR": "נקה", - "APPLY_FILTER": "החל סינון" + "APPLY_FILTER": "החל סינון", + "CREATE_NEW": "צור חדש" }, "hotkeys": { "HELP": "עזרה", diff --git a/apps/web/locales/it.json b/apps/web/locales/it.json index c9810b03a..5413c4aa9 100644 --- a/apps/web/locales/it.json +++ b/apps/web/locales/it.json @@ -267,7 +267,8 @@ "FILTER_CUSTOM_RANGE": "Intervallo personalizzato", "CLEAR_FILTER": "Cancella filtro", "CLEAR": "Cancella", - "APPLY_FILTER": "Applica filtro" + "APPLY_FILTER": "Applica filtro", + "CREATE_NEW": "Crea nuovo" }, "hotkeys": { "HELP": "Aiuto", diff --git a/apps/web/locales/nl.json b/apps/web/locales/nl.json index 15022dd3d..566273f74 100644 --- a/apps/web/locales/nl.json +++ b/apps/web/locales/nl.json @@ -267,7 +267,8 @@ "FILTER_CUSTOM_RANGE": "Aangepast bereik", "CLEAR_FILTER": "Filter wissen", "CLEAR": "Wissen", - "APPLY_FILTER": "Filter toepassen" + "APPLY_FILTER": "Filter toepassen", + "CREATE_NEW": "Nieuwe maken" }, "hotkeys": { "HELP": "Help", diff --git a/apps/web/locales/pl.json b/apps/web/locales/pl.json index 3ff42ee43..d694fcdc5 100644 --- a/apps/web/locales/pl.json +++ b/apps/web/locales/pl.json @@ -267,7 +267,8 @@ "FILTER_CUSTOM_RANGE": "Zakres niestandardowy", "CLEAR_FILTER": "Wyczyść filtr", "CLEAR": "Wyczyść", - "APPLY_FILTER": "Zastosuj filtr" + "APPLY_FILTER": "Zastosuj filtr", + "CREATE_NEW": "Utwórz nowy" }, "hotkeys": { "HELP": "Pomoc", diff --git a/apps/web/locales/pt.json b/apps/web/locales/pt.json index 4ae42a04d..a34170a2d 100644 --- a/apps/web/locales/pt.json +++ b/apps/web/locales/pt.json @@ -267,7 +267,8 @@ "FILTER_CUSTOM_RANGE": "Faixa personalizada", "CLEAR_FILTER": "Limpar filtro", "CLEAR": "Limpar", - "APPLY_FILTER": "Aplicar filtro" + "APPLY_FILTER": "Aplicar filtro", + "CREATE_NEW": "Criar novo" }, "hotkeys": { "HELP": "Ajuda", diff --git a/apps/web/locales/ru.json b/apps/web/locales/ru.json index a64a38134..fbb5d83c4 100644 --- a/apps/web/locales/ru.json +++ b/apps/web/locales/ru.json @@ -267,7 +267,8 @@ "FILTER_CUSTOM_RANGE": "Пользовательский диапазон", "CLEAR_FILTER": "Очистить фильтр", "CLEAR": "Очистить", - "APPLY_FILTER": "Применить фильтр" + "APPLY_FILTER": "Применить фильтр", + "CREATE_NEW": "Создать новый" }, "hotkeys": { "HELP": "Помощь", diff --git a/apps/web/locales/zh.json b/apps/web/locales/zh.json index 311077623..4445a5557 100644 --- a/apps/web/locales/zh.json +++ b/apps/web/locales/zh.json @@ -267,7 +267,8 @@ "FILTER_CUSTOM_RANGE": "自定义范围", "CLEAR_FILTER": "清除筛选", "CLEAR": "清除", - "APPLY_FILTER": "应用筛选" + "APPLY_FILTER": "应用筛选", + "CREATE_NEW": "创建新的" }, "hotkeys": { "HELP": "帮助",