diff --git a/backend/data/src/main/kotlin/io/tolgee/dtos/request/task/TaskFilters.kt b/backend/data/src/main/kotlin/io/tolgee/dtos/request/task/TaskFilters.kt index d5d8e93de1..d87186b257 100644 --- a/backend/data/src/main/kotlin/io/tolgee/dtos/request/task/TaskFilters.kt +++ b/backend/data/src/main/kotlin/io/tolgee/dtos/request/task/TaskFilters.kt @@ -45,4 +45,9 @@ open class TaskFilters { description = """Filter tasks without project""", ) var filterNotProject: List? = null + + @field:Parameter( + description = """Filter tasks by language""", + ) + var filterLanguage: List? = null } diff --git a/backend/data/src/main/kotlin/io/tolgee/repository/TaskRepository.kt b/backend/data/src/main/kotlin/io/tolgee/repository/TaskRepository.kt index 8d122347ac..d9a6cb2582 100644 --- a/backend/data/src/main/kotlin/io/tolgee/repository/TaskRepository.kt +++ b/backend/data/src/main/kotlin/io/tolgee/repository/TaskRepository.kt @@ -52,6 +52,10 @@ const val FILTERS = """ :#{#filters.filterNotProject} is null or t.project.id not in :#{#filters.filterNotProject} ) + and ( + :#{#filters.filterLanguage} is null + or t.language.id in :#{#filters.filterLanguage} + ) """ @Repository diff --git a/webapp/src/service/apiSchema.generated.ts b/webapp/src/service/apiSchema.generated.ts index 6c9db326d7..a60c20b25f 100644 --- a/webapp/src/service/apiSchema.generated.ts +++ b/webapp/src/service/apiSchema.generated.ts @@ -1148,6 +1148,21 @@ export interface components { * @example 200001,200004 */ permittedLanguageIds?: number[]; + /** + * @description List of languages user can translate to. If null, all languages editing is permitted. + * @example 200001,200004 + */ + translateLanguageIds?: number[]; + /** + * @description List of languages user can view. If null, all languages view is permitted. + * @example 200001,200004 + */ + viewLanguageIds?: number[]; + /** + * @description List of languages user can change state to. If null, changing state of all language values is permitted. + * @example 200001,200004 + */ + stateChangeLanguageIds?: number[]; /** * @description Granted scopes to the user. When user has type permissions, this field contains permission scopes of the type. * @example KEYS_EDIT,TRANSLATIONS_VIEW @@ -1180,21 +1195,6 @@ export interface components { | "content-delivery.publish" | "webhooks.manage" )[]; - /** - * @description List of languages user can translate to. If null, all languages editing is permitted. - * @example 200001,200004 - */ - translateLanguageIds?: number[]; - /** - * @description List of languages user can view. If null, all languages view is permitted. - * @example 200001,200004 - */ - viewLanguageIds?: number[]; - /** - * @description List of languages user can change state to. If null, changing state of all language values is permitted. - * @example 200001,200004 - */ - stateChangeLanguageIds?: number[]; }; LanguageModel: { /** Format: int64 */ @@ -1773,8 +1773,8 @@ export interface components { secretKey?: string; endpoint: string; signingRegion: string; - enabled?: boolean; contentStorageType?: "S3" | "AZURE"; + enabled?: boolean; }; AzureContentStorageConfigModel: { containerName?: string; @@ -2040,10 +2040,10 @@ export interface components { convertPlaceholdersToIcu: boolean; }; ImportSettingsModel: { - /** @description If true, key descriptions will be overridden by the import */ - overrideKeyDescriptions: boolean; /** @description If true, placeholders from other formats will be converted to ICU when possible */ convertPlaceholdersToIcu: boolean; + /** @description If true, key descriptions will be overridden by the import */ + overrideKeyDescriptions: boolean; }; TranslationCommentModel: { /** @@ -2203,14 +2203,14 @@ export interface components { /** Format: int64 */ id: number; /** Format: int64 */ - expiresAt?: number; + createdAt: number; /** Format: int64 */ - lastUsedAt?: number; + updatedAt: number; description: string; /** Format: int64 */ - createdAt: number; + expiresAt?: number; /** Format: int64 */ - updatedAt: number; + lastUsedAt?: number; }; SetOrganizationRoleDto: { roleType: "MEMBER" | "OWNER"; @@ -2350,15 +2350,15 @@ export interface components { id: number; projectName: string; userFullName?: string; - scopes: string[]; + username?: string; + description: string; + /** Format: int64 */ + projectId: number; /** Format: int64 */ expiresAt?: number; /** Format: int64 */ lastUsedAt?: number; - /** Format: int64 */ - projectId: number; - username?: string; - description: string; + scopes: string[]; }; SuperTokenRequest: { /** @description Has to be provided when TOTP enabled */ @@ -3501,17 +3501,17 @@ export interface components { /** Format: int64 */ id: number; basePermissions: components["schemas"]["PermissionModel"]; - avatar?: components["schemas"]["Avatar"]; - /** @example btforg */ - slug: string; + /** @example This is a beautiful organization full of beautiful and clever people */ + description?: string; /** * @description The role of currently authorized user. * * Can be null when user has direct access to one of the projects owned by the organization. */ currentUserRole?: "MEMBER" | "OWNER"; - /** @example This is a beautiful organization full of beautiful and clever people */ - description?: string; + /** @example btforg */ + slug: string; + avatar?: components["schemas"]["Avatar"]; }; PublicBillingConfigurationDTO: { enabled: boolean; @@ -3654,20 +3654,20 @@ export interface components { name: string; /** Format: int64 */ id: number; - baseTranslation?: string; - translation?: string; namespace?: string; description?: string; + baseTranslation?: string; + translation?: string; }; KeySearchSearchResultModel: { view?: components["schemas"]["KeySearchResultView"]; name: string; /** Format: int64 */ id: number; - baseTranslation?: string; - translation?: string; namespace?: string; description?: string; + baseTranslation?: string; + translation?: string; }; PagedModelKeySearchSearchResultModel: { _embedded?: { @@ -4216,14 +4216,14 @@ export interface components { /** Format: int64 */ id: number; /** Format: int64 */ - expiresAt?: number; + createdAt: number; /** Format: int64 */ - lastUsedAt?: number; + updatedAt: number; description: string; /** Format: int64 */ - createdAt: number; + expiresAt?: number; /** Format: int64 */ - updatedAt: number; + lastUsedAt?: number; }; PagedModelOrganizationModel: { _embedded?: { @@ -4344,15 +4344,15 @@ export interface components { id: number; projectName: string; userFullName?: string; - scopes: string[]; + username?: string; + description: string; + /** Format: int64 */ + projectId: number; /** Format: int64 */ expiresAt?: number; /** Format: int64 */ lastUsedAt?: number; - /** Format: int64 */ - projectId: number; - username?: string; - description: string; + scopes: string[]; }; PagedModelUserAccountModel: { _embedded?: { @@ -10525,6 +10525,8 @@ export interface operations { filterProject?: number[]; /** Filter tasks without project */ filterNotProject?: number[]; + /** Filter tasks by language */ + filterLanguage?: number[]; /** Zero-based page index (0..N) */ page?: number; /** The size of the page to be returned */ @@ -13506,6 +13508,8 @@ export interface operations { filterProject?: number[]; /** Filter tasks without project */ filterNotProject?: number[]; + /** Filter tasks by language */ + filterLanguage?: number[]; /** Zero-based page index (0..N) */ page?: number; /** The size of the page to be returned */ diff --git a/webapp/src/views/projects/export/components/LanguageSelector.tsx b/webapp/src/views/projects/export/components/LanguageSelector.tsx index 83b7c1d855..6f750b3482 100644 --- a/webapp/src/views/projects/export/components/LanguageSelector.tsx +++ b/webapp/src/views/projects/export/components/LanguageSelector.tsx @@ -17,7 +17,7 @@ type LanguageModel = components['schemas']['LanguageModel']; type Props = { languages: LanguageModel[] | undefined; - className: string; + className?: string; }; export const LanguageSelector: React.FC = ({ languages, className }) => { diff --git a/webapp/src/views/projects/tasks/ProjectTasksView.tsx b/webapp/src/views/projects/tasks/ProjectTasksView.tsx index bdb60ca4f8..198ea13f98 100644 --- a/webapp/src/views/projects/tasks/ProjectTasksView.tsx +++ b/webapp/src/views/projects/tasks/ProjectTasksView.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { Checkbox, Dialog, ListProps, PaperProps, styled } from '@mui/material'; +import { Dialog, ListProps, PaperProps, styled } from '@mui/material'; import { useTranslate } from '@tolgee/react'; import { useProject } from 'tg.hooks/useProject'; @@ -11,6 +11,7 @@ import { TaskDetail } from 'tg.component/task/TaskDetail'; import { components } from 'tg.service/apiSchema.generated'; import { BaseProjectView } from '../BaseProjectView'; +import { TasksHeader } from './TasksHeader'; type TaskModel = components['schemas']['TaskModel']; @@ -26,6 +27,7 @@ export const ProjectTasksView = () => { const [page, setPage] = useState(0); const [search, setSearch] = useState(''); const [showClosed, setShowClosed] = useState(false); + const [filterLanguages, setFilterLanguages] = useState([]); const [detail, setDetail] = useState(); @@ -42,6 +44,7 @@ export const ProjectTasksView = () => { page, search, filterNotState: showClosed ? undefined : ['CLOSED', 'DONE'], + filterLanguage: filterLanguages, }, options: { keepPreviousData: true, @@ -62,15 +65,17 @@ export const ProjectTasksView = () => { ], ]} > - setShowClosed(e.target.checked)} + void; + showClosed: boolean; + onShowClosedChange: (value: boolean) => void; + filterLanguages: number[]; + onFilterLanguagesChange: (value: number[]) => void; +}; + +export const TasksHeader = ({ + sx, + className, + onSearchChange, + showClosed, + onShowClosedChange, + filterLanguages, + onFilterLanguagesChange, +}: Props) => { + const [localSearch, setLocalSearch] = useState(''); + const onDebouncedSearchChange = useDebounceCallback(onSearchChange, 500); + const project = useProject(); + const { t } = useTranslate(); + const languagesLoadable = useApiQuery({ + url: '/v2/projects/{projectId}/languages', + method: 'get', + path: { projectId: project.id }, + query: { + page: 0, + size: 1000, + sort: ['tag'], + }, + }); + + const languages = languagesLoadable.data?._embedded?.languages ?? []; + + return ( + + + { + setLocalSearch(e.target.value); + onDebouncedSearchChange(e.target.value); + }} + placeholder={t('tasks_search_placeholder')} + InputProps={{ + startAdornment: ( + + + + ), + }} + /> + onShowClosedChange(!showClosed)} + control={} + label={t('tasks_show_closed_label')} + /> + + + onFilterLanguagesChange( + languages.filter((l) => tags.includes(l.tag)).map((l) => l.id) + ) + } + value={languages + .filter((l) => filterLanguages.includes(l.id)) + .map((l) => l.tag)} + languages={languages || []} + enableEmpty + context="tasks" + placeholder={t('tasks_filter_by_language')} + /> + + ); +};