From 2c3a278c00a0c0940de3c2ac369b1f32d34126a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Gran=C3=A1t?= Date: Thu, 25 Jul 2024 17:23:17 +0200 Subject: [PATCH] feat: tasks basic filters --- .../api/v2/controllers/TaskController.kt | 9 ++- .../tolgee/dtos/request/task/TaskFilters.kt | 17 ++++++ .../kotlin/io/tolgee/model/enums/TaskState.kt | 1 + .../io/tolgee/repository/TaskRepository.kt | 16 ++++- .../kotlin/io/tolgee/service/TaskService.kt | 5 +- webapp/src/service/apiSchema.generated.ts | 61 ++++++++++--------- .../views/projects/tasks/ProjectTasksView.tsx | 12 +++- 7 files changed, 87 insertions(+), 34 deletions(-) create mode 100644 backend/data/src/main/kotlin/io/tolgee/dtos/request/task/TaskFilters.kt diff --git a/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/TaskController.kt b/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/TaskController.kt index 27cb29d86c..0a487854de 100644 --- a/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/TaskController.kt +++ b/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/TaskController.kt @@ -3,6 +3,8 @@ package io.tolgee.api.v2.controllers import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag import io.tolgee.dtos.request.task.* +import io.tolgee.dtos.request.translation.GetTranslationsParams +import io.tolgee.dtos.request.translation.TranslationFilters import io.tolgee.hateoas.task.TaskModel import io.tolgee.hateoas.task.TaskModelAssembler import io.tolgee.hateoas.userAccount.UserAccountInProjectModel @@ -20,9 +22,12 @@ import io.tolgee.service.TaskService import io.tolgee.service.security.UserAccountService import jakarta.validation.Valid import org.springdoc.core.annotations.ParameterObject +import org.springframework.beans.propertyeditors.CustomCollectionEditor +import org.springframework.data.domain.PageRequest import org.springframework.data.domain.Pageable import org.springframework.data.web.PagedResourcesAssembler import org.springframework.hateoas.PagedModel +import org.springframework.web.bind.WebDataBinder import org.springframework.web.bind.annotation.* @RestController @@ -49,12 +54,14 @@ class TaskController( @UseDefaultPermissions @AllowApiAccess fun getTasks( + @ParameterObject + filters: TaskFilters, @ParameterObject pageable: Pageable, @RequestParam("search", required = false) search: String?, ): PagedModel { - val tasks = taskService.getAllPaged(projectHolder.projectEntity, pageable, search) + val tasks = taskService.getAllPaged(projectHolder.projectEntity, pageable, search, filters) return pagedTaskResourcesAssembler.toModel(tasks, taskModelAssembler) } diff --git a/backend/data/src/main/kotlin/io/tolgee/dtos/request/task/TaskFilters.kt b/backend/data/src/main/kotlin/io/tolgee/dtos/request/task/TaskFilters.kt new file mode 100644 index 0000000000..968241a1e2 --- /dev/null +++ b/backend/data/src/main/kotlin/io/tolgee/dtos/request/task/TaskFilters.kt @@ -0,0 +1,17 @@ +package io.tolgee.dtos.request.task + +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.media.ExampleObject +import io.tolgee.model.enums.TaskState + +open class TaskFilters { + @field:Parameter( + description = """Filter tasks with the state""", + ) + var filterState: List? = null + + @field:Parameter( + description = """Filter tasks without the state""", + ) + var filterNotState: List? = null +} diff --git a/backend/data/src/main/kotlin/io/tolgee/model/enums/TaskState.kt b/backend/data/src/main/kotlin/io/tolgee/model/enums/TaskState.kt index 6f028454fe..96cfaca2d7 100644 --- a/backend/data/src/main/kotlin/io/tolgee/model/enums/TaskState.kt +++ b/backend/data/src/main/kotlin/io/tolgee/model/enums/TaskState.kt @@ -3,4 +3,5 @@ package io.tolgee.model.enums enum class TaskState { IN_PROGRESS, DONE, + CLOSED, } diff --git a/backend/data/src/main/kotlin/io/tolgee/repository/TaskRepository.kt b/backend/data/src/main/kotlin/io/tolgee/repository/TaskRepository.kt index 74e5108d16..e7f1f925a5 100644 --- a/backend/data/src/main/kotlin/io/tolgee/repository/TaskRepository.kt +++ b/backend/data/src/main/kotlin/io/tolgee/repository/TaskRepository.kt @@ -1,5 +1,6 @@ package io.tolgee.repository +import io.tolgee.dtos.request.task.TaskFilters import io.tolgee.model.Project import io.tolgee.model.task.Task import io.tolgee.model.task.TaskId @@ -17,17 +18,28 @@ interface TaskRepository : JpaRepository { """ select t from Task t - where t.project.id = :projectId and - ( + where + t.project.id = :projectId + and ( lower(t.name) like lower(concat('%', cast(:search as text),'%')) or cast(:search as text) is null ) + and ( + cast(:#{#filters.filterNotState} as text) is null + or t.state not in :#{#filters.filterNotState} + ) + and ( + cast(:#{#filters.filterState} as text) is null + or t.state in :#{#filters.filterState} + ) + """, ) fun getAllByProjectId( projectId: Long, pageable: Pageable, search: String?, + filters: TaskFilters ): Page @Query( diff --git a/backend/data/src/main/kotlin/io/tolgee/service/TaskService.kt b/backend/data/src/main/kotlin/io/tolgee/service/TaskService.kt index 7606a5e09e..c52455758d 100644 --- a/backend/data/src/main/kotlin/io/tolgee/service/TaskService.kt +++ b/backend/data/src/main/kotlin/io/tolgee/service/TaskService.kt @@ -52,9 +52,10 @@ class TaskService( fun getAllPaged( project: Project, pageable: Pageable, - search: String? + search: String?, + filters: TaskFilters ): Page { - val pagedTasks = taskRepository.getAllByProjectId(project.id, pageable, search) + val pagedTasks = taskRepository.getAllByProjectId(project.id, pageable, search, filters) val withPrefetched = taskRepository.getByIdsWithAllPrefetched(pagedTasks.content) return PageImpl(getTasksWithScope(withPrefetched), pageable, pagedTasks.totalElements) } diff --git a/webapp/src/service/apiSchema.generated.ts b/webapp/src/service/apiSchema.generated.ts index 3ba80f193f..b1a85ab8ce 100644 --- a/webapp/src/service/apiSchema.generated.ts +++ b/webapp/src/service/apiSchema.generated.ts @@ -1359,7 +1359,7 @@ export interface components { */ dueDate?: number; assignees?: number[]; - state?: "IN_PROGRESS" | "DONE"; + state?: "IN_PROGRESS" | "DONE" | "CLOSED"; }; SimpleUserAccountModel: { /** Format: int64 */ @@ -1390,7 +1390,7 @@ export interface components { createdAt: number; /** Format: int64 */ closedAt?: number; - state: "IN_PROGRESS" | "DONE"; + state: "IN_PROGRESS" | "DONE" | "CLOSED"; }; AutoTranslationSettingsDto: { /** Format: int64 */ @@ -2040,10 +2040,10 @@ export interface components { convertPlaceholdersToIcu: boolean; }; ImportSettingsModel: { - /** @description If true, key descriptions will be overridden by the import */ - overrideKeyDescriptions: 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; }; TranslationCommentModel: { /** @@ -2200,14 +2200,14 @@ export interface components { }; RevealedPatModel: { token: string; - /** Format: int64 */ - id: number; description: string; /** Format: int64 */ - createdAt: number; + id: number; /** Format: int64 */ updatedAt: number; /** Format: int64 */ + createdAt: number; + /** Format: int64 */ expiresAt?: number; /** Format: int64 */ lastUsedAt?: number; @@ -2346,17 +2346,17 @@ export interface components { RevealedApiKeyModel: { /** @description Resulting user's api key */ key: string; + description: string; /** Format: int64 */ id: number; projectName: string; userFullName?: string; - description: string; username?: string; /** Format: int64 */ - projectId: number; - /** Format: int64 */ expiresAt?: number; /** Format: int64 */ + projectId: number; + /** Format: int64 */ lastUsedAt?: number; scopes: string[]; }; @@ -2433,7 +2433,7 @@ export interface components { languageId: number; assignees: number[]; keys: number[]; - state?: "IN_PROGRESS" | "DONE"; + state?: "IN_PROGRESS" | "DONE" | "CLOSED"; }; CalculateScopeRequest: { /** Format: int64 */ @@ -3373,7 +3373,7 @@ export interface components { createdAt: number; /** Format: int64 */ closedAt?: number; - state: "IN_PROGRESS" | "DONE"; + state: "IN_PROGRESS" | "DONE" | "CLOSED"; project: components["schemas"]["SimpleProjectModel"]; }; UserPreferencesModel: { @@ -3496,22 +3496,22 @@ export interface components { | "SLACK_INTEGRATION" )[]; quickStart?: components["schemas"]["QuickStartModel"]; + /** @example This is a beautiful organization full of beautiful and clever people */ + description?: string; /** @example Beautiful organization */ name: string; /** Format: int64 */ id: number; basePermissions: components["schemas"]["PermissionModel"]; - /** @example This is a beautiful organization full of beautiful and clever people */ - description?: string; /** * @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"; + avatar?: components["schemas"]["Avatar"]; /** @example btforg */ slug: string; - avatar?: components["schemas"]["Avatar"]; }; PublicBillingConfigurationDTO: { enabled: boolean; @@ -3572,9 +3572,9 @@ export interface components { defaultFileStructureTemplate: string; }; DocItem: { + description?: string; name: string; displayName?: string; - description?: string; }; PagedModelProjectModel: { _embedded?: { @@ -3651,23 +3651,23 @@ export interface components { formalitySupported: boolean; }; KeySearchResultView: { + description?: string; name: string; /** Format: int64 */ id: number; - namespace?: string; - description?: string; - baseTranslation?: string; translation?: string; + baseTranslation?: string; + namespace?: string; }; KeySearchSearchResultModel: { view?: components["schemas"]["KeySearchResultView"]; + description?: string; name: string; /** Format: int64 */ id: number; - namespace?: string; - description?: string; - baseTranslation?: string; translation?: string; + baseTranslation?: string; + namespace?: string; }; PagedModelKeySearchSearchResultModel: { _embedded?: { @@ -4213,14 +4213,14 @@ export interface components { }; PatWithUserModel: { user: components["schemas"]["SimpleUserAccountModel"]; - /** Format: int64 */ - id: number; description: string; /** Format: int64 */ - createdAt: number; + id: number; /** Format: int64 */ updatedAt: number; /** Format: int64 */ + createdAt: number; + /** Format: int64 */ expiresAt?: number; /** Format: int64 */ lastUsedAt?: number; @@ -4340,17 +4340,17 @@ export interface components { * @description Languages for which user has translate permission. */ permittedLanguageIds?: number[]; + description: string; /** Format: int64 */ id: number; projectName: string; userFullName?: string; - description: string; username?: string; /** Format: int64 */ - projectId: number; - /** Format: int64 */ expiresAt?: number; /** Format: int64 */ + projectId: number; + /** Format: int64 */ lastUsedAt?: number; scopes: string[]; }; @@ -10516,6 +10516,10 @@ export interface operations { /** Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ sort?: string[]; search?: string; + /** Filter tasks with the state */ + filterState?: ("IN_PROGRESS" | "DONE" | "CLOSED")[]; + /** Filter tasks without the state */ + filterNotState?: ("IN_PROGRESS" | "DONE" | "CLOSED")[]; }; path: { projectId: number; @@ -13480,6 +13484,7 @@ export interface operations { size?: number; /** Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. */ sort?: string[]; + search?: string; }; }; responses: { diff --git a/webapp/src/views/projects/tasks/ProjectTasksView.tsx b/webapp/src/views/projects/tasks/ProjectTasksView.tsx index e14acc40ec..bdb60ca4f8 100644 --- a/webapp/src/views/projects/tasks/ProjectTasksView.tsx +++ b/webapp/src/views/projects/tasks/ProjectTasksView.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { Dialog, ListProps, PaperProps, styled } from '@mui/material'; +import { Checkbox, Dialog, ListProps, PaperProps, styled } from '@mui/material'; import { useTranslate } from '@tolgee/react'; import { useProject } from 'tg.hooks/useProject'; @@ -24,6 +24,8 @@ export const ProjectTasksView = () => { const project = useProject(); const { t } = useTranslate(); const [page, setPage] = useState(0); + const [search, setSearch] = useState(''); + const [showClosed, setShowClosed] = useState(false); const [detail, setDetail] = useState(); @@ -38,6 +40,8 @@ export const ProjectTasksView = () => { query: { size: 20, page, + search, + filterNotState: showClosed ? undefined : ['CLOSED', 'DONE'], }, options: { keepPreviousData: true, @@ -58,9 +62,15 @@ export const ProjectTasksView = () => { ], ]} > + setShowClosed(e.target.checked)} + />