-
-
Notifications
You must be signed in to change notification settings - Fork 305
feat: glossary import and export #3268
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
base: main
Are you sure you want to change the base?
Conversation
WalkthroughAdds glossary import/export: new backend controllers, services, CSV parser/exporter, repository methods, activity/message enums, and delimiter detection; frontend import dialog, hooks, drag/drop file components, updated glossary UI, tests, and E2E flows; streaming CSV export and multipart import (replace/add) with bulk persistence. Changes
Sequence Diagram(s)sequenceDiagram
participant UI as User Interface
participant Dialog as GlossaryImportDialog
participant API as GlossaryImportController
participant Service as GlossaryImportService
participant Parser as GlossaryCSVParser
participant Repo as GlossaryTermRepository
UI->>Dialog: select CSV & submit (file + remove_existing_terms)
Dialog->>API: POST /v2/organizations/{org}/glossaries/{id}/import (multipart)
API->>Service: importCsv(glossary, inputStream)
Service->>Parser: parse(inputStream) (delimiter detect)
Parser-->>Service: List<ImportGlossaryTerm>
alt remove_existing_terms == true
Service->>Repo: deleteAllByGlossary(glossary)
end
Service->>Repo: saveAll(terms), saveAll(translations)
Service-->>API: imported count
API-->>Dialog: GlossaryImportResult
Dialog-->>UI: success / close
sequenceDiagram
participant UI as User Interface
participant Hook as useGlossaryExport
participant API as GlossaryExportController
participant Service as GlossaryExportService
participant Exporter as GlossaryCSVExporter
UI->>Hook: triggerExport()
Hook->>API: GET /v2/organizations/{org}/glossaries/{id}/export
API->>Service: exportCsv(glossary)
Service->>Exporter: export() -> InputStream
Exporter-->>Service: CSV InputStream
Service-->>API: InputStream
API-->>Hook: streaming response (blob)
Hook->>UI: download CSV file
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
🧹 Nitpick comments (6)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/ImportGlossaryTerm.kt (1)
7-10
: Verify if flag mutability is necessary.The flag properties are declared as
var
, making them mutable after construction. If these properties don't need to be modified after theImportGlossaryTerm
is created, consider changing them toval
for immutability and thread safety.If mutability is not required, apply this diff:
val term: String?, val description: String?, val translations: Map<String, String>, - var flagNonTranslatable: Boolean?, - var flagCaseSensitive: Boolean?, - var flagAbbreviation: Boolean?, - var flagForbiddenTerm: Boolean?, + val flagNonTranslatable: Boolean?, + val flagCaseSensitive: Boolean?, + val flagAbbreviation: Boolean?, + val flagForbiddenTerm: Boolean?,ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryImportController.kt (1)
52-54
: Consider exposing CSV delimiter as an API parameter.The CSV delimiter is currently hardcoded to comma (default in
GlossaryImportService.importCsv
). Consider adding an optional request parameter to support other delimiters (e.g., semicolon, tab) for broader CSV format compatibility.Example enhancement:
@RequestParam(name = "override_existing_terms", required = false, defaultValue = "false") overrideExistingTerms: Boolean, + @RequestParam(name = "delimiter", required = false, defaultValue = ",") + delimiter: String, ): GlossaryImportResult { val organization = organizationHolder.organization val glossary = glossaryService.get(organization.id, glossaryId) if (overrideExistingTerms) { glossaryTermService.deleteAllByGlossary(glossary) } val imported = file.inputStream.use { input -> - glossaryImportService.importCsv(glossary, input) + glossaryImportService.importCsv(glossary, input, delimiter.firstOrNull() ?: ',') } return GlossaryImportResult(imported) }ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryExportController.kt (1)
40-64
: LGTM! Consider more explicit resource management.The export implementation is correct and uses proper HTTP headers for CSV download. The streaming approach is efficient for large exports.
For more explicit resource management, consider using Kotlin's
use
for automatic stream closure even if exceptions occur:return ResponseEntity.ok().headers(headers).body( streamingResponseBodyProvider.createStreamingResponseBody { out: OutputStream -> - IOUtils.copy(stream, out) - stream.close() - out.close() + stream.use { input -> + IOUtils.copy(input, out) + } }, )Note: The OutputStream
out
is managed by Spring's streaming response framework and doesn't need explicit closing in the lambda.ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryExportService.kt (1)
13-17
: LGTM! Consider pagination for very large glossaries.The export implementation is straightforward and correct. All terms are loaded into memory before CSV generation.
For organizations with very large glossaries (thousands of terms), loading all data at once could cause memory pressure. Consider implementing batch processing or streaming if performance issues arise in production. For typical use cases, the current approach should be sufficient.
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt (1)
15-16
: Stabilize column order for language tagsSets can be unordered; sort for consistent exports.
- val languageTagsWithoutBaseLanguage by lazy { languageTags - glossary.baseLanguageTag } + val languageTagsWithoutBaseLanguage by lazy { (languageTags - glossary.baseLanguageTag).toList().sorted() }Ensure tests (if any) expect the new deterministic order (sorted lexicographically).
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (1)
14-17
: Specify UTF‑8 and close the CSVReader/InputStreamCurrent code uses platform default charset and never closes the reader/input stream. Create the reader with UTF‑8 and close it within parse().
Suggested refactor inside parse():
fun parse(): List<ImportGlossaryTerm> { val csvParser = CSVParserBuilder().withSeparator(delimiter).build() InputStreamReader(input, StandardCharsets.UTF_8).use { isr -> CSVReaderBuilder(isr).withCSVParser(csvParser).build().use { csv -> val rawRows = csv.readAll() if (rawRows.isEmpty()) return emptyList() val headers = rawRows.first().map { it.trim() } val idxTerm = headers.indexOfFirst { it.equals("Term", ignoreCase = true) }.takeIf { it >= 0 } // ...compute other indices similarly... val specialHeaderIndices = listOfNotNull(idxTerm, /* ... */) val idxTranslations = (0 until headers.size).filter { it !in specialHeaderIndices } return rawRows.drop(1).mapNotNull { row -> // build ImportGlossaryTerm using safe access as above } } } }And add imports:
import java.io.InputStreamReader import java.nio.charset.StandardCharsetsConfirm callers aren’t expected to close the InputStream themselves after parse(); with this refactor, parser takes ownership during parsing.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (14)
backend/data/src/main/kotlin/io/tolgee/activity/data/ActivityType.kt
(1 hunks)backend/data/src/main/kotlin/io/tolgee/formats/genericTable/in/TableParser.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryExportController.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryImportController.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/data/glossary/GlossaryImportResult.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermRepository.kt
(2 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermTranslationRepository.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryExportService.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryTermService.kt
(2 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryTermTranslationService.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/ImportGlossaryTerm.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt
(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt (1)
backend/data/src/main/kotlin/io/tolgee/model/glossary/GlossaryTermTranslation.kt (1)
GlossaryTermTranslation
(11-49)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Frontend static check 🪲
- GitHub Check: Build backend 🏗️
🔇 Additional comments (10)
backend/data/src/main/kotlin/io/tolgee/activity/data/ActivityType.kt (1)
83-83
: LGTM!The new
GLOSSARY_IMPORT
activity type follows the same pattern as the existingIMPORT
constant (line 37), correctly setsonlyCountsInList = true
, and is logically placed among other glossary-related activities.backend/data/src/main/kotlin/io/tolgee/formats/genericTable/in/TableParser.kt (2)
21-21
: Approve unconditional header dropAll existing uses of
parser.tableParser.rows
(e.g. in CsvFileProcessor and CsvImportFormatDetectorTest) correctly treat rows as data only; no code depends on the header being included as a data row.
14-14
: Unconditionally drop header column for languages The change toheaders?.drop(1) ?: emptyList()
handles single‐column inputs correctly and removes a redundant size check;TableParser.languages
is only used internally.ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermTranslationRepository.kt (1)
19-25
: LGTM! Cleaner API design.The refactoring to accept a
Glossary
entity instead of separate IDs simplifies both the method signature and the query logic.ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermRepository.kt (1)
32-32
: LGTM!The method signature is clear and follows Spring Data JPA conventions.
ee/backend/app/src/main/kotlin/io/tolgee/ee/data/glossary/GlossaryImportResult.kt (1)
5-8
: LGTM!Simple and well-documented DTO for the import result.
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryTermTranslationService.kt (1)
20-31
: LGTM! Well-structured refactoring.The refactoring maintains backward compatibility while providing a cleaner internal API that accepts the domain entity directly.
ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryImportController.kt (1)
38-55
: LGTM! Proper transactional handling.The
@Transactional
annotation ensures that if the import fails after deleting existing terms, all changes will be rolled back, preventing data loss.ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryTermService.kt (2)
69-81
: Helper for “with translations” looks goodMethod pair (by ids and by Glossary) reads clean and cohesive.
228-238
: Bulk delete/save helpers OKTransactional wrappers around repository methods are appropriate for import/export flows.
...ackend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt
Outdated
Show resolved
Hide resolved
...ackend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt
Outdated
Show resolved
Hide resolved
...end/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt
Outdated
Show resolved
Hide resolved
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt
Show resolved
Hide resolved
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt (1)
28-39
: Optimize translation lookups for better performance.Each call to
translations.find { it.languageTag == ... }
is O(n). For glossaries with many terms and languages, this results in O(terms × languages × translations) complexity. Grouping translations into a map would reduce this to O(terms × languages).Consider refactoring
asColumns()
to build a translation map once per term:fun GlossaryTerm.asColumns(): Array<String> { + val translationMap = translations.associateBy { it.languageTag } return arrayOf( - translations.find { it.languageTag == glossary.baseLanguageTag }?.text ?: "", + translationMap[glossary.baseLanguageTag]?.text ?: "", description, flagNonTranslatable.toString(), flagCaseSensitive.toString(), flagAbbreviation.toString(), flagForbiddenTerm.toString(), ) + languageTagsWithoutBaseLanguage.map { languageTag -> - translations.find { it.languageTag == languageTag }?.text ?: "" + translationMap[languageTag]?.text ?: "" } }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermRepository.kt
(2 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt
- ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermRepository.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Frontend static check 🪲
- GitHub Check: Build frontend 🏗️
- GitHub Check: Build backend 🏗️
🔇 Additional comments (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt (1)
41-48
: Previous issues have been resolved!The resource leak and encoding concerns from the earlier review have been properly addressed:
.use { }
now ensures the CSV writer is closedbyteInputStream(Charsets.UTF_8)
guarantees UTF-8 encoding
...end/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (1)
76-83
: Don't coerce unknown boolean tokens to false.This issue was flagged in a previous review but remains unaddressed. Unrecognized values (e.g., typos like "tru" or invalid entries) are silently mapped to
false
, which can corrupt data. Returnnull
for unknown values instead.Apply this diff:
private fun parseBoolean(value: String?): Boolean? { if (value.isNullOrBlank()) return null return when (value.trim().lowercase()) { "true", "1", "yes", "y", "t" -> true "false", "0", "no", "n", "f" -> false - else -> false + else -> null } }
🧹 Nitpick comments (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (1)
70-74
: Add negative index check for defensive programming.While fixing
findHeaderIndex
prevents -1 from being passed,getSafe
should defensively check both array bounds.Apply this diff:
fun Array<String>.getSafe(idx: Int?): String? { if (idx == null) return null - if (idx >= size) return null + if (idx < 0 || idx >= size) return null return this[idx] }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt
(1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Frontend static check 🪲
- GitHub Check: Build backend 🏗️
...ackend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt (1)
28-39
: Consider caching translation lookups in a map for better performance.The repeated
find
calls (lines 30, 37) iterate through thetranslations
collection for each language tag. For terms with many translations or glossaries with many languages, this becomes inefficient.Apply this diff to optimize translation lookups:
fun GlossaryTerm.asColumns(): Array<String> { + val translationMap = translations.associateBy { it.languageTag } return arrayOf( - translations.find { it.languageTag == glossary.baseLanguageTag }?.text ?: "", + translationMap[glossary.baseLanguageTag]?.text ?: "", description, flagNonTranslatable.toString(), flagCaseSensitive.toString(), flagAbbreviation.toString(), flagForbiddenTerm.toString(), ) + languageTagsWithoutBaseLanguage.map { languageTag -> - translations.find { it.languageTag == languageTag }?.text ?: "" + translationMap[languageTag]?.text ?: "" } }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt
(1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (21)
- GitHub Check: E2E testing 🔎 (15, 14)
- GitHub Check: E2E testing 🔎 (15, 12)
- GitHub Check: E2E testing 🔎 (15, 13)
- GitHub Check: E2E testing 🔎 (15, 6)
- GitHub Check: E2E testing 🔎 (15, 11)
- GitHub Check: E2E testing 🔎 (15, 8)
- GitHub Check: E2E testing 🔎 (15, 7)
- GitHub Check: E2E testing 🔎 (15, 5)
- GitHub Check: E2E testing 🔎 (15, 2)
- GitHub Check: E2E testing 🔎 (15, 1)
- GitHub Check: E2E testing 🔎 (15, 4)
- GitHub Check: E2E testing 🔎 (15, 3)
- GitHub Check: E2E testing 🔎 (15, 0)
- GitHub Check: BT 🔎 (ktlint:test)
- GitHub Check: BT 🔎 (data:test)
- GitHub Check: BT 🔎 (security:test)
- GitHub Check: BT 🔎 (server-app:runWebsocketTests)
- GitHub Check: BT 🔎 (server-app:runWithoutEeTests)
- GitHub Check: BT 🔎 (ee-test:test)
- GitHub Check: BT 🔎 (server-app:runStandardTests)
- GitHub Check: BT 🔎 (server-app:runContextRecreatingTests)
🔇 Additional comments (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt (1)
30-30
: Verify that the base language translation is always present.The code gracefully handles missing base language translations by defaulting to an empty string. However, if
glossary.baseLanguageTag
is not in a term's translations, the "Term" column will be empty, which might indicate a data consistency issue.Please confirm whether terms are guaranteed to have a translation in the base language, or if empty base terms are expected. If the former, consider adding validation when loading glossary data to ensure data integrity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (1)
76-83
: Don't coerce unrecognized boolean values to false.This issue was previously flagged but remains unaddressed. Mapping unknown tokens to
false
(line 81) can silently corrupt data. Returnnull
instead to surface the issue.Apply this diff:
private fun parseBoolean(value: String?): Boolean? { if (value.isNullOrBlank()) return null return when (value.trim().lowercase()) { "true", "1", "yes", "y", "t" -> true "false", "0", "no", "n", "f" -> false - else -> false + else -> null } }
🧹 Nitpick comments (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (1)
70-74
: Add negative index bounds check for complete safety.While callers currently pass valid indices, a method named
getSafe
should defensively guard both bounds.Apply this diff:
fun Array<String>.getSafe(idx: Int?): String? { if (idx == null) return null + if (idx < 0) return null if (idx >= size) return null return this[idx] }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt
(1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Build backend 🏗️
🔇 Additional comments (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (1)
66-68
: LGTM! The -1 to null conversion is properly handled.The
?.takeIf { it >= 0 }
correctly converts the -1 result fromindexOfFirst
to null when no header is found, preventing potential issues downstream.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
♻️ Duplicate comments (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt (1)
54-56
: Prevent duplicate base-language translations
You already add the base-language translation above, but theforEach
copies every entry—including the base language again. With the unique constraint on(term_id, language_tag)
, this will explode as soon as the CSV contains a column for the base language. Skipglossary.baseLanguageTag
here.- glossaryTerm.translations.forEach { (languageTag, text) -> - translations.add(GlossaryTermTranslation(languageTag, text).apply { term = this@applyFrom }) - } + glossaryTerm.translations + .filterKeys { it != glossary.baseLanguageTag } + .forEach { (languageTag, text) -> + translations.add(GlossaryTermTranslation(languageTag, text).apply { term = this@applyFrom }) + }
🧹 Nitpick comments (1)
webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialog.tsx (1)
15-18
: Permission check is correct.The permission check appropriately restricts term creation to users with OWNER or MAINTAINER roles, or admin users. This aligns with the glossary feature's access control requirements.
The permission check logic (
['OWNER', 'MAINTAINER'].includes(...)
) is duplicated inuseGlossaryImportDialog
. Consider extracting it into a shared helper likeuseCanManageGlossary()
to reduce duplication:// New shared hook export const useCanManageGlossary = () => { const { preferredOrganization } = usePreferredOrganization(); const isUserAdmin = useIsAdmin(); return ['OWNER', 'MAINTAINER'].includes( preferredOrganization?.currentUserRole || '' ) || isUserAdmin; };
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (16)
e2e/cypress/support/dataCyType.d.ts
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryExportController.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/component/PublicEnabledFeaturesProvider.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryTermTranslationService.kt
(2 hunks)webapp/src/ee/glossary/components/GlossaryEmptyListMessage.tsx
(3 hunks)webapp/src/ee/glossary/components/GlossaryImportDialog.tsx
(1 hunks)webapp/src/ee/glossary/components/GlossaryTermsList.tsx
(3 hunks)webapp/src/ee/glossary/components/GlossaryViewBody.tsx
(3 hunks)webapp/src/ee/glossary/components/GlossaryViewTopbar.tsx
(4 hunks)webapp/src/ee/glossary/hooks/useGlossaryExport.ts
(1 hunks)webapp/src/ee/glossary/hooks/useGlossaryImportDialog.tsx
(1 hunks)webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialog.tsx
(1 hunks)webapp/src/ee/glossary/views/GlossaryView.tsx
(2 hunks)webapp/src/service/apiSchema.generated.ts
(4 hunks)webapp/src/views/projects/export/downloadExported.ts
(2 hunks)
✅ Files skipped from review due to trivial changes (1)
- webapp/src/views/projects/export/downloadExported.ts
🧰 Additional context used
🧬 Code graph analysis (9)
webapp/src/ee/glossary/views/GlossaryView.tsx (1)
webapp/src/ee/glossary/components/GlossaryViewBody.tsx (1)
GlossaryViewBody
(16-73)
webapp/src/ee/glossary/components/GlossaryImportDialog.tsx (3)
webapp/src/globalContext/helpers.tsx (1)
usePreferredOrganization
(31-43)webapp/src/service/http/useQueryApi.ts (1)
useApiMutation
(224-265)webapp/src/service/MessageService.tsx (1)
messageService
(6-18)
webapp/src/ee/glossary/hooks/useGlossaryImportDialog.tsx (2)
webapp/src/globalContext/helpers.tsx (2)
usePreferredOrganization
(31-43)useIsAdmin
(21-22)webapp/src/ee/glossary/components/GlossaryImportDialog.tsx (1)
GlossaryImportDialog
(90-366)
webapp/src/ee/glossary/hooks/useGlossaryExport.ts (2)
webapp/src/views/projects/export/downloadExported.ts (1)
downloadExported
(3-24)webapp/src/service/http/useQueryApi.ts (1)
useApiMutation
(224-265)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt (1)
backend/data/src/main/kotlin/io/tolgee/model/glossary/GlossaryTermTranslation.kt (1)
GlossaryTermTranslation
(11-49)
webapp/src/ee/glossary/components/GlossaryViewTopbar.tsx (2)
webapp/src/ee/glossary/hooks/useGlossaryExport.ts (1)
useGlossaryExport
(14-48)webapp/src/component/layout/BaseViewAddButton.tsx (1)
BaseViewAddButton
(6-23)
webapp/src/service/apiSchema.generated.ts (1)
webapp/src/service/billingApiSchema.generated.ts (1)
components
(218-1464)
webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialog.tsx (1)
webapp/src/globalContext/helpers.tsx (2)
usePreferredOrganization
(31-43)useIsAdmin
(21-22)
webapp/src/ee/glossary/components/GlossaryViewBody.tsx (3)
webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialog.tsx (1)
useGlossaryTermCreateDialog
(5-35)webapp/src/ee/glossary/hooks/useGlossaryImportDialog.tsx (1)
useGlossaryImportDialog
(5-34)webapp/src/ee/glossary/components/GlossaryViewTopbar.tsx (1)
GlossaryViewTopbar
(28-94)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Build backend 🏗️
- GitHub Check: Build frontend 🏗️
🔇 Additional comments (10)
ee/backend/app/src/main/kotlin/io/tolgee/ee/component/PublicEnabledFeaturesProvider.kt (1)
21-21
: LGTM! GLOSSARY feature now enabled by default for free tier.The initialization of
forceEnabled
withsetOf(Feature.GLOSSARY)
enables glossary functionality by default in the public/free tier. This aligns with the PR's objective to add glossary import/export capabilities and makes the feature accessible without subscription gating.webapp/src/ee/glossary/views/GlossaryView.tsx (1)
4-4
: LGTM! Clean separation of concerns.The removal of dialog state management from this component improves the architecture. Dialog handling is now delegated to
GlossaryViewBody
via hooks (useGlossaryTermCreateDialog
,useGlossaryImportDialog
), making the component simpler and more focused on layout.Also applies to: 42-42
webapp/src/ee/glossary/components/GlossaryEmptyListMessage.tsx (2)
51-51
: LGTM! Prop rename aligns with new hook-based architecture.The rename from
onCreate
toonCreateTerm
is consistent with the new hook-based dialog management pattern (useGlossaryTermCreateDialog
,useGlossaryImportDialog
) used across glossary components.Also applies to: 58-58, 73-74
90-90
: LGTM! Import option now visible by default.Removing
sx={{ display: 'none' }}
from the second card makes the import option visible in the empty state, improving discoverability of the glossary import feature.webapp/src/ee/glossary/components/GlossaryTermsList.tsx (1)
80-81
: LGTM! Props updated to support new hook-based dialogs.The prop updates (
onCreate
→onCreateTerm
, addedonImport
) align with the new hook-based dialog management pattern and are correctly passed through toGlossaryEmptyListMessage
.Also applies to: 94-95, 154-155
e2e/cypress/support/dataCyType.d.ts (1)
332-341
: LGTM! Test selectors added for glossary import/export flows.The new data-cy selectors support E2E testing of the glossary export button, import dialog, dropzone, file controls, import modes, and action buttons.
webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialog.tsx (1)
20-28
: LGTM! Conditional rendering enforces permissions.The dialog is only rendered when
canCreate
is true,createTermDialogOpen
is true, andpreferredOrganization
is defined, ensuring proper access control and preventing errors when organization context is missing.webapp/src/ee/glossary/hooks/useGlossaryImportDialog.tsx (1)
15-18
: LGTM! Permission check and conditional rendering are correct.The permission check and conditional rendering pattern matches
useGlossaryTermCreateDialog
, ensuring consistent access control across glossary management operations. The dialog receives thehasExistingTerms
prop to conditionally show import mode options.Also applies to: 20-27
webapp/src/ee/glossary/components/GlossaryViewBody.tsx (2)
8-9
: LGTM! Clean integration of glossary creation and import hooks.The integration of
useGlossaryTermCreateDialog
anduseGlossaryImportDialog
follows React best practices. The component correctly:
- Imports and invokes hooks at the top level
- Renders dialog components using the conditional pattern (
false
or React element)- Passes derived handlers to child components consistently
This refactor provides better separation of concerns by moving dialog management logic into dedicated hooks.
Also applies to: 34-34, 41-42, 45-46, 60-61
36-37
: Import is already gated until glossary data loads
GlossaryViewTopbar only renders whenterms
is defined, and EmptyState hides the import button whileloading
is true, so users can’t click import beforetotal
is set. Differentiatingundefined
vs.0
forhasExistingTerms
isn’t needed.Likely an incorrect or invalid review comment.
...end/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryExportController.kt
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
webapp/src/ee/glossary/views/GlossaryTermCreateDialog.tsx (1)
7-7
: Remove unused import.The
usePreferredOrganization
hook is no longer used after the refactor on line 46.Apply this diff to remove the unused import:
-import { usePreferredOrganization } from 'tg.globalContext/helpers';
♻️ Duplicate comments (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryExportController.kt (1)
57-62
: Don't close the servlet output stream.Spring manages the lifecycle of the
StreamingResponseBody
output stream. Callingout.close()
can triggerIllegalStateException
when the container tries to close it again. Remove the explicitout.close()
call and let Spring handle it.Apply this diff to fix the issue:
return ResponseEntity.ok().headers(headers).body( streamingResponseBodyProvider.createStreamingResponseBody { out: OutputStream -> stream.use { IOUtils.copy(stream, out) } - out.close() }, )
🧹 Nitpick comments (3)
webapp/src/component/common/ModeSelector.tsx (1)
42-76
: Consider accessibility improvements for screen readers.While the component is functional and keyboard-accessible via the
Radio
elements, the current structure has accessibility gaps:
- The
Radio
button lacks a proper label association (no<label>
wrapping oraria-labelledby
).- The focusable element (Radio) doesn't match the visual clickable area (entire card), which can confuse users navigating by keyboard.
- The description text is not programmatically associated with the radio button, so screen readers won't announce it as part of the option.
To improve accessibility, consider wrapping the content in a
<label>
element or usingaria-labelledby
andaria-describedby
to associate the title and description with theRadio
. For example:<StyledRadioOption key={option.value} selected={value === option.value} onClick={() => onChange(option.value)} role="radio" aria-checked={value === option.value} tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onChange(option.value); } }} > <Box display="flex" flexDirection="column" width="100%"> <Box display="flex" justifyContent="space-between" alignItems="center" mb={0.5} > <Typography variant="body1" fontWeight={500} id={`option-title-${option.value}`}> {option.title} </Typography> <Radio checked={value === option.value} data-cy={option.dataCy} size="small" sx={{ padding: 0.5, pointerEvents: 'none' }} aria-hidden="true" /> </Box> <Typography variant="body2" color="text.secondary" id={`option-desc-${option.value}`}> {option.description} </Typography> </Box> </StyledRadioOption>This approach makes the entire card focusable and keyboard-navigable while ensuring screen readers properly announce the option's title and description.
webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialog.tsx (2)
27-33
: Consider simplifying the dialog rendering logic.The
createTermDialog
is only rendered whenisOpen
is true (which meanscreateTermDialogOpen
is true), yet the dialog still receivesopen={createTermDialogOpen}
. This prop is redundant at this point.Consider simplifying to:
const createTermDialog = isOpen && ( <GlossaryTermCreateDialog - open={createTermDialogOpen} + open={true} onClose={() => setCreateTermDialogOpen(false)} onFinished={() => setCreateTermDialogOpen(false)} /> );Or if the Dialog component defaults
open
to true, you could omit it entirely.
35-39
: Consider renaming the return value for clarity.The returned
createTermDialogOpen
property contains the computedisOpen
value (which combines dialog state with permissions), not the rawcreateTermDialogOpen
state. This naming could be confusing for consumers who might expect it to reflect only the dialog state.Consider renaming to better reflect what it represents:
return { onCreateTerm: canCreate ? onCreateTerm : undefined, - createTermDialogOpen: isOpen, + isDialogOpen: isOpen, createTermDialog, };Or alternatively, return both the raw state and permission check:
return { onCreateTerm: canCreate ? onCreateTerm : undefined, - createTermDialogOpen: isOpen, + createTermDialogOpen, + canCreate, createTermDialog, };
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (19)
e2e/cypress/support/dataCyType.d.ts
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryExportController.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt
(1 hunks)ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt
(1 hunks)ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestUtil.kt
(1 hunks)ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParserTest.kt
(1 hunks)ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporterTest.kt
(1 hunks)webapp/src/component/common/DragDropArea.tsx
(1 hunks)webapp/src/component/common/FileDropzone.tsx
(1 hunks)webapp/src/component/common/ModeSelector.tsx
(1 hunks)webapp/src/component/common/SingleFileDropzone.tsx
(1 hunks)webapp/src/ee/glossary/components/GlossaryImportDialog.tsx
(1 hunks)webapp/src/ee/glossary/hooks/useGlossaryExport.ts
(1 hunks)webapp/src/ee/glossary/hooks/useGlossaryImportDialog.tsx
(1 hunks)webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialog.tsx
(1 hunks)webapp/src/ee/glossary/views/GlossaryTermCreateDialog.tsx
(1 hunks)webapp/src/views/projects/export/downloadExported.ts
(1 hunks)webapp/src/views/projects/import/component/ImportFileDropzone.tsx
(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
- ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt
- webapp/src/ee/glossary/hooks/useGlossaryImportDialog.tsx
- webapp/src/views/projects/export/downloadExported.ts
- webapp/src/ee/glossary/hooks/useGlossaryExport.ts
🧰 Additional context used
🧬 Code graph analysis (7)
webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialog.tsx (2)
webapp/src/globalContext/helpers.tsx (2)
usePreferredOrganization
(31-43)useIsAdmin
(21-22)webapp/src/ee/glossary/views/GlossaryTermCreateDialog.tsx (1)
GlossaryTermCreateDialog
(29-85)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParserTest.kt (1)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestUtil.kt (14)
hasDescription
(39-41)hasTranslation
(47-49)isTranslatable
(59-61)isNotCaseSensitive
(71-73)isNotAbbreviation
(79-81)isNotForbidden
(87-89)isNotTranslatable
(63-65)isCaseSensitive
(67-69)isAbbreviation
(75-77)isForbidden
(83-85)hasNoDescription
(43-45)hasNoFlags
(91-96)hasNoTerm
(55-57)hasNoTranslation
(51-53)
webapp/src/ee/glossary/components/GlossaryImportDialog.tsx (4)
webapp/src/component/common/ModeSelector.tsx (2)
ModeOption
(23-28)ModeSelector
(36-77)webapp/src/service/http/useQueryApi.ts (1)
useApiMutation
(224-265)webapp/src/service/MessageService.tsx (1)
messageService
(6-18)webapp/src/component/common/SingleFileDropzone.tsx (1)
SingleFileDropzone
(13-32)
webapp/src/component/common/FileDropzone.tsx (2)
webapp/src/service/MessageService.tsx (1)
messageService
(6-18)webapp/src/component/common/DragDropArea.tsx (1)
DragDropArea
(45-96)
webapp/src/component/common/SingleFileDropzone.tsx (1)
webapp/src/component/common/FileDropzone.tsx (2)
FileDropzoneProps
(53-68)FileDropzone
(70-253)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt (1)
backend/data/src/main/kotlin/io/tolgee/model/glossary/GlossaryTermTranslation.kt (1)
GlossaryTermTranslation
(11-49)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporterTest.kt (2)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt (1)
glossary
(10-47)ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt (10)
glossary
(67-69)withBaseLanguageTag
(51-53)withDescription
(79-81)withTranslations
(87-89)translatable
(91-93)caseSensitive
(95-97)abbreviation
(99-101)forbiddenTerm
(103-105)withTranslation
(83-85)createGlossary
(8-13)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Build backend 🏗️
🔇 Additional comments (9)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParserTest.kt (1)
1-302
: LGTM!The test suite provides comprehensive coverage of CSV parsing scenarios, including edge cases like empty rows, case-insensitive headers, various boolean flag formats, and delimiter variations. The test structure is clear and uses well-designed assertion helpers.
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt (1)
1-47
: LGTM!The exporter implementation properly handles CSV generation with deterministic language tag ordering, explicit UTF-8 encoding, and proper resource management through the
use
block. The boolean flag inversion is clearly documented.ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestUtil.kt (1)
1-97
: LGTM!The test utility provides a clean, fluent DSL for asserting glossary term properties. The helper methods are well-named and focused, making tests more readable and maintainable.
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporterTest.kt (1)
1-310
: LGTM!The test suite thoroughly validates CSV export functionality across multiple scenarios, including delimiter variations, missing translations, special character escaping, and deterministic language tag ordering. The tests are well-structured and use the builder DSL effectively.
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt (1)
1-126
: LGTM!The test data builder DSL is well-designed with a fluent API that makes test setup clear and concise. The builder pattern implementation properly manages relationships between glossaries and terms, and the extension functions provide good composability.
webapp/src/ee/glossary/views/GlossaryTermCreateDialog.tsx (1)
46-46
: LGTM! Cleaner ownership resolution.Using
glossary.organizationOwner.id
directly is more appropriate than relying on the preferred organization, and it eliminates the need for the null-assertion operator.webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialog.tsx (3)
1-9
: LGTM! Clean hook setup.The imports and initial context retrieval are well-structured and appropriate for the hook's functionality.
16-20
: LGTM! Straightforward state management.The dialog state management is clean and appropriate for this use case.
22-23
: Confirm admin bypass of organization preference or validate current scope.The create and import dialogs both require
isGlossaryUnderPreference
even forisUserAdmin
(e.g. useGlossaryTermCreateDialog.tsx:23, useGlossaryImportDialog.tsx:23). If global admins should operate across all organizations, letisUserAdmin
short-circuit the preference check; otherwise confirm this restriction is intentional.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
webapp/src/views/projects/export/downloadExported.ts (1)
41-41
: Fix stateful regex bug with global flag.The
g
flag causesexec()
to maintain state vialastIndex
between calls. SinceextensionRegex
is module-scoped, subsequent calls toparseExtension()
will continue from where the previous call left off, causing intermittent match failures. Additionally, them
flag is unnecessary for single-line header parsing.Apply this diff to remove the problematic flags:
-const extensionRegex = /^.*\.(\w+)"$/gm; +const extensionRegex = /^.*\.(\w+)"$/;Alternatively, for more robust Content-Disposition parsing that handles various formats:
-const extensionRegex = /^.*\.(\w+)"$/gm; +const extensionRegex = /filename="[^"]*\.(\w+)"/;
♻️ Duplicate comments (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (1)
83-89
: Don’t coerce unknown boolean tokens tofalse
Treating any unrecognized value as
false
silently corrupts data—for example, an invalidtranslatable
token becomes “non-translatable” on import. Keep known true/false mappings, but returnnull
for everything else so callers can detect the missing/invalid flag.Recommended change:
private fun parseBoolean(value: String?): Boolean? { if (value.isNullOrBlank()) return null return when (value.trim().lowercase()) { "true", "1", "yes", "y", "t" -> true "false", "0", "no", "n", "f" -> false - else -> false + else -> null } }
🧹 Nitpick comments (6)
webapp/src/views/projects/export/downloadExported.ts (1)
30-39
: Consider more robust Content-Disposition parsing.The current regex pattern assumes the header value ends with a quoted filename and doesn't handle edge cases like:
- Unquoted filenames (valid per RFC 6266)
filename*
parameter (UTF-8 encoded filenames)- Missing filename attribute
While the fallback to
format.extension
provides resilience, consider using a more comprehensive parser or library for Content-Disposition headers.Example improvement:
const parseExtension = (response: Response) => { const contentDisposition = response.headers.get('Content-Disposition'); if (contentDisposition) { // Try filename* first (RFC 5987), then filename const filenameMatch = /filename\*?="?([^";\n]*)"?/i.exec(contentDisposition); if (filenameMatch) { const filename = filenameMatch[1]; const extensionMatch = /\.(\w+)$/.exec(filename); if (extensionMatch) { return extensionMatch[1]; } } } return null; };ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParserTest.kt (1)
134-161
: Update expectations for invalid boolean tokensAfter the parser stops coercing unknown tokens to
false
, the fourth row in this test should no longer assert negative flags. Please switch the expectations forTerm4
to check that the flags remain unset (e.g.,hasNoFlags()
or explicit null checks).webapp/src/ee/glossary/views/GlossaryTermCreateDialog.tsx (1)
7-7
: Remove unused import.The
usePreferredOrganization
import is no longer used after switching to derive organization context fromglossary.organizationOwner.id
.Apply this diff:
-import { usePreferredOrganization } from 'tg.globalContext/helpers';
webapp/src/views/projects/import/component/ImportFileDropzone.tsx (1)
91-97
: Good: prevent default on drag over; consider more robust drag depth handlingThe onDragOver guard is correct. To avoid drag state flicker from child enter/leave, consider a drag depth counter (as suggested for DragDropArea) instead of comparing event targets.
If aligning with DragDropArea, you can:
- increment depth on dragEnter, decrement on dragLeave;
- set active when depth > 0;
- reset to 0 on drop.
Also applies to: 128-128
webapp/src/ee/glossary/hooks/useGlossaryImportDialog.tsx (1)
36-41
: Expose canImport for consumers; clarify return shapeReturning onImport as undefined requires consumers to null-check. Expose a boolean for simpler usage and consider naming the open flag consistently.
Apply:
return { - onImport: canImport ? onImport : undefined, - importDialogOpen: isOpen, - importDialog, + canImport, + onImport: canImport ? onImport : undefined, + isOpen, + importDialog, };webapp/src/ee/glossary/components/GlossaryImportDialog.tsx (1)
44-44
: Replace deprecated React.VFC with React.FC.
React.VFC
was removed in React 18. UseReact.FC
instead.Apply this diff:
-export const GlossaryImportDialog: React.VFC<Props> = ({ +export const GlossaryImportDialog: React.FC<Props> = ({
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (19)
e2e/cypress/support/dataCyType.d.ts
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryExportController.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt
(1 hunks)ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt
(1 hunks)ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestUtil.kt
(1 hunks)ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParserTest.kt
(1 hunks)ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporterTest.kt
(1 hunks)webapp/src/component/common/DragDropArea.tsx
(1 hunks)webapp/src/component/common/FileDropzone.tsx
(1 hunks)webapp/src/component/common/ModeSelector.tsx
(1 hunks)webapp/src/component/common/SingleFileDropzone.tsx
(1 hunks)webapp/src/ee/glossary/components/GlossaryImportDialog.tsx
(1 hunks)webapp/src/ee/glossary/hooks/useGlossaryExport.ts
(1 hunks)webapp/src/ee/glossary/hooks/useGlossaryImportDialog.tsx
(1 hunks)webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialog.tsx
(1 hunks)webapp/src/ee/glossary/views/GlossaryTermCreateDialog.tsx
(1 hunks)webapp/src/views/projects/export/downloadExported.ts
(1 hunks)webapp/src/views/projects/import/component/ImportFileDropzone.tsx
(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- webapp/src/ee/glossary/hooks/useGlossaryExport.ts
- ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryExportController.kt
- webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialog.tsx
🧰 Additional context used
🧬 Code graph analysis (7)
webapp/src/ee/glossary/hooks/useGlossaryImportDialog.tsx (2)
webapp/src/globalContext/helpers.tsx (2)
usePreferredOrganization
(31-43)useIsAdmin
(21-22)webapp/src/ee/glossary/components/GlossaryImportDialog.tsx (1)
GlossaryImportDialog
(44-148)
webapp/src/component/common/SingleFileDropzone.tsx (1)
webapp/src/component/common/FileDropzone.tsx (2)
FileDropzoneProps
(53-68)FileDropzone
(70-253)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParserTest.kt (1)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestUtil.kt (14)
hasDescription
(39-41)hasTranslation
(47-49)isTranslatable
(59-61)isNotCaseSensitive
(71-73)isNotAbbreviation
(79-81)isNotForbidden
(87-89)isNotTranslatable
(63-65)isCaseSensitive
(67-69)isAbbreviation
(75-77)isForbidden
(83-85)hasNoDescription
(43-45)hasNoFlags
(91-96)hasNoTerm
(55-57)hasNoTranslation
(51-53)
webapp/src/component/common/FileDropzone.tsx (2)
webapp/src/service/MessageService.tsx (1)
messageService
(6-18)webapp/src/component/common/DragDropArea.tsx (1)
DragDropArea
(45-96)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt (1)
backend/data/src/main/kotlin/io/tolgee/model/glossary/GlossaryTermTranslation.kt (1)
GlossaryTermTranslation
(11-49)
webapp/src/ee/glossary/components/GlossaryImportDialog.tsx (4)
webapp/src/component/common/ModeSelector.tsx (2)
ModeOption
(23-28)ModeSelector
(36-77)webapp/src/service/http/useQueryApi.ts (1)
useApiMutation
(224-265)webapp/src/service/MessageService.tsx (1)
messageService
(6-18)webapp/src/component/common/SingleFileDropzone.tsx (1)
SingleFileDropzone
(13-32)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporterTest.kt (2)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt (11)
glossary
(69-71)withBaseLanguageTag
(53-55)glossaryTerm
(124-128)withDescription
(81-83)withTranslations
(89-91)translatable
(93-95)caseSensitive
(97-99)abbreviation
(101-103)forbiddenTerm
(105-107)withTranslation
(85-87)createGlossary
(8-13)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt (1)
glossary
(10-47)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Build backend 🏗️
🔇 Additional comments (5)
e2e/cypress/support/dataCyType.d.ts (1)
332-338
: LGTM! Clean addition of glossary import/export test selectors.The new data-cy selectors follow established naming conventions, maintain alphabetical order, and comprehensively cover the UI elements for the glossary import/export feature (export button, import button, dialog, mode switches, and action buttons).
webapp/src/views/projects/export/downloadExported.ts (1)
11-27
: LGTM: Proper resource cleanup with try/finally.The try/finally block ensures the object URL is always revoked, preventing memory leaks. This is a good improvement over the previous implementation.
webapp/src/ee/glossary/views/GlossaryTermCreateDialog.tsx (1)
46-46
: No null check needed forglossary.organizationOwner
:GlossaryModel
definesorganizationOwner
as a required, non-nullable property per the generated API schema.Likely an incorrect or invalid review comment.
webapp/src/component/common/ModeSelector.tsx (1)
1-77
: LGTM! Clean and accessible implementation.The component is well-structured with:
- Proper TypeScript generics for flexible option types
- Theme-aware styling with hover states
- Redundant click handlers (box + radio) ensuring good UX and accessibility
- Clear separation of concerns
webapp/src/ee/glossary/components/GlossaryImportDialog.tsx (1)
104-120
: acceptedFileTypes prop signature is correct; CsvLogo is a React component matching FileType.icon.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (6)
webapp/src/service/apiSchema.generated.ts (1)
8721-8747
: Fix export response media typeThe export endpoint still advertises
"application/json"
for its 200 response even though it streams a CSV file. Client SDKs generated from this schema will try to JSON‑decode the CSV stream and fail. Please declare the real media type (e.g."text/csv"
or another appropriate non‑JSON type) for the success response.- 200: { - content: { - "application/json": components["schemas"]["StreamingResponseBody"]; - }; - }; + 200: { + content: { + "text/csv": components["schemas"]["StreamingResponseBody"]; + }; + };e2e/cypress/support/dataCyType.d.ts (1)
335-341
: Unused glossary-import-mode selectors remain unresolved.The previous review identified that
glossary-import-mode-add
andglossary-import-mode-replace
are unused in the UI. Since this PR is still in draft, either:
- Add the corresponding
data-cy
attributes to the import mode controls in the UI- Remove these declarations if the mode selection UI won't be implemented
Verify if the other glossary selectors are used:
#!/bin/bash # Description: Verify usage of glossary export/import selectors (excluding mode selectors already flagged) echo "Checking usage of glossary import/export selectors..." for selector in "glossary-export-button" "glossary-import-button" "glossary-import-cancel-button" "glossary-import-dialog" "glossary-import-submit-button"; do echo "---" echo "Searching for: $selector" rg -n "data-cy=['\"]$selector['\"]" -g '*.{ts,tsx,js,jsx}' || echo " No matches found" donewebapp/src/component/common/SingleFileDropzone.tsx (1)
5-11
: Minor typing/readability nits
- Prefer a SingleFile alias (FilesType[number]) over FilesType[0].
- Use nullish coalescing for brevity: onFileSelect(newFiles[0] ?? null).
+type SingleFile = FilesType[number]; export type SingleFileDropzoneProps = Omit< FileDropzoneProps, 'files' | 'onFilesSelect' | 'maxFiles' > & { - file: FilesType[0] | null; - onFileSelect: (file: FilesType[0] | null) => void; + file: SingleFile | null; + onFileSelect: (file: SingleFile | null) => void; }; @@ - const handleFilesSelect = (newFiles: FilesType) => { - onFileSelect(newFiles.length > 0 ? newFiles[0] : null); - }; + const handleFilesSelect = (newFiles: FilesType) => { + onFileSelect(newFiles[0] ?? null); + };Also applies to: 20-22
webapp/src/component/common/DragDropArea.tsx (2)
71-101
: Stabilize drag state with a depth counter; reset on dropTarget equality is fragile and can flicker. Track depth and reset reliably.
- const [dragOver, setDragOver] = useState<null | 'valid' | 'invalid'>(null); - const [dragEnterTarget, setDragEnterTarget] = useState<EventTarget | null>( - null - ); + const [dragOver, setDragOver] = useState<null | 'valid' | 'invalid'>(null); + const [dragDepth, setDragDepth] = useState(0); @@ - const handleDragEnter = (e: React.DragEvent) => { - if (!active) return; + const handleDragEnter = (e: React.DragEvent) => { + if (!active) { + e.preventDefault(); + e.stopPropagation(); + return; + } e.preventDefault(); e.stopPropagation(); - setDragEnterTarget(e.target); + setDragDepth((d) => d + 1); @@ - const handleDragLeave = (e: React.DragEvent) => { - if (!active) return; + const handleDragLeave = (e: React.DragEvent) => { + if (!active) { + e.preventDefault(); + e.stopPropagation(); + return; + } e.preventDefault(); e.stopPropagation(); - if (e.target === dragEnterTarget) { - setDragOver(null); - setDragEnterTarget(null); - } + setDragDepth((d) => Math.max(0, d - 1)); + if (dragDepth <= 1) setDragOver(null); }; @@ - const handleDrop = async (e: React.DragEvent) => { - if (!active) return; + const handleDrop = async (e: React.DragEvent) => { + if (!active) { + e.preventDefault(); + e.stopPropagation(); + return; + } e.preventDefault(); e.stopPropagation(); @@ onFilesReceived(files); setDragOver(null); - setDragEnterTarget(null); + setDragDepth(0); };Also applies to: 108-117
122-160
: Add basic keyboard/ARIA support and prevent default when inactiveMake the surface operable via keyboard and non-destructive when disabled.
return ( - <Box + <Box position="relative" overflow="visible" - onClick={onClick} + onClick={onClick} + role={onClick ? 'button' : undefined} + tabIndex={onClick ? 0 : -1} + aria-disabled={!active || undefined} + onKeyDown={(e) => { + if (!onClick) return; + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onClick(); + } + }} data-cy={dataCy} onDragEnter={handleDragEnter} onDragLeave={handleDragLeave} onDragOver={handleDragOver} onDrop={handleDrop} >webapp/src/component/common/FileDropzone.tsx (1)
86-92
: Reset file input after selection to allow re-selecting the same fileWithout clearing the input value, choosing the same file again will not trigger onChange.
Apply this diff:
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { const selectedFiles = e.target.files; if (selectedFiles && selectedFiles.length > 0) { const fileArray = Array.from(selectedFiles); handleFilesReceived(fileArray.map((f) => ({ file: f, name: f.name }))); + e.currentTarget.value = ''; } };
🧹 Nitpick comments (13)
ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryImportController.kt (2)
32-45
: Consider clarifying the unused path parameter.The
organizationId
path parameter is declared but never used. Instead, the code correctly usesorganizationHolder.organization.id
(line 47), which is the right approach for security—it ensures users can only import into their own organization's glossaries. However, having the unused parameter in the path can be confusing for API consumers who might expect it to be validated or used.Consider either:
- Removing
organizationId
from the path if it's not semantically necessary for the API design- Or keeping it for REST convention but adding a comment explaining why it's not directly used (authentication context takes precedence)
46-56
: Solid implementation with proper resource management.The method body correctly:
- Uses authenticated organization context for security
- Ensures atomic delete+import via
@Transactional
- Manages file input stream with Kotlin's
use{}
(try-with-resources equivalent)Optional improvements:
- Consider adding explicit validation that the file is not empty before processing
- Document the expected CSV format in the
@Operation
annotation or API docs- For better UX, consider returning more detailed results (e.g., rows skipped, validation errors) instead of just the imported count
webapp/src/component/common/FileDropzoneSelectedFile.tsx (2)
45-53
: Add accessible name to remove buttonProvide aria-label so screen readers announce the action.
- <IconButton + <IconButton size="small" onClick={(e) => { e.stopPropagation(); onRemove?.(); }} + aria-label={`Remove ${file.name}`} data-cy={`file-dropzone-remove-button`} >
42-44
: Expose full filename on hoverLong names may be truncated; add title for discoverability.
- <Typography variant="body2" sx={{ flex: 1 }}> + <Typography variant="body2" sx={{ flex: 1 }} title={file.name}> {file.name} </Typography>webapp/src/views/projects/import/component/ImportFileInput.tsx (3)
193-199
: Use native click() instead of dispatchEventclick() is simpler and more reliable for file inputs.
- <Button - onClick={() => - fileRef.current?.dispatchEvent(new MouseEvent('click')) - } + <Button + onClick={() => fileRef.current?.click()} variant="outlined" color="primary" >
153-158
: Optional: Make the whole drop area open the picker on clickImproves UX; consistent with other dropzones.
- <DragDropArea - onFilesReceived={onNewFiles} - data-cy="dropzone" - active={!props.isProgressOverlayActive} - maxItems={MAX_FILE_COUNT} - > + <DragDropArea + onFilesReceived={onNewFiles} + data-cy="dropzone" + active={!props.isProgressOverlayActive} + maxItems={MAX_FILE_COUNT} + onClick={() => fileRef.current?.click()} + >
151-187
: Scope TS suppression to the non-standard attribute, not the whole returnAvoid broad @ts-ignore; annotate just the input element for webkitdirectory.
- /* @ts-ignore */ return ( @@ - <input + {/* @ts-expect-error: non-standard attribute not in React types */} + <input key={resetKey} data-cy={'import-file-input'} type="file" style={{ display: 'none' }} ref={fileRef} onChange={(e) => onFileSelected(e)} multiple webkitdirectory />webapp/src/views/projects/translations/Screenshots/ScreenshotDropzone.tsx (1)
31-38
: Remove no-op onClickUnnecessary handler; omit to reduce noise.
- <DragDropArea - onFilesReceived={validateAndUpload} - onClick={() => {}} + <DragDropArea + onFilesReceived={validateAndUpload}webapp/src/views/projects/translations/Screenshots/useScreenshotUpload.tsx (2)
127-129
: Use native click() instead of dispatchEventSimpler and consistent with other places.
- function openFiles() { - fileRef.current?.dispatchEvent(new MouseEvent('click')); - } + function openFiles() { + fileRef.current?.click(); + }
85-93
: Avoidas any
if API typing allows File/BlobIf the OpenAPI types accept File/Blob, drop the cast.
- screenshot: file.file as any, + screenshot: file.file,webapp/src/component/common/FileDropzone.tsx (3)
58-69
: Document or validate extension format assumptionThe code assumes
extension
includes the leading dot (e.g.,".csv"
):
- Line 60 uses
endsWith(config.extension)
, which would incorrectly match if the dot is missing (e.g., extension"sv"
would match filename"data.asv"
)- Line 128 passes extensions to the
accept
attribute, which per HTML spec requires a leading dot for extension-based file typesConsider adding TypeScript branding, JSDoc, or runtime validation to ensure extensions are properly formatted.
Example validation in the component:
const findFileType = (fileName: string) => acceptedFileTypes.find((config) => fileName.toLowerCase().endsWith(config.extension.toLowerCase()) ); + + // Validate extensions format on mount (development only) + if (process.env.NODE_ENV === 'development') { + acceptedFileTypes.forEach((fileType) => { + if (!fileType.extension.startsWith('.')) { + console.warn( + `FileDropzone: extension "${fileType.extension}" should start with a dot` + ); + } + }); + }
71-84
: Consider notifying users when files exceed the limitLine 81 silently truncates files beyond
maxFiles
. If a user drags 5 files butmaxFiles
is 1, only the first file is kept without notification, which may be confusing.Optional enhancement:
const handleFilesReceived = (receivedFiles: FilesType) => { const validFiles = receivedFiles.filter((receivedFile) => { if (!isValidFileType(receivedFile.name)) { messageService.error(<T keyName="error_message_invalid_file_type" />); return false; } return true; }); if (validFiles.length > 0) { const newFiles = [...files, ...validFiles].slice(0, maxFiles); + const droppedCount = files.length + validFiles.length - newFiles.length; + if (droppedCount > 0) { + messageService.error( + <T keyName="error_message_too_many_files" params={{ count: droppedCount }} /> + ); + } onFilesSelect(newFiles); } };
33-36
: Consider stronger typing for icon componentThe
icon
property usesReact.ComponentType<any>
, which sacrifices type safety. If all icons share a common interface (e.g., MUI'sSvgIconComponent
or a customIconProps
), consider using that type instead.Example:
+import { SvgIconComponent } from '@mui/material'; + export type FileType = { extension: string; - icon: React.ComponentType<any>; + icon: SvgIconComponent; };Note: Only apply this if the icon components conform to
SvgIconComponent
. Otherwise, you could define a minimal shared interface.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
e2e/cypress/support/dataCyType.d.ts
(2 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryImportController.kt
(1 hunks)webapp/src/component/common/DragDropArea.tsx
(1 hunks)webapp/src/component/common/FileDropzone.tsx
(1 hunks)webapp/src/component/common/FileDropzoneSelectedFile.tsx
(1 hunks)webapp/src/component/common/SingleFileDropzone.tsx
(1 hunks)webapp/src/ee/glossary/components/GlossaryImportDialog.tsx
(1 hunks)webapp/src/fixtures/FileUploadFixtures.ts
(0 hunks)webapp/src/service/apiSchema.generated.ts
(4 hunks)webapp/src/views/projects/import/component/ImportFileDropzone.tsx
(0 hunks)webapp/src/views/projects/import/component/ImportFileInput.tsx
(3 hunks)webapp/src/views/projects/translations/Screenshots/ScreenshotDropzone.tsx
(2 hunks)webapp/src/views/projects/translations/Screenshots/useScreenshotUpload.tsx
(5 hunks)
💤 Files with no reviewable changes (2)
- webapp/src/fixtures/FileUploadFixtures.ts
- webapp/src/views/projects/import/component/ImportFileDropzone.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- webapp/src/ee/glossary/components/GlossaryImportDialog.tsx
🧰 Additional context used
🧬 Code graph analysis (8)
webapp/src/views/projects/import/component/ImportFileInput.tsx (1)
webapp/src/component/common/DragDropArea.tsx (1)
DragDropArea
(62-161)
webapp/src/component/common/FileDropzoneSelectedFile.tsx (1)
webapp/src/fixtures/FileUploadFixtures.ts (1)
FilesType
(42-45)
webapp/src/views/projects/translations/Screenshots/useScreenshotUpload.tsx (2)
webapp/src/fixtures/FileUploadFixtures.ts (1)
FilesType
(42-45)webapp/src/views/projects/translations/Screenshots/Screenshots.tsx (1)
ALLOWED_UPLOAD_TYPES
(17-17)
webapp/src/component/common/DragDropArea.tsx (1)
webapp/src/fixtures/FileUploadFixtures.ts (2)
FilesType
(42-45)getFilesAsync
(3-40)
webapp/src/component/common/SingleFileDropzone.tsx (2)
webapp/src/component/common/FileDropzone.tsx (2)
FileDropzoneProps
(38-47)FileDropzone
(49-199)webapp/src/fixtures/FileUploadFixtures.ts (1)
FilesType
(42-45)
webapp/src/service/apiSchema.generated.ts (1)
webapp/src/service/billingApiSchema.generated.ts (1)
components
(218-1464)
webapp/src/views/projects/translations/Screenshots/ScreenshotDropzone.tsx (3)
webapp/src/fixtures/FileUploadFixtures.ts (1)
FilesType
(42-45)webapp/src/component/common/DragDropArea.tsx (1)
DragDropArea
(62-161)webapp/src/views/projects/translations/Screenshots/Screenshots.tsx (1)
MAX_FILE_COUNT
(16-16)
webapp/src/component/common/FileDropzone.tsx (4)
webapp/src/fixtures/FileUploadFixtures.ts (1)
FilesType
(42-45)webapp/src/service/MessageService.tsx (1)
messageService
(6-18)webapp/src/component/common/FileDropzoneSelectedFile.tsx (1)
FileDropzoneSelectedFile
(36-57)webapp/src/component/common/DragDropArea.tsx (1)
DragDropArea
(62-161)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Build backend 🏗️
🔇 Additional comments (4)
ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryImportController.kt (1)
23-31
: LGTM! Clean controller structure.The class declaration follows Spring and Kotlin best practices with constructor-based dependency injection and clear REST API boundaries.
webapp/src/component/common/SingleFileDropzone.tsx (1)
9-22
: Single-file typing and mapping look correctProps and handler match FileDropzone API; single-file wrapping is correct.
webapp/src/views/projects/translations/Screenshots/ScreenshotDropzone.tsx (1)
1-10
: Approve FilesType migration
All validateAndUpload call sites now use FilesType (verified in useScreenshotUpload.tsx).webapp/src/component/common/FileDropzone.tsx (1)
49-198
: Clean implementation with good separation of concernsThe component demonstrates solid patterns:
- Proper ref management for the hidden file input
- Conditional rendering based on file selection state
- Appropriate use of
stopPropagation
for nested click handlers- Integration with
DragDropArea
for drag-and-drop functionality- Data-cy attributes for testability
- User feedback via
messageService
for validation errors
...end/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryImportController.kt
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
♻️ Duplicate comments (1)
webapp/src/component/common/FileDropzone.tsx (1)
85-91
: Reset file input after selection to allow re-selecting the same fileWithout clearing the input value, choosing the same file again won’t trigger onChange.
Apply:
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { const selectedFiles = e.target.files; if (selectedFiles && selectedFiles.length > 0) { const fileArray = Array.from(selectedFiles); handleFilesReceived(fileArray.map((f) => ({ file: f, name: f.name }))); + // allow selecting the same file again + e.currentTarget.value = ''; } };
🧹 Nitpick comments (2)
webapp/src/fixtures/useOnFilePaste.ts (1)
6-18
: Consider simplifying the file collection logic.The manual loop through
clipboardData.files
can be simplified usingArray.from()
for cleaner code.Apply this diff to simplify the implementation:
const pasteListener = (e: ClipboardEvent) => { - const files: File[] = []; if (!e.clipboardData?.files.length) { return; } - for (let i = 0; i < e.clipboardData.files.length; i++) { - const item = e.clipboardData.files.item(i); - if (item) { - files.push(item); - } - } + const files = Array.from(e.clipboardData.files); onFilePaste(files.map((f) => ({ file: f, name: f.name }))); };webapp/src/component/common/FileDropzone.tsx (1)
110-119
: Accessibility: add aria-label to remove button (in FileDropzoneSelectedFile)The remove IconButton lives in FileDropzoneSelectedFile and lacks an aria-label. Add an accessible label using the file name.
Proposed change in webapp/src/component/common/FileDropzoneSelectedFile.tsx:
- <IconButton + <IconButton size="small" onClick={(e) => { e.stopPropagation(); onRemove?.(); }} data-cy={`file-dropzone-remove-button`} + aria-label={`Remove ${file.name}`} >
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
webapp/src/component/common/FileDropzone.tsx
(1 hunks)webapp/src/ee/glossary/views/GlossaryTermCreateDialog.tsx
(1 hunks)webapp/src/fixtures/useOnFilePaste.ts
(1 hunks)webapp/src/views/projects/import/ImportView.tsx
(1 hunks)webapp/src/views/projects/import/component/ImportFileInput.tsx
(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- webapp/src/views/projects/import/component/ImportFileInput.tsx
🧰 Additional context used
🧬 Code graph analysis (3)
webapp/src/fixtures/useOnFilePaste.ts (1)
webapp/src/fixtures/FileUploadFixtures.ts (1)
FilesType
(42-45)
webapp/src/component/common/FileDropzone.tsx (5)
webapp/src/fixtures/FileUploadFixtures.ts (1)
FilesType
(42-45)webapp/src/fixtures/useOnFilePaste.ts (1)
useOnFilePaste
(4-26)webapp/src/service/MessageService.tsx (1)
messageService
(6-18)webapp/src/component/common/FileDropzoneSelectedFile.tsx (1)
FileDropzoneSelectedFile
(36-57)webapp/src/component/common/DragDropArea.tsx (1)
DragDropArea
(62-161)
webapp/src/views/projects/import/ImportView.tsx (2)
webapp/src/views/projects/import/hooks/useImportDataHelper.tsx (1)
useImportDataHelper
(28-138)webapp/src/fixtures/useOnFilePaste.ts (1)
useOnFilePaste
(4-26)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Build backend 🏗️
🔇 Additional comments (4)
webapp/src/views/projects/import/ImportView.tsx (2)
25-25
: LGTM!The import statement correctly adds the new
useOnFilePaste
hook following the project's import path conventions.
29-29
: LGTM!The hook invocation correctly integrates paste functionality into the import flow by passing
dataHelper.onNewFiles
as the callback handler.webapp/src/fixtures/useOnFilePaste.ts (1)
20-24
: LGTM!The event listener setup and cleanup are correctly implemented, preventing memory leaks by removing the listener on unmount.
webapp/src/ee/glossary/views/GlossaryTermCreateDialog.tsx (1)
33-45
: No null checks required forglossary.organizationOwner.id
. TheGlossaryProvider
only renders its children onceglossary.data
is loaded, and theGlossaryModel
schema guarantees bothorganizationOwner.id
andid
are defined.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (2)
backend/data/src/main/kotlin/io/tolgee/constants/Message.kt (1)
312-312
: LGTM! Consider grouping with related constants.The new
FILE_PROCESSING_FAILED
constant is correctly added and follows the enum's naming convention. It aligns well with the PR's glossary import/export functionality.For improved organization, you might consider positioning it near related file/import error constants like
CANNOT_PARSE_FILE
(line 68),IMPORT_FAILED
(line 221), orNO_DATA_TO_IMPORT
(line 223), though placing it at the end is also acceptable as it minimizes merge conflicts.webapp/src/fixtures/useOnFilePaste.ts (1)
11-16
: Consider simplifying the FileList iteration.The standard for-loop works correctly, but you could simplify it using modern array methods.
Apply this diff for a more concise implementation:
- for (let i = 0; i < e.clipboardData.files.length; i++) { - const item = e.clipboardData.files.item(i); - if (item) { - files.push(item); - } - } - onFilePaste(files.map((f) => ({ file: f, name: f.name }))); + const clipboardFiles = Array.from(e.clipboardData.files); + onFilePaste(clipboardFiles.map((f) => ({ file: f, name: f.name })));This eliminates the need for the temporary
files
array and the null check, sinceArray.from()
handles the FileList conversion cleanly.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
backend/data/src/main/kotlin/io/tolgee/constants/Message.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt
(1 hunks)webapp/src/component/common/FileDropzone.tsx
(1 hunks)webapp/src/component/common/FileDropzoneSelectedFile.tsx
(1 hunks)webapp/src/component/common/ModeSelector.tsx
(1 hunks)webapp/src/component/common/SingleFileDropzone.tsx
(1 hunks)webapp/src/ee/glossary/components/GlossaryImportDialog.tsx
(1 hunks)webapp/src/fixtures/useOnFilePaste.ts
(1 hunks)webapp/src/views/projects/export/downloadExported.ts
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt
- webapp/src/ee/glossary/components/GlossaryImportDialog.tsx
- webapp/src/component/common/FileDropzoneSelectedFile.tsx
🧰 Additional context used
🧬 Code graph analysis (3)
webapp/src/fixtures/useOnFilePaste.ts (1)
webapp/src/fixtures/FileUploadFixtures.ts (1)
FilesType
(42-45)
webapp/src/component/common/SingleFileDropzone.tsx (2)
webapp/src/component/common/FileDropzone.tsx (2)
FileDropzoneProps
(39-48)FileDropzone
(50-208)webapp/src/fixtures/FileUploadFixtures.ts (1)
FilesType
(42-45)
webapp/src/component/common/FileDropzone.tsx (5)
webapp/src/fixtures/FileUploadFixtures.ts (1)
FilesType
(42-45)webapp/src/service/MessageService.tsx (1)
messageService
(6-18)webapp/src/fixtures/useOnFilePaste.ts (1)
useOnFilePaste
(4-26)webapp/src/component/common/FileDropzoneSelectedFile.tsx (1)
FileDropzoneSelectedFile
(37-60)webapp/src/component/common/DragDropArea.tsx (1)
DragDropArea
(62-161)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Build backend 🏗️
🔇 Additional comments (3)
webapp/src/fixtures/useOnFilePaste.ts (1)
5-25
: Stale closure issue fixed.The dependency array now correctly includes
onFilePaste
, resolving the previous stale closure concern. The listener will properly update when the callback changes.Note: This approach re-registers the listener whenever
onFilePaste
changes. If the parent component doesn't memoize the callback (e.g., withuseCallback
), this could cause frequent re-registrations. If that becomes a concern, consider the ref-based approach suggested in the previous review to avoid re-attaching the DOM listener.webapp/src/component/common/SingleFileDropzone.tsx (1)
9-22
: Props now wrap the single file correctlyUsing
FilesType[number]
for the prop and unwrapping the first element keeps the dropzone contract intact without nesting arrays. Nicely aligned.webapp/src/component/common/FileDropzone.tsx (1)
68-111
: Paste flow now reuses full validation pipelineRouting
useOnFilePaste
throughhandleFilesReceived
keeps the drag/drop, paste, and browse paths consistent on type checks and limits. Looks solid.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (1)
webapp/src/ee/billing/Invoices/DownloadButton.tsx (1)
37-37
: Remove unnecessary double type cast.The
res.blob()
method already returns aPromise<Blob>
, so the double castas any as Blob
is unnecessary.Apply this diff:
- const url = URL.createObjectURL(data as any as Blob); + const url = URL.createObjectURL(data);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
webapp/src/component/common/ModeSelector.tsx
(1 hunks)webapp/src/ee/billing/Invoices/DownloadButton.tsx
(1 hunks)webapp/src/ee/glossary/hooks/useGlossaryExport.ts
(1 hunks)webapp/src/views/projects/export/downloadExported.ts
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- webapp/src/ee/glossary/hooks/useGlossaryExport.ts
- webapp/src/component/common/ModeSelector.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Build backend 🏗️
🔇 Additional comments (3)
webapp/src/ee/billing/Invoices/DownloadButton.tsx (1)
38-52
: LGTM! Nested try/finally ensures proper cleanup.The nested try/finally blocks guarantee that:
- The anchor element is removed even if the click fails
- The blob URL is revoked after a delay to avoid Safari download issues
The 7000ms delay is conservative but safe for ensuring downloads complete before URL cleanup.
webapp/src/views/projects/export/downloadExported.ts (2)
37-37
: LGTM! Regex reset prevents state issues.The explicit reset of
lastIndex
is necessary becauseextensionRegex
uses the global flag (g
), which maintains state betweenexec()
calls. The comment clearly explains the purpose.
9-31
: LGTM! Download flow refactored with proper cleanup and Safari fix.The nested try/finally structure ensures:
- The anchor element is removed after the click
- The blob URL is revoked after a 7000ms delay
The 7000ms delay addresses the Safari zero-byte download issue flagged in the previous review (which suggested
setTimeout(..., 0)
). While more conservative than the suggested 0ms, the longer delay ensures downloads complete reliably across all browsers before URL cleanup.Based on past review comments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
backend/data/src/main/kotlin/io/tolgee/formats/csv/in/CsvFileProcessor.kt (1)
15-21
: Resource leak: detector's input stream is never closed.The detector is initialized with an input stream (line 15), reads from it when
detector.delimiter
is accessed (line 19), but never closes the stream. Since a second stream is created for the parser (line 18), the first stream should be explicitly closed after delimiter detection.Apply this diff to ensure the stream is closed:
- val detector = CsvDelimiterDetector(context.file.data.inputStream()) + val delimiter = context.file.data.inputStream().use { stream -> + CsvDelimiterDetector(stream).delimiter + } val parser = CsvFileParser( inputStream = context.file.data.inputStream(), - delimiter = detector.delimiter, + delimiter = delimiter, languageFallback = firstLanguageTagGuessOrUnknown, )backend/data/src/main/kotlin/io/tolgee/util/CsvDelimiterDetector.kt (1)
11-11
: Resource leak: reader and bufferedReader are never closed.The
reader()
andbuffered()
calls create resources that remain open after reading the first line. Since thedelimiter
property is lazily evaluated and the input stream is provided externally, the detector should close its internal readers to prevent resource leaks.Apply this diff to properly close resources:
val delimiter by lazy { - val headerLine = inputStream.reader().buffered().lineSequence().firstOrNull() ?: "" + val headerLine = inputStream.reader().use { reader -> + reader.buffered().use { buffered -> + buffered.lineSequence().firstOrNull() ?: "" + } + } val counts = DELIMITERS.map { delimiter ->
♻️ Duplicate comments (3)
webapp/src/service/apiSchema.generated.ts (1)
8723-8752
: Fix export response media type
GET /v2/organizations/{organizationId}/glossaries/{glossaryId}/export
returns a CSV stream, but the schema still advertises"application/json"
withStreamingResponseBody
. Any generated client will try to JSON-decode the stream and crash at runtime. Please expose the real media type (e.g."text/csv"
or the exact type you stream) so clients handle it correctly.webapp/src/component/common/DragDropArea.tsx (2)
70-72
: Drag state tracking via event target is fragile.The
dragEnterTarget
comparison on line 94 can cause flickering when dragging over child elements. A drag depth counter would be more reliable.This issue was raised in a previous review. As per the past comment, replace
dragEnterTarget
with adragCounter
(number) that increments ondragenter
and decrements ondragleave
, settingdragOver
when the counter is positive.Also applies to: 94-97
120-159
: Missing keyboard accessibility for the drop area.The drop zone lacks keyboard navigation support. Users who cannot use a mouse cannot activate the file picker or interact with the drop area.
This issue was raised in a previous review. As per the past comment, add:
role="button"
or appropriate ARIA roletabIndex={0}
to make it focusableonKeyDown
handler to activateonClick
on Enter/Space- Consider adding visual focus indicators
Apply this diff:
<Box position="relative" overflow="visible" onClick={onClick} data-cy="dropzone" + role="button" + tabIndex={onClick ? 0 : undefined} + onKeyDown={(e) => { + if (onClick && (e.key === 'Enter' || e.key === ' ')) { + e.preventDefault(); + onClick(); + } + }} onDragEnter={handleDragEnter} onDragLeave={handleDragLeave} onDragOver={handleDragOver} onDrop={handleDrop} >
🧹 Nitpick comments (9)
backend/data/src/main/kotlin/io/tolgee/util/CsvDelimiterDetector.kt (1)
12-21
: Consider handling quoted strings in delimiter detection.The current heuristic counts delimiter occurrences in the header line, which can be misled by delimiters inside quoted values. For example, a CSV with header
"Name","Description,with,comma","Value"
would count 4 commas as field separators when only 2 are actual delimiters.While the simple heuristic works for typical well-formed CSVs, consider enhancing the logic to respect quoted strings for more robust delimiter detection.
webapp/src/views/projects/translations/Screenshots/ScreenshotDropzone.tsx (1)
33-33
: Address empty onClick in ScreenshotDropzone
ScreenshotDropzone.tsx:33 setsonClick={() => {}}
, which is a no-op. Either remove this prop to disable clicks or implement a file-picker trigger (e.g. use a hidden<input>
and callinputRef.current?.click()
, as inwebapp/src/component/common/FileDropzone.tsx
).e2e/cypress/e2e/glossary/importExport.cy.ts (5)
5-7
: Remove unused imports to satisfy lints.E2GlossariesView and E2GlossaryCreateEditDialog are unused. Clean them up to fix static warnings.
-import { E2GlossariesView } from '../../compounds/glossaries/E2GlossariesView'; import { E2GlossaryView } from '../../compounds/glossaries/E2GlossaryView'; -import { E2GlossaryCreateEditDialog } from '../../compounds/glossaries/E2GlossaryCreateEditDialog';
23-38
: Wait on the import API for determinism.Add an intercept and assert 200 before DOM checks to reduce flakiness.
it('imports into an empty glossary', () => { login('Owner'); const view = new E2GlossaryView(); view.findAndVisit(data, 'Owner', 'Empty Glossary'); + cy.intercept('POST', '/v2/organizations/**/glossaries/**/import').as( + 'importGlossary' + ); const importDialog = view.openImportDialogWhenGlossaryIsEmpty(); importDialog.selectFile('glossary/import_basic.csv'); importDialog.submit(); + cy.wait('@importGlossary').its('response.statusCode').should('eq', 200); gcy('glossary-term-list-item') .filter(':contains("Imported Term One")') .should('be.visible'); gcy('glossary-term-list-item') .filter(':contains("Imported Term Two")') .should('be.visible'); });
45-55
: Replace mode: wait on import and scope the negative assertion.
- Wait for POST /import to complete.
- Scope “A.B.C Inc” assertion to the term list to avoid false positives.
const importDialog = view.openImportDialog(); + cy.intercept('POST', '/v2/organizations/**/glossaries/**/import').as( + 'importGlossary' + ); importDialog.selectFile('glossary/import_basic.csv'); importDialog.chooseReplace(); importDialog.submit(); + cy.wait('@importGlossary').its('response.statusCode').should('eq', 200); gcy('glossary-term-list-item') .filter(':contains("Imported Term One")') .should('be.visible'); - cy.contains('A.B.C Inc').should('not.exist'); + gcy('glossary-term-list-item') + .filter(':contains("A.B.C Inc")') + .should('not.exist');
62-72
: Add mode: wait on import and scope the positive assertion.Make the check deterministic and scoped.
const importDialog = view.openImportDialog(); + cy.intercept('POST', '/v2/organizations/**/glossaries/**/import').as( + 'importGlossary' + ); importDialog.selectFile('glossary/import_basic.csv'); importDialog.chooseAdd(); importDialog.submit(); + cy.wait('@importGlossary').its('response.statusCode').should('eq', 200); gcy('glossary-term-list-item') .filter(':contains("Imported Term One")') .should('be.visible'); - cy.contains('A.B.C Inc').should('be.visible'); + gcy('glossary-term-list-item') + .filter(':contains("A.B.C Inc")') + .should('be.visible');
79-91
: Strengthen export assertions.Also assert content-type is CSV and content-disposition includes attachment.
cy.wait('@exportGlossary').then(({ response }) => { expect(response?.statusCode).to.eq(200); const disp = response?.headers?.['content-disposition'] as string; expect(disp).to.exist; expect(disp).to.include('.csv'); + expect(response?.headers?.['content-type']).to.contain('text/csv'); + expect(disp).to.match(/attachment/i); });e2e/cypress/compounds/glossaries/E2GlossaryImportDialog.ts (2)
1-2
: Register cypress-file-upload in support for global availability.Importing the plugin per component works but scatters side-effects and types. Prefer adding it once in Cypress support (e.g., cypress/support/e2e.ts or commands.ts) and remove local imports.
Based on learnings
26-29
: Offer helper to submit and wait on network alias.To reduce duplication in tests, add a submit helper that waits on an alias (e.g., @importGlossary) before asserting dialog close.
Example:
submitAndWaitFor(alias = 'importGlossary') { gcy('glossary-import-submit-button').click(); cy.wait(`@${alias}`).its('response.statusCode').should('eq', 200); gcy('glossary-import-dialog').should('not.exist'); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
e2e/cypress/fixtures/glossary/import_basic.csv
is excluded by!**/*.csv
📒 Files selected for processing (12)
backend/data/src/main/kotlin/io/tolgee/formats/csv/in/CsvFileProcessor.kt
(1 hunks)backend/data/src/main/kotlin/io/tolgee/util/CsvDelimiterDetector.kt
(2 hunks)e2e/cypress/compounds/glossaries/E2GlossaryImportDialog.ts
(1 hunks)e2e/cypress/compounds/glossaries/E2GlossaryView.ts
(2 hunks)e2e/cypress/e2e/glossary/importExport.cy.ts
(1 hunks)e2e/cypress/support/dataCyType.d.ts
(2 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt
(1 hunks)webapp/src/component/common/DragDropArea.tsx
(1 hunks)webapp/src/component/common/FileDropzone.tsx
(1 hunks)webapp/src/service/apiSchema.generated.ts
(6 hunks)webapp/src/views/projects/import/component/ImportFileInput.tsx
(3 hunks)webapp/src/views/projects/translations/Screenshots/ScreenshotDropzone.tsx
(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- webapp/src/views/projects/import/component/ImportFileInput.tsx
- webapp/src/component/common/FileDropzone.tsx
- ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt
🧰 Additional context used
🧬 Code graph analysis (5)
e2e/cypress/compounds/glossaries/E2GlossaryImportDialog.ts (1)
e2e/cypress/common/shared.ts (1)
gcy
(28-29)
e2e/cypress/e2e/glossary/importExport.cy.ts (3)
e2e/cypress/common/apiCalls/testData/testData.ts (1)
glossaryTestData
(131-131)e2e/cypress/compounds/glossaries/E2GlossaryView.ts (1)
E2GlossaryView
(11-96)e2e/cypress/common/shared.ts (1)
gcy
(28-29)
webapp/src/views/projects/translations/Screenshots/ScreenshotDropzone.tsx (3)
webapp/src/fixtures/FileUploadFixtures.ts (1)
FilesType
(42-45)webapp/src/component/common/DragDropArea.tsx (1)
DragDropArea
(61-160)webapp/src/views/projects/translations/Screenshots/Screenshots.tsx (1)
MAX_FILE_COUNT
(16-16)
e2e/cypress/compounds/glossaries/E2GlossaryView.ts (2)
e2e/cypress/common/shared.ts (1)
gcy
(28-29)e2e/cypress/compounds/glossaries/E2GlossaryImportDialog.ts (1)
E2GlossaryImportDialog
(4-35)
webapp/src/component/common/DragDropArea.tsx (1)
webapp/src/fixtures/FileUploadFixtures.ts (2)
FilesType
(42-45)getFilesAsync
(3-40)
🪛 GitHub Check: E2E Static Check 🪲
e2e/cypress/e2e/glossary/importExport.cy.ts
[warning] 7-7:
'E2GlossaryCreateEditDialog' is defined but never used. Allowed unused vars must match /^_/u
[warning] 5-5:
'E2GlossariesView' is defined but never used. Allowed unused vars must match /^_/u
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Frontend static check 🪲
- GitHub Check: Build backend 🏗️
🔇 Additional comments (9)
e2e/cypress/support/dataCyType.d.ts (1)
295-297
: New selector declarations are well-formatted, but prior unused selector issues must be resolved.The ten new data-cy selector declarations follow the correct format, maintain alphabetical ordering, and use consistent naming conventions. However, past reviews identified three unused selectors that still need attention:
- Lines 296:
file-dropzone-remove-button
- not found in FileDropzone.tsx- Lines 339-340:
glossary-import-mode-add
andglossary-import-mode-replace
- not found in glossary import UI componentsBefore merging, either add the missing
data-cy
attributes to the corresponding UI elements or remove these declarations to maintain type/implementation consistency.Also applies to: 335-341
backend/data/src/main/kotlin/io/tolgee/util/CsvDelimiterDetector.kt (1)
1-1
: LGTM: Package relocation is appropriate.Moving
CsvDelimiterDetector
to theio.tolgee.util
package makes sense, as this is a reusable utility that can be leveraged across different modules (e.g., CSV import and glossary import/export).webapp/src/views/projects/translations/Screenshots/ScreenshotDropzone.tsx (2)
18-28
: LGTM - Global drag/drop prevention is appropriate.The global event listeners correctly prevent the browser's default behavior of opening dropped files in a new tab, which would disrupt the application flow.
30-40
: LGTM - DragDropArea integration is well-structured.The component correctly uses DragDropArea with permission-based activation, max items limit, and appropriate callbacks.
webapp/src/component/common/DragDropArea.tsx (3)
7-50
: LGTM - Styled components are well-structured.The overlay, icon wrapper, and icon components use appropriate theming and transitions. The styling provides clear visual feedback for valid and invalid drag states.
74-88
: LGTM - Drag enter validation logic is correct.The handler appropriately checks the
active
flag, prevents default behavior, and validates the item count againstmaxItems
to provide immediate visual feedback.
106-115
: LGTM - File drop processing is properly async.The handler correctly uses
getFilesAsync
to handle the drag-and-drop file system entries and resets state after processing.e2e/cypress/compounds/glossaries/E2GlossaryView.ts (2)
3-3
: Import aligns with new dialog usage. LGTM.
81-95
: New import/export helpers read well and are consistent.Good page-object API, visibility checks included, returns dialog appropriately.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
backend/data/src/main/kotlin/io/tolgee/util/CsvDelimiterDetector.kt (1)
11-21
: Delimiter detection doesn't handle CSV quoting or escaping.The current logic counts raw character occurrences in the header line without accounting for CSV quoting rules. If the header contains commas inside quoted strings (e.g.,
"Name, Title",Email
), the detector may incorrectly choose comma as the delimiter when the actual delimiter is a different character.Additionally, when the file is empty or has no header line, the logic defaults to returning comma (index 0), which may not be appropriate for all cases.
Consider enhancing the detection logic to:
- Parse quoted strings correctly before counting delimiters
- Validate that the detected delimiter produces consistent column counts across multiple rows
- Handle edge cases like empty files more explicitly
Example enhancement:
val delimiter by lazy { - val headerLine = inputStream.reader().buffered().lineSequence().firstOrNull() ?: "" + val lines = inputStream.reader().buffered().lineSequence().take(5).toList() + if (lines.isEmpty()) return@lazy ',' + + // Count delimiters outside of quoted strings val counts = DELIMITERS.map { delimiter -> - headerLine.count { it == delimiter } + lines.first().count { it == delimiter && !isInsideQuotes(it) } } val bestIndex = counts.foldIndexed(0) { index, maxIndex, value -> val maxValue = counts[maxIndex] index.takeIf { value > maxValue } ?: maxIndex } DELIMITERS[bestIndex] }
♻️ Duplicate comments (2)
e2e/cypress/support/dataCyType.d.ts (1)
335-341
: Verify usage of glossary import/export selectors in UI components.The seven glossary import/export selectors are correctly formatted and follow naming conventions. However, previous review comments flagged
glossary-import-mode-add
andglossary-import-mode-replace
as unused. Confirm all selectors are now properly used in the UI components.Run the following script to verify usage:
#!/bin/bash # Description: Verify all glossary import/export selectors are used in UI components echo "Checking usage of glossary import/export selectors..." for selector in "glossary-export-button" "glossary-import-button" "glossary-import-cancel-button" "glossary-import-dialog" "glossary-import-mode-add" "glossary-import-mode-replace" "glossary-import-submit-button"; do echo "---" echo "Searching for: $selector" rg -n "data-cy=['\"\`]$selector['\"\`]" -g '*.{ts,tsx,js,jsx}' || echo " No matches found" donewebapp/src/service/apiSchema.generated.ts (1)
8724-8726
: Fix the export media type declarationThe export endpoint still claims the 200 response is
"application/json"
, but the backend streams CSV. Generated clients will try to decode CSV as JSON and fail. Change the response content type to the actual CSV media type (e.g."text/csv"
).
🧹 Nitpick comments (3)
e2e/cypress/compounds/glossaries/E2GlossaryImportDialog.ts (2)
2-2
: Prefer Cypress built-in.selectFile()
over the plugin.The
cypress-file-upload
plugin is 4+ years old. Cypress 9.3+ includes a built-in.selectFile()
command that provides similar functionality and is actively maintained. Consider migrating to the built-in command for better long-term support.Based on learnings
5-10
: Consider using Cypress built-in.selectFile()
for better maintainability.While the current implementation works, the built-in
.selectFile()
command is more modern and doesn't require an external plugin.Example migration:
selectFile(fixturePath: string) { - gcy('file-dropzone-select-button').click(); - gcy('glossary-import-dialog') - .find('input[type="file"]') - .attachFile(fixturePath, { subjectType: 'input' }); + gcy('glossary-import-dialog') + .find('input[type="file"]') + .selectFile(`cypress/fixtures/${fixturePath}`);ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt (1)
26-32
: Consider streaming approach for very large files.The current implementation loads the entire CSV into memory before parsing. While this is acceptable for typical glossary sizes, consider a streaming approach if you expect to handle very large imports (100K+ terms).
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
e2e/cypress/fixtures/glossary/import_basic.csv
is excluded by!**/*.csv
📒 Files selected for processing (12)
backend/data/src/main/kotlin/io/tolgee/formats/csv/in/CsvFileProcessor.kt
(1 hunks)backend/data/src/main/kotlin/io/tolgee/util/CsvDelimiterDetector.kt
(1 hunks)e2e/cypress/compounds/glossaries/E2GlossaryImportDialog.ts
(1 hunks)e2e/cypress/compounds/glossaries/E2GlossaryView.ts
(2 hunks)e2e/cypress/e2e/glossary/importExport.cy.ts
(1 hunks)e2e/cypress/support/dataCyType.d.ts
(2 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt
(1 hunks)webapp/src/component/common/DragDropArea.tsx
(1 hunks)webapp/src/component/common/FileDropzone.tsx
(1 hunks)webapp/src/service/apiSchema.generated.ts
(6 hunks)webapp/src/views/projects/import/component/ImportFileInput.tsx
(3 hunks)webapp/src/views/projects/translations/Screenshots/ScreenshotDropzone.tsx
(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- webapp/src/views/projects/translations/Screenshots/ScreenshotDropzone.tsx
- webapp/src/component/common/DragDropArea.tsx
- webapp/src/component/common/FileDropzone.tsx
🧰 Additional context used
🧬 Code graph analysis (5)
e2e/cypress/e2e/glossary/importExport.cy.ts (3)
e2e/cypress/common/apiCalls/testData/testData.ts (1)
glossaryTestData
(131-131)e2e/cypress/compounds/glossaries/E2GlossaryView.ts (1)
E2GlossaryView
(11-96)e2e/cypress/common/shared.ts (1)
gcy
(28-29)
e2e/cypress/compounds/glossaries/E2GlossaryImportDialog.ts (1)
e2e/cypress/common/shared.ts (1)
gcy
(28-29)
e2e/cypress/compounds/glossaries/E2GlossaryView.ts (2)
e2e/cypress/common/shared.ts (1)
gcy
(28-29)e2e/cypress/compounds/glossaries/E2GlossaryImportDialog.ts (1)
E2GlossaryImportDialog
(4-35)
webapp/src/views/projects/import/component/ImportFileInput.tsx (2)
webapp/src/component/common/DragDropArea.tsx (1)
DragDropArea
(61-160)webapp/src/views/projects/translations/Screenshots/Screenshots.tsx (1)
MAX_FILE_COUNT
(16-16)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt (1)
backend/data/src/main/kotlin/io/tolgee/model/glossary/GlossaryTermTranslation.kt (1)
GlossaryTermTranslation
(11-49)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Frontend static check 🪲
- GitHub Check: Build backend 🏗️
🔇 Additional comments (20)
webapp/src/views/projects/import/component/ImportFileInput.tsx (1)
137-141
: Remove incorrect type mismatch concern TheDragDropArea
’sonFilesReceived
prop already usesFilesType
, matching theonNewFiles
callback signature.Likely an incorrect or invalid review comment.
e2e/cypress/compounds/glossaries/E2GlossaryView.ts (4)
3-3
: LGTM!The import is necessary for the new methods and properly placed.
81-85
: LGTM!The method follows the established pattern in the class and properly returns a helper instance for chaining.
87-91
: LGTM!The method is well-structured and maintains consistency with other dialog helpers in the class.
93-95
: LGTM!The method appropriately triggers the export action. Tests can intercept the API call to verify the download.
e2e/cypress/e2e/glossary/importExport.cy.ts (5)
1-19
: LGTM!The test setup properly manages test data lifecycle with cleanup before and after each test.
21-36
: LGTM!The test case correctly verifies the import flow for an empty glossary and asserts the presence of imported terms.
38-53
: LGTM!The test case correctly verifies that the replace mode removes old terms and imports new ones.
55-70
: LGTM!The test case correctly verifies that the add mode preserves existing terms while importing new ones.
72-89
: LGTM!The test case properly intercepts the export API call and verifies the response status and CSV content-disposition header.
e2e/cypress/compounds/glossaries/E2GlossaryImportDialog.ts (4)
12-17
: LGTM!The method properly selects the replace mode and verifies the radio button state.
19-24
: LGTM!The method correctly selects the add mode and verifies the state, maintaining consistency with
chooseReplace()
.
26-29
: LGTM!The method correctly submits the import and verifies the dialog closes, ensuring the action completes.
31-34
: LGTM!The method properly cancels the import and verifies the dialog closes, maintaining consistency with the
submit()
method.ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt (4)
1-20
: LGTM! Clean service structure.The service declaration follows Spring best practices with proper dependency injection. The imports are relevant and well-organized.
40-43
: Verify if separate translation save is necessary.Line 43 explicitly saves translations after saving terms. If
GlossaryTerm.translations
hasCascadeType.PERSIST
configured (as indicated by the addressed past review), this explicit save may be redundant—saving terms should cascade to their translations.Confirm whether:
- The cascade configuration is working as expected
- There's a specific reason to flush translations separately (e.g., to ensure proper ordering or to handle large batches)
If cascade is working and no explicit flush is needed, consider removing line 43 to simplify the code.
48-73
: LGTM! Well-structured translation mapping.The
applyFrom
extension function correctly:
- Sets back-references on all
GlossaryTermTranslation
instances (lines 53, 69)- Handles the duplicate base language scenario by preferring the
term
value over a translations column entry (lines 64-68)- Uses Kotlin's smart cast to safely access
glossaryTerm.term
after null check- Employs the
setIfNotNull
helper to cleanly assign optional fieldsThe logic addresses the concerns raised in previous reviews.
75-75
: LGTM! Elegant helper function.The
setIfNotNull
infix extension is a clean, type-safe utility that improves readability when conditionally assigning optional fields.backend/data/src/main/kotlin/io/tolgee/formats/csv/in/CsvFileProcessor.kt (1)
8-8
: LGTM!The import correctly reflects the package move of
CsvDelimiterDetector
toio.tolgee.util
. The usage pattern in lines 15-19 is also correct, creating separate input streams for the detector and parser to avoid stream consumption issues.backend/data/src/main/kotlin/io/tolgee/util/CsvDelimiterDetector.kt (1)
1-1
: CsvDelimiterDetector references updated to new package
No remaining imports or fully qualified references toio.tolgee.formats.csv.in.CsvDelimiterDetector
—all code usesio.tolgee.util.CsvDelimiterDetector
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
webapp/src/component/common/DragDropArea.tsx (1)
69-88
: Stabilize drag state (depth counter) and add keyboard accessibility; also guard clicks when inactiveCurrent target-based tracking flickers and isn’t keyboard-accessible. Use a drag depth counter, handle Enter/Space, and ignore clicks when inactive.
@@ - const [dragOver, setDragOver] = useState<null | 'valid' | 'invalid'>(null); - const [dragEnterTarget, setDragEnterTarget] = useState<EventTarget | null>( - null - ); + const [dragOver, setDragOver] = useState<null | 'valid' | 'invalid'>(null); + const [dragCounter, setDragCounter] = useState(0); @@ const handleDragEnter = (e: React.DragEvent) => { if (!active) return; e.preventDefault(); e.stopPropagation(); - setDragEnterTarget(e.target); + setDragCounter((c) => c + 1); @@ const handleDragLeave = (e: React.DragEvent) => { if (!active) return; e.preventDefault(); e.stopPropagation(); - if (e.target === dragEnterTarget) { - setDragOver(null); - setDragEnterTarget(null); - } + setDragCounter((c) => { + const next = Math.max(0, c - 1); + if (next === 0) setDragOver(null); + return next; + }); }; @@ const handleDrop = async (e: React.DragEvent) => { if (!active) return; e.preventDefault(); e.stopPropagation(); - const files = await getFilesAsync(e.dataTransfer); + const dt = e.dataTransfer; + const files = await getFilesAsync(dt); onFilesReceived(files); setDragOver(null); - setDragEnterTarget(null); + setDragCounter(0); }; @@ return ( <Box position="relative" overflow="visible" - onClick={onClick} + onClick={(e) => { + if (!active) { + e.preventDefault(); + return; + } + onClick?.(); + }} + role="button" + tabIndex={0} + aria-disabled={!active} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + if (active) onClick?.(); + } + if (e.key === 'Escape') { + setDragOver(null); + setDragCounter(0); + } + }} data-cy="dropzone" onDragEnter={handleDragEnter} onDragLeave={handleDragLeave} onDragOver={handleDragOver} onDrop={handleDrop} >Also applies to: 90-98, 106-115, 120-130
🧹 Nitpick comments (5)
webapp/src/ee/glossary/components/GlossaryViewTopbar.tsx (1)
75-86
: Consider adding a loading indicator for better UX.The export button handles the loading state by disabling itself, but users might not realize an export is in progress without a visual indicator.
Consider adding a CircularProgress spinner:
<Tooltip title={t('glossary_export_button')}> <IconButton size="small" color="primary" onClick={triggerExport} data-cy="glossary-export-button" disabled={exportLoading} aria-label={t('glossary_export_button')} > - <FileDownload03 height={20} width={20} /> + {exportLoading ? ( + <CircularProgress size={20} /> + ) : ( + <FileDownload03 height={20} width={20} /> + )} </IconButton> </Tooltip>Additionally, verify whether the export button should always be visible or if it should be conditionally rendered like the import button (e.g., based on whether there are terms to export or user permissions).
webapp/src/component/common/FileDropzone.tsx (4)
1-1
: Memoize handlers to avoid paste-listener churnStabilize findFileType and handleFilesReceived so useOnFilePaste doesn’t reattach on every render.
- import React, { useRef, ReactNode } from 'react'; + import React, { useRef, ReactNode, useCallback, useMemo } from 'react'; @@ - const findFileType = (fileName: string) => - acceptedFileTypes.find((config) => - fileName.toLowerCase().endsWith(config.extension.toLowerCase()) - ); + const findFileType = useCallback( + (fileName: string) => + acceptedFileTypes.find((config) => + fileName.toLowerCase().endsWith(config.extension.toLowerCase()) + ), + [acceptedFileTypes] + ); @@ - const handleFilesReceived = (receivedFiles: FilesType) => { + const handleFilesReceived = useCallback((receivedFiles: FilesType) => { const validFiles = receivedFiles.filter((receivedFile) => { if (!isValidFileType(receivedFile.name)) { messageService.error(<T keyName="error_message_invalid_file_type" />); return false; } return true; }); if (validFiles.length > 0) { const allFiles = [...files, ...validFiles]; const newFiles = allFiles.slice(0, maxFiles); if (newFiles.length !== allFiles.length) { messageService.error( <T keyName="error_message_too_many_files" params={{ maxFiles }} /> ); } onFilesSelect(newFiles); } - }; + }, [files, maxFiles, onFilesSelect, findFileType]);Also applies to: 74-81, 83-102
72-73
: Normalize accept attribute (ensure leading dot)Ensure accept strings are valid (e.g., ".csv,.json") even if extensions are provided without a dot.
const fileInputRef = useRef<HTMLInputElement>(null); + const acceptAttr = useMemo( + () => + acceptedFileTypes + .map((config) => + config.extension.startsWith('.') + ? config.extension + : `.${config.extension}` + ) + .join(','), + [acceptedFileTypes] + ); @@ - accept={acceptedFileTypes.map((config) => config.extension).join(',')} + accept={acceptAttr}Also applies to: 161-168
170-174
: Pass active and clamp maxItems to avoid negative valuesDisable interactions when the quota is reached and ensure maxItems isn’t negative.
- <DragDropArea - onFilesReceived={handleFilesReceived} - onClick={handleClick} - maxItems={maxFiles - files.length} - > + <DragDropArea + onFilesReceived={handleFilesReceived} + onClick={handleClick} + active={canAddMoreFiles} + maxItems={Math.max(0, maxFiles - files.length)} + >
221-226
: Add noreferrer with target="_blank"Minor security/privacy tweak.
- <Link + <Link href={helpLink.href} target="_blank" - rel="noopener" + rel="noopener noreferrer" onClick={(e) => e.stopPropagation()} >
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
webapp/src/component/common/DragDropArea.tsx
(1 hunks)webapp/src/component/common/FileDropzone.tsx
(1 hunks)webapp/src/ee/glossary/components/GlossaryImportDialog.tsx
(1 hunks)webapp/src/ee/glossary/components/GlossaryViewTopbar.tsx
(4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- webapp/src/ee/glossary/components/GlossaryImportDialog.tsx
🧰 Additional context used
🧬 Code graph analysis (3)
webapp/src/component/common/DragDropArea.tsx (1)
webapp/src/fixtures/FileUploadFixtures.ts (2)
FilesType
(42-45)getFilesAsync
(3-40)
webapp/src/component/common/FileDropzone.tsx (5)
webapp/src/fixtures/FileUploadFixtures.ts (1)
FilesType
(42-45)webapp/src/service/MessageService.tsx (1)
messageService
(6-18)webapp/src/fixtures/useOnFilePaste.ts (1)
useOnFilePaste
(4-26)webapp/src/component/common/FileDropzoneSelectedFile.tsx (1)
FileDropzoneSelectedFile
(37-60)webapp/src/component/common/DragDropArea.tsx (1)
DragDropArea
(61-160)
webapp/src/ee/glossary/components/GlossaryViewTopbar.tsx (2)
webapp/src/ee/glossary/hooks/useGlossaryExport.ts (1)
useGlossaryExport
(22-56)webapp/src/component/layout/BaseViewAddButton.tsx (1)
BaseViewAddButton
(6-23)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Build backend 🏗️
- GitHub Check: Frontend static check 🪲
🔇 Additional comments (8)
webapp/src/ee/glossary/components/GlossaryViewTopbar.tsx (6)
1-1
: LGTM! Imports are appropriate.The new imports (IconButton, Tooltip, icons, and useGlossaryExport hook) are all necessary for the import/export functionality and are used correctly in the component.
Also applies to: 7-8
19-20
: LGTM! Props are well-defined.The rename from
onCreate
toonCreateTerm
improves clarity, and the newonImport
prop follows the same optional pattern for conditional rendering of the import button.
29-30
: LGTM! Component setup is correct.The updated destructuring matches the new props interface, and the
useGlossaryExport
hook is properly initialized with bothtriggerExport
andexportLoading
being used in the render.Also applies to: 38-38
54-54
: Minor UI spacing adjustment.The gap reduction from 2 to 1 appears intentional to accommodate the new import/export buttons in the action row.
62-74
: LGTM! Import button is well-implemented.The import button is properly conditionally rendered, includes appropriate accessibility attributes (aria-label), has test identifiers (data-cy), and uses consistent styling with the export button.
87-92
: LGTM! Add button correctly updated.The add button properly uses the renamed
onCreateTerm
prop and maintains consistent conditional rendering with the other action buttons.webapp/src/component/common/FileDropzone.tsx (2)
110-113
: Nice: input reset after selectionGood call resetting the input to allow re-selecting the same file.
125-126
: Paste routed through the same handlerCorrectly reusing handleFilesReceived for paste. LGTM.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good Job! Thanks!
...end/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryImportController.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (3)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt (3)
26-26
: Consider streaming for large CSV files.
readAllBytes()
loads the entire CSV into memory, which may cause issues with large glossary files. If large imports are expected, consider using streaming or chunked processing.
42-43
: Redundant translation save if cascade is configured.Since the past review comments confirm that cascade (PERSIST/MERGE) was added to
GlossaryTerm.translations
, saving translations separately after saving terms is redundant. The cascade should automatically persist translations when terms are saved.Consider removing the explicit translation save:
val translations = terms.flatMap { it.translations } glossaryTermService.saveAll(terms) - glossaryTermTranslationService.saveAll(translations)
If you need the separate save for a specific reason (e.g., ensuring translations get IDs immediately), please add a comment explaining why.
Based on past review comments indicating cascade was configured.
51-57
: Consider validating base translation is not blank.While null is handled with the
let
block, an empty or blank string could still create a translation with no meaningful content. Consider adding a check:baseTranslation?.let { // We've found a base translation. + if (it.isBlank()) return this translations.add( GlossaryTermTranslation(glossary.baseLanguageTag, it) .apply { term = this@applyFrom } ) }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt
(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt (1)
backend/data/src/main/kotlin/io/tolgee/model/glossary/GlossaryTermTranslation.kt (1)
GlossaryTermTranslation
(11-49)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Build backend 🏗️
- GitHub Check: Frontend static check 🪲
🔇 Additional comments (5)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt (1)
81-82
: Nice Kotlin idiom!The
setIfNotNull
infix extension provides a clean, readable way to conditionally set properties. This is good Kotlin style.ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt (4)
10-20
: LGTM!The class structure is well-designed with immutable properties and lazy initialization. The deterministic ordering of language tags (via
.sorted()
) ensures consistent CSV output across runs.
41-48
: LGTM!The export implementation correctly uses
.use{}
for resource management and explicitly specifies UTF-8 encoding for deterministic output.
50-52
: LGTM!Clean helper function for boolean-to-string conversion.
27-27
: No changes needed for translatable flag header. The headerGLOSSARY_CSV_HEADER_FLAG_TRANSLATABLE
correctly reflects the inverted!flagNonTranslatable
logic.
d270783
to
0a12bb8
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
♻️ Duplicate comments (4)
ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryExportController.kt (1)
57-61
: Don’t close servlet OutputStream; use theuse { … }
stream variable.Spring closes the response stream. Manually closing can throw and isn’t needed. Also copy from the
use
lambda param.- return ResponseEntity.ok().headers(headers).body( - streamingResponseBodyProvider.createStreamingResponseBody { out: OutputStream -> - stream.use { IOUtils.copy(stream, out) } - out.close() - }, - ) + return ResponseEntity.ok().headers(headers).body( + streamingResponseBodyProvider.createStreamingResponseBody { out: OutputStream -> + stream.use { IOUtils.copy(it, out) } + }, + )webapp/src/component/common/DragDropArea.tsx (1)
69-115
: Consider adding keyboard accessibility.The drag-and-drop implementation works correctly, but lacks keyboard accessibility. Users who cannot use drag-and-drop (keyboard-only, assistive tech) cannot interact with this component.
Add keyboard support by making the container focusable and handling keyboard events:
return ( <Box position="relative" overflow="visible" onClick={onClick} data-cy="dropzone" onDragEnter={handleDragEnter} onDragLeave={handleDragLeave} onDragOver={handleDragOver} onDrop={handleDrop} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if ((e.key === 'Enter' || e.key === ' ') && onClick) { + e.preventDefault(); + onClick(); + } + }} >Note: The past review also suggested using a drag counter instead of
dragEnterTarget
to prevent flicker with nested elements. While the current implementation is functional, the counter approach would be more robust if you encounter visual issues.webapp/src/service/apiSchema.generated.ts (1)
9118-9125
: Fix export media type advertisement.
GET /v2/organizations/{organizationId}/glossaries/{glossaryId}/export
is still declared as returning"application/json"
even though the controller streams CSV. Any generated client will keep trying to JSON‑parse the CSV stream and blow up at runtime (exactly what I flagged earlier). Update the OpenAPI definition to advertise the real media type (e.g."text/csv": string
like other CSV endpoints in this repo) or clients remain broken.ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryImportController.kt (1)
52-54
: Service-level parsing/validation handling (duplicate of earlier feedback)Reiterating prior note: importCsv should translate CSV parsing/validation errors into user-friendly responses and ensure all-or-nothing behavior.
🧹 Nitpick comments (12)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestUtil.kt (1)
9-36
: Consider extracting the common find-assert-apply pattern.All three finder functions (
assertTerm
,assertTermWithTranslation
,assertTermWithDescription
) follow an identical pattern: find by predicate, assert non-null with description, apply block. This duplication increases maintenance burden.Consider refactoring to a common helper:
+private fun List<ImportGlossaryTerm>.assertTermBy( + description: String, + predicate: (ImportGlossaryTerm) -> Boolean, + block: TermAssertion.() -> Unit = {} +) { + val foundTerm = find(predicate) + assertThat(foundTerm).describedAs(description).isNotNull + TermAssertion(foundTerm!!).apply(block) +} + fun List<ImportGlossaryTerm>.assertTerm(term: String, block: TermAssertion.() -> Unit = {}) { - val foundTerm = find { it.term == term } - assertThat(foundTerm).describedAs("Term '$term' should be found").isNotNull - TermAssertion(foundTerm!!).apply(block) + assertTermBy("Term '$term' should be found", { it.term == term }, block) } fun List<ImportGlossaryTerm>.assertTermWithTranslation( languageTag: String, translation: String, block: TermAssertion.() -> Unit = {} ) { - val foundTerm = find { it.translations[languageTag] == translation } - assertThat(foundTerm) - .describedAs("Term with translation '$languageTag'='$translation' should be found") - .isNotNull - TermAssertion(foundTerm!!).apply(block) + assertTermBy( + "Term with translation '$languageTag'='$translation' should be found", + { it.translations[languageTag] == translation }, + block + ) } fun List<ImportGlossaryTerm>.assertTermWithDescription( description: String, block: TermAssertion.() -> Unit = {} ) { - val foundTerm = find { it.description == description } - assertThat(foundTerm) - .describedAs("Term with description '$description' should be found") - .isNotNull - TermAssertion(foundTerm!!).apply(block) + assertTermBy( + "Term with description '$description' should be found", + { it.description == description }, + block + ) }ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt (2)
6-6
: Remove unused import.The import
io.tolgee.model.glossary.Glossary_.terms
(JPA metamodel) is not used in this file.-import io.tolgee.model.glossary.Glossary_.terms
73-122
: Consider renaming for clarity.The
translatable(value: Boolean = true)
method (lines 93-95) setsflagNonTranslatable = !value
, creating a double-negative that may confuse readers. While functionally correct, consider either:
- Renaming to
nonTranslatable(value: Boolean = false)
to match the flag name directly, or- Keep
translatable()
but add a brief comment explaining the inversionThe rest of the builder implementation is well-structured.
- fun translatable(value: Boolean = true) = apply { - flagNonTranslatable = !value + fun nonTranslatable(value: Boolean = false) = apply { + flagNonTranslatable = valuee2e/cypress/e2e/glossary/importExport.cy.ts (1)
72-89
: Consider verifying CSV content.While the export test validates the HTTP status and content-disposition header, it doesn't verify the actual CSV content. Consider adding assertions on the response body to ensure the exported data is correct.
You could enhance the test by checking the CSV contains expected term data:
cy.wait('@exportGlossary').then(({ response }) => { expect(response?.statusCode).to.eq(200); const disp = response?.headers?.['content-disposition'] as string; expect(disp).to.exist; expect(disp).to.include('.csv'); + expect(response?.body).to.include('A.B.C Inc'); });
webapp/src/views/projects/translations/Screenshots/useScreenshotUpload.tsx (1)
30-43
: Reset the file input after processing to allow re‑selecting the same file(s).Without clearing, selecting the same files may not trigger change again. Clear both the event target and the ref.
function onFileSelected(e: SyntheticEvent) { const files = (e.target as HTMLInputElement).files; if (!files) { return; } const toUpload: FilesType = []; for (let i = 0; i < files.length; i++) { const item = files.item(i); if (item) { toUpload.push({ file: item, name: item.name }); } } validateAndUpload(toUpload); + // Clear to allow selecting the same files again + (e.target as HTMLInputElement).value = ''; + fileRef.current && (fileRef.current.value = ''); }ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt (1)
40-45
: Consider relying on cascade to avoid double persistence.If GlossaryTerm.translations has CascadeType.PERSIST (as previously discussed), saving parents will persist children; the extra saveAll(translations) adds overhead.
- val translations = terms.flatMap { it.translations } - glossaryTermService.saveAll(terms) - glossaryTermTranslationService.saveAll(translations)Note: Keep explicit save if cascade isn’t guaranteed. [Based on learnings]
webapp/src/component/common/FileDropzone.tsx (3)
83-101
: Consolidate validation messages and enforce per‑batch limit without silent dropsAggregate invalid-type errors to one toast and add only remaining slots, informing when files are trimmed.
- const handleFilesReceived = (receivedFiles: FilesType) => { - const validFiles = receivedFiles.filter((receivedFile) => { - if (!isValidFileType(receivedFile.name)) { - messageService.error(<T keyName="error_message_invalid_file_type" />); - return false; - } - return true; - }); - - if (validFiles.length > 0) { - const allFiles = [...files, ...validFiles]; - const newFiles = allFiles.slice(0, maxFiles); - if (newFiles.length !== allFiles.length) { - messageService.error( - <T keyName="error_message_too_many_files" params={{ maxFiles }} /> - ); - } - onFilesSelect(newFiles); - } - }; + const handleFilesReceived = (receivedFiles: FilesType) => { + const validFiles = receivedFiles.filter((f) => isValidFileType(f.name)); + const invalidCount = receivedFiles.length - validFiles.length; + + if (invalidCount > 0) { + messageService.error(<T keyName="error_message_invalid_file_type" />); + } + + const remaining = Math.max(0, maxFiles - files.length); + const toAdd = validFiles.slice(0, remaining); + + if (toAdd.length < validFiles.length) { + messageService.error( + <T keyName="error_message_too_many_files" params={{ maxFiles }} /> + ); + } + + if (toAdd.length > 0) { + onFilesSelect([...files, ...toAdd]); + } + };
164-166
: Normalize accept attr and reflect remaining slots for selection/dragEnsure extensions are prefixed with “.” (or allow MIME), set multiple by remaining capacity, and clamp maxItems.
- accept={acceptedFileTypes.map((config) => config.extension).join(',')} - multiple={maxFiles > 1} + accept={acceptedFileTypes + .map((c) => + c.extension.includes('/') || c.extension.startsWith('.') + ? c.extension + : `.${c.extension}` + ) + .join(',')} + multiple={maxFiles - files.length > 1} @@ - maxItems={maxFiles - files.length} + maxItems={Math.max(0, maxFiles - files.length)}Also applies to: 173-173
141-141
: Use a stable key to avoid re‑mounts on reorderIndex keys can cause unnecessary re-renders.
- key={`${file.name}-${index}`} + key={`${file.name}-${file.file.lastModified}-${file.file.size}`}ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryImportController.kt (3)
43-45
: Use Kotlin default value for query param instead of required=false + defaultValueCleaner and consistent with Kotlin style; keeps API behavior identical.
- @RequestParam(required = false, defaultValue = "false") - removeExistingTerms: Boolean, + @RequestParam("removeExistingTerms") + removeExistingTerms: Boolean = false,
38-39
: Avoid @transactional on controllers; move Tx boundary to servicePlace the transaction in GlossaryImportService to keep web layer thin and avoid long-lived HTTP-bound transactions.
- @Transactional fun importCsv(
Follow-up: wrap delete+import in a single @transactional service method (e.g., glossaryImportService.importCsv(glossary, input, removeExistingTerms)).
40-41
: Silence unused path variable by binding explicitly and prefixing with underscorePrevents IDE warnings while preserving binding.
- @PathVariable organizationId: Long, + @PathVariable("organizationId") _organizationId: Long,
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
e2e/cypress/fixtures/glossary/import_basic.csv
is excluded by!**/*.csv
📒 Files selected for processing (51)
backend/data/src/main/kotlin/io/tolgee/activity/data/ActivityType.kt
(1 hunks)backend/data/src/main/kotlin/io/tolgee/constants/Message.kt
(1 hunks)backend/data/src/main/kotlin/io/tolgee/formats/csv/in/CsvFileProcessor.kt
(1 hunks)backend/data/src/main/kotlin/io/tolgee/formats/genericTable/in/TableParser.kt
(1 hunks)backend/data/src/main/kotlin/io/tolgee/util/CsvDelimiterDetector.kt
(2 hunks)e2e/cypress/compounds/glossaries/E2GlossaryImportDialog.ts
(1 hunks)e2e/cypress/compounds/glossaries/E2GlossaryView.ts
(2 hunks)e2e/cypress/e2e/glossary/importExport.cy.ts
(1 hunks)e2e/cypress/support/dataCyType.d.ts
(2 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryExportController.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryImportController.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/data/glossary/GlossaryImportResult.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermRepository.kt
(2 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermTranslationRepository.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryExportService.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryTermService.kt
(2 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryTermTranslationService.kt
(2 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/ImportGlossaryTerm.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/Headers.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt
(1 hunks)ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt
(1 hunks)ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestUtil.kt
(1 hunks)ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParserTest.kt
(1 hunks)ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporterTest.kt
(1 hunks)webapp/src/component/common/DragDropArea.tsx
(1 hunks)webapp/src/component/common/FileDropzone.tsx
(1 hunks)webapp/src/component/common/FileDropzoneSelectedFile.tsx
(1 hunks)webapp/src/component/common/ModeSelector.tsx
(1 hunks)webapp/src/component/common/SingleFileDropzone.tsx
(1 hunks)webapp/src/ee/billing/Invoices/DownloadButton.tsx
(1 hunks)webapp/src/ee/glossary/components/GlossaryEmptyListMessage.tsx
(3 hunks)webapp/src/ee/glossary/components/GlossaryImportDialog.tsx
(1 hunks)webapp/src/ee/glossary/components/GlossaryTermsList.tsx
(3 hunks)webapp/src/ee/glossary/components/GlossaryViewBody.tsx
(3 hunks)webapp/src/ee/glossary/components/GlossaryViewTopbar.tsx
(4 hunks)webapp/src/ee/glossary/hooks/useGlossaryExport.ts
(1 hunks)webapp/src/ee/glossary/hooks/useGlossaryImportDialog.tsx
(1 hunks)webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialog.tsx
(1 hunks)webapp/src/ee/glossary/views/GlossaryTermCreateDialog.tsx
(1 hunks)webapp/src/ee/glossary/views/GlossaryView.tsx
(2 hunks)webapp/src/fixtures/FileUploadFixtures.ts
(0 hunks)webapp/src/fixtures/useOnFilePaste.ts
(1 hunks)webapp/src/service/apiSchema.generated.ts
(6 hunks)webapp/src/views/projects/export/downloadExported.ts
(1 hunks)webapp/src/views/projects/import/ImportView.tsx
(1 hunks)webapp/src/views/projects/import/component/ImportFileDropzone.tsx
(0 hunks)webapp/src/views/projects/import/component/ImportFileInput.tsx
(3 hunks)webapp/src/views/projects/translations/Screenshots/ScreenshotDropzone.tsx
(2 hunks)webapp/src/views/projects/translations/Screenshots/useScreenshotUpload.tsx
(5 hunks)
💤 Files with no reviewable changes (2)
- webapp/src/views/projects/import/component/ImportFileDropzone.tsx
- webapp/src/fixtures/FileUploadFixtures.ts
✅ Files skipped from review due to trivial changes (1)
- ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporterTest.kt
🚧 Files skipped from review as they are similar to previous changes (17)
- webapp/src/ee/glossary/hooks/useGlossaryImportDialog.tsx
- backend/data/src/main/kotlin/io/tolgee/activity/data/ActivityType.kt
- webapp/src/views/projects/import/ImportView.tsx
- backend/data/src/main/kotlin/io/tolgee/util/CsvDelimiterDetector.kt
- backend/data/src/main/kotlin/io/tolgee/formats/genericTable/in/TableParser.kt
- ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryTermService.kt
- ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt
- ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryExportService.kt
- webapp/src/ee/glossary/hooks/useGlossaryExport.ts
- webapp/src/ee/glossary/components/GlossaryViewBody.tsx
- webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialog.tsx
- backend/data/src/main/kotlin/io/tolgee/formats/csv/in/CsvFileProcessor.kt
- webapp/src/views/projects/import/component/ImportFileInput.tsx
- webapp/src/ee/glossary/views/GlossaryTermCreateDialog.tsx
- ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/ImportGlossaryTerm.kt
- webapp/src/component/common/ModeSelector.tsx
- ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermRepository.kt
🧰 Additional context used
🧬 Code graph analysis (16)
e2e/cypress/e2e/glossary/importExport.cy.ts (3)
e2e/cypress/common/apiCalls/testData/testData.ts (1)
glossaryTestData
(131-131)e2e/cypress/compounds/glossaries/E2GlossaryView.ts (1)
E2GlossaryView
(11-96)e2e/cypress/common/shared.ts (1)
gcy
(28-29)
webapp/src/component/common/DragDropArea.tsx (1)
webapp/src/fixtures/FileUploadFixtures.ts (2)
FilesType
(42-45)getFilesAsync
(3-40)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParserTest.kt (1)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestUtil.kt (14)
hasDescription
(39-41)hasTranslation
(47-49)isTranslatable
(59-61)isNotCaseSensitive
(71-73)isNotAbbreviation
(79-81)isNotForbidden
(87-89)isNotTranslatable
(63-65)isCaseSensitive
(67-69)isAbbreviation
(75-77)isForbidden
(83-85)hasNoDescription
(43-45)hasNoFlags
(91-96)hasNoTerm
(55-57)hasNoTranslation
(51-53)
e2e/cypress/compounds/glossaries/E2GlossaryImportDialog.ts (1)
e2e/cypress/common/shared.ts (1)
gcy
(28-29)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt (1)
backend/data/src/main/kotlin/io/tolgee/model/glossary/GlossaryTermTranslation.kt (1)
GlossaryTermTranslation
(11-49)
webapp/src/ee/glossary/components/GlossaryImportDialog.tsx (5)
webapp/src/component/common/ModeSelector.tsx (2)
ModeOption
(23-28)ModeSelector
(36-81)webapp/src/fixtures/FileUploadFixtures.ts (1)
FilesType
(42-45)webapp/src/service/http/useQueryApi.ts (1)
useApiMutation
(224-265)webapp/src/service/MessageService.tsx (1)
messageService
(6-18)webapp/src/component/common/SingleFileDropzone.tsx (1)
SingleFileDropzone
(13-32)
webapp/src/views/projects/translations/Screenshots/ScreenshotDropzone.tsx (2)
webapp/src/fixtures/FileUploadFixtures.ts (1)
FilesType
(42-45)webapp/src/component/common/DragDropArea.tsx (1)
DragDropArea
(61-160)
webapp/src/ee/glossary/components/GlossaryViewTopbar.tsx (2)
webapp/src/ee/glossary/hooks/useGlossaryExport.ts (1)
useGlossaryExport
(22-56)webapp/src/component/layout/BaseViewAddButton.tsx (1)
BaseViewAddButton
(6-23)
webapp/src/component/common/SingleFileDropzone.tsx (2)
webapp/src/component/common/FileDropzone.tsx (2)
FileDropzoneProps
(54-63)FileDropzone
(65-236)webapp/src/fixtures/FileUploadFixtures.ts (1)
FilesType
(42-45)
webapp/src/ee/glossary/views/GlossaryView.tsx (1)
webapp/src/ee/glossary/components/GlossaryViewBody.tsx (1)
GlossaryViewBody
(16-73)
webapp/src/component/common/FileDropzoneSelectedFile.tsx (1)
webapp/src/fixtures/FileUploadFixtures.ts (1)
FilesType
(42-45)
webapp/src/component/common/FileDropzone.tsx (5)
webapp/src/fixtures/FileUploadFixtures.ts (1)
FilesType
(42-45)webapp/src/service/MessageService.tsx (1)
messageService
(6-18)webapp/src/fixtures/useOnFilePaste.ts (1)
useOnFilePaste
(4-26)webapp/src/component/common/FileDropzoneSelectedFile.tsx (1)
FileDropzoneSelectedFile
(37-60)webapp/src/component/common/DragDropArea.tsx (1)
DragDropArea
(61-160)
e2e/cypress/compounds/glossaries/E2GlossaryView.ts (2)
e2e/cypress/common/shared.ts (1)
gcy
(28-29)e2e/cypress/compounds/glossaries/E2GlossaryImportDialog.ts (1)
E2GlossaryImportDialog
(4-35)
webapp/src/fixtures/useOnFilePaste.ts (1)
webapp/src/fixtures/FileUploadFixtures.ts (1)
FilesType
(42-45)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt (1)
backend/data/src/main/kotlin/io/tolgee/model/glossary/GlossaryTermTranslation.kt (1)
GlossaryTermTranslation
(11-49)
webapp/src/views/projects/translations/Screenshots/useScreenshotUpload.tsx (2)
webapp/src/fixtures/FileUploadFixtures.ts (1)
FilesType
(42-45)webapp/src/views/projects/translations/Screenshots/Screenshots.tsx (1)
ALLOWED_UPLOAD_TYPES
(17-17)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Frontend static check 🪲
- GitHub Check: Build frontend 🏗️
- GitHub Check: Build backend 🏗️
🔇 Additional comments (30)
backend/data/src/main/kotlin/io/tolgee/constants/Message.kt (1)
312-312
: LGTM –FILE_PROCESSING_FAILED
is referenced inGlossaryImportService
.webapp/src/ee/billing/Invoices/DownloadButton.tsx (1)
38-52
: Excellent cleanup pattern.The nested try/finally blocks guarantee proper resource cleanup (anchor removal and URL revocation) even if errors occur during the download. The 7-second delay before revoking the blob URL is appropriate for invoice PDFs.
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestUtil.kt (2)
1-7
: LGTM! Clean utility setup.The package structure and initial assertion helper are straightforward and appropriate for test utilities.
38-57
: LGTM! Description and translation assertions are well-structured.The assertion methods for descriptions, translations, and term presence are clear and correctly implemented.
webapp/src/views/projects/export/downloadExported.ts (2)
14-28
: LGTM! Clean separation of resource cleanup.The nested try/finally structure properly ensures both DOM cleanup (anchor removal) and blob URL cleanup (revocation) happen independently, even if errors occur during the download process.
37-37
: LGTM! Correct regex reset for global flag.Resetting
lastIndex
is necessary because the global flag causes it to advance after eachexec()
call. The explanatory comment is helpful for maintainability.ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt (6)
8-13
: LGTM!Clean factory function with sensible defaults for test data creation.
15-20
: LGTM!Proper initialization of the term with mutable collections for translations.
22-29
: LGTM!Correctly establishes the bidirectional relationship between term and translation while enabling fluent chaining.
31-42
: LGTM!Good use of nullable parameters with safe-call operators to enable selective flag updates.
69-71
: LGTM!Clean DSL entry point following standard Kotlin builder patterns.
124-128
: LGTM!Elegant extension enabling nested DSL syntax for adding terms within a glossary builder.
webapp/src/fixtures/useOnFilePaste.ts (1)
1-26
: LGTM! Dependency array issue resolved.The hook correctly includes
onFilePaste
in the dependency array (line 25), which addresses the previously flagged stale closure concern. The paste listener logic is sound: it reads clipboard files, filters empty pastes, and invokes the callback with the expectedFilesType
format.ee/backend/app/src/main/kotlin/io/tolgee/ee/data/glossary/GlossaryImportResult.kt (1)
1-8
: LGTM! Clean DTO for import response.The data class is well-structured for its purpose as an API response. The
@Schema
annotation provides helpful documentation for API consumers.ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermTranslationRepository.kt (1)
15-25
: LGTM! Type-safe refactoring.The method signature change from separate
organizationId
andglossaryId
parameters to a singleGlossary
entity improves type safety and aligns with similar methods in this repository (e.g.,updateBaseLanguage
). The JPQL query is cleaner when working with the entity directly.webapp/src/ee/glossary/views/GlossaryView.tsx (1)
1-45
: LGTM! Good separation of concerns.The refactoring moves dialog management from
GlossaryView
toGlossaryViewBody
, which now handles creation and import dialogs via hooks (useGlossaryTermCreateDialog
anduseGlossaryImportDialog
). This simplifiesGlossaryView
to focus on layout and navigation.webapp/src/component/common/SingleFileDropzone.tsx (1)
1-32
: LGTM! Type-safe single-file wrapper.The component correctly handles the conversion between a single file (
FilesType[number]
) and the array type (FilesType
) thatFileDropzone
expects. The type signatures are now correct, addressing the concerns raised in past reviews.e2e/cypress/compounds/glossaries/E2GlossaryView.ts (1)
81-95
: LGTM! Well-structured test helpers.The three new methods provide clear entry points for testing import/export flows. They follow the established patterns in this class, use proper data-cy selectors, and return appropriate page objects for chaining.
e2e/cypress/e2e/glossary/importExport.cy.ts (1)
1-90
: LGTM! Comprehensive E2E test coverage.The test suite thoroughly covers the import/export functionality:
- Import into empty glossary
- Replace existing terms
- Add to existing terms
- CSV export with proper response validation
The tests follow good practices with proper setup/teardown, page objects, and meaningful assertions.
e2e/cypress/support/dataCyType.d.ts (1)
295-297
: file-dropzone-remove-button verified in UI. Found inwebapp/src/component/common/FileDropzoneSelectedFile.tsx:54
.webapp/src/views/projects/translations/Screenshots/useScreenshotUpload.tsx (2)
80-92
: LGTM: FilesType adoption + multipart payload.Mapping FilesType to multipart via screenshot: file.file is consistent and clear.
57-74
: Verify config.maxUploadFileSize unit. Unable to locate its definition in the front-end config; confirm whethermaxUploadFileSize
is specified in kilobytes (making* 1024
correct) or already in bytes (in which case the multiplier should be removed).ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryTermTranslationService.kt (1)
26-33
: LGTM: delegation to glossary-based query and bulk save helper.Lazy injection prevents circular deps; the glossary overload and saveAll helper are straightforward.
Also applies to: 111-113
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/Headers.kt (1)
3-17
: LGTM: stable CSV header constants and ordered list.Names and ordering are clear and consistent for import/export.
webapp/src/component/common/FileDropzoneSelectedFile.tsx (1)
1-60
: LGTM! Clean component implementation.The component is well-structured with proper TypeScript typing, accessibility attributes, event handling, and test coverage. The use of styled components for layout, translation hooks for i18n, and proper event propagation control demonstrates good practices.
webapp/src/ee/glossary/components/GlossaryImportDialog.tsx (1)
44-140
: LGTM! All previous concerns addressed.The component now correctly:
- Uses
glossary.organizationOwner.id
instead of the nullablepreferredOrganization!.id
(line 68)- Stores the single file wrapper as
FilesType[number] | null
(line 52)- Passes the raw
File
blob viafile.file
to the mutation (line 73)- Includes proper mode selection UI, accessibility attributes, and loading states
The
as any
assertion on line 73 is acceptable given OpenAPI schema typing constraints.webapp/src/component/common/DragDropArea.tsx (1)
117-159
: LGTM! Overlay and icon logic is well-implemented.The overlay visibility logic correctly handles three states (canShow, forceShow, dragOver), and the valid/invalid icon selection provides clear visual feedback. The StyledOverlay with transition and backdrop-filter creates a polished UX.
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt (1)
1-53
: LGTM! All previous concerns resolved.The exporter implementation now correctly:
- Ensures deterministic column ordering with
.sorted()
(line 16)- Closes the CSV writer with
.use { }
to prevent resource leaks (line 43)- Uses explicit UTF-8 encoding with
byteInputStream(Charsets.UTF_8)
(line 47)- Properly handles non-translatable terms by replicating base text (lines 32-37)
- Documents the inverted
flagNonTranslatable
logic (line 27)The code is clean, well-structured, and production-ready.
webapp/src/component/common/FileDropzone.tsx (1)
79-81
: Empty acceptedFileTypes rejects everything — confirm intentAs written, an empty list makes every file invalid. Is that desired? If not, either enforce non-empty via prop validation or allow all types when empty.
Option if allowing all:
const isValidFileType = (fileName: string) => acceptedFileTypes.length === 0 || findFileType(fileName) !== undefined;ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryImportController.kt (1)
34-36
: ```markdownConfirm tokenType fits UI flow
GlossaryImportController.kt:34 uses @AllowApiAccess(AuthTokenType.ONLY_PAT), which restricts to PATs (default = ANY allows session/JWT). If the EE UI invokes this import endpoint with session/JWT, switch to default.
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt
Show resolved
Hide resolved
...ckend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt
Show resolved
Hide resolved
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestUtil.kt
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
webapp/src/ee/glossary/hooks/useGlossaryImportDialogController.ts (1)
12-13
: Verify null-safety of organization comparison.If
preferredOrganization
isnull
orundefined
, andglossary.organizationOwner.id
is alsonull
/undefined
, this comparison evaluates totrue
, potentially granting unintended permissions. This issue is shared withuseGlossaryTermCreateDialogController.ts
. Ensure that both values are defined before comparing.This issue is also present in
useGlossaryTermCreateDialogController.ts
at lines 12-13. See the verification script in that file's review comment.
🧹 Nitpick comments (2)
webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialogController.ts (1)
9-11
: Naming inconsistency:isUserMaintainer
checks both OWNER and MAINTAINER roles.The variable name
isUserMaintainer
only mentions "Maintainer" but checks for both'OWNER'
and'MAINTAINER'
roles. This is inconsistent with the parallel hook inuseGlossaryImportDialogController.ts
which uses the more accurate nameisUserMaintainerOrOwner
. Consider renaming for clarity.Apply this diff to improve naming consistency:
- const isUserMaintainer = ['OWNER', 'MAINTAINER'].includes( + const isUserMaintainerOrOwner = ['OWNER', 'MAINTAINER'].includes( preferredOrganization?.currentUserRole || '' ); const isGlossaryUnderPreference = glossary.organizationOwner.id === preferredOrganization?.id; const [createTermDialogOpen, setCreateTermDialogOpen] = useState(false); const onCreateTerm = () => { setCreateTermDialogOpen(true); }; const canCreate = - isGlossaryUnderPreference && (isUserMaintainer || isUserAdmin); + isGlossaryUnderPreference && (isUserMaintainerOrOwner || isUserAdmin);webapp/src/ee/glossary/hooks/useGlossaryImportDialogController.ts (1)
5-35
: Extract shared logic to reduce code duplication.This hook is almost identical to
useGlossaryTermCreateDialogController.ts
with only naming differences (import
vscreate
,isUserMaintainerOrOwner
vsisUserMaintainer
). Consider extracting the common pattern into a shared generic hook to improve maintainability and reduce duplication.For example, create a generic controller hook:
// useGlossaryDialogController.ts export const useGlossaryDialogController = (actionType: 'create' | 'import') => { const { preferredOrganization } = usePreferredOrganization(); const glossary = useGlossary(); const isUserAdmin = useIsAdmin(); const isUserMaintainerOrOwner = ['OWNER', 'MAINTAINER'].includes( preferredOrganization?.currentUserRole || '' ); const isGlossaryUnderPreference = glossary.organizationOwner.id === preferredOrganization?.id; const [dialogOpen, setDialogOpen] = useState(false); const onAction = () => { setDialogOpen(true); }; const canPerformAction = isGlossaryUnderPreference && (isUserMaintainerOrOwner || isUserAdmin); const isOpen = dialogOpen && canPerformAction; return { onAction: canPerformAction ? onAction : undefined, dialogOpen: isOpen, dialogProps: { open: isOpen, onClose: () => setDialogOpen(false), onFinished: () => setDialogOpen(false), }, }; };Then use it in both files:
// useGlossaryTermCreateDialogController.ts export const useGlossaryTermCreateDialogController = () => { const { onAction, dialogOpen, dialogProps } = useGlossaryDialogController('create'); return { onCreateTerm: onAction, createTermDialogOpen: dialogOpen, createTermDialogProps: dialogProps, }; }; // useGlossaryImportDialogController.ts export const useGlossaryImportDialogController = () => { const { onAction, dialogOpen, dialogProps } = useGlossaryDialogController('import'); return { onImport: onAction, importDialogOpen: dialogOpen, importDialogProps: dialogProps, }; };
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
webapp/src/ee/glossary/components/GlossaryViewBody.tsx
(3 hunks)webapp/src/ee/glossary/hooks/useGlossaryImportDialogController.ts
(1 hunks)webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialogController.ts
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- webapp/src/ee/glossary/components/GlossaryViewBody.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
webapp/src/ee/glossary/hooks/useGlossaryImportDialogController.ts (1)
webapp/src/globalContext/helpers.tsx (2)
usePreferredOrganization
(31-43)useIsAdmin
(21-22)
webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialogController.ts (1)
webapp/src/globalContext/helpers.tsx (2)
usePreferredOrganization
(31-43)useIsAdmin
(21-22)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Frontend static check 🪲
- GitHub Check: Build backend 🏗️
🔇 Additional comments (1)
webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialogController.ts (1)
12-13
: Verify null-safety of organization comparison
Ensure bothpreferredOrganization
andglossary.organizationOwner.id
are non-null before comparing (e.g. guard withif (!preferredOrganization?.id || !glossary.organizationOwner.id) return false;
) to avoid unintendedtrue
matches.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (1)
83-90
: Critical: Revert to coercing unknown values to false instead of null.Line 88 returns
false
for unrecognized boolean values, which can silently corrupt data. Past review comment #2 recommended changing this to returnnull
for unknown values and was marked as addressed, but the fix appears to have been reverted or not applied in the current code.Apply this diff to handle unrecognized values safely:
private fun parseBoolean(value: String?): Boolean? { if (value.isNullOrBlank()) return null return when (value.trim().lowercase()) { "true", "1", "yes", "y", "t" -> true "false", "0", "no", "n", "f" -> false - else -> false + else -> null } }
🧹 Nitpick comments (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (1)
77-81
: Add negative index guard for defensive coding.The function checks the upper bound but not the lower bound. While current callers (via
findHeaderIndex
) won't pass negative indices, adding a check foridx < 0
would make this helper more robust against future misuse.Apply this diff to add the defensive check:
fun Array<String>.getSafe(idx: Int?): String? { if (idx == null) return null + if (idx < 0) return null if (idx >= size) return null return this[idx].ifBlank { null } }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt
(1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Frontend static check 🪲
- GitHub Check: Build backend 🏗️
🔇 Additional comments (6)
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (6)
15-23
: LGTM!The constructor and lazy reader initialization are correctly implemented. The use of CSVReaderBuilder with the custom delimiter configuration is appropriate.
25-33
: LGTM!The data properties correctly handle CSV structure: reading all rows, extracting trimmed headers from the first row, and separating data rows.
35-48
: LGTM!The header index computation correctly identifies special columns and derives translation columns as the remaining indices. The lazy evaluation strategy is appropriate for this parsing workflow.
50-52
: LGTM!The parse method is concise and correct, delegating row-to-term conversion to the extension function and filtering null results.
54-71
: LGTM!The extension correctly converts array rows to ImportGlossaryTerm. The non-null assertion on line 64 is safe because
idxTranslations
would be empty ifheaders
were null. The logic for filtering empty rows and blank translations is appropriate.
73-75
: LGTM!The
findHeaderIndex
method correctly returns null when the header is not found or when headers are absent, avoiding the -1 issue from previous reviews.
… the beggining of the text when parsing file extension
3a6f910
to
c69541e
Compare
(resolved conflicts caused by the supporter being merged; no changes otherwise) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
webapp/src/views/projects/export/downloadExported.ts (1)
34-44
: Make parseExtension robust (RFC 6266) and drop stateful global regex.Current regex fails for filename* (UTF‑8) or when extra params follow filename; global flag + lastIndex is brittle.
-const parseExtension = (response: Response) => { - const contentDisposition = response.headers.get('Content-Disposition'); - if (contentDisposition) { - extensionRegex.lastIndex = 0; // Reset the regex to the start of the string - const match = extensionRegex.exec(contentDisposition); - if (match) { - return match[1]; - } - } - return null; -}; - -const extensionRegex = /^.*\.(\w+)"$/gm; +const parseExtension = (response: Response) => { + const cd = response.headers.get('Content-Disposition'); + if (!cd) return null; + let filename: string | null = null; + // Prefer RFC 5987/6266 filename* + const star = cd.match(/filename\*\s*=\s*([^;]+)/i); + if (star && star[1]) { + const raw = star[1].trim().replace(/^"(.*)"$/, '$1'); + const unprefixed = raw.replace(/^UTF-8''/i, ''); + try { + filename = decodeURIComponent(unprefixed); + } catch { + filename = unprefixed; + } + } + if (!filename) { + const normal = cd.match(/filename\s*=\s*(?:"([^"]+)"|([^;]+))/i); + if (normal) filename = (normal[1] || normal[2]).trim(); + } + if (!filename) return null; + const dot = filename.lastIndexOf('.'); + return dot > -1 ? filename.slice(dot + 1) : null; +};Also applies to: 46-46
ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermRepository.kt (1)
36-46
: Refactor glossary term search queries to handle empty sets and fix language filtering semantics.The review comment correctly identifies two critical bugs:
Empty set SQL failure: When controller converts empty request parameter to empty Set via
toSet()
, the SQLIN ()
clause fails. Theis null
check only catches null, not empty sets.Search semantics failure: Queries join only to
baseLanguageTag
translations, so searching with non-base languages (e.g.,languageTags={"cs","de"}
where base is "en") returns no results because the translation join yields nothing.All three methods are affected identically:
findByGlossaryPaged
(lines 34-53)findByGlossaryIdsPaged
(lines 55-75)findAllIds
(lines 95-113)The suggested EXISTS-based refactoring correctly addresses both issues by removing the problematic left join and allowing independent language filtering per translation. Additionally, ensure the controller or service layer converts empty lists to null before passing to repository (or apply the refactor, which eliminates the need for that conversion).
♻️ Duplicate comments (9)
webapp/src/component/common/FileDropzone.tsx (1)
221-226
: Addnoreferrer
to prevent referrer leakage.External links opened in a new tab should include
noreferrer
to prevent the Referer header from being sent to the target site, improving privacy and security.Apply:
- rel="noopener" + rel="noopener noreferrer"webapp/src/views/projects/export/downloadExported.ts (1)
29-31
: Reduce the 7s URL revoke delay to avoid memory bloat.A 7000 ms timeout holds large blobs unnecessarily; next‑tick or ~100 ms is enough for Safari/WebKit.
- setTimeout(() => URL.revokeObjectURL(url), 7000); + // Delay just enough for Safari/WebKit to start reading the blob + setTimeout(() => URL.revokeObjectURL(url), 100);webapp/src/ee/glossary/hooks/useGlossaryExport.ts (1)
42-46
: Type cast bypasses type safety.The double cast
response as unknown as Response
remains from the previous review and bypasses TypeScript's type checking. This occurs becauseuseApiMutation
withrawResponse: true
doesn't correctly type the response asResponse
.As noted in the previous review, the root cause should be addressed in the API schema generation or
useApiMutation
generic signature to properly type raw responses, eliminating the need for this unsafe cast.ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryImportController.kt (1)
52-54
: Ensure parse/validation failures abort the import (no partial or empty glossaries).If parsing or validation fails after delete, users could end up with data loss unless exceptions trigger rollback. Make importCsv surface deterministic errors and transactional semantics so failures roll back deletes/inserts. This mirrors prior feedback.
ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryExportController.kt (1)
57-61
: Don’t close the servlet OutputStream; let Spring manage it. Also use theuse
receiver.Remove
out.close()
and copy from theuse
receiver to avoid double-close and confusion.- return ResponseEntity.ok().headers(headers).body( - streamingResponseBodyProvider.createStreamingResponseBody { out: OutputStream -> - stream.use { IOUtils.copy(stream, out) } - out.close() - }, - ) + return ResponseEntity.ok().headers(headers).body( + streamingResponseBodyProvider.createStreamingResponseBody { out: OutputStream -> + stream.use { IOUtils.copy(it, out) } + }, + )ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (1)
83-89
: Don’t coerce unknown boolean tokens to false.Return null for unrecognized values to avoid silent data corruption; this was flagged before.
private fun parseBoolean(value: String?): Boolean? { if (value.isNullOrBlank()) return null return when (value.trim().lowercase()) { "true", "1", "yes", "y", "t" -> true "false", "0", "no", "n", "f" -> false - else -> false + else -> null } }webapp/src/service/apiSchema.generated.ts (1)
9170-9177
: Fix the export media type.
GET /v2/organizations/{organizationId}/glossaries/{glossaryId}/export
streams CSV, yet the schema announces"application/json"
. Clients generated from this spec will JSON-decode the stream and fail. Align the 200 response with the real payload (e.g."text/csv"
like other CSV endpoints).
Example fix:- content: { - "application/json": components["schemas"]["StreamingResponseBody"]; - }; + content: { + "text/csv": components["schemas"]["StreamingResponseBody"]; + };ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestUtil.kt (1)
59-96
: Fix nullable flag handling as previously noted.The type inconsistency issue with nullable Boolean flags remains unaddressed. As noted in the previous review,
hasNoFlags()
expects nullable flags but methods likeisTranslatable()
,isCaseSensitive()
, etc., useisTrue()
/isFalse()
which don't handle nullability semantically. When a flag isnull
(unset), these assertions may not behave as intended.Please apply the fix suggested in the previous review to handle nullability correctly.
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt (1)
44-67
: Address uninitializedbaseLanguageTag
as previously noted.The issue with the uninitialized
lateinit var baseLanguageTag
remains unresolved. Ifbuild()
is called without first callingwithBaseLanguageTag()
, it will throwUninitializedPropertyAccessException
, leading to confusing test failures.Please implement one of the solutions suggested in the previous review (either add validation in
build()
or make it a required constructor parameter).
🧹 Nitpick comments (10)
e2e/cypress/compounds/glossaries/E2GlossaryView.ts (1)
93-95
: Consider adding a visibility check before clicking.The export likely triggers a download rather than opening a dialog, which explains the minimal implementation. For better test robustness, you could add a visibility or enabled check before clicking:
clickExport() { + gcy('glossary-export-button').should('be.visible'); gcy('glossary-export-button').click(); }
This ensures the button is available before attempting to click it, making test failures more informative.
webapp/src/ee/billing/Invoices/DownloadButton.tsx (1)
38-52
: Nested try/finally ensures proper cleanup; however, consider documenting the 7-second timeout rationale.The nested try/finally blocks correctly guarantee cleanup of both the anchor element and object URL, even if errors occur. This pattern is consistently applied across the codebase (e.g.,
useGlossaryExport.ts
,downloadExported.ts
).However, web best practices recommend revoking blob URLs after 300–1000ms (once the download starts or via completion events), whereas this code uses 7000ms. While this conservative timeout is used consistently across multiple download/export utilities and appears intentional, consider adding a brief inline comment documenting why the 7-second delay is necessary. If this addresses specific browser compatibility or large-file-handling requirements discovered during development, that context will help future maintainers understand the choice.
webapp/src/component/common/SingleFileDropzone.tsx (1)
18-18
: Consider explicit type annotation for clarity.While TypeScript can infer the type, adding an explicit annotation improves readability and prevents future type-related confusion.
Apply:
- const files = file ? [file] : []; + const files: FilesType = file ? [file] : [];webapp/src/views/projects/export/downloadExported.ts (1)
14-28
: Append the anchor before click for cross‑browser reliability.Calling click() on a detached node can be flaky on some Safari/WebKit builds; remove() is a no‑op if never appended.
try { const a = document.createElement('a'); try { + document.body.appendChild(a); a.href = url; - if (data.type === 'application/zip') { - a.download = projectName + dateStr + '.zip'; - } else { - const extension = parseExtension(response) || format.extension; - a.download = - projectName + onlyPossibleLanguageString + dateStr + '.' + extension; - } + // set below (unified naming) a.click(); } finally { a.remove(); }webapp/src/ee/glossary/hooks/useGlossaryImportDialogController.ts (1)
1-1
: Stabilize handlers/props to reduce re-renders.Wrap onImport in useCallback and memoize importDialogProps.
-import { useState } from 'react'; +import { useState, useCallback, useMemo } from 'react'; @@ - const onImport = () => { - setImportDialogOpen(true); - }; + const onImport = useCallback(() => { + setImportDialogOpen(true); + }, []); @@ - return { - onImport: canImport ? onImport : undefined, - importDialogOpen: isOpen, - importDialogProps: { - open: isOpen, - onClose: () => setImportDialogOpen(false), - onFinished: () => setImportDialogOpen(false), - }, - }; + const importDialogProps = useMemo( + () => ({ + open: isOpen, + onClose: () => setImportDialogOpen(false), + onFinished: () => setImportDialogOpen(false), + }), + [isOpen] + ); + + return { + onImport: canImport ? onImport : undefined, + importDialogOpen: isOpen, + importDialogProps, + };Also applies to: 17-19, 26-34
ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryImportController.kt (1)
38-45
: Avoid controller-scoped transaction over file I/O; validate before delete.Parsing/upload can be slow; holding a DB tx here risks long locks. Parse and validate first (outside tx), then perform delete-and-bulk-insert inside a short service-level @transactional to guarantee rollback if any step fails. Also only delete after successful parse.
webapp/src/ee/glossary/components/GlossaryViewBody.tsx (2)
18-18
: PreferReact.FC
or a typed function over deprecatedReact.VFC
.
VFC
was removed from @types/react; using it may break on newer React typings. Switch toReact.FC<Props>
or a plain function component.
45-53
: Conditional mounting of dialogs is redundant.You can always render the dialogs and control visibility via
open
; simplifies diffing and avoids remount side effects.- {createTermDialogOpen && ( - <GlossaryTermCreateDialog {...createTermDialogProps} /> - )} - {importDialogOpen && ( - <GlossaryImportDialog - {...importDialogProps} - hasExistingTerms={hasExistingTerms} - /> - )} + <GlossaryTermCreateDialog {...createTermDialogProps} /> + <GlossaryImportDialog + {...importDialogProps} + hasExistingTerms={hasExistingTerms} + />ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt (1)
77-81
: Guard negative indices ingetSafe
.Extra safety if callers ever pass -1.
fun Array<String>.getSafe(idx: Int?): String? { if (idx == null) return null - if (idx >= size) return null + if (idx < 0 || idx >= size) return null return this[idx].ifBlank { null } }ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt (1)
15-20
: Consider preserving null description values.Line 17 converts
null
descriptions to empty strings, which loses the semantic difference between "no description provided" and "explicitly empty description." If the model supports nullable descriptions, consider preserving the null value.If intentional, this is fine. Otherwise, apply this diff:
fun createTerm(description: String? = null): GlossaryTerm { return GlossaryTerm().apply { - this.description = description ?: "" + this.description = description this.translations = mutableListOf() } }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
e2e/cypress/fixtures/glossary/import_basic.csv
is excluded by!**/*.csv
📒 Files selected for processing (51)
backend/data/src/main/kotlin/io/tolgee/activity/data/ActivityType.kt
(1 hunks)backend/data/src/main/kotlin/io/tolgee/constants/Message.kt
(1 hunks)backend/data/src/main/kotlin/io/tolgee/formats/csv/in/CsvFileProcessor.kt
(1 hunks)backend/data/src/main/kotlin/io/tolgee/formats/genericTable/in/TableParser.kt
(1 hunks)backend/data/src/main/kotlin/io/tolgee/util/CsvDelimiterDetector.kt
(2 hunks)e2e/cypress/compounds/glossaries/E2GlossaryImportDialog.ts
(1 hunks)e2e/cypress/compounds/glossaries/E2GlossaryView.ts
(2 hunks)e2e/cypress/e2e/glossary/importExport.cy.ts
(1 hunks)e2e/cypress/support/dataCyType.d.ts
(2 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryExportController.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/glossary/GlossaryImportController.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/data/glossary/GlossaryImportResult.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermRepository.kt
(2 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermTranslationRepository.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryExportService.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryTermService.kt
(2 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryTermTranslationService.kt
(2 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/ImportGlossaryTerm.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/Headers.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt
(1 hunks)ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt
(1 hunks)ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt
(1 hunks)ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestUtil.kt
(1 hunks)ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParserTest.kt
(1 hunks)ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporterTest.kt
(1 hunks)webapp/src/component/common/DragDropArea.tsx
(1 hunks)webapp/src/component/common/FileDropzone.tsx
(1 hunks)webapp/src/component/common/FileDropzoneSelectedFile.tsx
(1 hunks)webapp/src/component/common/ModeSelector.tsx
(1 hunks)webapp/src/component/common/SingleFileDropzone.tsx
(1 hunks)webapp/src/ee/billing/Invoices/DownloadButton.tsx
(1 hunks)webapp/src/ee/glossary/components/GlossaryEmptyListMessage.tsx
(3 hunks)webapp/src/ee/glossary/components/GlossaryImportDialog.tsx
(1 hunks)webapp/src/ee/glossary/components/GlossaryTermsList.tsx
(3 hunks)webapp/src/ee/glossary/components/GlossaryViewBody.tsx
(3 hunks)webapp/src/ee/glossary/components/GlossaryViewTopbar.tsx
(4 hunks)webapp/src/ee/glossary/hooks/useGlossaryExport.ts
(1 hunks)webapp/src/ee/glossary/hooks/useGlossaryImportDialogController.ts
(1 hunks)webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialogController.ts
(1 hunks)webapp/src/ee/glossary/views/GlossaryTermCreateDialog.tsx
(1 hunks)webapp/src/ee/glossary/views/GlossaryView.tsx
(2 hunks)webapp/src/fixtures/FileUploadFixtures.ts
(0 hunks)webapp/src/fixtures/useOnFilePaste.ts
(1 hunks)webapp/src/service/apiSchema.generated.ts
(6 hunks)webapp/src/views/projects/export/downloadExported.ts
(1 hunks)webapp/src/views/projects/import/ImportView.tsx
(1 hunks)webapp/src/views/projects/import/component/ImportFileDropzone.tsx
(0 hunks)webapp/src/views/projects/import/component/ImportFileInput.tsx
(3 hunks)webapp/src/views/projects/translations/Screenshots/ScreenshotDropzone.tsx
(2 hunks)webapp/src/views/projects/translations/Screenshots/useScreenshotUpload.tsx
(5 hunks)
💤 Files with no reviewable changes (2)
- webapp/src/fixtures/FileUploadFixtures.ts
- webapp/src/views/projects/import/component/ImportFileDropzone.tsx
🚧 Files skipped from review as they are similar to previous changes (17)
- webapp/src/ee/glossary/views/GlossaryTermCreateDialog.tsx
- ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/ImportGlossaryTerm.kt
- e2e/cypress/e2e/glossary/importExport.cy.ts
- e2e/cypress/support/dataCyType.d.ts
- backend/data/src/main/kotlin/io/tolgee/util/CsvDelimiterDetector.kt
- webapp/src/component/common/ModeSelector.tsx
- backend/data/src/main/kotlin/io/tolgee/formats/csv/in/CsvFileProcessor.kt
- webapp/src/fixtures/useOnFilePaste.ts
- backend/data/src/main/kotlin/io/tolgee/formats/genericTable/in/TableParser.kt
- e2e/cypress/compounds/glossaries/E2GlossaryImportDialog.ts
- webapp/src/component/common/DragDropArea.tsx
- webapp/src/ee/glossary/components/GlossaryImportDialog.tsx
- ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporterTest.kt
- ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryTermService.kt
- ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt
- ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/Headers.kt
- ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryExportService.kt
🧰 Additional context used
🧬 Code graph analysis (16)
webapp/src/ee/glossary/views/GlossaryView.tsx (1)
webapp/src/ee/glossary/components/GlossaryViewBody.tsx (1)
GlossaryViewBody
(18-84)
webapp/src/views/projects/import/component/ImportFileInput.tsx (2)
webapp/src/component/common/DragDropArea.tsx (1)
DragDropArea
(61-160)webapp/src/views/projects/translations/Screenshots/Screenshots.tsx (1)
MAX_FILE_COUNT
(16-16)
webapp/src/ee/glossary/hooks/useGlossaryExport.ts (2)
webapp/src/views/projects/export/downloadExported.ts (1)
downloadExported
(3-32)webapp/src/service/http/useQueryApi.ts (1)
useApiMutation
(224-265)
webapp/src/views/projects/translations/Screenshots/ScreenshotDropzone.tsx (3)
webapp/src/fixtures/FileUploadFixtures.ts (1)
FilesType
(42-45)webapp/src/component/common/DragDropArea.tsx (1)
DragDropArea
(61-160)webapp/src/views/projects/translations/Screenshots/Screenshots.tsx (1)
MAX_FILE_COUNT
(16-16)
webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialogController.ts (1)
webapp/src/globalContext/helpers.tsx (2)
usePreferredOrganization
(42-54)useIsAdmin
(21-22)
webapp/src/views/projects/translations/Screenshots/useScreenshotUpload.tsx (2)
webapp/src/fixtures/FileUploadFixtures.ts (1)
FilesType
(42-45)webapp/src/views/projects/translations/Screenshots/Screenshots.tsx (1)
ALLOWED_UPLOAD_TYPES
(17-17)
webapp/src/component/common/FileDropzoneSelectedFile.tsx (1)
webapp/src/fixtures/FileUploadFixtures.ts (1)
FilesType
(42-45)
webapp/src/component/common/SingleFileDropzone.tsx (2)
webapp/src/component/common/FileDropzone.tsx (2)
FileDropzoneProps
(54-63)FileDropzone
(65-236)webapp/src/fixtures/FileUploadFixtures.ts (1)
FilesType
(42-45)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParserTest.kt (1)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestUtil.kt (14)
hasDescription
(39-41)hasTranslation
(47-49)isTranslatable
(59-61)isNotCaseSensitive
(71-73)isNotAbbreviation
(79-81)isNotForbidden
(87-89)isNotTranslatable
(63-65)isCaseSensitive
(67-69)isAbbreviation
(75-77)isForbidden
(83-85)hasNoDescription
(43-45)hasNoFlags
(91-96)hasNoTerm
(55-57)hasNoTranslation
(51-53)
webapp/src/ee/glossary/components/GlossaryViewBody.tsx (5)
webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialogController.ts (1)
useGlossaryTermCreateDialogController
(5-35)webapp/src/ee/glossary/hooks/useGlossaryImportDialogController.ts (1)
useGlossaryImportDialogController
(5-35)webapp/src/ee/glossary/views/GlossaryTermCreateDialog.tsx (1)
GlossaryTermCreateDialog
(28-84)webapp/src/ee/glossary/components/GlossaryImportDialog.tsx (1)
GlossaryImportDialog
(44-140)webapp/src/ee/glossary/components/GlossaryViewTopbar.tsx (1)
GlossaryViewTopbar
(28-98)
webapp/src/ee/glossary/components/GlossaryViewTopbar.tsx (2)
webapp/src/ee/glossary/hooks/useGlossaryExport.ts (1)
useGlossaryExport
(22-56)webapp/src/component/layout/BaseViewAddButton.tsx (1)
BaseViewAddButton
(6-23)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt (1)
backend/data/src/main/kotlin/io/tolgee/model/glossary/GlossaryTermTranslation.kt (1)
GlossaryTermTranslation
(11-49)
webapp/src/component/common/FileDropzone.tsx (5)
webapp/src/fixtures/FileUploadFixtures.ts (1)
FilesType
(42-45)webapp/src/service/MessageService.tsx (1)
messageService
(6-18)webapp/src/fixtures/useOnFilePaste.ts (1)
useOnFilePaste
(4-26)webapp/src/component/common/FileDropzoneSelectedFile.tsx (1)
FileDropzoneSelectedFile
(37-60)webapp/src/component/common/DragDropArea.tsx (1)
DragDropArea
(61-160)
webapp/src/views/projects/import/ImportView.tsx (2)
webapp/src/views/projects/import/hooks/useImportDataHelper.tsx (1)
useImportDataHelper
(28-138)webapp/src/fixtures/useOnFilePaste.ts (1)
useOnFilePaste
(4-26)
webapp/src/ee/glossary/hooks/useGlossaryImportDialogController.ts (1)
webapp/src/globalContext/helpers.tsx (2)
usePreferredOrganization
(42-54)useIsAdmin
(21-22)
e2e/cypress/compounds/glossaries/E2GlossaryView.ts (2)
e2e/cypress/common/shared.ts (1)
gcy
(28-29)e2e/cypress/compounds/glossaries/E2GlossaryImportDialog.ts (1)
E2GlossaryImportDialog
(4-35)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Frontend static check 🪲
- GitHub Check: Build backend 🏗️
🔇 Additional comments (30)
backend/data/src/main/kotlin/io/tolgee/constants/Message.kt (1)
314-314
: LGTM! Enum constant is properly utilized.The new
FILE_PROCESSING_FAILED
constant is correctly positioned at the end of the enum and verified to be actively used in the glossary import error handling—specifically thrown when CSV parsing fails inGlossaryImportService.kt
(line 31). The naming and implementation are appropriate for the feature's requirements.e2e/cypress/compounds/glossaries/E2GlossaryView.ts (3)
3-3
: LGTM!The import is necessary for the new methods and follows the established pattern.
81-85
: LGTM!The method follows the established pattern (click → verify visibility → return dialog object) and is consistent with similar methods like
openCreateTermDialog
.
87-91
: LGTM!The method correctly implements the dialog-opening pattern and differentiates from the empty-state import flow by using a different button trigger.
webapp/src/views/projects/import/ImportView.tsx (1)
25-29
: LGTM! Clean integration of paste handling.The
useOnFilePaste
hook is correctly wired to the existingdataHelper.onNewFiles
handler, enabling paste-triggered file uploads at the view level.webapp/src/views/projects/import/component/ImportFileInput.tsx (1)
137-141
: LGTM! Successful migration to unified DragDropArea.The component correctly migrates from
ImportFileDropzone
toDragDropArea
with proper prop wiring andmaxItems
enforcement.webapp/src/views/projects/translations/Screenshots/ScreenshotDropzone.tsx (1)
31-39
: LGTM! Clean refactor to DragDropArea.The component correctly delegates drag-and-drop handling to
DragDropArea
while maintaining permission-based control andmaxItems
enforcement.webapp/src/views/projects/translations/Screenshots/useScreenshotUpload.tsx (1)
35-42
: LGTM! Correct FilesType migration.The conversion from
File[]
toFilesType
is properly implemented with correct property mapping and access patterns throughout the validation and upload flow.webapp/src/component/common/FileDropzoneSelectedFile.tsx (1)
31-60
: LGTM! Well-designed file chip component.Clean implementation with proper accessibility (
aria-label
), event handling (stopPropagation
), and type safety (FilesType[number]
).webapp/src/component/common/FileDropzone.tsx (2)
83-102
: LGTM! Robust file handling with validation and user feedback.The implementation correctly validates file types, enforces
maxFiles
limits, and provides clear error messages when files are rejected or the limit is exceeded.
125-125
: LGTM! Paste integration correctly routes through validation.Calling
useOnFilePaste
withhandleFilesReceived
ensures pasted files undergo the same validation, type checking, andmaxFiles
enforcement as dropped or selected files.ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryTermTranslationService.kt (2)
18-19
: Circular dependency detected.The
@Lazy
annotation onglossaryService
indicates a circular dependency betweenGlossaryTermTranslationService
andGlossaryService
. While@Lazy
resolves the injection issue, it suggests a potential design concern that may complicate testing and maintenance.Consider verifying whether the services can be refactored to break the circular dependency by introducing an intermediary service or moving shared logic to a separate component.
22-33
: LGTM!The refactored
getDistinctLanguageTags
method cleanly delegates to the new overload, improving reusability by accepting aGlossary
object directly. This aligns well with the broader glossary export/import flow.webapp/src/ee/glossary/views/GlossaryView.tsx (1)
1-45
: LGTM!The refactoring simplifies
GlossaryView
by moving dialog state management to dedicated hooks (controller pattern). This reduces component complexity and improves separation of concerns, aligning with the new glossary import/export architecture.ee/backend/app/src/main/kotlin/io/tolgee/ee/data/glossary/GlossaryImportResult.kt (1)
1-8
: LGTM!The
GlossaryImportResult
class is a clean, well-documented DTO with appropriate OpenAPI schema annotations. The structure is simple and suitable for its purpose.backend/data/src/main/kotlin/io/tolgee/activity/data/ActivityType.kt (1)
83-83
: LGTM!The new
GLOSSARY_IMPORT
activity type follows the established pattern (usingonlyCountsInList = true
likeIMPORT
) and is properly positioned among other glossary-related activities.ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermTranslationRepository.kt (1)
15-25
: LGTM!The refactored
findDistinctLanguageTagsByGlossary
method simplifies the query by accepting aGlossary
object instead of separate IDs. This improves type safety and aligns with the service layer changes.webapp/src/ee/glossary/hooks/useGlossaryTermCreateDialogController.ts (1)
1-35
: LGTM!The hook cleanly encapsulates permission checking and dialog state management. The authorization logic correctly combines organization role, admin status, and glossary ownership checks. The
canCreate
guard ensures the dialog and callback are only available to authorized users.webapp/src/ee/glossary/components/GlossaryEmptyListMessage.tsx (1)
48-113
: LGTM!The prop rename from
onCreate
toonCreateTerm
improves clarity and consistency with the broader glossary architecture. The removal ofdisplay: 'none'
from the import card (line 90) properly exposes the import functionality, aligning with the new glossary import/export feature.webapp/src/ee/glossary/hooks/useGlossaryExport.ts (1)
4-20
: LGTM!The memory leak has been properly addressed. The blob URL is now revoked after a 7-second delay in a
finally
block, and the temporary anchor element is removed from the DOM. This matches the pattern used in the existingdownloadExported
helper for project exports.webapp/src/ee/glossary/hooks/useGlossaryImportDialogController.ts (1)
9-14
: Confirm org-based gating source.canImport depends on preferredOrganization role and glossary being under that org. If users can access a glossary outside their “preferred” org, this will hide import even when they have rights on that glossary. Confirm this mirrors the create-term controller behavior and the intended UX.
Also applies to: 21-29
webapp/src/ee/glossary/components/GlossaryViewTopbar.tsx (1)
62-87
: Import/export controls wired correctly.Buttons are gated/disabled appropriately; aria-labels and data-cy are set. No issues.
Also applies to: 75-86, 88-91
webapp/src/ee/glossary/components/GlossaryTermsList.tsx (1)
80-82
: Props correctly renamed and wired throughout component hierarchy.Verification complete:
- GlossaryTermsList props (lines 80-82): ✅ onCreateTerm, onImport, onFetchNextPageHint correctly declared
- Destructuring (lines 94-96): ✅ Props extracted correctly
- GlossaryViewBody usage (line 71-73): ✅ Parent passes onCreateTerm, onImport correctly
- GlossaryEmptyListMessage forwarding (line 154): ✅ Props passed to child
- No stale onCreate prop assignments found in glossary module
ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermRepository.kt (1)
32-33
: No issues found—cascading delete is properly configured.The bulk delete operation in
deleteAllByGlossary()
is safe:
- Cascade configured:
GlossaryTerm.translations
hascascade = [CascadeType.REMOVE]
(line 25 of GlossaryTerm.kt), so deleting a term automatically cascades to all related translations.- FK constraint proper:
GlossaryTermTranslation.term
is@ManyToOne(optional = false)
, ensuring referential integrity.- Transactional: The service method is wrapped with
@Transactional
(line 228 of GlossaryTermService.kt), and the controller caller is also transactional (line 38 of GlossaryImportController.kt).No orphan rows or FK violations will occur.
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/out/GlossaryCSVExporter.kt (1)
41-48
: LGTM: writer lifecycle and UTF‑8 are handled.
use {}
closes the CSV writer and returningbyteInputStream(Charsets.UTF_8)
ensures deterministic encoding.ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestUtil.kt (2)
5-36
: LGTM! Well-structured assertion helpers.The extension functions provide a clear, fluent API for finding and asserting glossary terms by different criteria. The use of
describedAs
ensures meaningful test failure messages.
38-57
: LGTM! Clear and straightforward assertion methods.The assertion methods for description, translation, and term presence are well-implemented.
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestDataBuilder.kt (3)
22-42
: LGTM! Proper relationship management.The
addTranslation
extension correctly establishes bidirectional relationships, andwithFlags
appropriately uses nullable parameters to distinguish between "not set" (null) and "explicitly false."
69-71
: LGTM! Clean DSL entry point.
124-128
: LGTM! Good builder composition.The extension properly composes the term builder within the glossary builder DSL.
@Query( | ||
""" | ||
from GlossaryTerm te | ||
left join fetch te.translations | ||
where te.glossary = :glossary | ||
""", | ||
) | ||
fun findByGlossaryWithTranslations(glossary: Glossary): List<GlossaryTerm> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid duplicate parents with fetch-join. Use DISTINCT (and consider stable ordering).
left join fetch te.translations on a to-many can duplicate GlossaryTerm rows. Add DISTINCT to the select (and optionally order for determinism).
Apply this diff:
@Query(
"""
- from GlossaryTerm te
- left join fetch te.translations
- where te.glossary = :glossary
+ select distinct te
+ from GlossaryTerm te
+ left join fetch te.translations
+ where te.glossary = :glossary
+ order by te.id
""",
)
fun findByGlossaryWithTranslations(glossary: Glossary): List<GlossaryTerm>
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
@Query( | |
""" | |
from GlossaryTerm te | |
left join fetch te.translations | |
where te.glossary = :glossary | |
""", | |
) | |
fun findByGlossaryWithTranslations(glossary: Glossary): List<GlossaryTerm> | |
@Query( | |
""" | |
select distinct te | |
from GlossaryTerm te | |
left join fetch te.translations | |
where te.glossary = :glossary | |
order by te.id | |
""", | |
) | |
fun findByGlossaryWithTranslations(glossary: Glossary): List<GlossaryTerm> |
🤖 Prompt for AI Agents
In
ee/backend/app/src/main/kotlin/io/tolgee/ee/repository/glossary/GlossaryTermRepository.kt
around lines 86-93, the fetch-join on te.translations can produce duplicate
GlossaryTerm rows for to-many relations; update the JPQL to use SELECT DISTINCT
(e.g. "select distinct te from GlossaryTerm te ...") to de-duplicate results
and, for deterministic results, add an explicit ORDER BY (for example ORDER BY
te.id) as needed.
val reader: CSVReader by lazy { | ||
CSVReaderBuilder(input.reader()).withCSVParser( | ||
CSVParserBuilder().withSeparator(delimiter).build() | ||
).build() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix charset and close the reader.
Default platform charset can corrupt data (Windows-1252 vs UTF‑8). Wrap with InputStreamReader(input, UTF_8)
and close the reader.
- val reader: CSVReader by lazy {
- CSVReaderBuilder(input.reader()).withCSVParser(
- CSVParserBuilder().withSeparator(delimiter).build()
- ).build()
- }
+ val reader: CSVReader by lazy {
+ val parser = CSVParserBuilder().withSeparator(delimiter).build()
+ CSVReaderBuilder(java.io.InputStreamReader(input, java.nio.charset.StandardCharsets.UTF_8))
+ .withCSVParser(parser)
+ .build()
+ }
Optionally, read all rows with reader.use { it.readAll() }
in rawRows
to ensure closure.
- val rawRows: List<Array<String>> by lazy { reader.readAll() }
+ val rawRows: List<Array<String>> by lazy { reader.use { it.readAll() } }
🤖 Prompt for AI Agents
In
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt
around lines 19-23, the CSVReader is created with the platform default charset
and is never closed; change the reader creation to wrap the InputStream with an
InputStreamReader using StandardCharsets.UTF_8 (InputStreamReader(input, UTF_8))
and ensure the CSVReader is closed by using Kotlin's use/try-with-resources
pattern — e.g., replace lazy reader usage with reading logic that calls
reader.use { it.readAll() } (or otherwise closes the CSVReader) so the stream is
read with UTF-8 and properly closed.
val headers: List<String>? by lazy { | ||
rawRows.firstOrNull()?.map { it.trim() } | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Strip UTF‑8 BOM from the first header cell.
A BOM will prevent header matching. Remove it on the first header.
- val headers: List<String>? by lazy {
- rawRows.firstOrNull()?.map { it.trim() }
- }
+ val headers: List<String>? by lazy {
+ rawRows.firstOrNull()?.mapIndexed { i, s ->
+ val t = s.trim()
+ if (i == 0) t.removePrefix("\uFEFF") else t
+ }
+ }
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
val headers: List<String>? by lazy { | |
rawRows.firstOrNull()?.map { it.trim() } | |
} | |
val headers: List<String>? by lazy { | |
rawRows.firstOrNull()?.mapIndexed { i, s -> | |
val t = s.trim() | |
if (i == 0) t.removePrefix("\uFEFF") else t | |
} | |
} |
🤖 Prompt for AI Agents
In
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt
around lines 27 to 29, the headers lazy property currently trims cells but does
not remove a UTF‑8 BOM from the first header cell; update the headers
computation to strip any leading BOM (e.g. removePrefix("\uFEFF") or equivalent)
from the first element before or after trimming so header matching works
correctly when files include a BOM, while preserving null-safety and the
existing trimming behavior for all cells.
fun saveAll(translations: List<GlossaryTermTranslation>) { | ||
glossaryTermTranslationRepository.saveAll(translations) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing @Transactional
annotation on bulk operation.
The saveAll
method performs bulk persistence but lacks a @Transactional
annotation, unlike other mutation methods in this service (e.g., create
, updateOrCreate
, updateBaseLanguage
). Without a transaction boundary, partial saves may occur on failure, leading to inconsistent state.
Apply this diff to ensure atomicity:
+ @Transactional
fun saveAll(translations: List<GlossaryTermTranslation>) {
glossaryTermTranslationRepository.saveAll(translations)
}
🤖 Prompt for AI Agents
In
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryTermTranslationService.kt
around lines 111 to 113, the bulk persistence method saveAll lacks a
@Transactional annotation which can lead to partial commits on failure; add the
@Transactional annotation to the saveAll method (and the corresponding import if
missing) so the entire bulk save runs within a single transaction, ensuring
atomicity and consistency.
@Test | ||
fun `parses CSV with all fields`() { | ||
val csvContent = """ | ||
term,description,translatable,casesensitive,abbreviation,forbidden,en,cs,de | ||
Apple,A fruit,true,false,false,false,Apple,Jablko,Apfel | ||
API,Application Programming Interface,false,true,true,false,API,API,API | ||
BadWord,Forbidden term,true,false,false,true,BadWord,SpatneSlovo,SchlechteWort | ||
""".trimIndent() | ||
|
||
val terms = parseCSV(csvContent) | ||
|
||
terms.assertSize(3) | ||
|
||
terms.assertTerm("Apple") { | ||
hasDescription("A fruit") | ||
hasTranslation("en", "Apple") | ||
hasTranslation("cs", "Jablko") | ||
hasTranslation("de", "Apfel") | ||
isTranslatable() | ||
isNotCaseSensitive() | ||
isNotAbbreviation() | ||
isNotForbidden() | ||
} | ||
|
||
terms.assertTerm("API") { | ||
hasDescription("Application Programming Interface") | ||
hasTranslation("en", "API") | ||
hasTranslation("cs", "API") | ||
hasTranslation("de", "API") | ||
isNotTranslatable() | ||
isCaseSensitive() | ||
isAbbreviation() | ||
isNotForbidden() | ||
} | ||
|
||
terms.assertTerm("BadWord") { | ||
hasDescription("Forbidden term") | ||
hasTranslation("en", "BadWord") | ||
hasTranslation("cs", "SpatneSlovo") | ||
hasTranslation("de", "SchlechteWort") | ||
isTranslatable() | ||
isNotCaseSensitive() | ||
isNotAbbreviation() | ||
isForbidden() | ||
} | ||
} | ||
|
||
@Test | ||
fun `parses CSV with minimal data`() { | ||
val csvContent = """ | ||
term,en | ||
MinimalTerm,Minimal | ||
""".trimIndent() | ||
|
||
val terms = parseCSV(csvContent) | ||
|
||
terms.assertSize(1) | ||
terms.assertTerm("MinimalTerm") { | ||
hasNoDescription() | ||
hasTranslation("en", "Minimal") | ||
hasNoFlags() | ||
} | ||
} | ||
|
||
@Test | ||
fun `parses CSV with only translations`() { | ||
val csvContent = """ | ||
en,cs,de | ||
Hello,Ahoj,Hallo | ||
World,Svět,Welt | ||
""".trimIndent() | ||
|
||
val terms = parseCSV(csvContent) | ||
|
||
terms.assertSize(2) | ||
terms.assertTermWithTranslation("en", "Hello") { | ||
hasNoTerm() | ||
hasNoDescription() | ||
hasTranslation("en", "Hello") | ||
hasTranslation("cs", "Ahoj") | ||
hasTranslation("de", "Hallo") | ||
hasNoFlags() | ||
} | ||
|
||
terms.assertTermWithTranslation("en", "World") { | ||
hasNoTerm() | ||
hasNoDescription() | ||
hasTranslation("en", "World") | ||
hasTranslation("cs", "Svět") | ||
hasTranslation("de", "Welt") | ||
hasNoFlags() | ||
} | ||
} | ||
|
||
@Test | ||
fun `parses CSV with semicolon delimiter`() { | ||
val csvContent = """ | ||
term;description;en;cs | ||
TestTerm;Test description;TestValue;TestHodnota | ||
""".trimIndent() | ||
|
||
val terms = parseCSV(csvContent, ';') | ||
|
||
terms.assertSize(1) | ||
terms.assertTerm("TestTerm") { | ||
hasDescription("Test description") | ||
hasTranslation("en", "TestValue") | ||
hasTranslation("cs", "TestHodnota") | ||
} | ||
} | ||
|
||
@Test | ||
fun `parses boolean flag variations`() { | ||
val csvContent = """ | ||
term,translatable,casesensitive,abbreviation,forbidden,en | ||
Term1,true,1,yes,t,Value1 | ||
Term2,false,0,no,f,Value2 | ||
Term3,TRUE,Y,T,FALSE,Value3 | ||
Term4,invalid,n,false,gibberish,Value4 | ||
""".trimIndent() | ||
|
||
val terms = parseCSV(csvContent) | ||
|
||
terms.assertSize(4) | ||
|
||
terms.assertTerm("Term1") { | ||
isTranslatable() | ||
isCaseSensitive() | ||
isAbbreviation() | ||
isForbidden() | ||
} | ||
|
||
terms.assertTerm("Term2") { | ||
isNotTranslatable() | ||
isNotCaseSensitive() | ||
isNotAbbreviation() | ||
isNotForbidden() | ||
} | ||
|
||
terms.assertTerm("Term3") { | ||
isTranslatable() | ||
isCaseSensitive() | ||
isAbbreviation() | ||
isNotForbidden() | ||
} | ||
|
||
terms.assertTerm("Term4") { | ||
isNotTranslatable() // invalid defaults to false | ||
isNotCaseSensitive() | ||
isNotForbidden() // invalid defaults to false | ||
isNotAbbreviation() | ||
} | ||
} | ||
|
||
@Test | ||
fun `skips empty rows`() { | ||
val csvContent = """ | ||
term,en | ||
ValidTerm,ValidValue | ||
,, | ||
AnotherTerm,AnotherValue | ||
, | ||
""".trimIndent() | ||
|
||
val terms = parseCSV(csvContent) | ||
|
||
terms.assertSize(2) | ||
terms.assertTerm("ValidTerm") { | ||
hasTranslation("en", "ValidValue") | ||
} | ||
terms.assertTerm("AnotherTerm") { | ||
hasTranslation("en", "AnotherValue") | ||
} | ||
} | ||
|
||
@Test | ||
fun `handles case insensitive headers`() { | ||
val csvContent = """ | ||
TERM,DESCRIPTION,TRANSLATABLE,CASESENSITIVE,ABBREVIATION,FORBIDDEN,EN | ||
TestTerm,Test Desc,true,false,false,false,TestValue | ||
""".trimIndent() | ||
|
||
val terms = parseCSV(csvContent) | ||
|
||
terms.assertSize(1) | ||
terms.assertTerm("TestTerm") { | ||
hasDescription("Test Desc") | ||
hasTranslation("EN", "TestValue") | ||
isTranslatable() | ||
isNotCaseSensitive() | ||
isNotAbbreviation() | ||
isNotForbidden() | ||
} | ||
} | ||
|
||
@Test | ||
fun `handles mixed header spacing`() { | ||
val csvContent = """ | ||
term , description, en ,cs | ||
TestTerm,Test description,TestValue,TestHodnota | ||
""".trimIndent() | ||
|
||
val terms = parseCSV(csvContent) | ||
|
||
terms.assertSize(1) | ||
terms.assertTerm("TestTerm") { | ||
hasDescription("Test description") | ||
hasTranslation("en", "TestValue") | ||
hasTranslation("cs", "TestHodnota") | ||
} | ||
} | ||
|
||
@Test | ||
fun `returns empty list for empty CSV`() { | ||
val csvContent = """ | ||
term,en | ||
""".trimIndent() | ||
|
||
val terms = parseCSV(csvContent) | ||
terms.assertSize(0) | ||
} | ||
|
||
@Test | ||
fun `returns empty list for headers only`() { | ||
val csvContent = "term,description,en,cs" | ||
|
||
val terms = parseCSV(csvContent) | ||
terms.assertSize(0) | ||
} | ||
|
||
@Test | ||
fun `handles empty translation values`() { | ||
val csvContent = """ | ||
term,en,cs,de | ||
TestTerm,TestValue,, | ||
AnotherTerm,,AnotherValue, | ||
EmptyTerm,,, | ||
""".trimIndent() | ||
|
||
val terms = parseCSV(csvContent) | ||
|
||
terms.assertSize(3) | ||
|
||
terms.assertTerm("TestTerm") { | ||
hasTranslation("en", "TestValue") | ||
hasNoTranslation("cs") | ||
hasNoTranslation("de") | ||
} | ||
|
||
terms.assertTerm("AnotherTerm") { | ||
hasNoTranslation("en") | ||
hasTranslation("cs", "AnotherValue") | ||
hasNoTranslation("de") | ||
} | ||
|
||
terms.assertTerm("EmptyTerm") { | ||
hasNoTranslation("en") | ||
hasNoTranslation("cs") | ||
hasNoTranslation("de") | ||
} | ||
} | ||
|
||
@Test | ||
fun `handles term with description only`() { | ||
val csvContent = """ | ||
description,en,cs | ||
Just a description,, | ||
Another description,Value, | ||
""".trimIndent() | ||
|
||
val terms = parseCSV(csvContent) | ||
|
||
terms.assertSize(2) | ||
|
||
terms.assertTermWithDescription("Just a description") { | ||
hasNoTerm() | ||
hasDescription("Just a description") | ||
hasNoTranslation("en") | ||
hasNoTranslation("cs") | ||
} | ||
|
||
terms.assertTermWithDescription("Another description") { | ||
hasNoTerm() | ||
hasDescription("Another description") | ||
hasTranslation("en", "Value") | ||
hasNoTranslation("cs") | ||
} | ||
} | ||
|
||
private fun parseCSV(csvContent: String, delimiter: Char = ','): List<ImportGlossaryTerm> { | ||
return GlossaryCSVParser(ByteArrayInputStream(csvContent.toByteArray()), delimiter).parse() | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Great coverage; add a few edge‑case scenarios.
- Quoted fields with delimiters/newlines, Windows CRLF, UTF‑8 BOM handling.
- Very long fields and trailing/leading spaces in cell values.
Example additions:
- '"API, v2"' in a term/translation
- Headers/rows with CRLF
- File starting with BOM
Also, the “case insensitive headers” test asserts EN (uppercase). Confirm whether parser preserves header case or normalizes to canonical tags (lowercase). Align with import service to avoid mixed‑case language tags in persistence.
Locate normalization (if any):
🏁 Script executed:
#!/bin/bash
rg -nP -C2 'languageTag.*(lowercase|toLowerCase|Locale\.|normalize)' ee/backend/app/src/main/kotlin
rg -nP -C2 'ImportGlossaryTerm|GlossaryCSVParser' ee/backend/app/src/main/kotlin
Length of output: 6822
🏁 Script executed:
#!/bin/bash
# Inspect GlossaryCSVParser full implementation, focus on header parsing and translation map construction
cat ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParser.kt
Length of output: 3437
🏁 Script executed:
#!/bin/bash
# Check GlossaryImportService.applyFrom to see if normalization happens during import
rg -nP -A20 'private fun GlossaryTerm.applyFrom' ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt
Length of output: 930
🏁 Script executed:
#!/bin/bash
# Inspect ImportGlossaryTerm data class definition
cat ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/formats/ImportGlossaryTerm.kt
Length of output: 324
🏁 Script executed:
#!/bin/bash
# Check test helper methods to understand what hasTranslation asserts
rg -nP -B5 -A10 'fun hasTranslation|fun hasNoTranslation' ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParserTest.kt
Length of output: 0
🏁 Script executed:
#!/bin/bash
# Search for any language tag normalization in glossary-related code
rg -nP 'translations\[.*\]|\.get\(.*language' ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary --type=kotlin
Length of output: 187
🏁 Script executed:
#!/bin/bash
# Find the test class definition and helper methods
rg -nP 'class GlossaryCSVParserTest|fun.*assertTerm|fun.*hasTranslation' ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/ -B2 -A5
Length of output: 1064
🏁 Script executed:
#!/bin/bash
# Look for assertion helper in parent class or test utilities
fd -e kt -path '*/test*' -name '*.kt' | xargs rg -l 'assertTerm|hasTranslation' | head -5
Length of output: 362
🏁 Script executed:
#!/bin/bash
# Check how GlossaryImportService iterates/stores translations from ImportGlossaryTerm
rg -nP -A30 'glossaryTerm.translations' ee/backend/app/src/main/kotlin/io/tolgee/ee/service/glossary/GlossaryImportService.kt
Length of output: 1311
🏁 Script executed:
#!/bin/bash
# Inspect the test utility
cat ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/GlossaryTestUtil.kt
Length of output: 2767
Add edge-case scenarios and fix language tag case handling.
The current tests cover basic scenarios well, but miss important edge cases:
- Quoted fields containing delimiters or newlines, Windows CRLF, UTF-8 BOM handling
- Very long fields and leading/trailing spaces in cell values
Critical issue with language tags: The parser preserves header case (e.g., "EN", "En") in the translations map keys, and the import service stores these as-is into GlossaryTermTranslation
. However, the query layer uses lowercase normalized tags via Locale.forLanguageTag()
. This mismatch means uppercase or mixed-case language headers in the CSV will result in language tags that fail to match during lookups.
The test "handles case insensitive headers" is misleading—it verifies case-insensitive header matching in the parser, but does not normalize the resulting language tag keys. The assertion hasTranslation("EN", ...)
passes only because the test directly inspects the ImportGlossaryTerm
before it reaches the import service.
Recommended fixes:
- Normalize language tag headers to lowercase in the parser (line ~65) when building the translations map, or
- Apply normalization in
GlossaryImportService.applyFrom()
before storing. - Add tests for the full import flow to catch case mismatches between storage and query layers.
🤖 Prompt for AI Agents
In
ee/backend/tests/src/test/kotlin/io/tolgee/ee/service/glossary/formats/csv/in/GlossaryCSVParserTest.kt
around lines 9-302, the review points out that language header case is preserved
in the parser which later causes mismatches with lowercased query tags; also
tests miss edge cases like quoted fields with delimiters/newlines, CRLF, UTF-8
BOM, very long fields and leading/trailing spaces. Fix by normalizing language
header keys to a canonical form (e.g., lowercased Locale.forLanguageTag() or
.lowercase()) when building the translations map in the parser (around line ~65)
or alternatively normalize in GlossaryImportService.applyFrom() before
persisting, and add new tests in this file that validate: headers with
mixed/upper case produce normalized tags that survive the full import flow,
quoted fields with delimiters/newlines, CRLF and BOM handling, trimming of cell
values, and a test with very long field values to ensure no truncation.
class GlossaryTermBuilder { | ||
private var description: String? = null | ||
private val translations = mutableListOf<Pair<String, String>>() | ||
private var flagNonTranslatable: Boolean = false | ||
private var flagCaseSensitive: Boolean = false | ||
private var flagAbbreviation: Boolean = false | ||
private var flagForbiddenTerm: Boolean = false | ||
|
||
fun withDescription(description: String) = apply { | ||
this.description = description | ||
} | ||
|
||
fun withTranslation(languageTag: String, text: String) = apply { | ||
translations.add(languageTag to text) | ||
} | ||
|
||
fun withTranslations(vararg translations: Pair<String, String>) = apply { | ||
this.translations.addAll(translations) | ||
} | ||
|
||
fun translatable(value: Boolean = true) = apply { | ||
flagNonTranslatable = !value | ||
} | ||
|
||
fun caseSensitive(value: Boolean = true) = apply { | ||
flagCaseSensitive = value | ||
} | ||
|
||
fun abbreviation(value: Boolean = true) = apply { | ||
flagAbbreviation = value | ||
} | ||
|
||
fun forbiddenTerm(value: Boolean = true) = apply { | ||
flagForbiddenTerm = value | ||
} | ||
|
||
fun build(): GlossaryTerm { | ||
return createTerm(description).let { | ||
translations.forEach { (languageTag, text) -> | ||
it.addTranslation(languageTag, text) | ||
} | ||
it.withFlags( | ||
nonTranslatable = flagNonTranslatable, | ||
caseSensitive = flagCaseSensitive, | ||
abbreviation = flagAbbreviation, | ||
forbiddenTerm = flagForbiddenTerm, | ||
) | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix flag semantics: use nullable Booleans to support "unset" state.
The builder initializes all flags to false
(lines 76-79), which means flags are always explicitly set when build()
is called (lines 114-119). This prevents creating terms with unset (null) flags, contradicting the nullable semantics of withFlags()
.
Apply this diff to use nullable Booleans:
class GlossaryTermBuilder {
private var description: String? = null
private val translations = mutableListOf<Pair<String, String>>()
- private var flagNonTranslatable: Boolean = false
- private var flagCaseSensitive: Boolean = false
- private var flagAbbreviation: Boolean = false
- private var flagForbiddenTerm: Boolean = false
+ private var flagNonTranslatable: Boolean? = null
+ private var flagCaseSensitive: Boolean? = null
+ private var flagAbbreviation: Boolean? = null
+ private var flagForbiddenTerm: Boolean? = null
This allows the builder to distinguish between "explicitly set to false" and "not set at all."
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
class GlossaryTermBuilder { | |
private var description: String? = null | |
private val translations = mutableListOf<Pair<String, String>>() | |
private var flagNonTranslatable: Boolean = false | |
private var flagCaseSensitive: Boolean = false | |
private var flagAbbreviation: Boolean = false | |
private var flagForbiddenTerm: Boolean = false | |
fun withDescription(description: String) = apply { | |
this.description = description | |
} | |
fun withTranslation(languageTag: String, text: String) = apply { | |
translations.add(languageTag to text) | |
} | |
fun withTranslations(vararg translations: Pair<String, String>) = apply { | |
this.translations.addAll(translations) | |
} | |
fun translatable(value: Boolean = true) = apply { | |
flagNonTranslatable = !value | |
} | |
fun caseSensitive(value: Boolean = true) = apply { | |
flagCaseSensitive = value | |
} | |
fun abbreviation(value: Boolean = true) = apply { | |
flagAbbreviation = value | |
} | |
fun forbiddenTerm(value: Boolean = true) = apply { | |
flagForbiddenTerm = value | |
} | |
fun build(): GlossaryTerm { | |
return createTerm(description).let { | |
translations.forEach { (languageTag, text) -> | |
it.addTranslation(languageTag, text) | |
} | |
it.withFlags( | |
nonTranslatable = flagNonTranslatable, | |
caseSensitive = flagCaseSensitive, | |
abbreviation = flagAbbreviation, | |
forbiddenTerm = flagForbiddenTerm, | |
) | |
} | |
} | |
} | |
class GlossaryTermBuilder { | |
private var description: String? = null | |
private val translations = mutableListOf<Pair<String, String>>() | |
private var flagNonTranslatable: Boolean? = null | |
private var flagCaseSensitive: Boolean? = null | |
private var flagAbbreviation: Boolean? = null | |
private var flagForbiddenTerm: Boolean? = null | |
fun withDescription(description: String) = apply { | |
this.description = description | |
} | |
fun withTranslation(languageTag: String, text: String) = apply { | |
translations.add(languageTag to text) | |
} | |
fun withTranslations(vararg translations: Pair<String, String>) = apply { | |
this.translations.addAll(translations) | |
} | |
fun translatable(value: Boolean = true) = apply { | |
flagNonTranslatable = !value | |
} | |
fun caseSensitive(value: Boolean = true) = apply { | |
flagCaseSensitive = value | |
} | |
fun abbreviation(value: Boolean = true) = apply { | |
flagAbbreviation = value | |
} | |
fun forbiddenTerm(value: Boolean = true) = apply { | |
flagForbiddenTerm = value | |
} | |
fun build(): GlossaryTerm { | |
return createTerm(description).let { | |
translations.forEach { (languageTag, text) -> | |
it.addTranslation(languageTag, text) | |
} | |
it.withFlags( | |
nonTranslatable = flagNonTranslatable, | |
caseSensitive = flagCaseSensitive, | |
abbreviation = flagAbbreviation, | |
forbiddenTerm = flagForbiddenTerm, | |
) | |
} | |
} | |
} |
if (data.type === 'application/zip') { | ||
a.download = projectName + dateStr + '.zip'; | ||
} else { | ||
const extension = parseExtension(response) || format.extension; | ||
a.download = | ||
projectName + onlyPossibleLanguageString + dateStr + '.' + extension; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unify extension detection and zip handling (header or blob type).
Relying solely on data.type === 'application/zip' misses 'octet‑stream' or other zip CTs; header may still be .zip. Also avoids wrong fallback (e.g., naming a .zip as .json).
- if (data.type === 'application/zip') {
- a.download = projectName + dateStr + '.zip';
- } else {
- const extension = parseExtension(response) || format.extension;
- a.download =
- projectName + onlyPossibleLanguageString + dateStr + '.' + extension;
- }
+ const headerExt = parseExtension(response);
+ const isZip =
+ (!!data.type && data.type.includes('zip')) || headerExt === 'zip';
+ const ext = isZip ? 'zip' : (headerExt || format.extension);
+ const languagePart = isZip ? '' : onlyPossibleLanguageString;
+ a.download = `${projectName}${languagePart}${dateStr}.${ext}`;
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
if (data.type === 'application/zip') { | |
a.download = projectName + dateStr + '.zip'; | |
} else { | |
const extension = parseExtension(response) || format.extension; | |
a.download = | |
projectName + onlyPossibleLanguageString + dateStr + '.' + extension; | |
} | |
const headerExt = parseExtension(response); | |
const isZip = | |
(!!data.type && data.type.includes('zip')) || headerExt === 'zip'; | |
const ext = isZip ? 'zip' : (headerExt || format.extension); | |
const languagePart = isZip ? '' : onlyPossibleLanguageString; | |
a.download = `${projectName}${languagePart}${dateStr}.${ext}`; |
🤖 Prompt for AI Agents
In webapp/src/views/projects/export/downloadExported.ts around lines 18 to 24,
the code only treats blobs with data.type === 'application/zip' as zip files
which misses cases like application/octet-stream or other CTs where the response
headers or content-disposition filename indicate a .zip; change the logic to
first try to extract an extension from the response (content-disposition
filename if present, then response.headers['content-type'] mapping to known
extensions), then treat the file as zip if either the resolved extension ===
'zip' or the blob type explicitly contains 'zip'; otherwise fall back to
parseExtension(response) || format.extension; set a.download using the resolved
extension so .zip files are always named .zip even when the content-type is
generic.
Closes #3103
Summary by CodeRabbit
New Features
Bug Fixes
Tests