diff --git a/backend/api/query/adapters/patient.py b/backend/api/query/adapters/patient.py index 015fde5..d153e61 100644 --- a/backend/api/query/adapters/patient.py +++ b/backend/api/query/adapters/patient.py @@ -45,7 +45,6 @@ def _ensure_position_join(query: Select[Any], ctx: dict[str, Any]) -> tuple[Sele ln = aliased(models.LocationNode) ctx["position_node"] = ln query = query.outerjoin(ln, models.Patient.position_id == ln.id) - ctx["needs_distinct"] = True return query, ln @@ -172,6 +171,15 @@ def apply_patient_filter_clause( if c is not None: query = query.where(c) return query + if key in LOCATION_SORT_KEY_KINDS: + query, lineage_nodes = _ensure_position_lineage_joins(query, ctx) + expr = _location_title_for_kind( + lineage_nodes, LOCATION_SORT_KEY_KINDS[key] + ) + c = apply_ops_to_column(expr, op, val) + if c is not None: + query = query.where(c) + return query if key == "position": if op in (QueryOperator.EQ, QueryOperator.IN) and val: has_uuid = (val.uuid_value is not None and val.uuid_value != "") or ( @@ -248,7 +256,6 @@ def apply_patient_sorts( query, _pa, col = join_property_value( query, models.Patient, prop_id, ft, "patient" ) - ctx["needs_distinct"] = True if desc_order: order_parts.append(col.desc().nulls_last()) else: @@ -303,12 +310,12 @@ def apply_patient_sorts( elif key in LOCATION_SORT_KEY_KINDS: query, lineage_nodes = _ensure_position_lineage_joins(query, ctx) t = _location_title_for_kind(lineage_nodes, LOCATION_SORT_KEY_KINDS[key]) - order_parts.append(t.desc() if desc_order else t.asc()) + order_parts.append( + t.desc().nulls_last() if desc_order else t.asc().nulls_first() + ) if not order_parts: return query.order_by(models.Patient.id.asc()) - if ctx.get("needs_distinct"): - return query.order_by(models.Patient.id.asc(), *order_parts) return query.order_by(*order_parts, models.Patient.id.asc()) @@ -497,7 +504,7 @@ def build_patient_queryable_fields_static() -> list[QueryableField]: label=label, kind=QueryableFieldKind.SCALAR, value_type=QueryableValueType.STRING, - allowed_operators=[], + allowed_operators=str_ops, sortable=True, sort_directions=sort_directions_for(True), searchable=False, diff --git a/backend/api/query/adapters/task.py b/backend/api/query/adapters/task.py index 94fa962..43e9519 100644 --- a/backend/api/query/adapters/task.py +++ b/backend/api/query/adapters/task.py @@ -115,7 +115,6 @@ def _ensure_team_join(query: Select[Any], ctx: dict[str, Any]) -> tuple[Select[A ln = aliased(models.LocationNode) ctx["assignee_team"] = ln query = query.outerjoin(ln, models.Task.assignee_team_id == ln.id) - ctx["needs_distinct"] = True return query, ln @@ -322,7 +321,6 @@ def apply_task_sorts( query, _pa, col = join_property_value( query, models.Task, prop_id, ft, "task" ) - ctx["needs_distinct"] = True if desc_order: order_parts.append(col.desc().nulls_last()) else: @@ -392,8 +390,6 @@ def apply_task_sorts( if not order_parts: return query.order_by(models.Task.id.asc()) - if ctx.get("needs_distinct"): - return query.order_by(models.Task.id.asc(), *order_parts) return query.order_by(*order_parts, models.Task.id.asc()) diff --git a/backend/api/query/dedupe_select.py b/backend/api/query/dedupe_select.py new file mode 100644 index 0000000..de38498 --- /dev/null +++ b/backend/api/query/dedupe_select.py @@ -0,0 +1,14 @@ +from typing import Any + +from sqlalchemy import Select, select + + +def dedupe_orm_select_by_root_id(stmt: Select[Any], root_model: type) -> Select[Any]: + opts = getattr(stmt, "_with_options", None) or () + stmt_flat = stmt.order_by(None).limit(None).offset(None) + pk = root_model.id + ids_sq = stmt_flat.with_only_columns(pk).distinct().scalar_subquery() + out = select(root_model).where(root_model.id.in_(ids_sq)) + for opt in opts: + out = out.options(opt) + return out diff --git a/backend/api/query/engine.py b/backend/api/query/engine.py index 1856fe0..e8b81b4 100644 --- a/backend/api/query/engine.py +++ b/backend/api/query/engine.py @@ -6,6 +6,7 @@ from api.context import Info from api.inputs import PaginationInput from api.query.inputs import QueryFilterClauseInput, QuerySearchInput, QuerySortClauseInput +from api.query.dedupe_select import dedupe_orm_select_by_root_id from api.query.property_sql import load_property_field_types from api.query.registry import get_entity_handler @@ -65,12 +66,13 @@ async def apply_unified_query( if text: stmt = handler["apply_search"](stmt, search, ctx) + if ctx.get("needs_distinct"): + stmt = dedupe_orm_select_by_root_id(stmt, handler["root_model"]) + ctx["needs_distinct"] = False + if not for_count: stmt = handler["apply_sorts"](stmt, sorts, ctx, property_field_types) - if ctx.get("needs_distinct"): - stmt = stmt.distinct(handler["root_model"].id) - if ( not for_count and pagination is not None diff --git a/backend/api/resolvers/patient.py b/backend/api/resolvers/patient.py index 13b8cb9..29e2f6b 100644 --- a/backend/api/resolvers/patient.py +++ b/backend/api/resolvers/patient.py @@ -20,6 +20,7 @@ from api.services.property import PropertyService from api.types.patient import PatientType from api.errors import raise_forbidden +from api.query.dedupe_select import dedupe_orm_select_by_root_id from api.query.patient_location_scope import ( apply_patient_subtree_filter_from_cte, build_location_descendants_cte, @@ -87,7 +88,10 @@ async def _build_patients_base_query( ) if filter_cte is not None: - query = apply_patient_subtree_filter_from_cte(query, filter_cte).distinct() + query = dedupe_orm_select_by_root_id( + apply_patient_subtree_filter_from_cte(query, filter_cte), + models.Patient, + ) return query, accessible_location_ids @@ -220,7 +224,7 @@ async def recent_patients( root_cte = root_cte.union_all(root_children) patient_locations_root = aliased(models.patient_locations) patient_teams_root = aliased(models.patient_teams) - query = ( + query = dedupe_orm_select_by_root_id( query.outerjoin( patient_locations_root, models.Patient.id == patient_locations_root.c.patient_id, @@ -241,8 +245,8 @@ async def recent_patients( ) | (patient_locations_root.c.location_id.in_(select(root_cte.c.id))) | (patient_teams_root.c.location_id.in_(select(root_cte.c.id))) - ) - .distinct() + ), + models.Patient, ) return query @@ -301,7 +305,7 @@ async def recentPatientsTotal( root_cte = root_cte.union_all(root_children) patient_locations_root = aliased(models.patient_locations) patient_teams_root = aliased(models.patient_teams) - query = ( + query = dedupe_orm_select_by_root_id( query.outerjoin( patient_locations_root, models.Patient.id == patient_locations_root.c.patient_id, @@ -322,8 +326,8 @@ async def recentPatientsTotal( ) | (patient_locations_root.c.location_id.in_(select(root_cte.c.id))) | (patient_teams_root.c.location_id.in_(select(root_cte.c.id))) - ) - .distinct() + ), + models.Patient, ) return await count_unified_query( diff --git a/backend/api/services/authorization.py b/backend/api/services/authorization.py index eee0cab..998bdd8 100644 --- a/backend/api/services/authorization.py +++ b/backend/api/services/authorization.py @@ -177,7 +177,7 @@ def filter_patients_by_access( patient_locations = aliased(models.patient_locations) patient_teams = aliased(models.patient_teams) - return ( + expanded = ( query.outerjoin( patient_locations, models.Patient.id == patient_locations.c.patient_id, @@ -199,5 +199,10 @@ def filter_patients_by_access( | (patient_locations.c.location_id.in_(select(cte.c.id))) | (patient_teams.c.location_id.in_(select(cte.c.id))) ) - .distinct() ) + opts = getattr(expanded, "_with_options", None) or () + ids_sq = expanded.with_only_columns(models.Patient.id).distinct().scalar_subquery() + out = select(models.Patient).where(models.Patient.id.in_(ids_sq)) + for opt in opts: + out = out.options(opt) + return out diff --git a/backend/database/migrations/versions/merge_saved_views_and_task_assignees.py b/backend/database/migrations/versions/merge_saved_views_and_task_assignees.py index 106a4e3..686c153 100644 --- a/backend/database/migrations/versions/merge_saved_views_and_task_assignees.py +++ b/backend/database/migrations/versions/merge_saved_views_and_task_assignees.py @@ -8,8 +8,6 @@ from typing import Sequence, Union -from alembic import op - revision: str = "merge_saved_views_task_assignees" down_revision: Union[str, Sequence[str], None] = ( "add_saved_views_table", diff --git a/web/components/tables/PatientList.tsx b/web/components/tables/PatientList.tsx index a12a9bc..a70c6f3 100644 --- a/web/components/tables/PatientList.tsx +++ b/web/components/tables/PatientList.tsx @@ -21,7 +21,7 @@ import { columnFiltersToQueryFilterClauses, sortingStateToQuerySortClauses } fro import { LIST_PAGE_SIZE } from '@/utils/listPaging' import { useAccumulatedPagination } from '@/hooks/useAccumulatedPagination' import { PatientCardView } from '@/components/patients/PatientCardView' -import { queryableFieldsToFilterListItems, queryableFieldsToSortingListItems } from '@/utils/queryableFilterList' +import { queryableFieldsToFilterListItems, queryableFieldsToSortingListItems, type QueryableChoiceTagLabelResolver } from '@/utils/queryableFilterList' import { getPropertyFilterFn as getPropertyDatatype } from '@/utils/propertyFilterMapping' import { UserSelectFilterPopUp } from './UserSelectFilterPopUp' import { ExpandableTextBlock } from '@/components/common/ExpandableTextBlock' @@ -38,6 +38,7 @@ import { import { getParsedDocument } from '@/data/hooks/queryHelpers' import { replaceSavedViewInMySavedViewsCache } from '@/utils/savedViewsCache' import { useDeferredColumnOrderChange } from '@/hooks/useDeferredColumnOrderChange' +import { useStableSerializedList } from '@/hooks/useStableSerializedList' import { columnIdsFromColumnDefs, sanitizeColumnOrderForKnownColumns } from '@/utils/columnOrder' import { hasActiveLocationFilter, @@ -114,6 +115,22 @@ export const PatientList = forwardRef(({ initi const { refreshingPatientIds } = useRefreshingEntityIds() const { data: propertyDefinitionsData } = usePropertyDefinitions() const { data: queryableFieldsData } = useQueryableFields('Patient') + const queryableFieldsStable = useStableSerializedList( + queryableFieldsData?.queryableFields, + (f) => ({ + key: f.key, + label: f.label, + filterable: f.filterable, + sortable: f.sortable, + sortDirections: f.sortDirections, + propertyDefinitionId: f.propertyDefinitionId, + kind: f.kind, + valueType: f.valueType, + choice: f.choice + ? { keys: f.choice.optionKeys, labels: f.choice.optionLabels } + : null, + }) + ) const effectiveRootLocationIds = rootLocationIds ?? selectedRootLocationIds const [isPanelOpen, setIsPanelOpen] = useState(false) const [selectedPatient, setSelectedPatient] = useState(undefined) @@ -634,14 +651,31 @@ export const PatientList = forwardRef(({ initi 'tasks': translation('tasks'), 'updated': translation('updated'), 'updateDate': translation('updated'), + 'description': translation('description'), } return translatedByKey[key] ?? field.label }, [translation]) + const resolvePatientChoiceTagLabel = useCallback((field, optionKey, backendLabel) => { + if (field.propertyDefinitionId) return backendLabel + if (field.key === 'state') return translation('patientState', { state: optionKey }) + if (field.key === 'sex') { + if (optionKey === Sex.Male) return translation('male') + if (optionKey === Sex.Female) return translation('female') + return translation('diverse') + } + return backendLabel + }, [translation]) + const availableFilters: FilterListItem[] = useMemo(() => { - const raw = queryableFieldsData?.queryableFields + const raw = queryableFieldsStable if (raw?.length) { - return queryableFieldsToFilterListItems(raw, propertyFieldTypeByDefId, resolvePatientQueryableLabel) + return queryableFieldsToFilterListItems( + raw, + propertyFieldTypeByDefId, + resolvePatientQueryableLabel, + resolvePatientChoiceTagLabel + ) } return [ { @@ -698,15 +732,15 @@ export const PatientList = forwardRef(({ initi } }) ?? [], ] - }, [queryableFieldsData?.queryableFields, propertyFieldTypeByDefId, resolvePatientQueryableLabel, translation, allPatientStates, propertyDefinitionsData?.propertyDefinitions]) + }, [queryableFieldsStable, propertyFieldTypeByDefId, resolvePatientQueryableLabel, resolvePatientChoiceTagLabel, translation, allPatientStates, propertyDefinitionsData?.propertyDefinitions]) const availableSortItems = useMemo(() => { - const raw = queryableFieldsData?.queryableFields + const raw = queryableFieldsStable if (raw?.length) { return queryableFieldsToSortingListItems(raw, resolvePatientQueryableLabel) } return availableFilters.map(({ id, label, dataType }) => ({ id, label, dataType })) - }, [queryableFieldsData?.queryableFields, availableFilters, resolvePatientQueryableLabel]) + }, [queryableFieldsStable, availableFilters, resolvePatientQueryableLabel]) const knownColumnIdsOrdered = useMemo( () => columnIdsFromColumnDefs(columns), diff --git a/web/components/tables/TaskList.tsx b/web/components/tables/TaskList.tsx index 784751e..1589c41 100644 --- a/web/components/tables/TaskList.tsx +++ b/web/components/tables/TaskList.tsx @@ -20,12 +20,13 @@ import { UserInfoPopup } from '@/components/UserInfoPopup' import type { ColumnDef, ColumnFiltersState, ColumnOrderState, PaginationState, SortingState, TableState, VisibilityState } from '@tanstack/table-core' import type { Dispatch, SetStateAction } from 'react' import { useDeferredColumnOrderChange } from '@/hooks/useDeferredColumnOrderChange' +import { useStableSerializedList } from '@/hooks/useStableSerializedList' import { columnIdsFromColumnDefs, sanitizeColumnOrderForKnownColumns } from '@/utils/columnOrder' import { DueDateUtils } from '@/utils/dueDate' import { PriorityUtils } from '@/utils/priority' import { getPropertyColumnsForEntity } from '@/utils/propertyColumn' import { useColumnVisibilityWithPropertyDefaults } from '@/hooks/usePropertyColumnVisibility' -import { queryableFieldsToFilterListItems, queryableFieldsToSortingListItems } from '@/utils/queryableFilterList' +import { queryableFieldsToFilterListItems, queryableFieldsToSortingListItems, type QueryableChoiceTagLabelResolver } from '@/utils/queryableFilterList' import { LIST_PAGE_SIZE } from '@/utils/listPaging' import { TaskCardView } from '@/components/tasks/TaskCardView' import { RefreshingTaskIdsContext, TaskRowRefreshingGate } from '@/components/tables/TaskRowRefreshingGate' @@ -153,6 +154,22 @@ export const TaskList = forwardRef(({ tasks: initial const translation = useTasksTranslation() const { data: propertyDefinitionsData } = usePropertyDefinitions() const { data: queryableFieldsData } = useQueryableFields('Task') + const queryableFieldsStable = useStableSerializedList( + queryableFieldsData?.queryableFields, + (f) => ({ + key: f.key, + label: f.label, + filterable: f.filterable, + sortable: f.sortable, + sortDirections: f.sortDirections, + propertyDefinitionId: f.propertyDefinitionId, + kind: f.kind, + valueType: f.valueType, + choice: f.choice + ? { keys: f.choice.optionKeys, labels: f.choice.optionLabels } + : null, + }) + ) const [clientVisibleCount, setClientVisibleCount] = useState(LIST_PAGE_SIZE) const [listLayout, setListLayout] = useState<'table' | 'card'>(() => ( @@ -443,6 +460,7 @@ export const TaskList = forwardRef(({ tasks: initial priority: translation('priorityLabel'), patient: translation('patient'), assignee: translation('assignedTo'), + assigneeTeam: translation('assigneeTeam'), updated: translation('updated'), updateDate: translation('updated'), position: translation('location'), @@ -450,14 +468,27 @@ export const TaskList = forwardRef(({ tasks: initial firstname: translation('firstName'), lastname: translation('lastName'), birthdate: translation('birthdate'), + estimatedTime: translation('estimatedTime'), + creationDate: translation('creationDate'), } return translatedByKey[field.key] ?? field.label }, [translation]) + const resolveTaskChoiceTagLabel = useCallback((field, optionKey, backendLabel) => { + if (field.propertyDefinitionId) return backendLabel + if (field.key === 'priority') return translation('priority', { priority: optionKey }) + return backendLabel + }, [translation]) + const availableFilters: FilterListItem[] = useMemo(() => { - const raw = queryableFieldsData?.queryableFields + const raw = queryableFieldsStable if (raw?.length) { - return queryableFieldsToFilterListItems(raw, propertyFieldTypeByDefId, resolveTaskQueryableLabel) + return queryableFieldsToFilterListItems( + raw, + propertyFieldTypeByDefId, + resolveTaskQueryableLabel, + resolveTaskChoiceTagLabel + ) } return [ { id: 'title', label: translation('title'), dataType: 'text', tags: [] }, @@ -478,15 +509,15 @@ export const TaskList = forwardRef(({ tasks: initial tags: def.options.map((opt, idx) => ({ label: opt, tag: `${def.id}-opt-${idx}` })), })) ?? [], ] - }, [queryableFieldsData?.queryableFields, propertyFieldTypeByDefId, resolveTaskQueryableLabel, translation, propertyDefinitionsData?.propertyDefinitions]) + }, [queryableFieldsStable, propertyFieldTypeByDefId, resolveTaskQueryableLabel, resolveTaskChoiceTagLabel, translation, propertyDefinitionsData?.propertyDefinitions]) const availableSortItems = useMemo(() => { - const raw = queryableFieldsData?.queryableFields + const raw = queryableFieldsStable if (raw?.length) { return queryableFieldsToSortingListItems(raw, resolveTaskQueryableLabel) } return availableFilters.map(({ id, label, dataType }) => ({ id, label, dataType })) - }, [queryableFieldsData?.queryableFields, availableFilters, resolveTaskQueryableLabel]) + }, [queryableFieldsStable, availableFilters, resolveTaskQueryableLabel]) const columns = useMemo[]>(() => { const cols: ColumnDef[] = [ diff --git a/web/hooks/useStableSerializedList.ts b/web/hooks/useStableSerializedList.ts new file mode 100644 index 0000000..aafb9c8 --- /dev/null +++ b/web/hooks/useStableSerializedList.ts @@ -0,0 +1,19 @@ +import { useRef } from 'react' + +export function useStableSerializedList( + list: readonly T[] | undefined | null, + serializeItem: (item: T) => unknown +): T[] | undefined { + const ref = useRef<{ key: string, list: T[] | undefined }>({ key: '', list: undefined }) + if (!list?.length) { + ref.current = { key: '', list: undefined } + return undefined + } + const key = JSON.stringify(list.map(serializeItem)) + if (ref.current.key === key) { + return ref.current.list + } + const next = [...list] as T[] + ref.current = { key, list: next } + return next +} diff --git a/web/i18n/translations.ts b/web/i18n/translations.ts index b6ef161..adb9ab4 100644 --- a/web/i18n/translations.ts +++ b/web/i18n/translations.ts @@ -25,6 +25,7 @@ export type TasksTranslationEntries = { 'archivedPropertyDescription': string, 'archiveProperty': string, 'assignedTo': string, + 'assigneeTeam': string, 'authenticationFailed': string, 'birthdate': string, 'cancel': string, @@ -50,6 +51,7 @@ export type TasksTranslationEntries = { 'copyViewToMyViews': string, 'create': string, 'createTask': string, + 'creationDate': string, 'currentTime': string, 'dashboard': string, 'dashboardWelcomeAfternoon': (values: { name: string }) => string, @@ -272,6 +274,7 @@ export const tasksTranslation: Translation { @@ -625,6 +629,7 @@ export const tasksTranslation: Translation { @@ -978,6 +984,7 @@ export const tasksTranslation: Translation { @@ -1330,6 +1338,7 @@ export const tasksTranslation: Translation { @@ -1682,6 +1692,7 @@ export const tasksTranslation: Translation { @@ -2037,6 +2049,7 @@ export const tasksTranslation: Translation { diff --git a/web/locales/de-DE.arb b/web/locales/de-DE.arb index b70932d..41d5ea0 100644 --- a/web/locales/de-DE.arb +++ b/web/locales/de-DE.arb @@ -11,6 +11,7 @@ "archivedPropertyDescription": "Archivierte Eigenschaften können nicht mehr neu Objekten hinzugeügt werden.", "archiveProperty": "Eigenschaft Archivieren", "assignedTo": "Zugewiesen an", + "assigneeTeam": "Zugewiesenes Team", "additionalAssigneesCount": "+{count}", "@additionalAssigneesCount": { "placeholders": { @@ -46,6 +47,7 @@ "confirmDeleteView": "Diesen gespeicherten Schnellzugriff löschen? Dies kann nicht rückgängig gemacht werden.", "create": "Erstellen", "createTask": "Aufgabe erstellen", + "creationDate": "Erstellungsdatum", "currentTime": "Aktuelle Zeit", "dashboard": "Dashboard", "dashboardWelcomeDescription": "Hier ist, was heute passiert.", diff --git a/web/locales/en-US.arb b/web/locales/en-US.arb index 8192483..dd6e043 100644 --- a/web/locales/en-US.arb +++ b/web/locales/en-US.arb @@ -11,6 +11,7 @@ "archivedPropertyDescription": "Archived Properties can no longer be assigned to objects.", "archiveProperty": "Archive Property", "assignedTo": "Assigned to", + "assigneeTeam": "Assignee team", "additionalAssigneesCount": "+{count}", "@additionalAssigneesCount": { "placeholders": { @@ -46,6 +47,7 @@ "confirmDeleteView": "Delete this saved quick access? This cannot be undone.", "create": "Create", "createTask": "Create Task", + "creationDate": "Creation date", "currentTime": "Current Time", "dashboard": "Dashboard", "dashboardWelcomeDescription": "Here is what is happening today.", diff --git a/web/locales/es-ES.arb b/web/locales/es-ES.arb index db2ff29..aa8390d 100644 --- a/web/locales/es-ES.arb +++ b/web/locales/es-ES.arb @@ -11,6 +11,7 @@ "archivedPropertyDescription": "Las propiedades archivadas ya no pueden asignarse a objetos.", "archiveProperty": "Archivar propiedad", "assignedTo": "Asignado a", + "assigneeTeam": "Equipo asignado", "additionalAssigneesCount": "+{count}", "@additionalAssigneesCount": { "placeholders": { "count": { "type": "number" } } }, "authenticationFailed": "Error de autenticación", @@ -36,6 +37,7 @@ "confirmDeleteView": "¿Eliminar este acceso rápido guardado? Esta acción no se puede deshacer.", "create": "Crear", "createTask": "Crear tarea", + "creationDate": "Fecha de creación", "currentTime": "Hora actual", "dashboard": "Panel", "dashboardWelcomeDescription": "Esto es lo que ocurre hoy.", diff --git a/web/locales/fr-FR.arb b/web/locales/fr-FR.arb index ea2c532..950153a 100644 --- a/web/locales/fr-FR.arb +++ b/web/locales/fr-FR.arb @@ -11,6 +11,7 @@ "archivedPropertyDescription": "Les propriétés archivées ne peuvent plus être assignées aux objets.", "archiveProperty": "Archiver la propriété", "assignedTo": "Assigné à", + "assigneeTeam": "Équipe assignée", "additionalAssigneesCount": "+{count}", "@additionalAssigneesCount": { "placeholders": { "count": { "type": "number" } } }, "authenticationFailed": "Échec de l''authentification", @@ -36,6 +37,7 @@ "confirmDeleteView": "Supprimer cet accès rapide enregistré ? Cette action est irréversible.", "create": "Créer", "createTask": "Créer une tâche", + "creationDate": "Date de création", "currentTime": "Heure actuelle", "dashboard": "Tableau de bord", "dashboardWelcomeDescription": "Voici ce qui se passe aujourd''hui.", diff --git a/web/locales/nl-NL.arb b/web/locales/nl-NL.arb index 2e8c69e..aaca4ad 100644 --- a/web/locales/nl-NL.arb +++ b/web/locales/nl-NL.arb @@ -11,6 +11,7 @@ "archivedPropertyDescription": "Gearchiveerde eigenschappen kunnen niet meer aan objecten worden toegewezen.", "archiveProperty": "Eigenschap archiveren", "assignedTo": "Toegewezen aan", + "assigneeTeam": "Toegewezen team", "additionalAssigneesCount": "+{count}", "@additionalAssigneesCount": { "placeholders": { "count": { "type": "number" } } }, "authenticationFailed": "Authenticatie mislukt", @@ -36,6 +37,7 @@ "confirmDeleteView": "Deze opgeslagen snelle toegang verwijderen? Dit kan niet ongedaan worden gemaakt.", "create": "Aanmaken", "createTask": "Taak aanmaken", + "creationDate": "Aanmaakdatum", "currentTime": "Huidige tijd", "dashboard": "Dashboard", "dashboardWelcomeDescription": "Dit is wat er vandaag gebeurt.", diff --git a/web/locales/pt-BR.arb b/web/locales/pt-BR.arb index 63c1833..93beea2 100644 --- a/web/locales/pt-BR.arb +++ b/web/locales/pt-BR.arb @@ -11,6 +11,7 @@ "archivedPropertyDescription": "Propriedades arquivadas não podem mais ser atribuídas a objetos.", "archiveProperty": "Arquivar propriedade", "assignedTo": "Atribuído a", + "assigneeTeam": "Equipe atribuída", "additionalAssigneesCount": "+{count}", "@additionalAssigneesCount": { "placeholders": { "count": { "type": "number" } } }, "authenticationFailed": "Falha na autenticação", @@ -36,6 +37,7 @@ "confirmDeleteView": "Excluir este acesso rápido salvo? Esta ação não pode ser desfeita.", "create": "Criar", "createTask": "Criar tarefa", + "creationDate": "Data de criação", "currentTime": "Hora atual", "dashboard": "Painel", "dashboardWelcomeDescription": "Eis o que está acontecendo hoje.", diff --git a/web/utils/queryableFilterList.tsx b/web/utils/queryableFilterList.tsx index 68f00c0..169bf9b 100644 --- a/web/utils/queryableFilterList.tsx +++ b/web/utils/queryableFilterList.tsx @@ -28,20 +28,29 @@ function filterFieldDataType(field: QueryableField): DataType { export type QueryableSortListItem = Pick export type QueryableFieldLabelResolver = (field: QueryableField) => string +export type QueryableChoiceTagLabelResolver = ( + field: QueryableField, + optionKey: string, + backendLabel: string, +) => string export function queryableFieldsToFilterListItems( fields: QueryableField[], propertyFieldTypeByDefId: Map, - resolveLabel?: QueryableFieldLabelResolver + resolveLabel?: QueryableFieldLabelResolver, + resolveChoiceTagLabel?: QueryableChoiceTagLabelResolver ): FilterListItem[] { return fields.filter(field => field.filterable).map((field): FilterListItem => { const dataType = filterFieldDataType(field) const label = resolveLabel ? resolveLabel(field) : field.label const tags = field.choice - ? field.choice.optionLabels.map((label, idx) => ({ - label, - tag: field.choice!.optionKeys[idx] ?? label, - })) + ? field.choice.optionLabels.map((backendLabel, idx) => { + const optionKey = field.choice!.optionKeys[idx] ?? backendLabel + const displayLabel = resolveChoiceTagLabel + ? resolveChoiceTagLabel(field, optionKey, backendLabel) + : backendLabel + return { label: displayLabel, tag: optionKey } + }) : [] const ft = field.propertyDefinitionId