From aeb4ef082ffc97dede2b629aa8ebb761cd293901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Gran=C3=A1t?= Date: Wed, 8 Jan 2025 16:37:42 +0100 Subject: [PATCH 1/9] feat: task improvements --- .../TranslationsControllerFilterTest.kt | 36 ++++++ .../testDataBuilder/data/TaskTestData.kt | 9 ++ .../request/translation/TranslationFilters.kt | 10 ++ .../QueryGlobalFiltering.kt | 12 +- e2e/cypress/e2e/tasks/projectTasks.cy.ts | 42 ++++++- e2e/cypress/support/dataCyType.d.ts | 4 +- .../io/tolgee/ee/data/task/TaskFilters.kt | 6 + .../io/tolgee/ee/repository/TaskRepository.kt | 5 + .../io/tolgee/ee/service/TaskService.kt | 1 + webapp/src/component/common/LabelHint.tsx | 7 +- webapp/src/component/task/TaskTypeChip.tsx | 2 +- webapp/src/constants/links.tsx | 9 ++ .../OrderTranslationsDialog.tsx | 46 +++++++- webapp/src/ee/task/components/BoardItem.tsx | 7 +- .../src/ee/task/components/PrefilterTask.tsx | 14 ++- .../PrefilterTaskHideDoneSwitch.tsx | 43 +++++++ webapp/src/ee/task/components/TaskItem.tsx | 23 ++-- webapp/src/ee/task/components/TaskLabel.tsx | 10 +- webapp/src/ee/task/components/TaskScope.tsx | 4 +- webapp/src/ee/task/components/TasksBoard.tsx | 10 +- .../components/TranslationsTaskDetail.tsx | 5 +- .../taskCreate/EmptyScopeDialog.tsx | 41 +++++++ .../taskCreate/TaskCreateDialog.tsx | 53 ++++++++- .../components/taskCreate/TaskCreateForm.tsx | 44 ++++++- .../components/taskCreate/TaskPreview.tsx | 49 ++------ .../taskCreate/TranslationStateFilter.tsx | 1 + .../taskFilter/TaskFilterPopover.tsx | 2 +- .../components/tasksHeader/TasksHeaderBig.tsx | 18 +-- .../tasksHeader/TasksHeaderCompact.tsx | 16 +-- .../ee/task/views/myTasks/MyTasksBoard.tsx | 10 +- .../src/ee/task/views/myTasks/MyTasksList.tsx | 7 +- .../src/ee/task/views/myTasks/MyTasksView.tsx | 21 ++-- .../views/projectTasks/ProjectTasksBoard.tsx | 10 +- .../views/projectTasks/ProjectTasksList.tsx | 6 +- .../views/projectTasks/ProjectTasksView.tsx | 21 ++-- webapp/src/eeSetup/eeModule.ee.tsx | 1 + webapp/src/eeSetup/eeModule.oss.tsx | 5 +- webapp/src/globalContext/useAuthService.tsx | 25 ++-- webapp/src/hooks/useLocalStorageState.ts | 11 +- webapp/src/service/apiSchema.generated.ts | 108 ++++++++++-------- webapp/src/service/http/useQueryApi.ts | 41 +++++++ webapp/src/views/projects/TaskRedirect.tsx | 15 ++- .../members/component/InvitationItem.tsx | 6 +- .../projects/members/component/MemberItem.tsx | 8 +- .../OperationsSummary/OperationsList.tsx | 4 +- .../TranslationHeader/TranslationControls.tsx | 24 +++- .../TranslationControlsCompact.tsx | 27 ++++- .../TranslationHeader/TranslationsHeader.tsx | 2 +- .../services/useTranslationsService.tsx | 1 + .../translations/prefilters/usePrefilter.ts | 47 ++++++-- 50 files changed, 699 insertions(+), 230 deletions(-) create mode 100644 webapp/src/ee/task/components/PrefilterTaskHideDoneSwitch.tsx create mode 100644 webapp/src/ee/task/components/taskCreate/EmptyScopeDialog.tsx diff --git a/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/translations/v2TranslationsController/TranslationsControllerFilterTest.kt b/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/translations/v2TranslationsController/TranslationsControllerFilterTest.kt index c0420b8ced..d437577766 100644 --- a/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/translations/v2TranslationsController/TranslationsControllerFilterTest.kt +++ b/backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/translations/v2TranslationsController/TranslationsControllerFilterTest.kt @@ -2,6 +2,7 @@ package io.tolgee.api.v2.controllers.translations.v2TranslationsController import io.tolgee.ProjectAuthControllerTest import io.tolgee.development.testDataBuilder.data.NamespacesTestData +import io.tolgee.development.testDataBuilder.data.TaskTestData import io.tolgee.development.testDataBuilder.data.TranslationSourceChangeStateTestData import io.tolgee.development.testDataBuilder.data.TranslationsTestData import io.tolgee.fixtures.andAssertError @@ -357,4 +358,39 @@ class TranslationsControllerFilterTest : ProjectAuthControllerTest("/v2/projects node("_embedded.keys").isArray.hasSize(2) } } + + @ProjectJWTAuthTestMethod + @Test + fun `filters by task`() { + val testData = TaskTestData() + testData.processFirstKeyOfTranslateTask() + testDataService.saveTestData(testData.root) + userAccount = testData.user + projectSupplier = { testData.projectBuilder.self } + performProjectAuthGet( + "/translations?filterTaskNumber=${testData.translateTask.self.number}", + ).andIsOk.andAssertThatJson { + node("_embedded.keys") { + isArray.hasSize(2) + node("[0].keyName").isEqualTo("key 0") + node("[1].keyName").isEqualTo("key 1") + } + } + performProjectAuthGet( + "/translations?filterTaskNumber=${testData.translateTask.self.number}&filterTaskKeysNotDone=true", + ).andIsOk.andAssertThatJson { + node("_embedded.keys") { + isArray.hasSize(1) + node("[0].keyName").isEqualTo("key 1") + } + } + performProjectAuthGet( + "/translations?filterTaskNumber=${testData.translateTask.self.number}&filterTaskKeysDone=true", + ).andIsOk.andAssertThatJson { + node("_embedded.keys") { + isArray.hasSize(1) + node("[0].keyName").isEqualTo("key 0") + } + } + } } diff --git a/backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/TaskTestData.kt b/backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/TaskTestData.kt index 1d571ca957..a254796ee2 100644 --- a/backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/TaskTestData.kt +++ b/backend/data/src/main/kotlin/io/tolgee/development/testDataBuilder/data/TaskTestData.kt @@ -11,6 +11,7 @@ import io.tolgee.model.enums.OrganizationRoleType import io.tolgee.model.enums.ProjectPermissionType import io.tolgee.model.enums.Scope import io.tolgee.model.enums.TaskType +import io.tolgee.model.task.TaskKey class TaskTestData : BaseTestData("tasksTestUser", "Project with tasks") { var projectUser: UserAccountBuilder @@ -20,6 +21,7 @@ class TaskTestData : BaseTestData("tasksTestUser", "Project with tasks") { var projectViewRoleUser: UserAccountBuilder var projectManageRoleUser: UserAccountBuilder var translateTask: TaskBuilder + var translateTaskKeys: MutableSet = mutableSetOf() var reviewTask: TaskBuilder var relatedProject: ProjectBuilder var keysInTask: MutableSet = mutableSetOf() @@ -151,6 +153,7 @@ class TaskTestData : BaseTestData("tasksTestUser", "Project with tasks") { addTaskKey { task = translateTask.self key = it.self + translateTaskKeys.add(this) } } @@ -266,4 +269,10 @@ class TaskTestData : BaseTestData("tasksTestUser", "Project with tasks") { } return keys } + + fun processFirstKeyOfTranslateTask(): TaskKey { + val firstKey = translateTaskKeys.first() + firstKey.done = true + return firstKey + } } diff --git a/backend/data/src/main/kotlin/io/tolgee/dtos/request/translation/TranslationFilters.kt b/backend/data/src/main/kotlin/io/tolgee/dtos/request/translation/TranslationFilters.kt index eb4d5b8920..cb5afc10c5 100644 --- a/backend/data/src/main/kotlin/io/tolgee/dtos/request/translation/TranslationFilters.kt +++ b/backend/data/src/main/kotlin/io/tolgee/dtos/request/translation/TranslationFilters.kt @@ -104,4 +104,14 @@ To filter default namespace, set to empty string. description = "Select only keys which are in specified task", ) var filterTaskNumber: List? = null + + @field:Parameter( + description = "Filter task keys which are `not done`", + ) + var filterTaskKeysNotDone: Boolean? = null + + @field:Parameter( + description = "Filter task keys which are `done`", + ) + var filterTaskKeysDone: Boolean? = null } diff --git a/backend/data/src/main/kotlin/io/tolgee/service/queryBuilders/translationViewBuilder/QueryGlobalFiltering.kt b/backend/data/src/main/kotlin/io/tolgee/service/queryBuilders/translationViewBuilder/QueryGlobalFiltering.kt index 16db9e9752..0685a8f51e 100644 --- a/backend/data/src/main/kotlin/io/tolgee/service/queryBuilders/translationViewBuilder/QueryGlobalFiltering.kt +++ b/backend/data/src/main/kotlin/io/tolgee/service/queryBuilders/translationViewBuilder/QueryGlobalFiltering.kt @@ -145,12 +145,22 @@ class QueryGlobalFiltering( private fun filterTask() { if (params.filterTaskNumber != null) { - val translationTaskJoin = + val translationTaskKeyJoin = queryBase.root .join(Key_.tasks, JoinType.LEFT) + val translationTaskJoin = + translationTaskKeyJoin .join(TaskKey_.task, JoinType.LEFT) queryBase.whereConditions.add(translationTaskJoin.get(Task_.number).`in`(params.filterTaskNumber)) + + if (params.filterTaskKeysNotDone == true) { + queryBase.whereConditions.add(translationTaskKeyJoin.get(TaskKey_.done).`in`(false)) + } + + if (params.filterTaskKeysDone == true) { + queryBase.whereConditions.add(translationTaskKeyJoin.get(TaskKey_.done).`in`(true)) + } } } diff --git a/e2e/cypress/e2e/tasks/projectTasks.cy.ts b/e2e/cypress/e2e/tasks/projectTasks.cy.ts index d13439f10b..5e4d52cddc 100644 --- a/e2e/cypress/e2e/tasks/projectTasks.cy.ts +++ b/e2e/cypress/e2e/tasks/projectTasks.cy.ts @@ -77,6 +77,7 @@ describe('project tasks', () => { cy.gcy('create-task-field-description').type( 'This is task description ...' ); + cy.gcy('translations-state-filter-clear').click(); getTaskPreview('Czech').findDcy('assignee-select').click(); cy.gcy('assignee-search-select-popover') .contains('Organization member') @@ -116,6 +117,7 @@ describe('project tasks', () => { cy.gcy('create-task-field-languages-item').contains('Czech').click(); dismissMenu(); cy.waitForDom(); + cy.gcy('translations-state-filter-clear').click(); checkTaskPreview({ language: 'Czech', @@ -135,6 +137,7 @@ describe('project tasks', () => { cy.gcy('create-task-field-languages-item').contains('English').click(); dismissMenu(); cy.waitForDom(); + cy.gcy('translations-state-filter-clear').click(); checkTaskPreview({ language: 'Czech', @@ -157,18 +160,49 @@ describe('project tasks', () => { cy.gcy('create-task-field-languages').click(); cy.gcy('create-task-field-languages-item').contains('Czech').click(); dismissMenu(); + cy.gcy('translations-state-filter-clear').click(); + checkTaskPreview({ + language: 'Czech', + keys: 4, + alert: false, + words: 8, + characters: 52, + }); + }); + + it('wont allow creation of empty task', () => { + cy.gcy('tasks-header-add-task').click(); + cy.gcy('create-task-field-name').click().type('new task'); + cy.gcy('create-task-field-languages').click(); + cy.gcy('create-task-field-languages-item').contains('Czech').click(); + dismissMenu(); + + cy.gcy('translations-state-filter-clear').click(); cy.gcy('translations-state-filter').click(); - cy.gcy('translations-state-filter-option').contains('Untranslated').click(); + cy.gcy('translations-state-filter-option').contains('Outdated').click(); + dismissMenu(); cy.waitForDom(); checkTaskPreview({ language: 'Czech', - keys: 2, + keys: 0, alert: false, - words: 4, - characters: 26, + words: 0, + characters: 0, }); + + cy.gcy('create-task-submit').click(); + cy.gcy('empty-scope-dialog').should('be.visible'); + }); + + it('uses default state filters', () => { + cy.gcy('tasks-header-add-task').click(); + cy.gcy('translations-state-filter').contains('Untranslated'); + cy.gcy('create-task-field-type').click(); + cy.gcy('create-task-field-type-item').contains('Review').click(); + + cy.gcy('translations-state-filter').contains('Translated'); }); }); diff --git a/e2e/cypress/support/dataCyType.d.ts b/e2e/cypress/support/dataCyType.d.ts index 777b2a60cb..599f95b095 100644 --- a/e2e/cypress/support/dataCyType.d.ts +++ b/e2e/cypress/support/dataCyType.d.ts @@ -174,6 +174,7 @@ declare namespace DataCy { "edit-pat-dialog-content" | "edit-pat-dialog-description-input" | "edit-pat-dialog-title" | + "empty-scope-dialog" | "expiration-date-field" | "expiration-date-picker" | "expiration-select" | @@ -549,7 +550,7 @@ declare namespace DataCy { "tasks-header-add-task" | "tasks-header-filter-select" | "tasks-header-order-translation" | - "tasks-header-show-closed" | + "tasks-header-show-all" | "tasks-view-board-button" | "tasks-view-list-button" | "this-is-the-element" | @@ -609,6 +610,7 @@ declare namespace DataCy { "translations-select-all-button" | "translations-shortcuts-command" | "translations-state-filter" | + "translations-state-filter-clear" | "translations-state-filter-option" | "translations-state-indicator" | "translations-table-cell" | diff --git a/ee/backend/app/src/main/kotlin/io/tolgee/ee/data/task/TaskFilters.kt b/ee/backend/app/src/main/kotlin/io/tolgee/ee/data/task/TaskFilters.kt index dd18382075..24c7f916d1 100644 --- a/ee/backend/app/src/main/kotlin/io/tolgee/ee/data/task/TaskFilters.kt +++ b/ee/backend/app/src/main/kotlin/io/tolgee/ee/data/task/TaskFilters.kt @@ -61,8 +61,14 @@ open class TaskFilters { ) var filterAgency: List? = null + @Deprecated("Confusing logic and naming", ReplaceWith("excludeClosedBefore")) @field:Parameter( description = """Exclude "done" tasks which are older than specified timestamp""", ) var filterDoneMinClosedAt: Long? = null + + @field:Parameter( + description = """Exclude tasks closed before timestamp""", + ) + var excludeClosedBefore: Long? = null } diff --git a/ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/TaskRepository.kt b/ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/TaskRepository.kt index 2c99b028a6..9fb342011e 100644 --- a/ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/TaskRepository.kt +++ b/ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/TaskRepository.kt @@ -78,6 +78,11 @@ private const val TASK_FILTERS = """ or :#{#filters.filterDoneMinClosedAt} is null or tk.closedAt > :#{#filters.filterDoneMinClosedAt} ) + and ( + :#{#filters.excludeClosedBefore} is null + or tk.closedAt is null + or tk.closedAt > :#{#filters.excludeClosedBefore} + ) """ @Repository diff --git a/ee/backend/app/src/main/kotlin/io/tolgee/ee/service/TaskService.kt b/ee/backend/app/src/main/kotlin/io/tolgee/ee/service/TaskService.kt index 5559aea109..17ad905d26 100644 --- a/ee/backend/app/src/main/kotlin/io/tolgee/ee/service/TaskService.kt +++ b/ee/backend/app/src/main/kotlin/io/tolgee/ee/service/TaskService.kt @@ -241,6 +241,7 @@ class TaskService( } if (state == TaskState.NEW || state == TaskState.IN_PROGRESS) { task.state = if (taskWithScope.doneItems == 0L) TaskState.NEW else TaskState.IN_PROGRESS + task.closedAt = null } else { task.closedAt = currentDateProvider.date task.state = state diff --git a/webapp/src/component/common/LabelHint.tsx b/webapp/src/component/common/LabelHint.tsx index 94a0238916..f890f1ec9a 100644 --- a/webapp/src/component/common/LabelHint.tsx +++ b/webapp/src/component/common/LabelHint.tsx @@ -1,5 +1,5 @@ import { HelpCircle } from '@untitled-ui/icons-react'; -import { Tooltip, styled } from '@mui/material'; +import { SxProps, Tooltip, styled } from '@mui/material'; const StyledLabelBody = styled('div')` display: inline-flex; @@ -11,12 +11,13 @@ type Props = { size?: number; title: React.ReactNode; children: React.ReactNode; + sx?: SxProps; }; -export const LabelHint = ({ children, title, size = 15 }: Props) => { +export const LabelHint = ({ children, title, size = 15, sx }: Props) => { return ( - + {children} diff --git a/webapp/src/component/task/TaskTypeChip.tsx b/webapp/src/component/task/TaskTypeChip.tsx index bc5f8042cc..f4b27d250a 100644 --- a/webapp/src/component/task/TaskTypeChip.tsx +++ b/webapp/src/component/task/TaskTypeChip.tsx @@ -9,7 +9,7 @@ const StyledChip = styled(Chip)``; export function getBackgroundColor(type: TaskType, theme: Theme) { switch (type) { case 'TRANSLATE': - return theme.palette.tokens.text._states.focus; + return theme.palette.tokens.success._states.focusVisible; case 'REVIEW': return theme.palette.tokens.secondary._states.focus; } diff --git a/webapp/src/constants/links.tsx b/webapp/src/constants/links.tsx index fc376a8269..410e66bf06 100644 --- a/webapp/src/constants/links.tsx +++ b/webapp/src/constants/links.tsx @@ -390,3 +390,12 @@ export class LINKS { static SLACK_CONNECT = Link.ofParent(LINKS.SLACK, 'connect'); static SLACK_CONNECTED = Link.ofParent(LINKS.SLACK, 'connected'); } + +export enum QUERY { + TRANSLATIONS_PREFILTERS_ACTIVITY = 'activity', + TRANSLATIONS_PREFILTERS_FAILED_JOB = 'failedJob', + TRANSLATIONS_PREFILTERS_TASK = 'task', + TRANSLATIONS_PREFILTERS_TASK_HIDE_DONE = 'taskHideDone', + TRANSLATIONS_TASK_DETAIL = 'taskDetail', + TASKS_FILTERS_SHOW_ALL = 'showAll', +} diff --git a/webapp/src/ee/orderTranslations/OrderTranslationsDialog.tsx b/webapp/src/ee/orderTranslations/OrderTranslationsDialog.tsx index bc2154dcf5..d58a9753a5 100644 --- a/webapp/src/ee/orderTranslations/OrderTranslationsDialog.tsx +++ b/webapp/src/ee/orderTranslations/OrderTranslationsDialog.tsx @@ -37,11 +37,17 @@ import { useEnabledFeatures, useUser } from 'tg.globalContext/helpers'; import { TranslationAgency } from './TranslationAgency'; import { TranslationStateType } from 'tg.translationTools/useStateTranslation'; import { DisabledFeatureBanner } from 'tg.component/common/DisabledFeatureBanner'; -import { TaskCreateForm } from 'tg.ee.module/task/components/taskCreate/TaskCreateForm'; +import { + DEFAULT_STATE_FILTERS_REVIEW, + DEFAULT_STATE_FILTERS_TRANSLATE, + TaskCreateForm, +} from 'tg.ee.module/task/components/taskCreate/TaskCreateForm'; +import { EmptyScopeDialog } from 'tg.ee.module/task/components/taskCreate/EmptyScopeDialog'; type CreateTaskRequest = components['schemas']['CreateTaskRequest']; type TaskType = CreateTaskRequest['type']; type LanguageModel = components['schemas']['LanguageModel']; +type KeysScopeView = components['schemas']['KeysScopeView']; const StyledMainTitle = styled(DialogTitle)` padding-bottom: 0px; @@ -120,7 +126,7 @@ export const OrderTranslationsDialog: React.FC = ({ }); const [filters, setFilters] = useState({}); - const [stateFilters, setStateFilters] = useState([]); + const [_stateFilters, setStateFilters] = useState(); const [languages, setLanguages] = useState(initialValues?.languages ?? []); const [successMessage, setSuccessMessage] = useState(false); @@ -168,6 +174,20 @@ export const OrderTranslationsDialog: React.FC = ({ const selectedKeys = initialValues?.selection ?? selectedLoadable.data?.ids ?? []; + const [scope, setScope] = useState<(KeysScopeView | undefined)[]>([]); + const [emptyScope, setEmptyScope] = useState(); + + const canBeSubmitted = scope.every(Boolean); + + function getStateFilters(taskType: TaskType) { + if (_stateFilters) { + return _stateFilters; + } + return taskType === 'TRANSLATE' + ? DEFAULT_STATE_FILTERS_TRANSLATE + : DEFAULT_STATE_FILTERS_REVIEW; + } + const isLoading = preferredAgencyLoadable.isLoading || agenciesLoadable.isLoading || @@ -240,6 +260,14 @@ export const OrderTranslationsDialog: React.FC = ({ }} validationSchema={Validation.CREATE_TASK_FORM(t)} onSubmit={async (values) => { + const emptyScope = scope.findIndex((sc) => !sc?.keyCount); + if (emptyScope != -1) { + const language = allLanguages.find( + (l) => l.id === languages[emptyScope] + ); + setEmptyScope(language || true); + return; + } const data = languages.map( (languageId) => ({ @@ -253,6 +281,7 @@ export const OrderTranslationsDialog: React.FC = ({ keys: selectedKeys, } satisfies CreateTaskRequest) ); + const stateFilters = getStateFilters(values.type); createTasksLoadable.mutate( { path: { projectId }, @@ -358,12 +387,13 @@ export const OrderTranslationsDialog: React.FC = ({ setFilters={ !initialValues?.selection ? setFilters : undefined } - stateFilters={stateFilters} + stateFilters={getStateFilters(values.type)} setStateFilters={setStateFilters} projectId={projectId} hideDueDate hideAssignees disabled={disabled} + onScopeChange={setScope} /> @@ -434,7 +464,9 @@ export const OrderTranslationsDialog: React.FC = ({ ) : ( = ({ }} )} + {emptyScope && ( + setEmptyScope(undefined)} + /> + )} ); }; diff --git a/webapp/src/ee/task/components/BoardItem.tsx b/webapp/src/ee/task/components/BoardItem.tsx index fee9e0a843..38d28d0595 100644 --- a/webapp/src/ee/task/components/BoardItem.tsx +++ b/webapp/src/ee/task/components/BoardItem.tsx @@ -2,13 +2,12 @@ import { Link } from 'react-router-dom'; import { useState } from 'react'; import { useTranslate } from '@tolgee/react'; import { Box, IconButton, styled, Tooltip } from '@mui/material'; -import { AlarmClock, DotsVertical } from '@untitled-ui/icons-react'; +import { AlarmClock, DotsVertical, InfoCircle } from '@untitled-ui/icons-react'; import { stopAndPrevent } from 'tg.fixtures/eventHandler'; import { Scope } from 'tg.fixtures/permissions'; import { components } from 'tg.service/apiSchema.generated'; import { useDateFormatter } from 'tg.hooks/useLocale'; -import { TaskDetail } from 'tg.component/CustomIcons'; import { BatchProgress } from 'tg.views/projects/translations/BatchOperations/OperationsSummary/BatchProgress'; import { TaskLabel } from './TaskLabel'; @@ -104,7 +103,7 @@ export const BoardItem = ({ size="small" onClick={stopAndPrevent(() => onDetailOpen(task))} > - + - {t('task_keys_count', { value: task.totalItems })} + {t('task_word_count', { value: task.baseWordCount })} {task.dueDate ? ( diff --git a/webapp/src/ee/task/components/PrefilterTask.tsx b/webapp/src/ee/task/components/PrefilterTask.tsx index 495e96e567..8a2ab16241 100644 --- a/webapp/src/ee/task/components/PrefilterTask.tsx +++ b/webapp/src/ee/task/components/PrefilterTask.tsx @@ -1,11 +1,15 @@ import React from 'react'; import { T, useTranslate } from '@tolgee/react'; import { Box, IconButton, styled, Tooltip } from '@mui/material'; -import { AlertCircle, ClipboardCheck, X } from '@untitled-ui/icons-react'; +import { + AlertCircle, + ClipboardCheck, + InfoCircle, + X, +} from '@untitled-ui/icons-react'; import { useApiQuery } from 'tg.service/http/useQueryApi'; import { useProject } from 'tg.hooks/useProject'; -import { TaskDetail as TaskDetailIcon } from 'tg.component/CustomIcons'; import { PrefilterContainer } from 'tg.views/projects/translations/prefilters/ContainerPrefilter'; import { useUrlSearchState } from 'tg.hooks/useUrlSearchState'; @@ -16,6 +20,7 @@ import { TaskTooltip } from './TaskTooltip'; import { TaskLabel } from './TaskLabel'; import { PrefilterTaskProps } from '../../../eeSetup/EeModuleType'; import { TASK_ACTIVE_STATES } from 'tg.component/task/taskActiveStates'; +import { QUERY } from 'tg.constants/links'; const StyledWarning = styled('div')` display: flex; @@ -50,7 +55,8 @@ export const PrefilterTask = ({ taskNumber }: PrefilterTaskProps) => { path: { projectId: project.id, taskNumber }, }); - const [_, setTaskDetail] = useUrlSearchState('taskDetail'); + const [_, setTaskDetail] = useUrlSearchState(QUERY.TRANSLATIONS_TASK_DETAIL); + const { clear } = usePrefilter(); function handleShowDetails() { @@ -112,7 +118,7 @@ export const PrefilterTask = ({ taskNumber }: PrefilterTaskProps) => { - + {!isActive && } diff --git a/webapp/src/ee/task/components/PrefilterTaskHideDoneSwitch.tsx b/webapp/src/ee/task/components/PrefilterTaskHideDoneSwitch.tsx new file mode 100644 index 0000000000..827f1279da --- /dev/null +++ b/webapp/src/ee/task/components/PrefilterTaskHideDoneSwitch.tsx @@ -0,0 +1,43 @@ +import { Checkbox, FormControlLabel, styled, SxProps } from '@mui/material'; +import { useTranslate } from '@tolgee/react'; +import { LabelHint } from 'tg.component/common/LabelHint'; +import { QUERY } from 'tg.constants/links'; +import { useUrlSearchState } from 'tg.hooks/useUrlSearchState'; + +const StyledLabel = styled('div')` + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + flex-shrink: 1; +`; + +type Props = { + sx?: SxProps; +}; + +export const PrefilterTaskHideDoneSwitch = ({ sx }: Props) => { + const [taskHideDone, setTaskHideDone] = useUrlSearchState( + QUERY.TRANSLATIONS_PREFILTERS_TASK_HIDE_DONE + ); + const { t } = useTranslate(); + + return ( + + {t('task_filter_hide_done')} + + } + control={ + + setTaskHideDone(taskHideDone === 'true' ? undefined : 'true') + } + /> + } + /> + ); +}; diff --git a/webapp/src/ee/task/components/TaskItem.tsx b/webapp/src/ee/task/components/TaskItem.tsx index 33a0e995dd..3584b18417 100644 --- a/webapp/src/ee/task/components/TaskItem.tsx +++ b/webapp/src/ee/task/components/TaskItem.tsx @@ -2,9 +2,8 @@ import { useState } from 'react'; import { Link } from 'react-router-dom'; import { useTranslate } from '@tolgee/react'; import { Box, IconButton, styled, Tooltip, useTheme } from '@mui/material'; -import { AlarmClock, DotsVertical } from '@untitled-ui/icons-react'; +import { AlarmClock, DotsVertical, InfoCircle } from '@untitled-ui/icons-react'; -import { TaskDetail } from 'tg.component/CustomIcons'; import { components } from 'tg.service/apiSchema.generated'; import { BatchProgress } from 'tg.views/projects/translations/BatchOperations/OperationsSummary/BatchProgress'; import { useDateFormatter } from 'tg.hooks/useLocale'; @@ -95,7 +94,7 @@ export const TaskItem = ({ alignItems="center" justifyContent="center" > - {t('task_keys_count', { value: task.totalItems })} + {t('task_word_count', { value: task.baseWordCount })} {task.state === 'IN_PROGRESS' ? ( @@ -137,16 +136,18 @@ export const TaskItem = ({ onClick={stopAndPrevent(() => onDetailOpen(task))} data-cy="task-item-detail" > - + + + + + setAnchorEl(e.currentTarget))} + data-cy="task-item-menu" + > + - setAnchorEl(e.currentTarget))} - data-cy="task-item-menu" - > - - { + const { t } = useTranslate(); return ( )} {!hideType && } - {task.agency && } + {task.agency && ( + + + + + + )} ); }; diff --git a/webapp/src/ee/task/components/TaskScope.tsx b/webapp/src/ee/task/components/TaskScope.tsx index de3f0647e6..6c47c8f4f8 100644 --- a/webapp/src/ee/task/components/TaskScope.tsx +++ b/webapp/src/ee/task/components/TaskScope.tsx @@ -7,7 +7,7 @@ import { TaskState } from 'tg.component/task/TaskState'; import { AvatarImg } from 'tg.component/common/avatar/AvatarImg'; import React from 'react'; import { UserName } from 'tg.component/common/UserName'; -import { File06 } from '@untitled-ui/icons-react'; +import { FileDownload03 } from '@untitled-ui/icons-react'; import { useTaskReport } from './utils'; type TaskModel = components['schemas']['TaskModel']; @@ -59,7 +59,7 @@ export const TaskScope = ({ task, perUserData, projectId }: Props) => { onClick={() => downloadReport(projectId, task)} sx={{ margin: -1, position: 'relative', left: -8 }} > - + diff --git a/webapp/src/ee/task/components/TasksBoard.tsx b/webapp/src/ee/task/components/TasksBoard.tsx index b562d1506e..0b2d05958c 100644 --- a/webapp/src/ee/task/components/TasksBoard.tsx +++ b/webapp/src/ee/task/components/TasksBoard.tsx @@ -30,7 +30,7 @@ const StyledContainer = styled(Box)` type TasksLoadable = ReturnType; type Props = { - showClosed: boolean; + showAll: boolean; onOpenDetail: (task: TaskModel) => void; newTasks: TasksLoadable; inProgressTasks: TasksLoadable; @@ -40,7 +40,7 @@ type Props = { }; export const TasksBoard = ({ - showClosed, + showAll, onOpenDetail, newTasks, inProgressTasks, @@ -116,7 +116,7 @@ export const TasksBoard = ({ {translateState('DONE')} @@ -131,6 +131,10 @@ export const TasksBoard = ({ {translateState('DONE')} + + {' & '} + {translateState('CLOSED')} + { - const [taskDetail, setTaskDetail] = useUrlSearchState('taskDetail'); + const [taskDetail, setTaskDetail] = useUrlSearchState( + QUERY.TRANSLATIONS_TASK_DETAIL + ); const project = useProject(); return ( <> diff --git a/webapp/src/ee/task/components/taskCreate/EmptyScopeDialog.tsx b/webapp/src/ee/task/components/taskCreate/EmptyScopeDialog.tsx new file mode 100644 index 0000000000..e40d71baa7 --- /dev/null +++ b/webapp/src/ee/task/components/taskCreate/EmptyScopeDialog.tsx @@ -0,0 +1,41 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, +} from '@mui/material'; +import { T } from '@tolgee/react'; +import { components } from 'tg.service/apiSchema.generated'; + +type LanguageModel = components['schemas']['LanguageModel']; + +type Props = { + language: LanguageModel | true; + onClose: () => void; +}; + +export const EmptyScopeDialog = ({ language, onClose }: Props) => { + return ( + + + {typeof language === 'object' && language.name ? ( + + ) : ( + + )} + + + + + + + + + ); +}; diff --git a/webapp/src/ee/task/components/taskCreate/TaskCreateDialog.tsx b/webapp/src/ee/task/components/taskCreate/TaskCreateDialog.tsx index 43386f0f2b..bce9cbadc6 100644 --- a/webapp/src/ee/task/components/taskCreate/TaskCreateDialog.tsx +++ b/webapp/src/ee/task/components/taskCreate/TaskCreateDialog.tsx @@ -15,10 +15,16 @@ import { StateType } from 'tg.constants/translationStates'; import { useEnabledFeatures } from 'tg.globalContext/helpers'; import { DisabledFeatureBanner } from 'tg.component/common/DisabledFeatureBanner'; -import { TaskCreateForm } from './TaskCreateForm'; +import { + DEFAULT_STATE_FILTERS_REVIEW, + DEFAULT_STATE_FILTERS_TRANSLATE, + TaskCreateForm, +} from './TaskCreateForm'; +import { EmptyScopeDialog } from './EmptyScopeDialog'; type TaskType = components['schemas']['TaskModel']['type']; type LanguageModel = components['schemas']['LanguageModel']; +type KeysScopeView = components['schemas']['KeysScopeView']; const StyledMainTitle = styled(DialogTitle)` padding-bottom: 0px; @@ -52,6 +58,8 @@ export type InitialValues = { dueDate: number; languageAssignees: Record; selection: number[]; + filters: FiltersType; + stateFilters: TranslationStateType[]; }; type Props = { @@ -83,7 +91,7 @@ export const TaskCreateDialog = ({ }); const [filters, setFilters] = useState({}); - const [stateFilters, setStateFilters] = useState([]); + const [_stateFilters, setStateFilters] = useState(); const [languages, setLanguages] = useState(initialValues?.languages ?? []); const selectedLoadable = useApiQuery({ @@ -102,6 +110,20 @@ export const TaskCreateDialog = ({ const selectedKeys = initialValues?.selection ?? selectedLoadable.data?.ids ?? []; + const [scope, setScope] = useState<(KeysScopeView | undefined)[]>([]); + const [emptyScope, setEmptyScope] = useState(); + + const canBeSubmitted = scope.every(Boolean); + + function getStateFilters(taskType: TaskType) { + if (_stateFilters) { + return _stateFilters; + } + return taskType === 'TRANSLATE' + ? DEFAULT_STATE_FILTERS_TRANSLATE + : DEFAULT_STATE_FILTERS_REVIEW; + } + return ( {!taskFeature && ( @@ -127,6 +149,15 @@ export const TaskCreateDialog = ({ }} validationSchema={Validation.CREATE_TASK_FORM(t)} onSubmit={async (values) => { + const emptyScope = scope.findIndex((sc) => !sc?.keyCount); + if (emptyScope != -1) { + const language = allLanguages.find( + (l) => l.id === languages[emptyScope] + ); + setEmptyScope(language || true); + return; + } + const data = languages.map((languageId) => ({ type: values.type, name: values.name, @@ -136,6 +167,9 @@ export const TaskCreateDialog = ({ assignees: values.assignees[languageId]?.map((u) => u.id) ?? [], keys: selectedKeys, })); + + const stateFilters = getStateFilters(values.type); + createTasksLoadable.mutate( { path: { projectId }, @@ -163,7 +197,7 @@ export const TaskCreateDialog = ({ ); }} > - {({ submitForm }) => { + {({ submitForm, values }) => { return ( + {emptyScope && ( + setEmptyScope(undefined)} + /> + )} ); }} diff --git a/webapp/src/ee/task/components/taskCreate/TaskCreateForm.tsx b/webapp/src/ee/task/components/taskCreate/TaskCreateForm.tsx index b2c5fe0fac..c816c76949 100644 --- a/webapp/src/ee/task/components/taskCreate/TaskCreateForm.tsx +++ b/webapp/src/ee/task/components/taskCreate/TaskCreateForm.tsx @@ -21,12 +21,19 @@ import { FiltersType } from 'tg.component/translation/translationFilters/tools'; import { Select } from 'tg.component/common/Select'; import { useEffect } from 'react'; import { TranslationStateType } from 'tg.translationTools/useStateTranslation'; +import { useApiQueries } from 'tg.service/http/useQueryApi'; +import { stringHash } from 'tg.fixtures/stringHash'; +import { StateType } from 'tg.constants/translationStates'; type TaskType = components['schemas']['TaskModel']['type']; type LanguageModel = components['schemas']['LanguageModel']; +type KeysScopeView = components['schemas']['KeysScopeView']; const TASK_TYPES: TaskType[] = ['TRANSLATE', 'REVIEW']; +export const DEFAULT_STATE_FILTERS_TRANSLATE: StateType[] = ['UNTRANSLATED']; +export const DEFAULT_STATE_FILTERS_REVIEW: StateType[] = ['TRANSLATED']; + const StyledTopPart = styled(Box)` display: grid; gap: ${({ theme }) => theme.spacing(0.5, 2)}; @@ -60,6 +67,7 @@ type Props = { projectId: number; hideDueDate?: boolean; hideAssignees?: boolean; + onScopeChange?: (data: (KeysScopeView | undefined)[]) => void; }; export const TaskCreateForm = ({ @@ -75,12 +83,39 @@ export const TaskCreateForm = ({ projectId, hideDueDate, hideAssignees, + onScopeChange, }: Props) => { const { t } = useTranslate(); const translateTaskType = useTaskTypeTranslation(); const { values, setFieldValue } = useFormikContext(); + const taskScopes = useApiQueries( + languages.map((languageId) => { + const content = { + keys: selectedKeys, + type: values.type, + languageId, + }; + return { + url: '/v2/projects/{projectId}/tasks/calculate-scope', + method: 'post', + path: { projectId }, + content: { 'application/json': content }, + query: { + // @ts-ignore + hash: stringHash(JSON.stringify(content)), + filterState: stateFilters.filter((i) => i !== 'OUTDATED'), + filterOutdated: stateFilters.includes('OUTDATED'), + }, + }; + }) + ); + + useEffect(() => { + onScopeChange?.(taskScopes.map((i) => i.data)); + }, [taskScopes.map((i) => String(i.dataUpdatedAt)).join(',')]); + useEffect(() => { // make sure base language is not selected const baseLang = allLanguages.find((l) => l.base); @@ -215,19 +250,18 @@ export const TaskCreateForm = ({ {allLanguages && ( - {languages?.map((language) => ( + {languages?.map((language, i) => ( l.id === language)!} type={values.type} - keys={selectedKeys} - assigness={values.assignees[language] ?? []} + projectId={projectId} + assignees={values.assignees[language] ?? []} onUpdateAssignees={(users) => { setFieldValue(`assignees[${language}]`, users); }} - filters={stateFilters} - projectId={projectId} hideAssignees={hideAssignees} + scope={taskScopes[i]?.data} /> ))} diff --git a/webapp/src/ee/task/components/taskCreate/TaskPreview.tsx b/webapp/src/ee/task/components/taskCreate/TaskPreview.tsx index 898f8ed4fe..ea2526ed0d 100644 --- a/webapp/src/ee/task/components/taskCreate/TaskPreview.tsx +++ b/webapp/src/ee/task/components/taskCreate/TaskPreview.tsx @@ -3,17 +3,15 @@ import { Box, Skeleton, styled, Tooltip, useTheme } from '@mui/material'; import { useTranslate } from '@tolgee/react'; import { components } from 'tg.service/apiSchema.generated'; -import { useApiQuery } from 'tg.service/http/useQueryApi'; -import { stringHash } from 'tg.fixtures/stringHash'; import { FlagImage } from 'tg.component/languages/FlagImage'; import { useNumberFormatter } from 'tg.hooks/useLocale'; import { User } from 'tg.component/UserAccount'; import { AssigneeSearchSelect } from '../assigneeSelect/AssigneeSearchSelect'; import { useTaskTypeTranslation } from 'tg.translationTools/useTaskTranslation'; -import { TranslationStateType } from 'tg.translationTools/useStateTranslation'; type TaskType = components['schemas']['TaskModel']['type']; type LanguageModel = components['schemas']['LanguageModel']; +type KeysScopeView = components['schemas']['KeysScopeView']; const StyledContainer = styled('div')` display: grid; @@ -47,43 +45,27 @@ const StyledSmallCaption = styled('div')` type Props = { type: TaskType; language: LanguageModel; - keys: number[]; - assigness: User[]; + assignees: User[]; onUpdateAssignees: (users: User[]) => void; - filters: TranslationStateType[]; projectId: number; hideAssignees?: boolean; + scope: KeysScopeView | undefined; }; export const TaskPreview = ({ type, language, - keys, - assigness, + assignees, onUpdateAssignees, - filters, projectId, hideAssignees, + scope, }: Props) => { const { t } = useTranslate(); const formatNumber = useNumberFormatter(); const theme = useTheme(); const translateTaskType = useTaskTypeTranslation(); - const content = { keys, type, languageId: language.id }; - const statsLoadable = useApiQuery({ - url: '/v2/projects/{projectId}/tasks/calculate-scope', - method: 'post', - path: { projectId }, - content: { 'application/json': content }, - query: { - // @ts-ignore add dependencies to url, so react query works correctly - hash: stringHash(JSON.stringify(content)), - filterState: filters.filter((i) => i !== 'OUTDATED'), - filterOutdated: filters.includes('OUTDATED'), - }, - }); - return ( {t('create_task_preview_keys')} - {statsLoadable.data ? ( + {scope ? ( - {formatNumber(statsLoadable.data.keyCount)} - {statsLoadable.data.keyCount !== - statsLoadable.data.keyCountIncludingConflicts && ( + {formatNumber(scope.keyCount)} + {scope.keyCount !== scope.keyCountIncludingConflicts && ( {t('create_task_preview_words')} - {statsLoadable.data ? ( - formatNumber(statsLoadable.data.wordCount) - ) : ( - - )} + {scope ? formatNumber(scope.wordCount) : } {t('create_task_preview_characters')} - {statsLoadable.data ? ( - formatNumber(statsLoadable.data.characterCount) - ) : ( - - )} + {scope ? formatNumber(scope.characterCount) : } {!hideAssignees && ( onChange([]))} tabIndex={-1} + data-cy="translations-state-filter-clear" > diff --git a/webapp/src/ee/task/components/taskFilter/TaskFilterPopover.tsx b/webapp/src/ee/task/components/taskFilter/TaskFilterPopover.tsx index b0427e65fc..e6f6852dd2 100644 --- a/webapp/src/ee/task/components/taskFilter/TaskFilterPopover.tsx +++ b/webapp/src/ee/task/components/taskFilter/TaskFilterPopover.tsx @@ -33,7 +33,7 @@ export type TaskFilterType = { agencies?: number[]; projects?: number[]; types?: TaskType[]; - doneMinClosedAt?: number; + excludeClosedBefore?: number; }; type Props = { diff --git a/webapp/src/ee/task/components/tasksHeader/TasksHeaderBig.tsx b/webapp/src/ee/task/components/tasksHeader/TasksHeaderBig.tsx index c706cf728a..48e924e5da 100644 --- a/webapp/src/ee/task/components/tasksHeader/TasksHeaderBig.tsx +++ b/webapp/src/ee/task/components/tasksHeader/TasksHeaderBig.tsx @@ -45,8 +45,8 @@ type Props = { sx?: SxProps; className?: string; onSearchChange: (value: string) => void; - showClosed: boolean; - onShowClosedChange: (value: boolean) => void; + showAll: boolean; + onShowAllChange: (value: boolean) => void; filter: TaskFilterType; onFilterChange: (value: TaskFilterType) => void; onAddTask?: () => void; @@ -60,8 +60,8 @@ export const TasksHeaderBig = ({ sx, className, onSearchChange, - showClosed, - onShowClosedChange, + showAll, + onShowAllChange, filter, onFilterChange, onAddTask, @@ -100,14 +100,14 @@ export const TasksHeaderBig = ({ project={project} /> onShowClosedChange(!showClosed)} + checked={showAll} + onChange={() => onShowAllChange(!showAll)} control={} - data-cy="tasks-header-show-closed" + data-cy="tasks-header-show-all" label={ - {t('tasks_show_closed_label')} - + {t('tasks_show_all_label')} + diff --git a/webapp/src/ee/task/components/tasksHeader/TasksHeaderCompact.tsx b/webapp/src/ee/task/components/tasksHeader/TasksHeaderCompact.tsx index d292c4e51e..ecf7de2d94 100644 --- a/webapp/src/ee/task/components/tasksHeader/TasksHeaderCompact.tsx +++ b/webapp/src/ee/task/components/tasksHeader/TasksHeaderCompact.tsx @@ -61,8 +61,8 @@ type Props = { sx?: SxProps; className?: string; onSearchChange: (value: string) => void; - showClosed: boolean; - onShowClosedChange: (value: boolean) => void; + showAll: boolean; + onShowAllChange: (value: boolean) => void; filter: TaskFilterType; onFilterChange: (value: TaskFilterType) => void; onOrderTranslation?: () => void; @@ -76,8 +76,8 @@ export const TasksHeaderCompact = ({ sx, className, onSearchChange, - showClosed, - onShowClosedChange, + showAll, + onShowAllChange, onOrderTranslation, filter, onFilterChange, @@ -167,14 +167,14 @@ export const TasksHeaderCompact = ({ /> )} onShowClosedChange(!showClosed)} + checked={showAll} + onChange={() => onShowAllChange(!showAll)} control={} sx={{ pl: 1 }} label={ - {t('tasks_show_closed_label')} - + {t('tasks_show_all_label')} + diff --git a/webapp/src/ee/task/views/myTasks/MyTasksBoard.tsx b/webapp/src/ee/task/views/myTasks/MyTasksBoard.tsx index 69e8e7ff2c..763c1ecf18 100644 --- a/webapp/src/ee/task/views/myTasks/MyTasksBoard.tsx +++ b/webapp/src/ee/task/views/myTasks/MyTasksBoard.tsx @@ -8,14 +8,14 @@ type TaskWithProjectModel = components['schemas']['TaskWithProjectModel']; type QueryParameters = operations['getTasks_1']['parameters']['query']; type Props = { - showClosed: boolean; + showAll: boolean; filter: TaskFilterType; onOpenDetail: (task: TaskWithProjectModel) => void; search: string; }; export const MyTasksBoard = ({ - showClosed, + showAll, filter, onOpenDetail, search, @@ -39,15 +39,15 @@ export const MyTasksBoard = ({ const doneTasks = useMyBoardTask({ query: { ...query, - filterState: showClosed ? ['DONE', 'CLOSED'] : ['DONE'], - filterDoneMinClosedAt: filter.doneMinClosedAt, + filterState: ['DONE', 'CLOSED'], + excludeClosedBefore: filter.excludeClosedBefore, filterAgency: filter.agencies, }, }); return ( onOpenDetail(t as TaskWithProjectModel)} doneTasks={doneTasks} inProgressTasks={inProgressTasks} diff --git a/webapp/src/ee/task/views/myTasks/MyTasksList.tsx b/webapp/src/ee/task/views/myTasks/MyTasksList.tsx index 04901c5e78..3aee516b1d 100644 --- a/webapp/src/ee/task/views/myTasks/MyTasksList.tsx +++ b/webapp/src/ee/task/views/myTasks/MyTasksList.tsx @@ -18,14 +18,14 @@ const StyledSeparator = styled('div')` `; type Props = { - showClosed: boolean; + showAll: boolean; filter: TaskFilterType; onOpenDetail: (task: TaskWithProjectModel) => void; search: string; }; export const MyTasksList = ({ - showClosed, + showAll, filter, search, onOpenDetail, @@ -43,10 +43,9 @@ export const MyTasksList = ({ page: Number(page), search, sort: ['number,desc'], - filterNotState: showClosed ? undefined : ['CLOSED'], filterProject: filter.projects, filterType: filter.types, - filterDoneMinClosedAt: filter.doneMinClosedAt, + excludeClosedBefore: filter.excludeClosedBefore, filterAgency: filter.agencies, }, options: { diff --git a/webapp/src/ee/task/views/myTasks/MyTasksView.tsx b/webapp/src/ee/task/views/myTasks/MyTasksView.tsx index e3cca51d4a..97a6871e1d 100644 --- a/webapp/src/ee/task/views/myTasks/MyTasksView.tsx +++ b/webapp/src/ee/task/views/myTasks/MyTasksView.tsx @@ -5,7 +5,7 @@ import { useTranslate } from '@tolgee/react'; import { BaseView } from 'tg.component/layout/BaseView'; import { DashboardPage } from 'tg.component/layout/DashboardPage'; -import { LINKS } from 'tg.constants/links'; +import { LINKS, QUERY } from 'tg.constants/links'; import { components } from 'tg.service/apiSchema.generated'; import { useUrlSearchState } from 'tg.hooks/useUrlSearchState'; import { useGlobalContext } from 'tg.globalContext/GlobalContext'; @@ -30,9 +30,12 @@ export const MyTasksView = () => { }); const [search, setSearch] = useUrlSearchState('search', { defaultVal: '' }); - const [showClosed, setShowClosed] = useUrlSearchState('showClosed', { - defaultVal: 'false', - }); + const [showAll, setShowAll] = useUrlSearchState( + QUERY.TASKS_FILTERS_SHOW_ALL, + { + defaultVal: 'false', + } + ); const [projects, setProjects] = useUrlSearchState('project', { array: true, @@ -47,7 +50,7 @@ export const MyTasksView = () => { const filter: TaskFilterType = { projects: projects?.map((p) => Number(p)), types: types as any[], - doneMinClosedAt: showClosed === 'true' ? undefined : minus30Days, + excludeClosedBefore: showAll === 'true' ? undefined : minus30Days, agencies: agencies?.map((a) => Number(a)), }; @@ -85,8 +88,8 @@ export const MyTasksView = () => { setShowClosed(String(val))} + showAll={showAll === 'true'} + onShowAllChange={(val) => setShowAll(String(val))} filter={filter} onFilterChange={setFilter} view={view as TaskView} @@ -97,14 +100,14 @@ export const MyTasksView = () => { ) : ( )} diff --git a/webapp/src/ee/task/views/projectTasks/ProjectTasksBoard.tsx b/webapp/src/ee/task/views/projectTasks/ProjectTasksBoard.tsx index 58af8cdbea..3a539ec846 100644 --- a/webapp/src/ee/task/views/projectTasks/ProjectTasksBoard.tsx +++ b/webapp/src/ee/task/views/projectTasks/ProjectTasksBoard.tsx @@ -9,14 +9,14 @@ type TaskModel = components['schemas']['TaskModel']; type QueryParameters = operations['getTasks_1']['parameters']['query']; type Props = { - showClosed: boolean; + showAll: boolean; filter: TaskFilterType; onOpenDetail: (task: TaskModel) => void; search: string; }; export const ProjectTasksBoard = ({ - showClosed, + showAll, filter, onOpenDetail, search, @@ -47,14 +47,14 @@ export const ProjectTasksBoard = ({ projectId: project.id, query: { ...query, - filterState: showClosed ? ['DONE', 'CLOSED'] : ['DONE'], - filterDoneMinClosedAt: filter.doneMinClosedAt, + filterState: ['DONE', 'CLOSED'], + excludeClosedBefore: filter.excludeClosedBefore, }, }); return ( void; search: string; @@ -26,7 +26,6 @@ type Props = { }; export const ProjectTasksList = ({ - showClosed, filter, search, onOpenDetail, @@ -47,11 +46,10 @@ export const ProjectTasksList = ({ page: Number(page), search, sort: ['number,desc'], - filterNotState: showClosed ? undefined : ['CLOSED'], filterAssignee: filter.assignees, filterLanguage: filter.languages, filterType: filter.types, - filterDoneMinClosedAt: filter.doneMinClosedAt, + excludeClosedBefore: filter.excludeClosedBefore, filterAgency: filter.agencies, }, options: { diff --git a/webapp/src/ee/task/views/projectTasks/ProjectTasksView.tsx b/webapp/src/ee/task/views/projectTasks/ProjectTasksView.tsx index 89bf0067d1..832e73e2bb 100644 --- a/webapp/src/ee/task/views/projectTasks/ProjectTasksView.tsx +++ b/webapp/src/ee/task/views/projectTasks/ProjectTasksView.tsx @@ -3,7 +3,7 @@ import { Box, Dialog, useMediaQuery } from '@mui/material'; import { useTranslate } from '@tolgee/react'; import { useProject } from 'tg.hooks/useProject'; -import { LINKS, PARAMS } from 'tg.constants/links'; +import { LINKS, PARAMS, QUERY } from 'tg.constants/links'; import { useApiQuery } from 'tg.service/http/useQueryApi'; import { components } from 'tg.service/apiSchema.generated'; import { useUrlSearchState } from 'tg.hooks/useUrlSearchState'; @@ -29,9 +29,12 @@ export const ProjectTasksView = () => { const project = useProject(); const { t } = useTranslate(); const [search, setSearch] = useUrlSearchState('search', { defaultVal: '' }); - const [showClosed, setShowClosed] = useUrlSearchState('showClosed', { - defaultVal: 'false', - }); + const [showAll, setShowAll] = useUrlSearchState( + QUERY.TASKS_FILTERS_SHOW_ALL, + { + defaultVal: 'false', + } + ); const { satisfiesPermission } = useProjectPermissions(); const [view, setView] = useUrlSearchState('view', { @@ -75,7 +78,7 @@ export const ProjectTasksView = () => { languages: languages?.map((l) => Number(l)), agencies: agencies?.map((a) => Number(a)), types: types as any[], - doneMinClosedAt: showClosed === 'true' ? undefined : minus30Days, + excludeClosedBefore: showAll === 'true' ? undefined : minus30Days, }; function setFilter(val: TaskFilterType) { @@ -118,8 +121,8 @@ export const ProjectTasksView = () => { setShowClosed(String(val))} + showAll={showAll === 'true'} + onShowAllChange={(val) => setShowAll(String(val))} filter={filter} onFilterChange={setFilter} onAddTask={canEditTasks ? () => setAddDialog(true) : undefined} @@ -136,7 +139,7 @@ export const ProjectTasksView = () => { @@ -144,7 +147,7 @@ export const ProjectTasksView = () => { )} diff --git a/webapp/src/eeSetup/eeModule.ee.tsx b/webapp/src/eeSetup/eeModule.ee.tsx index 29c4ec4466..35299d550f 100644 --- a/webapp/src/eeSetup/eeModule.ee.tsx +++ b/webapp/src/eeSetup/eeModule.ee.tsx @@ -8,6 +8,7 @@ export { TranslationTaskIndicator } from '../ee/task/components/TranslationTaskI export { PermissionsAdvancedEe } from '../ee/PermissionsAdvanced/PermissionsAdvancedEe'; export { TranslationsTaskDetail } from '../ee/task/components/TranslationsTaskDetail'; export { PrefilterTask } from '../ee/task/components/PrefilterTask'; +export { PrefilterTaskHideDoneSwitch as PrefilterTaskShowDoneSwitch } from '../ee/task/components/PrefilterTaskHideDoneSwitch'; export { OrderTranslationsDialog } from '../ee/orderTranslations/OrderTranslationsDialog'; export { AgencyLabel } from '../ee/orderTranslations/AgencyLabel'; export { TaskItem } from '../ee/task/components/TaskItem'; diff --git a/webapp/src/eeSetup/eeModule.oss.tsx b/webapp/src/eeSetup/eeModule.oss.tsx index 0d6625171b..2ad428dcb6 100644 --- a/webapp/src/eeSetup/eeModule.oss.tsx +++ b/webapp/src/eeSetup/eeModule.oss.tsx @@ -2,7 +2,7 @@ import React from 'react'; import type { BillingMenuItemsProps } from './EeModuleType'; const NotIncludedInOss = - (name: string): ((props: any) => any) => + (name: string): ((props?: any) => any) => // eslint-disable-next-line react/display-name () => { return
Not included in OSS ({name})
; @@ -34,6 +34,9 @@ export const TranslationTaskIndicator = NotIncludedInOss( 'TranslationTaskIndicator' ); export const PrefilterTask = NotIncludedInOss('PrefilterTask'); +export const PrefilterTaskShowDoneSwitch = NotIncludedInOss( + 'PrefilterTaskShowDoneSwitch' +); export const TranslationsTaskDetail = Empty; export const useAddDeveloperViewItems = () => (existingItems) => existingItems; diff --git a/webapp/src/globalContext/useAuthService.tsx b/webapp/src/globalContext/useAuthService.tsx index 23ab8b8b65..8d0a7e143b 100644 --- a/webapp/src/globalContext/useAuthService.tsx +++ b/webapp/src/globalContext/useAuthService.tsx @@ -101,10 +101,11 @@ export const useAuthService = ( [] ); const [userId, setUserId] = useState(); - const [invitationCode, _setInvitationCode] = useLocalStorageState({ - initial: undefined, - key: INVITATION_CODE_STORAGE_KEY, - }); + const [invitationCode, _setInvitationCode, getInvitationCode] = + useLocalStorageState({ + initial: undefined, + key: INVITATION_CODE_STORAGE_KEY, + }); const [allowRegistration, setAllowRegistration] = useState( Boolean(invitationCode) @@ -144,10 +145,13 @@ export const useAuthService = ( } async function handleAcceptInvitation() { - if (invitationCode) { + // use code directly from localstorage + // react state might be outdated, but we don't want to wait for next render + const code = getInvitationCode(); + if (code) { try { await acceptInvitationLoadable.mutateAsync({ - path: { code: invitationCode }, + path: { code }, }); } catch (error: any) { // we want to continue regardless, error will be logged @@ -199,7 +203,7 @@ export const useAuthService = ( query: { code, redirect_uri: redirectUri, - invitationCode: invitationCode, + invitationCode: getInvitationCode(), domain, }, }, @@ -226,7 +230,12 @@ export const useAuthService = ( async signUp(data: Omit) { signupLoadable.mutate( { - content: { 'application/json': { ...data, invitationCode } }, + content: { + 'application/json': { + ...data, + invitationCode: getInvitationCode(), + }, + }, }, { onError: (error) => { diff --git a/webapp/src/hooks/useLocalStorageState.ts b/webapp/src/hooks/useLocalStorageState.ts index d46a2e0a8f..e22a8fab7d 100644 --- a/webapp/src/hooks/useLocalStorageState.ts +++ b/webapp/src/hooks/useLocalStorageState.ts @@ -7,7 +7,7 @@ type Props = { }; export function useLocalStorageState({ initial, key, derive }: Props) { - const [value, _setValue] = useState(() => { + function getLocalStorageValue() { try { const storedValue = localStorage.getItem(key); if (storedValue) { @@ -18,7 +18,7 @@ export function useLocalStorageState({ initial, key, derive }: Props) { } catch (e) { return initial; } - }); + } function setLocalStorageValue(value: string | undefined) { if (value === undefined) { @@ -28,6 +28,10 @@ export function useLocalStorageState({ initial, key, derive }: Props) { } } + const [value, _setValue] = useState(() => + getLocalStorageValue() + ); + const setValue: Dispatch> = useCallback( (valueOrFunction) => { if (typeof valueOrFunction === 'function') { @@ -45,6 +49,7 @@ export function useLocalStorageState({ initial, key, derive }: Props) { }, [_setValue] ); + derive?.(value, true); - return [value, setValue, setLocalStorageValue] as const; + return [value, setValue, getLocalStorageValue] as const; } diff --git a/webapp/src/service/apiSchema.generated.ts b/webapp/src/service/apiSchema.generated.ts index c5a36394e3..d3fc7f7d9b 100644 --- a/webapp/src/service/apiSchema.generated.ts +++ b/webapp/src/service/apiSchema.generated.ts @@ -1230,6 +1230,24 @@ export interface components { | "SERVER_ADMIN"; /** @description The user's permission type. This field is null if uses granular permissions */ type?: "NONE" | "VIEW" | "TRANSLATE" | "REVIEW" | "EDIT" | "MANAGE"; + /** + * @description List of languages user can translate to. If null, all languages editing is permitted. + * @example 200001,200004 + */ + translateLanguageIds?: number[]; + /** + * @description List of languages user can change state to. If null, changing state of all language values is permitted. + * @example 200001,200004 + */ + stateChangeLanguageIds?: number[]; + /** + * @deprecated + * @description Deprecated (use translateLanguageIds). + * + * List of languages current user has TRANSLATE permission to. If null, all languages edition is permitted. + * @example 200001,200004 + */ + permittedLanguageIds?: number[]; /** * @description List of languages user can view. If null, all languages view is permitted. * @example 200001,200004 @@ -1269,24 +1287,6 @@ export interface components { | "tasks.view" | "tasks.edit" )[]; - /** - * @deprecated - * @description Deprecated (use translateLanguageIds). - * - * List of languages current user has TRANSLATE permission to. If null, all languages edition is permitted. - * @example 200001,200004 - */ - permittedLanguageIds?: number[]; - /** - * @description List of languages user can translate to. If null, all languages editing is permitted. - * @example 200001,200004 - */ - translateLanguageIds?: number[]; - /** - * @description List of languages user can change state to. If null, changing state of all language values is permitted. - * @example 200001,200004 - */ - stateChangeLanguageIds?: number[]; }; LanguageModel: { /** Format: int64 */ @@ -2233,12 +2233,12 @@ export interface components { createNewKeys: boolean; }; ImportSettingsModel: { - /** @description If false, only updates keys, skipping the creation of new keys */ - createNewKeys: boolean; /** @description If true, placeholders from other formats will be converted to ICU when possible */ convertPlaceholdersToIcu: boolean; /** @description If true, key descriptions will be overridden by the import */ overrideKeyDescriptions: boolean; + /** @description If false, only updates keys, skipping the creation of new keys */ + createNewKeys: boolean; }; TranslationCommentModel: { /** @@ -2397,15 +2397,15 @@ export interface components { token: string; /** Format: int64 */ id: number; - /** Format: int64 */ - lastUsedAt?: number; - /** Format: int64 */ - expiresAt?: number; description: string; /** Format: int64 */ createdAt: number; /** Format: int64 */ updatedAt: number; + /** Format: int64 */ + lastUsedAt?: number; + /** Format: int64 */ + expiresAt?: number; }; SetOrganizationRoleDto: { roleType: "MEMBER" | "OWNER"; @@ -2566,16 +2566,16 @@ export interface components { /** Format: int64 */ id: number; userFullName?: string; - projectName: string; - /** Format: int64 */ - lastUsedAt?: number; + description: string; + username?: string; /** Format: int64 */ projectId: number; - username?: string; - scopes: string[]; + /** Format: int64 */ + lastUsedAt?: number; /** Format: int64 */ expiresAt?: number; - description: string; + scopes: string[]; + projectName: string; }; SuperTokenRequest: { /** @description Has to be provided when TOTP enabled */ @@ -3874,18 +3874,18 @@ export interface components { name: string; /** Format: int64 */ id: number; - basePermissions: components["schemas"]["PermissionModel"]; /** * @description The role of currently authorized user. * * Can be null when user has direct access to one of the projects owned by the organization. */ currentUserRole?: "MEMBER" | "OWNER"; - /** @example btforg */ - slug: string; - avatar?: components["schemas"]["Avatar"]; + basePermissions: components["schemas"]["PermissionModel"]; /** @example This is a beautiful organization full of beautiful and clever people */ description?: string; + avatar?: components["schemas"]["Avatar"]; + /** @example btforg */ + slug: string; }; PublicBillingConfigurationDTO: { enabled: boolean; @@ -4064,8 +4064,8 @@ export interface components { id: number; baseTranslation?: string; translation?: string; - description?: string; namespace?: string; + description?: string; }; KeySearchSearchResultModel: { view?: components["schemas"]["KeySearchResultView"]; @@ -4074,8 +4074,8 @@ export interface components { id: number; baseTranslation?: string; translation?: string; - description?: string; namespace?: string; + description?: string; }; PagedModelKeySearchSearchResultModel: { _embedded?: { @@ -4676,15 +4676,15 @@ export interface components { user: components["schemas"]["SimpleUserAccountModel"]; /** Format: int64 */ id: number; - /** Format: int64 */ - lastUsedAt?: number; - /** Format: int64 */ - expiresAt?: number; description: string; /** Format: int64 */ createdAt: number; /** Format: int64 */ updatedAt: number; + /** Format: int64 */ + lastUsedAt?: number; + /** Format: int64 */ + expiresAt?: number; }; PagedModelOrganizationModel: { _embedded?: { @@ -4804,16 +4804,16 @@ export interface components { /** Format: int64 */ id: number; userFullName?: string; - projectName: string; - /** Format: int64 */ - lastUsedAt?: number; + description: string; + username?: string; /** Format: int64 */ projectId: number; - username?: string; - scopes: string[]; + /** Format: int64 */ + lastUsedAt?: number; /** Format: int64 */ expiresAt?: number; - description: string; + scopes: string[]; + projectName: string; }; PagedModelUserAccountModel: { _embedded?: { @@ -8493,6 +8493,10 @@ export interface operations { filterFailedKeysOfJob?: number; /** Select only keys which are in specified task */ filterTaskNumber?: number[]; + /** Filter task keys which are `not done` */ + filterTaskKeysNotDone?: boolean; + /** Filter task keys which are `done` */ + filterTaskKeysDone?: boolean; /** Zero-based page index (0..N) */ page?: number; /** The size of the page to be returned */ @@ -11636,6 +11640,8 @@ export interface operations { filterAgency?: number[]; /** Exclude "done" tasks which are older than specified timestamp */ filterDoneMinClosedAt?: number; + /** Exclude tasks closed before timestamp */ + excludeClosedBefore?: number; /** Zero-based page index (0..N) */ page?: number; /** The size of the page to be returned */ @@ -14690,6 +14696,8 @@ export interface operations { filterAgency?: number[]; /** Exclude "done" tasks which are older than specified timestamp */ filterDoneMinClosedAt?: number; + /** Exclude tasks closed before timestamp */ + excludeClosedBefore?: number; /** Zero-based page index (0..N) */ page?: number; /** The size of the page to be returned */ @@ -16882,6 +16890,10 @@ export interface operations { filterFailedKeysOfJob?: number; /** Select only keys which are in specified task */ filterTaskNumber?: number[]; + /** Filter task keys which are `not done` */ + filterTaskKeysNotDone?: boolean; + /** Filter task keys which are `done` */ + filterTaskKeysDone?: boolean; }; path: { projectId: number; @@ -16982,6 +16994,10 @@ export interface operations { filterFailedKeysOfJob?: number; /** Select only keys which are in specified task */ filterTaskNumber?: number[]; + /** Filter task keys which are `not done` */ + filterTaskKeysNotDone?: boolean; + /** Filter task keys which are `done` */ + filterTaskKeysDone?: boolean; }; path: { projectId: number; diff --git a/webapp/src/service/http/useQueryApi.ts b/webapp/src/service/http/useQueryApi.ts index a375fdb6c8..b5349f852a 100644 --- a/webapp/src/service/http/useQueryApi.ts +++ b/webapp/src/service/http/useQueryApi.ts @@ -6,6 +6,7 @@ import { UseInfiniteQueryOptions, useMutation, UseMutationOptions, + useQueries, useQuery, useQueryClient, UseQueryOptions, @@ -38,6 +39,20 @@ export type QueryProps< CustomOptions; } & RequestParamsType; +export type QueriesProps< + Url extends keyof Paths, + Method extends keyof Paths[Url], + Paths = paths +> = { + queries: ({ + url: Url; + method: Method; + } & RequestParamsType)[]; + fetchOptions?: RequestOptions; + options?: UseQueryOptions, ApiError> & + CustomOptions; +}; + export type InfiniteQueryProps< Url extends keyof Paths, Method extends keyof Paths[Url], @@ -125,6 +140,32 @@ export const useApiQuery = < ); }; +export const useApiQueries = < + Url extends keyof Paths, + Method extends keyof Paths[Url], + Paths = paths +>( + props: QueryProps[] +) => { + return useQueries( + props.map((query) => { + const { url, method, fetchOptions, options, ...request } = query; + return { + queryKey: [url, (request as any)?.path, (request as any)?.query], + queryFn: () => + apiSchemaHttpService.schemaRequest(url, method, { + ...fetchOptions, + disableAutoErrorHandle: true, + })(request), + options: autoErrorHandling( + options as UseQueryOptions, + Boolean(fetchOptions?.disableAutoErrorHandle) + ), + }; + }) + ); +}; + function autoErrorHandling( options: UseQueryOptions | undefined, disabled: boolean diff --git a/webapp/src/views/projects/TaskRedirect.tsx b/webapp/src/views/projects/TaskRedirect.tsx index 70c6346df9..f2aa773473 100644 --- a/webapp/src/views/projects/TaskRedirect.tsx +++ b/webapp/src/views/projects/TaskRedirect.tsx @@ -1,7 +1,8 @@ import { useEffect } from 'react'; import { useHistory } from 'react-router-dom'; import { BoxLoading } from 'tg.component/common/BoxLoading'; -import { LINKS, PARAMS } from 'tg.constants/links'; +import { LINKS, PARAMS, QUERY } from 'tg.constants/links'; +import { useUser } from 'tg.globalContext/helpers'; import { useProject } from 'tg.hooks/useProject'; import { useUrlSearchState } from 'tg.hooks/useUrlSearchState'; import { components } from 'tg.service/apiSchema.generated'; @@ -12,6 +13,7 @@ type TaskModel = components['schemas']['TaskModel']; export const TaskRedirect = () => { const project = useProject(); const history = useHistory(); + const user = useUser(); const [taskNum] = useUrlSearchState('number', { defaultVal: undefined, }); @@ -22,10 +24,17 @@ export const TaskRedirect = () => { let url = `${LINKS.PROJECT_TRANSLATIONS.build({ [PARAMS.PROJECT_ID]: project.id, - })}?task=${task.number}`; + })}?${QUERY.TRANSLATIONS_PREFILTERS_TASK}=${task.number}`; if (detail === 'true') { - url += `&taskDetail=${task.number}`; + url += `&${QUERY.TRANSLATIONS_TASK_DETAIL}=${task.number}`; + } + + if ( + task.assignees.find((u) => u.id === user?.id) && + (task.state === 'IN_PROGRESS' || task.state === 'NEW') + ) { + url += `&${QUERY.TRANSLATIONS_PREFILTERS_TASK_HIDE_DONE}=true`; } url += diff --git a/webapp/src/views/projects/members/component/InvitationItem.tsx b/webapp/src/views/projects/members/component/InvitationItem.tsx index 106f3d1d35..cb2c1b3256 100644 --- a/webapp/src/views/projects/members/component/InvitationItem.tsx +++ b/webapp/src/views/projects/members/component/InvitationItem.tsx @@ -92,7 +92,11 @@ export const InvitationItem: React.FC = ({ invitation }) => { {invitation.invitedUserName || invitation.invitedUserEmail}{' '} {invitation.permission.agency && ( - + + + + + )}
diff --git a/webapp/src/views/projects/members/component/MemberItem.tsx b/webapp/src/views/projects/members/component/MemberItem.tsx index a4e45071ac..b5ca9b2409 100644 --- a/webapp/src/views/projects/members/component/MemberItem.tsx +++ b/webapp/src/views/projects/members/component/MemberItem.tsx @@ -1,5 +1,5 @@ import { T, useTranslate } from '@tolgee/react'; -import { Chip, styled } from '@mui/material'; +import { Chip, styled, Tooltip } from '@mui/material'; import { AgencyLabel } from 'tg.ee'; import { PermissionsMenu } from 'tg.component/PermissionsSettings/PermissionsMenu'; @@ -100,7 +100,11 @@ export const MemberItem: React.FC = ({ user }) => { )} {user.directPermission?.agency && ( - + + + + + )} diff --git a/webapp/src/views/projects/translations/BatchOperations/OperationsSummary/OperationsList.tsx b/webapp/src/views/projects/translations/BatchOperations/OperationsSummary/OperationsList.tsx index 8f2c0a4cee..d5a1009212 100644 --- a/webapp/src/views/projects/translations/BatchOperations/OperationsSummary/OperationsList.tsx +++ b/webapp/src/views/projects/translations/BatchOperations/OperationsSummary/OperationsList.tsx @@ -8,7 +8,7 @@ import { AvatarImg } from 'tg.component/common/avatar/AvatarImg'; import { TranslatedError } from 'tg.translationTools/TranslatedError'; import { useBatchOperationTypeTranslate } from 'tg.translationTools/useBatchOperationTypeTranslation'; import { OperationAbortButton } from './OperationAbortButton'; -import { LINKS, PARAMS } from 'tg.constants/links'; +import { LINKS, PARAMS, QUERY } from 'tg.constants/links'; import { useProject } from 'tg.hooks/useProject'; import { Link } from 'react-router-dom'; @@ -99,7 +99,7 @@ export const OperationsList = ({ data }: Props) => { component={Link} to={`${LINKS.PROJECT_TRANSLATIONS.build({ [PARAMS.PROJECT_ID]: project.id, - })}?failedJob=${o.id}`} + })}?${QUERY.TRANSLATIONS_PREFILTERS_FAILED_JOB}=${o.id}`} > {t('batch_operation_show_failed_keys')} diff --git a/webapp/src/views/projects/translations/TranslationHeader/TranslationControls.tsx b/webapp/src/views/projects/translations/TranslationHeader/TranslationControls.tsx index 4e3a379786..ff3b446087 100644 --- a/webapp/src/views/projects/translations/TranslationHeader/TranslationControls.tsx +++ b/webapp/src/views/projects/translations/TranslationHeader/TranslationControls.tsx @@ -1,5 +1,5 @@ import { LayoutGrid02, LayoutLeft, Plus } from '@untitled-ui/icons-react'; -import { Button, ButtonGroup, styled } from '@mui/material'; +import { Box, Button, ButtonGroup, styled } from '@mui/material'; import { T, useTranslate } from '@tolgee/react'; import { LanguagesSelect } from 'tg.component/common/form/LanguagesSelect/LanguagesSelect'; @@ -7,6 +7,7 @@ import { useProjectPermissions } from 'tg.hooks/useProjectPermissions'; import { TranslationFilters } from 'tg.component/translation/translationFilters/TranslationFilters'; import { QuickStartHighlight } from 'tg.component/layout/QuickStartGuide/QuickStartHighlight'; import { HeaderSearchField } from 'tg.component/layout/HeaderSearchField'; +import { PrefilterTaskShowDoneSwitch } from 'tg.ee'; import { useTranslationsActions, @@ -15,10 +16,9 @@ import { import { StickyHeader } from './StickyHeader'; const StyledContainer = styled('div')` - display: flex; - justify-content: space-between; - align-items: flex-start; - flex-wrap: wrap; + display: grid; + grid-template-columns: auto 1fr auto; + align-items: start; padding-bottom: 8px; padding-top: 13px; `; @@ -27,7 +27,6 @@ const StyledSpaced = styled('div')` display: flex; gap: 10px; padding: 0px 5px; - flex-wrap: wrap; `; const StyledTranslationsSearchField = styled(HeaderSearchField)` @@ -57,6 +56,9 @@ export const TranslationControls: React.FC = ({ onDialogOpen }) => { const { setFilters } = useTranslationsActions(); const selectedLanguagesMapped = allLanguages?.filter((l) => selectedLanguages?.includes(l.tag)) ?? []; + const taskPrefilter = useTranslationsSelector( + (c) => c.prefilter?.task !== undefined + ); const handleAddTranslation = () => { onDialogOpen(); @@ -80,6 +82,16 @@ export const TranslationControls: React.FC = ({ onDialogOpen }) => { /> + + {taskPrefilter && ( + + )} + + theme.spacing(-1)}; margin-right: ${({ theme }) => theme.spacing(-2)}; @@ -103,6 +111,9 @@ export const TranslationControlsCompact: React.FC = ({ setSearch(value); }; const filters = useTranslationsSelector((c) => c.filters); + const taskPrefilter = useTranslationsSelector( + (c) => c.prefilter?.task !== undefined + ); const activeFilters = getActiveFilters(filters); const { setFilters } = useTranslationsActions(); const selectedLanguagesMapped = @@ -179,6 +190,16 @@ export const TranslationControlsCompact: React.FC = ({ /> + + {taskPrefilter && ( + + )} + + { }; const isSmall = useMediaQuery( - `@media(max-width: ${rightPanelWidth + 1000}px)` + `@media(max-width: ${rightPanelWidth + 1200}px)` ); const translationsTotal = useTranslationsSelector((c) => c.translationsTotal); diff --git a/webapp/src/views/projects/translations/context/services/useTranslationsService.tsx b/webapp/src/views/projects/translations/context/services/useTranslationsService.tsx index 59d4efc0e9..e8e8e27976 100644 --- a/webapp/src/views/projects/translations/context/services/useTranslationsService.tsx +++ b/webapp/src/views/projects/translations/context/services/useTranslationsService.tsx @@ -148,6 +148,7 @@ export const useTranslationsService = (props: Props) => { filterFailedKeysOfJob: props.prefilter?.failedJob, filterTaskNumber: props.prefilter?.task !== undefined ? [props.prefilter.task] : undefined, + filterTaskKeysNotDone: props.prefilter?.taskFilterNotDone || undefined, }; const translations = useApiInfiniteQuery({ diff --git a/webapp/src/views/projects/translations/prefilters/usePrefilter.ts b/webapp/src/views/projects/translations/prefilters/usePrefilter.ts index 254f37fd14..d98ef2d4fa 100644 --- a/webapp/src/views/projects/translations/prefilters/usePrefilter.ts +++ b/webapp/src/views/projects/translations/prefilters/usePrefilter.ts @@ -1,9 +1,11 @@ +import { QUERY } from 'tg.constants/links'; import { useUrlSearchState } from 'tg.hooks/useUrlSearchState'; export type PrefilterType = { activity?: number; failedJob?: number; task?: number; + taskFilterNotDone?: boolean; clear: () => void; }; @@ -16,18 +18,35 @@ const stringToNumber = (input: string | undefined) => { }; export const usePrefilter = (): PrefilterType => { - const [activity, setActivity] = useUrlSearchState('activity', { - defaultVal: undefined, - history: true, - }); - const [failedJob, setFailedJob] = useUrlSearchState('failedJob', { - defaultVal: undefined, - history: true, - }); - const [task, setTask] = useUrlSearchState('task', { - defaultVal: undefined, - history: true, - }); + const [activity, setActivity] = useUrlSearchState( + QUERY.TRANSLATIONS_PREFILTERS_ACTIVITY, + { + defaultVal: undefined, + history: true, + } + ); + const [failedJob, setFailedJob] = useUrlSearchState( + QUERY.TRANSLATIONS_PREFILTERS_FAILED_JOB, + { + defaultVal: undefined, + history: true, + } + ); + const [task, setTask] = useUrlSearchState( + QUERY.TRANSLATIONS_PREFILTERS_TASK, + { + defaultVal: undefined, + history: true, + } + ); + + const [taskHideDone, setTaskHideDone] = useUrlSearchState( + QUERY.TRANSLATIONS_PREFILTERS_TASK_HIDE_DONE, + { + defaultVal: undefined, + history: true, + } + ); const activityId = stringToNumber(activity); const failedJobId = stringToNumber(failedJob); @@ -37,6 +56,7 @@ export const usePrefilter = (): PrefilterType => { setActivity(undefined); setFailedJob(undefined); setTask(undefined); + setTaskHideDone(undefined); } const result: PrefilterType = { @@ -49,6 +69,9 @@ export const usePrefilter = (): PrefilterType => { result.failedJob = failedJobId; } else if (taskNumber !== undefined) { result.task = taskNumber; + if (taskHideDone !== undefined) { + result.taskFilterNotDone = taskHideDone === 'true'; + } } return result; From 5bd0d1ca2cc0cd3308e4e92fe28b886c53cbac62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Gran=C3=A1t?= Date: Mon, 13 Jan 2025 12:40:28 +0100 Subject: [PATCH 2/9] chore: rename task filter --- .../src/main/kotlin/io/tolgee/ee/data/task/TaskFilters.kt | 6 +++--- .../main/kotlin/io/tolgee/ee/repository/TaskRepository.kt | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ee/backend/app/src/main/kotlin/io/tolgee/ee/data/task/TaskFilters.kt b/ee/backend/app/src/main/kotlin/io/tolgee/ee/data/task/TaskFilters.kt index 24c7f916d1..e1ee4dd057 100644 --- a/ee/backend/app/src/main/kotlin/io/tolgee/ee/data/task/TaskFilters.kt +++ b/ee/backend/app/src/main/kotlin/io/tolgee/ee/data/task/TaskFilters.kt @@ -61,14 +61,14 @@ open class TaskFilters { ) var filterAgency: List? = null - @Deprecated("Confusing logic and naming", ReplaceWith("excludeClosedBefore")) + @Deprecated("Confusing logic and naming", ReplaceWith("filterNotClosedBefore")) @field:Parameter( description = """Exclude "done" tasks which are older than specified timestamp""", ) var filterDoneMinClosedAt: Long? = null @field:Parameter( - description = """Exclude tasks closed before timestamp""", + description = """Exclude tasks which were closed before specified timestamp""", ) - var excludeClosedBefore: Long? = null + var filterNotClosedBefore: Long? = null } diff --git a/ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/TaskRepository.kt b/ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/TaskRepository.kt index 9fb342011e..394df41991 100644 --- a/ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/TaskRepository.kt +++ b/ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/TaskRepository.kt @@ -79,9 +79,9 @@ private const val TASK_FILTERS = """ or tk.closedAt > :#{#filters.filterDoneMinClosedAt} ) and ( - :#{#filters.excludeClosedBefore} is null + :#{#filters.filterNotClosedBefore} is null or tk.closedAt is null - or tk.closedAt > :#{#filters.excludeClosedBefore} + or tk.closedAt > :#{#filters.filterNotClosedBefore} ) """ From a56b6a8949126f54cd1a50c5fe40167235e44a32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Gran=C3=A1t?= Date: Mon, 13 Jan 2025 13:15:25 +0100 Subject: [PATCH 3/9] chore: add task filter test --- .../v2/controllers/task/TaskControllerTest.kt | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/ee/backend/tests/src/test/kotlin/io/tolgee/ee/api/v2/controllers/task/TaskControllerTest.kt b/ee/backend/tests/src/test/kotlin/io/tolgee/ee/api/v2/controllers/task/TaskControllerTest.kt index 0dfc11e1a7..40d3f51664 100644 --- a/ee/backend/tests/src/test/kotlin/io/tolgee/ee/api/v2/controllers/task/TaskControllerTest.kt +++ b/ee/backend/tests/src/test/kotlin/io/tolgee/ee/api/v2/controllers/task/TaskControllerTest.kt @@ -13,6 +13,8 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import java.math.BigDecimal +import java.time.Instant +import java.time.ZonedDateTime class TaskControllerTest : ProjectAuthControllerTest("/v2/projects/") { lateinit var testData: TaskTestData @@ -355,4 +357,32 @@ class TaskControllerTest : ProjectAuthControllerTest("/v2/projects/") { node("state").isEqualTo("NEW") } } + + @Test + @ProjectJWTAuthTestMethod + fun `closed tasks can be filtered out by timestamp`() { + val timeBeforeCreation = System.currentTimeMillis() + performProjectAuthPut( + "tasks/${testData.translateTask.self.number}/close", + ).andIsOk.andAssertThatJson { + node("state").isEqualTo("CLOSED") + } + val timeAfterCreation = System.currentTimeMillis() + + // should be included + performProjectAuthGet( + "tasks?filterNotClosedBefore=${timeBeforeCreation}", + ).andIsOk.andAssertThatJson { + node("page").node("totalElements").isEqualTo(2) + node("_embedded.tasks[0].name").isEqualTo("Translate task") + } + + // should be excluded + performProjectAuthGet( + "tasks?filterNotClosedBefore=${timeAfterCreation}", + ).andIsOk.andAssertThatJson { + node("page").node("totalElements").isEqualTo(1) + node("_embedded.tasks[0].name").isEqualTo("Review task") + } + } } From e67254b8e603ddafaff5cce631c7f8291313b00b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Gran=C3=A1t?= Date: Mon, 13 Jan 2025 13:26:21 +0100 Subject: [PATCH 4/9] chore: ktlint --- .../tolgee/ee/api/v2/controllers/task/TaskControllerTest.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ee/backend/tests/src/test/kotlin/io/tolgee/ee/api/v2/controllers/task/TaskControllerTest.kt b/ee/backend/tests/src/test/kotlin/io/tolgee/ee/api/v2/controllers/task/TaskControllerTest.kt index 40d3f51664..2a7e7bcea0 100644 --- a/ee/backend/tests/src/test/kotlin/io/tolgee/ee/api/v2/controllers/task/TaskControllerTest.kt +++ b/ee/backend/tests/src/test/kotlin/io/tolgee/ee/api/v2/controllers/task/TaskControllerTest.kt @@ -13,8 +13,6 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import java.math.BigDecimal -import java.time.Instant -import java.time.ZonedDateTime class TaskControllerTest : ProjectAuthControllerTest("/v2/projects/") { lateinit var testData: TaskTestData @@ -371,7 +369,7 @@ class TaskControllerTest : ProjectAuthControllerTest("/v2/projects/") { // should be included performProjectAuthGet( - "tasks?filterNotClosedBefore=${timeBeforeCreation}", + "tasks?filterNotClosedBefore=$timeBeforeCreation", ).andIsOk.andAssertThatJson { node("page").node("totalElements").isEqualTo(2) node("_embedded.tasks[0].name").isEqualTo("Translate task") @@ -379,7 +377,7 @@ class TaskControllerTest : ProjectAuthControllerTest("/v2/projects/") { // should be excluded performProjectAuthGet( - "tasks?filterNotClosedBefore=${timeAfterCreation}", + "tasks?filterNotClosedBefore=$timeAfterCreation", ).andIsOk.andAssertThatJson { node("page").node("totalElements").isEqualTo(1) node("_embedded.tasks[0].name").isEqualTo("Review task") From 1106e53c897d4d11a7944ebf0489c4f063c15165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Gran=C3=A1t?= Date: Mon, 13 Jan 2025 15:42:27 +0100 Subject: [PATCH 5/9] fix: responsivity --- webapp/src/ee/task/components/BoardColumn.tsx | 20 ++++++++++++++----- webapp/src/ee/task/components/TasksBoard.tsx | 2 +- .../components/tasksHeader/TasksHeaderBig.tsx | 1 + .../tasksHeader/TasksHeaderCompact.tsx | 6 +++--- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/webapp/src/ee/task/components/BoardColumn.tsx b/webapp/src/ee/task/components/BoardColumn.tsx index 596c1f0949..4b37549dc0 100644 --- a/webapp/src/ee/task/components/BoardColumn.tsx +++ b/webapp/src/ee/task/components/BoardColumn.tsx @@ -19,7 +19,8 @@ const StyledColumn = styled(Box)` `; const StyledColumnTitle = styled(Box)` - display: flex; + display: grid; + grid-auto-flow: column; justify-content: center; align-items: center; gap: 8px; @@ -28,6 +29,13 @@ const StyledColumnTitle = styled(Box)` font-weight: 500; `; +const StyledTitleText = styled(Box)` + display: block; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +`; + const StyledEmptyMessage = styled(Box)` display: grid; height: 138px; @@ -67,10 +75,12 @@ export const BoardColumn = ({ return ( - {title ?? - (state && ( - {translateState(state)} - ))} + + {title ?? + (state && ( + {translateState(state)} + ))} + diff --git a/webapp/src/ee/task/components/TasksBoard.tsx b/webapp/src/ee/task/components/TasksBoard.tsx index 0b2d05958c..99034972ba 100644 --- a/webapp/src/ee/task/components/TasksBoard.tsx +++ b/webapp/src/ee/task/components/TasksBoard.tsx @@ -127,7 +127,7 @@ export const TasksBoard = ({ ) : ( - + {translateState('DONE')} diff --git a/webapp/src/ee/task/components/tasksHeader/TasksHeaderBig.tsx b/webapp/src/ee/task/components/tasksHeader/TasksHeaderBig.tsx index 48e924e5da..89ac8b0618 100644 --- a/webapp/src/ee/task/components/tasksHeader/TasksHeaderBig.tsx +++ b/webapp/src/ee/task/components/tasksHeader/TasksHeaderBig.tsx @@ -104,6 +104,7 @@ export const TasksHeaderBig = ({ onChange={() => onShowAllChange(!showAll)} control={} data-cy="tasks-header-show-all" + sx={{ whiteSpace: 'nowrap' }} label={ {t('tasks_show_all_label')} diff --git a/webapp/src/ee/task/components/tasksHeader/TasksHeaderCompact.tsx b/webapp/src/ee/task/components/tasksHeader/TasksHeaderCompact.tsx index ecf7de2d94..aabbd698af 100644 --- a/webapp/src/ee/task/components/tasksHeader/TasksHeaderCompact.tsx +++ b/webapp/src/ee/task/components/tasksHeader/TasksHeaderCompact.tsx @@ -129,7 +129,7 @@ export const TasksHeaderCompact = ({ ) : ( <> - + onShowAllChange(!showAll)} control={} - sx={{ pl: 1 }} + sx={{ pl: 1, overflow: 'hidden' }} label={ - + {t('tasks_show_all_label')} From 24fb6fdd460cdd8a901ae8560df65f56af8b9472 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Gran=C3=A1t?= Date: Mon, 13 Jan 2025 16:09:32 +0100 Subject: [PATCH 6/9] fix: redirect without cache --- webapp/src/views/projects/TaskRedirect.tsx | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/webapp/src/views/projects/TaskRedirect.tsx b/webapp/src/views/projects/TaskRedirect.tsx index f2aa773473..95614491d5 100644 --- a/webapp/src/views/projects/TaskRedirect.tsx +++ b/webapp/src/views/projects/TaskRedirect.tsx @@ -1,12 +1,13 @@ import { useEffect } from 'react'; import { useHistory } from 'react-router-dom'; + import { BoxLoading } from 'tg.component/common/BoxLoading'; import { LINKS, PARAMS, QUERY } from 'tg.constants/links'; import { useUser } from 'tg.globalContext/helpers'; import { useProject } from 'tg.hooks/useProject'; import { useUrlSearchState } from 'tg.hooks/useUrlSearchState'; import { components } from 'tg.service/apiSchema.generated'; -import { useApiQuery } from 'tg.service/http/useQueryApi'; +import { useApiMutation } from 'tg.service/http/useQueryApi'; type TaskModel = components['schemas']['TaskModel']; @@ -45,17 +46,23 @@ export const TaskRedirect = () => { return url; }; - const taskLoadable = useApiQuery({ + const taskLoadable = useApiMutation({ url: '/v2/projects/{projectId}/tasks/{taskNumber}', method: 'get', - path: { projectId: project.id, taskNumber: Number(taskNum) }, }); useEffect(() => { - if (taskLoadable.data) { - history.replace(getLinkToTask(taskLoadable.data)); - } - }, [taskLoadable.data]); + taskLoadable.mutate( + { + path: { projectId: project.id, taskNumber: Number(taskNum) }, + }, + { + onSuccess(data) { + history.replace(getLinkToTask(data)); + }, + } + ); + }, []); return ; }; From a148ac4cd46b0ec70f64f21ec1b4a197f324e800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Gran=C3=A1t?= Date: Mon, 13 Jan 2025 16:52:28 +0100 Subject: [PATCH 7/9] feat: add task done but not marked as done empty state --- .../components/TaskAllDonePlaceholder.tsx | 59 +++++++++++++++++++ webapp/src/eeSetup/eeModule.ee.tsx | 1 + webapp/src/eeSetup/eeModule.oss.tsx | 1 + .../projects/translations/Translations.tsx | 6 ++ .../context/TranslationsContext.ts | 3 + .../context/services/useTaskService.tsx | 18 +++--- 6 files changed, 78 insertions(+), 10 deletions(-) create mode 100644 webapp/src/ee/task/components/TaskAllDonePlaceholder.tsx diff --git a/webapp/src/ee/task/components/TaskAllDonePlaceholder.tsx b/webapp/src/ee/task/components/TaskAllDonePlaceholder.tsx new file mode 100644 index 0000000000..631223f420 --- /dev/null +++ b/webapp/src/ee/task/components/TaskAllDonePlaceholder.tsx @@ -0,0 +1,59 @@ +import { Box, Button } from '@mui/material'; +import { T } from '@tolgee/react'; +import { EmptyListMessage } from 'tg.component/common/EmptyListMessage'; +import { QUERY } from 'tg.constants/links'; +import { useUser } from 'tg.globalContext/helpers'; +import { useUrlSearchState } from 'tg.hooks/useUrlSearchState'; +import { useApiQuery } from 'tg.service/http/useQueryApi'; +import { useTranslationsActions } from 'tg.views/projects/translations/context/TranslationsContext'; +import { TaskState } from './utils'; + +type Props = { + taskNumber: number; + projectId: number; +}; + +export const TaskAllDonePlaceholder = ({ taskNumber, projectId }: Props) => { + const [_, setTaskHideDone] = useUrlSearchState( + QUERY.TRANSLATIONS_PREFILTERS_TASK_HIDE_DONE + ); + const { finishTask } = useTranslationsActions(); + const user = useUser(); + + const handleFinishTask = async () => { + await finishTask(taskNumber); + setTaskHideDone('false'); + }; + + const taskLoadable = useApiQuery({ + url: '/v2/projects/{projectId}/tasks/{taskNumber}', + method: 'get', + path: { projectId, taskNumber }, + }); + + const isAssigned = taskLoadable.data?.assignees.find( + (u) => u.id === user?.id + ); + + return ( + + + + ) + } + > + + {taskLoadable.data?.type === 'TRANSLATE' ? ( + + ) : taskLoadable.data?.type === 'REVIEW' ? ( + + ) : null} + + + ); +}; diff --git a/webapp/src/eeSetup/eeModule.ee.tsx b/webapp/src/eeSetup/eeModule.ee.tsx index 35299d550f..0a6e8e4a10 100644 --- a/webapp/src/eeSetup/eeModule.ee.tsx +++ b/webapp/src/eeSetup/eeModule.ee.tsx @@ -9,6 +9,7 @@ export { PermissionsAdvancedEe } from '../ee/PermissionsAdvanced/PermissionsAdva export { TranslationsTaskDetail } from '../ee/task/components/TranslationsTaskDetail'; export { PrefilterTask } from '../ee/task/components/PrefilterTask'; export { PrefilterTaskHideDoneSwitch as PrefilterTaskShowDoneSwitch } from '../ee/task/components/PrefilterTaskHideDoneSwitch'; +export { TaskAllDonePlaceholder } from '../ee/task/components/TaskAllDonePlaceholder'; export { OrderTranslationsDialog } from '../ee/orderTranslations/OrderTranslationsDialog'; export { AgencyLabel } from '../ee/orderTranslations/AgencyLabel'; export { TaskItem } from '../ee/task/components/TaskItem'; diff --git a/webapp/src/eeSetup/eeModule.oss.tsx b/webapp/src/eeSetup/eeModule.oss.tsx index 2ad428dcb6..70d85d740a 100644 --- a/webapp/src/eeSetup/eeModule.oss.tsx +++ b/webapp/src/eeSetup/eeModule.oss.tsx @@ -22,6 +22,7 @@ export const AgencyLabel = NotIncludedInOss('AgencyLabel'); export const OrderTranslationsDialog = Empty; export const TaskItem = Empty; export const TaskFilterPopover = Empty; +export const TaskAllDonePlaceholder = Empty; export const routes = { Root: Empty, diff --git a/webapp/src/views/projects/translations/Translations.tsx b/webapp/src/views/projects/translations/Translations.tsx index b20f3034e8..c1ec8ca7b2 100644 --- a/webapp/src/views/projects/translations/Translations.tsx +++ b/webapp/src/views/projects/translations/Translations.tsx @@ -27,6 +27,7 @@ import { BatchOperationsChangeIndicator } from './BatchOperations/BatchOperation import { FloatingToolsPanel } from './ToolsPanel/FloatingToolsPanel'; import { Prefilter } from './prefilters/Prefilter'; import { TranslationsTaskDetail } from 'tg.ee'; +import { TaskAllDonePlaceholder } from 'tg.ee'; const StyledContainer = styled('div')` display: grid; @@ -106,6 +107,11 @@ export const Translations = () => { > + ) : prefilter?.task && prefilter.taskFilterNotDone ? ( + ) : ( { const project = useProject(); - const finishTask = useFinishTask(); + const finishTaskLoadable = useFinishTask(); const putTaskTranslation = usePutTaskTranslation(); - const handleFinishTask = (taskNumber: number) => { - return finishTask.mutateAsync({ + const finishTask = async (taskNumber: number) => { + await finishTaskLoadable.mutateAsync({ path: { projectId: project.id, taskNumber }, }); + messageService.success(); + await translations.refetchTranslations(); }; const setTaskTranslationState = (data: SetTaskTranslationState) => @@ -64,12 +66,7 @@ export const useTaskService = ({ translations }: Props) => { ), onConfirm() { - handleFinishTask(data.taskNumber).then(() => { - translations.refetchTranslations(); - }); - messageService.success( - - ); + finishTask(data.taskNumber); }, }); } @@ -79,6 +76,7 @@ export const useTaskService = ({ translations }: Props) => { return { setTaskTranslationState, - isLoading: putTaskTranslation.isLoading || finishTask.isLoading, + finishTask, + isLoading: putTaskTranslation.isLoading || finishTaskLoadable.isLoading, }; }; From a9ddfe53baffc029e92f33976acd392792f767a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Gran=C3=A1t?= Date: Mon, 13 Jan 2025 17:05:53 +0100 Subject: [PATCH 8/9] fix: download report --- webapp/src/ee/task/components/TaskMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/ee/task/components/TaskMenu.tsx b/webapp/src/ee/task/components/TaskMenu.tsx index 389b758428..38f82c2004 100644 --- a/webapp/src/ee/task/components/TaskMenu.tsx +++ b/webapp/src/ee/task/components/TaskMenu.tsx @@ -250,7 +250,7 @@ export const TaskMenu = ({ {newTaskActions && } - {t('task_menu_generate_report')} + {t('task_menu_download_report')} {taskCreate && languagesLoadable.data && ( From 26e98c216cb56f55211cf9f21fe83818c43fe69a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Gran=C3=A1t?= Date: Tue, 14 Jan 2025 09:34:38 +0100 Subject: [PATCH 9/9] fix: task type color --- webapp/src/component/task/TaskTypeChip.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/src/component/task/TaskTypeChip.tsx b/webapp/src/component/task/TaskTypeChip.tsx index f4b27d250a..7de7d8a639 100644 --- a/webapp/src/component/task/TaskTypeChip.tsx +++ b/webapp/src/component/task/TaskTypeChip.tsx @@ -9,9 +9,9 @@ const StyledChip = styled(Chip)``; export function getBackgroundColor(type: TaskType, theme: Theme) { switch (type) { case 'TRANSLATE': - return theme.palette.tokens.success._states.focusVisible; + return theme.palette.tokens.text._states.focus; case 'REVIEW': - return theme.palette.tokens.secondary._states.focus; + return theme.palette.tokens.success._states.focusVisible; } }