Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: ability to sort keys #2977

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ 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.Timestamp
import java.util.*

class CursorUtilUnitTest {
Expand Down Expand Up @@ -40,6 +41,7 @@ class CursorUtilUnitTest {
unresolvedCommentCount = 1,
),
),
createdAt = Timestamp(1000003022),
contextPresent = false,
)
cursor =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?,
Expand All @@ -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?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(
private val cb: CriteriaBuilder,
Expand All @@ -45,6 +47,7 @@ class QueryBase<T>(
val whereConditions: MutableSet<Predicate> = HashSet()
val root: Root<Key> = query.from(Key::class.java)
val keyNameExpression: Path<String> = root.get(Key_.name)
val keyCreatedAtExpression: Path<Date> = root.get(Key_.createdAt)
val keyIsPluralExpression: Path<Boolean> = root.get(Key_.isPlural)
val keyArgNameExpression: Path<String?> = root.get(Key_.pluralArgName)
val keyIdExpression: Path<Long> = root.get(Key_.id)
Expand All @@ -59,6 +62,7 @@ class QueryBase<T>(

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
Expand Down
69 changes: 69 additions & 0 deletions e2e/cypress/e2e/translations/translationSorting.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { ProjectDTO } from '../../../../webapp/src/service/response.types';
import { createKey } from '../../common/apiCalls/common';
import { waitForGlobalLoading } from '../../common/loading';
import {
create4Translations,
translationsBeforeEach,
visitTranslations,
} from '../../common/translations';

describe('Translations sorting', () => {
let project: ProjectDTO = null;

beforeEach(() => {
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));
});

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 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');
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();
waitForGlobalLoading();
cy.gcy('translations-key-name')
.eq(0)
.should('contain', 'zzz.first.created');
cy.gcy('translations-key-name').eq(1).should('contain', 'Cool key 01');
});

it('sort from oldest keys', () => {
cy.gcy('translation-controls-order').click();
cy.waitForDom();
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');
});
});
2 changes: 2 additions & 0 deletions e2e/cypress/support/dataCyType.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" |
Expand Down
1 change: 1 addition & 0 deletions webapp/src/component/CustomIcons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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 (
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={onClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
>
{options.map((o) => (
<MenuItem
value={o.value}
key={o.value}
onClick={() => {
onChange(o.value);
onClose();
}}
selected={o.value === value}
data-cy="translation-controls-order-item"
>
{o.label}
</MenuItem>
))}
</Menu>
);
};
Original file line number Diff line number Diff line change
@@ -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') },
];
};
3 changes: 3 additions & 0 deletions webapp/src/svgs/icons/sort.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
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';
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 {
useTranslationsActions,
useTranslationsSelector,
} from '../context/TranslationsContext';
import { StickyHeader } from './StickyHeader';
import { useState } from 'react';
import { Sort } from 'tg.component/CustomIcons';

const StyledContainer = styled('div')`
display: grid;
Expand Down Expand Up @@ -47,12 +58,17 @@ export const TranslationControls: React.FC<Props> = ({ onDialogOpen }) => {
const search = useTranslationsSelector((v) => v.search);
const languages = useTranslationsSelector((v) => v.languages);
const { t } = useTranslate();
const [anchorOrderEl, setAnchorOrderEl] = useState<HTMLButtonElement | null>(
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)) ?? [];
Expand Down Expand Up @@ -80,6 +96,29 @@ export const TranslationControls: React.FC<Props> = ({ onDialogOpen }) => {
value={filters}
onChange={setFilters}
/>

<Tooltip title={t('translation_controls_order_tooltip')}>
<Badge
color="primary"
variant="dot"
badgeContent={order === 'keyName' ? 0 : 1}
overlap="circular"
>
<IconButton
onClick={(e) => setAnchorOrderEl(e.currentTarget)}
data-cy="translation-controls-order"
>
<Sort />
</IconButton>
</Badge>
</Tooltip>

<TranslationOrderMenu
anchorEl={anchorOrderEl}
onClose={() => setAnchorOrderEl(null)}
onChange={setOrder}
value={order}
/>
</StyledSpaced>

<Box overflow="hidden" position="relative">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
ButtonGroup,
IconButton,
styled,
Tooltip,
} from '@mui/material';
import { useTranslate } from '@tolgee/react';

Expand All @@ -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;
Expand Down Expand Up @@ -98,15 +101,20 @@ export const TranslationControlsCompact: React.FC<Props> = ({
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<HTMLButtonElement | null>(null);
const [anchorLanguagesEl, setAnchorLanguagesEl] =
useState<HTMLButtonElement | null>(null);
const [anchorOrderEl, setAnchorOrderEl] = useState<HTMLButtonElement | null>(
null
);

const handleSearchChange = (value: string) => {
setSearch(value);
Expand Down Expand Up @@ -189,6 +197,29 @@ export const TranslationControlsCompact: React.FC<Props> = ({
filtersContent={filtersContent}
onChange={setFilters}
/>
<Tooltip title={t('translation_controls_order_tooltip')}>
<Badge
color="primary"
variant="dot"
badgeContent={order === 'keyName' ? 0 : 1}
overlap="circular"
>
<StyledIconButton
size="small"
onClick={(e) => setAnchorOrderEl(e.currentTarget)}
data-cy="translation-controls-order"
>
<Sort />
</StyledIconButton>
</Badge>
</Tooltip>

<TranslationOrderMenu
anchorEl={anchorOrderEl}
onClose={() => setAnchorOrderEl(null)}
onChange={setOrder}
value={order}
/>
</StyledSpaced>

<Box overflow="hidden" position="relative">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading