Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make namespaces opt-in #2786

Merged
merged 24 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
45a20e1
chore: Remove unused `keyService`
StaNov Dec 10, 2024
0259c3f
BE: saving `useNamespaces` option
Nov 25, 2024
73a0de6
BE: Enable namespaces for projects which already use namespaces
Nov 27, 2024
e77caea
BE: Forbid using namespaces if the feature is disabled, useful for pl…
StaNov Dec 2, 2024
273adcf
FE: Checkbox in settings
Nov 26, 2024
026f5b7
FE: Do not show snackbar error on failed validation in project settings
StaNov Dec 2, 2024
5b69297
FE: Hide default namespace checkbox if `useNamespaces` is false
Nov 27, 2024
4085b0b
FE: Hide namespace dropdown in edit key
Nov 26, 2024
f4a4d59
FE: Hide namespace in filtering of keys
Nov 27, 2024
4d2b7bc
FE: Hide namespace in export dialog
Nov 27, 2024
73f3b9e
FE: Hide namespace choose column on the import page
StaNov Dec 4, 2024
83013f8
FE: Hide change namespace option from batch operations if namespaces …
StaNov Dec 10, 2024
1f01a5f
BE Import: Add new column `detectedNamespace`
StaNov Dec 16, 2024
29e1493
BE Import: `processFiles` separated to public and private part
StaNov Dec 16, 2024
5545fc7
BE Import: Show warning when namespaces are detected but feature is off
StaNov Dec 16, 2024
2724c25
E2E: Fix cypress tests which require namespaces to be enabled
StaNov Dec 16, 2024
6b836c7
E2E: Cypress tests for existence of edit key namespace selectbox
StaNov Dec 17, 2024
d6c86e1
E2E: Hidden default namespace selectbox
StaNov Dec 17, 2024
d361980
E2E: Import, add files
StaNov Dec 17, 2024
2690ede
E2E: Batch operation permission testing needs namespaces enabled
StaNov Dec 17, 2024
d9d765b
BE: Delete imports on useNamespaces change
StaNov Dec 18, 2024
a61ecef
BE: Handling errors the same way as warnings
StaNov Dec 18, 2024
3c2c5c1
BE Import: Translated warning box as a standalone component
StaNov Dec 18, 2024
3c0951d
fix: make frontend beautiful
stepan662 Dec 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -82,24 +82,27 @@ class V2ImportController(
filteredFiles.map {
ImportFileDto(it.originalFilename ?: "", it.inputStream.readAllBytes())
}
val errors =
val (errors, warnings) =
importService.addFiles(
files = fileDtos,
project = projectHolder.projectEntity,
userAccount = authenticationFacade.authenticatedUserEntity,
params = params,
)
return getImportAddFilesResultModel(errors)
return getImportAddFilesResultModel(errors, warnings)
}

