diff --git a/src/group/components/GroupView.js b/src/group/components/GroupView.js
index adc0088b8..d1ff24cd6 100644
--- a/src/group/components/GroupView.js
+++ b/src/group/components/GroupView.js
@@ -1,4 +1,4 @@
-import React, { useCallback, useEffect } from 'react';
+import React, { useCallback, useEffect, useMemo } from 'react';
import _ from 'lodash/fp';
import { useTranslation } from 'react-i18next';
@@ -24,6 +24,7 @@ import PageContainer from 'layout/PageContainer';
import PageHeader from 'layout/PageHeader';
import PageLoader from 'layout/PageLoader';
import { useRefreshProgressContext } from 'layout/RefreshProgressProvider';
+import { useBreadcrumbsParams } from 'navigation/breadcrumbsContext';
import Restricted from 'permissions/components/Restricted';
import { GroupContextProvider } from 'group/groupContext';
@@ -142,6 +143,13 @@ const GroupView = () => {
fetching
);
+ useBreadcrumbsParams(
+ useMemo(
+ () => ({ groupName: group?.name, loading: !group?.name }),
+ [group?.name]
+ )
+ );
+
useEffect(() => {
dispatch(fetchGroupView(slug));
}, [dispatch, slug]);
diff --git a/src/group/membership/components/GroupMembers.js b/src/group/membership/components/GroupMembers.js
index 51b98b268..acc34a5a0 100644
--- a/src/group/membership/components/GroupMembers.js
+++ b/src/group/membership/components/GroupMembers.js
@@ -1,4 +1,4 @@
-import React, { useEffect } from 'react';
+import React, { useEffect, useMemo } from 'react';
import _ from 'lodash/fp';
import { usePermission } from 'permissions';
@@ -12,6 +12,7 @@ import { useDocumentTitle } from 'common/document';
import PageContainer from 'layout/PageContainer';
import PageHeader from 'layout/PageHeader';
import PageLoader from 'layout/PageLoader';
+import { useBreadcrumbsParams } from 'navigation/breadcrumbsContext';
import { GroupContextProvider } from 'group/groupContext';
import { fetchGroupForMembers } from 'group/groupSlice';
@@ -40,6 +41,13 @@ const Header = () => {
fetching
);
+ useBreadcrumbsParams(
+ useMemo(
+ () => ({ groupName: group?.name, loading: !group?.name }),
+ [group?.name]
+ )
+ );
+
const { loading: loadingPermissions, allowed } = usePermission(
'group.manageMembers',
group
diff --git a/src/index.js b/src/index.js
index cf400b7de..c2b4888c0 100644
--- a/src/index.js
+++ b/src/index.js
@@ -17,6 +17,8 @@ import theme from 'theme';
import 'index.css';
+import Breadcrumbs from 'navigation/Breadcrumbs';
+
const App = () => {
const contentRef = useRef();
const navigationRef = useRef();
@@ -43,6 +45,7 @@ const App = () => {
flex: 1,
}}
>
+
diff --git a/src/landscape/components/LandscapeView.js b/src/landscape/components/LandscapeView.js
index dd18b98df..08c319115 100644
--- a/src/landscape/components/LandscapeView.js
+++ b/src/landscape/components/LandscapeView.js
@@ -1,4 +1,4 @@
-import React, { useCallback, useEffect } from 'react';
+import React, { useCallback, useEffect, useMemo } from 'react';
import _ from 'lodash/fp';
import { Trans, useTranslation } from 'react-i18next';
@@ -27,6 +27,7 @@ import PageContainer from 'layout/PageContainer';
import PageHeader from 'layout/PageHeader';
import PageLoader from 'layout/PageLoader';
import { useRefreshProgressContext } from 'layout/RefreshProgressProvider';
+import { useBreadcrumbsParams } from 'navigation/breadcrumbsContext';
import Restricted from 'permissions/components/Restricted';
import { GroupContextProvider } from 'group/groupContext';
@@ -118,6 +119,13 @@ const LandscapeView = () => {
fetching
);
+ useBreadcrumbsParams(
+ useMemo(
+ () => ({ landscapeName: landscape?.name, loading: !landscape?.name }),
+ [landscape?.name]
+ )
+ );
+
useEffect(() => {
dispatch(fetchLandscapeView(slug));
}, [dispatch, slug]);
diff --git a/src/landscape/membership/components/LandscapeMembers.js b/src/landscape/membership/components/LandscapeMembers.js
index 0fe65e051..61e122a12 100644
--- a/src/landscape/membership/components/LandscapeMembers.js
+++ b/src/landscape/membership/components/LandscapeMembers.js
@@ -1,4 +1,4 @@
-import React, { useEffect } from 'react';
+import React, { useEffect, useMemo } from 'react';
import _ from 'lodash/fp';
import { usePermission } from 'permissions';
@@ -12,6 +12,7 @@ import { useDocumentTitle } from 'common/document';
import PageContainer from 'layout/PageContainer';
import PageHeader from 'layout/PageHeader';
import PageLoader from 'layout/PageLoader';
+import { useBreadcrumbsParams } from 'navigation/breadcrumbsContext';
import { GroupContextProvider } from 'group/groupContext';
import GroupMembersList from 'group/membership/components/GroupMembersList';
@@ -42,6 +43,13 @@ const Header = ({ landscape, fetching }) => {
fetching
);
+ useBreadcrumbsParams(
+ useMemo(
+ () => ({ landscapeName: landscape?.name, loading: !landscape?.name }),
+ [landscape?.name]
+ )
+ );
+
if (loadingPermissions) {
return null;
}
diff --git a/src/layout/AppWrappers.js b/src/layout/AppWrappers.js
index dfc1951df..09798f857 100644
--- a/src/layout/AppWrappers.js
+++ b/src/layout/AppWrappers.js
@@ -16,10 +16,12 @@ import 'forms/yup';
// Analytics
import 'monitoring/analytics';
+import { BreadcrumbsContextProvider } from 'navigation/breadcrumbsContext';
+
import RefreshProgressProvider from './RefreshProgressProvider';
// Wrappers
-// Router, Theme, Global State, Permissions, Notifications
+// Router, Theme, Global State, Permissions, Notifications, Breadcrumbs
const AppWrappers = ({ children, theme, store, permissionsRules }) => (
@@ -28,7 +30,11 @@ const AppWrappers = ({ children, theme, store, permissionsRules }) => (
- {children}
+
+
+ {children}
+
+
diff --git a/src/localization/locales/en-US.json b/src/localization/locales/en-US.json
index 3aeb4098c..525db48ce 100644
--- a/src/localization/locales/en-US.json
+++ b/src/localization/locales/en-US.json
@@ -119,7 +119,9 @@
"members_list_pending_confirmation_message": "Do you want to deny {{userName}}’s request for membership in {{name}}?",
"members_list_pending_confirmation_button": "Deny Request",
"members_list_pending_reject": "Deny",
- "home_pending_message": "Waiting for the group manager’s approval"
+ "home_pending_message": "Waiting for the group manager’s approval",
+ "breadcrumbs_view": "{{groupName}}",
+ "breadcrumbs_members": "Members"
},
"tool": {
"requirements": "System requirements",
@@ -191,7 +193,8 @@
"nav_label": "Main navigation",
"nav_label_short": "Main",
"skip_to_main_content": "Skip to main content",
- "skip_to_main_navigation": "Skip to main navigation"
+ "skip_to_main_navigation": "Skip to main navigation",
+ "breadcrumbs_label": "Breadcrumbs"
},
"landscape": {
"add": "Add a Landscape",
@@ -292,7 +295,9 @@
"list_map_help": "<0>Can’t find your landscape on this map? <1>Add it to Terraso1> or <3>learn more about landscapes3>.0>",
"list_map_help_url": "https://terraso.org/help/add-a-new-landscape-to-terraso/",
"list_map_section_label": "Landscapes map",
- "list_map_popup_link": "View details about {{name}}"
+ "list_map_popup_link": "View details about {{name}}",
+ "breadcrumbs_view": "{{landscapeName}}",
+ "breadcrumbs_members": "Members"
},
"sharedData": {
"title": "Shared files",
diff --git a/src/localization/locales/es-ES.json b/src/localization/locales/es-ES.json
index 34ef9b665..c3c2da57b 100644
--- a/src/localization/locales/es-ES.json
+++ b/src/localization/locales/es-ES.json
@@ -119,7 +119,9 @@
"members_list_pending_confirmation_message": "¿Quieres denegar la solicitud de membresía de {{userName}} en {{name}}?",
"members_list_pending_confirmation_button": "Denegar solicitud",
"members_list_pending_reject": "Denegar",
- "home_pending_message": "Esperando la aprobación del administrador del grupo"
+ "home_pending_message": "Esperando la aprobación del administrador del grupo",
+ "breadcrumbs_view": "{{groupName}}",
+ "breadcrumbs_members": "Miembros"
},
"tool": {
"avilability": "Disponibilidad",
@@ -191,7 +193,8 @@
"nav_label": "Navegación principal",
"nav_label_short": "Principal",
"skip_to_main_content": "Saltar al contenido principal",
- "skip_to_main_navigation": "Saltar a la navegación principal"
+ "skip_to_main_navigation": "Saltar a la navegación principal",
+ "breadcrumbs_label": "Árbol de navegación"
},
"landscape": {
"add": "Agregar un paisaje",
@@ -293,7 +296,9 @@
"list_map_help": "<0>¿No puedes encontrar tu paisaje en este mapa? <1>Añádelo a Terraso1> o <3>aprende más sobre paisajes3>.0>",
"list_map_help_url": "https://terraso.org/es/ayuda/anade-un-nuevo-paisaje-a-terraso/",
"list_map_section_label": "Mapa de paisajes",
- "list_map_popup_link": "Ver detalles sobre {{name}}"
+ "list_map_popup_link": "Ver detalles sobre {{name}}",
+ "breadcrumbs_view": "{{landscapeName}}",
+ "breadcrumbs_members": "Miembros"
},
"common": {
"dialog_cancel_label": "Cancelar",
diff --git a/src/navigation/Breadcrumbs.js b/src/navigation/Breadcrumbs.js
new file mode 100644
index 000000000..626bc1f1d
--- /dev/null
+++ b/src/navigation/Breadcrumbs.js
@@ -0,0 +1,62 @@
+import React from 'react';
+
+import _ from 'lodash/fp';
+import { useTranslation } from 'react-i18next';
+import { Link as RouterLink } from 'react-router-dom';
+
+import {
+ Container,
+ Link,
+ Breadcrumbs as MuiBreadcrumbs,
+ Typography,
+} from '@mui/material';
+import { visuallyHidden } from '@mui/utils';
+
+import { useBreadcrumbs } from './Routes';
+import { useBreadcrumbsContext } from './breadcrumbsContext';
+
+const Breadcrumbs = () => {
+ const { t } = useTranslation();
+ const breadcrumbs = useBreadcrumbs();
+ const { breadcrumbsParams } = useBreadcrumbsContext();
+ const { loading = true } = breadcrumbsParams;
+
+ if (loading || _.isEmpty(breadcrumbs)) {
+ return null;
+ }
+ return (
+ <>
+
+ {t('navigation.breadcrumbs_label')}
+
+
+
+ {t('home.title')}
+
+ {breadcrumbs.map(({ to, label, current }) => (
+
+ {t(label, breadcrumbsParams)}
+
+ ))}
+
+ >
+ );
+};
+
+export default Breadcrumbs;
diff --git a/src/navigation/Breadcrumbs.test.js b/src/navigation/Breadcrumbs.test.js
new file mode 100644
index 000000000..0e84e9453
--- /dev/null
+++ b/src/navigation/Breadcrumbs.test.js
@@ -0,0 +1,62 @@
+import { render, screen } from 'tests/utils';
+
+import React, { useMemo } from 'react';
+
+import { useLocation } from 'react-router-dom';
+
+import Breadcrumbs from './Breadcrumbs';
+import { useBreadcrumbsParams } from './breadcrumbsContext';
+
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useLocation: jest.fn(),
+}));
+
+const TestComponent = () => {
+ useBreadcrumbsParams(
+ useMemo(() => ({ groupName: 'Group Name', loading: false }), [])
+ );
+ return ;
+};
+
+const setup = async () => {
+ await render();
+};
+test('Breadcrumbs: Dont Show items', async () => {
+ useLocation.mockReturnValue({
+ pathname: '/groups',
+ });
+ await setup();
+ expect(
+ screen.queryByRole('navigation', { name: 'Breadcrumbs' })
+ ).not.toBeInTheDocument();
+});
+test('Breadcrumbs: Show items', async () => {
+ useLocation.mockReturnValue({
+ pathname: '/groups/group-1/members',
+ });
+ await setup();
+ expect(
+ screen.getByRole('navigation', { name: 'Breadcrumbs' })
+ ).toBeInTheDocument();
+ expect(screen.getByRole('link', { name: 'Home' })).toHaveAttribute(
+ 'href',
+ '/'
+ );
+ expect(screen.getByRole('link', { name: 'Groups' })).toHaveAttribute(
+ 'href',
+ '/groups'
+ );
+ expect(screen.getByRole('link', { name: 'Group Name' })).toHaveAttribute(
+ 'href',
+ '/groups/group-1'
+ );
+ expect(screen.getByRole('link', { name: 'Members' })).toHaveAttribute(
+ 'href',
+ '/groups/group-1/members'
+ );
+ expect(screen.getByRole('link', { name: 'Members' })).toHaveAttribute(
+ 'aria-current',
+ 'page'
+ );
+});
diff --git a/src/navigation/Routes.js b/src/navigation/Routes.js
index 9a5c75926..7e51d3798 100644
--- a/src/navigation/Routes.js
+++ b/src/navigation/Routes.js
@@ -1,6 +1,6 @@
-import React from 'react';
+import React, { useMemo } from 'react';
-import { Route, Routes } from 'react-router-dom';
+import { Route, Routes, matchPath, useLocation } from 'react-router-dom';
import NotFound from 'layout/NotFound';
@@ -23,34 +23,98 @@ import LandscapeView from 'landscape/components/LandscapeView';
import LandscapeMembers from 'landscape/membership/components/LandscapeMembers';
import ToolsList from 'tool/components/ToolList';
-const path = (path, Component, auth = true) => ({
+const path = (
+ path,
+ Component,
+ { auth = true, showBreadcrumbs = false, breadcrumbsLabel } = {}
+) => ({
path,
Component,
auth,
+ showBreadcrumbs,
+ breadcrumbsLabel,
});
const paths = [
path('/', Home),
- path('/groups', GroupList),
+ path('/groups', GroupList, {
+ breadcrumbsLabel: 'group.home_title',
+ }),
+ path('/groups/:slug', GroupView, {
+ showBreadcrumbs: true,
+ breadcrumbsLabel: 'group.breadcrumbs_view',
+ }),
path('/groups/new', GroupForm),
path('/groups/:slug/edit', GroupForm),
- path('/groups/:slug/members', GroupMembers),
+ path('/groups/:slug/members', GroupMembers, {
+ showBreadcrumbs: true,
+ breadcrumbsLabel: 'group.breadcrumbs_members',
+ }),
path('/groups/:slug/upload', GroupSharedDataUpload),
- path('/groups/:slug', GroupView),
- path('/landscapes', LandscapeList),
+ path('/landscapes', LandscapeList, {
+ breadcrumbsLabel: 'landscape.home_title',
+ }),
path('/landscapes/new', LandscapeNew),
+ path('/landscapes/:slug', LandscapeView, {
+ showBreadcrumbs: true,
+ breadcrumbsLabel: 'landscape.breadcrumbs_view',
+ }),
path('/landscapes/:slug/edit', LandscapeUpdateProfile),
path('/landscapes/:slug/boundaries', LandscapeBoundariesUpdate),
- path('/landscapes/:slug/members', LandscapeMembers),
+ path('/landscapes/:slug/members', LandscapeMembers, {
+ showBreadcrumbs: true,
+ breadcrumbsLabel: 'landscape.breadcrumbs_members',
+ }),
path('/landscapes/:slug/upload', LandscapeSharedDataUpload),
- path('/landscapes/:slug', LandscapeView),
path('/tools', ToolsList),
- path('/account', AccountLogin, false),
+ path('/account', AccountLogin, { auth: false }),
path('/account/profile', AccountProfile),
path('/contact', ContactForm),
path('*', NotFound),
];
+const getPath = to => paths.find(path => matchPath({ path: path.path }, to));
+
+export const useBreadcrumbs = () => {
+ const { pathname: currentPathname } = useLocation();
+ const pathnames = useMemo(
+ () => currentPathname.split('/'),
+ [currentPathname]
+ );
+ const currentPath = useMemo(
+ () => getPath(currentPathname),
+ [currentPathname]
+ );
+ const items = useMemo(() => {
+ if (!currentPath.showBreadcrumbs) {
+ return null;
+ }
+ return pathnames
+ .map((item, index) => {
+ const to = `/${pathnames.slice(1, index + 1).join('/')}`;
+ if (to === currentPathname) {
+ return {
+ to,
+ label: currentPath.breadcrumbsLabel,
+ current: true,
+ };
+ }
+ const path = getPath(to);
+ if (!path || !path.breadcrumbsLabel) {
+ return null;
+ }
+
+ return {
+ to,
+ label: path.breadcrumbsLabel,
+ };
+ })
+ .filter(pathname => pathname);
+ }, [pathnames, currentPath, currentPathname]);
+
+ return items;
+};
+
const RoutesComponent = () => (
{paths.map(({ path, Component, auth }) => (
diff --git a/src/navigation/breadcrumbsContext.js b/src/navigation/breadcrumbsContext.js
new file mode 100644
index 000000000..f50353c8e
--- /dev/null
+++ b/src/navigation/breadcrumbsContext.js
@@ -0,0 +1,22 @@
+import React, { useContext, useEffect, useState } from 'react';
+
+const BreadcrumbsContext = React.createContext();
+
+export const BreadcrumbsContextProvider = props => {
+ const [breadcrumbsParams, setBreadcrumbsParams] = useState({});
+
+ return (
+
+ {props.children}
+
+ );
+};
+
+export const useBreadcrumbsContext = () => useContext(BreadcrumbsContext);
+
+export const useBreadcrumbsParams = params => {
+ const { setBreadcrumbsParams } = useBreadcrumbsContext();
+ useEffect(() => setBreadcrumbsParams(params), [params, setBreadcrumbsParams]);
+};