From c11cba6c4ce491e393df1d1c35cbc6d6c3c56279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Gran=C3=A1t?= Date: Fri, 30 Aug 2024 15:28:48 +0200 Subject: [PATCH] feat: prepare for activity --- .../api/v2/controllers/TaskController.kt | 67 +++++----- .../translation/TranslationsController.kt | 2 +- .../io/tolgee/hateoas/task/TaskModel.kt | 4 +- .../tolgee/hateoas/task/TaskModelAssembler.kt | 4 +- .../hateoas/task/TaskWithProjectModel.kt | 4 +- .../task/TaskWithProjectModelAssembler.kt | 4 +- .../hateoas/translations/KeyTaskViewModel.kt | 2 +- .../translations/KeyTaskViewModelAssembler.kt | 2 +- .../io/tolgee/activity/data/ActivityType.kt | 4 + .../tolgee/component/task/TaskReportHelper.kt | 4 +- .../request/translation/TranslationFilters.kt | 2 +- .../main/kotlin/io/tolgee/model/task/Task.kt | 19 ++- .../kotlin/io/tolgee/model/task/TaskId.kt | 12 -- .../io/tolgee/model/views/KeyTaskView.kt | 2 +- .../io/tolgee/model/views/TaskScopeView.kt | 1 - .../tolgee/model/views/TaskWithScopeView.kt | 4 +- .../model/views/TranslationToTaskView.kt | 2 +- .../io/tolgee/repository/TaskRepository.kt | 57 ++++---- .../kotlin/io/tolgee/service/TaskService.kt | 73 ++++++----- .../QueryGlobalFiltering.kt | 4 +- .../service/security/SecurityService.kt | 8 +- .../main/resources/db/changelog/schema.xml | 100 +++++--------- webapp/src/component/task/TaskDetail.tsx | 12 +- webapp/src/component/task/TaskId.tsx | 2 +- webapp/src/component/task/TaskLabel.tsx | 4 +- webapp/src/component/task/TaskMenu.tsx | 16 +-- webapp/src/component/task/TaskTooltip.tsx | 6 +- .../src/component/task/TaskTooltipContent.tsx | 12 +- .../taskSelect/TaskSearchSelectPopover.tsx | 8 +- webapp/src/component/task/utils.ts | 6 +- webapp/src/service/TranslationHooks.ts | 6 +- webapp/src/service/apiSchema.generated.ts | 124 +++++++++--------- webapp/src/views/myTasks/MyTasksView.tsx | 2 +- .../BatchOperations/OperationTaskAddKeys.tsx | 4 +- .../OperationTaskRemoveKeys.tsx | 8 +- .../ToolsPanel/panels/Tasks/Tasks.tsx | 6 +- .../translations/cell/ControlsEditorMain.tsx | 2 +- .../translations/cell/ControlsEditorSmall.tsx | 2 +- .../translations/cell/ControlsTranslation.tsx | 2 +- .../translations/cell/TranslationFlags.tsx | 4 +- .../context/services/useEditService.tsx | 2 +- .../context/services/useStateService.tsx | 2 +- .../context/services/useTaskService.tsx | 12 +- .../services/useTranslationsService.tsx | 2 +- .../projects/translations/context/types.ts | 4 +- .../translations/prefilters/Prefilter.tsx | 2 +- .../translations/prefilters/PrefilterTask.tsx | 20 +-- .../translations/prefilters/usePrefilter.ts | 6 +- .../translations/useTranslationCell.ts | 2 +- 49 files changed, 321 insertions(+), 338 deletions(-) delete mode 100644 backend/data/src/main/kotlin/io/tolgee/model/task/TaskId.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 ce54ba0377..dc42da2162 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 @@ -2,6 +2,7 @@ package io.tolgee.api.v2.controllers import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag +import io.tolgee.activity.data.ActivityType import io.tolgee.dtos.request.task.* import io.tolgee.dtos.request.userAccount.UserAccountPermissionsFilters import io.tolgee.hateoas.task.TaskModel @@ -86,58 +87,58 @@ class TaskController( return taskModelAssembler.toModel(task) } - @GetMapping("/{taskId}") + @GetMapping("/{taskNumber}") @Operation(summary = "Get task") @UseDefaultPermissions @AllowApiAccess fun getTask( @PathVariable - taskId: Long, + taskNumber: Long, ): TaskModel { // user can view tasks assigned to him - securityService.hasTaskEditScopeOrIsAssigned(projectHolder.projectEntity.id, taskId) - val task = taskService.getTask(projectHolder.projectEntity, taskId) + securityService.hasTaskEditScopeOrIsAssigned(projectHolder.projectEntity.id, taskNumber) + val task = taskService.getTask(projectHolder.projectEntity, taskNumber) return taskModelAssembler.toModel(task) } - @PutMapping("/{taskId}") + @PutMapping("/{taskNumber}") @Operation(summary = "Update task") @RequiresProjectPermissions([Scope.TASKS_EDIT]) @AllowApiAccess fun updateTask( @PathVariable - taskId: Long, + taskNumber: Long, @RequestBody @Valid dto: UpdateTaskRequest, ): TaskModel { - val task = taskService.updateTask(projectHolder.projectEntity, taskId, dto) + val task = taskService.updateTask(projectHolder.projectEntity, taskNumber, dto) return taskModelAssembler.toModel(task) } - @GetMapping("/{taskId}/per-user-report") + @GetMapping("/{taskNumber}/per-user-report") @Operation(summary = "Report who did what") @UseDefaultPermissions @AllowApiAccess fun getPerUserReport( @PathVariable - taskId: Long, + taskNumber: Long, ): List { - securityService.hasTaskViewScopeOrIsAssigned(projectHolder.projectEntity.id, taskId) + securityService.hasTaskViewScopeOrIsAssigned(projectHolder.projectEntity.id, taskNumber) - val result = taskService.getReport(projectHolder.projectEntity, taskId) + val result = taskService.getReport(projectHolder.projectEntity, taskNumber) return result.map { taskPerUserReportModelAssembler.toModel(it) } } - @GetMapping("/{taskId}/csv-report") + @GetMapping("/{taskNumber}/csv-report") @Operation(summary = "Report who did what") @UseDefaultPermissions @AllowApiAccess fun getCsvReport( @PathVariable - taskId: Long, + taskNumber: Long, ): ResponseEntity { - securityService.hasTaskViewScopeOrIsAssigned(projectHolder.projectEntity.id, taskId) - val byteArray = taskService.getExcelFile(projectHolder.projectEntity, taskId) + securityService.hasTaskViewScopeOrIsAssigned(projectHolder.projectEntity.id, taskNumber) + val byteArray = taskService.getExcelFile(projectHolder.projectEntity, taskNumber) val resource = ByteArrayResource(byteArray) val headers = HttpHeaders() @@ -148,59 +149,59 @@ class TaskController( return ResponseEntity(resource, headers, HttpStatus.OK) } - @GetMapping("/{taskId}/keys") + @GetMapping("/{taskNumber}/keys") @Operation(summary = "Get task keys") @UseDefaultPermissions @AllowApiAccess fun getTaskKeys( @PathVariable - taskId: Long, + taskNumber: Long, ): TaskKeysResponse { - securityService.hasTaskViewScopeOrIsAssigned(projectHolder.projectEntity.id, taskId) + securityService.hasTaskViewScopeOrIsAssigned(projectHolder.projectEntity.id, taskNumber) return TaskKeysResponse( - keys = taskService.getTaskKeys(projectHolder.projectEntity, taskId), + keys = taskService.getTaskKeys(projectHolder.projectEntity, taskNumber), ) } - @PutMapping("/{taskId}/keys") + @PutMapping("/{taskNumber}/keys") @Operation(summary = "Add or remove task keys") @RequiresProjectPermissions([Scope.TASKS_EDIT]) @AllowApiAccess fun updateTaskKeys( @PathVariable - taskId: Long, + taskNumber: Long, @RequestBody @Valid dto: UpdateTaskKeysRequest, ) { - taskService.updateTaskKeys(projectHolder.projectEntity, taskId, dto) + taskService.updateTaskKeys(projectHolder.projectEntity, taskNumber, dto) } - @GetMapping("/{taskId}/blocking-tasks") + @GetMapping("/{taskNumber}/blocking-tasks") @Operation(summary = "Get task ids which block this task") @UseDefaultPermissions @AllowApiAccess fun getBlockingTasks( @PathVariable - taskId: Long, + taskNumber: Long, ): List { - return taskService.getBlockingTasks(projectHolder.projectEntity, taskId) + return taskService.getBlockingTasks(projectHolder.projectEntity, taskNumber) } - @PostMapping("/{taskId}/finish") + @PostMapping("/{taskNumber}/finish") @Operation(summary = "Finish task") // permissions checked inside @UseDefaultPermissions @AllowApiAccess fun finishTask( @PathVariable - taskId: Long, + taskNumber: Long, ): TaskModel { // user can only finish tasks assigned to him - securityService.hasTaskEditScopeOrIsAssigned(projectHolder.projectEntity.id, taskId) + securityService.hasTaskEditScopeOrIsAssigned(projectHolder.projectEntity.id, taskNumber) val task = taskService.updateTask( projectHolder.projectEntity, - taskId, + taskNumber, UpdateTaskRequest( state = TaskState.DONE, ), @@ -208,22 +209,22 @@ class TaskController( return taskModelAssembler.toModel(task) } - @PutMapping("/{taskId}/keys/{keyId}") + @PutMapping("/{taskNumber}/keys/{keyId}") @Operation(summary = "Update task key") // permissions checked inside @UseDefaultPermissions @AllowApiAccess fun updateTaskKey( @PathVariable - taskId: Long, + taskNumber: Long, @PathVariable keyId: Long, @RequestBody @Valid dto: UpdateTaskKeyRequest, ): UpdateTaskKeyResponse { // user can only update tasks assigned to him - securityService.hasTaskEditScopeOrIsAssigned(projectHolder.projectEntity.id, taskId) - return taskService.updateTaskKey(projectHolder.projectEntity, taskId, keyId, dto) + securityService.hasTaskEditScopeOrIsAssigned(projectHolder.projectEntity.id, taskNumber) + return taskService.updateTaskKey(projectHolder.projectEntity, taskNumber, keyId, dto) } @PostMapping("/create-multiple") diff --git a/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/translation/TranslationsController.kt b/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/translation/TranslationsController.kt index 047e5f5374..6cf6635418 100644 --- a/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/translation/TranslationsController.kt +++ b/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/translation/TranslationsController.kt @@ -284,7 +284,7 @@ When null, resulting file will be a flat key-value object. key.tasks = translationsWithTasks[key.keyId]?.map { KeyTaskView( - it.taskId, + it.taskNumber, it.languageId, it.languageTag, it.taskDone, diff --git a/backend/api/src/main/kotlin/io/tolgee/hateoas/task/TaskModel.kt b/backend/api/src/main/kotlin/io/tolgee/hateoas/task/TaskModel.kt index d4fe5ec5d0..d971a271e9 100644 --- a/backend/api/src/main/kotlin/io/tolgee/hateoas/task/TaskModel.kt +++ b/backend/api/src/main/kotlin/io/tolgee/hateoas/task/TaskModel.kt @@ -9,7 +9,7 @@ import org.springframework.hateoas.server.core.Relation @Relation(collectionRelation = "tasks", itemRelation = "task") class TaskModel( - var id: Long = 0L, + var number: Long = 0L, var name: String = "", var description: String = "", var type: TaskType = TaskType.TRANSLATE, @@ -21,7 +21,7 @@ class TaskModel( var baseWordCount: Long = 0, var baseCharacterCount: Long = 0, var author: SimpleUserAccountModel? = null, - var createdAt: Long = 0, + var createdAt: Long? = 0, var closedAt: Long? = null, var state: TaskState = TaskState.IN_PROGRESS, ) : RepresentationModel() diff --git a/backend/api/src/main/kotlin/io/tolgee/hateoas/task/TaskModelAssembler.kt b/backend/api/src/main/kotlin/io/tolgee/hateoas/task/TaskModelAssembler.kt index b136cf1415..4efe83f4d1 100644 --- a/backend/api/src/main/kotlin/io/tolgee/hateoas/task/TaskModelAssembler.kt +++ b/backend/api/src/main/kotlin/io/tolgee/hateoas/task/TaskModelAssembler.kt @@ -18,7 +18,7 @@ class TaskModelAssembler( ) { override fun toModel(entity: TaskWithScopeView): TaskModel { return TaskModel( - id = entity.id, + number = entity.number, name = entity.name, description = entity.description, type = entity.type, @@ -34,7 +34,7 @@ class TaskModelAssembler( dueDate = entity.dueDate?.time, assignees = entity.assignees.map { simpleUserAccountModelAssembler.toModel(it) }.toMutableSet(), author = entity.author?.let { simpleUserAccountModelAssembler.toModel(it) }, - createdAt = entity.createdAt.time, + createdAt = entity.createdAt?.time, closedAt = entity.closedAt?.time, totalItems = entity.totalItems, doneItems = entity.doneItems, diff --git a/backend/api/src/main/kotlin/io/tolgee/hateoas/task/TaskWithProjectModel.kt b/backend/api/src/main/kotlin/io/tolgee/hateoas/task/TaskWithProjectModel.kt index 84865ecf12..ca40b0d6fc 100644 --- a/backend/api/src/main/kotlin/io/tolgee/hateoas/task/TaskWithProjectModel.kt +++ b/backend/api/src/main/kotlin/io/tolgee/hateoas/task/TaskWithProjectModel.kt @@ -10,7 +10,7 @@ import org.springframework.hateoas.server.core.Relation @Relation(collectionRelation = "tasks", itemRelation = "task") data class TaskWithProjectModel( - var id: Long = 0L, + var number: Long = 0L, var name: String = "", var description: String = "", var type: TaskType = TaskType.TRANSLATE, @@ -22,7 +22,7 @@ data class TaskWithProjectModel( var baseWordCount: Long = 0, var baseCharacterCount: Long = 0, var author: SimpleUserAccountModel? = null, - var createdAt: Long = 0, + var createdAt: Long? = 0, var closedAt: Long? = null, var state: TaskState = TaskState.IN_PROGRESS, var project: SimpleProjectModel, diff --git a/backend/api/src/main/kotlin/io/tolgee/hateoas/task/TaskWithProjectModelAssembler.kt b/backend/api/src/main/kotlin/io/tolgee/hateoas/task/TaskWithProjectModelAssembler.kt index f7a7a144fc..6ebf859688 100644 --- a/backend/api/src/main/kotlin/io/tolgee/hateoas/task/TaskWithProjectModelAssembler.kt +++ b/backend/api/src/main/kotlin/io/tolgee/hateoas/task/TaskWithProjectModelAssembler.kt @@ -20,7 +20,7 @@ class TaskWithProjectModelAssembler( ) { override fun toModel(entity: TaskWithScopeView): TaskWithProjectModel { return TaskWithProjectModel( - id = entity.id, + number = entity.number, name = entity.name, description = entity.description, type = entity.type, @@ -36,7 +36,7 @@ class TaskWithProjectModelAssembler( dueDate = entity.dueDate?.time, assignees = entity.assignees.map { simpleUserAccountModelAssembler.toModel(it) }.toMutableSet(), author = entity.author?.let { simpleUserAccountModelAssembler.toModel(it) }, - createdAt = entity.createdAt.time, + createdAt = entity.createdAt?.time, closedAt = entity.closedAt?.time, totalItems = entity.totalItems, doneItems = entity.doneItems, diff --git a/backend/api/src/main/kotlin/io/tolgee/hateoas/translations/KeyTaskViewModel.kt b/backend/api/src/main/kotlin/io/tolgee/hateoas/translations/KeyTaskViewModel.kt index 3431c32ff6..804b5a9076 100644 --- a/backend/api/src/main/kotlin/io/tolgee/hateoas/translations/KeyTaskViewModel.kt +++ b/backend/api/src/main/kotlin/io/tolgee/hateoas/translations/KeyTaskViewModel.kt @@ -4,7 +4,7 @@ import io.tolgee.model.enums.TaskType import org.springframework.hateoas.RepresentationModel open class KeyTaskViewModel( - val id: Long, + val number: Long, val languageId: Long, val languageTag: String, val done: Boolean, diff --git a/backend/api/src/main/kotlin/io/tolgee/hateoas/translations/KeyTaskViewModelAssembler.kt b/backend/api/src/main/kotlin/io/tolgee/hateoas/translations/KeyTaskViewModelAssembler.kt index 42c0aa2a5e..29e4ab4cdb 100644 --- a/backend/api/src/main/kotlin/io/tolgee/hateoas/translations/KeyTaskViewModelAssembler.kt +++ b/backend/api/src/main/kotlin/io/tolgee/hateoas/translations/KeyTaskViewModelAssembler.kt @@ -13,7 +13,7 @@ class KeyTaskViewModelAssembler : ) { override fun toModel(view: KeyTaskView): KeyTaskViewModel { return KeyTaskViewModel( - id = view.id, + number = view.number, languageId = view.languageId, languageTag = view.languageTag, done = view.done, diff --git a/backend/data/src/main/kotlin/io/tolgee/activity/data/ActivityType.kt b/backend/data/src/main/kotlin/io/tolgee/activity/data/ActivityType.kt index 849d17fdb3..f19d7cb0b4 100644 --- a/backend/data/src/main/kotlin/io/tolgee/activity/data/ActivityType.kt +++ b/backend/data/src/main/kotlin/io/tolgee/activity/data/ActivityType.kt @@ -65,4 +65,8 @@ enum class ActivityType( WEBHOOK_CONFIG_UPDATE, WEBHOOK_CONFIG_DELETE, COMPLEX_TAG_OPERATION(onlyCountsInList = true), + TASK_CREATE, + TASK_UPDATE, + TASK_KEYS_UPDATE, + TASK_FINISH, } diff --git a/backend/data/src/main/kotlin/io/tolgee/component/task/TaskReportHelper.kt b/backend/data/src/main/kotlin/io/tolgee/component/task/TaskReportHelper.kt index d53905aa5f..773b49a1e2 100644 --- a/backend/data/src/main/kotlin/io/tolgee/component/task/TaskReportHelper.kt +++ b/backend/data/src/main/kotlin/io/tolgee/component/task/TaskReportHelper.kt @@ -83,7 +83,9 @@ class TaskReportHelper( sheet.createRow(6).let { it.createCell(0).setCellValue("Created at") - it.createCell(1).setCellValue(formatDate(task.createdAt)) + task.createdAt?.let { createdAt -> + it.createCell(1).setCellValue(formatDate(createdAt)) + } } sheet.createRow(7).let { 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 9100dc1203..eb4d5b8920 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 @@ -103,5 +103,5 @@ To filter default namespace, set to empty string. @field:Parameter( description = "Select only keys which are in specified task", ) - var filterTaskId: List? = null + var filterTaskNumber: List? = null } diff --git a/backend/data/src/main/kotlin/io/tolgee/model/task/Task.kt b/backend/data/src/main/kotlin/io/tolgee/model/task/Task.kt index e1bf3ae539..1d55ed77c3 100644 --- a/backend/data/src/main/kotlin/io/tolgee/model/task/Task.kt +++ b/backend/data/src/main/kotlin/io/tolgee/model/task/Task.kt @@ -1,8 +1,6 @@ package io.tolgee.model.task -import io.tolgee.model.Language -import io.tolgee.model.Project -import io.tolgee.model.UserAccount +import io.tolgee.model.* import io.tolgee.model.enums.TaskState import io.tolgee.model.enums.TaskType import jakarta.persistence.* @@ -10,14 +8,17 @@ import jakarta.validation.constraints.Size import java.util.* @Entity -@IdClass(TaskId::class) -class Task { - @Id +@Table(uniqueConstraints = [ + UniqueConstraint( + columnNames = ["project_id", "number"], + name = "project_number_unique" + ) +]) +class Task : StandardAuditModel() { @ManyToOne(fetch = FetchType.LAZY) var project: Project = Project() // Initialize to avoid null issues - @Id - var id: Long = 1L + var number: Long = 1L @field:Size(max = 255) @Column(length = 255) @@ -44,8 +45,6 @@ class Task { @ManyToOne(fetch = FetchType.LAZY, optional = true) var author: UserAccount? = null - var createdAt: Date = Date() - @Enumerated(EnumType.STRING) var state: TaskState = TaskState.IN_PROGRESS diff --git a/backend/data/src/main/kotlin/io/tolgee/model/task/TaskId.kt b/backend/data/src/main/kotlin/io/tolgee/model/task/TaskId.kt deleted file mode 100644 index cbe68f4255..0000000000 --- a/backend/data/src/main/kotlin/io/tolgee/model/task/TaskId.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.tolgee.model.task - -import io.tolgee.model.Project -import jakarta.persistence.FetchType -import jakarta.persistence.ManyToOne -import java.io.Serializable - -data class TaskId( - @ManyToOne(fetch = FetchType.LAZY) - var project: Project = Project(), - var id: Long = 1L, -) : Serializable diff --git a/backend/data/src/main/kotlin/io/tolgee/model/views/KeyTaskView.kt b/backend/data/src/main/kotlin/io/tolgee/model/views/KeyTaskView.kt index 8f16b51de7..ca4d3744f4 100644 --- a/backend/data/src/main/kotlin/io/tolgee/model/views/KeyTaskView.kt +++ b/backend/data/src/main/kotlin/io/tolgee/model/views/KeyTaskView.kt @@ -3,7 +3,7 @@ package io.tolgee.model.views import io.tolgee.model.enums.TaskType class KeyTaskView( - val id: Long, + val number: Long, val languageId: Long, val languageTag: String, val done: Boolean, diff --git a/backend/data/src/main/kotlin/io/tolgee/model/views/TaskScopeView.kt b/backend/data/src/main/kotlin/io/tolgee/model/views/TaskScopeView.kt index bc6c1320d2..d5f377c11c 100644 --- a/backend/data/src/main/kotlin/io/tolgee/model/views/TaskScopeView.kt +++ b/backend/data/src/main/kotlin/io/tolgee/model/views/TaskScopeView.kt @@ -2,7 +2,6 @@ package io.tolgee.model.views interface TaskScopeView { val taskId: Long? - val projectId: Long? val totalItems: Long val doneItems: Long val baseCharacterCount: Long diff --git a/backend/data/src/main/kotlin/io/tolgee/model/views/TaskWithScopeView.kt b/backend/data/src/main/kotlin/io/tolgee/model/views/TaskWithScopeView.kt index 5dcd85f85e..667042af01 100644 --- a/backend/data/src/main/kotlin/io/tolgee/model/views/TaskWithScopeView.kt +++ b/backend/data/src/main/kotlin/io/tolgee/model/views/TaskWithScopeView.kt @@ -10,7 +10,7 @@ import java.util.* data class TaskWithScopeView( val project: Project, - val id: Long, + val number: Long, val name: String, val description: String, val type: TaskType, @@ -19,7 +19,7 @@ data class TaskWithScopeView( val assignees: MutableSet, val keys: MutableSet, val author: UserAccount, - val createdAt: Date, + val createdAt: Date?, val state: TaskState, val closedAt: Date?, val totalItems: Long, diff --git a/backend/data/src/main/kotlin/io/tolgee/model/views/TranslationToTaskView.kt b/backend/data/src/main/kotlin/io/tolgee/model/views/TranslationToTaskView.kt index 24b471e505..13f1da5453 100644 --- a/backend/data/src/main/kotlin/io/tolgee/model/views/TranslationToTaskView.kt +++ b/backend/data/src/main/kotlin/io/tolgee/model/views/TranslationToTaskView.kt @@ -6,7 +6,7 @@ interface TranslationToTaskView { var keyId: Long var languageId: Long var languageTag: String - var taskId: Long + var taskNumber: Long var taskDone: Boolean var taskAssigned: Boolean var taskType: TaskType 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 641c25995d..06e49eab4f 100644 --- a/backend/data/src/main/kotlin/io/tolgee/repository/TaskRepository.kt +++ b/backend/data/src/main/kotlin/io/tolgee/repository/TaskRepository.kt @@ -6,7 +6,6 @@ import io.tolgee.model.Project import io.tolgee.model.UserAccount import io.tolgee.model.enums.TaskType import io.tolgee.model.task.Task -import io.tolgee.model.task.TaskId import io.tolgee.model.views.KeysScopeView import io.tolgee.model.views.TaskPerUserReportView import io.tolgee.model.views.TaskScopeView @@ -16,6 +15,7 @@ import org.springframework.data.domain.Pageable import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository +import java.util.Optional const val TASK_SEARCH = """ ( @@ -39,11 +39,11 @@ const val TASK_FILTERS = """ ) and ( :#{#filters.filterId} is null - or tk.id in :#{#filters.filterId} + or tk.number in :#{#filters.filterId} ) and ( :#{#filters.filterNotId} is null - or tk.id not in :#{#filters.filterNotId} + or tk.number not in :#{#filters.filterNotId} ) and ( :#{#filters.filterProject} is null @@ -76,7 +76,7 @@ const val TASK_FILTERS = """ """ @Repository -interface TaskRepository : JpaRepository { +interface TaskRepository : JpaRepository { @Query( """ select tk @@ -121,13 +121,13 @@ interface TaskRepository : JpaRepository { tt.key_id as keyId, l.id as languageId, l.tag as languageTag, - t.id as taskId, + t.number as taskNumber, tt.done as taskDone, CASE WHEN u.id IS NULL THEN FALSE ELSE TRUE END as taskAssigned, t.type as taskType from task t - join task_key tt on (t.id = tt.task_id and tt.task_project_id = t.project_id) - left join task_assignees ta on (ta.tasks_id = t.id and ta.tasks_project_id = t.project_id) + join task_key tt on (t.id = tt.task_id) + left join task_assignees ta on (ta.tasks_id = t.id) left join user_account u on ta.assignees_id = u.id left join language l on (t.language_id = l.id) where @@ -164,10 +164,10 @@ interface TaskRepository : JpaRepository { where t.project = :project and l.deletedAt is null - order by t.id desc + order by t.number desc """, ) - fun findByProjectOrderByIdDesc(project: Project): List + fun findByProjectOrderByNumberDesc(project: Project): List @Query( nativeQuery = true, @@ -177,7 +177,7 @@ interface TaskRepository : JpaRepository { left join ( select key.id as key_id from key join task_key on (key.id = task_key.key_id) - join task on (task_key.task_id = task.id and task_key.task_project_id = :projectId) + join task on (task_key.task_id = task.id) left join language l on (task.language_id = l.id) where task.type = :taskType and task.language_id = :languageId @@ -215,12 +215,13 @@ interface TaskRepository : JpaRepository { select k.id from Key k left join k.tasks tt - where k.project.id = :projectId and tt.task.id = :taskId + left join tt.task t + where k.project.id = :projectId and t.number = :taskNumber """, ) fun getTaskKeys( projectId: Long, - taskId: Long, + taskNumber: Long, ): List @Query( @@ -243,7 +244,6 @@ interface TaskRepository : JpaRepository { value = """ select tk.id as taskId, - tk.project.id as projectId, count(k.id) as totalItems, coalesce(sum(case when tt.done then 1 else 0 end), 0) as doneItems, coalesce(sum(bt.characterCount), 0) as baseCharacterCount, @@ -268,7 +268,7 @@ interface TaskRepository : JpaRepository { left join Key k on element(tt).key.id = k.id left join k.translations as btr on btr.language.id = :baseLangId where tk.project.id = :projectId - and tk.id = :taskId + and tk.number = :taskNumber and tt.done and u.id is not NULL group by u @@ -276,7 +276,7 @@ interface TaskRepository : JpaRepository { ) fun perUserReport( projectId: Long, - taskId: Long, + taskNumber: Long, baseLangId: Long, ): List @@ -285,14 +285,14 @@ interface TaskRepository : JpaRepository { select u from UserAccount u join u.tasks tk - where tk.id = :taskId + where tk.number = :taskNumber and tk.project.id = :projectId and u.id = :userId """, ) fun findAssigneeById( projectId: Long, - taskId: Long, + taskNumber: Long, userId: Long, ): List @@ -318,20 +318,33 @@ interface TaskRepository : JpaRepository { @Query( """ - select distinct t.id + select distinct t.number from Task t join t.keys tt - join Task at on (at.id = :taskId and at.project.id = :projectId) + join Task at on (at.number = :taskNumber and at.project.id = :projectId) join at.keys att join Key k on (element(att).key.id = k.id and element(tt).key.id = k.id) - where (t.id > :taskId or t.type != at.type) + where (t.number > :taskNumber or t.type != at.type) and t.language = at.language and t.type >= at.type and t.state = 'IN_PROGRESS' """, ) - fun getBlockingTaskIds( + fun getBlockingTaskNumbers( projectId: Long, - taskId: Long, + taskNumber: Long, ): List + + + @Query( + """ + from Task t + where t.number = :taskNumber + and t.project.id = :projectId + """, + ) + fun findByNumber( + projectId: Long, + taskNumber: Long + ): Optional } 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 5146e2a926..7e7efb63b4 100644 --- a/backend/data/src/main/kotlin/io/tolgee/service/TaskService.kt +++ b/backend/data/src/main/kotlin/io/tolgee/service/TaskService.kt @@ -12,7 +12,6 @@ import io.tolgee.model.enums.TaskState import io.tolgee.model.enums.TaskType import io.tolgee.model.key.Key import io.tolgee.model.task.Task -import io.tolgee.model.task.TaskId import io.tolgee.model.task.TaskKey import io.tolgee.model.task.TaskKeyId import io.tolgee.model.views.* @@ -117,8 +116,8 @@ class TaskService( filters: TranslationScopeFilters, ): Task { // Find the maximum ID for the given project - val lastTask = taskRepository.findByProjectOrderByIdDesc(project).firstOrNull() - val newId = (lastTask?.id ?: 0L) + 1 + val lastTask = taskRepository.findByProjectOrderByNumberDesc(project).firstOrNull() + val newNumber = (lastTask?.number ?: 0L) + 1 val language = checkLanguage(dto.languageId!!, project) val assignees = checkAssignees(dto.assignees ?: mutableSetOf(), project) @@ -133,7 +132,7 @@ class TaskService( val task = Task() - task.id = newId + task.number = newNumber task.project = project task.name = dto.name task.type = dto.type @@ -156,11 +155,11 @@ class TaskService( @Transactional fun updateTask( projectEntity: Project, - taskId: Long, + taskNumber: Long, dto: UpdateTaskRequest, ): TaskWithScopeView { val task = - taskRepository.findById(TaskId(projectEntity, taskId)).or { + taskRepository.findByNumber(projectEntity.id, taskNumber).or { throw NotFoundException(Message.TASK_NOT_FOUND) }.get() @@ -199,33 +198,35 @@ class TaskService( @Transactional fun deleteTask( projectEntity: Project, - taskId: Long, + taskNumber: Long, ) { - val taskComposedId = TaskId(projectEntity, taskId) - taskKeyRepository.deleteByTask(entityManager.getReference(Task::class.java, taskComposedId)) - taskRepository.deleteById(taskComposedId) + val task = taskRepository.findByNumber(projectEntity.id, taskNumber).or { + throw NotFoundException(Message.TASK_NOT_FOUND) + }.get() + taskKeyRepository.deleteByTask(task) + taskRepository.delete(task) } @Transactional fun getTask( projectEntity: Project, - taskId: Long, + taskNumber: Long, ): TaskWithScopeView { - val taskComposedId = TaskId(projectEntity, taskId) - val task = taskRepository.getReferenceById(taskComposedId) + val task = taskRepository.findByNumber(projectEntity.id, taskNumber).or { + throw NotFoundException(Message.TASK_NOT_FOUND) + }.get() return getTasksWithScope(listOf(task)).first() } @Transactional fun updateTaskKeys( projectEntity: Project, - taskId: Long, + taskNumber: Long, dto: UpdateTaskKeysRequest, ) { - val task = - taskRepository.findById(TaskId(projectEntity, taskId)).or { - throw NotFoundException(Message.TASK_NOT_FOUND) - }.get() + val task = taskRepository.findByNumber(projectEntity.id, taskNumber).or { + throw NotFoundException(Message.TASK_NOT_FOUND) + }.get() dto.removeKeys?.let { toRemove -> val taskKeysToRemove = @@ -253,14 +254,18 @@ class TaskService( @Transactional fun updateTaskKey( projectEntity: Project, - taskId: Long, + taskNumber: Long, keyId: Long, dto: UpdateTaskKeyRequest, ): UpdateTaskKeyResponse { + val task = taskRepository.findByNumber(projectEntity.id, taskNumber).or { + throw NotFoundException(Message.TASK_NOT_FOUND) + }.get() + val taskKey = taskKeyRepository.findById( TaskKeyId( - task = entityManager.getReference(Task::class.java, TaskId(projectEntity, taskId)), + task = task, key = entityManager.getReference(Key::class.java, keyId), ), ).or { @@ -282,7 +287,7 @@ class TaskService( taskKeyRepository.saveAndFlush(taskKey) if (!previousValue && taskKey.done) { - val taskItem = getTask(projectEntity, taskId) + val taskItem = getTask(projectEntity, taskNumber) return UpdateTaskKeyResponse( done = taskKey.done, taskFinished = taskItem.doneItems == taskItem.totalItems, @@ -297,10 +302,10 @@ class TaskService( fun findAssigneeById( projectId: Long, - taskId: Long, + taskNumber: Long, userId: Long, ): List { - return taskRepository.findAssigneeById(projectId, taskId, userId) + return taskRepository.findAssigneeById(projectId, taskNumber, userId) } fun findAssigneeByKey( @@ -337,16 +342,16 @@ class TaskService( @Transactional fun getTaskKeys( projectEntity: Project, - taskId: Long, + taskNumber: Long, ): List { - return taskRepository.getTaskKeys(projectEntity.id, taskId) + return taskRepository.getTaskKeys(projectEntity.id, taskNumber) } fun getBlockingTasks( projectEntity: Project, - taskId: Long, + taskNumber: Long, ): List { - return taskRepository.getBlockingTaskIds(projectEntity.id, taskId) + return taskRepository.getBlockingTaskNumbers(projectEntity.id, taskNumber) } fun getKeysWithTasks( @@ -365,11 +370,11 @@ class TaskService( fun getReport( projectEntity: Project, - taskId: Long, + taskNumber: Long, ): List { return taskRepository.perUserReport( projectEntity.id, - taskId, + taskNumber, projectEntity.baseLanguage!!.id, ) } @@ -418,10 +423,10 @@ class TaskService( private fun getTasksWithScope(tasks: Collection): List { val scopes = taskRepository.getTasksScopes(tasks) return tasks.map { task -> - val scope = scopes.find { it.taskId == task.id && it.projectId == task.project.id }!! + val scope = scopes.find { it.taskId == task.id }!! TaskWithScopeView( project = task.project, - id = task.id, + number = task.number, name = task.name, description = task.description, type = task.type, @@ -443,10 +448,10 @@ class TaskService( fun getExcelFile( projectEntity: Project, - taskId: Long, + taskNumber: Long, ): ByteArray { - val task = getTask(projectEntity, taskId) - val report = getReport(projectEntity, taskId) + val task = getTask(projectEntity, taskNumber) + val report = getReport(projectEntity, taskNumber) val workbook = TaskReportHelper(task, report).generateExcelReport() 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 42391f4da3..655dc06c79 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 @@ -144,13 +144,13 @@ class QueryGlobalFiltering( } private fun filterTask() { - if (params.filterTaskId != null) { + if (params.filterTaskNumber != null) { val translationTaskJoin = queryBase.root .join(Key_.tasks, JoinType.LEFT) .join(TaskKey_.task, JoinType.LEFT) - queryBase.whereConditions.add(translationTaskJoin.get(Task_.id).`in`(params.filterTaskId)) + queryBase.whereConditions.add(translationTaskJoin.get(Task_.id).`in`(params.filterTaskNumber)) } } diff --git a/backend/data/src/main/kotlin/io/tolgee/service/security/SecurityService.kt b/backend/data/src/main/kotlin/io/tolgee/service/security/SecurityService.kt index 63dc146ee1..91ed12e2eb 100644 --- a/backend/data/src/main/kotlin/io/tolgee/service/security/SecurityService.kt +++ b/backend/data/src/main/kotlin/io/tolgee/service/security/SecurityService.kt @@ -86,12 +86,12 @@ class SecurityService( fun hasTaskEditScopeOrIsAssigned( projectId: Long, - taskId: Long, + taskNumber: Long, ) { try { checkProjectPermission(projectId, Scope.TASKS_EDIT) } catch (err: PermissionException) { - val assignees = taskService.findAssigneeById(projectId, taskId, activeUser.id) + val assignees = taskService.findAssigneeById(projectId, taskNumber, activeUser.id) if (assignees.isEmpty() || assignees[0].id != activeUser.id) { throw err } @@ -100,12 +100,12 @@ class SecurityService( fun hasTaskViewScopeOrIsAssigned( projectId: Long, - taskId: Long, + taskNumber: Long, ) { try { checkProjectPermission(projectId, Scope.TASKS_VIEW) } catch (err: PermissionException) { - val assignees = taskService.findAssigneeById(projectId, taskId, activeUser.id) + val assignees = taskService.findAssigneeById(projectId, taskNumber, activeUser.id) if (assignees.isEmpty() || assignees[0].id != activeUser.id) { throw err } diff --git a/backend/data/src/main/resources/db/changelog/schema.xml b/backend/data/src/main/resources/db/changelog/schema.xml index f32dc8c21e..ddd746d3be 100644 --- a/backend/data/src/main/resources/db/changelog/schema.xml +++ b/backend/data/src/main/resources/db/changelog/schema.xml @@ -3440,118 +3440,80 @@ - + + + + + + + - + + + - - - + - + - - - - - + + - - + + - - - - + - - - - + - + - - - - + - - + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - + - - + + + + + diff --git a/webapp/src/component/task/TaskDetail.tsx b/webapp/src/component/task/TaskDetail.tsx index ab4dbec934..f4e6bea187 100644 --- a/webapp/src/component/task/TaskDetail.tsx +++ b/webapp/src/component/task/TaskDetail.tsx @@ -74,19 +74,19 @@ export const TaskDetail = ({ task, onClose, projectId }: Props) => { const formatDate = useDateFormatter(); const taskLoadable = useApiQuery({ - url: '/v2/projects/{projectId}/tasks/{taskId}', + url: '/v2/projects/{projectId}/tasks/{taskNumber}', method: 'get', - path: { projectId, taskId: task.id }, + path: { projectId, taskNumber: task.number }, }); const perUserReportLoadable = useApiQuery({ - url: '/v2/projects/{projectId}/tasks/{taskId}/per-user-report', + url: '/v2/projects/{projectId}/tasks/{taskNumber}/per-user-report', method: 'get', - path: { projectId, taskId: task.id }, + path: { projectId, taskNumber: task.number }, }); const updateLoadable = useApiMutation({ - url: '/v2/projects/{projectId}/tasks/{taskId}', + url: '/v2/projects/{projectId}/tasks/{taskNumber}', method: 'put', invalidatePrefix: ['/v2/projects/{projectId}/tasks', '/v2/user-tasks'], }); @@ -128,7 +128,7 @@ export const TaskDetail = ({ task, onClose, projectId }: Props) => { onSubmit={(values, actions) => { updateLoadable.mutate( { - path: { projectId, taskId: task.id }, + path: { projectId, taskNumber: task.number }, content: { 'application/json': { name: values.name, diff --git a/webapp/src/component/task/TaskId.tsx b/webapp/src/component/task/TaskId.tsx index f5589463bb..2657622aae 100644 --- a/webapp/src/component/task/TaskId.tsx +++ b/webapp/src/component/task/TaskId.tsx @@ -12,6 +12,6 @@ type Props = { className?: string; }; -export const TaskId = ({ children, sx, className }: Props) => { +export const TaskNumber = ({ children, sx, className }: Props) => { return #{children}; }; diff --git a/webapp/src/component/task/TaskLabel.tsx b/webapp/src/component/task/TaskLabel.tsx index d46ebfb07e..1169ce19c3 100644 --- a/webapp/src/component/task/TaskLabel.tsx +++ b/webapp/src/component/task/TaskLabel.tsx @@ -1,7 +1,7 @@ import { Box, styled, SxProps, Tooltip } from '@mui/material'; import { FlagImage } from 'tg.component/languages/FlagImage'; import { components } from 'tg.service/apiSchema.generated'; -import { TaskId } from './TaskId'; +import { TaskNumber } from './TaskId'; import { TaskTypeChip } from './TaskTypeChip'; type TaskModel = components['schemas']['TaskModel']; @@ -39,7 +39,7 @@ export const TaskLabel = ({ task, sx, className }: Props) => { {task.name} - {task.id} + {task.number} ); diff --git a/webapp/src/component/task/TaskMenu.tsx b/webapp/src/component/task/TaskMenu.tsx index 659b9aab99..c453b7e0ca 100644 --- a/webapp/src/component/task/TaskMenu.tsx +++ b/webapp/src/component/task/TaskMenu.tsx @@ -34,13 +34,13 @@ export const TaskMenu = ({ const isOpen = Boolean(anchorEl); const [taskCreate, setTaskCreate] = useState>(); const updateMutation = useApiMutation({ - url: '/v2/projects/{projectId}/tasks/{taskId}', + url: '/v2/projects/{projectId}/tasks/{taskNumber}', method: 'put', invalidatePrefix: ['/v2/projects/{projectId}/tasks', '/v2/user-tasks'], }); const finishMutation = useApiMutation({ - url: '/v2/projects/{projectId}/tasks/{taskId}/finish', + url: '/v2/projects/{projectId}/tasks/{taskNumber}/finish', method: 'post', invalidatePrefix: ['/v2/projects/{projectId}/tasks', '/v2/user-tasks'], }); @@ -82,7 +82,7 @@ export const TaskMenu = ({ }); const taskKeysMutation = useApiMutation({ - url: '/v2/projects/{projectId}/tasks/{taskId}/keys', + url: '/v2/projects/{projectId}/tasks/{taskNumber}/keys', method: 'get', }); @@ -93,7 +93,7 @@ export const TaskMenu = ({ onClose(); updateMutation.mutate( { - path: { projectId: project.id, taskId: task.id }, + path: { projectId: project.id, taskNumber: task.number }, content: { 'application/json': { state: 'CLOSED' } }, }, { @@ -109,7 +109,7 @@ export const TaskMenu = ({ function handleChangeState(state: TaskModel['state']) { updateMutation.mutate( { - path: { projectId: project.id, taskId: task.id }, + path: { projectId: project.id, taskNumber: task.number }, content: { 'application/json': { state } }, }, { @@ -126,7 +126,7 @@ export const TaskMenu = ({ function handleMarkAsDone() { finishMutation.mutate( { - path: { projectId: project.id, taskId: task.id }, + path: { projectId: project.id, taskNumber: task.number }, }, { onSuccess() { @@ -147,7 +147,7 @@ export const TaskMenu = ({ function handleCloneTask() { taskKeysMutation.mutate( { - path: { projectId: project.id, taskId: task.id }, + path: { projectId: project.id, taskNumber: task.number }, }, { onSuccess(data) { @@ -166,7 +166,7 @@ export const TaskMenu = ({ function handleCreateReviewTask() { taskKeysMutation.mutate( { - path: { projectId: project.id, taskId: task.id }, + path: { projectId: project.id, taskNumber: task.number }, }, { onSuccess(data) { diff --git a/webapp/src/component/task/TaskTooltip.tsx b/webapp/src/component/task/TaskTooltip.tsx index 7817f90ed0..7c34f15875 100644 --- a/webapp/src/component/task/TaskTooltip.tsx +++ b/webapp/src/component/task/TaskTooltip.tsx @@ -14,14 +14,14 @@ type SimpleProjectModel = components['schemas']['SimpleProjectModel']; type Action = 'open' | 'detail'; type Props = { - taskId: number; + taskNumber: number; project: SimpleProjectModel; children: React.ReactElement; actions?: Action[] | React.ReactNode | ((task: TaskModel) => React.ReactNode); } & Omit, 'title'>; export const TaskTooltip = ({ - taskId, + taskNumber, project, children, actions = ['open', 'detail'], @@ -56,7 +56,7 @@ export const TaskTooltip = ({ {...tooltipProps} title={ diff --git a/webapp/src/component/task/TaskTooltipContent.tsx b/webapp/src/component/task/TaskTooltipContent.tsx index 8a3ee339ad..827dc07434 100644 --- a/webapp/src/component/task/TaskTooltipContent.tsx +++ b/webapp/src/component/task/TaskTooltipContent.tsx @@ -12,18 +12,22 @@ import { TaskLabel } from './TaskLabel'; type TaskModel = components['schemas']['TaskModel']; type Props = { - taskId: number; + taskNumber: number; projectId: number; actions?: React.ReactNode | ((task: TaskModel) => React.ReactNode); }; -export const TaskTooltipContent = ({ projectId, taskId, actions }: Props) => { +export const TaskTooltipContent = ({ + projectId, + taskNumber, + actions, +}: Props) => { const task = useApiQuery({ - url: '/v2/projects/{projectId}/tasks/{taskId}', + url: '/v2/projects/{projectId}/tasks/{taskNumber}', method: 'get', path: { projectId, - taskId, + taskNumber, }, fetchOptions: { disableAuthRedirect: true, diff --git a/webapp/src/component/task/taskSelect/TaskSearchSelectPopover.tsx b/webapp/src/component/task/taskSelect/TaskSearchSelectPopover.tsx index 10857dfae5..874df7ec3a 100644 --- a/webapp/src/component/task/taskSelect/TaskSearchSelectPopover.tsx +++ b/webapp/src/component/task/taskSelect/TaskSearchSelectPopover.tsx @@ -166,7 +166,7 @@ export const TaskSearchSelectPopover: React.FC = ({ clearOnEscape={false} noOptionsText={t('global_nothing_found')} loadingText={t('global_loading_text')} - isOptionEqualToValue={(o, v) => o.id === v.id} + isOptionEqualToValue={(o, v) => o.number === v.number} onInputChange={(_, value, reason) => reason === 'input' && setInputValue(value) } @@ -174,19 +174,19 @@ export const TaskSearchSelectPopover: React.FC = ({ PopperComponent={PopperComponent} PaperComponent={PaperComponent} renderOption={(props, option) => ( - + { onSelect(option); }} - selected={option.id === selected?.id} + selected={option.number === selected?.number} data-cy="task-select-item" > {usersLoadable.hasNextPage && - option.id === items![items!.length - 1].id && ( + option.number === items![items!.length - 1].number && (