private fun getImportAddFilesResultModel(errors: List<ErrorResponseBody>): ImportAddFilesResultModel {
private fun getImportAddFilesResultModel(
errors: List<ErrorResponseBody>,
warnings: List<ErrorResponseBody>,
): ImportAddFilesResultModel {
val result: PagedModel<ImportLanguageModel>? =
try {
this.getImportResult(PageRequest.of(0, 100))
} catch (e: NotFoundException) {
null
}
return ImportAddFilesResultModel(errors, result)
return ImportAddFilesResultModel(errors, warnings, result)
}

@PutMapping("/apply")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import io.tolgee.activity.RequestActivity
import io.tolgee.activity.data.ActivityType
import io.tolgee.api.v2.controllers.IController
import io.tolgee.component.KeyComplexEditHelper
import io.tolgee.constants.Message
import io.tolgee.dtos.cacheable.LanguageDto
import io.tolgee.dtos.queryResults.KeyView
import io.tolgee.dtos.request.GetKeysRequestDto
Expand All @@ -16,6 +17,7 @@ import io.tolgee.dtos.request.key.DeleteKeysDto
import io.tolgee.dtos.request.key.EditKeyDto
import io.tolgee.dtos.request.translation.ImportKeysDto
import io.tolgee.dtos.request.translation.importKeysResolvable.ImportKeysResolvableDto
import io.tolgee.dtos.request.validators.exceptions.ValidationException
import io.tolgee.exceptions.NotFoundException
import io.tolgee.hateoas.key.KeyImportResolvableResultModel
import io.tolgee.hateoas.key.KeyModel
Expand Down Expand Up @@ -114,6 +116,7 @@ class KeyController(
checkTranslatePermission(dto)
checkCanStoreBigMeta(dto)
checkStateChangePermission(dto)
checkNamespaceFeature(dto.namespace)

val key = keyService.create(projectHolder.projectEntity, dto)
return ResponseEntity(keyWithDataModelAssembler.toModel(key), HttpStatus.CREATED)
Expand Down Expand Up @@ -162,6 +165,7 @@ class KeyController(
): KeyModel {
val key = keyService.findOptional(id).orElseThrow { NotFoundException() }
key.checkInProject()
checkNamespaceFeature(dto.namespace)
keyService.edit(id, dto)
val view = KeyView(key.id, key.name, key?.namespace?.name, key.keyMeta?.description, key.keyMeta?.custom)
return keyModelAssembler.toModel(view)
Expand Down Expand Up @@ -194,6 +198,7 @@ class KeyController(
@RequestBody @Valid
dto: ComplexEditKeyDto,
): KeyWithDataModel {
checkNamespaceFeature(dto.namespace)
return KeyComplexEditHelper(applicationContext, id, dto).doComplexUpdate()
}

Expand Down Expand Up @@ -382,4 +387,10 @@ class KeyController(
projectHolder.projectEntity.checkScreenshotsUploadPermission()
}
}

private fun checkNamespaceFeature(namespace: String?) {
if (!projectHolder.projectEntity.useNamespaces && namespace != null) {
throw ValidationException(Message.NAMESPACE_CANNOT_BE_USED_WHEN_FEATURE_IS_DISABLED)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ import org.springframework.hateoas.server.core.Relation
@Relation(collectionRelation = "fileIssues", itemRelation = "fileIssue")
open class ImportAddFilesResultModel(
val errors: List<ErrorResponseBody>,
val warnings: List<ErrorResponseBody>,
val result: PagedModel<ImportLanguageModel>?,
) : RepresentationModel<ImportAddFilesResultModel>()
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ open class ProjectModel(
val avatar: Avatar?,
val organizationOwner: SimpleOrganizationModel?,
val baseLanguage: LanguageModel?,
val useNamespaces: Boolean,
val defaultNamespace: NamespaceModel?,
val organizationRole: OrganizationRoleType?,
@Schema(description = "Current user's direct permission", example = "MANAGE")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class ProjectModelAssembler(
organizationRole = view.organizationRole,
organizationOwner = view.organizationOwner.let { simpleOrganizationModelAssembler.toModel(it) },
baseLanguage = baseLanguage.let { languageModelAssembler.toModel(LanguageDto.fromEntity(it, it.id)) },
useNamespaces = view.useNamespaces,
defaultNamespace = defaultNamespace,
directPermission = view.directPermission?.let { permissionModelAssembler.toModel(it) },
computedPermission = computedPermissionModelAssembler.toModel(computedPermissions),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class StartupImportCommandLineRunnerTest : AbstractSpringTest() {
assertThat(translationService.getAllByLanguageId(it.id)).hasSize(10)
}
assertThat(project.apiKeys.first().keyHash).isEqualTo("Zy98PdrKTEla1Ix7I1WbZPRoIDttk+Byk77tEjgRIzs=")
assertThat(project.useNamespaces).isFalse()
}
}

Expand All @@ -70,6 +71,7 @@ class StartupImportCommandLineRunnerTest : AbstractSpringTest() {
assertThat(projects).isNotEmpty
val project = projects.first()
project.namespaces.assert.hasSize(7)
assertThat(project.useNamespaces).isTrue()
}
}

Expand All @@ -80,6 +82,7 @@ class StartupImportCommandLineRunnerTest : AbstractSpringTest() {
assertThat(projects).isNotEmpty
val project = projects.first()
project.baseLanguage!!.tag.assert.isEqualTo("de")
assertThat(project.useNamespaces).isFalse()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ class SingleStepImportControllerTest : ProjectAuthControllerTest("/v2/projects/"
@ProjectJWTAuthTestMethod
fun `maps namespace`() {
saveAndPrepare()
enableNamespaces()
performImport(
projectId = testData.project.id,
listOf(Pair(jsonFileName, simpleJson)),
Expand All @@ -152,10 +153,35 @@ class SingleStepImportControllerTest : ProjectAuthControllerTest("/v2/projects/"
}
}

@Test
@ProjectJWTAuthTestMethod
fun `namespace mapping fails if namespaces are disabled`() {
saveAndPrepare()
performImport(
projectId = testData.project.id,
listOf(Pair(jsonFileName, simpleJson)),
getFileMappings(jsonFileName, namespace = "test"),
).andIsBadRequest.andHasErrorMessage(Message.NAMESPACE_CANNOT_BE_USED_WHEN_FEATURE_IS_DISABLED)
}
StaNov marked this conversation as resolved.
Show resolved Hide resolved

@Test
@ProjectJWTAuthTestMethod
fun `detected namespaces are ignored if namespaces are disabled`() {
saveAndPrepare()
performImport(
projectId = testData.project.id,
listOf(Pair("test-namespace/$jsonFileName", simpleJson)),
).andIsOk
executeInNewTransaction {
getTestTranslation(namespace = null).assert.isNotNull
}
}

@Test
@ProjectJWTAuthTestMethod
fun `maps null namespace from non-null mapping`() {
saveAndPrepare()
enableNamespaces()
val fileName = "guessed-ns/en.json"
performImport(
projectId = testData.project.id,
Expand All @@ -166,6 +192,13 @@ class SingleStepImportControllerTest : ProjectAuthControllerTest("/v2/projects/"
executeInNewTransaction {
getTestTranslation().assert.isNotNull
}

performImport(
projectId = testData.project.id,
listOf(Pair(fileName, simpleJson)),
getFileMappings(fileName, namespace = ""),
).andIsOk

performImport(
projectId = testData.project.id,
listOf(Pair(fileName, simpleJson)),
Expand Down Expand Up @@ -366,4 +399,10 @@ class SingleStepImportControllerTest : ProjectAuthControllerTest("/v2/projects/"
userAccount = testData.user
projectSupplier = { testData.project }
}

private fun enableNamespaces() {
val fetchedProject = projectService.find(testData.project.id)!!
fetchedProject.useNamespaces = true
projectService.save(fetchedProject)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import io.tolgee.testing.assert
import io.tolgee.testing.assertions.Assertions.assertThat
import io.tolgee.util.InMemoryFileStorage
import io.tolgee.util.performImport
import net.javacrumbs.jsonunit.core.internal.Node.JsonMap
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Value
Expand Down Expand Up @@ -241,14 +242,18 @@ class V2ImportControllerAddFilesTest : ProjectAuthControllerTest("/v2/projects/"
@Test
fun `pre-selects namespaces and languages correctly`() {
val base = dbPopulator.createBase()
base.project.useNamespaces = true
projectService.save(base.project)
commitTransaction()
tolgeeProperties.maxTranslationTextLength = 20

executeInNewTransaction {
performImport(
projectId = base.project.id,
listOf(Pair("namespaces.zip", namespacesZip)),
).andIsOk
).andIsOk.andAssertThatJson {
node("warnings").isArray.isEmpty()
}
}

executeInNewTransaction {
Expand All @@ -262,6 +267,27 @@ class V2ImportControllerAddFilesTest : ProjectAuthControllerTest("/v2/projects/"
}
}

@Test
fun `returns warning and blank namespaces when namespaces are detected but disabled`() {
val base = dbPopulator.createBase()
base.project.useNamespaces = false
projectService.save(base.project)
commitTransaction()

executeInNewTransaction {
performImport(
projectId = base.project.id,
listOf(Pair("namespaces.zip", namespacesZip)),
).andIsOk.andAssertThatJson {
node("warnings").isArray.hasSize(1)
node("warnings[0].code").isEqualTo("namespace_cannot_be_used_when_feature_is_disabled")
node("result._embedded.languages").isArray.allSatisfy {
(it as JsonMap)["namespace"].assert.isNull()
}
}
}
}

@Test
fun `works fine with Mac generated zip`() {
val base = dbPopulator.createBase()
Expand Down Expand Up @@ -322,6 +348,23 @@ class V2ImportControllerAddFilesTest : ProjectAuthControllerTest("/v2/projects/"
}
}

@Test
fun `import gets deleted after namespaces feature is toggled`() {
val base = dbPopulator.createBase()

performImport(projectId = base.project.id, listOf("simple.json" to simpleJson))
.andIsOk

assertThat(importService.getAllByProject(base.project.id)).isNotEmpty()

val project = projectService.get(base.project.id)
project.useNamespaces = !project.useNamespaces
projectService.save(project)
commitTransaction()

assertThat(importService.getAllByProject(project.id)).isEmpty()
}

private fun validateSavedJsonImportData(
project: Project,
userAccount: UserAccount,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class KeyControllerWithNamespacesTest : ProjectAuthControllerTest("/v2/projects/
@ProjectJWTAuthTestMethod
@Test
fun `creates key and namespace`() {
enableNamespaces()
performProjectAuthPost("keys", mapOf("name" to "super_key", "namespace" to "new_ns"))
.andIsCreated.andAssertThatJson {
node("name").isEqualTo("super_key")
Expand All @@ -50,6 +51,7 @@ class KeyControllerWithNamespacesTest : ProjectAuthControllerTest("/v2/projects/
@ProjectJWTAuthTestMethod
@Test
fun `blank namespace doesn't create ns`() {
enableNamespaces()
performProjectAuthPost("keys", CreateKeyDto(name = "super_key", namespace = ""))
.andIsCreated
namespaceService.find("", project.id).assert.isNull()
Expand All @@ -58,6 +60,7 @@ class KeyControllerWithNamespacesTest : ProjectAuthControllerTest("/v2/projects/
@ProjectJWTAuthTestMethod
@Test
fun `creates key in existing namespace`() {
enableNamespaces()
performProjectAuthPost("keys", CreateKeyDto(name = "super_key", namespace = "ns-1"))
.andIsCreated.andAssertThatJson {
node("name").isEqualTo("super_key")
Expand All @@ -69,6 +72,7 @@ class KeyControllerWithNamespacesTest : ProjectAuthControllerTest("/v2/projects/
@ProjectJWTAuthTestMethod
@Test
fun `does not create key when not unique in ns`() {
enableNamespaces()
performProjectAuthPost("keys", CreateKeyDto(name = "key", "ns-1"))
.andAssertError
.isCustomValidation.hasMessage("key_exists")
Expand All @@ -77,6 +81,7 @@ class KeyControllerWithNamespacesTest : ProjectAuthControllerTest("/v2/projects/
@ProjectJWTAuthTestMethod
@Test
fun `updates key in ns`() {
enableNamespaces()
performProjectAuthPut("keys/${testData.keyInNs1.id}", EditKeyDto(name = "super_k", "ns-2"))
.andIsOk.andAssertThatJson {
node("id").isValidId
Expand Down Expand Up @@ -109,6 +114,7 @@ class KeyControllerWithNamespacesTest : ProjectAuthControllerTest("/v2/projects/
@ProjectJWTAuthTestMethod
@Test
fun `throws error when moving key to default ns where a key with same name already exists`() {
enableNamespaces()
val keyName = "super_ultra_cool_key"
val namespace = "super_ultra_cool_namespace"

Expand Down Expand Up @@ -155,6 +161,7 @@ class KeyControllerWithNamespacesTest : ProjectAuthControllerTest("/v2/projects/
@ProjectJWTAuthTestMethod
@Test
fun `deletes ns when empty on update`() {
enableNamespaces()
performProjectAuthPut("keys/${testData.singleKeyInNs2.id}", EditKeyDto(name = "super_k", "ns-1"))
.andIsOk
namespaceService.find("ns-2", project.id).assert.isNull()
Expand All @@ -171,4 +178,57 @@ class KeyControllerWithNamespacesTest : ProjectAuthControllerTest("/v2/projects/

namespaceService.getAllInProject(testData.projectBuilder.self.id).assert.isEmpty()
}

@ProjectJWTAuthTestMethod
@Test
fun `key with namespace cannot be created when useNamespaces feature is disabled`() {
performProjectAuthPost("keys", mapOf("name" to "super_key", "namespace" to ""))
.andIsCreated.andAssertThatJson {
node("namespace").isNull()
}

performProjectAuthPost("keys", mapOf("name" to "super_key", "namespace" to "new_ns"))
.andIsBadRequest
.andAssertError
.isCustomValidation.hasMessage("namespace_cannot_be_used_when_feature_is_disabled")
}

@ProjectJWTAuthTestMethod
@Test
fun `key with namespace cannot be edited when useNamespaces feature is disabled`() {
performProjectAuthPut("keys/${testData.keyWithoutNs.id}", EditKeyDto(name = "super_k", ""))
.andIsOk.andAssertThatJson {
node("namespace").isNull()
}

performProjectAuthPut("keys/${testData.keyWithoutNs.id}", EditKeyDto(name = "super_k", "ns-2"))
.andIsBadRequest
.andAssertError
.isCustomValidation.hasMessage("namespace_cannot_be_used_when_feature_is_disabled")
}

@ProjectJWTAuthTestMethod
@Test
fun `key with namespace cannot be complex-edited when useNamespaces feature is disabled`() {
performProjectAuthPut(
"keys/${testData.keyWithoutNs.id}/complex-update",
mapOf("name" to "new-name", "namespace" to ""),
).andIsOk.andAssertThatJson {
node("namespace").isNull()
}

performProjectAuthPut(
"keys/${testData.keyWithoutNs.id}/complex-update",
mapOf("name" to "new-name", "namespace" to "ns-2"),
)
.andIsBadRequest
.andAssertError
.isCustomValidation.hasMessage("namespace_cannot_be_used_when_feature_is_disabled")
}
StaNov marked this conversation as resolved.
Show resolved Hide resolved

private fun enableNamespaces() {
val projectFetched = projectService.get(project.id)
projectFetched.useNamespaces = true
projectService.save(projectFetched)
}
}
Loading
Loading