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

Use hooks and redux for breadcrumbs #1601

Merged
Merged
Show file tree
Hide file tree
Changes from 4 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
5 changes: 5 additions & 0 deletions src/core/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import {
createListenerMiddleware,
} from '@reduxjs/toolkit';

import breadcrumbsSlice, {
BreadcrumbsStoreSlice,
} from 'features/breadcrumbs/store';
import callAssignmentsSlice, {
callAssignmentCreated,
CallAssignmentSlice,
Expand Down Expand Up @@ -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;
Expand All @@ -51,6 +55,7 @@ export interface RootState {
}

const reducer = {
breadcrumbs: breadcrumbsSlice.reducer,
callAssignments: callAssignmentsSlice.reducer,
campaigns: campaignsSlice.reducer,
events: eventsSlice.reducer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, { highlight?: boolean }>((theme) =>
createStyles({
Expand Down Expand Up @@ -48,40 +37,30 @@ const useStyles = makeStyles<Theme, { highlight?: boolean }>((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 <div />;
}

const getLabel = (crumb: Breadcrumb) => {
if (crumb.labelMsg) {
const msgId = validMessageId(crumb.labelMsg);
if (msgId) {
return <Msg id={messageIds.breadcrumbs[msgId]} />;
return <Msg id={messageIds.elements[msgId]} />;
}
}

Expand All @@ -97,8 +76,8 @@ const ZUIBreadcrumbTrail = ({
maxItems={smallScreen ? 2 : mediumScreen ? 4 : largeScreen ? 6 : 10}
separator={<NavigateNextIcon fontSize="small" />}
>
{breadcrumbsQuery.data.map((crumb, index) => {
if (index < breadcrumbsQuery.data.length - 1) {
{breadcrumbs.map((crumb, index) => {
if (index < breadcrumbs.length - 1) {
return (
<NextLink key={crumb.href} href={crumb.href} passHref>
<Link
Expand Down Expand Up @@ -126,4 +105,4 @@ const ZUIBreadcrumbTrail = ({
);
};

export default ZUIBreadcrumbTrail;
export default BreadcrumbTrail;
38 changes: 38 additions & 0 deletions src/features/breadcrumbs/hooks/useBreadcrumbs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { BreadcrumbElement } from 'pages/api/breadcrumbs';
import { loadItemIfNecessary } from 'core/caching/cacheUtils';
import { crumbsLoad, crumbsLoaded } from '../store';
import { NextRouter, useRouter } from 'next/router';
import { useApiClient, useAppDispatch, useAppSelector } from 'core/hooks';

export default function useBreadcrumbElements() {
const apiClient = useApiClient();
const dispatch = useAppDispatch();
const router = useRouter();
const path = router.pathname;
const item = useAppSelector((state) => state.breadcrumbs.crumbsByPath[path]);
richardolsson marked this conversation as resolved.
Show resolved Hide resolved

const query = getQueryString(router);

const future = loadItemIfNecessary(item, dispatch, {
actionOnLoad: () => crumbsLoad(path),
actionOnSuccess: (item) => crumbsLoaded([path, item]),
loader: async () => {
const elements = await apiClient.get<BreadcrumbElement[]>(
`/api/breadcrumbs?pathname=${path}&${query}`
);

return { elements, id: path };
richardolsson marked this conversation as resolved.
Show resolved Hide resolved
},
});

return future.data?.elements ?? [];
}

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_*)
richardolsson marked this conversation as resolved.
Show resolved Hide resolved
return Object.entries(router.query)
.filter(([key]) => router.pathname.includes(`[${key}]`))
.map(([key, val]) => `${key}=${val}`)
.join('&');
};
34 changes: 34 additions & 0 deletions src/features/breadcrumbs/l10n/messageIds.ts
Original file line number Diff line number Diff line change
@@ -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'),
},
});
50 changes: 50 additions & 0 deletions src/features/breadcrumbs/store.ts
Original file line number Diff line number Diff line change
@@ -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<string, RemoteItem<BreadcrumbItem>>;
}

const initialState: BreadcrumbsStoreSlice = {
crumbsByPath: {},
};

function isUpdatedAction(action: Action<string>) {
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.
richardolsson marked this conversation as resolved.
Show resolved Hide resolved
Object.keys(state.crumbsByPath).forEach((path) => {
state.crumbsByPath[path].isStale = true;
});
});
},
initialState,
name: 'breadcrumbs',
reducers: {
crumbsLoad: (state, action: PayloadAction<string>) => {
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<BreadcrumbItem>(path, {
data: loadedItem,
loaded: new Date().toISOString(),
});
},
},
});

export default breadcrumbsSlice;
export const { crumbsLoad, crumbsLoaded } = breadcrumbsSlice.actions;
57 changes: 29 additions & 28 deletions src/locale/da.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
55 changes: 28 additions & 27 deletions src/locale/de.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading