Skip to content

Commit

Permalink
feat: app title (#565)
Browse files Browse the repository at this point in the history
  • Loading branch information
Fran McDade authored and Fran McDade committed Feb 28, 2024
1 parent 7af6a1c commit e427a8c
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 17 deletions.
10 changes: 8 additions & 2 deletions packages/data-explorer-ui/src/components/Head/head.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,15 @@ const FAV_ICONS = {
siteWebManifest: "/favicons/site.webmanifest",
};

export const Head = (): JSX.Element => {
export interface HeadProps {
pageTitle?: string;
}

export const Head = ({ pageTitle }: HeadProps): JSX.Element => {
const { config } = useConfig();
const router = useRouter();
const { appTitle } = config;
const title = pageTitle ? `${pageTitle} - ${appTitle}` : appTitle;

const renderIcons = (): JSX.Element => {
return (
Expand Down Expand Up @@ -56,7 +62,7 @@ export const Head = (): JSX.Element => {

return (
<NextHead key="page-head">
<title>{config.appTitle}</title>
<title>{title}</title>
{renderIcons()}
</NextHead>
);
Expand Down
9 changes: 8 additions & 1 deletion packages/data-explorer-ui/src/config/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ export interface EntityConfig<T = any, I = any> extends TabConfig {
entityMapper?: EntityMapper<T, I>;
exploreMode: ExploreMode;
getId?: GetIdFunction<T>;
getTitle?: GetTitleFunction<T>;
list: ListConfig;
listView?: ListViewConfig;
options?: Options;
Expand Down Expand Up @@ -187,6 +188,11 @@ export interface FloatingConfig {
*/
export type GetIdFunction<T> = (detail: T) => string;

/**
* Get title function.
*/
export type GetTitleFunction<T> = (detail?: T) => string | undefined;

/**
* Google GIS authentication configuration.
*/
Expand Down Expand Up @@ -405,11 +411,12 @@ export interface SystemStatusConfig {
/**
* Interface used to define the tab label and route.
*/
interface TabConfig {
export interface TabConfig {
label: ReactNode;
route: string;
tabIcon?: MTabProps["icon"];
tabIconPosition?: MTabProps["iconPosition"];
tabName?: string; // Used by the entity view to generate a title for the <Head> component; when label is not typed string.
}

export interface TerraAuthConfig {
Expand Down
62 changes: 62 additions & 0 deletions packages/data-explorer-ui/src/hooks/useEntityHeadTitle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useMemo } from "react";
import { TabConfig } from "../config/entities";
import { useConfig } from "./useConfig";
import { useCurrentDetailTab } from "./useCurrentDetailTab";

/**
* Returns dynamically generated title for use in the <Head> component.
* The title combines the entity title, current tab name, and application title, if available.
* @param response - Entity response.
* @returns dynamically generated title for use in the <Head> component.
*/
export const useEntityHeadTitle = <T>(response: T): string | undefined => {
const { config, entityConfig } = useConfig();
const { currentTab } = useCurrentDetailTab();
const { appTitle } = config;
const { getTitle: getEntityTitle } = entityConfig;
// Get the tab name from the current tab.
const tabName = getTabName(currentTab);
// Get the entity title from the response.
const entityTitle = getEntityTitle?.(response);

return useMemo(
() => getTitle(appTitle, tabName, entityTitle),
[appTitle, tabName, entityTitle]
);
};

/**
* Returns the tab name as a string for the current tab, from the given tab configuration.
* For the case where tab label is not a string, default to tab name.
* @param tab - Tab configuration.
* @returns tab name.
*/
function getTabName(tab: TabConfig): string | undefined {
const { label, tabName } = tab;
return typeof label === "string" ? label : tabName;
}

/**
* Returns the full title string for the <Head> element by combining the app title, tab name, and entity title.
* @param appTitle - App title.
* @param tabName - Tab name.
* @param entityTitle - Entity title.
* @returns full title string for the <Head> element.
*/
function getTitle(
appTitle: string,
tabName?: string,
entityTitle?: string
): string | undefined {
const titles = [];
if (entityTitle) {
titles.push(entityTitle);
}
if (tabName) {
titles.push(tabName);
}
if (titles.length > 0) {
titles.push(appTitle);
return titles.join(" - ");
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import Head from "next/head";
import { useRouter } from "next/router";
import React from "react";
import React, { Fragment } from "react";
import { PARAMS_INDEX_UUID } from "../../common/constants";
import { Tab, Tabs, TabValue } from "../../components/common/Tabs/tabs";
import { ComponentCreator } from "../../components/ComponentCreator/ComponentCreator";
import { Detail as DetailView } from "../../components/Detail/detail";
import { EntityConfig } from "../../config/entities";
import { useConfig } from "../../hooks/useConfig";
import { useCurrentDetailTab } from "../../hooks/useCurrentDetailTab";
import { useEntityHeadTitle } from "../../hooks/useEntityHeadTitle";
import { useFetchEntity } from "../../hooks/useFetchEntity";

// eslint-disable-next-line @typescript-eslint/no-explicit-any -- this data type can't be determined beforehand
Expand Down Expand Up @@ -42,6 +44,7 @@ export const EntityDetailView = (props: EntityDetailViewProps): JSX.Element => {
const uuid = query.params?.[PARAMS_INDEX_UUID];
const isDetailOverview = detailOverviews?.includes(currentTab.label);
const tabs = getTabs(entityConfig);
const title = useEntityHeadTitle(response);

if (!response) {
return <span></span>; //TODO: return the loading UI component
Expand All @@ -57,18 +60,25 @@ export const EntityDetailView = (props: EntityDetailViewProps): JSX.Element => {
};

return (
<DetailView
isDetailOverview={isDetailOverview}
mainColumn={
<ComponentCreator components={mainColumn} response={response} />
}
sideColumn={
sideColumn ? (
<ComponentCreator components={sideColumn} response={response} />
) : undefined
}
Tabs={<Tabs onTabChange={onTabChange} tabs={tabs} value={tabRoute} />}
top={<ComponentCreator components={top} response={response} />}
/>
<Fragment>
{title && (
<Head>
<title>{title}</title>
</Head>
)}
<DetailView
isDetailOverview={isDetailOverview}
mainColumn={
<ComponentCreator components={mainColumn} response={response} />
}
sideColumn={
sideColumn ? (
<ComponentCreator components={sideColumn} response={response} />
) : undefined
}
Tabs={<Tabs onTabChange={onTabChange} tabs={tabs} value={tabRoute} />}
top={<ComponentCreator components={top} response={response} />}
/>
</Fragment>
);
};

0 comments on commit e427a8c

Please sign in to comment.