diff --git a/src/api/config/terminology.ts b/src/api/config/terminology.ts new file mode 100644 index 00000000..48e65948 --- /dev/null +++ b/src/api/config/terminology.ts @@ -0,0 +1,11 @@ +import { type GetTerminologyResult } from '@/models/v4/configs/getTerminologyResult'; + +import { createApiEndpoint } from '../common/client'; + +const getTerminologyApi = createApiEndpoint('/Config/GetTerminology'); + +/** The department's own names for modules and built-in record types (setting 118). */ +export const getTerminology = async () => { + const response = await getTerminologyApi.get(); + return response.data; +}; diff --git a/src/app/(app)/__tests__/init-hub-connect.test.tsx b/src/app/(app)/__tests__/init-hub-connect.test.tsx index d7c765bb..09c0e0f0 100644 --- a/src/app/(app)/__tests__/init-hub-connect.test.tsx +++ b/src/app/(app)/__tests__/init-hub-connect.test.tsx @@ -123,6 +123,11 @@ jest.mock('@/stores/feature-flags/store', () => ({ featureFlagsStore: { getState: () => ({ fetchFlags: jest.fn().mockResolvedValue(undefined), isEnabled: mockIsChatEnabled }) }, })); +const mockFetchTerminology = jest.fn().mockResolvedValue(undefined); +jest.mock('@/stores/terminology/store', () => ({ + terminologyStore: { getState: () => ({ fetchTerminology: mockFetchTerminology }) }, +})); + const mockConnectUpdateHub = jest.fn().mockResolvedValue(undefined); const mockConnectGeolocationHub = jest.fn().mockResolvedValue(undefined); const mockConnectChatHub = jest.fn().mockResolvedValue(undefined); @@ -148,6 +153,7 @@ describe('app initialization connects the SignalR update hub', () => { mockConnectUpdateHub.mockResolvedValue(undefined); mockConnectGeolocationHub.mockResolvedValue(undefined); mockConnectChatHub.mockResolvedValue(undefined); + mockFetchTerminology.mockResolvedValue(undefined); mockUseSignalRLifecycle.mockImplementation(({ hasInitialized }) => { lifecycleCalls.push({ hasInitialized, updateConnectsSoFar: mockConnectUpdateHub.mock.calls.length }); return undefined as unknown as ReturnType; @@ -162,6 +168,17 @@ describe('app initialization connects the SignalR update hub', () => { }); }); + it('fetches the department terminology after rights and before the hubs connect', async () => { + render(); + + await waitFor(() => { + expect(mockConnectUpdateHub).toHaveBeenCalledTimes(1); + }); + expect(mockFetchTerminology).toHaveBeenCalledTimes(1); + expect(mockSecurityState.getRights.mock.invocationCallOrder[0]).toBeLessThan(mockFetchTerminology.mock.invocationCallOrder[0]); + expect(mockFetchTerminology.mock.invocationCallOrder[0]).toBeLessThan(mockConnectUpdateHub.mock.invocationCallOrder[0]); + }); + it('connects the update hub before arming useSignalRLifecycle, so the two cannot both connect', async () => { render(); @@ -221,5 +238,6 @@ describe('app initialization connects the SignalR update hub', () => { expect(mockConnectUpdateHub).not.toHaveBeenCalled(); expect(mockConnectGeolocationHub).not.toHaveBeenCalled(); expect(mockConnectChatHub).not.toHaveBeenCalled(); + expect(mockFetchTerminology).not.toHaveBeenCalled(); }); }); diff --git a/src/app/(app)/__tests__/map.test.tsx b/src/app/(app)/__tests__/map.test.tsx index 4a24a4cd..b4098a18 100644 --- a/src/app/(app)/__tests__/map.test.tsx +++ b/src/app/(app)/__tests__/map.test.tsx @@ -293,7 +293,9 @@ jest.mock('@/lib/env', () => ({ jest.mock('@/lib/logging', () => ({ logger: { + debug: jest.fn(), info: jest.fn(), + warn: jest.fn(), error: jest.fn(), }, })); diff --git a/src/app/(app)/_layout.tsx b/src/app/(app)/_layout.tsx index 4dd943f8..77ce09e0 100644 --- a/src/app/(app)/_layout.tsx +++ b/src/app/(app)/_layout.tsx @@ -48,6 +48,7 @@ import { usePersonnelStore } from '@/stores/personnel/store'; import { useRolesStore } from '@/stores/roles/store'; import { securityStore } from '@/stores/security/store'; import { useSignalRStore } from '@/stores/signalr/signalr-store'; +import { terminologyStore } from '@/stores/terminology/store'; import { useWeatherAlertsStore } from '@/stores/weather-alerts/weather-alerts-store'; // Sets the SDK token now (stored server token, else the built-in one) and again whenever it changes. @@ -142,9 +143,10 @@ export default function TabLayout() { //await usePersonnelStore.getState().init(); if (!isCurrentRun()) return; - // Feature flags must follow rights: the identity key that decides whether persisted - // flags belong to this account reads securityStore.rights.DepartmentId. - await Promise.all([featureFlagsStore.getState().fetchFlags(), dataProtectionStore.getState().fetchCapabilities()]); + // Feature flags and terminology must follow rights: the identity key that decides whether + // persisted flags or names belong to this account reads securityStore.rights.DepartmentId. + // Terminology only relabels screens, and its fetch never rejects. + await Promise.all([featureFlagsStore.getState().fetchFlags(), dataProtectionStore.getState().fetchCapabilities(), terminologyStore.getState().fetchTerminology()]); if (!isCurrentRun()) return; // Realtime feeds. Every one of them has to follow the Promise.all above: opening a hub reads @@ -477,6 +479,9 @@ export default function TabLayout() { return ( ('today'); @@ -252,7 +254,7 @@ export default function CalendarScreen() { <> diff --git a/src/app/(app)/checklists.tsx b/src/app/(app)/checklists.tsx index c144d48b..4e7f0304 100644 --- a/src/app/(app)/checklists.tsx +++ b/src/app/(app)/checklists.tsx @@ -11,9 +11,11 @@ import { useChecklistLiveUpdates } from '@/hooks/use-checklist-live-updates'; import { useProtectedReveal } from '@/hooks/use-protected-reveal'; import { checklistError } from '@/lib/checklists/sync'; import { useChecklistsStore } from '@/stores/checklists/store'; +import { TermKeys, useTerm } from '@/stores/terminology/store'; export default function ChecklistsScreen() { const { t } = useTranslation(); + const checklistsTitle = useTerm(TermKeys.Checklists, t('checklists.labels.Checklists')); const { occurrenceId } = useLocalSearchParams<{ occurrenceId?: string }>(); const unitId: string | undefined = undefined; const state = useChecklistsStore(); @@ -46,7 +48,7 @@ export default function ChecklistsScreen() { const message = error ?? state.error; return ( - +