Skip to content

Commit

Permalink
refactor(zimbra): tabs & breadcrumb handling
Browse files Browse the repository at this point in the history
ref: MANAGER-16089

Signed-off-by: Tristan WAGNER <[email protected]>
  • Loading branch information
tristanwagner committed Dec 2, 2024
1 parent d4a9216 commit ea823d6
Show file tree
Hide file tree
Showing 11 changed files with 287 additions and 251 deletions.
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import React, { useMemo } from 'react';
import {
useParams,
useSearchParams,
createSearchParams,
useLocation,
useMatches,
} from 'react-router-dom';
import {
OdsBreadcrumb,
OdsBreadcrumbItem,
} from '@ovhcloud/ods-components/react';
import { useTranslation } from 'react-i18next';
import { ODS_LINK_COLOR } from '@ovhcloud/ods-components';
import { urls } from '@/routes/routes.constants';
import { useGenerateUrl, useOrganization } from '@/hooks';
import { useOrganization } from '@/hooks';

export type BreadcrumbProps = {
items?: { label: string; href?: string }[];
Expand All @@ -21,64 +20,41 @@ export type BreadcrumbProps = {

const whiteListedSearchParams = ['organizationId'];

export const Breadcrumb: React.FC<BreadcrumbProps> = ({
items = [],
overviewUrl,
}) => {
const { serviceName } = useParams();
export const Breadcrumb: React.FC<BreadcrumbProps> = () => {
const { t } = useTranslation('dashboard');
const location = useLocation();
const [searchParams] = useSearchParams();
const params = useMemo(() => {
const matches = useMatches();

const queryParams = useMemo(() => {
return Array.from(searchParams.entries()).filter(([key]) =>
whiteListedSearchParams.includes(key),
);
}, [searchParams]);

const search = useMemo(
() => (params.length ? `?${createSearchParams(params)}` : ''),
[params],
() => (queryParams.length ? `?${createSearchParams(queryParams)}` : ''),
[queryParams],
);
const { data: organization, isLoading } = useOrganization();

const rootUrl = serviceName
? '#/:serviceName'.replace(':serviceName', serviceName)
: '#/';

const overviewUrlValue = useGenerateUrl(
overviewUrl ||
(serviceName ? urls.overview.replace(':serviceName', serviceName) : '/'),
'href',
);

const breadcrumbItems = useMemo(() => {
const pathParts = location.pathname.split('/').filter(Boolean);
const breadcrumbParts = pathParts.slice(1);

return [
{
label: t('zimbra_dashboard_title'),
href: rootUrl,
},
...(organization && !isLoading
? [
{
label: organization?.currentState.name,
href: overviewUrlValue,
},
]
: []),
...breadcrumbParts.map((_, index) => {
const url = `#/${pathParts.slice(0, index + 2).join('/')}${search}`;
const label = t(
`zimbra_dashboard_${breadcrumbParts.slice(0, index + 1).join('_')}`,
);
return {
label,
href: url,
};
}),
...items,
].filter(Boolean);
return matches.slice(1).reduce((allParts, curr, index, array) => {
if (index === 1 && organization && !isLoading) {
allParts.push({
label: organization.currentState.name,
href: `#${array[0].pathname}${search}`,
});
}
allParts.push({
label: t(
(curr.handle as Record<string, string>)?.breadcrumbLabel ||
'to_be_defined',
),
href: `#${curr.pathname}${index === 0 ? '' : search}`,
});
return allParts;
}, []);
}, [location, organization]);

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, { useContext } from 'react';
import React, { useContext, useMemo } from 'react';
import {
Outlet,
useResolvedPath,
useLocation,
useParams,
useNavigate,
useSearchParams,
} from 'react-router-dom';

import {
Expand All @@ -19,19 +20,22 @@ import { useTranslation } from 'react-i18next';
import { ShellContext } from '@ovh-ux/manager-react-shell-client';
import { OdsTag } from '@ovhcloud/ods-components/react';
import { ODS_TAG_COLOR, ODS_TAG_SIZE } from '@ovhcloud/ods-components';
import TabsPanel, { TabItemProps } from './TabsPanel';
import TabsPanel, { computePathMatchers, TabItemProps } from './TabsPanel';
import Breadcrumb from '@/components/Breadcrumb/Breadcrumb';
import { GUIDES_LIST } from '@/guides.constants';
import { urls } from '@/routes/routes.constants';

import './Dashboard.scss';
import { FEATURE_FLAGS } from '@/utils';
import { useOrganization } from '@/hooks';
import { useGenerateUrl, useOrganization, useOverridePage } from '@/hooks';

const whiteListedSearchParams = ['organizationId'];

export const Dashboard: React.FC = () => {
const { platformId } = useParams();
const { notifications } = useNotifications();
const { data: organization } = useOrganization();
const isOverridePage = useOverridePage();
const navigate = useNavigate();
const { t } = useTranslation('dashboard');
const context = useContext(ShellContext);
Expand All @@ -49,73 +53,78 @@ export const Dashboard: React.FC = () => {
},
];

const params = new URLSearchParams(location.search);
const selectedOrganizationId = params.get('organizationId');
function computePathMatchers(routes: string[]) {
return routes.map(
(path) => new RegExp(path.replace(':serviceName', platformId)),
const [searchParams] = useSearchParams();
const selectedOrganizationId = searchParams.get('organizationId');
const params = useMemo(() => {
return Object.fromEntries(
Array.from(searchParams.entries()).filter(([key]) =>
whiteListedSearchParams.includes(key),
),
);
}
}, [searchParams]);

const tabsList: TabItemProps[] = [
{
name: 'general_informations',
title: t('zimbra_dashboard_general_informations'),
to: basePath,
pathMatchers: computePathMatchers([urls.dashboard]),
to: useGenerateUrl(basePath, 'path', params),
pathMatchers: computePathMatchers([urls.dashboard], platformId),
},
{
name: 'organizations',
title: t('zimbra_dashboard_organizations'),
to: `${basePath}/organizations`,
pathMatchers: computePathMatchers([
urls.organizations,
urls.organizationsDelete,
]),
to: useGenerateUrl(`${basePath}/organizations`, 'path', params),
pathMatchers: computePathMatchers(
[urls.organizations, urls.organizationsDelete],
platformId,
),
hidden: selectedOrganizationId !== null,
},
{
name: 'domains',
title: t('zimbra_dashboard_domains'),
to: `${basePath}/domains`,
pathMatchers: computePathMatchers([
urls.domains,
urls.domainsEdit,
urls.domainsDelete,
urls.domains_diagnostic,
]),
to: useGenerateUrl(`${basePath}/domains`, 'path', params),
pathMatchers: computePathMatchers(
[
urls.domains,
urls.domainsEdit,
urls.domainsDelete,
urls.domains_diagnostic,
],
platformId,
),
},
{
name: 'email_accounts',
title: t('zimbra_dashboard_email_accounts'),
to: `${basePath}/email_accounts`,
pathMatchers: computePathMatchers([urls.email_accounts]),
to: useGenerateUrl(`${basePath}/email_accounts`, 'path', params),
pathMatchers: computePathMatchers([urls.email_accounts], platformId),
},
{
name: 'mailing_lists',
title: t('zimbra_dashboard_mailing_lists'),
to: `${basePath}/mailing_lists`,
pathMatchers: computePathMatchers([
urls.mailing_lists,
urls.mailing_lists_delete,
]),
to: useGenerateUrl(`${basePath}/mailing_lists`, 'path', params),
pathMatchers: computePathMatchers(
[urls.mailing_lists, urls.mailing_lists_delete],
platformId,
),
hidden: !FEATURE_FLAGS.MAILINGLISTS,
},
{
name: 'redirections',
title: t('zimbra_dashboard_redirections'),
to: `${basePath}/redirections`,
pathMatchers: computePathMatchers([
urls.redirections,
urls.redirections_delete,
urls.redirections_edit,
]),
to: useGenerateUrl(`${basePath}/redirections`, 'path', params),
pathMatchers: computePathMatchers(
[urls.redirections, urls.redirections_delete, urls.redirections_edit],
platformId,
),
hidden: !FEATURE_FLAGS.REDIRECTIONS,
},
{
name: 'auto_replies',
title: t('zimbra_dashboard_auto_replies'),
to: `${basePath}/auto_replies`,
pathMatchers: computePathMatchers([urls.auto_replies]),
to: useGenerateUrl(`${basePath}/auto_replies`, 'path', params),
pathMatchers: computePathMatchers([urls.auto_replies], platformId),
hidden: !FEATURE_FLAGS.AUTOREPLIES,
},
];
Expand Down Expand Up @@ -146,7 +155,7 @@ export const Dashboard: React.FC = () => {
// temporary fix margin even if empty
notifications.length ? <Notifications /> : null
}
tabs={<TabsPanel tabs={tabsList} />}
tabs={isOverridePage ? null : <TabsPanel tabs={tabsList} />}
>
<Outlet />
</BaseLayout>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,75 +1,79 @@
import React, { useState, useEffect } from 'react';
import { NavLink, useLocation, useNavigate } from 'react-router-dom';
import React, { useState, useEffect, ReactNode } from 'react';
import { Location, NavLink, useLocation, useNavigate } from 'react-router-dom';
import { OdsTabs, OdsTab } from '@ovhcloud/ods-components/react';
import { useOverridePage, useOrganization } from '@/hooks';

export type TabItemProps = {
name: string;
title: string;
title: ReactNode;
pathMatchers?: RegExp[];
to: string;
hidden?: boolean;
isDisabled?: boolean;
};

export type TabsProps = {
tabs: TabItemProps[];
};

export const activatedTabs = (pathMatchers: RegExp[], location: Location) => {
return pathMatchers?.some((pathMatcher) =>
pathMatcher.test(location.pathname),
);
};

export const computePathMatchers = (routes: string[], platformId: string) => {
return routes.map(
(path) => new RegExp(path.replace(':serviceName', platformId)),
);
};

const TabsPanel: React.FC<TabsProps> = ({ tabs }) => {
const [activePanel, setActivePanel] = useState('');
const location = useLocation();
const navigate = useNavigate();
const { data: organization } = useOrganization();

const isOverriddedPage = useOverridePage();

useEffect(() => {
if (!location.pathname) {
setActivePanel(tabs[0].name);
navigate(tabs[0].to);
} else {
const activeTab = tabs.find(
(tab) =>
tab.to === location.pathname ||
const activeTab = tabs.find((tab) => {
const [pathname] = tab.to.split('?');
return (
pathname === location.pathname ||
tab.pathMatchers?.some((pathMatcher) =>
pathMatcher.test(location.pathname),
),
);
)
);
});
if (activeTab) {
setActivePanel(activeTab.name);
}
}
}, [location.pathname]);

return (
<>
{!isOverriddedPage && (
<OdsTabs>
{tabs.map(
(tab: TabItemProps) =>
!tab.hidden && (
<NavLink
key={`osds-tab-bar-item-${tab.name}`}
to={
organization?.id
? `${tab.to}?organizationId=${organization?.id}`
: tab.to
}
className="no-underline"
>
<OdsTab
id={tab.name}
role="tab"
isSelected={activePanel === tab.name}
>
{tab.title}
</OdsTab>
</NavLink>
),
)}
</OdsTabs>
<OdsTabs>
{tabs.map(
(tab: TabItemProps) =>
!tab.hidden && (
<NavLink
key={`osds-tab-bar-item-${tab.name}`}
to={tab.to}
className="no-underline"
>
<OdsTab
id={tab.name}
role="tab"
isSelected={activePanel === tab.name}
isDisabled={tab.isDisabled}
>
{tab.title}
</OdsTab>
</NavLink>
),
)}
</>
</OdsTabs>
);
};

Expand Down
Loading

0 comments on commit ea823d6

Please sign in to comment.