diff --git a/src/core/store.ts b/src/core/store.ts index 633a1782ac..3a1cd54e97 100644 --- a/src/core/store.ts +++ b/src/core/store.ts @@ -5,6 +5,9 @@ import { createListenerMiddleware, } from '@reduxjs/toolkit'; +import breadcrumbsSlice, { + BreadcrumbsStoreSlice, +} from 'features/breadcrumbs/store'; import callAssignmentsSlice, { callAssignmentCreated, CallAssignmentSlice, @@ -36,6 +39,7 @@ import userSlice, { UserStoreSlice } from 'features/user/store'; import viewsSlice, { ViewsStoreSlice } from 'features/views/store'; export interface RootState { + breadcrumbs: BreadcrumbsStoreSlice; callAssignments: CallAssignmentSlice; campaigns: CampaignsStoreSlice; events: EventsStoreSlice; @@ -51,6 +55,7 @@ export interface RootState { } const reducer = { + breadcrumbs: breadcrumbsSlice.reducer, callAssignments: callAssignmentsSlice.reducer, campaigns: campaignsSlice.reducer, events: eventsSlice.reducer, diff --git a/src/zui/ZUIBreadcrumbTrail.tsx b/src/features/breadcrumbs/components/BreadcrumbTrail.tsx similarity index 67% rename from src/zui/ZUIBreadcrumbTrail.tsx rename to src/features/breadcrumbs/components/BreadcrumbTrail.tsx index d9375bc4b9..52f7420b5c 100644 --- a/src/zui/ZUIBreadcrumbTrail.tsx +++ b/src/features/breadcrumbs/components/BreadcrumbTrail.tsx @@ -3,24 +3,13 @@ import makeStyles from '@mui/styles/makeStyles'; import NavigateNextIcon from '@mui/icons-material/NavigateNext'; import NextLink from 'next/link'; import { Theme } from '@mui/material/styles'; -import { useQuery } from 'react-query'; import { Breadcrumbs, Link, Typography, useMediaQuery } from '@mui/material'; -import { NextRouter, useRouter } from 'next/router'; import { Breadcrumb } from 'utils/types'; -import getBreadcrumbs from '../utils/fetching/getBreadcrumbs'; import { Msg } from 'core/i18n'; -import messageIds from './l10n/messageIds'; - -const getQueryString = function (router: NextRouter): string { - // Only use parameters that are part of the path (e.g. [personId]) - // and not ones that are part of the actual querystring (e.g. ?filter_*) - return Object.entries(router.query) - .filter(([key]) => router.pathname.includes(`[${key}]`)) - .map(([key, val]) => `${key}=${val}`) - .join('&'); -}; +import messageIds from '../l10n/messageIds'; +import useBreadcrumbElements from '../hooks/useBreadcrumbs'; const useStyles = makeStyles((theme) => createStyles({ @@ -48,40 +37,30 @@ const useStyles = makeStyles((theme) => function validMessageId( idStr: string -): keyof typeof messageIds.breadcrumbs | null { - if (idStr in messageIds.breadcrumbs) { - return idStr as keyof typeof messageIds.breadcrumbs; +): keyof typeof messageIds.elements | null { + if (idStr in messageIds.elements) { + return idStr as keyof typeof messageIds.elements; } else { return null; } } -const ZUIBreadcrumbTrail = ({ +const BreadcrumbTrail = ({ highlight, }: { highlight?: boolean; }): JSX.Element | null => { const classes = useStyles({ highlight }); - const router = useRouter(); - const path = router.pathname; - const query = getQueryString(router); - const breadcrumbsQuery = useQuery( - ['breadcrumbs', path, query], - getBreadcrumbs(path, query) - ); + const breadcrumbs = useBreadcrumbElements(); const smallScreen = useMediaQuery('(max-width:700px)'); const mediumScreen = useMediaQuery('(max-width:960px)'); const largeScreen = useMediaQuery('(max-width:1200px)'); - if (!breadcrumbsQuery.isSuccess) { - return
; - } - const getLabel = (crumb: Breadcrumb) => { if (crumb.labelMsg) { const msgId = validMessageId(crumb.labelMsg); if (msgId) { - return ; + return ; } } @@ -97,8 +76,8 @@ const ZUIBreadcrumbTrail = ({ maxItems={smallScreen ? 2 : mediumScreen ? 4 : largeScreen ? 6 : 10} separator={} > - {breadcrumbsQuery.data.map((crumb, index) => { - if (index < breadcrumbsQuery.data.length - 1) { + {breadcrumbs.map((crumb, index) => { + if (index < breadcrumbs.length - 1) { return ( state.breadcrumbs.crumbsByPath[path] + ); + + const query = getPathParameters(router); + + const future = loadItemIfNecessary(crumbsItem, dispatch, { + actionOnLoad: () => crumbsLoad(path), + actionOnSuccess: (item) => crumbsLoaded([path, item]), + loader: async () => { + const elements = await apiClient.get( + `/api/breadcrumbs?pathname=${path}&${query}` + ); + + return { elements, id: path }; + }, + }); + + return future.data?.elements ?? []; +} + +const getPathParameters = function (router: NextRouter): string { + // Only use parameters that are part of the path (e.g. [personId]) + // and not ones that are part of the actual querystring (e.g. ?filter_*) + return Object.entries(router.query) + .filter(([key]) => router.pathname.includes(`[${key}]`)) + .map(([key, val]) => `${key}=${val}`) + .join('&'); +}; diff --git a/src/features/breadcrumbs/l10n/messageIds.ts b/src/features/breadcrumbs/l10n/messageIds.ts new file mode 100644 index 0000000000..b51e2d8f89 --- /dev/null +++ b/src/features/breadcrumbs/l10n/messageIds.ts @@ -0,0 +1,34 @@ +import { m, makeMessages } from 'core/i18n'; + +export default makeMessages('feat.breadcrumbs', { + elements: { + activities: m('Activities'), + archive: m('Archive'), + areas: m('Areas'), + assignees: m('Assignees'), + calendar: m('Calendar'), + callassignments: m('Call assignments'), + callers: m('Callers'), + campaigns: m('Projects'), + closed: m('Closed'), + conversation: m('Conversation'), + events: m('Events'), + folders: m('Lists'), + insights: m('Insights'), + instances: m('Instances'), + journeys: m('Journeys'), + manage: m('Manage'), + milestones: m('Milestones'), + new: m('New'), + organize: m('Organize'), + participants: m('Participants'), + people: m('People'), + projects: m('Projects'), + questions: m('Questions'), + submissions: m('Submissions'), + surveys: m('Surveys'), + tasks: m('Tasks'), + untitledEvent: m('Untitled event'), + views: m('Lists'), + }, +}); diff --git a/src/features/breadcrumbs/store.ts b/src/features/breadcrumbs/store.ts new file mode 100644 index 0000000000..7827118970 --- /dev/null +++ b/src/features/breadcrumbs/store.ts @@ -0,0 +1,50 @@ +import { BreadcrumbElement } from 'pages/api/breadcrumbs'; +import { Action, createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { remoteItem, RemoteItem } from 'utils/storeUtils'; + +type BreadcrumbItem = { + elements: BreadcrumbElement[]; + id: string; +}; + +export interface BreadcrumbsStoreSlice { + crumbsByPath: Record>; +} + +const initialState: BreadcrumbsStoreSlice = { + crumbsByPath: {}, +}; + +function isUpdatedAction(action: Action) { + return action.type.endsWith('Updated'); +} + +const breadcrumbsSlice = createSlice({ + extraReducers: (builder) => { + builder.addMatcher(isUpdatedAction, (state) => { + // In lieu of a general-purpose way of identifying what changed, when + // anything is updated, we invalidate all breadcrumbs. + Object.keys(state.crumbsByPath).forEach((path) => { + state.crumbsByPath[path].isStale = true; + }); + }); + }, + initialState, + name: 'breadcrumbs', + reducers: { + crumbsLoad: (state, action: PayloadAction) => { + const path = action.payload; + state.crumbsByPath[path] = remoteItem(path, { isLoading: true }); + }, + crumbsLoaded: (state, action: PayloadAction<[string, BreadcrumbItem]>) => { + const [path, loadedItem] = action.payload; + state.crumbsByPath[path] = remoteItem(path, { + data: loadedItem, + loaded: new Date().toISOString(), + }); + }, + }, +}); + +export default breadcrumbsSlice; +export const { crumbsLoad, crumbsLoaded } = breadcrumbsSlice.actions; diff --git a/src/locale/da.yml b/src/locale/da.yml index 3afe297bd5..ce4d12aed6 100644 --- a/src/locale/da.yml +++ b/src/locale/da.yml @@ -11,6 +11,35 @@ core: du vil bruge. Du bliver omdirigeret til en ældre version af Zetkin, der understøtter den funktion. feat: + breadcrumbs: + elements: + activities: Aktiviteter + archive: Arkiv + areas: Områder + assignees: Ansvarlige + calendar: Kalender + callassignments: Ringeopgaver + callers: Ringere + campaigns: Projekter + closed: Lukket + conversation: Samtale + events: Begivenheder + folders: Lister + insights: Indsigter + instances: Forekomster + manage: Administrer + milestones: Milepæle + new: Ny + organize: Organiser + participants: Deltagere + people: Personer + projects: Projekter + questions: Spørgsmål + submissions: Besvarelser + surveys: Spørgeskema + tasks: Opgaver + untitledEvent: Unnavngiven begivenhed + views: Lister calendar: createMenu: singleEvent: Opret enkel begivenhed @@ -1605,34 +1634,6 @@ zui: accessList: added: Tilføjet af {sharer} {updated} removeAccess: Fjern adgang - breadcrumbs: - activities: Aktiviteter - archive: Arkiv - areas: Områder - assignees: Ansvarlige - calendar: Kalender - callassignments: Ringeopgaver - callers: Ringere - campaigns: Projekter - closed: Lukket - conversation: Samtale - events: Begivenheder - folders: Lister - insights: Indsigter - instances: Forekomster - manage: Administrer - milestones: Milepæle - new: Ny - organize: Organiser - participants: Deltagere - people: Personer - projects: Projekter - questions: Spørgsmål - submissions: Besvarelser - surveys: Spørgeskema - tasks: Opgaver - untitledEvent: Unnavngiven begivenhed - views: Lister collapse: collapse: Skjul expand: Vis mere diff --git a/src/locale/de.yml b/src/locale/de.yml index d3d6e400e1..1043ab1410 100644 --- a/src/locale/de.yml +++ b/src/locale/de.yml @@ -10,6 +10,34 @@ core: info: Du nutzt eine vorläufige Version von Zetkin, in der diese Funktion nicht verfügbar ist. Deswegen wirst du nun zur Vorgängerversion weitergeleitet. feat: + breadcrumbs: + elements: + activities: Aktivitäten + archive: Archiv + areas: Bereiche + assignees: Beauftragte + calendar: Kalender + callassignments: Telefonaktionen + callers: Telefonierer*innen + campaigns: Projekte + closed: Beendet + conversation: Gespräch + events: Veranstaltungen + folders: Listen + insights: Statistiken + manage: Verwalten + milestones: Meilensteine + new: Neu + organize: Organize + participants: Teilnehmende + people: Personen + projects: Projekte + questions: Fragen + submissions: Teilnahmen + surveys: Umfragen + tasks: Aufgaben + untitledEvent: Veranstaltung ohne Titel + views: Listen calendar: createMenu: singleEvent: Erstelle einzelne Veranstaltung @@ -1394,33 +1422,6 @@ glob: zui: accessList: removeAccess: Rechte beschränken - breadcrumbs: - activities: Aktivitäten - archive: Archiv - areas: Bereiche - assignees: Beauftragte - calendar: Kalender - callassignments: Telefonaktionen - callers: Telefonierer*innen - campaigns: Projekte - closed: Beendet - conversation: Gespräch - events: Veranstaltungen - folders: Listen - insights: Statistiken - manage: Verwalten - milestones: Meilensteine - new: Neu - organize: Organize - participants: Teilnehmende - people: Personen - projects: Projekte - questions: Fragen - submissions: Teilnahmen - surveys: Umfragen - tasks: Aufgaben - untitledEvent: Veranstaltung ohne Titel - views: Listen collapse: collapse: Ausblenden expand: Einblenden diff --git a/src/locale/en.yml b/src/locale/en.yml index 22e775cc7d..f6c6ca056d 100644 --- a/src/locale/en.yml +++ b/src/locale/en.yml @@ -12,6 +12,36 @@ core: feature you want to use. You are being redirected to the older version of Zetkin which supports that feature. feat: + breadcrumbs: + elements: + activities: Activities + archive: Archive + areas: Areas + assignees: Assignees + calendar: Calendar + callassignments: Call assignments + callers: Callers + campaigns: Projects + closed: Closed + conversation: Conversation + events: Events + folders: Views + insights: Insights + instances: Instances + journeys: Journeys + manage: Manage + milestones: Milestones + new: New + organize: Organize + participants: Participants + people: People + projects: Projects + questions: Questions + submissions: Submissions + surveys: Surveys + tasks: Tasks + untitledEvent: Untitled event + views: Views calendar: createMenu: singleEvent: Create single event @@ -1679,35 +1709,6 @@ zui: accessList: added: Added by {sharer} {updated} removeAccess: Remove access - breadcrumbs: - activities: Activities - archive: Archive - areas: Areas - assignees: Assignees - calendar: Calendar - callassignments: Call assignments - callers: Callers - campaigns: Projects - closed: Closed - conversation: Conversation - events: Events - folders: Views - insights: Insights - instances: Instances - journeys: Journeys - manage: Manage - milestones: Milestones - new: New - organize: Organize - participants: Participants - people: People - projects: Projects - questions: Questions - submissions: Submissions - surveys: Surveys - tasks: Tasks - untitledEvent: Untitled event - views: Views collapse: collapse: Collapse expand: Expand diff --git a/src/locale/nn.yml b/src/locale/nn.yml index 29186998d9..9c339b3686 100644 --- a/src/locale/nn.yml +++ b/src/locale/nn.yml @@ -11,6 +11,33 @@ core: vil bruke, du blir videresendt til den gamle versjonen av Zetkin som støtter denne funksjonen. feat: + breadcrumbs: + elements: + activities: Aktiviteter + archive: Arkiv + areas: Områder + assignees: Kontaktpersoner + calendar: Kalender + callassignments: Ringeoppdrag + callers: Ringere + campaigns: Prosjekter + closed: Stengt + conversation: Samtale + events: Arrangementer + folders: Lister + insights: Analyse + manage: Behandle + new: Ny + organize: Organiser + participants: Deltakere + people: Folk + projects: Prosjekter + questions: Spørsmål + submissions: Svar + surveys: Spørreundersøkelser + tasks: App-innhold + untitledEvent: Arrangement uten tittel + views: Lister calendar: createMenu: singleEvent: Nytt arrangement @@ -1546,32 +1573,6 @@ zui: accessList: added: Lagt til av {sharer} {updated} removeAccess: Fjern tilgang - breadcrumbs: - activities: Aktiviteter - archive: Arkiv - areas: Områder - assignees: Kontaktpersoner - calendar: Kalender - callassignments: Ringeoppdrag - callers: Ringere - campaigns: Prosjekter - closed: Stengt - conversation: Samtale - events: Arrangementer - folders: Lister - insights: Analyse - manage: Behandle - new: Ny - organize: Organiser - participants: Deltakere - people: Folk - projects: Prosjekter - questions: Spørsmål - submissions: Svar - surveys: Spørreundersøkelser - tasks: App-innhold - untitledEvent: Arrangement uten tittel - views: Lister collapse: collapse: Skjul expand: Vis diff --git a/src/locale/sv.yml b/src/locale/sv.yml index 181ad707f8..7e7cb61342 100644 --- a/src/locale/sv.yml +++ b/src/locale/sv.yml @@ -11,6 +11,36 @@ core: använda. Du omdirigeras nu till den äldre versionen av Zetkin som stödjer den funktionen. feat: + breadcrumbs: + elements: + activities: Aktiviteter + archive: Arkiv + areas: Areas + assignees: Ansvariga + calendar: Kalender + callassignments: Ringuppdrag + callers: Ringare + campaigns: Projekt + closed: Stängd + conversation: Samtal + events: Events + folders: Listor + insights: Insikter + instances: Exempel + journeys: Journeys + manage: Hantera + milestones: Milstolpar + new: Ny + organize: Organisera + participants: Deltagare + people: Människor + projects: Projekt + questions: Frågor + submissions: Enkätsvar + surveys: Enkäter + tasks: Uppgifter + untitledEvent: Event utan titel + views: Listor calendar: createMenu: singleEvent: Skapa ett enskild event @@ -1705,35 +1735,6 @@ zui: accessList: added: Tillagd av {sharer} {updated} removeAccess: Ta bort behörighet - breadcrumbs: - activities: Aktiviteter - archive: Arkiv - areas: Areas - assignees: Ansvariga - calendar: Kalender - callassignments: Ringuppdrag - callers: Ringare - campaigns: Projekt - closed: Stängd - conversation: Samtal - events: Events - folders: Listor - insights: Insikter - instances: Exempel - journeys: Journeys - manage: Hantera - milestones: Milstolpar - new: Ny - organize: Organisera - participants: Deltagare - people: Människor - projects: Projekt - questions: Frågor - submissions: Enkätsvar - surveys: Enkäter - tasks: Uppgifter - untitledEvent: Event utan titel - views: Listor collapse: collapse: Kollapsa expand: Expandera diff --git a/src/pages/api/breadcrumbs.ts b/src/pages/api/breadcrumbs.ts index 7028ee7e24..6c3040ef83 100644 --- a/src/pages/api/breadcrumbs.ts +++ b/src/pages/api/breadcrumbs.ts @@ -63,7 +63,7 @@ const breadcrumbs = async ( }); } } - res.status(200).json({ breadcrumbs }); + res.status(200).json({ data: breadcrumbs }); } else { return res.status(400).json({ error }); } diff --git a/src/utils/fetching/getBreadcrumbs.ts b/src/utils/fetching/getBreadcrumbs.ts deleted file mode 100644 index f4017ea73d..0000000000 --- a/src/utils/fetching/getBreadcrumbs.ts +++ /dev/null @@ -1,19 +0,0 @@ -import APIError from 'utils/apiError'; -import { Breadcrumb } from '../types'; -import defaultFetch from './defaultFetch'; - -export default function getBreadcrumbs( - pathname: string, - queryString: string, - fetch = defaultFetch -) { - return async (): Promise => { - const url = `/breadcrumbs?pathname=${pathname}&${queryString}`; - const bRes = await fetch(url); - if (!bRes.ok) { - throw new APIError('GET', url); - } - const bData = await bRes.json(); - return bData.breadcrumbs; - }; -} diff --git a/src/zui/ZUIHeader.tsx b/src/zui/ZUIHeader.tsx index 47b2f3865e..7224e6243c 100644 --- a/src/zui/ZUIHeader.tsx +++ b/src/zui/ZUIHeader.tsx @@ -10,8 +10,8 @@ import { Typography, } from '@mui/material'; +import BreadcrumbTrail from 'features/breadcrumbs/components/BreadcrumbTrail'; import { Msg } from 'core/i18n'; -import ZUIBreadcrumbTrail from 'zui/ZUIBreadcrumbTrail'; import ZUIEllipsisMenu, { ZUIEllipsisMenuProps } from 'zui/ZUIEllipsisMenu'; import messageIds from './l10n/messageIds'; @@ -84,7 +84,7 @@ const Header: React.FC = ({ - + {/* Search and collapse buttons */} {!!onToggleCollapsed && ( diff --git a/src/zui/l10n/messageIds.ts b/src/zui/l10n/messageIds.ts index 8348171caf..7b95615493 100644 --- a/src/zui/l10n/messageIds.ts +++ b/src/zui/l10n/messageIds.ts @@ -8,36 +8,6 @@ export default makeMessages('zui', { ), removeAccess: m('Remove access'), }, - breadcrumbs: { - activities: m('Activities'), - archive: m('Archive'), - areas: m('Areas'), - assignees: m('Assignees'), - calendar: m('Calendar'), - callassignments: m('Call assignments'), - callers: m('Callers'), - campaigns: m('Projects'), - closed: m('Closed'), - conversation: m('Conversation'), - events: m('Events'), - folders: m('Lists'), - insights: m('Insights'), - instances: m('Instances'), - journeys: m('Journeys'), - manage: m('Manage'), - milestones: m('Milestones'), - new: m('New'), - organize: m('Organize'), - participants: m('Participants'), - people: m('People'), - projects: m('Projects'), - questions: m('Questions'), - submissions: m('Submissions'), - surveys: m('Surveys'), - tasks: m('Tasks'), - untitledEvent: m('Untitled event'), - views: m('Lists'), - }, collapse: { collapse: m('Collapse'), expand: m('Expand'),