From 6df1c7b3d61b61d4ed04a6f1ae3f6f46d26b1e57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Gran=C3=A1t?= Date: Wed, 16 Oct 2024 14:48:56 +0200 Subject: [PATCH] feat: order professional translation --- .github/workflows/prerelease-alpha.yml | 6 +- .github/workflows/preview.yml | 2 +- .github/workflows/release.yml | 9 +- .github/workflows/reportIntermittentTests.yml | 17 +- .github/workflows/test.yml | 15 +- .run/Backend localhost.run.xml | 4 +- .../v2/controllers/V2InvitationController.kt | 41 +- .../controllers/project/ProjectsController.kt | 10 +- .../hateoas/TranslationAgencySimpleModel.kt | 11 + .../TranslationAgencySimpleModelAssembler.kt | 20 + .../invitation/ProjectInvitationModel.kt | 6 +- .../ProjectInvitationModelAssembler.kt | 14 +- .../permission/PermissionWithAgencyModel.kt | 16 + .../PermissionWithAgencyModelAssembler.kt | 24 + .../io/tolgee/hateoas/task/TaskModel.kt | 2 + .../TranslationAgencyModel.kt | 18 + .../TranslationAgencyPublicModel.kt | 15 + .../userAccount/UserAccountInProjectModel.kt | 3 +- .../UserAccountInProjectModelAssembler.kt | 4 +- .../io/tolgee/activity/data/ActivityType.kt | 1 + .../component/email/InvitationEmailSender.kt | 2 +- .../component/email/TolgeeEmailSender.kt | 6 + .../EnabledFeaturesProvider.kt | 9 + .../kotlin/io/tolgee/constants/Feature.kt | 1 + .../kotlin/io/tolgee/constants/Message.kt | 1 + .../dtos/misc/CreateInvitationParams.kt | 1 + .../CreateOrganizationInvitationParams.kt | 1 + .../misc/CreateProjectInvitationParams.kt | 1 + .../kotlin/io/tolgee/dtos/misc/EmailParams.kt | 2 + .../request/project/ProjectInviteUserDto.kt | 4 + .../request/userAccount/UserAccountFilters.kt | 5 + .../main/kotlin/io/tolgee/model/Permission.kt | 6 + .../model/enums/ProjectPermissionType.kt | 4 + .../main/kotlin/io/tolgee/model/task/Task.kt | 5 + .../translationAgency/TranslationAgency.kt | 41 + .../tolgee/model/views/TaskWithScopeView.kt | 2 + .../tolgee/repository/PermissionRepository.kt | 8 + .../repository/UserAccountRepository.kt | 4 + .../kotlin/io/tolgee/service/AvatarService.kt | 12 +- .../kotlin/io/tolgee/service/ITaskService.kt | 2 + .../service/PublicTranslationAgencyService.kt | 14 + .../service/TranslationAgencyService.kt | 7 + .../organization/OrganizationService.kt | 3 +- .../service/security/PermissionService.kt | 35 + .../main/resources/db/changelog/schema.xml | 3115 +++++++++-------- e2e/cypress/common/apiCalls/common.ts | 21 +- e2e/cypress/e2e/projects/projectMembers.cy.ts | 12 +- e2e/cypress/support/dataCyType.d.ts | 18 + e2e/cypress/tsconfig.json | 3 +- e2e/package-lock.json | 10 +- e2e/package.json | 2 +- .../ee/api/v2/controllers/TaskController.kt | 19 +- .../hateoas/assemblers/TaskModelAssembler.kt | 3 + .../task/CreateTranslationOrderRequest.kt | 7 + .../io/tolgee/ee/data/task/TaskFilters.kt | 5 + .../CreateTranslationAgencyRequest.kt | 18 + .../UpdateTranslationAgencyRequest.kt | 18 + .../io/tolgee/ee/repository/TaskRepository.kt | 25 + .../ee/service/AssigneeNotificationService.kt | 15 +- .../tolgee/ee/service/EePermissionService.kt | 7 + .../io/tolgee/ee/service/TaskService.kt | 22 +- .../task/TaskControllerPermissionsTest.kt | 24 - .../PermissionsSettings/PermissionsBasic.tsx | 3 + .../PermissionsSettings/PermissionsRole.tsx | 22 +- .../PermissionsSettings.tsx | 5 +- .../PermissionsSettings/RoleLanguages.tsx | 3 + webapp/src/component/UserAccount.tsx | 2 + .../src/component/activity/configuration.tsx | 8 + .../component/common/avatar/AvatarEdit.tsx | 15 +- .../common/avatar/AvatarEditDialog.tsx | 9 +- .../component/common/avatar/ProfileAvatar.tsx | 10 +- .../src/constants/GlobalValidationSchema.tsx | 13 +- webapp/src/constants/links.tsx | 16 + .../AdministrationCloudPlanCreateView.tsx | 0 .../AdministrationCloudPlanEditView.tsx | 0 .../AdministrationCloudPlansView.tsx | 0 .../AdministrationEeLicenseView.tsx | 6 +- .../AdministrationEePlanCreateView.tsx | 0 .../AdministrationEePlanEditView.tsx | 0 .../AdministrationEePlansView.tsx | 0 .../components/AssignSwitchCheckbox.tsx | 0 .../components/CloudPlanForm.tsx | 0 .../components/CloudPlanOrganizations.tsx | 0 .../components/EePlanForm.tsx | 0 .../components/EePlanOrganizations.tsx | 0 .../AdministrationEeTACreateView.tsx | 55 + .../AdministrationEeTAEditView.tsx | 98 + .../AdministrationEeTAView.tsx | 109 + .../translationAgencies/TAEditForm.tsx | 144 + .../translationAgencies/TAProfileAvatar.tsx | 90 + .../src/ee/orderTranslations/AgencyLabel.tsx | 26 + .../OrderTranslationsDialog.tsx | 407 +++ .../orderTranslations/ProviderDescription.tsx | 28 + .../orderTranslations/TranslationAgency.tsx | 94 + webapp/src/ee/task/components/TaskDetail.tsx | 7 +- webapp/src/ee/task/components/TaskLabel.tsx | 2 + webapp/src/ee/task/components/TasksBoard.tsx | 25 +- .../taskCreate/TaskCreateDialog.tsx | 190 +- .../components/taskCreate/TaskCreateForm.tsx | 218 ++ .../components/taskCreate/TaskPreview.tsx | 46 +- .../taskFilter/SubfilterAgencies.tsx | 80 + .../task/components/taskFilter/TaskFilter.tsx | 23 +- .../taskFilter/TaskFilterPopover.tsx | 16 + .../components/taskFilter/taskFilterUtils.ts | 3 +- .../components/tasksHeader/TasksHeaderBig.tsx | 20 +- .../tasksHeader/TasksHeaderCompact.tsx | 23 +- .../src/ee/task/views/myTasks/MyTasksList.tsx | 12 + .../src/ee/task/views/myTasks/MyTasksView.tsx | 60 +- .../{TasksList.tsx => ProjectTasksList.tsx} | 15 +- .../views/projectTasks/ProjectTasksView.tsx | 156 +- webapp/src/fixtures/isValidUrl.ts | 8 + webapp/src/service/apiSchema.generated.ts | 1318 +++++-- .../src/service/billingApiSchema.generated.ts | 970 ++++- webapp/src/translationTools/useFeatures.tsx | 1 + .../administration/AdministrationView.tsx | 26 +- .../components/BaseAdministrationView.tsx | 4 + .../organizations/members/InvitationItem.tsx | 20 +- .../organizations/members/InviteDialog.tsx | 8 +- .../projects/members/ProjectMembersView.tsx | 49 +- .../members/component/AgencyFilter.tsx | 89 + .../members/component/AgencySelect.tsx | 51 + .../members/component/InvitationItem.tsx | 32 +- .../members/component/InviteDialog.tsx | 128 +- .../projects/members/component/MemberItem.tsx | 8 + .../members/component/useCreateInvitation.tsx | 10 +- .../BatchOperations/BatchOperations.tsx | 3 + .../BatchOperations/BatchSelect.tsx | 7 + .../OperationOrderTranslation.tsx | 55 + .../translations/BatchOperations/types.ts | 1 + 129 files changed, 6231 insertions(+), 2374 deletions(-) create mode 100644 backend/api/src/main/kotlin/io/tolgee/hateoas/TranslationAgencySimpleModel.kt create mode 100644 backend/api/src/main/kotlin/io/tolgee/hateoas/TranslationAgencySimpleModelAssembler.kt create mode 100644 backend/api/src/main/kotlin/io/tolgee/hateoas/permission/PermissionWithAgencyModel.kt create mode 100644 backend/api/src/main/kotlin/io/tolgee/hateoas/permission/PermissionWithAgencyModelAssembler.kt create mode 100644 backend/api/src/main/kotlin/io/tolgee/hateoas/translationAgency/TranslationAgencyModel.kt create mode 100644 backend/api/src/main/kotlin/io/tolgee/hateoas/translationAgency/TranslationAgencyPublicModel.kt create mode 100644 backend/data/src/main/kotlin/io/tolgee/model/translationAgency/TranslationAgency.kt create mode 100644 backend/data/src/main/kotlin/io/tolgee/service/PublicTranslationAgencyService.kt create mode 100644 backend/data/src/main/kotlin/io/tolgee/service/TranslationAgencyService.kt create mode 100644 ee/backend/app/src/main/kotlin/io/tolgee/ee/data/task/CreateTranslationOrderRequest.kt create mode 100644 ee/backend/app/src/main/kotlin/io/tolgee/ee/data/translationAgency/CreateTranslationAgencyRequest.kt create mode 100644 ee/backend/app/src/main/kotlin/io/tolgee/ee/data/translationAgency/UpdateTranslationAgencyRequest.kt rename webapp/src/ee/billing/administration/{ => subscriptionPlans}/AdministrationCloudPlanCreateView.tsx (100%) rename webapp/src/ee/billing/administration/{ => subscriptionPlans}/AdministrationCloudPlanEditView.tsx (100%) rename webapp/src/ee/billing/administration/{ => subscriptionPlans}/AdministrationCloudPlansView.tsx (100%) rename webapp/src/ee/billing/administration/{ => subscriptionPlans}/AdministrationEeLicenseView.tsx (87%) rename webapp/src/ee/billing/administration/{ => subscriptionPlans}/AdministrationEePlanCreateView.tsx (100%) rename webapp/src/ee/billing/administration/{ => subscriptionPlans}/AdministrationEePlanEditView.tsx (100%) rename webapp/src/ee/billing/administration/{ => subscriptionPlans}/AdministrationEePlansView.tsx (100%) rename webapp/src/ee/billing/administration/{ => subscriptionPlans}/components/AssignSwitchCheckbox.tsx (100%) rename webapp/src/ee/billing/administration/{ => subscriptionPlans}/components/CloudPlanForm.tsx (100%) rename webapp/src/ee/billing/administration/{ => subscriptionPlans}/components/CloudPlanOrganizations.tsx (100%) rename webapp/src/ee/billing/administration/{ => subscriptionPlans}/components/EePlanForm.tsx (100%) rename webapp/src/ee/billing/administration/{ => subscriptionPlans}/components/EePlanOrganizations.tsx (100%) create mode 100644 webapp/src/ee/billing/administration/translationAgencies/AdministrationEeTACreateView.tsx create mode 100644 webapp/src/ee/billing/administration/translationAgencies/AdministrationEeTAEditView.tsx create mode 100644 webapp/src/ee/billing/administration/translationAgencies/AdministrationEeTAView.tsx create mode 100644 webapp/src/ee/billing/administration/translationAgencies/TAEditForm.tsx create mode 100644 webapp/src/ee/billing/administration/translationAgencies/TAProfileAvatar.tsx create mode 100644 webapp/src/ee/orderTranslations/AgencyLabel.tsx create mode 100644 webapp/src/ee/orderTranslations/OrderTranslationsDialog.tsx create mode 100644 webapp/src/ee/orderTranslations/ProviderDescription.tsx create mode 100644 webapp/src/ee/orderTranslations/TranslationAgency.tsx create mode 100644 webapp/src/ee/task/components/taskCreate/TaskCreateForm.tsx create mode 100644 webapp/src/ee/task/components/taskFilter/SubfilterAgencies.tsx rename webapp/src/ee/task/views/projectTasks/{TasksList.tsx => ProjectTasksList.tsx} (82%) create mode 100644 webapp/src/fixtures/isValidUrl.ts create mode 100644 webapp/src/views/projects/members/component/AgencyFilter.tsx create mode 100644 webapp/src/views/projects/members/component/AgencySelect.tsx create mode 100644 webapp/src/views/projects/translations/BatchOperations/OperationOrderTranslation.tsx diff --git a/.github/workflows/prerelease-alpha.yml b/.github/workflows/prerelease-alpha.yml index 4777b60b1a..24fdf17253 100644 --- a/.github/workflows/prerelease-alpha.yml +++ b/.github/workflows/prerelease-alpha.yml @@ -2,7 +2,7 @@ name: Release on: push: - branches: [ "tolgee-3" ] + branches: ["tolgee-3"] jobs: main: @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@v3 with: - token: '${{ secrets.TOLGEE_MACHINE_PAT }}' + token: "${{ secrets.TOLGEE_MACHINE_PAT }}" - uses: actions/setup-java@v3 with: @@ -20,7 +20,7 @@ jobs: - name: Setup node uses: actions/setup-node@v3 with: - node-version: "18.x" + node-version: "22.x" - name: Set git globals run: | diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 82e2ef6845..1583b72887 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -22,7 +22,7 @@ jobs: - name: Setup node uses: actions/setup-node@v3 with: - node-version: "18.x" + node-version: "22.x" - name: Set git globals run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 895d8c172b..a2d9f6f174 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,8 +2,8 @@ name: Release on: workflow_run: - workflows: [ "Test" ] - branches: [ "main", "next" ] + workflows: ["Test"] + branches: ["main", "next"] types: - completed @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/checkout@v3 with: - token: '${{ secrets.TOLGEE_MACHINE_PAT }}' + token: "${{ secrets.TOLGEE_MACHINE_PAT }}" - uses: actions/setup-java@v3 with: @@ -24,7 +24,7 @@ jobs: - name: Setup node uses: actions/setup-node@v3 with: - node-version: "18.x" + node-version: "22.x" - name: Set git globals run: | @@ -93,7 +93,6 @@ jobs: TOLGEE_API_KEY: ${{secrets.TOLGEE_API_KEY}} working-directory: ./webapp - - uses: actions/upload-artifact@v3 if: always() with: diff --git a/.github/workflows/reportIntermittentTests.yml b/.github/workflows/reportIntermittentTests.yml index f99852877f..178f048ea3 100644 --- a/.github/workflows/reportIntermittentTests.yml +++ b/.github/workflows/reportIntermittentTests.yml @@ -2,7 +2,7 @@ name: Report intermittent E2E Tests on: schedule: - - cron: '42 04 * * *' + - cron: "42 04 * * *" jobs: backend-build: @@ -19,7 +19,7 @@ jobs: - name: Setup node uses: actions/setup-node@v3 with: - node-version: "18.x" + node-version: "22.x" - name: Cache Gradle packages uses: actions/cache@v3 @@ -86,7 +86,7 @@ jobs: - name: Setup node uses: actions/setup-node@v3 with: - node-version: "18.x" + node-version: "22.x" - name: Cache Gradle packages uses: actions/cache@v3 @@ -133,7 +133,7 @@ jobs: ./**/build/reports/**/* e2e: - needs: [ frontend-build, backend-build, e2e-install-deps ] + needs: [frontend-build, backend-build, e2e-install-deps] runs-on: ubuntu-latest name: E2E testing โ€๐Ÿ”Ž strategy: @@ -152,7 +152,7 @@ jobs: - name: Setup node uses: actions/setup-node@v3 with: - node-version: "18.x" + node-version: "22.x" - name: Cache Gradle packages uses: actions/cache@v3 @@ -246,7 +246,7 @@ jobs: - name: Setup node uses: actions/setup-node@v3 with: - node-version: "18.x" + node-version: "22.x" - name: Cache node modules uses: actions/cache@v3 @@ -275,7 +275,7 @@ jobs: - name: Setup node uses: actions/setup-node@v3 with: - node-version: "18.x" + node-version: "22.x" - name: Cache node modules uses: actions/cache@v3 @@ -306,7 +306,7 @@ jobs: cleanup: if: always() - needs: [ e2e ] + needs: [e2e] name: Delete artifacts ๐Ÿงน runs-on: ubuntu-latest steps: @@ -316,4 +316,3 @@ jobs: e2e_deps webapp backend - diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e35a43cdcd..c8ddbb7eb7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,7 @@ jobs: - name: Setup node uses: actions/setup-node@v3 with: - node-version: "18.x" + node-version: "22.x" - name: Cache Gradle packages uses: actions/cache@v3 @@ -89,7 +89,7 @@ jobs: - name: Setup node uses: actions/setup-node@v3 with: - node-version: "18.x" + node-version: "22.x" - name: Cache Gradle packages uses: actions/cache@v3 @@ -149,7 +149,6 @@ jobs: path: "**/build/test-results/**/TEST-*.xml" reporter: java-junit - e2e: needs: [frontend-build, backend-build, e2e-install-deps] runs-on: ubuntu-latest @@ -170,7 +169,7 @@ jobs: - name: Setup node uses: actions/setup-node@v3 with: - node-version: "18.x" + node-version: "22.x" - name: Cache Gradle packages uses: actions/cache@v3 @@ -284,7 +283,7 @@ jobs: - name: Setup node uses: actions/setup-node@v3 with: - node-version: "18.x" + node-version: "22.x" - name: Cache node modules uses: actions/cache@v3 @@ -355,7 +354,7 @@ jobs: - name: Setup node uses: actions/setup-node@v3 with: - node-version: "18.x" + node-version: "22.x" - name: Cache node modules uses: actions/cache@v3 @@ -384,7 +383,7 @@ jobs: - name: Setup node uses: actions/setup-node@v3 with: - node-version: "18.x" + node-version: "22.x" - name: Cache node modules uses: actions/cache@v3 @@ -427,7 +426,7 @@ jobs: - name: Setup node uses: actions/setup-node@v3 with: - node-version: "18.x" + node-version: "22.x" - name: Cache Gradle packages uses: actions/cache@v3 diff --git a/.run/Backend localhost.run.xml b/.run/Backend localhost.run.xml index 3f94b63a76..d8e7961f17 100644 --- a/.run/Backend localhost.run.xml +++ b/.run/Backend localhost.run.xml @@ -2,10 +2,10 @@ - + \ No newline at end of file diff --git a/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/V2InvitationController.kt b/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/V2InvitationController.kt index e75478b519..22d1367fc0 100644 --- a/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/V2InvitationController.kt +++ b/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/V2InvitationController.kt @@ -19,13 +19,16 @@ import io.tolgee.model.enums.OrganizationRoleType import io.tolgee.model.enums.Scope import io.tolgee.security.ProjectHolder import io.tolgee.security.authentication.AllowApiAccess +import io.tolgee.security.authentication.AuthenticationFacade import io.tolgee.security.authentication.RequiresSuperAuthentication import io.tolgee.security.authorization.RequiresOrganizationRole import io.tolgee.security.authorization.RequiresProjectPermissions import io.tolgee.service.InvitationService +import io.tolgee.service.TranslationAgencyService import io.tolgee.service.organization.OrganizationRoleService import io.tolgee.service.organization.OrganizationService import io.tolgee.service.project.ProjectService +import io.tolgee.service.security.PermissionService import io.tolgee.service.security.SecurityService import jakarta.validation.Valid import org.springframework.hateoas.CollectionModel @@ -53,6 +56,9 @@ class V2InvitationController( private val eeInvitationService: EeInvitationService, private val organizationService: OrganizationService, private val organizationInvitationModelAssembler: OrganizationInvitationModelAssembler, + private val permissionService: PermissionService, + private val authenticationFacade: AuthenticationFacade, + private val translationAgencyService: TranslationAgencyService, ) { @GetMapping("/v2/invitations/{code}/accept") @Operation(summary = "Accepts invitation to project or organization") @@ -109,18 +115,37 @@ class V2InvitationController( invitation: ProjectInviteUserDto, ): ProjectInvitationModel { validatePermissions(invitation) + val currentUserPermissions = + permissionService.findPermissionNonCached( + projectHolder.project.id, + authenticationFacade.authenticatedUser.id, + ) val languagesPermissions = projectPermissionFacade.getLanguages(invitation, projectHolder.project.id) val params = - CreateProjectInvitationParams( - project = projectHolder.projectEntity, - type = invitation.type, - scopes = invitation.scopes, - email = invitation.email, - name = invitation.name, - languagePermissions = languagesPermissions, - ) + if (invitation.agencyId != null) { + val agency = translationAgencyService.findById(invitation.agencyId!!) + CreateProjectInvitationParams( + project = projectHolder.projectEntity, + type = invitation.type, + scopes = invitation.scopes, + email = agency.email, + name = "Agency invitation", + languagePermissions = languagesPermissions, + agencyId = agency.id, + ) + } else { + CreateProjectInvitationParams( + project = projectHolder.projectEntity, + type = invitation.type, + scopes = invitation.scopes, + email = invitation.email, + name = invitation.name, + languagePermissions = languagesPermissions, + agencyId = currentUserPermissions?.agency?.id, + ) + } val created = if (!params.scopes.isNullOrEmpty()) { diff --git a/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/project/ProjectsController.kt b/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/project/ProjectsController.kt index e2b93807d7..9aef9535f5 100644 --- a/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/project/ProjectsController.kt +++ b/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/project/ProjectsController.kt @@ -13,6 +13,7 @@ import io.tolgee.dtos.request.project.CreateProjectRequest import io.tolgee.dtos.request.project.EditProjectRequest import io.tolgee.dtos.request.project.ProjectFilters import io.tolgee.dtos.request.project.SetPermissionLanguageParams +import io.tolgee.dtos.request.task.UserAccountFilters import io.tolgee.exceptions.BadRequestException import io.tolgee.facade.ProjectPermissionFacade import io.tolgee.facade.ProjectWithStatsFacade @@ -178,8 +179,15 @@ class ProjectsController( @PathVariable("projectId") projectId: Long, @ParameterObject pageable: Pageable, @RequestParam("search", required = false) search: String?, + @ParameterObject filters: UserAccountFilters = UserAccountFilters(), ): PagedModel { - return userAccountService.getAllInProjectWithPermittedLanguages(projectId, pageable, search).let { users -> + return userAccountService.getAllInProjectWithPermittedLanguages( + projectId, + pageable, + search, + filters = filters, + ).let { + users -> userArrayResourcesAssembler.toModel(users, userAccountInProjectModelAssembler) } } diff --git a/backend/api/src/main/kotlin/io/tolgee/hateoas/TranslationAgencySimpleModel.kt b/backend/api/src/main/kotlin/io/tolgee/hateoas/TranslationAgencySimpleModel.kt new file mode 100644 index 0000000000..103e7a6e9c --- /dev/null +++ b/backend/api/src/main/kotlin/io/tolgee/hateoas/TranslationAgencySimpleModel.kt @@ -0,0 +1,11 @@ +package io.tolgee.hateoas + +import io.tolgee.dtos.Avatar +import org.springframework.hateoas.RepresentationModel + +data class TranslationAgencySimpleModel( + var id: Long = 0L, + var name: String = "", + var url: String? = "", + val avatar: Avatar?, +) : RepresentationModel() diff --git a/backend/api/src/main/kotlin/io/tolgee/hateoas/TranslationAgencySimpleModelAssembler.kt b/backend/api/src/main/kotlin/io/tolgee/hateoas/TranslationAgencySimpleModelAssembler.kt new file mode 100644 index 0000000000..7349a4f51a --- /dev/null +++ b/backend/api/src/main/kotlin/io/tolgee/hateoas/TranslationAgencySimpleModelAssembler.kt @@ -0,0 +1,20 @@ +package io.tolgee.hateoas + +import io.tolgee.model.translationAgency.TranslationAgency +import io.tolgee.service.AvatarService +import org.springframework.hateoas.server.RepresentationModelAssembler +import org.springframework.stereotype.Component + +@Component +class TranslationAgencySimpleModelAssembler( + private val avatarService: AvatarService, +) : RepresentationModelAssembler { + override fun toModel(entity: TranslationAgency): TranslationAgencySimpleModel { + return TranslationAgencySimpleModel( + id = entity.id, + name = entity.name, + url = entity.url, + avatar = avatarService.getAvatarLinks(entity.avatarHash), + ) + } +} diff --git a/backend/api/src/main/kotlin/io/tolgee/hateoas/invitation/ProjectInvitationModel.kt b/backend/api/src/main/kotlin/io/tolgee/hateoas/invitation/ProjectInvitationModel.kt index 54ac017282..f339fb1438 100644 --- a/backend/api/src/main/kotlin/io/tolgee/hateoas/invitation/ProjectInvitationModel.kt +++ b/backend/api/src/main/kotlin/io/tolgee/hateoas/invitation/ProjectInvitationModel.kt @@ -1,6 +1,6 @@ package io.tolgee.hateoas.invitation -import io.tolgee.hateoas.permission.PermissionModel +import io.tolgee.hateoas.permission.PermissionWithAgencyModel import io.tolgee.model.enums.ProjectPermissionType import org.springframework.hateoas.RepresentationModel import org.springframework.hateoas.server.core.Relation @@ -9,7 +9,7 @@ import java.util.* @Relation(collectionRelation = "invitations", itemRelation = "invitation") open class ProjectInvitationModel( val id: Long, - val code: String, + val code: String?, @Deprecated("Use permission object instead") val type: ProjectPermissionType?, @Deprecated("Use permission object instead") @@ -17,5 +17,5 @@ open class ProjectInvitationModel( val createdAt: Date, val invitedUserName: String?, val invitedUserEmail: String?, - val permission: PermissionModel, + val permission: PermissionWithAgencyModel, ) : RepresentationModel() diff --git a/backend/api/src/main/kotlin/io/tolgee/hateoas/invitation/ProjectInvitationModelAssembler.kt b/backend/api/src/main/kotlin/io/tolgee/hateoas/invitation/ProjectInvitationModelAssembler.kt index 5c21759753..179886ccdc 100644 --- a/backend/api/src/main/kotlin/io/tolgee/hateoas/invitation/ProjectInvitationModelAssembler.kt +++ b/backend/api/src/main/kotlin/io/tolgee/hateoas/invitation/ProjectInvitationModelAssembler.kt @@ -1,7 +1,7 @@ package io.tolgee.hateoas.invitation import io.tolgee.api.v2.controllers.V2InvitationController -import io.tolgee.hateoas.permission.PermissionModelAssembler +import io.tolgee.hateoas.permission.PermissionWithAgencyModelAssembler import io.tolgee.model.Invitation import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport import org.springframework.hateoas.server.mvc.linkTo @@ -9,22 +9,28 @@ import org.springframework.stereotype.Component @Component class ProjectInvitationModelAssembler( - private val permissionModelAssembler: PermissionModelAssembler, + private val permissionWithAgencyModelAssembler: PermissionWithAgencyModelAssembler, ) : RepresentationModelAssemblerSupport( V2InvitationController::class.java, ProjectInvitationModel::class.java, ) { override fun toModel(entity: Invitation): ProjectInvitationModel { + val code = + if (entity.permission?.agency == null) { + entity.code + } else { + null + } return ProjectInvitationModel( id = entity.id!!, - code = entity.code, + code = code, type = entity.permission!!.type, permittedLanguageIds = entity.permission!!.translateLanguages.map { it.id }, createdAt = entity.createdAt!!, invitedUserName = entity.name, invitedUserEmail = entity.email, - permission = permissionModelAssembler.toModel(entity.permission!!), + permission = permissionWithAgencyModelAssembler.toModel(entity.permission!!), ) .add(linkTo { acceptInvitation(entity.code) }.withRel("accept")) .add(linkTo { deleteInvitation(entity.id!!) }.withRel("delete")) diff --git a/backend/api/src/main/kotlin/io/tolgee/hateoas/permission/PermissionWithAgencyModel.kt b/backend/api/src/main/kotlin/io/tolgee/hateoas/permission/PermissionWithAgencyModel.kt new file mode 100644 index 0000000000..fc949b41ff --- /dev/null +++ b/backend/api/src/main/kotlin/io/tolgee/hateoas/permission/PermissionWithAgencyModel.kt @@ -0,0 +1,16 @@ +package io.tolgee.hateoas.permission + +import io.tolgee.hateoas.TranslationAgencySimpleModel +import io.tolgee.model.enums.ProjectPermissionType +import io.tolgee.model.enums.Scope +import org.springframework.hateoas.RepresentationModel + +class PermissionWithAgencyModel( + override val scopes: Array, + override val type: ProjectPermissionType?, + override val permittedLanguageIds: Collection?, + override val translateLanguageIds: Collection?, + override val viewLanguageIds: Collection?, + override val stateChangeLanguageIds: Collection?, + val agency: TranslationAgencySimpleModel?, +) : RepresentationModel(), IDeprecatedPermissionModel diff --git a/backend/api/src/main/kotlin/io/tolgee/hateoas/permission/PermissionWithAgencyModelAssembler.kt b/backend/api/src/main/kotlin/io/tolgee/hateoas/permission/PermissionWithAgencyModelAssembler.kt new file mode 100644 index 0000000000..7fa7b57254 --- /dev/null +++ b/backend/api/src/main/kotlin/io/tolgee/hateoas/permission/PermissionWithAgencyModelAssembler.kt @@ -0,0 +1,24 @@ +package io.tolgee.hateoas.permission + +import io.tolgee.hateoas.TranslationAgencySimpleModelAssembler +import io.tolgee.model.Permission +import io.tolgee.model.enums.Scope +import org.springframework.hateoas.server.RepresentationModelAssembler +import org.springframework.stereotype.Component + +@Component +class PermissionWithAgencyModelAssembler( + private val translationAgencySimpleModelAssembler: TranslationAgencySimpleModelAssembler, +) : RepresentationModelAssembler { + override fun toModel(entity: Permission): PermissionWithAgencyModel { + return PermissionWithAgencyModel( + scopes = Scope.expand(entity.scopes), + permittedLanguageIds = entity.translateLanguageIds, + translateLanguageIds = entity.translateLanguageIds, + stateChangeLanguageIds = entity.stateChangeLanguageIds, + viewLanguageIds = entity.viewLanguageIds, + type = entity.type, + agency = entity.agency?.let { translationAgencySimpleModelAssembler.toModel(it) }, + ) + } +} 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 d971a271e9..84d5456f32 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 @@ -1,5 +1,6 @@ package io.tolgee.hateoas.task +import io.tolgee.hateoas.TranslationAgencySimpleModel import io.tolgee.hateoas.language.LanguageModel import io.tolgee.hateoas.userAccount.SimpleUserAccountModel import io.tolgee.model.enums.TaskState @@ -24,4 +25,5 @@ class TaskModel( var createdAt: Long? = 0, var closedAt: Long? = null, var state: TaskState = TaskState.IN_PROGRESS, + var agency: TranslationAgencySimpleModel? = null, ) : RepresentationModel() diff --git a/backend/api/src/main/kotlin/io/tolgee/hateoas/translationAgency/TranslationAgencyModel.kt b/backend/api/src/main/kotlin/io/tolgee/hateoas/translationAgency/TranslationAgencyModel.kt new file mode 100644 index 0000000000..71b5246864 --- /dev/null +++ b/backend/api/src/main/kotlin/io/tolgee/hateoas/translationAgency/TranslationAgencyModel.kt @@ -0,0 +1,18 @@ +package io.tolgee.hateoas.translationAgency + +import io.tolgee.dtos.Avatar +import io.tolgee.hateoas.task.TaskModel +import org.springframework.hateoas.RepresentationModel +import org.springframework.hateoas.server.core.Relation + +@Relation(collectionRelation = "translationAgencies", itemRelation = "translationAgency") +class TranslationAgencyModel( + var id: Long = 0L, + var name: String = "", + var description: String? = "", + var services: List = listOf(), + var url: String? = "", + val avatar: Avatar?, + val email: String? = "", + val emailBcc: List = listOf(), +) : RepresentationModel() diff --git a/backend/api/src/main/kotlin/io/tolgee/hateoas/translationAgency/TranslationAgencyPublicModel.kt b/backend/api/src/main/kotlin/io/tolgee/hateoas/translationAgency/TranslationAgencyPublicModel.kt new file mode 100644 index 0000000000..2a8c12eb0d --- /dev/null +++ b/backend/api/src/main/kotlin/io/tolgee/hateoas/translationAgency/TranslationAgencyPublicModel.kt @@ -0,0 +1,15 @@ +package io.tolgee.hateoas.translationAgency + +import io.tolgee.dtos.Avatar +import org.springframework.hateoas.RepresentationModel +import org.springframework.hateoas.server.core.Relation + +@Relation(collectionRelation = "translationAgencies", itemRelation = "translationAgency") +class TranslationAgencyPublicModel ( + var id: Long = 0L, + var name: String = "", + var description: String? = "", + var services: List = listOf(), + var url: String? = "", + val avatar: Avatar? +) : RepresentationModel() diff --git a/backend/api/src/main/kotlin/io/tolgee/hateoas/userAccount/UserAccountInProjectModel.kt b/backend/api/src/main/kotlin/io/tolgee/hateoas/userAccount/UserAccountInProjectModel.kt index 53b3169e97..fc8a4e9d05 100644 --- a/backend/api/src/main/kotlin/io/tolgee/hateoas/userAccount/UserAccountInProjectModel.kt +++ b/backend/api/src/main/kotlin/io/tolgee/hateoas/userAccount/UserAccountInProjectModel.kt @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema import io.tolgee.dtos.Avatar import io.tolgee.hateoas.permission.ComputedPermissionModel import io.tolgee.hateoas.permission.PermissionModel +import io.tolgee.hateoas.permission.PermissionWithAgencyModel import io.tolgee.model.enums.OrganizationRoleType import org.springframework.hateoas.RepresentationModel import org.springframework.hateoas.server.core.Relation @@ -16,7 +17,7 @@ data class UserAccountInProjectModel( var avatar: Avatar?, val organizationRole: OrganizationRoleType?, val organizationBasePermission: PermissionModel, - val directPermission: PermissionModel?, + val directPermission: PermissionWithAgencyModel?, @Schema( description = "Actual user's permissions on selected project. You can not sort data by this column!", example = "EDIT", diff --git a/backend/api/src/main/kotlin/io/tolgee/hateoas/userAccount/UserAccountInProjectModelAssembler.kt b/backend/api/src/main/kotlin/io/tolgee/hateoas/userAccount/UserAccountInProjectModelAssembler.kt index 64fdfd529d..74e5260bb9 100644 --- a/backend/api/src/main/kotlin/io/tolgee/hateoas/userAccount/UserAccountInProjectModelAssembler.kt +++ b/backend/api/src/main/kotlin/io/tolgee/hateoas/userAccount/UserAccountInProjectModelAssembler.kt @@ -3,6 +3,7 @@ package io.tolgee.hateoas.userAccount import io.tolgee.api.v2.controllers.V2UserController import io.tolgee.hateoas.permission.ComputedPermissionModelAssembler import io.tolgee.hateoas.permission.PermissionModelAssembler +import io.tolgee.hateoas.permission.PermissionWithAgencyModelAssembler import io.tolgee.model.UserAccount import io.tolgee.model.views.ExtendedUserAccountInProject import io.tolgee.service.AvatarService @@ -16,6 +17,7 @@ class UserAccountInProjectModelAssembler( private val permissionModelAssembler: PermissionModelAssembler, private val computedPermissionModelAssembler: ComputedPermissionModelAssembler, private val avatarService: AvatarService, + private val permissionWithAgencyModelAssembler: PermissionWithAgencyModelAssembler, ) : RepresentationModelAssemblerSupport( V2UserController::class.java, UserAccountInProjectModel::class.java, @@ -35,7 +37,7 @@ class UserAccountInProjectModelAssembler( name = view.name, organizationRole = view.organizationRole, organizationBasePermission = permissionModelAssembler.toModel(view.organizationBasePermission), - directPermission = view.directPermission?.let { permissionModelAssembler.toModel(it) }, + directPermission = view.directPermission?.let { permissionWithAgencyModelAssembler.toModel(it) }, computedPermission = computedPermissionModelAssembler.toModel(computedPermissions), avatar = avatar, ) 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 27dacf9622..fbdcf0c2cf 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 @@ -73,4 +73,5 @@ enum class ActivityType( TASK_CLOSE, TASK_REOPEN, TASK_KEY_UPDATE(hideInList = true), + ORDER_TRANSLATION, } diff --git a/backend/data/src/main/kotlin/io/tolgee/component/email/InvitationEmailSender.kt b/backend/data/src/main/kotlin/io/tolgee/component/email/InvitationEmailSender.kt index e26dd0b422..f9ca814384 100644 --- a/backend/data/src/main/kotlin/io/tolgee/component/email/InvitationEmailSender.kt +++ b/backend/data/src/main/kotlin/io/tolgee/component/email/InvitationEmailSender.kt @@ -54,7 +54,7 @@ class InvitationEmailSender( return "You have been invited to $toWhat $escapedName in Tolgee." } - private fun getInvitationAcceptUrl(code: String): String { + fun getInvitationAcceptUrl(code: String): String { return "${frontendUrlProvider.url}/accept_invitation/$code" } } diff --git a/backend/data/src/main/kotlin/io/tolgee/component/email/TolgeeEmailSender.kt b/backend/data/src/main/kotlin/io/tolgee/component/email/TolgeeEmailSender.kt index f75d61d8cb..cc61780c6e 100644 --- a/backend/data/src/main/kotlin/io/tolgee/component/email/TolgeeEmailSender.kt +++ b/backend/data/src/main/kotlin/io/tolgee/component/email/TolgeeEmailSender.kt @@ -17,6 +17,12 @@ class TolgeeEmailSender( val helper = mimeMessageHelperFactory.create() helper.setFrom(tolgeeProperties.smtp.from!!) helper.setTo(params.to) + params.replyTo?.let { + helper.setReplyTo(it) + } + if (!params.bcc.isNullOrEmpty()) { + helper.setBcc(params.bcc!!) + } helper.setSubject(params.subject) val content = """ diff --git a/backend/data/src/main/kotlin/io/tolgee/component/enabledFeaturesProvider/EnabledFeaturesProvider.kt b/backend/data/src/main/kotlin/io/tolgee/component/enabledFeaturesProvider/EnabledFeaturesProvider.kt index 956b4345e0..ea07ced834 100644 --- a/backend/data/src/main/kotlin/io/tolgee/component/enabledFeaturesProvider/EnabledFeaturesProvider.kt +++ b/backend/data/src/main/kotlin/io/tolgee/component/enabledFeaturesProvider/EnabledFeaturesProvider.kt @@ -22,4 +22,13 @@ interface EnabledFeaturesProvider { throw BadRequestException(Message.FEATURE_NOT_ENABLED, listOf(feature)) } } + + fun checkOneOfFeaturesEnabled( + organizationId: Long?, + features: Collection, + ) { + if (features.find { this.isFeatureEnabled(organizationId, it) } == null) { + throw BadRequestException(Message.FEATURE_NOT_ENABLED, features.toList()) + } + } } diff --git a/backend/data/src/main/kotlin/io/tolgee/constants/Feature.kt b/backend/data/src/main/kotlin/io/tolgee/constants/Feature.kt index b014083e12..0adf6ceac1 100644 --- a/backend/data/src/main/kotlin/io/tolgee/constants/Feature.kt +++ b/backend/data/src/main/kotlin/io/tolgee/constants/Feature.kt @@ -19,4 +19,5 @@ enum class Feature { AI_PROMPT_CUSTOMIZATION, SLACK_INTEGRATION, TASKS, + ORDER_TRANSLATION, } diff --git a/backend/data/src/main/kotlin/io/tolgee/constants/Message.kt b/backend/data/src/main/kotlin/io/tolgee/constants/Message.kt index f3e5e1691c..74136a50ec 100644 --- a/backend/data/src/main/kotlin/io/tolgee/constants/Message.kt +++ b/backend/data/src/main/kotlin/io/tolgee/constants/Message.kt @@ -242,6 +242,7 @@ enum class Message { TASK_NOT_FOUND, TASK_NOT_FINISHED, TASK_NOT_OPEN, + TRANSLATION_AGENCY_NOT_FOUND, ; val code: String diff --git a/backend/data/src/main/kotlin/io/tolgee/dtos/misc/CreateInvitationParams.kt b/backend/data/src/main/kotlin/io/tolgee/dtos/misc/CreateInvitationParams.kt index 956cca7bcf..e22ad6286a 100644 --- a/backend/data/src/main/kotlin/io/tolgee/dtos/misc/CreateInvitationParams.kt +++ b/backend/data/src/main/kotlin/io/tolgee/dtos/misc/CreateInvitationParams.kt @@ -3,4 +3,5 @@ package io.tolgee.dtos.misc interface CreateInvitationParams { val email: String? val name: String? + val agencyId: Long? } diff --git a/backend/data/src/main/kotlin/io/tolgee/dtos/misc/CreateOrganizationInvitationParams.kt b/backend/data/src/main/kotlin/io/tolgee/dtos/misc/CreateOrganizationInvitationParams.kt index b1afb8985c..8d354010e8 100644 --- a/backend/data/src/main/kotlin/io/tolgee/dtos/misc/CreateOrganizationInvitationParams.kt +++ b/backend/data/src/main/kotlin/io/tolgee/dtos/misc/CreateOrganizationInvitationParams.kt @@ -8,4 +8,5 @@ data class CreateOrganizationInvitationParams( var type: OrganizationRoleType, override val email: String? = null, override val name: String? = null, + override val agencyId: Long? = null, ) : CreateInvitationParams diff --git a/backend/data/src/main/kotlin/io/tolgee/dtos/misc/CreateProjectInvitationParams.kt b/backend/data/src/main/kotlin/io/tolgee/dtos/misc/CreateProjectInvitationParams.kt index 67a4e18bf1..5f524254f8 100644 --- a/backend/data/src/main/kotlin/io/tolgee/dtos/misc/CreateProjectInvitationParams.kt +++ b/backend/data/src/main/kotlin/io/tolgee/dtos/misc/CreateProjectInvitationParams.kt @@ -11,4 +11,5 @@ data class CreateProjectInvitationParams( var scopes: Set? = null, override val email: String? = null, override val name: String? = null, + override val agencyId: Long? = null, ) : CreateInvitationParams diff --git a/backend/data/src/main/kotlin/io/tolgee/dtos/misc/EmailParams.kt b/backend/data/src/main/kotlin/io/tolgee/dtos/misc/EmailParams.kt index 1260cab5df..7c3d23ec37 100644 --- a/backend/data/src/main/kotlin/io/tolgee/dtos/misc/EmailParams.kt +++ b/backend/data/src/main/kotlin/io/tolgee/dtos/misc/EmailParams.kt @@ -2,7 +2,9 @@ package io.tolgee.dtos.misc class EmailParams( var to: String, + var bcc: Array? = null, var text: String, var subject: String, var attachments: List = listOf(), + var replyTo: String? = null, ) diff --git a/backend/data/src/main/kotlin/io/tolgee/dtos/request/project/ProjectInviteUserDto.kt b/backend/data/src/main/kotlin/io/tolgee/dtos/request/project/ProjectInviteUserDto.kt index e975daf57f..f83473fe9a 100644 --- a/backend/data/src/main/kotlin/io/tolgee/dtos/request/project/ProjectInviteUserDto.kt +++ b/backend/data/src/main/kotlin/io/tolgee/dtos/request/project/ProjectInviteUserDto.kt @@ -27,4 +27,8 @@ data class ProjectInviteUserDto( ) @field:Size(max = 250) val name: String? = null, + @Schema( + description = """Id of invited agency""", + ) + val agencyId: Long? = null, ) : RequestWithLanguagePermissions diff --git a/backend/data/src/main/kotlin/io/tolgee/dtos/request/userAccount/UserAccountFilters.kt b/backend/data/src/main/kotlin/io/tolgee/dtos/request/userAccount/UserAccountFilters.kt index df76c7bb19..d848519296 100644 --- a/backend/data/src/main/kotlin/io/tolgee/dtos/request/userAccount/UserAccountFilters.kt +++ b/backend/data/src/main/kotlin/io/tolgee/dtos/request/userAccount/UserAccountFilters.kt @@ -12,4 +12,9 @@ open class UserAccountFilters { description = """Filter users without id""", ) var filterNotId: List? = null + + @field:Parameter( + description = """Filter users from agency""", + ) + var filterAgency: List? = null } diff --git a/backend/data/src/main/kotlin/io/tolgee/model/Permission.kt b/backend/data/src/main/kotlin/io/tolgee/model/Permission.kt index 1ceb8b7b1a..1f3dcb9562 100644 --- a/backend/data/src/main/kotlin/io/tolgee/model/Permission.kt +++ b/backend/data/src/main/kotlin/io/tolgee/model/Permission.kt @@ -5,6 +5,7 @@ import io.tolgee.dtos.cacheable.IPermission import io.tolgee.dtos.request.project.LanguagePermissions import io.tolgee.model.enums.ProjectPermissionType import io.tolgee.model.enums.Scope +import io.tolgee.model.translationAgency.TranslationAgency import jakarta.persistence.Column import jakarta.persistence.Entity import jakarta.persistence.EntityListeners @@ -33,6 +34,7 @@ import org.hibernate.annotations.Type indexes = [ Index(columnList = "user_id"), Index(columnList = "project_id"), + Index(columnList = "agency_id"), ], ) class Permission( @@ -48,6 +50,8 @@ class Permission( var organization: Organization? = null, @OneToOne(fetch = FetchType.LAZY) var invitation: Invitation? = null, + @ManyToOne(fetch = FetchType.LAZY, optional = true) + var agency: TranslationAgency? = null, ) : AuditModel(), IPermission { @Type( EnumArrayType::class, @@ -114,6 +118,7 @@ class Permission( type: ProjectPermissionType? = ProjectPermissionType.VIEW, languagePermissions: LanguagePermissions? = null, scopes: Array? = null, + agency: TranslationAgency? = null, ) : this( id = id, user = user, @@ -127,6 +132,7 @@ class Permission( this.viewLanguages = languagePermissions?.view?.toMutableSet() ?: mutableSetOf() this.translateLanguages = languagePermissions?.translate?.toMutableSet() ?: mutableSetOf() this.stateChangeLanguages = languagePermissions?.stateChange?.toMutableSet() ?: mutableSetOf() + this.agency = agency } @ManyToOne diff --git a/backend/data/src/main/kotlin/io/tolgee/model/enums/ProjectPermissionType.kt b/backend/data/src/main/kotlin/io/tolgee/model/enums/ProjectPermissionType.kt index 5191100661..43b1167fba 100644 --- a/backend/data/src/main/kotlin/io/tolgee/model/enums/ProjectPermissionType.kt +++ b/backend/data/src/main/kotlin/io/tolgee/model/enums/ProjectPermissionType.kt @@ -8,6 +8,7 @@ enum class ProjectPermissionType(val availableScopes: Array) { Scope.SCREENSHOTS_VIEW, Scope.ACTIVITY_VIEW, Scope.KEYS_VIEW, + Scope.TASKS_VIEW, ), ), TRANSLATE( @@ -19,6 +20,7 @@ enum class ProjectPermissionType(val availableScopes: Array) { Scope.ACTIVITY_VIEW, Scope.TRANSLATIONS_COMMENTS_ADD, Scope.TRANSLATIONS_COMMENTS_SET_STATE, + Scope.TASKS_VIEW, ), ), REVIEW( @@ -31,6 +33,7 @@ enum class ProjectPermissionType(val availableScopes: Array) { Scope.TRANSLATIONS_COMMENTS_ADD, Scope.TRANSLATIONS_COMMENTS_SET_STATE, Scope.TRANSLATIONS_STATE_EDIT, + Scope.TASKS_VIEW, ), ), EDIT( @@ -52,6 +55,7 @@ enum class ProjectPermissionType(val availableScopes: Array) { Scope.BATCH_PRE_TRANSLATE_BY_TM, Scope.BATCH_MACHINE_TRANSLATE, Scope.BATCH_JOBS_VIEW, + Scope.TASKS_VIEW, ), ), MANAGE( 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 f958388ac7..ed36e9b07e 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 @@ -11,6 +11,7 @@ import io.tolgee.model.StandardAuditModel import io.tolgee.model.UserAccount import io.tolgee.model.enums.TaskState import io.tolgee.model.enums.TaskType +import io.tolgee.model.translationAgency.TranslationAgency import jakarta.persistence.Column import jakarta.persistence.Entity import jakarta.persistence.EnumType @@ -36,6 +37,7 @@ import java.util.* indexes = [ Index(columnList = "author_id"), Index(columnList = "language_id"), + Index(columnList = "agency_id"), ], ) @ActivityLoggedEntity @@ -87,4 +89,7 @@ class Task : StandardAuditModel() { @ActivityLoggedProp var closedAt: Date? = null + + @ManyToOne(fetch = FetchType.LAZY, optional = true) + var agency: TranslationAgency? = null } diff --git a/backend/data/src/main/kotlin/io/tolgee/model/translationAgency/TranslationAgency.kt b/backend/data/src/main/kotlin/io/tolgee/model/translationAgency/TranslationAgency.kt new file mode 100644 index 0000000000..185b3747e1 --- /dev/null +++ b/backend/data/src/main/kotlin/io/tolgee/model/translationAgency/TranslationAgency.kt @@ -0,0 +1,41 @@ +package io.tolgee.model.translationAgency + +import io.hypersistence.utils.hibernate.type.json.JsonBinaryType +import io.tolgee.model.EntityWithId +import io.tolgee.model.ModelWithAvatar +import io.tolgee.model.StandardAuditModel +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.Table +import jakarta.validation.constraints.Size +import org.hibernate.annotations.Type + +@Entity +@Table() +class TranslationAgency : StandardAuditModel(), ModelWithAvatar, EntityWithId { + @field:Size(max = 255) + @Column(length = 255) + var name: String = "" + + @field:Size(max = 2000) + @Column(length = 2000) + var description: String? = null + + @Type(JsonBinaryType::class) + @Column(columnDefinition = "jsonb") + var services: MutableList = mutableListOf() + + @field:Size(max = 255) + @Column(length = 255) + var url: String? = null + + @field:Size(max = 255, min = 3) + @Column(length = 255) + var email: String = "" + + @Type(JsonBinaryType::class) + @Column(columnDefinition = "jsonb") + var emailBcc: MutableList = mutableListOf() + + override var avatarHash: String? = null +} 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 667042af01..ae3f8b308c 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 @@ -6,6 +6,7 @@ import io.tolgee.model.UserAccount import io.tolgee.model.enums.TaskState import io.tolgee.model.enums.TaskType import io.tolgee.model.task.TaskKey +import io.tolgee.model.translationAgency.TranslationAgency import java.util.* data class TaskWithScopeView( @@ -26,4 +27,5 @@ data class TaskWithScopeView( val doneItems: Long, val baseWordCount: Long, val baseCharacterCount: Long, + val agency: TranslationAgency?, ) diff --git a/backend/data/src/main/kotlin/io/tolgee/repository/PermissionRepository.kt b/backend/data/src/main/kotlin/io/tolgee/repository/PermissionRepository.kt index 6d4399397d..90adc95ca9 100644 --- a/backend/data/src/main/kotlin/io/tolgee/repository/PermissionRepository.kt +++ b/backend/data/src/main/kotlin/io/tolgee/repository/PermissionRepository.kt @@ -97,4 +97,12 @@ interface PermissionRepository : JpaRepository { organizationId: Long, userId: Long, ): List + + @Query( + """ + from Permission p + where p.agency.id = :agencyId + """, + ) + fun findAllByAgencyId(agencyId: Long): List } diff --git a/backend/data/src/main/kotlin/io/tolgee/repository/UserAccountRepository.kt b/backend/data/src/main/kotlin/io/tolgee/repository/UserAccountRepository.kt index ec6435c76b..2499606a90 100644 --- a/backend/data/src/main/kotlin/io/tolgee/repository/UserAccountRepository.kt +++ b/backend/data/src/main/kotlin/io/tolgee/repository/UserAccountRepository.kt @@ -23,6 +23,10 @@ private const val USER_FILTERS = """ :#{#filters.filterNotId} is null or ua.id not in :#{#filters.filterNotId} ) + and ( + :#{#filters.filterAgency} is null + or p.agency.id in :#{#filters.filterAgency} + ) """ private const val PROJECT_PERMISSIONS_CTE = """ diff --git a/backend/data/src/main/kotlin/io/tolgee/service/AvatarService.kt b/backend/data/src/main/kotlin/io/tolgee/service/AvatarService.kt index fce4889cfa..c78b33889c 100644 --- a/backend/data/src/main/kotlin/io/tolgee/service/AvatarService.kt +++ b/backend/data/src/main/kotlin/io/tolgee/service/AvatarService.kt @@ -29,11 +29,15 @@ class AvatarService( fun storeAvatarFiles( avatar: InputStream, entity: ModelWithAvatar, + dimension: Dimension? = null, + thumbnailSize: Int? = null, ): String { + val dim = dimension ?: Dimension(200, 200) + val thumbSize = thumbnailSize ?: 50 val avatarBytes = avatar.readAllBytes() val converter = ImageConverter(avatarBytes.inputStream()) - val large = converter.getImage(-1f, Dimension(200, 200)).toByteArray() - val thumb = converter.getThumbnail(50).toByteArray() + val large = converter.getImage(-1f, dim).toByteArray() + val thumb = converter.getThumbnail(thumbSize).toByteArray() val idByteArray = "${entity::class.simpleName}-${entity.id}---".toByteArray() val bytesToHash = idByteArray + large val hashBinary = MessageDigest.getInstance("SHA-256").digest(bytesToHash) @@ -48,8 +52,10 @@ class AvatarService( fun setAvatar( entity: ModelWithAvatar, avatar: InputStream, + dimension: Dimension? = null, + thumbnailSize: Int? = null, ) { - val hash = storeAvatarFiles(avatar, entity) + val hash = storeAvatarFiles(avatar, entity, dimension, thumbnailSize) removeAvatar(entity) entity.avatarHash = hash } diff --git a/backend/data/src/main/kotlin/io/tolgee/service/ITaskService.kt b/backend/data/src/main/kotlin/io/tolgee/service/ITaskService.kt index 4bfe1b3b2b..46d17b5e5e 100644 --- a/backend/data/src/main/kotlin/io/tolgee/service/ITaskService.kt +++ b/backend/data/src/main/kotlin/io/tolgee/service/ITaskService.kt @@ -25,4 +25,6 @@ interface ITaskService { userId: Long, keyIds: Collection, ): Map> + + fun getAgencyTasks(agencyId: Long): List } diff --git a/backend/data/src/main/kotlin/io/tolgee/service/PublicTranslationAgencyService.kt b/backend/data/src/main/kotlin/io/tolgee/service/PublicTranslationAgencyService.kt new file mode 100644 index 0000000000..641eedcbe9 --- /dev/null +++ b/backend/data/src/main/kotlin/io/tolgee/service/PublicTranslationAgencyService.kt @@ -0,0 +1,14 @@ +package io.tolgee.service + +import io.tolgee.constants.Feature +import io.tolgee.constants.Message +import io.tolgee.exceptions.BadRequestException +import io.tolgee.model.translationAgency.TranslationAgency +import org.springframework.stereotype.Component + +@Component +class PublicTranslationAgencyService : TranslationAgencyService { + override fun findById(id: Long): TranslationAgency { + throw BadRequestException(Message.FEATURE_NOT_ENABLED, listOf(Feature.ORDER_TRANSLATION)) + } +} diff --git a/backend/data/src/main/kotlin/io/tolgee/service/TranslationAgencyService.kt b/backend/data/src/main/kotlin/io/tolgee/service/TranslationAgencyService.kt new file mode 100644 index 0000000000..c3caa21fc8 --- /dev/null +++ b/backend/data/src/main/kotlin/io/tolgee/service/TranslationAgencyService.kt @@ -0,0 +1,7 @@ +package io.tolgee.service + +import io.tolgee.model.translationAgency.TranslationAgency + +interface TranslationAgencyService { + fun findById(id: Long): TranslationAgency +} diff --git a/backend/data/src/main/kotlin/io/tolgee/service/organization/OrganizationService.kt b/backend/data/src/main/kotlin/io/tolgee/service/organization/OrganizationService.kt index 0f5dbd0eb0..d32a602372 100644 --- a/backend/data/src/main/kotlin/io/tolgee/service/organization/OrganizationService.kt +++ b/backend/data/src/main/kotlin/io/tolgee/service/organization/OrganizationService.kt @@ -39,6 +39,7 @@ import org.springframework.data.domain.PageRequest import org.springframework.data.domain.Pageable import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional +import java.awt.Dimension import java.io.InputStream import io.tolgee.dtos.cacheable.OrganizationDto as CachedOrganizationDto @@ -301,7 +302,7 @@ class OrganizationService( organization: Organization, avatar: InputStream, ) { - avatarService.setAvatar(organization, avatar) + avatarService.setAvatar(organization, avatar, Dimension(300, 60)) } /** diff --git a/backend/data/src/main/kotlin/io/tolgee/service/security/PermissionService.kt b/backend/data/src/main/kotlin/io/tolgee/service/security/PermissionService.kt index ae3063dc88..efcbcb71e3 100644 --- a/backend/data/src/main/kotlin/io/tolgee/service/security/PermissionService.kt +++ b/backend/data/src/main/kotlin/io/tolgee/service/security/PermissionService.kt @@ -22,12 +22,14 @@ import io.tolgee.model.UserAccount import io.tolgee.model.enums.OrganizationRoleType import io.tolgee.model.enums.ProjectPermissionType import io.tolgee.model.enums.Scope +import io.tolgee.model.translationAgency.TranslationAgency import io.tolgee.repository.PermissionRepository import io.tolgee.service.CachedPermissionService import io.tolgee.service.language.LanguageService import io.tolgee.service.organization.OrganizationRoleService import io.tolgee.service.organization.OrganizationService import io.tolgee.service.project.ProjectService +import jakarta.persistence.EntityManager import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.ApplicationContext import org.springframework.context.annotation.Lazy @@ -46,6 +48,7 @@ class PermissionService( private val userPreferencesService: UserPreferencesService, @Lazy private val applicationContext: ApplicationContext, + private val entityManager: EntityManager, ) { @set:Autowired @set:Lazy @@ -59,6 +62,19 @@ class PermissionService( @set:Autowired lateinit var projectService: ProjectService + @Transactional + fun findPermissionNonCached( + projectId: Long? = null, + userId: Long? = null, + organizationId: Long? = null, + ): Permission? { + return permissionRepository.findOneByProjectIdAndUserIdAndOrganizationId( + projectId = projectId, + userId = userId, + organizationId = organizationId, + ) + } + fun getAllOfProject(project: Project?): Set { return permissionRepository.getAllByProjectAndUserNotNull(project) } @@ -109,6 +125,13 @@ class PermissionService( ) } + fun getUserProjectPermission( + projectId: Long, + userId: Long, + ): PermissionDto? { + return find(projectId, userId) + } + fun getPermittedTranslateLanguagesForUserIds( userIds: List, projectId: Long, @@ -229,6 +252,10 @@ class PermissionService( invitation = invitation, project = params.project, type = type, + agency = + params.agencyId?.let { + entityManager.getReference(TranslationAgency::class.java, it) + }, ) setPermissionLanguages(permission, params.languagePermissions, params.project.id) @@ -461,4 +488,12 @@ class PermissionService( permissions.forEach { delete(it) } return permissions } + + fun deleteAll(permissions: List) { + permissionRepository.deleteAll(permissions) + } + + fun getAgencyPermissions(agencyId: Long): List { + return permissionRepository.findAllByAgencyId(agencyId) + } } diff --git a/backend/data/src/main/resources/db/changelog/schema.xml b/backend/data/src/main/resources/db/changelog/schema.xml index 6f57803241..9746ef7064 100644 --- a/backend/data/src/main/resources/db/changelog/schema.xml +++ b/backend/data/src/main/resources/db/changelog/schema.xml @@ -1,3881 +1,4088 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd"> - + - + - + - - - - + + + + - + - + - + - + - + - + - + - - - + + + - + - + - + - - - - + + + + - + - + - + - - - + + + - + - + - + - - + + - + - + - + - - - + + + - + - + - + - - - - - - - + + + + + + + - + - + - + - + - + - + - + + constraintName="useraccount_authtype_auth_id" tableName="user_account" /> - + - + - - + + + constraintName="FK1yntjh7ggi4te3ovft3q98vjh" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="invitation" + validate="true" /> + constraintName="FK4eao8empmjkh4djn65mgpo0xy" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="repository" + validate="true" /> + constraintName="FK4w7p5ffylu2s25r0fvcg7m16m" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="repository" + validate="true" /> + constraintName="FKan8l57bdrd3yfsra16gy812nh" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="source" + validate="true" /> + constraintName="FKc2d46w0v0yhqdf6nyel7g0b3u" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="language" + validate="true" /> + constraintName="FKd4loxn3vchrap5ulsueynd7m2" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="user_account" + validate="true" /> + constraintName="FKk3nnt1okbdkvp7igkexvbyxnf" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="repository" + validate="true" /> + constraintName="FKl6qxgkp6jih1aup8laua5ime9" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="repository" + validate="true" /> + constraintName="FKoiov5289pveggjm2sthuhw4qm" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="user_account" + validate="true" /> + constraintName="FKsryptvc972nlvw7we7fkr06mg" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="user_account" + validate="true" /> - + - + - + - + - - + + - + - + - + - + + constraintName="FKojcltkbo4pxlf41nwlxwkgtjy" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="key" + validate="true" /> - + - + - + - + - + - + - + - + - + + constraintName="FKspqbeiygfq5lk5dw2sgt9pbml" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="api_key" + validate="true" /> - + - - insert into API_KEY_SCOPES_ENUM - select id, 0 - from API_KEY - where SCOPES like '%translations.view%'; - insert into API_KEY_SCOPES_ENUM - select id, 1 - from API_KEY - where SCOPES like '%translations.edit%'; - insert into API_KEY_SCOPES_ENUM - select id, 2 - from API_KEY - where SCOPES like '%keys.edit%'; - - - delete from API_KEY_SCOPES_ENUM where 1 = 1 - + insert into API_KEY_SCOPES_ENUM select id, 0 from API_KEY where SCOPES like + '%translations.view%'; insert into API_KEY_SCOPES_ENUM select id, 1 from API_KEY where + SCOPES like '%translations.edit%'; insert into API_KEY_SCOPES_ENUM select id, 2 from + API_KEY where SCOPES like '%keys.edit%'; + delete from API_KEY_SCOPES_ENUM where 1 = 1 - + - - - - + + + + - + - + - + - - - - + + + + - + - + - + + constraintName="FK581ryvy4rn9ey7rx3lestkuix" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="invitation" + validate="true" /> + constraintName="FK6p6nbxihffjckr4tayum4x67v" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="user_account" + validate="true" /> + constraintName="FKqj2a0uojwh4p5rc9vfsu92fdk" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="organization" + validate="true" /> - + - + - + - + + constraintName="FKe8q5ar4olfji1mw1p0163ctyk" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="user_account" + validate="true" /> + constraintName="FKp2q5w860n1uai6oyel74udi83" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="organization" + validate="true" /> + constraintName="organization_role_user_organization_unique" + tableName="organization_role" /> - + - + - + - + - + - + - + - + - + - + - - + + - + - + - + - + - + - + - + - + - + - - + + - + - + - + - + - + - + - + - + - + - + - - + + - + - + - + - + - + - - - + + + - + - + - + - + - + - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - - + + - + + constraintName="FK2q8pryy6fohmtjm1hmu7tw63s" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="import_file" + validate="true" /> + constraintName="FK2twe5erdt7yfludaoiq7gv3mf" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="user_account" + validate="true" /> + constraintName="FK5yemkngk70scn7l0r4cwlmtn" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="import_file" + validate="true" /> + constraintName="FK8hvxh3e4cxn5561l76xejytnx" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="import" + validate="true" /> + constraintName="FKapx5aa95fl74isypara87i0oe" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="key_meta" + validate="true" /> + constraintName="FKcrettb2iyas70c96411bncjb7" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="key_meta" + validate="true" /> + constraintName="FKfm8n337a6jyvume29k80edgvj" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" + referencedTableName="import_language" validate="true" /> + constraintName="FKgdb4y0up36tyubhysfpbru11b" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="key" + validate="true" /> + constraintName="FKikmqn4hh8e4fp8k4dj16fkh3y" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="import_key" + validate="true" /> + constraintName="FKjawljy0kpt4o2j8ix7qv1x196" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="import_file" + validate="true" /> + constraintName="FKlry3bmimp7a094jj0kioe6ga5" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" + referencedTableName="import_file_issue" validate="true" /> + constraintName="FKnhtysiwn7w2tn04nr4smmc6af" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="user_account" + validate="true" /> + constraintName="FKnus5ewftf9lxhm520whg3msnh" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="user_account" + validate="true" /> + constraintName="FKp7xnyjio5do5p6yb8ch2hrudw" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="import_file" + validate="true" /> - + + constraintName="FKqpyb4896knd8m41ffn1f7wgeo" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="import_key" + validate="true" /> + constraintName="FKr3w7b3sw9aiegenlgjjnjt2yi" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="repository" + validate="true" /> + constraintName="FKrlpc5t6f2d5v6w7r1jsk7y01l" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="translation" + validate="true" /> + constraintName="FKrtx4aq0bqg6m83cdbi8vcuyve" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="import_key" + validate="true" /> - + + tableName="api_key" + newColumnName="project_id" + oldColumnName="repository_id" /> + tableName="import" + newColumnName="project_id" + oldColumnName="repository_id" /> + tableName="key" + newColumnName="project_id" + oldColumnName="repository_id" /> + tableName="language" + newColumnName="project_id" + oldColumnName="repository_id" /> + tableName="permission" + newColumnName="project_id" + oldColumnName="repository_id" /> - + - + + tableName="language" /> + tableName="project" /> - - + + + constraintName="FK1egspeawij8o52ckn75n0di8u" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="project" + validate="true" /> + constraintName="FKb0ubhe0qiqd6er95wwhguincu" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="project" + validate="true" /> + constraintName="FKboemy1qcry5x32aoimycf6jrt" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="organization" + validate="true" /> + constraintName="FKe30ekt7igqooxef59tbedyfdj" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="project" + validate="true" /> + constraintName="FKg12n5rjbnfoqlwpdkv3tql81p" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="project" + validate="true" /> + constraintName="FKno8mwmeoh2j8hv6yb6ghrepew" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="user_account" + validate="true" /> + constraintName="FKoknj6iem9tqstdembw2rrvvs1" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="project" + validate="true" /> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + constraintName="FKe87b2cgwpk3qttqh95tgrehlt" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="language" + validate="true" /> - + - + - + - + - + - - - - - + + + + + - + - + - + - + - + - + - - - - + + + + - + - + - - + + - + - + - - - + + + - + - + - - - - - - + + + + + + - + - + - - - - - + + + + + - + - + - - - - - + + + + + - + - + - - - - - + + + + + - + - + - - - - - - - + + + + + + + - + - + - + - + - + - - + + - + - + - - - - - + + + + + - + - + - + - - - - - + + + + + - + - + - - - - - - + + + + + + - + - + - - - - - - - - + + + + + + + + + constraintName="FK1diai27pxl7epr7wmiddlwns5" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="revision" + validate="true" /> + constraintName="FK3jkyqktbidh2cyx3xy3qml99l" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="revision" + validate="true" /> + constraintName="FK9ac0dmcy0armtdh781gs8bq01" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="user_account" + validate="true" /> + constraintName="FKb7qk3ljn3o8bqwum6a5io3tpe" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="user_account" + validate="true" /> + constraintName="FKcwyby3t6jg4ybxox3nchfbpqy" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="revision" + validate="true" /> + constraintName="FKdkc05rqm5p3ahqp80ukr4y6se" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="revision" + validate="true" /> + constraintName="FKfi9ywuk505xl9g17jib6b7urx" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="revision" + validate="true" /> - + + constraintName="FKgey5u1uwkslc6hhyp3uka2wj8" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="revision" + validate="true" /> + constraintName="FKgrkrhev0nmxi9r1h096cm8lxe" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="revision" + validate="true" /> + constraintName="FKhqt2jx03076gcmame7hggtppa" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="revision" + validate="true" /> + constraintName="FKi84l90hnwf1m1wg1hn2ov7ykr" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="user_account" + validate="true" /> + constraintName="FKlkb2y39ix1nhtnjkco5pkwa5w" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="revision" + validate="true" /> + constraintName="FKlkd3khuueqm30qwpdrku4w6eo" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="revision" + validate="true" /> + constraintName="FKq4t05s4qw8uasc9mjl2tk83sm" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="revision" + validate="true" /> + constraintName="FKq6jhbxmwvr0o19sfqwog5gk7s" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="revision" + validate="true" /> + constraintName="FKq8g6phhnoyyuyjh9omekg8ja7" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="revision" + validate="true" /> + constraintName="FKqsyp928iixx9oharj1ofavbk5" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="revision" + validate="true" /> - + - + - + - + - + - + - - + + + constraintName="FK1hhq8sx3mn7isckcoaq277ix6" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="tag" + validate="true" /> + constraintName="FKbyy56vice9njgl86752up8120" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="project" + validate="true" /> + constraintName="FKsbnhi8xkxfybrf26ypraemrmk" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="key_meta" + validate="true" /> - + - + - + - + - + - + - + - + - + - + - + - + - - + + + tableName="uploaded_image" /> + constraintName="FKelfpas8y8t5pq40m57ttdcnf" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="user_account" + validate="true" /> - + - + + constraintName="organization_third_party_billing_id_unique" tableName="organization" /> + columnName="name" + newDataType="varchar(2000)" + tableName="key" /> - + - + - + - + - - - + + + - + - + - + - - - + + + - + - + - + + constraintName="FK7cklk7byifpo0jj5q58dwn6rt" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="project" + validate="true" /> + constraintName="FKlgwarx2xdphg9imptapjv2p6j" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="user_account" + validate="true" /> + constraintName="FKsx03b0rw40yo8jtqkklir7h04" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="organization" + validate="true" /> + baseTableName="mt_service_config_enabled_services" + constraintName="FKvo4mgub2uvhr99nqjm2gwy57" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" + referencedTableName="mt_service_config" validate="true" /> - - CREATE - EXTENSION IF NOT EXISTS pg_trgm; - CREATE INDEX translation_text_gin_trgm_index ON translation USING GIN (text gin_trgm_ops); - + CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE INDEX + translation_text_gin_trgm_index ON translation USING GIN (text gin_trgm_ops); - + - + - + - + - + - + - + - + - + - + - + - + + constraintName="FK209w4e9w8xrj3grtabgpuolnr" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="language" + validate="true" /> - + + constraintName="FKqrbr6c2d8ujo1y43a6ftsdp8c" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="revision" + validate="true" /> - + - + - + - + - - UPDATE translation - set state = 1 - where state = 2 - or state = 4; - UPDATE translation - set state = 2 - where state = 3; - + UPDATE translation set state = 1 where state = 2 or state = 4; + UPDATE translation set state = 2 where state = 3; - + - + - + - + - + - + - + - + - + - + - + - - UPDATE translation_aud - set state = 1 - where state = 2 - or state = 4; - UPDATE translation_aud - set state = 2 - where state = 3; - + UPDATE translation_aud set state = 1 where state = 2 or state = 4; + UPDATE translation_aud set state = 2 where state = 3; - + - + - + - - - + + + - + - + - + - - - - + + + + - + - + - - - + + + - + - + + constraintName="activity_describing_entityPK" tableName="activity_describing_entity" /> + constraintName="activity_modified_entityPK" tableName="activity_modified_entity" /> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + constraintName="FKqrbr6c2d8ujo1y43a6ftsdp8c" /> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + constraintName="FKdhlxchq1m65xxjrioelwhqrh7" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="user_account" + validate="true" /> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + constraintName="FKh2vk27p76ib735etoyfl2x0p" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="language" + validate="true" /> - + - + - + - + - + - + - + - - - - - + + + + + - + + constraintName="FKdjjacqlrm6ps628vl2tgv6af9" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="user_account" + validate="true" /> - + - + - + - + - + - + - + - + - + - - UPDATE user_account - SET account_type = 'LOCAL'; - UPDATE user_account - SET account_type = 'THIRD_PARTY' - WHERE password IS NULL; - UPDATE user_account - SET account_type = 'LDAP' - WHERE password IS NULL - AND third_party_auth_type IS NULL; - + UPDATE user_account SET account_type = 'LOCAL'; UPDATE user_account + SET account_type = 'THIRD_PARTY' WHERE password IS NULL; UPDATE user_account SET + account_type = 'LDAP' WHERE password IS NULL AND third_party_auth_type IS NULL; - + - + - + - + - - create unique index on public.user_account (username) where deleted_at is null; - + create unique index on public.user_account (username) where deleted_at is null; - - create unique index on public.user_account (third_party_auth_id, third_party_auth_type) where deleted_at is null; - + create unique index on public.user_account (third_party_auth_id, + third_party_auth_type) where deleted_at is null; - + - + - + - + - - + + - + + constraintName="FK7ex52jk5v6xtvpbaojlmv9cas" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="namespace" + validate="true" /> + constraintName="FKo9xwhbk7m02ohl0qnxk658hud" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="project" + validate="true" /> - + - - create unique index on public.key (project_id, "name", namespace_id) where namespace_id is not null; - - - create unique index on public.key (project_id, "name") where namespace_id is null; - + create unique index on public.key (project_id, "name", namespace_id) where + namespace_id is not null; + create unique index on public.key (project_id, "name") where namespace_id is null; - + + tableName="namespace" /> - + + constraintName="FK2lyqy9kydj78d72iatgt0c509" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="import_file" + validate="true" /> - + - + - + - + - + - + ANY - - CREATE - EXTENSION IF NOT EXISTS unaccent; - CREATE - EXTENSION IF NOT EXISTS pg_trgm; - - CREATE - OR REPLACE FUNCTION public.f_unaccent(text) - RETURNS text - LANGUAGE sql IMMUTABLE PARALLEL SAFE STRICT AS - $func$ - SELECT public.unaccent('public.unaccent', $1) - $func$; - - CREATE INDEX translation_text_unaccent_gin_trgm_index ON translation USING GIN ( lower (f_unaccent(text)) gin_trgm_ops); - CREATE INDEX key_name_unaccent_gin_trgm_index ON key USING GIN (lower (f_unaccent(name)) gin_trgm_ops); - CREATE INDEX namespace_name_unaccent_gin_trgm_index ON namespace USING GIN (lower (f_unaccent(name)) gin_trgm_ops); - + CREATE EXTENSION IF NOT EXISTS unaccent; CREATE EXTENSION IF NOT + EXISTS pg_trgm; CREATE OR REPLACE FUNCTION public.f_unaccent(text) RETURNS text LANGUAGE + sql IMMUTABLE PARALLEL SAFE STRICT AS $func$ SELECT public.unaccent('public.unaccent', + $1) $func$; CREATE INDEX translation_text_unaccent_gin_trgm_index ON translation USING + GIN ( lower (f_unaccent(text)) gin_trgm_ops); CREATE INDEX + key_name_unaccent_gin_trgm_index ON key USING GIN (lower (f_unaccent(name)) + gin_trgm_ops); CREATE INDEX namespace_name_unaccent_gin_trgm_index ON namespace USING + GIN (lower (f_unaccent(name)) gin_trgm_ops); - - + + - + - + - + - + + constraintName="FKp6wnbfbgoxnqv6wihs7m63v5t" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="key" + validate="true" /> - - update screenshot - set path = k.project_id || '/' || k.id from key k - where k.id = screenshot.key_id; - - insert into key_screenshot_reference(key_id, screenshot_id) - select s.key_id, s.id - from screenshot s - join key k on k.id = s.key_id; - + update screenshot set path = k.project_id || '/' || k.id from key k + where k.id = screenshot.key_id; insert into key_screenshot_reference(key_id, + screenshot_id) select s.key_id, s.id from screenshot s join key k on k.id = s.key_id; - + - + - + - + - + - + - + - + - + - + - + - + - - ALTER TABLE key_screenshot_reference ALTER COLUMN original_text TYPE text; - + ALTER TABLE key_screenshot_reference ALTER COLUMN original_text TYPE text; - + - + ANY - - create unique index permission_null_idx on permission (organization_id); - insert into permission (id, type, organization_id, created_at, updated_at) - select nextval('permission_id_seq'), base_permissions, id, current_date, current_date - from organization; - + create unique index permission_null_idx on permission (organization_id); insert into + permission (id, type, organization_id, created_at, updated_at) select + nextval('permission_id_seq'), base_permissions, id, current_date, current_date from + organization; - + + constraintName="FKodqlqlhjgsfwnq418388o4vy7" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="organization" + validate="true" /> - + - + - + - + - + - + - + - + + baseTableName="permission_state_change_languages" + constraintName="FKpxsgkx94rrmdmqocvliirtevo" deferrable="false" + initiallyDeferred="false" referencedColumnNames="id" referencedTableName="language" + validate="true" /> - - alter table api_key_scopes_enum alter column scopes_enum type varchar(255) - using case - when scopes_enum = 0 then 'TRANSLATIONS_VIEW' - when scopes_enum = 1 then 'TRANSLATIONS_EDIT' - when scopes_enum = 2 then 'KEYS_EDIT' - when scopes_enum = 3 then 'SCREENSHOTS_UPLOAD' - when scopes_enum = 4 then 'SCREENSHOTS_DELETE' - when scopes_enum = 5 then 'SCREENSHOTS_VIEW' - when scopes_enum = 6 then 'ACTIVITY_VIEW' - when scopes_enum = 7 then 'LANGUAGES_EDIT' - end; - + alter table api_key_scopes_enum alter column scopes_enum type varchar(255) using case + when scopes_enum = 0 then 'TRANSLATIONS_VIEW' when scopes_enum = 1 then + 'TRANSLATIONS_EDIT' when scopes_enum = 2 then 'KEYS_EDIT' when scopes_enum = 3 then + 'SCREENSHOTS_UPLOAD' when scopes_enum = 4 then 'SCREENSHOTS_DELETE' when scopes_enum = 5 + then 'SCREENSHOTS_VIEW' when scopes_enum = 6 then 'ACTIVITY_VIEW' when scopes_enum = 7 + then 'LANGUAGES_EDIT' end; - - insert into api_key_scopes_enum (api_key_id, scopes_enum) - select api_key_id, 'KEYS_CREATE' - from api_key_scopes_enum - where scopes_enum = 'KEYS_EDIT'; - insert into api_key_scopes_enum (api_key_id, scopes_enum) - select api_key_id, 'KEYS_DELETE' - from api_key_scopes_enum - where scopes_enum = 'KEYS_EDIT'; - + insert into api_key_scopes_enum (api_key_id, scopes_enum) select api_key_id, + 'KEYS_CREATE' from api_key_scopes_enum where scopes_enum = 'KEYS_EDIT'; insert into + api_key_scopes_enum (api_key_id, scopes_enum) select api_key_id, 'KEYS_DELETE' from + api_key_scopes_enum where scopes_enum = 'KEYS_EDIT'; - + - + - + + tableName="translation" /> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - + + + - + - + - - - + + + - + - + - + - + - - - + + + - + - - + + - + - + - + - - + + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - UPDATE user_account - SET is_initial_user = true - WHERE id = ( - SELECT id FROM user_account WHERE username != '___implicit_user' ORDER BY id LIMIT 1 - ); - + UPDATE user_account SET is_initial_user = true WHERE id = ( SELECT + id FROM user_account WHERE username != '___implicit_user' ORDER BY id LIMIT 1 ); - - UPDATE user_account - SET account_type = 'MANAGED' - WHERE account_type = 'LDAP'; - + UPDATE user_account SET account_type = 'MANAGED' WHERE account_type + = 'LDAP'; - + - + - + - + - + - + - + - + - - - - + + + + - + - + - + - - - - + + + + - + - - + + - + - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - + - + - + - - - + + + - + - - - - - + + + + + - + - + - + - - - - - + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - SELECT CASE - WHEN EXISTS (SELECT 1 - FROM information_schema.columns - WHERE table_name = 'batch_job_execution_params' - AND column_name = 'parameter_value') - THEN 'true' - ELSE 'false' - END; - - - SELECT CASE - WHEN EXISTS (SELECT 1 - FROM information_schema.columns - WHERE table_name = 'batch_job_execution_params') - THEN 'true' - ELSE 'false' - END; - + SELECT CASE WHEN EXISTS (SELECT 1 FROM + information_schema.columns WHERE table_name = 'batch_job_execution_params' AND + column_name = 'parameter_value') THEN 'true' ELSE 'false' END; + SELECT CASE WHEN EXISTS (SELECT 1 FROM + information_schema.columns WHERE table_name = 'batch_job_execution_params') THEN + 'true' ELSE 'false' END; - - ALTER TABLE BATCH_STEP_EXECUTION ADD CREATE_TIME TIMESTAMP NOT NULL DEFAULT '1970-01-01 00:00:00'; - - ALTER TABLE BATCH_STEP_EXECUTION ALTER COLUMN START_TIME DROP NOT NULL; - - ALTER TABLE BATCH_JOB_EXECUTION_PARAMS DROP COLUMN DATE_VAL; - - ALTER TABLE BATCH_JOB_EXECUTION_PARAMS DROP COLUMN LONG_VAL; - - ALTER TABLE BATCH_JOB_EXECUTION_PARAMS DROP COLUMN DOUBLE_VAL; - - ALTER TABLE BATCH_JOB_EXECUTION_PARAMS ALTER COLUMN TYPE_CD TYPE VARCHAR(100); - - ALTER TABLE BATCH_JOB_EXECUTION_PARAMS RENAME TYPE_CD TO PARAMETER_TYPE; - - ALTER TABLE BATCH_JOB_EXECUTION_PARAMS ALTER COLUMN KEY_NAME TYPE VARCHAR(100); - - ALTER TABLE BATCH_JOB_EXECUTION_PARAMS RENAME KEY_NAME TO PARAMETER_NAME; - - ALTER TABLE BATCH_JOB_EXECUTION_PARAMS ALTER COLUMN STRING_VAL TYPE VARCHAR(2500); - - ALTER TABLE BATCH_JOB_EXECUTION_PARAMS RENAME STRING_VAL TO PARAMETER_VALUE; - + ALTER TABLE BATCH_STEP_EXECUTION ADD CREATE_TIME TIMESTAMP NOT NULL DEFAULT + '1970-01-01 00:00:00'; ALTER TABLE BATCH_STEP_EXECUTION ALTER COLUMN START_TIME DROP NOT + NULL; ALTER TABLE BATCH_JOB_EXECUTION_PARAMS DROP COLUMN DATE_VAL; ALTER TABLE + BATCH_JOB_EXECUTION_PARAMS DROP COLUMN LONG_VAL; ALTER TABLE BATCH_JOB_EXECUTION_PARAMS + DROP COLUMN DOUBLE_VAL; ALTER TABLE BATCH_JOB_EXECUTION_PARAMS ALTER COLUMN TYPE_CD TYPE + VARCHAR(100); ALTER TABLE BATCH_JOB_EXECUTION_PARAMS RENAME TYPE_CD TO PARAMETER_TYPE; + ALTER TABLE BATCH_JOB_EXECUTION_PARAMS ALTER COLUMN KEY_NAME TYPE VARCHAR(100); ALTER + TABLE BATCH_JOB_EXECUTION_PARAMS RENAME KEY_NAME TO PARAMETER_NAME; ALTER TABLE + BATCH_JOB_EXECUTION_PARAMS ALTER COLUMN STRING_VAL TYPE VARCHAR(2500); ALTER TABLE + BATCH_JOB_EXECUTION_PARAMS RENAME STRING_VAL TO PARAMETER_VALUE; - + - + - - CREATE INDEX project_deleted_at_null ON project((deleted_at IS NULL)); - CREATE INDEX organization_deleted_at_null ON organization((deleted_at IS NULL)); - + CREATE INDEX project_deleted_at_null ON project((deleted_at IS NULL)); CREATE INDEX + organization_deleted_at_null ON organization((deleted_at IS NULL)); - + - - CREATE INDEX import_deleted_at_null ON import((deleted_at IS NULL)); - + CREATE INDEX import_deleted_at_null ON import((deleted_at IS NULL)); - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - create unique index import_author_project_unique on import(author_id, project_id) where deleted_at is null - + + create unique index import_author_project_unique on import(author_id, project_id) + where deleted_at is null - + - + - - CREATE INDEX key_description_gin_trgm_index ON key_meta USING GIN (lower (f_unaccent(description)) gin_trgm_ops); - + CREATE INDEX key_description_gin_trgm_index ON key_meta USING GIN (lower + (f_unaccent(description)) gin_trgm_ops); - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - create unique index content_delivery_config_unique_slug on content_delivery_config(slug) where content_storage_id is null; - + + create unique index content_delivery_config_unique_slug on + content_delivery_config(slug) where content_storage_id is null; - + - + - + - + - + - - create index translation_comment_translation_id_state on translation_comment(translation_id, state); - + create index translation_comment_translation_id_state on + translation_comment(translation_id, state); - + - - CREATE OR REPLACE FUNCTION empty_json(j jsonb) RETURNS boolean AS ' - BEGIN - RETURN j = ''{}''::jsonb; - END; - ' LANGUAGE plpgsql IMMUTABLE; - - - CREATE INDEX empty_modifications ON activity_modified_entity (empty_json(modifications)); - + CREATE OR REPLACE FUNCTION empty_json(j jsonb) RETURNS boolean AS ' BEGIN RETURN j = + ''{}''::jsonb; END; ' LANGUAGE plpgsql IMMUTABLE; + CREATE INDEX empty_modifications ON activity_modified_entity + (empty_json(modifications)); - + - + - + - - - - - + + + + + - + - + - + - + - + - - - + + + - + - + - + - - - - - - - + + + + + + + - + - + - + - - - + + + - + - + - + - - - - + + + + - + - + - + - - - + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - create unique index language_project_name on language (project_id, name) where deleted_at is null; - create unique index language_tag_name on language (project_id, tag) where deleted_at is null; - + + + create unique index language_project_name on language (project_id, name) where + deleted_at is null; create unique index language_tag_name on language (project_id, tag) + where deleted_at is null; - + - + - + - + - + - - - - + + + + - + - - - - - + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - create index if not exists import_translation_language_id_id on import_translation (language_id); - + create index if not exists import_translation_language_id_id on import_translation + (language_id); - + - + - + - + - + - + - + - + - + - + - + - + - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - + + - + - + - + - + - + - + - + - + - + - - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/e2e/cypress/common/apiCalls/common.ts b/e2e/cypress/common/apiCalls/common.ts index 3524b5eb9c..940578b57c 100644 --- a/e2e/cypress/common/apiCalls/common.ts +++ b/e2e/cypress/common/apiCalls/common.ts @@ -1,7 +1,7 @@ import { API_URL, PASSWORD, USERNAME } from '../constants'; import { ArgumentTypes, Scope } from '../types'; import { components } from '../../../../webapp/src/service/apiSchema.generated'; -import bcrypt = require('bcryptjs'); +import * as bcrypt from 'bcryptjs'; import Chainable = Cypress.Chainable; type AccountType = @@ -381,6 +381,25 @@ export const getParsedEmailInvitationLink = () => emails[0].html.replace(/.*(http:\/\/[\w:/]*).*/gs, '$1') as string ); +export const getAgencyInvitationLinks = () => + getAllEmails().then((emails) => { + const email = emails[0].html as string; + const links = Array.from( + email.matchAll(/(http:\/\/[\w:/]*)/g), + (m) => m[0] + ); + const invitation = links.find((l) => l.includes('accept_invitation')); + const project = links.find( + (l) => l.includes('/projects/') && !l.includes('/task') + ); + const tasks = links.filter((l) => l.includes('/task')); + return { + invitation, + project, + tasks, + }; + }); + export const getAllEmails = () => cy.request('http://localhost:21080/api/emails').then((r) => r.body); export const deleteAllEmails = () => diff --git a/e2e/cypress/e2e/projects/projectMembers.cy.ts b/e2e/cypress/e2e/projects/projectMembers.cy.ts index 1682767d70..3e9ec54657 100644 --- a/e2e/cypress/e2e/projects/projectMembers.cy.ts +++ b/e2e/cypress/e2e/projects/projectMembers.cy.ts @@ -54,7 +54,7 @@ describe('Project members', () => { visitList(); enterProjectSettings('Facebook itself', 'Facebook'); selectInProjectMenu('Members'); - gcy('global-list-search').find('input').type('Doe'); + gcy('global-search-field').find('input').type('Doe'); gcy('project-member-item') .should('have.length', 1) @@ -117,13 +117,15 @@ describe('Project members', () => { describe('Modifying access', () => { let info: ProjectInfo; - beforeEach(async () => { - info = await visitProjectWithPermissions( + beforeEach(() => { + visitProjectWithPermissions( { scopes: ['activity.view'], }, 'admin@admin.com' - ); + ).then((infoData) => { + info = infoData; + }); }); it('selects Translate role for the user', () => { @@ -140,6 +142,7 @@ describe('Project members', () => { checkPermissions(info, { 'project-menu-item-dashboard': RUN, 'project-menu-item-translations': RUN, + 'project-menu-item-tasks': RUN, 'project-menu-item-import': RUN, 'project-menu-item-export': RUN, 'project-menu-item-integrate': RUN, @@ -164,6 +167,7 @@ describe('Project members', () => { checkPermissions(info, { 'project-menu-item-dashboard': RUN, 'project-menu-item-translations': RUN, + 'project-menu-item-tasks': RUN, 'project-menu-item-import': RUN, 'project-menu-item-export': RUN, 'project-menu-item-integrate': RUN, diff --git a/e2e/cypress/support/dataCyType.d.ts b/e2e/cypress/support/dataCyType.d.ts index c6253a5bd5..74b3ebf93a 100644 --- a/e2e/cypress/support/dataCyType.d.ts +++ b/e2e/cypress/support/dataCyType.d.ts @@ -32,6 +32,7 @@ declare namespace DataCy { "administration-debug-customer-exit-button" | "administration-ee-license-key-input" | "administration-ee-license-release-key-button" | + "administration-ee-plan-cancel-button" | "administration-ee-plan-field-feature" | "administration-ee-plan-field-free" | "administration-ee-plan-field-included-mt-credits" | @@ -48,6 +49,12 @@ declare namespace DataCy { "administration-ee-plans-item-delete" | "administration-ee-plans-item-edit" | "administration-ee-plans-item-public-badge" | + "administration-ee-translation-agencies-field-description" | + "administration-ee-translation-agencies-field-email" | + "administration-ee-translation-agencies-field-email-bcc" | + "administration-ee-translation-agencies-field-name" | + "administration-ee-translation-agencies-field-services" | + "administration-ee-translation-agencies-field-url" | "administration-frame" | "administration-organizations-list-item" | "administration-organizations-projects-button" | @@ -59,6 +66,9 @@ declare namespace DataCy { "administration-user-menu" | "administration-user-role-select" | "administration-users-list-item" | + "agency-label" | + "agency-select" | + "agency-select-item" | "ai-customization-project-description" | "ai-customization-project-description-add" | "ai-customization-project-description-edit" | @@ -256,6 +266,7 @@ declare namespace DataCy { "invitation-dialog-input-field" | "invitation-dialog-invite-button" | "invitation-dialog-role-button" | + "invitation-dialog-type-agency-button" | "invitation-dialog-type-email-button" | "invitation-dialog-type-link-button" | "invite-generate-button" | @@ -327,6 +338,10 @@ declare namespace DataCy { "namespaces-select-text-field" | "namespaces-selector" | "navigation-item" | + "order-translation-invitation-checkbox" | + "order-translation-next" | + "order-translation-sharing-details-consent-checkbox" | + "order-translation-submit" | "organization-address-part-field" | "organization-description-field" | "organization-invitation-cancel-button" | @@ -402,6 +417,7 @@ declare namespace DataCy { "project-list-translations-button" | "project-member-item" | "project-member-revoke-button" | + "project-members-invitation-item" | "project-menu-item" | "project-menu-item-dashboard" | "project-menu-item-developer" | @@ -520,6 +536,7 @@ declare namespace DataCy { "tasks-filter-menu" | "tasks-header-add-task" | "tasks-header-filter-select" | + "tasks-header-order-translation" | "tasks-header-show-closed" | "tasks-view-board-button" | "tasks-view-list-button" | @@ -528,6 +545,7 @@ declare namespace DataCy { "top-banner-content" | "top-banner-dismiss-button" | "transfer-project-apply-button" | + "translation-agency-item" | "translation-create-description-input" | "translation-create-key-input" | "translation-create-namespace-input" | diff --git a/e2e/cypress/tsconfig.json b/e2e/cypress/tsconfig.json index 475037681b..babe0be29b 100644 --- a/e2e/cypress/tsconfig.json +++ b/e2e/cypress/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "es2020", "lib": [ "es5", "dom", @@ -18,6 +18,7 @@ "skipLibCheck": true, "allowSyntheticDefaultImports": true, "downlevelIteration": true, + "moduleResolution": "Node" }, } diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 131e5263b9..787e8639ef 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -29,7 +29,7 @@ "eslint-plugin-react": "^7.30.1", "prettier": "^2.7.1", "totp-generator": "^0.0.13", - "typescript": "^4.7.4" + "typescript": "^5.3.2" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -4313,16 +4313,16 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/unbox-primitive": { diff --git a/e2e/package.json b/e2e/package.json index 95369de4f4..54fc34dc2c 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -29,7 +29,7 @@ "eslint-plugin-react": "^7.30.1", "prettier": "^2.7.1", "totp-generator": "^0.0.13", - "typescript": "^4.7.4" + "typescript": "^5.3.2" }, "dependencies": { "xlsx": "^0.18.5" diff --git a/ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/TaskController.kt b/ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/TaskController.kt index 5a27de2646..88176e08e1 100644 --- a/ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/TaskController.kt +++ b/ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/TaskController.kt @@ -9,16 +9,7 @@ import io.tolgee.constants.Feature import io.tolgee.dtos.request.userAccount.UserAccountPermissionsFilters import io.tolgee.ee.api.v2.hateoas.assemblers.TaskModelAssembler import io.tolgee.ee.api.v2.hateoas.assemblers.TaskPerUserReportModelAssembler -import io.tolgee.ee.data.task.CalculateScopeRequest -import io.tolgee.ee.data.task.CreateMultipleTasksRequest -import io.tolgee.ee.data.task.CreateTaskRequest -import io.tolgee.ee.data.task.TaskFilters -import io.tolgee.ee.data.task.TaskKeysResponse -import io.tolgee.ee.data.task.TranslationScopeFilters -import io.tolgee.ee.data.task.UpdateTaskKeyRequest -import io.tolgee.ee.data.task.UpdateTaskKeyResponse -import io.tolgee.ee.data.task.UpdateTaskKeysRequest -import io.tolgee.ee.data.task.UpdateTaskRequest +import io.tolgee.ee.data.task.* import io.tolgee.ee.service.TaskService import io.tolgee.hateoas.task.TaskModel import io.tolgee.hateoas.task.TaskPerUserReportModel @@ -163,9 +154,9 @@ class TaskController( @RequestBody @Valid dto: UpdateTaskRequest, ): TaskModel { - enabledFeaturesProvider.checkFeatureEnabled( + enabledFeaturesProvider.checkOneOfFeaturesEnabled( projectHolder.project.organizationOwnerId, - Feature.TASKS, + listOf(Feature.TASKS, Feature.ORDER_TRANSLATION), ) val task = taskService.updateTask(projectHolder.project.id, taskNumber, dto) @@ -213,9 +204,9 @@ class TaskController( @PathVariable taskNumber: Long, ): TaskModel { - enabledFeaturesProvider.checkFeatureEnabled( + enabledFeaturesProvider.checkOneOfFeaturesEnabled( projectHolder.project.organizationOwnerId, - Feature.TASKS, + listOf(Feature.TASKS, Feature.ORDER_TRANSLATION), ) val task = taskService.setTaskState(projectHolder.project.id, taskNumber, TaskState.IN_PROGRESS) return taskModelAssembler.toModel(task) diff --git a/ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/hateoas/assemblers/TaskModelAssembler.kt b/ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/hateoas/assemblers/TaskModelAssembler.kt index 7fbaba77f8..03fc28a457 100644 --- a/ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/hateoas/assemblers/TaskModelAssembler.kt +++ b/ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/hateoas/assemblers/TaskModelAssembler.kt @@ -2,6 +2,7 @@ package io.tolgee.ee.api.v2.hateoas.assemblers import io.tolgee.dtos.cacheable.LanguageDto import io.tolgee.ee.api.v2.controllers.TaskController +import io.tolgee.hateoas.TranslationAgencySimpleModelAssembler import io.tolgee.hateoas.language.LanguageModelAssembler import io.tolgee.hateoas.task.TaskModel import io.tolgee.hateoas.userAccount.SimpleUserAccountModelAssembler @@ -13,6 +14,7 @@ import org.springframework.stereotype.Component class TaskModelAssembler( private val simpleUserAccountModelAssembler: SimpleUserAccountModelAssembler, private val languageModelAssembler: LanguageModelAssembler, + private val translationAgencySimpleModelAssembler: TranslationAgencySimpleModelAssembler, ) : RepresentationModelAssemblerSupport( TaskController::class.java, TaskModel::class.java, @@ -42,6 +44,7 @@ class TaskModelAssembler( baseWordCount = entity.baseWordCount, baseCharacterCount = entity.baseCharacterCount, state = entity.state, + agency = entity.agency?.let { translationAgencySimpleModelAssembler.toModel(it) }, ) } } diff --git a/ee/backend/app/src/main/kotlin/io/tolgee/ee/data/task/CreateTranslationOrderRequest.kt b/ee/backend/app/src/main/kotlin/io/tolgee/ee/data/task/CreateTranslationOrderRequest.kt new file mode 100644 index 0000000000..71c5ba8642 --- /dev/null +++ b/ee/backend/app/src/main/kotlin/io/tolgee/ee/data/task/CreateTranslationOrderRequest.kt @@ -0,0 +1,7 @@ +package io.tolgee.ee.data.task + +class CreateTranslationOrderRequest( + var agencyId: Long, + var tasks: MutableSet = mutableSetOf(), + var sendReadOnlyInvitation: Boolean, +) 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 a64fcb5419..dd18382075 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 @@ -56,6 +56,11 @@ open class TaskFilters { ) var filterKey: List? = null + @field:Parameter( + description = """Filter tasks by agency""", + ) + var filterAgency: List? = null + @field:Parameter( description = """Exclude "done" tasks which are older than specified timestamp""", ) diff --git a/ee/backend/app/src/main/kotlin/io/tolgee/ee/data/translationAgency/CreateTranslationAgencyRequest.kt b/ee/backend/app/src/main/kotlin/io/tolgee/ee/data/translationAgency/CreateTranslationAgencyRequest.kt new file mode 100644 index 0000000000..582946487a --- /dev/null +++ b/ee/backend/app/src/main/kotlin/io/tolgee/ee/data/translationAgency/CreateTranslationAgencyRequest.kt @@ -0,0 +1,18 @@ +package io.tolgee.ee.data.translationAgency + +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.Size + +data class CreateTranslationAgencyRequest( + @field:NotBlank + @field:Size(min = 3, max = 255) + var name: String = "", + @field:Size(min = 0, max = 2000) + var description: String = "", + var services: List = emptyList(), + @field:Size(min = 0, max = 255) + var url: String = "", + @field:Size(min = 3, max = 255) + var email: String = "", + var emailBcc: List = emptyList(), +) diff --git a/ee/backend/app/src/main/kotlin/io/tolgee/ee/data/translationAgency/UpdateTranslationAgencyRequest.kt b/ee/backend/app/src/main/kotlin/io/tolgee/ee/data/translationAgency/UpdateTranslationAgencyRequest.kt new file mode 100644 index 0000000000..7346cae1b5 --- /dev/null +++ b/ee/backend/app/src/main/kotlin/io/tolgee/ee/data/translationAgency/UpdateTranslationAgencyRequest.kt @@ -0,0 +1,18 @@ +package io.tolgee.ee.data.translationAgency + +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.Size + +data class UpdateTranslationAgencyRequest( + @field:NotBlank + @field:Size(min = 3, max = 255) + var name: String = "", + @field:Size(min = 0, max = 2000) + var description: String = "", + var services: List = emptyList(), + @field:Size(min = 0, max = 255) + var url: String = "", + @field:Size(min = 3, max = 255) + var email: String = "", + var emailBcc: List = emptyList(), +) 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 dddf41e1b4..0270a38046 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 @@ -53,6 +53,10 @@ private const val TASK_FILTERS = """ :#{#filters.filterLanguage} is null or tk.language.id in :#{#filters.filterLanguage} ) + and ( + :#{#filters.filterAgency} is null + or tk.agency.id in :#{#filters.filterAgency} + ) and ( :#{#filters.filterAssignee} is null or exists ( @@ -376,4 +380,25 @@ interface TaskRepository : JpaRepository { projectId: Long, taskNumber: Long, ): Task? + + @Query( + """ + from Task t + where t.agency.id = :agencyId + """, + ) + fun getByAgencyId(agencyId: Long): List + + @Query( + """ + select t.agency_id + from Task t + where t.agency_id is not null + and t.project_id = :projectId + order by t.number desc + limit 1 + """, + nativeQuery = true, + ) + fun getLastTaskWithAgency(projectId: Long): Long? } diff --git a/ee/backend/app/src/main/kotlin/io/tolgee/ee/service/AssigneeNotificationService.kt b/ee/backend/app/src/main/kotlin/io/tolgee/ee/service/AssigneeNotificationService.kt index c4c986ac50..56cf668675 100644 --- a/ee/backend/app/src/main/kotlin/io/tolgee/ee/service/AssigneeNotificationService.kt +++ b/ee/backend/app/src/main/kotlin/io/tolgee/ee/service/AssigneeNotificationService.kt @@ -20,8 +20,8 @@ class AssigneeNotificationService( user: UserAccount, task: Task, ) { - val taskUrl = "${frontendUrlProvider.url}/projects/${task.project.id}/task?number=${task.number}&detail=true" - val myTasksUrl = "${frontendUrlProvider.url}/my-tasks" + val taskUrl = getTaskUrl(task.project.id, task.number) + val myTasksUrl = getMyTasksUrl() val params = EmailParams( @@ -58,4 +58,15 @@ class AssigneeNotificationService( "review" } } + + fun getTaskUrl( + projectId: Long, + taskId: Long, + ): String { + return "${frontendUrlProvider.url}/projects/$projectId/task?number=$taskId&detail=true" + } + + fun getMyTasksUrl(): String { + return "${frontendUrlProvider.url}/my-tasks" + } } diff --git a/ee/backend/app/src/main/kotlin/io/tolgee/ee/service/EePermissionService.kt b/ee/backend/app/src/main/kotlin/io/tolgee/ee/service/EePermissionService.kt index 6d26facb79..172b3c1161 100644 --- a/ee/backend/app/src/main/kotlin/io/tolgee/ee/service/EePermissionService.kt +++ b/ee/backend/app/src/main/kotlin/io/tolgee/ee/service/EePermissionService.kt @@ -7,8 +7,10 @@ import io.tolgee.exceptions.BadRequestException import io.tolgee.model.Invitation import io.tolgee.model.Permission import io.tolgee.model.enums.Scope +import io.tolgee.model.translationAgency.TranslationAgency import io.tolgee.service.organization.OrganizationService import io.tolgee.service.security.PermissionService +import jakarta.persistence.EntityManager import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -16,6 +18,7 @@ import org.springframework.transaction.annotation.Transactional class EePermissionService( private val permissionService: PermissionService, private val organizationService: OrganizationService, + private val entityManager: EntityManager, ) { @Transactional fun setUserDirectPermission( @@ -63,6 +66,10 @@ class EePermissionService( invitation = invitation, project = params.project, scopes = scopes.toTypedArray(), + agency = + params.agencyId?.let { + entityManager.getReference(TranslationAgency::class.java, it) + }, ) permissionService.setPermissionLanguages(permission, params.languagePermissions, params.project.id) 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 e8247d9743..d1c8f22d90 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 @@ -21,6 +21,7 @@ import io.tolgee.model.enums.TaskState import io.tolgee.model.enums.TaskType import io.tolgee.model.task.Task import io.tolgee.model.task.TaskKey +import io.tolgee.model.translationAgency.TranslationAgency import io.tolgee.model.views.KeysScopeView import io.tolgee.model.views.TaskPerUserReportView import io.tolgee.model.views.TaskWithScopeView @@ -31,6 +32,7 @@ import io.tolgee.service.ITaskService import io.tolgee.service.key.KeyService import io.tolgee.service.language.LanguageService import io.tolgee.service.project.ProjectService +import io.tolgee.service.security.PermissionService import io.tolgee.service.security.SecurityService import io.tolgee.util.executeInNewRepeatableTransaction import jakarta.persistence.EntityManager @@ -65,6 +67,7 @@ class TaskService( private val currentDateProvider: CurrentDateProvider, private val keyService: KeyService, private val projectService: ProjectService, + private val permissionService: PermissionService, ) : ITaskService { fun getAllPaged( projectId: Long, @@ -111,8 +114,9 @@ class TaskService( projectId: Long, dto: CreateTaskRequest, filters: TranslationScopeFilters, + agencyId: Long? = null, ): TaskWithScopeView { - val task = taskService.createSingleTask(projectId, dto, filters) + val task = taskService.createSingleTask(projectId, dto, filters, agencyId) val prefetched = taskService.getPrefetchedTasks(listOf(task)).first() return getTaskWithScope(prefetched) } @@ -132,13 +136,14 @@ class TaskService( projectId: Long, dto: CreateTaskRequest, filters: TranslationScopeFilters, + agencyId: Long? = null, ): Task { var lastErr = DataIntegrityViolationException("Error") repeat(10) { // necessary for proper transaction creation try { return executeInNewRepeatableTransaction(platformTransactionManager) { - val task = taskService.createTaskInTransaction(projectId, dto, filters) + val task = taskService.createTaskInTransaction(projectId, dto, filters, agencyId) entityManager.flush() task.assignees.forEach { assigneeNotificationService.notifyNewAssignee(it, task) @@ -157,6 +162,7 @@ class TaskService( projectId: Long, dto: CreateTaskRequest, filters: TranslationScopeFilters, + agencyId: Long? = null, ): Task { val newNumber = getNextTaskNumber(projectId) val language = checkLanguage(dto.languageId!!, projectId) @@ -172,6 +178,8 @@ class TaskService( val task = Task() + val authorId = authenticationFacade.authenticatedUser.id + task.number = newNumber task.project = entityManager.getReference(Project::class.java, projectId) task.name = dto.name @@ -180,7 +188,10 @@ class TaskService( task.dueDate = dto.dueDate?.let { Date(it) } task.language = language task.assignees = assignees - task.author = entityManager.getReference(UserAccount::class.java, authenticationFacade.authenticatedUser.id) + task.author = entityManager.getReference(UserAccount::class.java, authorId) + task.agency = agencyId?.let { + entityManager.getReference(TranslationAgency::class.java, it) + } ?: permissionService.findPermissionNonCached(projectId, authorId)?.agency task.state = TaskState.NEW taskRepository.saveAndFlush(task) val keys = keyService.getByIds(keyIds) @@ -478,6 +489,7 @@ class TaskService( doneItems = scope.doneItems, baseWordCount = scope.baseWordCount, baseCharacterCount = scope.baseCharacterCount, + agency = task.agency, ) } } @@ -535,4 +547,8 @@ class TaskService( } return null } + + override fun getAgencyTasks(agencyId: Long): List { + return taskRepository.getByAgencyId(agencyId) + } } diff --git a/ee/backend/tests/src/test/kotlin/io/tolgee/ee/api/v2/controllers/task/TaskControllerPermissionsTest.kt b/ee/backend/tests/src/test/kotlin/io/tolgee/ee/api/v2/controllers/task/TaskControllerPermissionsTest.kt index fa7970994e..d3750d7c91 100644 --- a/ee/backend/tests/src/test/kotlin/io/tolgee/ee/api/v2/controllers/task/TaskControllerPermissionsTest.kt +++ b/ee/backend/tests/src/test/kotlin/io/tolgee/ee/api/v2/controllers/task/TaskControllerPermissionsTest.kt @@ -89,28 +89,4 @@ class TaskControllerPermissionsTest : ProjectAuthControllerTest("/v2/projects/") "tasks/${testData.translateTask.self.number}/reopen", ).andIsForbidden } - - @Test - @ProjectJWTAuthTestMethod - fun `can't access unassigned review task's data`() { - userAccount = testData.projectUser.self - - performProjectAuthGet("tasks/${testData.reviewTask.self.number}").andIsForbidden - performProjectAuthGet("tasks/${testData.reviewTask.self.number}/per-user-report").andIsForbidden - performProjectAuthGet("tasks/${testData.reviewTask.self.number}/xlsx-report").andIsForbidden - performProjectAuthGet("tasks/${testData.reviewTask.self.number}/keys").andIsForbidden - performProjectAuthPut( - "tasks/${testData.reviewTask.self.number}/keys/${testData.keysInTask.first().self.id}", - UpdateTaskKeyRequest(done = true), - ).andIsForbidden - performProjectAuthPut("tasks/${testData.reviewTask.self.number}/finish").andIsForbidden - performProjectAuthPut( - "tasks/${testData.reviewTask.self.number}/keys", - UpdateTaskKeysRequest(addKeys = mutableSetOf(testData.keysOutOfTask.first().self.id)), - ).andIsForbidden - performProjectAuthPut( - "tasks/${testData.reviewTask.self.number}", - UpdateTaskRequest(name = "Test"), - ).andIsForbidden - } } diff --git a/webapp/src/component/PermissionsSettings/PermissionsBasic.tsx b/webapp/src/component/PermissionsSettings/PermissionsBasic.tsx index fe46e31712..1861ac5c18 100644 --- a/webapp/src/component/PermissionsSettings/PermissionsBasic.tsx +++ b/webapp/src/component/PermissionsSettings/PermissionsBasic.tsx @@ -14,6 +14,7 @@ type Props = { roles: RolesMap; allLangs?: LanguageModel[]; hideNone?: boolean; + disabled?: boolean; }; export const PermissionsBasic: React.FC = ({ @@ -22,6 +23,7 @@ export const PermissionsBasic: React.FC = ({ roles, allLangs, hideNone, + disabled, }) => { const rolesList = Object.keys(roles).filter( (role) => role !== 'NONE' || !hideNone @@ -38,6 +40,7 @@ export const PermissionsBasic: React.FC = ({ scopes={roles[role]} onChange={onChange} allLangs={allLangs} + disabled={disabled} /> ); })} diff --git a/webapp/src/component/PermissionsSettings/PermissionsRole.tsx b/webapp/src/component/PermissionsSettings/PermissionsRole.tsx index 3755215c25..d92bd81505 100644 --- a/webapp/src/component/PermissionsSettings/PermissionsRole.tsx +++ b/webapp/src/component/PermissionsSettings/PermissionsRole.tsx @@ -30,6 +30,12 @@ const StyledListItem = styled('div')` background-color: ${({ theme }) => theme.palette.emphasis[50]}; border-color: ${({ theme }) => theme.palette.divider1}; } + + &.disabled { + cursor: default; + color: ${({ theme }) => theme.palette.text.disabled}; + background-color: unset; + } `; const StyledTypography = styled(Typography)` @@ -39,6 +45,10 @@ const StyledTypography = styled(Typography)` &.selected { color: ${({ theme }) => theme.palette.primary.main}; } + + &.disabled { + color: ${({ theme }) => theme.palette.text.disabled}; + } `; type Props = { @@ -47,6 +57,7 @@ type Props = { scopes: PermissionModelScope[]; onChange: (value: PermissionBasicState) => void; allLangs?: LanguageModel[]; + disabled?: boolean; }; export const PermissionsRole: React.FC = ({ @@ -55,9 +66,10 @@ export const PermissionsRole: React.FC = ({ onChange, scopes, allLangs, + disabled, }) => { const handleSelect = () => { - if (role !== state.role) { + if (role !== state.role && !disabled) { onChange({ ...state, role, @@ -71,9 +83,12 @@ export const PermissionsRole: React.FC = ({ ); return ( - + - + {getRoleTranslation(role)} {getRoleHint(role)} @@ -87,6 +102,7 @@ export const PermissionsRole: React.FC = ({ state={state} onChange={onChange} allLangs={allLangs} + disabled={disabled} /> )} diff --git a/webapp/src/component/PermissionsSettings/PermissionsSettings.tsx b/webapp/src/component/PermissionsSettings/PermissionsSettings.tsx index daf988ef76..d642c7775a 100644 --- a/webapp/src/component/PermissionsSettings/PermissionsSettings.tsx +++ b/webapp/src/component/PermissionsSettings/PermissionsSettings.tsx @@ -25,6 +25,7 @@ type Props = { height?: number; allLangs?: LanguageModel[]; hideNone?: boolean; + disabled?: boolean; }; export const PermissionsSettings: React.FC = ({ @@ -33,6 +34,7 @@ export const PermissionsSettings: React.FC = ({ onChange, allLangs, hideNone, + disabled, }) => { const [tab, setTab] = useState( permissions.type ? 'basic' : 'advanced' @@ -104,7 +106,7 @@ export const PermissionsSettings: React.FC = ({ mb={2} > {title} - + + + {t('global_form_save')} + + + + )} + + ); +} diff --git a/webapp/src/ee/billing/administration/translationAgencies/TAProfileAvatar.tsx b/webapp/src/ee/billing/administration/translationAgencies/TAProfileAvatar.tsx new file mode 100644 index 0000000000..3e1fbcb095 --- /dev/null +++ b/webapp/src/ee/billing/administration/translationAgencies/TAProfileAvatar.tsx @@ -0,0 +1,90 @@ +import { Box } from '@mui/material'; +import { ProfileAvatar } from 'tg.component/common/avatar/ProfileAvatar'; +import { components } from 'tg.service/billingApiSchema.generated'; +import { useBillingApiMutation } from 'tg.service/http/useQueryApi'; + +type TranslationAgencyModel = components['schemas']['TranslationAgencyModel']; + +type Props = { + agency: TranslationAgencyModel; +}; + +export const TAProfileAvatar = ({ agency }: Props) => { + const uploadLoadable = useBillingApiMutation({ + url: '/v2/administration/billing/translation-agency/{agencyId}/avatar', + method: 'put', + invalidatePrefix: [ + '/v2/billing/translation-agency', + '/v2/administration/billing/translation-agency', + ], + }); + + const removeLoadable = useBillingApiMutation({ + url: '/v2/administration/billing/translation-agency/{agencyId}/avatar', + method: 'delete', + invalidatePrefix: [ + '/v2/billing/translation-agency', + '/v2/administration/billing/translation-agency', + ], + }); + + return ( + { + return props.avatar?.large ? ( + + ) : ( + +
No avatar
+
+ ); + }} + onUpload={(blob: Blob) => + uploadLoadable.mutateAsync({ + path: { + agencyId: agency.id, + }, + content: { + 'multipart/form-data': { + avatar: new File([blob], 'Avatar', { type: 'image/png' }) as any, + }, + }, + }) + } + onRemove={() => + removeLoadable.mutateAsync({ + path: { + agencyId: agency.id, + }, + }) + } + /> + ); +}; diff --git a/webapp/src/ee/orderTranslations/AgencyLabel.tsx b/webapp/src/ee/orderTranslations/AgencyLabel.tsx new file mode 100644 index 0000000000..6ee888c623 --- /dev/null +++ b/webapp/src/ee/orderTranslations/AgencyLabel.tsx @@ -0,0 +1,26 @@ +import { Box, styled } from '@mui/material'; +import { components } from 'tg.service/apiSchema.generated'; + +type TranslationAgencySimpleModel = + components['schemas']['TranslationAgencySimpleModel']; + +const StyledAgencyName = styled(Box)` + font-size: 16px; + font-weight: 500; +`; + +type Props = { + agency: TranslationAgencySimpleModel; +}; + +export const AgencyLabel = ({ agency }: Props) => { + return ( + + {agency.avatar ? ( + {agency.name} + ) : ( + {agency.name} + )} + + ); +}; diff --git a/webapp/src/ee/orderTranslations/OrderTranslationsDialog.tsx b/webapp/src/ee/orderTranslations/OrderTranslationsDialog.tsx new file mode 100644 index 0000000000..65b5bb81a3 --- /dev/null +++ b/webapp/src/ee/orderTranslations/OrderTranslationsDialog.tsx @@ -0,0 +1,407 @@ +import { + Box, + Button, + Checkbox, + Dialog, + DialogTitle, + FormControlLabel, + Step, + StepContent, + StepLabel, + Stepper, + styled, + Typography, + useTheme, +} from '@mui/material'; +import { T, useTranslate } from '@tolgee/react'; +import { Formik } from 'formik'; +import { useState } from 'react'; + +import { Validation } from 'tg.constants/GlobalValidationSchema'; +import { components } from 'tg.service/apiSchema.generated'; +import { + useApiQuery, + useBillingApiMutation, + useBillingApiQuery, +} from 'tg.service/http/useQueryApi'; +import { messageService } from 'tg.service/MessageService'; +import LoadingButton from 'tg.component/common/form/LoadingButton'; +import { FiltersType } from 'tg.component/translation/translationFilters/tools'; +import { User } from 'tg.component/UserAccount'; +import { TaskCreateForm } from 'tg.ee/task/components/taskCreate/TaskCreateForm'; +import { TranslationStateType } from 'tg.ee/task/components/taskCreate/TranslationStateFilter'; +import { TranslationAgency } from './TranslationAgency'; +import { BoxLoading } from 'tg.component/common/BoxLoading'; +import { useEnabledFeatures, useUser } from 'tg.globalContext/helpers'; +import { PaidFeatureBanner } from 'tg.ee/common/PaidFeatureBanner'; +import clsx from 'clsx'; + +type CreateTaskRequest = components['schemas']['CreateTaskRequest']; +type TaskType = CreateTaskRequest['type']; +type LanguageModel = components['schemas']['LanguageModel']; + +const StyledMainTitle = styled(DialogTitle)` + padding-bottom: 0px; +`; + +const StyledSubtitle = styled('div')` + padding: ${({ theme }) => theme.spacing(0, 3, 2, 3)}; + color: ${({ theme }) => theme.palette.text.secondary}; +`; + +const StyledContainer = styled('div')` + display: grid; + padding: ${({ theme }) => theme.spacing(3)}; + gap: ${({ theme }) => theme.spacing(0.5, 3)}; + padding-top: ${({ theme }) => theme.spacing(1)}; + width: min(calc(100vw - 64px), 1000px); +`; + +const StyledActions = styled('div')` + display: flex; + gap: 8px; + padding-top: 24px; + justify-content: end; +`; + +const StyledStepLabel = styled(StepLabel)` + &.clickable { + cursor: pointer; + } +`; + +const StyledStepContent = styled(Box)` + display: grid; + padding-top: ${({ theme }) => theme.spacing(1)}; + gap: ${({ theme }) => theme.spacing(0.5, 3)}; +`; + +export type InitialValues = { + type: TaskType; + name: string; + description: string; + languages: number[]; + dueDate: number; + languageAssignees: Record; + selection: number[]; +}; + +type Props = { + open: boolean; + onClose: () => void; + onFinished: () => void; + projectId: number; + allLanguages: LanguageModel[]; + initialValues?: Partial; +}; + +export const OrderTranslationsDialog = ({ + open, + onClose, + onFinished, + projectId, + allLanguages, + initialValues, +}: Props) => { + const theme = useTheme(); + const { features } = useEnabledFeatures(); + const taskFeature = features.includes('ORDER_TRANSLATION'); + const disabled = !taskFeature; + const { t } = useTranslate(); + const user = useUser(); + + const createTasksLoadable = useBillingApiMutation({ + url: '/v2/projects/{projectId}/billing/order-translation', + method: 'post', + invalidatePrefix: ['/v2/projects/{projectId}/tasks', '/v2/user-tasks'], + }); + + const [filters, setFilters] = useState({}); + const [stateFilters, setStateFilters] = useState([]); + const [languages, setLanguages] = useState(initialValues?.languages ?? []); + + const [_step, setStep] = useState(undefined); + + const step = disabled ? -1 : _step; + + const preferredAgencyLoadable = useBillingApiQuery({ + url: '/v2/projects/{projectId}/billing/order-translation/preferred-agency', + method: 'get', + path: { + projectId, + }, + options: { + onSuccess(data) { + setStep((step) => (data.preferredAgencyId !== null ? 1 : 0)); + }, + }, + }); + + const agencyAlreadyContacted = + preferredAgencyLoadable.data?.preferredAgencyId !== undefined; + + const agenciesLoadable = useBillingApiQuery({ + url: '/v2/billing/translation-agency', + method: 'get', + query: { + size: 1000, + }, + }); + + const selectedLoadable = useApiQuery({ + url: '/v2/projects/{projectId}/translations/select-all', + method: 'get', + path: { projectId }, + query: { + ...filters, + languages: allLanguages.map((l) => l.tag), + }, + options: { + enabled: !initialValues?.selection, + }, + }); + + const selectedKeys = + initialValues?.selection ?? selectedLoadable.data?.ids ?? []; + + const isLoading = + preferredAgencyLoadable.isLoading || + agenciesLoadable.isLoading || + step === undefined; + + return ( + + {!taskFeature && ( + + )} + + + + + + + + {isLoading ? ( + + ) : ( + { + const data = languages.map( + (languageId) => + ({ + type: values.type, + name: values.name, + description: values.description, + languageId: languageId, + dueDate: values.dueDate, + assignees: + values.assignees[languageId]?.map((u) => u.id) ?? [], + keys: selectedKeys, + } satisfies CreateTaskRequest) + ); + createTasksLoadable.mutate( + { + path: { projectId }, + query: { + filterState: stateFilters.filter((i) => i !== 'OUTDATED'), + filterOutdated: stateFilters.includes('OUTDATED'), + }, + content: { + 'application/json': { + tasks: data, + agencyId: values.agencyId!, + sendReadOnlyInvitation: values.agreeInvite, + }, + }, + }, + { + onSuccess() { + messageService.success( + + ); + onFinished(); + }, + } + ); + }} + > + {({ submitForm, values, setFieldValue }) => { + const selectedAgency = + agenciesLoadable.data?._embedded?.translationAgencies?.find( + (a) => a.id === values.agencyId + ); + return ( + + + + setStep(0)} + role="button" + > + + + + + {selectedAgency && + (selectedAgency.avatar ? ( + {selectedAgency.name} + ) : ( + {`(${selectedAgency.name})`} + ))} + + + + + + + setFieldValue( + 'agreeSharing', + !values.agreeSharing + ) + } + control={} + /> + + + + + {agenciesLoadable.data?._embedded?.translationAgencies?.map( + (agency, i) => ( + setFieldValue('agencyId', id)} + /> + ) + )} + + + + + + + + + + + + + + + setFieldValue('agreeInvite', !values.agreeInvite) + } + control={} + /> + {!values.agreeInvite && !agencyAlreadyContacted && ( + + + + )} + + + + + + + + {step === 0 ? ( + + ) : ( + + {t('order_translation_submit_button')} + + )} + + + ); + }} + + )} + + ); +}; diff --git a/webapp/src/ee/orderTranslations/ProviderDescription.tsx b/webapp/src/ee/orderTranslations/ProviderDescription.tsx new file mode 100644 index 0000000000..19b3b4a0aa --- /dev/null +++ b/webapp/src/ee/orderTranslations/ProviderDescription.tsx @@ -0,0 +1,28 @@ +import { Box, Link as MuiLink } from '@mui/material'; +import ReactMarkdown from 'react-markdown'; + +type Props = { + description: string; +}; + +export const ProviderDescription: React.FC = ({ description }) => { + return ( + + ( + + {props.children} + + ), + }} + > + {description} + + + ); +}; diff --git a/webapp/src/ee/orderTranslations/TranslationAgency.tsx b/webapp/src/ee/orderTranslations/TranslationAgency.tsx new file mode 100644 index 0000000000..df0da5d1eb --- /dev/null +++ b/webapp/src/ee/orderTranslations/TranslationAgency.tsx @@ -0,0 +1,94 @@ +import { Box, Link, styled } from '@mui/material'; +import { ProviderDescription } from './ProviderDescription'; +import clsx from 'clsx'; +import { LinkExternal01 } from '@untitled-ui/icons-react'; +import { isValidHttpUrl } from 'tg.fixtures/isValidUrl'; +import { components } from 'tg.service/billingApiSchema.generated'; + +type TranslationAgencyPublicModel = + components['schemas']['TranslationAgencyPublicModel']; + +const StyledLinkExternal01 = styled(LinkExternal01)` + margin-left: 3px; + position: relative; + top: 2px; +`; + +const StyledContainer = styled(Box)` + border: 1px solid ${({ theme }) => theme.palette.tokens.border.soft}; + gap: 20px; + display: grid; + border-radius: 16px; + background: ${({ theme }) => theme.palette.tokens.background['paper-2']}; + padding: 20px; + cursor: pointer; + transition: box-shadow ease-in-out 0.2s, border-color ease-in-out 0.2s; + &.selected { + border-color: ${({ theme }) => theme.palette.primary.main}; + cursor: unset; + box-shadow: 0px 0px 17px 0px + ${({ theme }) => theme.palette.primary.main + '55'}; + } +`; + +const StyledServices = styled(Box)` + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); +`; + +const StyledDescription = styled(Box)` + background: ${({ theme }) => theme.palette.tokens.background['paper-3']}; + padding: 0px 16px; + border-radius: 16px; +`; + +type Props = { + agency: TranslationAgencyPublicModel; + selected: boolean; + onSelect: (id: number) => void; +}; + +export const TranslationAgency = ({ agency, selected, onSelect }: Props) => { + const url = + agency.url && isValidHttpUrl(agency.url) ? new URL(agency.url) : undefined; + return ( + onSelect(agency.id)} + data-cy="translation-agency-item" + > + + {agency.avatar ? ( + {agency.name} + ) : ( +

{agency.name}

+ )} + + {url && ( + + {url.host} + + + )} + +
+ {Boolean(agency.services.length) && ( + + {agency.services.map((item) => ( + {item} + ))} + + )} + {agency.description && ( + + + + )} +
+ ); +}; diff --git a/webapp/src/ee/task/components/TaskDetail.tsx b/webapp/src/ee/task/components/TaskDetail.tsx index c5ae855a26..d118e92cce 100644 --- a/webapp/src/ee/task/components/TaskDetail.tsx +++ b/webapp/src/ee/task/components/TaskDetail.tsx @@ -31,6 +31,7 @@ import { TaskScope } from './TaskScope'; import { getTaskRedirect } from './utils'; import { TaskMenu } from './TaskMenu'; import { BoxLoading } from 'tg.component/common/BoxLoading'; +import { useEnabledFeatures } from 'tg.globalContext/helpers'; type TaskModel = components['schemas']['TaskModel']; @@ -93,6 +94,7 @@ export const TaskDetail = ({ onClose, projectId, taskNumber, task }: Props) => { const { t } = useTranslate(); const formatDate = useDateFormatter(); const [anchorEl, setAnchorEl] = useState(null); + const { isEnabled } = useEnabledFeatures(); const taskLoadable = useApiQuery({ url: '/v2/projects/{projectId}/tasks/{taskNumber}', @@ -125,7 +127,9 @@ export const TaskDetail = ({ onClose, projectId, taskNumber, task }: Props) => { const scopes = projectLoadable.data?.computedPermission.scopes ?? []; const project = projectLoadable.data; - const canEditTask = scopes.includes('tasks.edit'); + const canEditTask = + scopes.includes('tasks.edit') && + (isEnabled('TASKS') || isEnabled('ORDER_TRANSLATION')); const handleClose = () => { setAnchorEl(null); @@ -276,6 +280,7 @@ export const TaskDetail = ({ onClose, projectId, taskNumber, task }: Props) => { display="grid" gridTemplateColumns="repeat(4, 1fr)" pt="20px" + gap={1} > )} {!hideType && } + {task.agency && } ); }; diff --git a/webapp/src/ee/task/components/TasksBoard.tsx b/webapp/src/ee/task/components/TasksBoard.tsx index d4872e8ab1..597980b04f 100644 --- a/webapp/src/ee/task/components/TasksBoard.tsx +++ b/webapp/src/ee/task/components/TasksBoard.tsx @@ -9,6 +9,8 @@ import { useTaskStateTranslation } from 'tg.translationTools/useTaskStateTransla import { useProjectBoardTasks } from '../views/projectTasks/useProjectBoardTasks'; import { useStateColor } from './TaskState'; import { BoardColumn } from './BoardColumn'; +import { PaidFeatureBanner } from 'tg.ee/common/PaidFeatureBanner'; +import { useEnabledFeatures } from 'tg.globalContext/helpers'; type TaskModel = components['schemas']['TaskModel']; type SimpleProjectModel = components['schemas']['SimpleProjectModel']; @@ -50,6 +52,9 @@ export const TasksBoard = ({ const { t } = useTranslate(); const translateState = useTaskStateTranslation(); const stateColor = useStateColor(); + const { isEnabled } = useEnabledFeatures(); + + const tasksFeature = isEnabled('TASKS'); const canFetchMore = newTasks.hasNextPage || @@ -61,11 +66,10 @@ export const TasksBoard = ({ inProgressTasks.hasNextPage && inProgressTasks.fetchNextPage(); doneTasks.hasNextPage && doneTasks.fetchNextPage(); } + const loadables = [newTasks, inProgressTasks, doneTasks]; - const isLoading = - newTasks.isLoading || inProgressTasks.isLoading || doneTasks.isLoading; - const isFetching = - newTasks.isFetching || inProgressTasks.isFetching || doneTasks.isFetching; + const isLoading = loadables.some((l) => l.isLoading); + const isFetching = loadables.some((l) => l.isFetching); if (isLoading) { return ( @@ -75,6 +79,19 @@ export const TasksBoard = ({ ); } + const allReady = loadables.every((l) => l.isFetched); + const allEmpty = loadables.every( + (l) => l.data?.pages?.[0].page?.totalElements === 0 + ); + + if (allReady && allEmpty && !tasksFeature) { + return ( + + + + ); + } + return ( diff --git a/webapp/src/ee/task/components/taskCreate/TaskCreateDialog.tsx b/webapp/src/ee/task/components/taskCreate/TaskCreateDialog.tsx index 52b7ee205d..ca2c5c130b 100644 --- a/webapp/src/ee/task/components/taskCreate/TaskCreateDialog.tsx +++ b/webapp/src/ee/task/components/taskCreate/TaskCreateDialog.tsx @@ -1,14 +1,4 @@ -import { - Box, - Button, - Checkbox, - Dialog, - DialogTitle, - ListItemText, - MenuItem, - styled, - Typography, -} from '@mui/material'; +import { Button, Dialog, DialogTitle, styled } from '@mui/material'; import { T, useTranslate } from '@tolgee/react'; import { Formik } from 'formik'; import { useState } from 'react'; @@ -17,27 +7,18 @@ import { Validation } from 'tg.constants/GlobalValidationSchema'; import { components } from 'tg.service/apiSchema.generated'; import { useApiMutation, useApiQuery } from 'tg.service/http/useQueryApi'; import { messageService } from 'tg.service/MessageService'; -import { useTaskTypeTranslation } from 'tg.translationTools/useTaskTranslation'; import LoadingButton from 'tg.component/common/form/LoadingButton'; -import { Select as FormSelect } from 'tg.component/common/form/fields/Select'; -import { TextField } from 'tg.component/common/form/fields/TextField'; import { FiltersType } from 'tg.component/translation/translationFilters/tools'; -import { TranslationFilters } from 'tg.component/translation/translationFilters/TranslationFilters'; -import { Select } from 'tg.component/common/Select'; import { User } from 'tg.component/UserAccount'; -import { TaskDatePicker } from '../TaskDatePicker'; -import { TaskPreview } from './TaskPreview'; -import { - TranslationStateFilter, - TranslationStateType, -} from './TranslationStateFilter'; +import { TranslationStateType } from './TranslationStateFilter'; +import { useEnabledFeatures } from 'tg.globalContext/helpers'; +import { PaidFeatureBanner } from 'tg.ee/common/PaidFeatureBanner'; +import { TaskCreateForm } from './TaskCreateForm'; type TaskType = components['schemas']['TaskModel']['type']; type LanguageModel = components['schemas']['LanguageModel']; -const TASK_TYPES: TaskType[] = ['TRANSLATE', 'REVIEW']; - const StyledMainTitle = styled(DialogTitle)` padding-bottom: 0px; `; @@ -55,26 +36,6 @@ const StyledContainer = styled('div')` width: min(calc(100vw - 64px), 800px); `; -const StyledTopPart = styled(Box)` - display: grid; - gap: ${({ theme }) => theme.spacing(0.5, 2)}; - grid-template-columns: 3fr 5fr; - align-items: start; - ${({ theme }) => theme.breakpoints.down('sm')} { - grid-template-columns: 1fr; - } -`; - -const StyledFilters = styled(Box)` - display: grid; - gap: ${({ theme }) => theme.spacing(0.5, 2)}; - grid-template-columns: 3fr 3fr 2fr; - ${({ theme }) => theme.breakpoints.down('sm')} { - grid-template-columns: 1fr; - gap: ${({ theme }) => theme.spacing(2)}; - } -`; - const StyledActions = styled('div')` display: flex; gap: 8px; @@ -111,8 +72,6 @@ export const TaskCreateDialog = ({ }: Props) => { const { t } = useTranslate(); - const translateTaskType = useTaskTypeTranslation(); - const createTasksLoadable = useApiMutation({ url: '/v2/projects/{projectId}/tasks/create-multiple-tasks', method: 'post', @@ -122,6 +81,9 @@ export const TaskCreateDialog = ({ const [filters, setFilters] = useState({}); const [stateFilters, setStateFilters] = useState([]); const [languages, setLanguages] = useState(initialValues?.languages ?? []); + const { features } = useEnabledFeatures(); + + const taskFeature = features.includes('TASKS'); const selectedLoadable = useApiQuery({ url: '/v2/projects/{projectId}/translations/select-all', @@ -139,8 +101,13 @@ export const TaskCreateDialog = ({ const selectedKeys = initialValues?.selection ?? selectedLoadable.data?.ids ?? []; + const disabled = !taskFeature; + return ( + {!taskFeature && ( + + )} @@ -195,130 +162,25 @@ export const TaskCreateDialog = ({ ); }} > - {({ values, setFieldValue, submitForm }) => { + {({ submitForm }) => { return ( - - translateTaskType(v)} - fullWidth - data-cy="create-task-field-type" - > - {TASK_TYPES.map((v) => ( - - {translateTaskType(v)} - - ))} - - - - setFieldValue('dueDate', value)} - /> - - - - - {t('create_task_tasks_and_assignees_title')} - - - {!initialValues?.selection && ( - - languages.includes(l.id) - )} - placeholder={t('create_task_filter_keys_placeholder')} - filterOptions={{ keyRelatedOnly: true }} - sx={{ width: '100%', maxWidth: '270px' }} - /> - )} - - - - {allLanguages && ( - - {languages?.map((language) => ( - l.id === language)!} - type={values.type} - keys={selectedKeys} - assigness={values.assignees[language] ?? []} - onUpdateAssignees={(users) => { - setFieldValue(`assignees[${language}]`, users); - }} - filters={stateFilters} - projectId={projectId} - /> - ))} - - )} theme.spacing(0.5, 2)}; + grid-template-columns: 3fr 5fr; + align-items: start; + ${({ theme }) => theme.breakpoints.down('sm')} { + grid-template-columns: 1fr; + } +`; + +const StyledFilters = styled(Box)` + display: grid; + gap: ${({ theme }) => theme.spacing(0.5, 2)}; + grid-template-columns: 250px 250px 2fr; + ${({ theme }) => theme.breakpoints.down('sm')} { + grid-template-columns: 1fr; + gap: ${({ theme }) => theme.spacing(2)}; + } +`; + +type Props = { + selectedKeys: number[]; + disabled?: boolean; + languages: number[]; + setLanguages: (languages: number[]) => void; + allLanguages: LanguageModel[]; + filters: FiltersType; + setFilters?: (filters: FiltersType) => void; + stateFilters: TranslationStateType[]; + setStateFilters: (filters: TranslationStateType[]) => void; + projectId: number; + hideDueDate?: boolean; + hideAssignees?: boolean; +}; + +export const TaskCreateForm = ({ + selectedKeys, + disabled, + languages, + setLanguages, + allLanguages, + filters, + setFilters, + stateFilters, + setStateFilters, + projectId, + hideDueDate, + hideAssignees, +}: Props) => { + const { t } = useTranslate(); + const translateTaskType = useTaskTypeTranslation(); + + const { values, setFieldValue } = useFormikContext(); + + return ( + <> + + translateTaskType(v)} + fullWidth + data-cy="create-task-field-type" + disabled={disabled} + > + {TASK_TYPES.map((v) => ( + + {translateTaskType(v)} + + ))} + + + + {!hideDueDate && ( + + {(field, form) => ( + form.setFieldValue(field.name, value)} + /> + )} + + )} + + + + {!disabled && ( + <> + + {hideAssignees + ? t('create_task_tasks_summary') + : t('create_task_tasks_and_assignees_title')} + + + {setFilters && ( + + languages.includes(l.id) + )} + placeholder={t('create_task_filter_keys_placeholder')} + filterOptions={{ keyRelatedOnly: true }} + sx={{ width: '100%', maxWidth: '270px' }} + /> + )} + + + + {allLanguages && ( + + {languages?.map((language) => ( + l.id === language)!} + type={values.type} + keys={selectedKeys} + assigness={values.assignees[language] ?? []} + onUpdateAssignees={(users) => { + setFieldValue(`assignees[${language}]`, users); + }} + filters={stateFilters} + projectId={projectId} + hideAssignees={hideAssignees} + /> + ))} + + )} + + )} + + ); +}; diff --git a/webapp/src/ee/task/components/taskCreate/TaskPreview.tsx b/webapp/src/ee/task/components/taskCreate/TaskPreview.tsx index 129c9dd845..51b2fe6695 100644 --- a/webapp/src/ee/task/components/taskCreate/TaskPreview.tsx +++ b/webapp/src/ee/task/components/taskCreate/TaskPreview.tsx @@ -18,7 +18,6 @@ type LanguageModel = components['schemas']['LanguageModel']; const StyledContainer = styled('div')` display: grid; padding: 16px 20px; - grid-template-columns: 1fr 3fr 2fr; border-radius: 8px; background: ${({ theme }) => theme.palette.tokens.background.selected}; ${({ theme }) => theme.breakpoints.down('sm')} { @@ -53,6 +52,7 @@ type Props = { onUpdateAssignees: (users: User[]) => void; filters: TranslationStateType[]; projectId: number; + hideAssignees?: boolean; }; export const TaskPreview = ({ @@ -63,6 +63,7 @@ export const TaskPreview = ({ onUpdateAssignees, filters, projectId, + hideAssignees, }: Props) => { const { t } = useTranslate(); const formatNumber = useNumberFormatter(); @@ -84,7 +85,12 @@ export const TaskPreview = ({ }); return ( - + - - {t('create_task_preview_assignee')} - - } - filters={{ - filterMinimalScope: 'TRANSLATIONS_VIEW', - filterViewLanguageId: language.id, - }} - /> + {!hideAssignees && ( + + {t('create_task_preview_assignee')} + + } + filters={{ + filterMinimalScope: 'TRANSLATIONS_VIEW', + filterViewLanguageId: language.id, + }} + /> + )} ); }; diff --git a/webapp/src/ee/task/components/taskFilter/SubfilterAgencies.tsx b/webapp/src/ee/task/components/taskFilter/SubfilterAgencies.tsx new file mode 100644 index 0000000000..d3af05d734 --- /dev/null +++ b/webapp/src/ee/task/components/taskFilter/SubfilterAgencies.tsx @@ -0,0 +1,80 @@ +import { useRef, useState } from 'react'; +import { useTranslate } from '@tolgee/react'; +import { Checkbox, Menu, MenuItem } from '@mui/material'; + +import { SubmenuItem } from './SubmenuItem'; +import { useBillingApiQuery } from 'tg.service/http/useQueryApi'; +import { AgencyLabel } from 'tg.ee/orderTranslations/AgencyLabel'; + +type Props = { + value: number[]; + onChange: (value: number[]) => void; +}; + +export const SubfilterAgencies = ({ value, onChange }: Props) => { + const agenciesLoadable = useBillingApiQuery({ + url: '/v2/billing/translation-agency', + method: 'get', + query: { + size: 1000, + }, + }); + + const { t } = useTranslate(); + const [open, setOpen] = useState(false); + const anchorEl = useRef(null); + const handleAgencyToggle = (id: number) => () => { + if (value.includes(id)) { + onChange(value.filter((l) => l !== id)); + } else { + onChange([...value, id]); + } + }; + + const data = agenciesLoadable.data?._embedded?.translationAgencies; + + return ( + <> + setOpen(true)} + selected={Boolean(value?.length)} + /> + {open && ( + { + setOpen(false); + }} + data-cy="language-select-popover" + > + {data?.map((agency) => ( + + + + + ))} + + )} + + ); +}; diff --git a/webapp/src/ee/task/components/taskFilter/TaskFilter.tsx b/webapp/src/ee/task/components/taskFilter/TaskFilter.tsx index 80b941b360..7d4e24310b 100644 --- a/webapp/src/ee/task/components/taskFilter/TaskFilter.tsx +++ b/webapp/src/ee/task/components/taskFilter/TaskFilter.tsx @@ -12,7 +12,9 @@ import { FlagImage } from 'tg.component/languages/FlagImage'; import { TaskTypeChip } from '../TaskTypeChip'; import { filterEmpty } from './taskFilterUtils'; import { stopBubble } from 'tg.fixtures/eventHandler'; -import { useApiQuery } from 'tg.service/http/useQueryApi'; +import { useApiQuery, useBillingApiQuery } from 'tg.service/http/useQueryApi'; +import { AgencyLabel } from 'tg.ee/orderTranslations/AgencyLabel'; +import { useConfig } from 'tg.globalContext/helpers'; type SimpleProjectModel = components['schemas']['SimpleProjectModel']; @@ -64,6 +66,7 @@ export const TaskFilter = ({ const anchorEl = useRef(null); const [open, setOpen] = useState(false); const { t } = useTranslate(); + const config = useConfig(); const usersLoadable = useApiQuery({ url: '/v2/projects/{projectId}/tasks/possible-assignees', @@ -97,6 +100,19 @@ export const TaskFilter = ({ }, }); + const agenciesLoadable = useBillingApiQuery({ + url: '/v2/billing/translation-agency', + method: 'get', + query: { + size: 1000, + }, + options: { + enabled: + !project && Boolean(value.agencies?.length) && config.billing.enabled, + keepPreviousData: true, + }, + }); + const languages = languagesLoadable.data?._embedded?.languages ?? []; function getFilterValue(value: TaskFilterType) { @@ -146,6 +162,11 @@ export const TaskFilter = ({ /> ))} + {agenciesLoadable.data?._embedded?.translationAgencies + ?.filter((a) => value.agencies?.includes(a.id)) + .map((agency) => ( + + ))} {value.types?.map((type) => ( ))} diff --git a/webapp/src/ee/task/components/taskFilter/TaskFilterPopover.tsx b/webapp/src/ee/task/components/taskFilter/TaskFilterPopover.tsx index 3ca5c381e1..f5835e7f14 100644 --- a/webapp/src/ee/task/components/taskFilter/TaskFilterPopover.tsx +++ b/webapp/src/ee/task/components/taskFilter/TaskFilterPopover.tsx @@ -14,6 +14,8 @@ import { components } from 'tg.service/apiSchema.generated'; import { SubfilterAssignees } from './SubfilterAssignees'; import { SubfilterLanguages } from './SubfilterLanguages'; import { SubfilterProjects } from './SubfilterProjects'; +import { SubfilterAgencies } from './SubfilterAgencies'; +import { useConfig, useEnabledFeatures } from 'tg.globalContext/helpers'; type SimpleProjectModel = components['schemas']['SimpleProjectModel']; type TaskType = components['schemas']['TaskModel']['type']; @@ -28,6 +30,7 @@ const StyledListSubheader = styled(ListSubheader)` export type TaskFilterType = { languages?: number[]; assignees?: number[]; + agencies?: number[]; projects?: number[]; types?: TaskType[]; doneMinClosedAt?: number; @@ -55,6 +58,12 @@ export const TaskFilterPopover: React.FC = ({ const [value, setValue] = useState(initialValue); const debouncedOnChange = useDebouncedCallback(onChange, 200); + const config = useConfig(); + const { isEnabled } = useEnabledFeatures(); + + const agencyVisible = + config.billing.enabled && isEnabled('ORDER_TRANSLATION'); + function handleChange(value: TaskFilterType) { setValue(value); debouncedOnChange(value); @@ -106,6 +115,13 @@ export const TaskFilterPopover: React.FC = ({ /> )} + {project && agencyVisible && ( + handleChange({ ...value, agencies })} + /> + )} + {!project && ( { !filter.assignees?.length && !filter.languages?.length && !filter.projects?.length && - !filter.types?.length + !filter.types?.length && + !filter.agencies?.length ); }; diff --git a/webapp/src/ee/task/components/tasksHeader/TasksHeaderBig.tsx b/webapp/src/ee/task/components/tasksHeader/TasksHeaderBig.tsx index 78d4fea557..c706cf728a 100644 --- a/webapp/src/ee/task/components/tasksHeader/TasksHeaderBig.tsx +++ b/webapp/src/ee/task/components/tasksHeader/TasksHeaderBig.tsx @@ -8,6 +8,7 @@ import { InputAdornment, styled, SxProps, + Tooltip, } from '@mui/material'; import { useDebounceCallback } from 'usehooks-ts'; import { @@ -15,6 +16,7 @@ import { Plus, Rows03, SearchSm, + ShoppingCart01, } from '@untitled-ui/icons-react'; import { useTranslate } from '@tolgee/react'; @@ -48,6 +50,7 @@ type Props = { filter: TaskFilterType; onFilterChange: (value: TaskFilterType) => void; onAddTask?: () => void; + onOrderTranslation?: () => void; view: TaskView; onViewChange: (view: TaskView) => void; project?: SimpleProjectModel; @@ -62,6 +65,7 @@ export const TasksHeaderBig = ({ filter, onFilterChange, onAddTask, + onOrderTranslation, view, onViewChange, project, @@ -110,7 +114,7 @@ export const TasksHeaderBig = ({ } /> - + + {onOrderTranslation && ( + + + + )} + {onAddTask && ( + )} + {addDialog && ( + setAddDialog(false)} + onFinished={() => setAddDialog(false)} + initialValues={{ + languages: allLanguages + .filter((l) => languagesPreference.includes(l.tag)) + .filter((l) => !l.base) + .map((l) => l.id), + }} + projectId={project.id} + allLanguages={allLanguages} + /> + )} + {orderDialog && ( + setOrderDialog(false)} + onFinished={() => setOrderDialog(false)} + initialValues={{ + languages: allLanguages + .filter((l) => languagesPreference.includes(l.tag)) + .filter((l) => !l.base) + .map((l) => l.id), + }} + projectId={project.id} + allLanguages={allLanguages} + /> + )} + ); }; diff --git a/webapp/src/fixtures/isValidUrl.ts b/webapp/src/fixtures/isValidUrl.ts new file mode 100644 index 0000000000..e494b5b944 --- /dev/null +++ b/webapp/src/fixtures/isValidUrl.ts @@ -0,0 +1,8 @@ +export function isValidHttpUrl(link: string) { + try { + const url = new URL(link); + return url.protocol === 'http:' || url.protocol === 'https:'; + } catch (_) { + return false; + } +} diff --git a/webapp/src/service/apiSchema.generated.ts b/webapp/src/service/apiSchema.generated.ts index cd63011a2f..5487cb3719 100644 --- a/webapp/src/service/apiSchema.generated.ts +++ b/webapp/src/service/apiSchema.generated.ts @@ -339,6 +339,12 @@ export interface paths { /** Pairs user account with slack account. */ post: operations["userLogin"]; }; + "/v2/public/translator/translate": { + post: operations["translate"]; + }; + "/v2/public/telemetry/report": { + post: operations["report"]; + }; "/v2/public/slack": { post: operations["slackCommand"]; }; @@ -354,8 +360,26 @@ export interface paths { */ post: operations["fetchBotEvent"]; }; + "/v2/public/licensing/subscription": { + post: operations["getMySubscription"]; + }; + "/v2/public/licensing/set-key": { + post: operations["onLicenceSetKey"]; + }; + "/v2/public/licensing/report-usage": { + post: operations["reportUsage"]; + }; + "/v2/public/licensing/report-error": { + post: operations["reportError"]; + }; + "/v2/public/licensing/release-key": { + post: operations["releaseKey"]; + }; + "/v2/public/licensing/prepare-set-key": { + post: operations["prepareSetLicenseKey"]; + }; "/v2/public/business-events/report": { - post: operations["report"]; + post: operations["report_1"]; }; "/v2/public/business-events/identify": { post: operations["identify"]; @@ -434,7 +458,7 @@ export interface paths { }; "/v2/projects/{projectId}/start-batch-job/pre-translate-by-tm": { /** Pre-translate provided keys to provided languages by TM. */ - post: operations["translate"]; + post: operations["translate_1"]; }; "/v2/projects/{projectId}/start-batch-job/machine-translate": { /** Translate provided keys to provided languages through primary MT provider. */ @@ -520,7 +544,7 @@ export interface paths { }; "/v2/ee-license/prepare-set-license-key": { /** Get info about the upcoming EE subscription. This will show, how much the subscription will cost when key is applied. */ - post: operations["prepareSetLicenseKey"]; + post: operations["prepareSetLicenseKey_1"]; }; "/v2/api-keys": { get: operations["allByUser"]; @@ -630,12 +654,24 @@ export interface paths { get: operations["getMachineTranslationLanguageInfo"]; }; "/v2/projects/{projectId}/keys/search": { - /** This endpoint helps you to find desired key by keyName, base translation or translation in specified language. */ + /** + * This endpoint helps you to find desired key by keyName, base translation or translation in specified language. + * + * Sort is ignored for this request. + */ get: operations["searchForKey"]; }; "/v2/projects/{projectId}/all-keys": { get: operations["getAllKeys"]; }; + "/v2/projects/{projectId}/all-keys-with-disabled-languages": { + /** + * Returns all project key with any disabled language. + * + * If key has no disabled language, it is not returned. + */ + get: operations["getDisabledLanguages_2"]; + }; "/v2/projects/{projectId}/activity/revisions/{revisionId}/modified-entities": { get: operations["getModifiedEntitiesByRevision"]; }; @@ -1095,7 +1131,8 @@ export interface components { | "plan_auto_assignment_organization_ids_not_in_for_organization_ids" | "task_not_found" | "task_not_finished" - | "task_not_open"; + | "task_not_open" + | "translation_agency_not_found"; params?: { [key: string]: unknown }[]; }; ErrorResponseBody: { @@ -1168,24 +1205,6 @@ 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 @@ -1225,6 +1244,24 @@ 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 change state to. If null, changing state of all language values is permitted. + * @example 200001,200004 + */ + stateChangeLanguageIds?: number[]; + /** + * @description List of languages user can translate to. If null, all languages editing is permitted. + * @example 200001,200004 + */ + translateLanguageIds?: number[]; }; LanguageModel: { /** Format: int64 */ @@ -1406,6 +1443,14 @@ export interface components { /** Format: int64 */ closedAt?: number; state: "NEW" | "IN_PROGRESS" | "DONE" | "CLOSED"; + agency?: components["schemas"]["TranslationAgencySimpleModel"]; + }; + TranslationAgencySimpleModel: { + /** Format: int64 */ + id: number; + name: string; + url?: string; + avatar?: components["schemas"]["Avatar"]; }; UpdateTaskKeyRequest: { done: boolean; @@ -1785,18 +1830,85 @@ export interface components { email?: string; /** @description Name of invited user */ name?: string; + /** + * Format: int64 + * @description Id of invited agency + */ + agencyId?: number; + }; + PermissionWithAgencyModel: { + /** + * @description Granted scopes to the user. When user has type permissions, this field contains permission scopes of the type. + * @example KEYS_EDIT,TRANSLATIONS_VIEW + */ + scopes: ( + | "translations.view" + | "translations.edit" + | "keys.edit" + | "screenshots.upload" + | "screenshots.delete" + | "screenshots.view" + | "activity.view" + | "languages.edit" + | "admin" + | "project.edit" + | "members.view" + | "members.edit" + | "translation-comments.add" + | "translation-comments.edit" + | "translation-comments.set-state" + | "translations.state-edit" + | "keys.view" + | "keys.delete" + | "keys.create" + | "batch-jobs.view" + | "batch-jobs.cancel" + | "translations.batch-by-tm" + | "translations.batch-machine" + | "content-delivery.manage" + | "content-delivery.publish" + | "webhooks.manage" + | "tasks.view" + | "tasks.edit" + )[]; + /** @description The user's permission type. This field is null if uses granular permissions */ + type?: "NONE" | "VIEW" | "TRANSLATE" | "REVIEW" | "EDIT" | "MANAGE"; + /** + * @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 view. If null, all languages view is permitted. + * @example 200001,200004 + */ + viewLanguageIds?: 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[]; + agency?: components["schemas"]["TranslationAgencySimpleModel"]; }; ProjectInvitationModel: { /** Format: int64 */ id: number; - code: string; + code?: string; type?: "NONE" | "VIEW" | "TRANSLATE" | "REVIEW" | "EDIT" | "MANAGE"; permittedLanguageIds?: number[]; /** Format: date-time */ createdAt: string; invitedUserName?: string; invitedUserEmail?: string; - permission: components["schemas"]["PermissionModel"]; + permission: components["schemas"]["PermissionWithAgencyModel"]; }; AzureContentStorageConfigDto: { connectionString?: string; @@ -1865,10 +1977,8 @@ export interface components { languages?: string[]; /** @description Format to export to */ format: - | "CSV" | "JSON" | "JSON_TOLGEE" - | "JSON_I18NEXT" | "XLIFF" | "PO" | "APPLE_STRINGS_STRINGSDICT" @@ -1877,7 +1987,9 @@ export interface components { | "FLUTTER_ARB" | "PROPERTIES" | "YAML_RUBY" - | "YAML"; + | "YAML" + | "JSON_I18NEXT" + | "CSV"; /** * @description Delimiter to structure file content. * @@ -1965,10 +2077,8 @@ export interface components { languages?: string[]; /** @description Format to export to */ format: - | "CSV" | "JSON" | "JSON_TOLGEE" - | "JSON_I18NEXT" | "XLIFF" | "PO" | "APPLE_STRINGS_STRINGSDICT" @@ -1977,7 +2087,9 @@ export interface components { | "FLUTTER_ARB" | "PROPERTIES" | "YAML_RUBY" - | "YAML"; + | "YAML" + | "JSON_I18NEXT" + | "CSV"; /** * @description Delimiter to structure file content. * @@ -2089,12 +2201,12 @@ export interface components { createNewKeys: boolean; }; ImportSettingsModel: { - /** @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; + /** @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; }; TranslationCommentModel: { /** @@ -2253,15 +2365,15 @@ export interface components { token: string; /** Format: int64 */ id: number; - description: string; + /** Format: int64 */ + lastUsedAt?: number; /** Format: int64 */ createdAt: number; /** Format: int64 */ updatedAt: number; /** Format: int64 */ expiresAt?: number; - /** Format: int64 */ - lastUsedAt?: number; + description: string; }; SetOrganizationRoleDto: { roleType: "MEMBER" | "OWNER"; @@ -2332,6 +2444,7 @@ export interface components { | "AI_PROMPT_CUSTOMIZATION" | "SLACK_INTEGRATION" | "TASKS" + | "ORDER_TRANSLATION" )[]; /** Format: int64 */ currentPeriodEnd?: number; @@ -2402,15 +2515,15 @@ export interface components { id: number; userFullName?: string; projectName: string; - description: string; - username?: string; - scopes: string[]; + /** Format: int64 */ + lastUsedAt?: number; /** Format: int64 */ projectId: number; + scopes: string[]; + username?: string; /** Format: int64 */ expiresAt?: number; - /** Format: int64 */ - lastUsedAt?: number; + description: string; }; SuperTokenRequest: { /** @description Has to be provided when TOTP enabled */ @@ -2422,6 +2535,49 @@ export interface components { name: string; oldSlug?: string; }; + ExampleItem: { + source: string; + target: string; + key: string; + keyNamespace?: string; + }; + Metadata: { + examples: components["schemas"]["ExampleItem"][]; + closeItems: components["schemas"]["ExampleItem"][]; + keyDescription?: string; + projectDescription?: string; + languageDescription?: string; + }; + TolgeeTranslateParams: { + text: string; + keyName?: string; + sourceTag: string; + targetTag: string; + metadata?: components["schemas"]["Metadata"]; + formality?: "FORMAL" | "INFORMAL" | "DEFAULT"; + isBatch: boolean; + pluralForms?: { [key: string]: string }; + pluralFormExamples?: { [key: string]: string }; + }; + MtResult: { + translated?: string; + /** Format: int32 */ + price: number; + contextDescription?: string; + }; + TelemetryReportRequest: { + instanceId: string; + /** Format: int64 */ + projectsCount: number; + /** Format: int64 */ + translationsCount: number; + /** Format: int64 */ + languagesCount: number; + /** Format: int64 */ + distinctLanguagesCount: number; + /** Format: int64 */ + usersCount: number; + }; SlackCommandDto: { token?: string; team_id: string; @@ -2434,84 +2590,206 @@ export interface components { trigger_id?: string; team_domain: string; }; - BusinessEventReportRequest: { - eventName: string; - anonymousUserId?: string; - /** Format: int64 */ - organizationId?: number; - /** Format: int64 */ - projectId?: number; - data?: { [key: string]: { [key: string]: unknown } }; - }; - IdentifyRequest: { - anonymousUserId: string; - }; - CreateProjectRequest: { - name: string; - languages: components["schemas"]["LanguageRequest"][]; - /** @description Slug of your project used in url e.g. "/v2/projects/what-a-project". If not provided, it will be generated */ - slug?: string; - /** - * Format: int64 - * @description Organization to create the project in - */ - organizationId: number; - /** @description Tag of one of created languages, to select it as base language. If not provided, first language will be selected as base. */ - baseLanguageTag?: string; - /** @description Whether to use ICU placeholder visualization in the editor and it's support. */ - icuPlaceholders: boolean; - }; - WebhookTestResponse: { - success: boolean; - }; - CreateMultipleTasksRequest: { - tasks: components["schemas"]["CreateTaskRequest"][]; - }; - CreateTaskRequest: { - name: string; - description: string; - type: "TRANSLATE" | "REVIEW"; - /** - * Format: int64 - * @description Due to date in epoch format (milliseconds). - * @example 1661172869000 - */ - dueDate?: number; - /** - * Format: int64 - * @description Id of language, this task is attached to. - * @example 1 - */ - languageId: number; - assignees: number[]; - keys: number[]; - }; - CalculateScopeRequest: { - /** Format: int64 */ - languageId: number; - type: "TRANSLATE" | "REVIEW"; - keys: number[]; + GetMySubscriptionDto: { + licenseKey: string; + instanceId: string; }; - KeysScopeView: { + PlanIncludedUsageModel: { /** Format: int64 */ - keyCount: number; + seats: number; /** Format: int64 */ - characterCount: number; + translationSlots: number; /** Format: int64 */ - wordCount: number; + translations: number; /** Format: int64 */ - keyCountIncludingConflicts: number; + mtCredits: number; }; - GetKeysRequestDto: { - keys: components["schemas"]["KeyDefinitionDto"][]; - /** @description Tags to return language translations in */ - languageTags: string[]; + PlanPricesModel: { + perSeat: number; + perThousandTranslations?: number; + perThousandMtCredits?: number; + subscriptionMonthly: number; + subscriptionYearly: number; }; - KeyDefinitionDto: { + SelfHostedEePlanModel: { + /** Format: int64 */ + id: number; name: string; - namespace?: string; - }; - CollectionModelKeyWithDataModel: { + public: boolean; + enabledFeatures: ( + | "GRANULAR_PERMISSIONS" + | "PRIORITIZED_FEATURE_REQUESTS" + | "PREMIUM_SUPPORT" + | "DEDICATED_SLACK_CHANNEL" + | "ASSISTED_UPDATES" + | "DEPLOYMENT_ASSISTANCE" + | "BACKUP_CONFIGURATION" + | "TEAM_TRAINING" + | "ACCOUNT_MANAGER" + | "STANDARD_SUPPORT" + | "PROJECT_LEVEL_CONTENT_STORAGES" + | "WEBHOOKS" + | "MULTIPLE_CONTENT_DELIVERY_CONFIGS" + | "AI_PROMPT_CUSTOMIZATION" + | "SLACK_INTEGRATION" + | "TASKS" + | "ORDER_TRANSLATION" + )[]; + prices: components["schemas"]["PlanPricesModel"]; + includedUsage: components["schemas"]["PlanIncludedUsageModel"]; + hasYearlyPrice: boolean; + free: boolean; + }; + SelfHostedEeSubscriptionModel: { + /** Format: int64 */ + id: number; + /** Format: int64 */ + currentPeriodStart?: number; + /** Format: int64 */ + currentPeriodEnd?: number; + currentBillingPeriod: "MONTHLY" | "YEARLY"; + /** Format: int64 */ + createdAt: number; + plan: components["schemas"]["SelfHostedEePlanModel"]; + status: + | "ACTIVE" + | "CANCELED" + | "PAST_DUE" + | "UNPAID" + | "ERROR" + | "KEY_USED_BY_ANOTHER_INSTANCE"; + licenseKey?: string; + estimatedCosts?: number; + }; + SetLicenseKeyLicensingDto: { + licenseKey: string; + /** Format: int64 */ + seats: number; + instanceId: string; + }; + ReportUsageDto: { + licenseKey: string; + /** Format: int64 */ + seats: number; + }; + ReportErrorDto: { + stackTrace: string; + licenseKey: string; + }; + ReleaseKeyDto: { + licenseKey: string; + }; + PrepareSetLicenseKeyDto: { + licenseKey: string; + /** Format: int64 */ + seats: number; + }; + AverageProportionalUsageItemModel: { + total: number; + unusedQuantity: number; + usedQuantity: number; + usedQuantityOverPlan: number; + }; + PrepareSetEeLicenceKeyModel: { + plan: components["schemas"]["SelfHostedEePlanModel"]; + usage: components["schemas"]["UsageModel"]; + }; + SumUsageItemModel: { + total: number; + /** Format: int64 */ + unusedQuantity: number; + /** Format: int64 */ + usedQuantity: number; + /** Format: int64 */ + usedQuantityOverPlan: number; + }; + UsageModel: { + subscriptionPrice?: number; + /** @description Relevant for invoices only. When there are applied stripe credits, we need to reduce the total price by this amount. */ + appliedStripeCredits?: number; + seats: components["schemas"]["AverageProportionalUsageItemModel"]; + translations: components["schemas"]["AverageProportionalUsageItemModel"]; + credits?: components["schemas"]["SumUsageItemModel"]; + total: number; + }; + BusinessEventReportRequest: { + eventName: string; + anonymousUserId?: string; + /** Format: int64 */ + organizationId?: number; + /** Format: int64 */ + projectId?: number; + data?: { [key: string]: { [key: string]: unknown } }; + }; + IdentifyRequest: { + anonymousUserId: string; + }; + CreateProjectRequest: { + name: string; + languages: components["schemas"]["LanguageRequest"][]; + /** @description Slug of your project used in url e.g. "/v2/projects/what-a-project". If not provided, it will be generated */ + slug?: string; + /** + * Format: int64 + * @description Organization to create the project in + */ + organizationId: number; + /** @description Tag of one of created languages, to select it as base language. If not provided, first language will be selected as base. */ + baseLanguageTag?: string; + /** @description Whether to use ICU placeholder visualization in the editor and it's support. */ + icuPlaceholders: boolean; + }; + WebhookTestResponse: { + success: boolean; + }; + CreateMultipleTasksRequest: { + tasks: components["schemas"]["CreateTaskRequest"][]; + }; + CreateTaskRequest: { + name: string; + description: string; + type: "TRANSLATE" | "REVIEW"; + /** + * Format: int64 + * @description Due to date in epoch format (milliseconds). + * @example 1661172869000 + */ + dueDate?: number; + /** + * Format: int64 + * @description Id of language, this task is attached to. + * @example 1 + */ + languageId: number; + assignees: number[]; + keys: number[]; + }; + CalculateScopeRequest: { + /** Format: int64 */ + languageId: number; + type: "TRANSLATE" | "REVIEW"; + keys: number[]; + }; + KeysScopeView: { + /** Format: int64 */ + keyCount: number; + /** Format: int64 */ + characterCount: number; + /** Format: int64 */ + wordCount: number; + /** Format: int64 */ + keyCountIncludingConflicts: number; + }; + GetKeysRequestDto: { + keys: components["schemas"]["KeyDefinitionDto"][]; + /** @description Tags to return language translations in */ + languageTags: string[]; + }; + KeyDefinitionDto: { + name: string; + namespace?: string; + }; + CollectionModelKeyWithDataModel: { _embedded?: { keys?: components["schemas"]["KeyWithDataModel"][]; }; @@ -2545,11 +2823,12 @@ export interface components { * * - KEEP: Translation is not changed * - OVERRIDE: Translation is overridden - * - NEW: New translation is created) + * - NEW: New translation is created + * - FORCE_OVERRIDE: Translation is updated, created or kept. * * @example OVERRIDE */ - resolution: "KEEP" | "OVERRIDE" | "NEW"; + resolution: "KEEP" | "OVERRIDE" | "NEW" | "FORCE_OVERRIDE"; }; KeyImportResolvableResultModel: { /** @description List of keys */ @@ -2844,7 +3123,8 @@ export interface components { | "plan_auto_assignment_organization_ids_not_in_for_organization_ids" | "task_not_found" | "task_not_finished" - | "task_not_open"; + | "task_not_open" + | "translation_agency_not_found"; params?: { [key: string]: unknown }[]; }; UntagKeysRequest: { @@ -3106,10 +3386,8 @@ export interface components { languages?: string[]; /** @description Format to export to */ format: - | "CSV" | "JSON" | "JSON_TOLGEE" - | "JSON_I18NEXT" | "XLIFF" | "PO" | "APPLE_STRINGS_STRINGSDICT" @@ -3118,7 +3396,9 @@ export interface components { | "FLUTTER_ARB" | "PROPERTIES" | "YAML_RUBY" - | "YAML"; + | "YAML" + | "JSON_I18NEXT" + | "CSV"; /** * @description Delimiter to structure file content. * @@ -3290,79 +3570,6 @@ export interface components { createdAt: string; location?: string; }; - AverageProportionalUsageItemModel: { - total: number; - unusedQuantity: number; - usedQuantity: number; - usedQuantityOverPlan: number; - }; - PlanIncludedUsageModel: { - /** Format: int64 */ - seats: number; - /** Format: int64 */ - translationSlots: number; - /** Format: int64 */ - translations: number; - /** Format: int64 */ - mtCredits: number; - }; - PlanPricesModel: { - perSeat: number; - perThousandTranslations?: number; - perThousandMtCredits?: number; - subscriptionMonthly: number; - subscriptionYearly: number; - }; - PrepareSetEeLicenceKeyModel: { - plan: components["schemas"]["SelfHostedEePlanModel"]; - usage: components["schemas"]["UsageModel"]; - }; - SelfHostedEePlanModel: { - /** Format: int64 */ - id: number; - name: string; - public: boolean; - enabledFeatures: ( - | "GRANULAR_PERMISSIONS" - | "PRIORITIZED_FEATURE_REQUESTS" - | "PREMIUM_SUPPORT" - | "DEDICATED_SLACK_CHANNEL" - | "ASSISTED_UPDATES" - | "DEPLOYMENT_ASSISTANCE" - | "BACKUP_CONFIGURATION" - | "TEAM_TRAINING" - | "ACCOUNT_MANAGER" - | "STANDARD_SUPPORT" - | "PROJECT_LEVEL_CONTENT_STORAGES" - | "WEBHOOKS" - | "MULTIPLE_CONTENT_DELIVERY_CONFIGS" - | "AI_PROMPT_CUSTOMIZATION" - | "SLACK_INTEGRATION" - | "TASKS" - )[]; - prices: components["schemas"]["PlanPricesModel"]; - includedUsage: components["schemas"]["PlanIncludedUsageModel"]; - hasYearlyPrice: boolean; - free: boolean; - }; - SumUsageItemModel: { - total: number; - /** Format: int64 */ - unusedQuantity: number; - /** Format: int64 */ - usedQuantity: number; - /** Format: int64 */ - usedQuantityOverPlan: number; - }; - UsageModel: { - subscriptionPrice?: number; - /** @description Relevant for invoices only. When there are applied stripe credits, we need to reduce the total price by this amount. */ - appliedStripeCredits?: number; - seats: components["schemas"]["AverageProportionalUsageItemModel"]; - translations: components["schemas"]["AverageProportionalUsageItemModel"]; - credits?: components["schemas"]["SumUsageItemModel"]; - total: number; - }; CreateApiKeyDto: { /** Format: int64 */ projectId: number; @@ -3576,24 +3783,25 @@ export interface components { | "AI_PROMPT_CUSTOMIZATION" | "SLACK_INTEGRATION" | "TASKS" + | "ORDER_TRANSLATION" )[]; quickStart?: components["schemas"]["QuickStartModel"]; /** @example Beautiful organization */ 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"; - 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; + /** @example This is a beautiful organization full of beautiful and clever people */ + description?: string; }; PublicBillingConfigurationDTO: { enabled: boolean; @@ -3602,6 +3810,7 @@ export interface components { machineTranslationServices: components["schemas"]["MtServicesDTO"]; billing: components["schemas"]["PublicBillingConfigurationDTO"]; version: string; + contentDeliveryEnabled?: boolean; authentication: boolean; authMethods?: components["schemas"]["AuthMethodsDTO"]; passwordResettable: boolean; @@ -3639,10 +3848,8 @@ export interface components { }; ExportFormatModel: { format: - | "CSV" | "JSON" | "JSON_TOLGEE" - | "JSON_I18NEXT" | "XLIFF" | "PO" | "APPLE_STRINGS_STRINGSDICT" @@ -3651,7 +3858,9 @@ export interface components { | "FLUTTER_ARB" | "PROPERTIES" | "YAML_RUBY" - | "YAML"; + | "YAML" + | "JSON_I18NEXT" + | "CSV"; extension: string; mediaType: string; defaultFileStructureTemplate: string; @@ -3687,7 +3896,7 @@ export interface components { avatar?: components["schemas"]["Avatar"]; organizationRole?: "MEMBER" | "OWNER"; organizationBasePermission: components["schemas"]["PermissionModel"]; - directPermission?: components["schemas"]["PermissionModel"]; + directPermission?: components["schemas"]["PermissionWithAgencyModel"]; computedPermission: components["schemas"]["ComputedPermissionModel"]; }; CollectionModelUsedNamespaceModel: { @@ -3758,9 +3967,9 @@ export interface components { /** Format: int64 */ id: number; baseTranslation?: string; + translation?: string; namespace?: string; description?: string; - translation?: string; }; KeySearchSearchResultModel: { view?: components["schemas"]["KeySearchResultView"]; @@ -3768,9 +3977,9 @@ export interface components { /** Format: int64 */ id: number; baseTranslation?: string; + translation?: string; namespace?: string; description?: string; - translation?: string; }; PagedModelKeySearchSearchResultModel: { _embedded?: { @@ -3801,10 +4010,40 @@ export interface components { keys?: components["schemas"]["KeyModel"][]; }; }; - EntityDescriptionWithRelations: { - entityClass: string; + CollectionModelKeyDisabledLanguagesModel: { + _embedded?: { + keys?: components["schemas"]["KeyDisabledLanguagesModel"][]; + }; + }; + /** @description Disabled languages */ + KeyDisabledLanguageModel: { /** Format: int64 */ - entityId: number; + id: number; + tag: string; + }; + KeyDisabledLanguagesModel: { + /** + * Format: int64 + * @description Id of key record + */ + id: number; + /** + * @description Name of key + * @example this_is_super_key + */ + name: string; + /** + * @description Namespace of key + * @example homepage + */ + namespace?: string; + /** @description Disabled languages */ + disabledLanguages: components["schemas"]["KeyDisabledLanguageModel"][]; + }; + EntityDescriptionWithRelations: { + entityClass: string; + /** Format: int64 */ + entityId: number; data: { [key: string]: { [key: string]: unknown } }; }; ExistenceEntityDescription: { @@ -3905,7 +4144,8 @@ export interface components { | "TASK_FINISH" | "TASK_CLOSE" | "TASK_REOPEN" - | "TASK_KEY_UPDATE"; + | "TASK_KEY_UPDATE" + | "ORDER_TRANSLATION"; author?: components["schemas"]["ProjectActivityAuthorModel"]; modifiedEntities?: { [key: string]: components["schemas"]["ModifiedEntityModel"][]; @@ -4340,15 +4580,15 @@ export interface components { user: components["schemas"]["SimpleUserAccountModel"]; /** Format: int64 */ id: number; - description: string; + /** Format: int64 */ + lastUsedAt?: number; /** Format: int64 */ createdAt: number; /** Format: int64 */ updatedAt: number; /** Format: int64 */ expiresAt?: number; - /** Format: int64 */ - lastUsedAt?: number; + description: string; }; PagedModelOrganizationModel: { _embedded?: { @@ -4469,15 +4709,15 @@ export interface components { id: number; userFullName?: string; projectName: string; - description: string; - username?: string; - scopes: string[]; + /** Format: int64 */ + lastUsedAt?: number; /** Format: int64 */ projectId: number; + scopes: string[]; + username?: string; /** Format: int64 */ expiresAt?: number; - /** Format: int64 */ - lastUsedAt?: number; + description: string; }; PagedModelUserAccountModel: { _embedded?: { @@ -9633,7 +9873,369 @@ export interface operations { /** OK */ 200: { content: { - "application/json": components["schemas"]["EeSubscriptionModel"]; + "application/json": components["schemas"]["EeSubscriptionModel"]; + }; + }; + /** Bad Request */ + 400: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Unauthorized */ + 401: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Forbidden */ + 403: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Not Found */ + 404: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + }; + }; + update_9: { + parameters: { + path: { + apiKeyId: number; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": components["schemas"]["ApiKeyModel"]; + }; + }; + /** Bad Request */ + 400: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Unauthorized */ + 401: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Forbidden */ + 403: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Not Found */ + 404: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["V2EditApiKeyDto"]; + }; + }; + }; + delete_13: { + parameters: { + path: { + apiKeyId: number; + }; + }; + responses: { + /** OK */ + 200: unknown; + /** Bad Request */ + 400: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Unauthorized */ + 401: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Forbidden */ + 403: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Not Found */ + 404: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + }; + }; + regenerate_1: { + parameters: { + path: { + apiKeyId: number; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": components["schemas"]["RevealedApiKeyModel"]; + }; + }; + /** Bad Request */ + 400: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Unauthorized */ + 401: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Forbidden */ + 403: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Not Found */ + 404: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["RegenerateApiKeyDto"]; + }; + }; + }; + /** Enables previously disabled user. */ + enableUser: { + parameters: { + path: { + userId: number; + }; + }; + responses: { + /** OK */ + 200: unknown; + /** Bad Request */ + 400: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Unauthorized */ + 401: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Forbidden */ + 403: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Not Found */ + 404: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + }; + }; + /** Disables user account. User will not be able to log in, but their user data will be preserved, so you can enable the user later using the `enable` endpoint. */ + disableUser: { + parameters: { + path: { + userId: number; + }; + }; + responses: { + /** OK */ + 200: unknown; + /** Bad Request */ + 400: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Unauthorized */ + 401: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Forbidden */ + 403: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Not Found */ + 404: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + }; + }; + /** Set's the global role on the Tolgee Platform server. */ + setRole: { + parameters: { + path: { + userId: number; + role: "USER" | "ADMIN"; + }; + }; + responses: { + /** OK */ + 200: unknown; + /** Bad Request */ + 400: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Unauthorized */ + 401: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Forbidden */ + 403: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Not Found */ + 404: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + }; + }; + /** Resends email verification email to currently authenticated user. */ + sendEmailVerification: { + responses: { + /** OK */ + 200: unknown; + /** Bad Request */ + 400: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Unauthorized */ + 401: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Forbidden */ + 403: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Not Found */ + 404: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + }; + }; + /** Generates new JWT token permitted to sensitive operations */ + getSuperToken: { + responses: { + /** OK */ + 200: { + content: { + "application/json": components["schemas"]["JwtAuthenticationResponse"]; }; }; /** Bad Request */ @@ -9669,18 +10271,18 @@ export interface operations { }; }; }; - }; - update_9: { - parameters: { - path: { - apiKeyId: number; + requestBody: { + content: { + "application/json": components["schemas"]["SuperTokenRequest"]; }; }; + }; + generateProjectSlug: { responses: { /** OK */ 200: { content: { - "application/json": components["schemas"]["ApiKeyModel"]; + "application/json": string; }; }; /** Bad Request */ @@ -9718,19 +10320,18 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["V2EditApiKeyDto"]; + "application/json": components["schemas"]["GenerateSlugDto"]; }; }; }; - delete_13: { - parameters: { - path: { - apiKeyId: number; - }; - }; + generateOrganizationSlug: { responses: { /** OK */ - 200: unknown; + 200: { + content: { + "application/json": string; + }; + }; /** Bad Request */ 400: { content: { @@ -9764,20 +10365,23 @@ export interface operations { }; }; }; + requestBody: { + content: { + "application/json": components["schemas"]["GenerateSlugDto"]; + }; + }; }; - regenerate_1: { + /** Pairs user account with slack account. */ + userLogin: { parameters: { - path: { - apiKeyId: number; + query: { + /** The encrypted data about the desired connection between Slack account and Tolgee account */ + data: string; }; }; responses: { /** OK */ - 200: { - content: { - "application/json": components["schemas"]["RevealedApiKeyModel"]; - }; - }; + 200: unknown; /** Bad Request */ 400: { content: { @@ -9811,22 +10415,15 @@ export interface operations { }; }; }; - requestBody: { - content: { - "application/json": components["schemas"]["RegenerateApiKeyDto"]; - }; - }; }; - /** Enables previously disabled user. */ - enableUser: { - parameters: { - path: { - userId: number; - }; - }; + translate: { responses: { /** OK */ - 200: unknown; + 200: { + content: { + "application/json": components["schemas"]["MtResult"]; + }; + }; /** Bad Request */ 400: { content: { @@ -9860,14 +10457,13 @@ export interface operations { }; }; }; - }; - /** Disables user account. User will not be able to log in, but their user data will be preserved, so you can enable the user later using the `enable` endpoint. */ - disableUser: { - parameters: { - path: { - userId: number; + requestBody: { + content: { + "application/json": components["schemas"]["TolgeeTranslateParams"]; }; }; + }; + report: { responses: { /** OK */ 200: unknown; @@ -9904,18 +10500,26 @@ export interface operations { }; }; }; + requestBody: { + content: { + "application/json": components["schemas"]["TelemetryReportRequest"]; + }; + }; }; - /** Set's the global role on the Tolgee Platform server. */ - setRole: { + slackCommand: { parameters: { - path: { - userId: number; - role: "USER" | "ADMIN"; + header: { + "X-Slack-Signature": string; + "X-Slack-Request-Timestamp": string; }; }; responses: { /** OK */ - 200: unknown; + 200: { + content: { + "application/json": string; + }; + }; /** Bad Request */ 400: { content: { @@ -9949,9 +10553,23 @@ export interface operations { }; }; }; + requestBody: { + content: { + "application/json": { + payload?: components["schemas"]["SlackCommandDto"]; + body?: string; + }; + }; + }; }; - /** Resends email verification email to currently authenticated user. */ - sendEmailVerification: { + /** This is triggered when interactivity event is triggered. E.g., when user clicks button provided in previous messages. */ + onInteractivityEvent: { + parameters: { + header: { + "X-Slack-Signature": string; + "X-Slack-Request-Timestamp": string; + }; + }; responses: { /** OK */ 200: unknown; @@ -9988,14 +10606,29 @@ export interface operations { }; }; }; + requestBody: { + content: { + "application/json": string; + }; + }; }; - /** Generates new JWT token permitted to sensitive operations */ - getSuperToken: { + /** + * This is triggered when bot event is triggered. E.g., when app is uninstalled from workspace. + * + * Heads up! The events have to be configured via Slack App configuration in Event Subscription section. + */ + fetchBotEvent: { + parameters: { + header: { + "X-Slack-Signature": string; + "X-Slack-Request-Timestamp": string; + }; + }; responses: { /** OK */ 200: { content: { - "application/json": components["schemas"]["JwtAuthenticationResponse"]; + "application/json": { [key: string]: unknown }; }; }; /** Bad Request */ @@ -10033,16 +10666,16 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["SuperTokenRequest"]; + "application/json": string; }; }; }; - generateProjectSlug: { + getMySubscription: { responses: { /** OK */ 200: { content: { - "application/json": string; + "application/json": components["schemas"]["SelfHostedEeSubscriptionModel"]; }; }; /** Bad Request */ @@ -10080,16 +10713,16 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["GenerateSlugDto"]; + "application/json": components["schemas"]["GetMySubscriptionDto"]; }; }; }; - generateOrganizationSlug: { + onLicenceSetKey: { responses: { /** OK */ 200: { content: { - "application/json": string; + "application/json": components["schemas"]["SelfHostedEeSubscriptionModel"]; }; }; /** Bad Request */ @@ -10127,18 +10760,11 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["GenerateSlugDto"]; + "application/json": components["schemas"]["SetLicenseKeyLicensingDto"]; }; }; }; - /** Pairs user account with slack account. */ - userLogin: { - parameters: { - query: { - /** The encrypted data about the desired connection between Slack account and Tolgee account */ - data: string; - }; - }; + reportUsage: { responses: { /** OK */ 200: unknown; @@ -10175,21 +10801,16 @@ export interface operations { }; }; }; - }; - slackCommand: { - parameters: { - header: { - "X-Slack-Signature": string; - "X-Slack-Request-Timestamp": string; + requestBody: { + content: { + "application/json": components["schemas"]["ReportUsageDto"]; }; }; + }; + reportError: { responses: { /** OK */ - 200: { - content: { - "application/json": string; - }; - }; + 200: unknown; /** Bad Request */ 400: { content: { @@ -10225,21 +10846,11 @@ export interface operations { }; requestBody: { content: { - "application/json": { - payload?: components["schemas"]["SlackCommandDto"]; - body?: string; - }; + "application/json": components["schemas"]["ReportErrorDto"]; }; }; }; - /** This is triggered when interactivity event is triggered. E.g., when user clicks button provided in previous messages. */ - onInteractivityEvent: { - parameters: { - header: { - "X-Slack-Signature": string; - "X-Slack-Request-Timestamp": string; - }; - }; + releaseKey: { responses: { /** OK */ 200: unknown; @@ -10278,27 +10889,16 @@ export interface operations { }; requestBody: { content: { - "application/json": string; + "application/json": components["schemas"]["ReleaseKeyDto"]; }; }; }; - /** - * This is triggered when bot event is triggered. E.g., when app is uninstalled from workspace. - * - * Heads up! The events have to be configured via Slack App configuration in Event Subscription section. - */ - fetchBotEvent: { - parameters: { - header: { - "X-Slack-Signature": string; - "X-Slack-Request-Timestamp": string; - }; - }; + prepareSetLicenseKey: { responses: { /** OK */ 200: { content: { - "application/json": { [key: string]: unknown }; + "application/json": components["schemas"]["PrepareSetEeLicenceKeyModel"]; }; }; /** Bad Request */ @@ -10336,11 +10936,11 @@ export interface operations { }; requestBody: { content: { - "application/json": string; + "application/json": components["schemas"]["PrepareSetLicenseKeyDto"]; }; }; }; - report: { + report_1: { responses: { /** OK */ 200: unknown; @@ -10829,6 +11429,8 @@ export interface operations { filterLanguage?: number[]; /** Filter tasks by key */ filterKey?: number[]; + /** Filter tasks by agency */ + filterAgency?: number[]; /** Exclude "done" tasks which are older than specified timestamp */ filterDoneMinClosedAt?: number; /** Zero-based page index (0..N) */ @@ -11837,7 +12439,7 @@ export interface operations { }; }; /** Pre-translate provided keys to provided languages by TM. */ - translate: { + translate_1: { parameters: { path: { projectId: number; @@ -12266,10 +12868,8 @@ export interface operations { languages?: string[]; /** Format to export to */ format?: - | "CSV" | "JSON" | "JSON_TOLGEE" - | "JSON_I18NEXT" | "XLIFF" | "PO" | "APPLE_STRINGS_STRINGSDICT" @@ -12278,7 +12878,9 @@ export interface operations { | "FLUTTER_ARB" | "PROPERTIES" | "YAML_RUBY" - | "YAML"; + | "YAML" + | "JSON_I18NEXT" + | "CSV"; /** * Delimiter to structure file content. * @@ -13355,7 +13957,7 @@ export interface operations { }; }; /** Get info about the upcoming EE subscription. This will show, how much the subscription will cost when key is applied. */ - prepareSetLicenseKey: { + prepareSetLicenseKey_1: { responses: { /** OK */ 200: { @@ -13832,6 +14434,8 @@ export interface operations { filterLanguage?: number[]; /** Filter tasks by key */ filterKey?: number[]; + /** Filter tasks by agency */ + filterAgency?: number[]; /** Exclude "done" tasks which are older than specified timestamp */ filterDoneMinClosedAt?: number; /** Zero-based page index (0..N) */ @@ -14384,6 +14988,12 @@ 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 users by id */ + filterId?: number[]; + /** Filter users without id */ + filterNotId?: number[]; + /** Filter users from agency */ + filterAgency?: number[]; }; }; responses: { @@ -14840,7 +15450,11 @@ export interface operations { }; }; }; - /** This endpoint helps you to find desired key by keyName, base translation or translation in specified language. */ + /** + * This endpoint helps you to find desired key by keyName, base translation or translation in specified language. + * + * Sort is ignored for this request. + */ searchForKey: { parameters: { query: { @@ -14947,6 +15561,58 @@ export interface operations { }; }; }; + /** + * Returns all project key with any disabled language. + * + * If key has no disabled language, it is not returned. + */ + getDisabledLanguages_2: { + parameters: { + path: { + projectId: number; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": components["schemas"]["CollectionModelKeyDisabledLanguagesModel"]; + }; + }; + /** Bad Request */ + 400: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Unauthorized */ + 401: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Forbidden */ + 403: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Not Found */ + 404: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + }; + }; getModifiedEntitiesByRevision: { parameters: { query: { diff --git a/webapp/src/service/billingApiSchema.generated.ts b/webapp/src/service/billingApiSchema.generated.ts index 380c591b6b..aa993f2c02 100644 --- a/webapp/src/service/billingApiSchema.generated.ts +++ b/webapp/src/service/billingApiSchema.generated.ts @@ -20,6 +20,15 @@ export interface paths { /** When applied, current subscription will be cancelled at the period end. */ put: operations["cancelSubscription"]; }; + "/v2/administration/billing/translation-agency/{agencyId}": { + get: operations["get_1"]; + put: operations["update"]; + delete: operations["delete"]; + }; + "/v2/administration/billing/translation-agency/{agencyId}/avatar": { + put: operations["uploadAvatar"]; + delete: operations["removeAvatar"]; + }; "/v2/administration/billing/self-hosted-ee-plans/{planId}": { get: operations["getPlan"]; put: operations["updatePlan"]; @@ -30,6 +39,12 @@ export interface paths { put: operations["updatePlan_1"]; delete: operations["deletePlan_1"]; }; + "/v2/administration/billing/add-usage-items-to-invoice-and-finalize-it/{invoiceId}": { + put: operations["addUsageItemsToInvoiceAndFinalizeIt"]; + }; + "/v2/projects/{projectId}/billing/order-translation": { + post: operations["createTranslationOrder"]; + }; "/v2/organizations/{organizationId}/billing/subscribe": { post: operations["subscribe"]; }; @@ -43,13 +58,17 @@ export interface paths { "/v2/organizations/{organizationId}/billing/buy-more-credits": { post: operations["getBuyMoreCreditsCheckoutSessionUrl"]; }; + "/v2/administration/billing/translation-agency": { + get: operations["getAll_1"]; + post: operations["create"]; + }; "/v2/administration/billing/self-hosted-ee-plans": { get: operations["getPlans_1"]; - post: operations["create"]; + post: operations["create_1"]; }; "/v2/administration/billing/cloud-plans": { get: operations["getPlans_2"]; - post: operations["create_1"]; + post: operations["create_2"]; }; "/v2/public/billing/plans": { get: operations["getPlans"]; @@ -57,6 +76,9 @@ export interface paths { "/v2/public/billing/mt-credit-prices": { get: operations["getMtCreditPrices"]; }; + "/v2/projects/{projectId}/billing/order-translation/preferred-agency": { + get: operations["getPreferredAgency"]; + }; "/v2/organizations/{organizationId}/billing/subscription": { get: operations["getSubscription"]; }; @@ -86,12 +108,22 @@ export interface paths { "/v2/organizations/{organizationId}/billing/expected-usage": { get: operations["getExpectedUsage_1"]; }; + "/v2/organizations/{organizationId}/billing/expected-usage/{type}.csv": { + /** Returns CSV file with usage detail for each time period and it's usage. */ + get: operations["getExpectedUsageDetail"]; + }; "/v2/organizations/{organizationId}/billing/customer-portal": { get: operations["goToCustomerPortal"]; }; "/v2/organizations/{organizationId}/billing/billing-info": { get: operations["getBillingInfo"]; }; + "/v2/billing/translation-agency": { + get: operations["getAll"]; + }; + "/v2/billing/translation-agency/{agencyId}": { + get: operations["get"]; + }; "/v2/administration/billing/stripe-products": { get: operations["getStripeProducts"]; }; @@ -347,7 +379,8 @@ export interface components { | "plan_auto_assignment_organization_ids_not_in_for_organization_ids" | "task_not_found" | "task_not_finished" - | "task_not_open"; + | "task_not_open" + | "translation_agency_not_found"; params?: { [key: string]: unknown }[]; }; ErrorResponseBody: { @@ -401,6 +434,7 @@ export interface components { | "AI_PROMPT_CUSTOMIZATION" | "SLACK_INTEGRATION" | "TASKS" + | "ORDER_TRANSLATION" )[]; prices: components["schemas"]["PlanPricesModel"]; includedUsage: components["schemas"]["PlanIncludedUsageModel"]; @@ -450,6 +484,7 @@ export interface components { | "AI_PROMPT_CUSTOMIZATION" | "SLACK_INTEGRATION" | "TASKS" + | "ORDER_TRANSLATION" )[]; type: "PAY_AS_YOU_GO" | "FIXED" | "SLOTS_FIXED"; prices: components["schemas"]["PlanPricesModel"]; @@ -493,6 +528,28 @@ export interface components { prorationDate: number; endingBalance: number; }; + UpdateTranslationAgencyRequest: { + name: string; + description: string; + services: string[]; + url: string; + email: string; + }; + Avatar: { + large: string; + thumbnail: string; + }; + TranslationAgencyModel: { + /** Format: int64 */ + id: number; + name: string; + description?: string; + services: string[]; + url?: string; + avatar?: components["schemas"]["Avatar"]; + email?: string; + emailBcc: string[]; + }; PlanIncludedUsageRequest: { /** Format: int64 */ seats: number; @@ -527,6 +584,7 @@ export interface components { | "AI_PROMPT_CUSTOMIZATION" | "SLACK_INTEGRATION" | "TASKS" + | "ORDER_TRANSLATION" )[]; prices: components["schemas"]["PlanPricesRequest"]; includedUsage: components["schemas"]["PlanIncludedUsageRequest"]; @@ -563,6 +621,7 @@ export interface components { | "AI_PROMPT_CUSTOMIZATION" | "SLACK_INTEGRATION" | "TASKS" + | "ORDER_TRANSLATION" )[]; prices: components["schemas"]["PlanPricesModel"]; includedUsage: components["schemas"]["PlanIncludedUsageModel"]; @@ -591,6 +650,7 @@ export interface components { | "AI_PROMPT_CUSTOMIZATION" | "SLACK_INTEGRATION" | "TASKS" + | "ORDER_TRANSLATION" )[]; type: "PAY_AS_YOU_GO" | "FIXED" | "SLOTS_FIXED"; prices: components["schemas"]["PlanPricesRequest"]; @@ -628,6 +688,7 @@ export interface components { | "AI_PROMPT_CUSTOMIZATION" | "SLACK_INTEGRATION" | "TASKS" + | "ORDER_TRANSLATION" )[]; type: "PAY_AS_YOU_GO" | "FIXED" | "SLOTS_FIXED"; prices: components["schemas"]["PlanPricesModel"]; @@ -637,6 +698,31 @@ export interface components { stripeProductId: string; forOrganizationIds: number[]; }; + CreateTaskRequest: { + name: string; + description: string; + type: "TRANSLATE" | "REVIEW"; + /** + * Format: int64 + * @description Due to date in epoch format (milliseconds). + * @example 1661172869000 + */ + dueDate?: number; + /** + * Format: int64 + * @description Id of language, this task is attached to. + * @example 1 + */ + languageId: number; + assignees: number[]; + keys: number[]; + }; + CreateTranslationOrderRequest: { + /** Format: int64 */ + agencyId: number; + tasks: components["schemas"]["CreateTaskRequest"][]; + sendReadOnlyInvitation: boolean; + }; CloudSubscribeRequest: { /** * Format: int64 @@ -672,6 +758,13 @@ export interface components { BuyMoreCreditsModel: { url: string; }; + CreateTranslationAgencyRequest: { + name: string; + description: string; + services: string[]; + url: string; + email: string; + }; CollectionModelCloudPlanModel: { _embedded?: { plans?: components["schemas"]["CloudPlanModel"][]; @@ -689,6 +782,10 @@ export interface components { /** Format: int64 */ amount: number; }; + PreferredAgencyResponse: { + /** Format: int64 */ + preferredAgencyId?: number; + }; AverageProportionalUsageItemModel: { total: number; unusedQuantity: number; @@ -763,6 +860,27 @@ export interface components { vatNo?: string; email?: string; }; + PagedModelTranslationAgencyPublicModel: { + _embedded?: { + translationAgencies?: components["schemas"]["TranslationAgencyPublicModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; + TranslationAgencyPublicModel: { + /** Format: int64 */ + id: number; + name: string; + description?: string; + services: string[]; + url?: string; + avatar?: components["schemas"]["Avatar"]; + }; + PagedModelTranslationAgencyModel: { + _embedded?: { + translationAgencies?: components["schemas"]["TranslationAgencyModel"][]; + }; + page?: components["schemas"]["PageMetadata"]; + }; CollectionModelStripeProductModel: { _embedded?: { stripeProducts?: components["schemas"]["StripeProductModel"][]; @@ -779,11 +897,6 @@ export interface components { plans?: components["schemas"]["SelfHostedEePlanAdministrationModel"][]; }; }; - /** @example Links to avatar images */ - Avatar: { - large: string; - thumbnail: string; - }; PagedModelSimpleOrganizationModel: { _embedded?: { organizations?: components["schemas"]["SimpleOrganizationModel"][]; @@ -1110,17 +1223,17 @@ export interface operations { }; }; }; - getPlan: { + get_1: { parameters: { path: { - planId: number; + agencyId: number; }; }; responses: { /** OK */ 200: { content: { - "application/json": components["schemas"]["SelfHostedEePlanAdministrationModel"]; + "application/json": components["schemas"]["TranslationAgencyModel"]; }; }; /** Bad Request */ @@ -1157,17 +1270,17 @@ export interface operations { }; }; }; - updatePlan: { + update: { parameters: { path: { - planId: number; + agencyId: number; }; }; responses: { /** OK */ 200: { content: { - "application/json": components["schemas"]["SelfHostedEePlanAdministrationModel"]; + "application/json": components["schemas"]["TranslationAgencyModel"]; }; }; /** Bad Request */ @@ -1205,14 +1318,14 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["SelfHostedEePlanRequest"]; + "application/json": components["schemas"]["UpdateTranslationAgencyRequest"]; }; }; }; - deletePlan: { + delete: { parameters: { path: { - planId: number; + agencyId: number; }; }; responses: { @@ -1252,19 +1365,15 @@ export interface operations { }; }; }; - getPlan_1: { + uploadAvatar: { parameters: { path: { - planId: number; + agencyId: number; }; }; responses: { /** OK */ - 200: { - content: { - "application/json": components["schemas"]["CloudPlanAdministrationModel"]; - }; - }; + 200: unknown; /** Bad Request */ 400: { content: { @@ -1298,20 +1407,24 @@ export interface operations { }; }; }; + requestBody: { + content: { + "multipart/form-data": { + /** Format: binary */ + avatar: string; + }; + }; + }; }; - updatePlan_1: { + removeAvatar: { parameters: { path: { - planId: number; + agencyId: number; }; }; responses: { /** OK */ - 200: { - content: { - "application/json": components["schemas"]["CloudPlanAdministrationModel"]; - }; - }; + 200: unknown; /** Bad Request */ 400: { content: { @@ -1345,13 +1458,8 @@ export interface operations { }; }; }; - requestBody: { - content: { - "application/json": components["schemas"]["CloudPlanRequest"]; - }; - }; }; - deletePlan_1: { + getPlan: { parameters: { path: { planId: number; @@ -1359,7 +1467,11 @@ export interface operations { }; responses: { /** OK */ - 200: unknown; + 200: { + content: { + "application/json": components["schemas"]["SelfHostedEePlanAdministrationModel"]; + }; + }; /** Bad Request */ 400: { content: { @@ -1394,17 +1506,17 @@ export interface operations { }; }; }; - subscribe: { + updatePlan: { parameters: { path: { - organizationId: number; + planId: number; }; }; responses: { /** OK */ 200: { content: { - "application/json": components["schemas"]["SubscribeModel"]; + "application/json": components["schemas"]["SelfHostedEePlanAdministrationModel"]; }; }; /** Bad Request */ @@ -1442,23 +1554,19 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["CloudSubscribeRequest"]; + "application/json": components["schemas"]["SelfHostedEePlanRequest"]; }; }; }; - getSelfHostedEeSubscriptions: { + deletePlan: { parameters: { path: { - organizationId: number; + planId: number; }; }; responses: { /** OK */ - 200: { - content: { - "application/json": components["schemas"]["CollectionModelSelfHostedEeSubscriptionModel"]; - }; - }; + 200: unknown; /** Bad Request */ 400: { content: { @@ -1493,17 +1601,17 @@ export interface operations { }; }; }; - setupEeSubscription: { + getPlan_1: { parameters: { path: { - organizationId: number; + planId: number; }; }; responses: { /** OK */ 200: { content: { - "application/json": components["schemas"]["SubscribeModel"]; + "application/json": components["schemas"]["CloudPlanAdministrationModel"]; }; }; /** Bad Request */ @@ -1539,23 +1647,18 @@ export interface operations { }; }; }; - requestBody: { - content: { - "application/json": components["schemas"]["SelfHostedEeSubscribeRequest"]; - }; - }; }; - setupFreeEeSubscription: { + updatePlan_1: { parameters: { path: { - organizationId: number; + planId: number; }; }; responses: { /** OK */ 200: { content: { - "application/json": components["schemas"]["SelfHostedEeSubscriptionModel"]; + "application/json": components["schemas"]["CloudPlanAdministrationModel"]; }; }; /** Bad Request */ @@ -1593,23 +1696,19 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["SelfHostedEeFreeSubscribeRequest"]; + "application/json": components["schemas"]["CloudPlanRequest"]; }; }; }; - getBuyMoreCreditsCheckoutSessionUrl: { + deletePlan_1: { parameters: { path: { - organizationId: number; + planId: number; }; }; responses: { /** OK */ - 200: { - content: { - "application/json": components["schemas"]["BuyMoreCreditsModel"]; - }; - }; + 200: unknown; /** Bad Request */ 400: { content: { @@ -1643,20 +1742,16 @@ export interface operations { }; }; }; - requestBody: { - content: { - "application/json": components["schemas"]["BuyMoreCreditsRequest"]; + }; + addUsageItemsToInvoiceAndFinalizeIt: { + parameters: { + path: { + invoiceId: string; }; }; - }; - getPlans_1: { responses: { /** OK */ - 200: { - content: { - "application/json": components["schemas"]["CollectionModelSelfHostedEePlanAdministrationModel"]; - }; - }; + 200: unknown; /** Bad Request */ 400: { content: { @@ -1691,14 +1786,24 @@ export interface operations { }; }; }; - create: { + createTranslationOrder: { + parameters: { + query: { + filterState?: ( + | "UNTRANSLATED" + | "TRANSLATED" + | "REVIEWED" + | "DISABLED" + )[]; + filterOutdated?: boolean; + }; + path: { + projectId: number; + }; + }; responses: { /** OK */ - 200: { - content: { - "application/json": components["schemas"]["SelfHostedEePlanAdministrationModel"]; - }; - }; + 200: unknown; /** Bad Request */ 400: { content: { @@ -1734,16 +1839,21 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["SelfHostedEePlanRequest"]; + "application/json": components["schemas"]["CreateTranslationOrderRequest"]; }; }; }; - getPlans_2: { + subscribe: { + parameters: { + path: { + organizationId: number; + }; + }; responses: { /** OK */ 200: { content: { - "application/json": components["schemas"]["CollectionModelCloudPlanAdministrationModel"]; + "application/json": components["schemas"]["SubscribeModel"]; }; }; /** Bad Request */ @@ -1779,13 +1889,23 @@ export interface operations { }; }; }; + requestBody: { + content: { + "application/json": components["schemas"]["CloudSubscribeRequest"]; + }; + }; }; - create_1: { + getSelfHostedEeSubscriptions: { + parameters: { + path: { + organizationId: number; + }; + }; responses: { /** OK */ 200: { content: { - "application/json": components["schemas"]["CloudPlanAdministrationModel"]; + "application/json": components["schemas"]["CollectionModelSelfHostedEeSubscriptionModel"]; }; }; /** Bad Request */ @@ -1821,18 +1941,18 @@ export interface operations { }; }; }; - requestBody: { - content: { - "application/json": components["schemas"]["CloudPlanRequest"]; + }; + setupEeSubscription: { + parameters: { + path: { + organizationId: number; }; }; - }; - getPlans: { responses: { /** OK */ 200: { content: { - "application/json": components["schemas"]["CollectionModelCloudPlanModel"]; + "application/json": components["schemas"]["SubscribeModel"]; }; }; /** Bad Request */ @@ -1868,13 +1988,23 @@ export interface operations { }; }; }; + requestBody: { + content: { + "application/json": components["schemas"]["SelfHostedEeSubscribeRequest"]; + }; + }; }; - getMtCreditPrices: { + setupFreeEeSubscription: { + parameters: { + path: { + organizationId: number; + }; + }; responses: { /** OK */ 200: { content: { - "application/json": components["schemas"]["CollectionModelMtCreditsPriceModel"]; + "application/json": components["schemas"]["SelfHostedEeSubscriptionModel"]; }; }; /** Bad Request */ @@ -1910,8 +2040,13 @@ export interface operations { }; }; }; + requestBody: { + content: { + "application/json": components["schemas"]["SelfHostedEeFreeSubscribeRequest"]; + }; + }; }; - getSubscription: { + getBuyMoreCreditsCheckoutSessionUrl: { parameters: { path: { organizationId: number; @@ -1921,7 +2056,7 @@ export interface operations { /** OK */ 200: { content: { - "application/json": components["schemas"]["CloudSubscriptionModel"]; + "application/json": components["schemas"]["BuyMoreCreditsModel"]; }; }; /** Bad Request */ @@ -1957,21 +2092,482 @@ export interface operations { }; }; }; - }; - getExpectedUsage: { - parameters: { - path: { - organizationId: number; - subscriptionId: number; + requestBody: { + content: { + "application/json": components["schemas"]["BuyMoreCreditsRequest"]; }; }; - responses: { - /** OK */ - 200: { - content: { - "application/json": components["schemas"]["UsageModel"]; - }; - }; + }; + getAll_1: { + parameters: { + query: { + /** Zero-based page index (0..N) */ + page?: number; + /** The size of the page to be returned */ + 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: { + /** OK */ + 200: { + content: { + "application/json": components["schemas"]["PagedModelTranslationAgencyModel"]; + }; + }; + /** Bad Request */ + 400: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Unauthorized */ + 401: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Forbidden */ + 403: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Not Found */ + 404: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + }; + }; + create: { + responses: { + /** OK */ + 200: { + content: { + "application/json": components["schemas"]["TranslationAgencyModel"]; + }; + }; + /** Bad Request */ + 400: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Unauthorized */ + 401: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Forbidden */ + 403: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Not Found */ + 404: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateTranslationAgencyRequest"]; + }; + }; + }; + getPlans_1: { + responses: { + /** OK */ + 200: { + content: { + "application/json": components["schemas"]["CollectionModelSelfHostedEePlanAdministrationModel"]; + }; + }; + /** Bad Request */ + 400: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Unauthorized */ + 401: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Forbidden */ + 403: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Not Found */ + 404: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + }; + }; + create_1: { + responses: { + /** OK */ + 200: { + content: { + "application/json": components["schemas"]["SelfHostedEePlanAdministrationModel"]; + }; + }; + /** Bad Request */ + 400: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Unauthorized */ + 401: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Forbidden */ + 403: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Not Found */ + 404: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SelfHostedEePlanRequest"]; + }; + }; + }; + getPlans_2: { + responses: { + /** OK */ + 200: { + content: { + "application/json": components["schemas"]["CollectionModelCloudPlanAdministrationModel"]; + }; + }; + /** Bad Request */ + 400: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Unauthorized */ + 401: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Forbidden */ + 403: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Not Found */ + 404: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + }; + }; + create_2: { + responses: { + /** OK */ + 200: { + content: { + "application/json": components["schemas"]["CloudPlanAdministrationModel"]; + }; + }; + /** Bad Request */ + 400: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Unauthorized */ + 401: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Forbidden */ + 403: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Not Found */ + 404: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CloudPlanRequest"]; + }; + }; + }; + getPlans: { + responses: { + /** OK */ + 200: { + content: { + "application/json": components["schemas"]["CollectionModelCloudPlanModel"]; + }; + }; + /** Bad Request */ + 400: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Unauthorized */ + 401: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Forbidden */ + 403: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Not Found */ + 404: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + }; + }; + getMtCreditPrices: { + responses: { + /** OK */ + 200: { + content: { + "application/json": components["schemas"]["CollectionModelMtCreditsPriceModel"]; + }; + }; + /** Bad Request */ + 400: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Unauthorized */ + 401: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Forbidden */ + 403: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Not Found */ + 404: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + }; + }; + getPreferredAgency: { + parameters: { + path: { + projectId: number; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": components["schemas"]["PreferredAgencyResponse"]; + }; + }; + /** Bad Request */ + 400: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Unauthorized */ + 401: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Forbidden */ + 403: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Not Found */ + 404: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + }; + }; + getSubscription: { + parameters: { + path: { + organizationId: number; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": components["schemas"]["CloudSubscriptionModel"]; + }; + }; + /** Bad Request */ + 400: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Unauthorized */ + 401: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Forbidden */ + 403: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Not Found */ + 404: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + }; + }; + getExpectedUsage: { + parameters: { + path: { + organizationId: number; + subscriptionId: number; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": components["schemas"]["UsageModel"]; + }; + }; /** Bad Request */ 400: { content: { @@ -2349,6 +2945,55 @@ export interface operations { }; }; }; + /** Returns CSV file with usage detail for each time period and it's usage. */ + getExpectedUsageDetail: { + parameters: { + path: { + organizationId: number; + type: "SEATS" | "TRANSLATIONS"; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "text/csv": string; + }; + }; + /** Bad Request */ + 400: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Unauthorized */ + 401: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Forbidden */ + 403: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Not Found */ + 404: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + }; + }; goToCustomerPortal: { parameters: { path: { @@ -2443,6 +3088,106 @@ export interface operations { }; }; }; + getAll: { + parameters: { + query: { + /** Zero-based page index (0..N) */ + page?: number; + /** The size of the page to be returned */ + 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: { + /** OK */ + 200: { + content: { + "application/json": components["schemas"]["PagedModelTranslationAgencyPublicModel"]; + }; + }; + /** Bad Request */ + 400: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Unauthorized */ + 401: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Forbidden */ + 403: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Not Found */ + 404: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + }; + }; + get: { + parameters: { + path: { + agencyId: number; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": components["schemas"]["TranslationAgencyPublicModel"]; + }; + }; + /** Bad Request */ + 400: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Unauthorized */ + 401: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Forbidden */ + 403: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + /** Not Found */ + 404: { + content: { + "application/json": + | components["schemas"]["ErrorResponseTyped"] + | components["schemas"]["ErrorResponseBody"]; + }; + }; + }; + }; getStripeProducts: { responses: { /** OK */ @@ -2563,6 +3308,7 @@ export interface operations { | "AI_PROMPT_CUSTOMIZATION" | "SLACK_INTEGRATION" | "TASKS" + | "ORDER_TRANSLATION" )[]; }; }; diff --git a/webapp/src/translationTools/useFeatures.tsx b/webapp/src/translationTools/useFeatures.tsx index e65e4264d8..baf35fa198 100644 --- a/webapp/src/translationTools/useFeatures.tsx +++ b/webapp/src/translationTools/useFeatures.tsx @@ -34,6 +34,7 @@ export function useFeatures() { ), SLACK_INTEGRATION: t('billing_subscriptions_slack_integration'), TASKS: t('billing_subscriptions_tasks'), + ORDER_TRANSLATION: t('billing_subscriptions_order_translation'), } as const satisfies Record; } diff --git a/webapp/src/views/administration/AdministrationView.tsx b/webapp/src/views/administration/AdministrationView.tsx index 4fac1b3921..7c0e2511c1 100644 --- a/webapp/src/views/administration/AdministrationView.tsx +++ b/webapp/src/views/administration/AdministrationView.tsx @@ -4,14 +4,17 @@ import { PrivateRoute } from 'tg.component/common/PrivateRoute'; import { LINKS } from 'tg.constants/links'; import { AdministrationOrganizations } from './AdministrationOrganizations'; import { AdministrationUsers } from './AdministrationUsers'; -import { AdministrationEeLicenseView } from 'tg.ee/billing/administration/AdministrationEeLicenseView'; -import { AdministrationCloudPlansView } from 'tg.ee/billing/administration/AdministrationCloudPlansView'; -import { AdministrationCloudPlanEditView } from 'tg.ee/billing/administration/AdministrationCloudPlanEditView'; -import { AdministrationCloudPlanCreateView } from 'tg.ee/billing/administration/AdministrationCloudPlanCreateView'; -import { AdministrationEePlansView } from 'tg.ee/billing/administration/AdministrationEePlansView'; -import { AdministrationEePlanEditView } from 'tg.ee/billing/administration/AdministrationEePlanEditView'; -import { AdministrationEePlanCreateView } from 'tg.ee/billing/administration/AdministrationEePlanCreateView'; +import { AdministrationEeLicenseView } from 'tg.ee/billing/administration/subscriptionPlans/AdministrationEeLicenseView'; +import { AdministrationCloudPlansView } from 'tg.ee/billing/administration/subscriptionPlans/AdministrationCloudPlansView'; +import { AdministrationCloudPlanEditView } from 'tg.ee/billing/administration/subscriptionPlans/AdministrationCloudPlanEditView'; +import { AdministrationCloudPlanCreateView } from 'tg.ee/billing/administration/subscriptionPlans/AdministrationCloudPlanCreateView'; +import { AdministrationEePlansView } from 'tg.ee/billing/administration/subscriptionPlans/AdministrationEePlansView'; +import { AdministrationEePlanEditView } from 'tg.ee/billing/administration/subscriptionPlans/AdministrationEePlanEditView'; +import { AdministrationEePlanCreateView } from 'tg.ee/billing/administration/subscriptionPlans/AdministrationEePlanCreateView'; import { useUrlSearchState } from 'tg.hooks/useUrlSearchState'; +import { AdministrationEeTAView } from 'tg.ee/billing/administration/translationAgencies/AdministrationEeTAView'; +import { AdministrationEeTACreateView } from 'tg.ee/billing/administration/translationAgencies/AdministrationEeTACreateView'; +import { AdministrationEeTAEditView } from 'tg.ee/billing/administration/translationAgencies/AdministrationEeTAEditView'; export const AdministrationView = () => { const [search, setSearch] = useUrlSearchState('search'); @@ -34,6 +37,15 @@ export const AdministrationView = () => { + + + + + + + + + = ({ ]; if (config.billing.enabled) { + menuItems.push({ + link: LINKS.ADMINISTRATION_EE_TA.build(), + label: t('administration_ee_translation_agencies'), + }); menuItems.push({ link: LINKS.ADMINISTRATION_BILLING_CLOUD_PLANS.build(), label: t('administration_cloud_plans'), diff --git a/webapp/src/views/organizations/members/InvitationItem.tsx b/webapp/src/views/organizations/members/InvitationItem.tsx index 78449c50ca..32370fec1a 100644 --- a/webapp/src/views/organizations/members/InvitationItem.tsx +++ b/webapp/src/views/organizations/members/InvitationItem.tsx @@ -84,15 +84,17 @@ export const InvitationItem: React.FC = ({ invitation }) => { {translateRole(invitation.type)} - - - - - + {invitation.code && ( + + + + + + )} = ({ open, onClose }) => { invalidatePrefix: '/v2/organizations/{organizationId}/invitations', }); - const yupSchema = useMemo( - () => Validation.INVITE_DIALOG_ORGANIZATION(t), - [t] - ); - return ( = ({ open, onClose }) => { type: 'email' as 'email' | 'link', text: '', }} - validationSchema={yupSchema} + validationSchema={Validation.INVITE_DIALOG_ORGANIZATION(t)} validateOnMount={true} onSubmit={(data) => { invite.mutate( diff --git a/webapp/src/views/projects/members/ProjectMembersView.tsx b/webapp/src/views/projects/members/ProjectMembersView.tsx index 3b8deaddd3..02a5555e58 100644 --- a/webapp/src/views/projects/members/ProjectMembersView.tsx +++ b/webapp/src/views/projects/members/ProjectMembersView.tsx @@ -7,22 +7,35 @@ import { LINKS, PARAMS } from 'tg.constants/links'; import { useProject } from 'tg.hooks/useProject'; import { useApiQuery } from 'tg.service/http/useQueryApi'; import { ProjectLanguagesProvider } from 'tg.hooks/ProjectLanguagesProvider'; +import { useProjectPermissions } from 'tg.hooks/useProjectPermissions'; +import { useReportEvent } from 'tg.hooks/useReportEvent'; +import { QuickStartHighlight } from 'tg.component/layout/QuickStartGuide/QuickStartHighlight'; +import SearchField from 'tg.component/common/form/fields/SearchField'; +import { useUrlSearchState } from 'tg.hooks/useUrlSearchState'; +import { AgencyFilter } from './component/AgencyFilter'; import { MemberItem } from './component/MemberItem'; import { InviteDialog } from './component/InviteDialog'; import { InvitationItem } from './component/InvitationItem'; import { BaseProjectView } from '../BaseProjectView'; -import { useProjectPermissions } from 'tg.hooks/useProjectPermissions'; -import { useReportEvent } from 'tg.hooks/useReportEvent'; -import { QuickStartHighlight } from 'tg.component/layout/QuickStartGuide/QuickStartHighlight'; +import { useConfig, useEnabledFeatures } from 'tg.globalContext/helpers'; export const ProjectMembersView: FunctionComponent = () => { const project = useProject(); + const config = useConfig(); + const { isEnabled } = useEnabledFeatures(); + const showAgencyFilter = + config.billing.enabled && isEnabled('ORDER_TRANSLATION'); const { t } = useTranslate(); const [inviteOpen, setInviteOpen] = useState(false); const [search, setSearch] = useState(''); const [page, setPage] = useState(0); + const [filterAgecy, setFilterAgency] = useUrlSearchState('agency', { + array: true, + defaultVal: [], + }); + const membersLoadable = useApiQuery({ url: '/v2/projects/{projectId}/users', method: 'get', @@ -31,6 +44,7 @@ export const ProjectMembersView: FunctionComponent = () => { page, sort: ['name'], search, + filterAgency: filterAgecy?.map((a) => Number(a)), }, options: { keepPreviousData: true, @@ -128,11 +142,36 @@ export const ProjectMembersView: FunctionComponent = () => { offset={10} > + + + {t('project_menu_members')} + + + {showAgencyFilter && ( + Number(a))} + onChange={(value) => + setFilterAgency(value.map((a) => String(a))) + } + /> + )} + + + diff --git a/webapp/src/views/projects/members/component/AgencyFilter.tsx b/webapp/src/views/projects/members/component/AgencyFilter.tsx new file mode 100644 index 0000000000..ffd0ffe1ec --- /dev/null +++ b/webapp/src/views/projects/members/component/AgencyFilter.tsx @@ -0,0 +1,89 @@ +import { useRef } from 'react'; +import { useTranslate } from '@tolgee/react'; +import { Checkbox, MenuItem, styled } from '@mui/material'; + +import { useBillingApiQuery } from 'tg.service/http/useQueryApi'; +import { AgencyLabel } from 'tg.ee/orderTranslations/AgencyLabel'; +import { Select } from 'tg.component/common/Select'; + +const StyledPlaceholder = styled('span')` + opacity: 0.5; + font-size: 15px; +`; + +const StyledInputContent = styled('div')` + height: 23px; + text-wrap: nowrap; + display: flex; + gap: 8px; + align-items: center; + overflow: hidden; + max-width: 150px; +`; + +type Props = { + value: number[]; + onChange: (value: number[]) => void; +}; + +export const AgencyFilter = ({ value, onChange }: Props) => { + const agenciesLoadable = useBillingApiQuery({ + url: '/v2/billing/translation-agency', + method: 'get', + query: { + size: 1000, + }, + }); + + const { t } = useTranslate(); + const anchorEl = useRef(null); + const handleAgencyToggle = (id: number) => () => { + if (value.includes(id)) { + onChange(value.filter((l) => l !== id)); + } else { + onChange([...value, id]); + } + }; + + const data = agenciesLoadable.data?._embedded?.translationAgencies; + + return ( + + ); +}; diff --git a/webapp/src/views/projects/members/component/AgencySelect.tsx b/webapp/src/views/projects/members/component/AgencySelect.tsx new file mode 100644 index 0000000000..34d9bb248e --- /dev/null +++ b/webapp/src/views/projects/members/component/AgencySelect.tsx @@ -0,0 +1,51 @@ +import { Box, MenuItem } from '@mui/material'; + +import { useBillingApiQuery } from 'tg.service/http/useQueryApi'; +import { AgencyLabel } from 'tg.ee/orderTranslations/AgencyLabel'; +import { Select } from 'tg.component/common/Select'; +import { useTranslate } from '@tolgee/react'; + +type Props = { + value: number; + onChange: (value: number) => void; + error?: boolean; + helperText?: string; +}; + +export const AgencySelect = ({ value, onChange, error, helperText }: Props) => { + const { t } = useTranslate(); + const agenciesLoadable = useBillingApiQuery({ + url: '/v2/billing/translation-agency', + method: 'get', + query: { + size: 1000, + }, + }); + + const data = agenciesLoadable.data?._embedded?.translationAgencies; + + return ( + + ); +}; diff --git a/webapp/src/views/projects/members/component/InvitationItem.tsx b/webapp/src/views/projects/members/component/InvitationItem.tsx index 1884ba88c5..733a7d7ba5 100644 --- a/webapp/src/views/projects/members/component/InvitationItem.tsx +++ b/webapp/src/views/projects/members/component/InvitationItem.tsx @@ -1,5 +1,5 @@ import { T, useTranslate } from '@tolgee/react'; -import { IconButton, styled, Tooltip } from '@mui/material'; +import { Box, IconButton, styled, Tooltip } from '@mui/material'; import { XClose, Link02 } from '@untitled-ui/icons-react'; import { components } from 'tg.service/apiSchema.generated'; @@ -11,6 +11,7 @@ import { LanguagePermissionSummary } from 'tg.component/PermissionsSettings/Lang import { ScopesInfo } from 'tg.component/PermissionsSettings/ScopesInfo'; import { usePermissionTranslation } from 'tg.translationTools/usePermissionTranslation'; import { messageService } from 'tg.service/MessageService'; +import { AgencyLabel } from 'tg.ee/orderTranslations/AgencyLabel'; type UserAccountInProjectModel = components['schemas']['ProjectInvitationModel']; @@ -27,7 +28,7 @@ const StyledListItem = styled('div')` padding: ${({ theme }) => theme.spacing(1)}; flex-wrap: wrap; align-items: center; - justify-content: flex-end; + justify-content: space-between; `; const StyledItemText = styled('div')` @@ -78,17 +79,22 @@ export const InvitationItem: React.FC = ({ invitation }) => { const handleGetLink = () => { navigator.clipboard.writeText( LINKS.ACCEPT_INVITATION.buildWithOrigin({ - [PARAMS.INVITATION_CODE]: invitation.code, + [PARAMS.INVITATION_CODE]: invitation.code!, }) ); messageService.success(); }; return ( - - - {invitation.invitedUserName || invitation.invitedUserEmail}{' '} - + + + + {invitation.invitedUserName || invitation.invitedUserEmail}{' '} + + {invitation.permission.agency && ( + + )} + @@ -105,11 +111,13 @@ export const InvitationItem: React.FC = ({ invitation }) => { - - - - - + {invitation.code && ( + + + + + + )} theme.palette.tokens.background['paper-1']}; + border-radius: 3px; + padding: 2px 6px; +`; + type Props = { open: boolean; onClose: () => void; @@ -48,6 +58,28 @@ export const InviteDialog: React.FC = ({ open, onClose }) => { const { t } = useTranslate(); const project = useProject(); const langauges = useProjectLanguages(); + const config = useConfig(); + const { isEnabled } = useEnabledFeatures(); + + const agencyEnabled = + config.billing.enabled && isEnabled('ORDER_TRANSLATION'); + + const preferredAgencyLoadable = useBillingApiQuery({ + url: '/v2/projects/{projectId}/billing/order-translation/preferred-agency', + method: 'get', + options: { + enabled: agencyEnabled, + }, + path: { + projectId: project.id, + }, + }); + + const preferredAgency = preferredAgencyLoadable.isLoading + ? undefined + : preferredAgencyLoadable.data?.preferredAgencyId ?? false; + + const agencyTaskExists = typeof preferredAgency === 'number'; const initialPermissions: PermissionModel = { type: 'TRANSLATE', @@ -67,7 +99,7 @@ export const InviteDialog: React.FC = ({ open, onClose }) => { async function handleCreateInvitation(data: CreateInvitationData) { const result = await createInvitation(data); - if (!result.invitedUserEmail) { + if (!result.invitedUserEmail && result.code) { copy( LINKS.ACCEPT_INVITATION.buildWithOrigin({ [PARAMS.INVITATION_CODE]: result.code, @@ -88,9 +120,11 @@ export const InviteDialog: React.FC = ({ open, onClose }) => { { @@ -98,12 +132,14 @@ export const InviteDialog: React.FC = ({ open, onClose }) => { return handleCreateInvitation({ email: data.type === 'email' ? data.text : undefined, name: data.type === 'link' ? data.text : undefined, + agency: data.type === 'agency' ? data.agency : undefined, permissions: settingsState, }); } }} > {({ values, handleSubmit, isValid, ...formik }) => { + const disabled = values.type === 'agency' && !agencyTaskExists; return (
@@ -130,26 +166,76 @@ export const InviteDialog: React.FC = ({ open, onClose }) => { > {t('invite_type_link')} + {agencyEnabled && ( + + )} - - {({ field, meta }) => ( - - )} - + {values.type === 'agency' ? ( + preferredAgencyLoadable.data && + (!agencyTaskExists ? ( +
+ + {t('project_menu_tasks')} + + ), + orderButton: ( + + {' '} + {t('tasks_order_translation')} + + ), + }} + /> +
+ ) : ( + + {({ field, meta }) => { + return ( + + formik.setFieldValue(field.name, value) + } + error={Boolean(meta.touched && meta.error)} + helperText={meta.touched && meta.error} + /> + ); + }} + + )) + ) : ( + + {({ field, meta }) => ( + + )} + + )}
= ({ open, onClose }) => { onChange={setSettingsState} allLangs={langauges} hideNone + disabled={disabled} />
@@ -173,8 +260,9 @@ export const InviteDialog: React.FC = ({ open, onClose }) => { type="submit" data-cy="invitation-dialog-invite-button" loading={isLoading} + disabled={disabled} > - {values.type === 'email' + {values.type !== 'link' ? t('project_members_dialog_invite_button') : t('project_members_dialog_create_link_button')} diff --git a/webapp/src/views/projects/members/component/MemberItem.tsx b/webapp/src/views/projects/members/component/MemberItem.tsx index fcf9463195..775efda964 100644 --- a/webapp/src/views/projects/members/component/MemberItem.tsx +++ b/webapp/src/views/projects/members/component/MemberItem.tsx @@ -14,6 +14,7 @@ import { useProjectLanguages } from 'tg.hooks/useProjectLanguages'; import { LanguagePermissionSummary } from 'tg.component/PermissionsSettings/LanguagePermissionsSummary'; import { ScopesInfo } from 'tg.component/PermissionsSettings/ScopesInfo'; import { AvatarImg } from 'tg.component/common/avatar/AvatarImg'; +import { AgencyLabel } from 'tg.ee/orderTranslations/AgencyLabel'; type UserAccountInProjectModel = components['schemas']['UserAccountInProjectModel']; @@ -34,6 +35,10 @@ const StyledListItem = styled('div')` const StyledItemText = styled('div')` flex-grow: 1; padding: ${({ theme }) => theme.spacing(1)}; + display: flex; + align-items: center; + gap: 4px; + flex-wrap: wrap; `; const StyledItemActions = styled('div')` @@ -94,6 +99,9 @@ export const MemberItem: React.FC = ({ user }) => { {user.organizationRole && ( )} + {user.directPermission?.agency && ( + + )} diff --git a/webapp/src/views/projects/members/component/useCreateInvitation.tsx b/webapp/src/views/projects/members/component/useCreateInvitation.tsx index b08277dd12..8c6f226b37 100644 --- a/webapp/src/views/projects/members/component/useCreateInvitation.tsx +++ b/webapp/src/views/projects/members/component/useCreateInvitation.tsx @@ -18,6 +18,7 @@ export type CreateInvitationData = { permissions: PermissionSettingsState; email?: string; name?: string; + agency?: string; }; export const useCreateInvitation = ({ projectId, allLangs }: Props) => { @@ -30,7 +31,12 @@ export const useCreateInvitation = ({ projectId, allLangs }: Props) => { const messages = useMessage(); return { - async createInvitation({ permissions, email, name }: CreateInvitationData) { + async createInvitation({ + permissions, + email, + name, + agency, + }: CreateInvitationData) { if (permissions.tab === 'advanced' && permissions.advancedState.scopes) { if (permissions.advancedState.scopes.length === 0) { messages.error(); @@ -54,6 +60,7 @@ export const useCreateInvitation = ({ projectId, allLangs }: Props) => { 'application/json': { email, name, + agencyId: Number(agency), scopes: permissions.advancedState.scopes, ...languagePermissions, }, @@ -83,6 +90,7 @@ export const useCreateInvitation = ({ projectId, allLangs }: Props) => { type: role, email, name, + agencyId: Number(agency), ...languagePermissions, }, }, diff --git a/webapp/src/views/projects/translations/BatchOperations/BatchOperations.tsx b/webapp/src/views/projects/translations/BatchOperations/BatchOperations.tsx index 5baf538537..7aa22570dc 100644 --- a/webapp/src/views/projects/translations/BatchOperations/BatchOperations.tsx +++ b/webapp/src/views/projects/translations/BatchOperations/BatchOperations.tsx @@ -24,6 +24,7 @@ import { OperationExportTranslations } from './OperationExportTranslations'; import { OperationTaskCreate } from './OperationTaskCreate'; import { OperationTaskAddKeys } from './OperationTaskAddKeys'; import { OperationTaskRemoveKeys } from './OperationTaskRemoveKeys'; +import { OperationOrderTranslation } from './OperationOrderTranslation'; const StyledContainer = styled('div')` position: absolute; @@ -127,6 +128,8 @@ export const BatchOperations = ({ open, onClose }: Props) => { return ; case 'task_remove_keys': return ; + case 'order_translation': + return ; case 'add_tags': return ; case 'remove_tags': diff --git a/webapp/src/views/projects/translations/BatchOperations/BatchSelect.tsx b/webapp/src/views/projects/translations/BatchOperations/BatchSelect.tsx index 46f74664ea..994728b37d 100644 --- a/webapp/src/views/projects/translations/BatchOperations/BatchSelect.tsx +++ b/webapp/src/views/projects/translations/BatchOperations/BatchSelect.tsx @@ -43,6 +43,7 @@ export const BatchSelect = ({ value, onChange }: Props) => { const { features } = useEnabledFeatures(); const taskFeature = features.includes('TASKS'); + const orderTranslationsFeature = features.includes('ORDER_TRANSLATION'); const options: { id: BatchActions; @@ -105,6 +106,12 @@ export const BatchSelect = ({ value, onChange }: Props) => { enabled: canEditTasks, hidden: !prefilteredTask || !taskFeature, }, + { + id: 'order_translation', + label: t('batch_operations_order_translation'), + enabled: canEditTasks, + hidden: !orderTranslationsFeature, + }, { id: 'add_tags', label: t('batch_operations_add_tags'), diff --git a/webapp/src/views/projects/translations/BatchOperations/OperationOrderTranslation.tsx b/webapp/src/views/projects/translations/BatchOperations/OperationOrderTranslation.tsx new file mode 100644 index 0000000000..51abbb870d --- /dev/null +++ b/webapp/src/views/projects/translations/BatchOperations/OperationOrderTranslation.tsx @@ -0,0 +1,55 @@ +import { useState } from 'react'; +import { useProject } from 'tg.hooks/useProject'; +import { User } from 'tg.component/UserAccount'; +import { OrderTranslationsDialog } from 'tg.ee/orderTranslations/OrderTranslationsDialog'; + +import { OperationProps } from './types'; +import { BatchOperationsSubmit } from './components/BatchOperationsSubmit'; +import { OperationContainer } from './components/OperationContainer'; +import { useTranslationsSelector } from '../context/TranslationsContext'; +import { getPreselectedLanguagesIds } from './getPreselectedLanguages'; + +type Props = OperationProps; + +export const OperationOrderTranslation = ({ disabled, onFinished }: Props) => { + const project = useProject(); + const [dialogOpen, setDialogOpen] = useState(true); + + const allLanguages = useTranslationsSelector((c) => c.languages) ?? []; + const selection = useTranslationsSelector((c) => c.selection); + const translationsLanguages = useTranslationsSelector( + (c) => c.translationsLanguages + ); + + const languageAssignees = {} as Record; + const selectedLanguages = getPreselectedLanguagesIds( + allLanguages.filter((l) => !l.base), + translationsLanguages ?? [] + ); + + selectedLanguages.forEach((langId) => { + languageAssignees[langId] = []; + }); + + return ( + + setDialogOpen(true)} + /> + setDialogOpen(false)} + initialValues={{ + selection, + languageAssignees, + languages: selectedLanguages, + }} + allLanguages={allLanguages} + projectId={project.id} + onFinished={onFinished} + /> + + ); +}; diff --git a/webapp/src/views/projects/translations/BatchOperations/types.ts b/webapp/src/views/projects/translations/BatchOperations/types.ts index 77f2696e70..dece91dd5f 100644 --- a/webapp/src/views/projects/translations/BatchOperations/types.ts +++ b/webapp/src/views/projects/translations/BatchOperations/types.ts @@ -9,6 +9,7 @@ export type BatchActions = | 'task_create' | 'task_add_keys' | 'task_remove_keys' + | 'order_translation' | 'add_tags' | 'remove_tags' | 'change_namespace'