From 6dfa4f57a894ce4b13eee5079265205d73f54fc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Gran=C3=A1t?= Date: Mon, 10 Mar 2025 15:09:07 +0100 Subject: [PATCH 1/7] feat: sort menu under icon --- .../translations/KeyWithTranslationsModel.kt | 2 + .../KeyWithTranslationsModelAssembler.kt | 1 + .../model/views/KeyWithTranslationsView.kt | 3 ++ .../translationViewBuilder/QueryBase.kt | 4 ++ webapp/src/component/CustomIcons.tsx | 1 + .../translationOrder/TranslationOrderMenu.tsx | 43 +++++++++++++++++++ .../translationOrder/useOrderOptions.tsx | 14 ++++++ webapp/src/svgs/icons/sort.svg | 3 ++ .../TranslationHeader/TranslationControls.tsx | 40 ++++++++++++++++- .../TranslationControlsCompact.tsx | 32 +++++++++++++- .../context/TranslationsContext.ts | 7 +++ .../services/useTranslationsService.tsx | 7 ++- 12 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 webapp/src/component/translation/translationOrder/TranslationOrderMenu.tsx create mode 100644 webapp/src/component/translation/translationOrder/useOrderOptions.tsx create mode 100644 webapp/src/svgs/icons/sort.svg diff --git a/backend/api/src/main/kotlin/io/tolgee/hateoas/translations/KeyWithTranslationsModel.kt b/backend/api/src/main/kotlin/io/tolgee/hateoas/translations/KeyWithTranslationsModel.kt index de28e152d9..e6b4c10d22 100644 --- a/backend/api/src/main/kotlin/io/tolgee/hateoas/translations/KeyWithTranslationsModel.kt +++ b/backend/api/src/main/kotlin/io/tolgee/hateoas/translations/KeyWithTranslationsModel.kt @@ -9,6 +9,8 @@ import org.springframework.hateoas.server.core.Relation @Suppress("unused") @Relation(collectionRelation = "keys", itemRelation = "key") open class KeyWithTranslationsModel( + @Schema(description = "The time when the key was created") + val createdAt: Long, @Schema(description = "Id of key record") val keyId: Long, @Schema(description = "Name of key", example = "this_is_super_key") diff --git a/backend/api/src/main/kotlin/io/tolgee/hateoas/translations/KeyWithTranslationsModelAssembler.kt b/backend/api/src/main/kotlin/io/tolgee/hateoas/translations/KeyWithTranslationsModelAssembler.kt index 8d16645371..c30b86e81d 100644 --- a/backend/api/src/main/kotlin/io/tolgee/hateoas/translations/KeyWithTranslationsModelAssembler.kt +++ b/backend/api/src/main/kotlin/io/tolgee/hateoas/translations/KeyWithTranslationsModelAssembler.kt @@ -20,6 +20,7 @@ class KeyWithTranslationsModelAssembler( override fun toModel(view: KeyWithTranslationsView) = KeyWithTranslationsModel( keyId = view.keyId, + createdAt = view.createdAt.time, keyName = view.keyName, keyNamespaceId = view.keyNamespaceId, keyIsPlural = view.keyIsPlural, diff --git a/backend/data/src/main/kotlin/io/tolgee/model/views/KeyWithTranslationsView.kt b/backend/data/src/main/kotlin/io/tolgee/model/views/KeyWithTranslationsView.kt index e0a4041a98..57e77b33d6 100644 --- a/backend/data/src/main/kotlin/io/tolgee/model/views/KeyWithTranslationsView.kt +++ b/backend/data/src/main/kotlin/io/tolgee/model/views/KeyWithTranslationsView.kt @@ -6,9 +6,11 @@ import io.tolgee.model.Screenshot import io.tolgee.model.enums.TranslationState import io.tolgee.model.key.Tag import io.tolgee.service.queryBuilders.Cursorable +import java.sql.Timestamp data class KeyWithTranslationsView( val keyId: Long, + val createdAt: Timestamp, val keyName: String, val keyIsPlural: Boolean, val keyPluralArgName: String?, @@ -32,6 +34,7 @@ data class KeyWithTranslationsView( val result = KeyWithTranslationsView( keyId = data.removeFirst() as Long, + createdAt = data.removeFirst() as Timestamp, keyName = data.removeFirst() as String, keyIsPlural = data.removeFirst() as Boolean, keyPluralArgName = data.removeFirst() as String?, diff --git a/backend/data/src/main/kotlin/io/tolgee/service/queryBuilders/translationViewBuilder/QueryBase.kt b/backend/data/src/main/kotlin/io/tolgee/service/queryBuilders/translationViewBuilder/QueryBase.kt index a8e8b4b479..7f27bc9623 100644 --- a/backend/data/src/main/kotlin/io/tolgee/service/queryBuilders/translationViewBuilder/QueryBase.kt +++ b/backend/data/src/main/kotlin/io/tolgee/service/queryBuilders/translationViewBuilder/QueryBase.kt @@ -31,6 +31,8 @@ import jakarta.persistence.criteria.Path import jakarta.persistence.criteria.Predicate import jakarta.persistence.criteria.Root import jakarta.persistence.criteria.Subquery +import java.util.* +import kotlin.collections.HashSet class QueryBase( private val cb: CriteriaBuilder, @@ -45,6 +47,7 @@ class QueryBase( val whereConditions: MutableSet = HashSet() val root: Root = query.from(Key::class.java) val keyNameExpression: Path = root.get(Key_.name) + val keyCreatedAtExpression: Path = root.get(Key_.createdAt) val keyIsPluralExpression: Path = root.get(Key_.isPlural) val keyArgNameExpression: Path = root.get(Key_.pluralArgName) val keyIdExpression: Path = root.get(Key_.id) @@ -59,6 +62,7 @@ class QueryBase( init { querySelection[KeyWithTranslationsView::keyId.name] = keyIdExpression + querySelection[KeyWithTranslationsView::createdAt.name] = keyCreatedAtExpression querySelection[KeyWithTranslationsView::keyName.name] = keyNameExpression querySelection[KeyWithTranslationsView::keyIsPlural.name] = keyIsPluralExpression querySelection[KeyWithTranslationsView::keyPluralArgName.name] = keyArgNameExpression diff --git a/webapp/src/component/CustomIcons.tsx b/webapp/src/component/CustomIcons.tsx index 43aac6eb1c..389accbc18 100644 --- a/webapp/src/component/CustomIcons.tsx +++ b/webapp/src/component/CustomIcons.tsx @@ -14,3 +14,4 @@ export { default as ArrowDropDown } from '../svgs/icons/arrow-drop-down.svg?reac export { default as ArrowRight } from '../svgs/icons/arrow-right.svg?react'; export { default as CheckBoxOutlineBlank } from '../svgs/icons/check-box-outline-blank.svg?react'; export { default as Google } from '../svgs/icons/google.svg?react'; +export { default as Sort } from '../svgs/icons/sort.svg?react'; diff --git a/webapp/src/component/translation/translationOrder/TranslationOrderMenu.tsx b/webapp/src/component/translation/translationOrder/TranslationOrderMenu.tsx new file mode 100644 index 0000000000..297f80323b --- /dev/null +++ b/webapp/src/component/translation/translationOrder/TranslationOrderMenu.tsx @@ -0,0 +1,43 @@ +import { Menu, MenuItem, MenuProps } from '@mui/material'; +import { useOrderOptions } from './useOrderOptions'; + +type Props = { + value: string; + onChange: (value: string) => void; + anchorEl: MenuProps['anchorEl']; + onClose: () => void; +}; + +export const TranslationOrderMenu = ({ + value, + onChange, + anchorEl, + onClose, +}: Props) => { + const options = useOrderOptions(); + return ( + + {options.map((o) => ( + { + onChange(o.value); + onClose(); + }} + selected={o.value === value} + > + {o.label} + + ))} + + ); +}; diff --git a/webapp/src/component/translation/translationOrder/useOrderOptions.tsx b/webapp/src/component/translation/translationOrder/useOrderOptions.tsx new file mode 100644 index 0000000000..d3c26f2bcc --- /dev/null +++ b/webapp/src/component/translation/translationOrder/useOrderOptions.tsx @@ -0,0 +1,14 @@ +import { useTranslate } from '@tolgee/react'; + +export const useOrderOptions = () => { + const { t } = useTranslate(); + return [ + { value: 'keyName', label: t('translation_order_item_key_name_a_to_z') }, + { + value: 'keyName,desc', + label: t('translation_order_item_key_name_z_to_a'), + }, + { value: 'createdAt', label: t('translation_order_item_first_added') }, + { value: 'createdAt,desc', label: t('translation_order_item_last_added') }, + ]; +}; diff --git a/webapp/src/svgs/icons/sort.svg b/webapp/src/svgs/icons/sort.svg new file mode 100644 index 0000000000..a6d2ec02ad --- /dev/null +++ b/webapp/src/svgs/icons/sort.svg @@ -0,0 +1,3 @@ + + + diff --git a/webapp/src/views/projects/translations/TranslationHeader/TranslationControls.tsx b/webapp/src/views/projects/translations/TranslationHeader/TranslationControls.tsx index ff3b446087..e6454e81e6 100644 --- a/webapp/src/views/projects/translations/TranslationHeader/TranslationControls.tsx +++ b/webapp/src/views/projects/translations/TranslationHeader/TranslationControls.tsx @@ -1,5 +1,13 @@ import { LayoutGrid02, LayoutLeft, Plus } from '@untitled-ui/icons-react'; -import { Box, Button, ButtonGroup, styled } from '@mui/material'; +import { + Badge, + Box, + Button, + ButtonGroup, + IconButton, + styled, + Tooltip, +} from '@mui/material'; import { T, useTranslate } from '@tolgee/react'; import { LanguagesSelect } from 'tg.component/common/form/LanguagesSelect/LanguagesSelect'; @@ -7,6 +15,7 @@ import { useProjectPermissions } from 'tg.hooks/useProjectPermissions'; import { TranslationFilters } from 'tg.component/translation/translationFilters/TranslationFilters'; import { QuickStartHighlight } from 'tg.component/layout/QuickStartGuide/QuickStartHighlight'; import { HeaderSearchField } from 'tg.component/layout/HeaderSearchField'; +import { TranslationOrderMenu } from 'tg.component/translation/translationOrder/TranslationOrderMenu'; import { PrefilterTaskShowDoneSwitch } from 'tg.ee'; import { @@ -14,6 +23,8 @@ import { useTranslationsSelector, } from '../context/TranslationsContext'; import { StickyHeader } from './StickyHeader'; +import { useState } from 'react'; +import { Sort } from 'tg.component/CustomIcons'; const StyledContainer = styled('div')` display: grid; @@ -47,12 +58,17 @@ export const TranslationControls: React.FC = ({ onDialogOpen }) => { const search = useTranslationsSelector((v) => v.search); const languages = useTranslationsSelector((v) => v.languages); const { t } = useTranslate(); + const [anchorOrderEl, setAnchorOrderEl] = useState( + null + ); - const { setSearch, selectLanguages, changeView } = useTranslationsActions(); + const { setSearch, selectLanguages, changeView, setOrder } = + useTranslationsActions(); const view = useTranslationsSelector((v) => v.view); const selectedLanguages = useTranslationsSelector((c) => c.selectedLanguages); const allLanguages = useTranslationsSelector((c) => c.languages); const filters = useTranslationsSelector((c) => c.filters); + const order = useTranslationsSelector((c) => c.order); const { setFilters } = useTranslationsActions(); const selectedLanguagesMapped = allLanguages?.filter((l) => selectedLanguages?.includes(l.tag)) ?? []; @@ -80,6 +96,26 @@ export const TranslationControls: React.FC = ({ onDialogOpen }) => { value={filters} onChange={setFilters} /> + + + + setAnchorOrderEl(e.currentTarget)}> + + + + + + setAnchorOrderEl(null)} + onChange={setOrder} + value={order} + /> diff --git a/webapp/src/views/projects/translations/TranslationHeader/TranslationControlsCompact.tsx b/webapp/src/views/projects/translations/TranslationHeader/TranslationControlsCompact.tsx index fe677a1711..37974677fa 100644 --- a/webapp/src/views/projects/translations/TranslationHeader/TranslationControlsCompact.tsx +++ b/webapp/src/views/projects/translations/TranslationHeader/TranslationControlsCompact.tsx @@ -15,6 +15,7 @@ import { ButtonGroup, IconButton, styled, + Tooltip, } from '@mui/material'; import { useTranslate } from '@tolgee/react'; @@ -33,6 +34,8 @@ import { } from '../context/TranslationsContext'; import { ViewMode } from '../context/types'; import { StickyHeader } from './StickyHeader'; +import { TranslationOrderMenu } from 'tg.component/translation/translationOrder/TranslationOrderMenu'; +import { Sort } from 'tg.component/CustomIcons'; const StyledContainer = styled('div')` display: grid; @@ -98,15 +101,20 @@ export const TranslationControlsCompact: React.FC = ({ const [searchOpen, setSearchOpen] = useState(false); const search = useTranslationsSelector((v) => v.search); const languages = useTranslationsSelector((v) => v.languages); + const order = useTranslationsSelector((v) => v.order); const { t } = useTranslate(); - const { setSearch, changeView, selectLanguages } = useTranslationsActions(); + const { setSearch, changeView, selectLanguages, setOrder } = + useTranslationsActions(); const view = useTranslationsSelector((v) => v.view); const selectedLanguages = useTranslationsSelector((c) => c.selectedLanguages); const [anchorFiltersEl, setAnchorFiltersEl] = useState(null); const [anchorLanguagesEl, setAnchorLanguagesEl] = useState(null); + const [anchorOrderEl, setAnchorOrderEl] = useState( + null + ); const handleSearchChange = (value: string) => { setSearch(value); @@ -189,6 +197,28 @@ export const TranslationControlsCompact: React.FC = ({ filtersContent={filtersContent} onChange={setFilters} /> + + + setAnchorOrderEl(e.currentTarget)} + > + + + + + + setAnchorOrderEl(null)} + onChange={setOrder} + value={order} + /> diff --git a/webapp/src/views/projects/translations/context/TranslationsContext.ts b/webapp/src/views/projects/translations/context/TranslationsContext.ts index e384a27a1f..c27bd09e43 100644 --- a/webapp/src/views/projects/translations/context/TranslationsContext.ts +++ b/webapp/src/views/projects/translations/context/TranslationsContext.ts @@ -166,6 +166,12 @@ export const [ return handleTranslationsReset(); } }, + async setOrder(value: string) { + if (await positionService.confirmUnsavedChanges()) { + translationService.setOrder(value); + return handleTranslationsReset(); + } + }, async setEdit(edit: EditorProps | undefined) { if (await positionService.confirmUnsavedChanges(edit)) { setSidePanelOpen(true); @@ -297,6 +303,7 @@ export const [ search: translationService.search as string, urlSearch: translationService.urlSearch, filters: translationService.filters, + order: translationService.order, cursor: positionService.position, selection: selectionService.data, view: view as ViewMode, diff --git a/webapp/src/views/projects/translations/context/services/useTranslationsService.tsx b/webapp/src/views/projects/translations/context/services/useTranslationsService.tsx index 061b3a1853..5d137cbdca 100644 --- a/webapp/src/views/projects/translations/context/services/useTranslationsService.tsx +++ b/webapp/src/views/projects/translations/context/services/useTranslationsService.tsx @@ -84,6 +84,9 @@ export const useTranslationsService = (props: Props) => { const [filters, _setFilters] = useUrlSearchState('filters', { defaultVal: JSON.stringify({}), }); + const [order, setOrder] = useUrlSearchState('order', { + defaultVal: 'keyName', + }); const parsedFilters = useMemo( () => (filters ? JSON.parse(filters as string) : {}) as FiltersType, [filters] @@ -106,7 +109,6 @@ export const useTranslationsService = (props: Props) => { const [query, setQuery] = useState>({ size: props.pageSize || PAGE_SIZE, - sort: ['keyNamespace', 'keyName'], languages: props.initialLangs || [], }); @@ -150,6 +152,7 @@ export const useTranslationsService = (props: Props) => { filterTaskNumber: props.prefilter?.task !== undefined ? [props.prefilter.task] : undefined, filterTaskKeysNotDone: props.prefilter?.taskFilterNotDone || undefined, + sort: ['keyNamespace', order], }; const translations = useApiInfiniteQuery({ @@ -398,6 +401,7 @@ export const useTranslationsService = (props: Props) => { hasNextPage: translations.hasNextPage, query, filters: parsedFilters, + order, fetchNextPage: translations.fetchNextPage, selectedLanguages, translationsLanguages, @@ -413,6 +417,7 @@ export const useTranslationsService = (props: Props) => { setLanguages, setUrlSearch, setFilters, + setOrder, updateTranslationKeys, updateTranslation, insertAsFirst, From 6e6abd44e5b1af6b85374daba71e91e843790ca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Gran=C3=A1t?= Date: Mon, 10 Mar 2025 15:21:59 +0100 Subject: [PATCH 2/7] feat: sort menu under icon --- .../translations/context/services/useTranslationsService.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/views/projects/translations/context/services/useTranslationsService.tsx b/webapp/src/views/projects/translations/context/services/useTranslationsService.tsx index 5d137cbdca..786440c113 100644 --- a/webapp/src/views/projects/translations/context/services/useTranslationsService.tsx +++ b/webapp/src/views/projects/translations/context/services/useTranslationsService.tsx @@ -152,7 +152,7 @@ export const useTranslationsService = (props: Props) => { filterTaskNumber: props.prefilter?.task !== undefined ? [props.prefilter.task] : undefined, filterTaskKeysNotDone: props.prefilter?.taskFilterNotDone || undefined, - sort: ['keyNamespace', order], + sort: ['keyNamespace', order, 'keyId'], }; const translations = useApiInfiniteQuery({ From 0b835046a2b3fc0cf4715f3e41687f4f8ae5f1e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Gran=C3=A1t?= Date: Mon, 10 Mar 2025 15:34:48 +0100 Subject: [PATCH 3/7] chore: e2e sorting --- .../e2e/translations/translationSorting.cy.ts | 45 +++++++++++++++++++ e2e/cypress/support/dataCyType.d.ts | 2 + .../translationOrder/TranslationOrderMenu.tsx | 1 + .../TranslationHeader/TranslationControls.tsx | 5 ++- .../TranslationControlsCompact.tsx | 1 + 5 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 e2e/cypress/e2e/translations/translationSorting.cy.ts diff --git a/e2e/cypress/e2e/translations/translationSorting.cy.ts b/e2e/cypress/e2e/translations/translationSorting.cy.ts new file mode 100644 index 0000000000..650a727131 --- /dev/null +++ b/e2e/cypress/e2e/translations/translationSorting.cy.ts @@ -0,0 +1,45 @@ +import { ProjectDTO } from '../../../../webapp/src/service/response.types'; +import { visitTranslations } from '../../common/translations'; + +import { waitForGlobalLoading } from '../../common/loading'; +import { translationsTestData } from '../../common/apiCalls/testData/testData'; +import { login } from '../../common/apiCalls/common'; + +const TIMEOUT_ONE_MINUTE = 1000 * 60; + +describe('Translations sorting', () => { + let project: ProjectDTO = null; + + before(() => { + translationsTestData.cleanupForFilters(); + translationsTestData + .generateForFilters() + .then((p) => { + project = p; + }) + .then(() => { + login('franta', 'admin'); + visit(); + }); + }); + + beforeEach(() => { + login('franta', 'admin'); + visit(); + waitForGlobalLoading(); + cy.contains('A key', { timeout: TIMEOUT_ONE_MINUTE }).should('be.visible'); + }); + + after(() => { + translationsTestData.cleanupForFilters(); + }); + + it('sort by key name', () => { + cy.gcy('translation-controls-order-item').click(); + cy.wait(1000000); + }); + + const visit = () => { + visitTranslations(project.id); + }; +}); diff --git a/e2e/cypress/support/dataCyType.d.ts b/e2e/cypress/support/dataCyType.d.ts index e50057588f..3ba57a47b1 100644 --- a/e2e/cypress/support/dataCyType.d.ts +++ b/e2e/cypress/support/dataCyType.d.ts @@ -591,6 +591,8 @@ declare namespace DataCy { "topbar-trial-popover-trial-plan-chip" | "transfer-project-apply-button" | "translation-agency-item" | + "translation-controls-order" | + "translation-controls-order-item" | "translation-create-description-input" | "translation-create-key-input" | "translation-create-namespace-input" | diff --git a/webapp/src/component/translation/translationOrder/TranslationOrderMenu.tsx b/webapp/src/component/translation/translationOrder/TranslationOrderMenu.tsx index 297f80323b..fc89dfc756 100644 --- a/webapp/src/component/translation/translationOrder/TranslationOrderMenu.tsx +++ b/webapp/src/component/translation/translationOrder/TranslationOrderMenu.tsx @@ -34,6 +34,7 @@ export const TranslationOrderMenu = ({ onClose(); }} selected={o.value === value} + data-cy="translation-controls-order-item" > {o.label} diff --git a/webapp/src/views/projects/translations/TranslationHeader/TranslationControls.tsx b/webapp/src/views/projects/translations/TranslationHeader/TranslationControls.tsx index e6454e81e6..7cbab72b5b 100644 --- a/webapp/src/views/projects/translations/TranslationHeader/TranslationControls.tsx +++ b/webapp/src/views/projects/translations/TranslationHeader/TranslationControls.tsx @@ -104,7 +104,10 @@ export const TranslationControls: React.FC = ({ onDialogOpen }) => { badgeContent={order === 'keyName' ? 0 : 1} overlap="circular" > - setAnchorOrderEl(e.currentTarget)}> + setAnchorOrderEl(e.currentTarget)} + data-cy="translation-controls-order" + > diff --git a/webapp/src/views/projects/translations/TranslationHeader/TranslationControlsCompact.tsx b/webapp/src/views/projects/translations/TranslationHeader/TranslationControlsCompact.tsx index 37974677fa..a604757b8b 100644 --- a/webapp/src/views/projects/translations/TranslationHeader/TranslationControlsCompact.tsx +++ b/webapp/src/views/projects/translations/TranslationHeader/TranslationControlsCompact.tsx @@ -207,6 +207,7 @@ export const TranslationControlsCompact: React.FC = ({ setAnchorOrderEl(e.currentTarget)} + data-cy="translation-controls-order" > From d9ef81803d467b7df7941f4e750c7a1b4e5bed5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Gran=C3=A1t?= Date: Mon, 10 Mar 2025 15:58:01 +0100 Subject: [PATCH 4/7] chore: e2e tests --- .../e2e/translations/translationSorting.cy.ts | 81 ++++++++++++------- 1 file changed, 50 insertions(+), 31 deletions(-) diff --git a/e2e/cypress/e2e/translations/translationSorting.cy.ts b/e2e/cypress/e2e/translations/translationSorting.cy.ts index 650a727131..7c57afecde 100644 --- a/e2e/cypress/e2e/translations/translationSorting.cy.ts +++ b/e2e/cypress/e2e/translations/translationSorting.cy.ts @@ -1,45 +1,64 @@ import { ProjectDTO } from '../../../../webapp/src/service/response.types'; -import { visitTranslations } from '../../common/translations'; - -import { waitForGlobalLoading } from '../../common/loading'; -import { translationsTestData } from '../../common/apiCalls/testData/testData'; -import { login } from '../../common/apiCalls/common'; - -const TIMEOUT_ONE_MINUTE = 1000 * 60; +import { createKey } from '../../common/apiCalls/common'; +import { + create4Translations, + translationsBeforeEach, + visitTranslations, +} from '../../common/translations'; describe('Translations sorting', () => { let project: ProjectDTO = null; - before(() => { - translationsTestData.cleanupForFilters(); - translationsTestData - .generateForFilters() - .then((p) => { - project = p; - }) - .then(() => { - login('franta', 'admin'); - visit(); - }); - }); - beforeEach(() => { - login('franta', 'admin'); - visit(); - waitForGlobalLoading(); - cy.contains('A key', { timeout: TIMEOUT_ONE_MINUTE }).should('be.visible'); + translationsBeforeEach() + .then((p) => (project = p)) + .then(() => createKey(project.id, 'zzz.first.created', {})) + .then(() => create4Translations(project.id)) + .then(() => createKey(project.id, 'aaa.last.created', {})) + .then(() => visitTranslations(project.id)); }); - after(() => { - translationsTestData.cleanupForFilters(); + it('sort by key name', () => { + cy.gcy('translation-controls-order').click(); + cy.waitForDom(); + cy.gcy('translation-controls-order-item') + .contains('Key name A to Z') + .click(); + cy.gcy('translations-key-name').eq(0).should('contain', 'aaa.last.created'); + cy.gcy('translations-key-name').eq(1).should('contain', 'Cool key 01'); }); it('sort by key name', () => { - cy.gcy('translation-controls-order-item').click(); - cy.wait(1000000); + cy.gcy('translation-controls-order').click(); + cy.waitForDom(); + cy.gcy('translation-controls-order-item') + .contains('Key name Z to A') + .click(); + cy.gcy('translations-key-name') + .eq(0) + .should('contain', 'zzz.first.created'); + cy.gcy('translations-key-name').eq(1).should('contain', 'Cool key 04'); + }); + + it('sort from newest keys', () => { + cy.gcy('translation-controls-order').click(); + cy.waitForDom(); + cy.gcy('translation-controls-order-item') + .contains('First key added') + .click(); + cy.gcy('translations-key-name') + .eq(0) + .should('contain', 'zzz.first.created'); + cy.gcy('translations-key-name').eq(1).should('contain', 'Cool key 01'); }); - const visit = () => { - visitTranslations(project.id); - }; + it('sort from oldest keys', () => { + cy.gcy('translation-controls-order').click(); + cy.waitForDom(); + cy.gcy('translation-controls-order-item') + .contains('Last key added') + .click(); + cy.gcy('translations-key-name').eq(0).should('contain', 'aaa.last.created'); + cy.gcy('translations-key-name').eq(1).should('contain', 'Cool key 04'); + }); }); From 4272389f8e46a5d339180a0f125b866a7f07912c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Gran=C3=A1t?= Date: Mon, 10 Mar 2025 16:09:58 +0100 Subject: [PATCH 5/7] chore: fix BE build --- .../io/tolgee/service/queryBuilders/CursorUtilUnitTest.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/app/src/test/kotlin/io/tolgee/service/queryBuilders/CursorUtilUnitTest.kt b/backend/app/src/test/kotlin/io/tolgee/service/queryBuilders/CursorUtilUnitTest.kt index aee08daa93..d9a3797924 100644 --- a/backend/app/src/test/kotlin/io/tolgee/service/queryBuilders/CursorUtilUnitTest.kt +++ b/backend/app/src/test/kotlin/io/tolgee/service/queryBuilders/CursorUtilUnitTest.kt @@ -9,6 +9,8 @@ import net.javacrumbs.jsonunit.assertj.assertThatJson import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.springframework.data.domain.Sort +import java.sql.Time +import java.sql.Timestamp import java.util.* class CursorUtilUnitTest { @@ -40,6 +42,7 @@ class CursorUtilUnitTest { unresolvedCommentCount = 1, ), ), + createdAt = Timestamp(1000003022), contextPresent = false, ) cursor = From c8f266f4ee99a3c905c5ba6fe99661809d374784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Gran=C3=A1t?= Date: Mon, 10 Mar 2025 16:13:01 +0100 Subject: [PATCH 6/7] chore: fix BE build --- .../kotlin/io/tolgee/service/queryBuilders/CursorUtilUnitTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/app/src/test/kotlin/io/tolgee/service/queryBuilders/CursorUtilUnitTest.kt b/backend/app/src/test/kotlin/io/tolgee/service/queryBuilders/CursorUtilUnitTest.kt index d9a3797924..724a116f03 100644 --- a/backend/app/src/test/kotlin/io/tolgee/service/queryBuilders/CursorUtilUnitTest.kt +++ b/backend/app/src/test/kotlin/io/tolgee/service/queryBuilders/CursorUtilUnitTest.kt @@ -9,7 +9,6 @@ import net.javacrumbs.jsonunit.assertj.assertThatJson import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.springframework.data.domain.Sort -import java.sql.Time import java.sql.Timestamp import java.util.* From 666d281ed0d46552d7ec23aa7a69663f0d805795 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0t=C4=9Bp=C3=A1n=20Gran=C3=A1t?= Date: Mon, 10 Mar 2025 16:29:30 +0100 Subject: [PATCH 7/7] chore: improve e2e reliability --- e2e/cypress/e2e/translations/translationSorting.cy.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/e2e/cypress/e2e/translations/translationSorting.cy.ts b/e2e/cypress/e2e/translations/translationSorting.cy.ts index 7c57afecde..e5249d7459 100644 --- a/e2e/cypress/e2e/translations/translationSorting.cy.ts +++ b/e2e/cypress/e2e/translations/translationSorting.cy.ts @@ -1,5 +1,6 @@ import { ProjectDTO } from '../../../../webapp/src/service/response.types'; import { createKey } from '../../common/apiCalls/common'; +import { waitForGlobalLoading } from '../../common/loading'; import { create4Translations, translationsBeforeEach, @@ -18,22 +19,24 @@ describe('Translations sorting', () => { .then(() => visitTranslations(project.id)); }); - it('sort by key name', () => { + it('sort by key name a to z', () => { cy.gcy('translation-controls-order').click(); cy.waitForDom(); cy.gcy('translation-controls-order-item') .contains('Key name A to Z') .click(); + waitForGlobalLoading(); cy.gcy('translations-key-name').eq(0).should('contain', 'aaa.last.created'); cy.gcy('translations-key-name').eq(1).should('contain', 'Cool key 01'); }); - it('sort by key name', () => { + it('sort by key name z to a', () => { cy.gcy('translation-controls-order').click(); cy.waitForDom(); cy.gcy('translation-controls-order-item') .contains('Key name Z to A') .click(); + cy.gcy('translations-key-name') .eq(0) .should('contain', 'zzz.first.created'); @@ -46,6 +49,7 @@ describe('Translations sorting', () => { cy.gcy('translation-controls-order-item') .contains('First key added') .click(); + waitForGlobalLoading(); cy.gcy('translations-key-name') .eq(0) .should('contain', 'zzz.first.created'); @@ -58,6 +62,7 @@ describe('Translations sorting', () => { cy.gcy('translation-controls-order-item') .contains('Last key added') .click(); + waitForGlobalLoading(); cy.gcy('translations-key-name').eq(0).should('contain', 'aaa.last.created'); cy.gcy('translations-key-name').eq(1).should('contain', 'Cool key 04'); });