diff --git a/webapp/src/views/projects/tasks/TasksBoard.tsx b/webapp/src/component/task/TasksBoard.tsx similarity index 76% rename from webapp/src/views/projects/tasks/TasksBoard.tsx rename to webapp/src/component/task/TasksBoard.tsx index 8256375409..72fb79e76a 100644 --- a/webapp/src/views/projects/tasks/TasksBoard.tsx +++ b/webapp/src/component/task/TasksBoard.tsx @@ -1,19 +1,16 @@ import { Box, styled, useTheme } from '@mui/material'; - -import { TaskFilterType } from 'tg.component/task/taskFilter/TaskFilterPopover'; -import { useProject } from 'tg.hooks/useProject'; -import { components, operations } from 'tg.service/apiSchema.generated'; - +import { components } from 'tg.service/apiSchema.generated'; import { BoxLoading } from 'tg.component/common/BoxLoading'; import { BoardColumn } from 'tg.component/task/BoardColumn'; -import { useProjectBoardTasks } from './useProjectBoardTasks'; import { useTranslate } from '@tolgee/react'; import { LoadingButton } from '@mui/lab'; + import { useTaskStateTranslation } from 'tg.translationTools/useTaskStateTranslation'; import { useStateColor } from 'tg.component/task/TaskState'; +import type { useProjectBoardTasks } from 'tg.views/projects/tasks/useProjectBoardTasks'; type TaskModel = components['schemas']['TaskModel']; -type QueryParameters = operations['getTasks_1']['parameters']['query']; +type SimpleProjectModel = components['schemas']['SimpleProjectModel']; const StyledColumns = styled(Box)` padding-top: 12px; @@ -28,53 +25,30 @@ const StyledContainer = styled(Box)` overflow: auto; `; +type TasksLoadable = ReturnType; + type Props = { showClosed: boolean; - filter: TaskFilterType; onOpenDetail: (task: TaskModel) => void; - search: string; + newTasks: TasksLoadable; + inProgressTasks: TasksLoadable; + doneTasks: TasksLoadable; + project?: SimpleProjectModel; }; export const TasksBoard = ({ showClosed, - filter, onOpenDetail, - search, + newTasks, + inProgressTasks, + doneTasks, + project, }: Props) => { - const project = useProject(); const theme = useTheme(); const { t } = useTranslate(); const translateState = useTaskStateTranslation(); const stateColor = useStateColor(); - const query = { - size: 20, - search, - sort: ['createdAt,desc'], - filterAssignee: filter.assignees, - filterLanguage: filter.languages, - filterType: filter.types, - } satisfies QueryParameters; - - const newTasks = useProjectBoardTasks({ - projectId: project.id, - query: { ...query, filterState: ['NEW'] }, - }); - - const inProgressTasks = useProjectBoardTasks({ - projectId: project.id, - query: { ...query, filterState: ['IN_PROGRESS'] }, - }); - - const doneTasks = useProjectBoardTasks({ - projectId: project.id, - query: { - ...query, - filterState: showClosed ? ['DONE', 'CLOSED'] : ['DONE'], - filterDoneMinClosedAt: filter.doneMinClosedAt, - }, - }); - const canFetchMore = newTasks.hasNextPage || inProgressTasks.hasNextPage || diff --git a/webapp/src/component/task/tasksHeader/TasksHeader.tsx b/webapp/src/component/task/tasksHeader/TasksHeader.tsx new file mode 100644 index 0000000000..117f677ecb --- /dev/null +++ b/webapp/src/component/task/tasksHeader/TasksHeader.tsx @@ -0,0 +1,16 @@ +import { TasksHeaderBig } from './TasksHeaderBig'; +import { TasksHeaderCompact } from './TasksHeaderCompact'; + +type Props = React.ComponentProps & { isSmall: boolean }; + +export const TasksHeader = ({ isSmall, ...props }: Props) => { + return ( + <> + {isSmall ? ( + + ) : ( + + )} + + ); +}; diff --git a/webapp/src/views/projects/tasks/TasksHeader.tsx b/webapp/src/component/task/tasksHeader/TasksHeaderBig.tsx similarity index 94% rename from webapp/src/views/projects/tasks/TasksHeader.tsx rename to webapp/src/component/task/tasksHeader/TasksHeaderBig.tsx index b8a8341ac5..678a22a99c 100644 --- a/webapp/src/views/projects/tasks/TasksHeader.tsx +++ b/webapp/src/component/task/tasksHeader/TasksHeaderBig.tsx @@ -21,8 +21,10 @@ import { import { useTranslate } from '@tolgee/react'; import { TaskFilterType } from 'tg.component/task/taskFilter/TaskFilterPopover'; import { TaskFilter } from 'tg.component/task/taskFilter/TaskFilter'; -import { useProject } from 'tg.hooks/useProject'; import { LabelHint } from 'tg.component/common/LabelHint'; +import { components } from 'tg.service/apiSchema.generated'; + +type SimpleProjectModel = components['schemas']['SimpleProjectModel']; const StyledContainer = styled(Box)` display: flex; @@ -47,9 +49,10 @@ type Props = { onAddTask?: () => void; view: TaskView; onViewChange: (view: TaskView) => void; + project?: SimpleProjectModel; }; -export const TasksHeader = ({ +export const TasksHeaderBig = ({ sx, className, onSearchChange, @@ -60,11 +63,11 @@ export const TasksHeader = ({ onAddTask, view, onViewChange, + project, }: Props) => { const [localSearch, setLocalSearch] = useState(''); const onDebouncedSearchChange = useDebounceCallback(onSearchChange, 500); const { t } = useTranslate(); - const project = useProject(); return ( diff --git a/webapp/src/views/projects/tasks/TasksHeaderCompact.tsx b/webapp/src/component/task/tasksHeader/TasksHeaderCompact.tsx similarity index 96% rename from webapp/src/views/projects/tasks/TasksHeaderCompact.tsx rename to webapp/src/component/task/tasksHeader/TasksHeaderCompact.tsx index 53ffa68dcf..2ed88a1cad 100644 --- a/webapp/src/views/projects/tasks/TasksHeaderCompact.tsx +++ b/webapp/src/component/task/tasksHeader/TasksHeaderCompact.tsx @@ -16,11 +16,13 @@ import { TaskFilterPopover, TaskFilterType, } from 'tg.component/task/taskFilter/TaskFilterPopover'; -import { useProject } from 'tg.hooks/useProject'; import { LabelHint } from 'tg.component/common/LabelHint'; import { filterEmpty } from 'tg.component/task/taskFilter/taskFilterUtils'; import { useApiQuery } from 'tg.service/http/useQueryApi'; import { HeaderSearchField } from 'tg.component/layout/HeaderSearchField'; +import { components } from 'tg.service/apiSchema.generated'; + +type SimpleProjectModel = components['schemas']['SimpleProjectModel']; const StyledContainer = styled(Box)` display: flex; @@ -59,6 +61,7 @@ type Props = { onAddTask?: () => void; view: TaskView; onViewChange: (view: TaskView) => void; + project?: SimpleProjectModel; }; export const TasksHeaderCompact = ({ @@ -70,14 +73,12 @@ export const TasksHeaderCompact = ({ filter, onFilterChange, onAddTask, - view, - onViewChange, + project, }: Props) => { const [localSearch, setLocalSearch] = useState(''); const [searchOpen, setSearchOpen] = useState(false); const onDebouncedSearchChange = useDebounceCallback(onSearchChange, 500); const { t } = useTranslate(); - const project = useProject(); const filtersAnchorEl = useRef(null); const [filtersOpen, setFiltersOpen] = useState(false); diff --git a/webapp/src/views/myTasks/MyTasksBoard.tsx b/webapp/src/views/myTasks/MyTasksBoard.tsx index 66faeccb1a..83039bedea 100644 --- a/webapp/src/views/myTasks/MyTasksBoard.tsx +++ b/webapp/src/views/myTasks/MyTasksBoard.tsx @@ -1,26 +1,12 @@ -import { Box, styled, useTheme } from '@mui/material'; - import { TaskFilterType } from 'tg.component/task/taskFilter/TaskFilterPopover'; import { components, operations } from 'tg.service/apiSchema.generated'; -import { BoxLoading } from 'tg.component/common/BoxLoading'; -import { BoardColumn } from 'tg.component/task/BoardColumn'; import { useMyBoardTask } from './useMyBoardTask'; -import LoadingButton from 'tg.component/common/form/LoadingButton'; -import { useTranslate } from '@tolgee/react'; -import { useTaskStateTranslation } from 'tg.translationTools/useTaskStateTranslation'; -import { useStateColor } from 'tg.component/task/TaskState'; +import { TasksBoard } from 'tg.component/task/TasksBoard'; type TaskWithProjectModel = components['schemas']['TaskWithProjectModel']; type QueryParameters = operations['getTasks_1']['parameters']['query']; -const StyledColumns = styled(Box)` - padding-top: 12px; - display: grid; - grid-template-columns: 1fr 1fr 1fr; - gap: 16px; -`; - type Props = { showClosed: boolean; filter: TaskFilterType; @@ -34,11 +20,6 @@ export const MyTasksBoard = ({ onOpenDetail, search, }: Props) => { - const { t } = useTranslate(); - const translateState = useTaskStateTranslation(); - const stateColor = useStateColor(); - const theme = useTheme(); - const query = { size: 20, search, @@ -63,90 +44,13 @@ export const MyTasksBoard = ({ }, }); - const canFetchMore = - newTasks.hasNextPage || - inProgressTasks.hasNextPage || - doneTasks.hasNextPage; - - function handleFetchMore() { - newTasks.fetchNextPage(); - inProgressTasks.fetchNextPage(); - doneTasks.fetchNextPage(); - } - - const isLoading = - newTasks.isLoading || inProgressTasks.isLoading || doneTasks.isLoading; - const isFetching = - newTasks.isFetching || inProgressTasks.isFetching || doneTasks.isFetching; - - if (isLoading) { - return ( - - - - ); - } - return ( - - onOpenDetail(t as TaskWithProjectModel)} - /> - onOpenDetail(t as TaskWithProjectModel)} - /> - - - {translateState('DONE')} - - - {' & '} - {translateState('CLOSED')} - - - ) : ( - - - {translateState('DONE')} - - - {' '} - {t('task_board_last_30_days')} - - - ) - } - tasks={doneTasks.items} - total={doneTasks.data?.pages?.[0]?.page?.totalElements ?? 0} - onDetailOpen={(t) => onOpenDetail(t as TaskWithProjectModel)} - /> - {canFetchMore && ( - - - {t('global_load_more')} - - - )} - + onOpenDetail(t as TaskWithProjectModel)} + doneTasks={doneTasks} + inProgressTasks={inProgressTasks} + newTasks={newTasks} + /> ); }; diff --git a/webapp/src/views/myTasks/MyTasksHeader.tsx b/webapp/src/views/myTasks/MyTasksHeader.tsx deleted file mode 100644 index d9c1eb0f23..0000000000 --- a/webapp/src/views/myTasks/MyTasksHeader.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { useState } from 'react'; -import { - Box, - Button, - ButtonGroup, - Checkbox, - FormControlLabel, - InputAdornment, - styled, - SxProps, -} from '@mui/material'; -import { useDebounceCallback } from 'usehooks-ts'; - -import { TextField } from 'tg.component/common/TextField'; -import { BarChartSquare01, Rows03, SearchSm } from '@untitled-ui/icons-react'; -import { useTranslate } from '@tolgee/react'; -import { TaskFilterType } from 'tg.component/task/taskFilter/TaskFilterPopover'; -import { TaskFilter } from 'tg.component/task/taskFilter/TaskFilter'; -import { LabelHint } from 'tg.component/common/LabelHint'; - -const StyledContainer = styled(Box)` - display: flex; - gap: 16px; - justify-content: space-between; -`; - -const StyledToggleButton = styled(Button)` - padding: 4px 8px; -`; - -export type TaskView = 'LIST' | 'BOARD'; - -type Props = { - sx?: SxProps; - className?: string; - onSearchChange: (value: string) => void; - showClosed: boolean; - onShowClosedChange: (value: boolean) => void; - filter: TaskFilterType; - onFilterChange: (value: TaskFilterType) => void; - view: TaskView; - onViewChange: (view: TaskView) => void; -}; - -export const MyTasksHeader = ({ - sx, - className, - onSearchChange, - showClosed, - onShowClosedChange, - filter, - onFilterChange, - view, - onViewChange, -}: Props) => { - const [localSearch, setLocalSearch] = useState(''); - const onDebouncedSearchChange = useDebounceCallback(onSearchChange, 500); - const { t } = useTranslate(); - - return ( - - - { - setLocalSearch(e.target.value); - onDebouncedSearchChange(e.target.value); - }} - placeholder={t('tasks_search_placeholder')} - InputProps={{ - startAdornment: ( - - - - ), - }} - /> - - onShowClosedChange(!showClosed)} - control={} - label={ - - {t('tasks_show_closed_label')} - - - - - } - /> - - - - onViewChange('LIST')} - data-cy="tasks-view-list-button" - > - - - onViewChange('BOARD')} - data-cy="tasks-view-table-button" - > - - - - - - ); -}; diff --git a/webapp/src/views/myTasks/MyTasksView.tsx b/webapp/src/views/myTasks/MyTasksView.tsx index b796d6470d..089bc737b5 100644 --- a/webapp/src/views/myTasks/MyTasksView.tsx +++ b/webapp/src/views/myTasks/MyTasksView.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; import { HomeLine } from '@untitled-ui/icons-react'; -import { Dialog } from '@mui/material'; +import { Dialog, useMediaQuery } from '@mui/material'; import { useTranslate } from '@tolgee/react'; import { BaseView } from 'tg.component/layout/BaseView'; @@ -8,11 +8,14 @@ import { DashboardPage } from 'tg.component/layout/DashboardPage'; import { LINKS } from 'tg.constants/links'; import { components } from 'tg.service/apiSchema.generated'; import { TaskDetail } from 'tg.component/task/TaskDetail'; -import { MyTasksHeader, TaskView } from './MyTasksHeader'; import { useUrlSearchState } from 'tg.hooks/useUrlSearchState'; import { TaskFilterType } from 'tg.component/task/taskFilter/TaskFilterPopover'; +import { TasksHeader } from 'tg.component/task/tasksHeader/TasksHeader'; +import { TaskView } from 'tg.component/task/tasksHeader/TasksHeaderBig'; + import { MyTasksList } from './MyTasksList'; import { MyTasksBoard } from './MyTasksBoard'; +import { useGlobalContext } from 'tg.globalContext/GlobalContext'; type TaskWithProjectModel = components['schemas']['TaskWithProjectModel']; @@ -44,6 +47,12 @@ export const MyTasksView = () => { doneMinClosedAt: showClosed === 'true' ? undefined : minus30Days, }; + const rightPanelWidth = useGlobalContext((c) => c.layout.rightPanelWidth); + + const isSmall = useMediaQuery( + `@media(max-width: ${rightPanelWidth + 1000}px)` + ); + function setFilter(val: TaskFilterType) { setProjects(val.projects?.map((p) => String(p))); setTypes(val.types?.map((l) => String(l))); @@ -68,7 +77,7 @@ export const MyTasksView = () => { [t('my_tasks_title'), LINKS.MY_TASKS.build()], ]} > - { onFilterChange={setFilter} view={view as TaskView} onViewChange={setView} + isSmall={isSmall} /> - {view === 'LIST' ? ( + {view === 'LIST' && !isSmall ? ( void; + search: string; +}; + +export const ProjectTasksBoard = ({ + showClosed, + filter, + onOpenDetail, + search, +}: Props) => { + const project = useProject(); + + const query = { + size: 20, + search, + sort: ['createdAt,desc'], + filterAssignee: filter.assignees, + filterLanguage: filter.languages, + filterType: filter.types, + } satisfies QueryParameters; + + const newTasks = useProjectBoardTasks({ + projectId: project.id, + query: { ...query, filterState: ['NEW'] }, + }); + + const inProgressTasks = useProjectBoardTasks({ + projectId: project.id, + query: { ...query, filterState: ['IN_PROGRESS'] }, + }); + + const doneTasks = useProjectBoardTasks({ + projectId: project.id, + query: { + ...query, + filterState: showClosed ? ['DONE', 'CLOSED'] : ['DONE'], + filterDoneMinClosedAt: filter.doneMinClosedAt, + }, + }); + + return ( + + ); +}; diff --git a/webapp/src/views/projects/tasks/ProjectTasksView.tsx b/webapp/src/views/projects/tasks/ProjectTasksView.tsx index 8fef3aab3f..50d9f9af0b 100644 --- a/webapp/src/views/projects/tasks/ProjectTasksView.tsx +++ b/webapp/src/views/projects/tasks/ProjectTasksView.tsx @@ -12,13 +12,13 @@ import { useUrlSearchState } from 'tg.hooks/useUrlSearchState'; import { TaskCreateDialog } from 'tg.component/task/taskCreate/TaskCreateDialog'; import { projectPreferencesService } from 'tg.service/ProjectPreferencesService'; import { useProjectPermissions } from 'tg.hooks/useProjectPermissions'; +import { useGlobalContext } from 'tg.globalContext/GlobalContext'; +import { TasksHeader } from 'tg.component/task/tasksHeader/TasksHeader'; +import { TaskView } from 'tg.component/task/tasksHeader/TasksHeaderBig'; import { BaseProjectView } from '../BaseProjectView'; -import { TasksHeader, TaskView } from './TasksHeader'; import { TasksList } from './TasksList'; -import { TasksBoard } from './TasksBoard'; -import { useGlobalContext } from 'tg.globalContext/GlobalContext'; -import { TasksHeaderCompact } from './TasksHeaderCompact'; +import { ProjectTasksBoard } from './ProjectTasksBoard'; type TaskModel = components['schemas']['TaskModel']; @@ -107,31 +107,19 @@ export const ProjectTasksView = () => { ]} > - {isSmall ? ( - setShowClosed(String(val))} - filter={filter} - onFilterChange={setFilter} - onAddTask={canEditTasks ? () => setAddDialog(true) : undefined} - view={view as TaskView} - onViewChange={setView} - /> - ) : ( - setShowClosed(String(val))} - filter={filter} - onFilterChange={setFilter} - onAddTask={canEditTasks ? () => setAddDialog(true) : undefined} - view={view as TaskView} - onViewChange={setView} - /> - )} + setShowClosed(String(val))} + filter={filter} + onFilterChange={setFilter} + onAddTask={canEditTasks ? () => setAddDialog(true) : undefined} + view={view as TaskView} + onViewChange={setView} + isSmall={isSmall} + /> + {view === 'LIST' && !isSmall ? ( { onOpenDetail={setDetail} /> ) : ( -