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

feat: Use StudioContentMenu in ContentLibrary #13927

Merged
merged 13 commits into from
Nov 6, 2024
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { screen } from '@testing-library/react';
import { AppContentLibrary } from './AppContentLibrary';
import { textMock } from '@studio/testing/mocks/i18nMock';
import { renderWithProviders } from '../../test/mocks';

describe('AppContentLibrary', () => {
it('renders the AppContentLibrary with codeLists and images resources', () => {
Expand All @@ -20,5 +21,5 @@ describe('AppContentLibrary', () => {
});

const renderAppContentLibrary = () => {
render(<AppContentLibrary />);
renderWithProviders()(<AppContentLibrary />);
};
3 changes: 2 additions & 1 deletion frontend/language/src/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@
"app_content_library.images.page_name": "Bilder",
"app_content_library.info_box.title": "En kort beskrivelse om bruk av og hensikt med ressursen i bibliotket.",
"app_content_library.landing_page.description": "Når du utvikler skjemaer, er det nyttig å samle ulike filer og ressurser på ett sted. I biblioteket kan du laste opp ting andre har laget som du har bruk for, eller selv lage det du trenger til de tjenestene du utvikler.",
"app_content_library.landing_page.page_name": "Bibliotek",
"app_content_library.landing_page.page_name": "Om biblioteket",
"app_content_library.landing_page.title": "Med biblioteket kan du effektivt utvikle mer konsekvente tjenester",
"app_content_library.library_heading": "Bibliotek",
"app_create_release.build_version": "Bygg versjon",
"app_create_release.check_status": "Sjekker status på appen din...",
"app_create_release.loading": "Laster...",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,9 @@
.libraryContainer {
background-color: var(--fds-semantic-background-default);
border-radius: var(--fds-border_radius-xlarge);
border-bottom: solid 1px var(--fds-semantic-border-neutral-subtle);
border: solid 1px var(--fds-semantic-border-neutral-subtle);
overflow: hidden;
min-height: 80%;
max-height: 100%;
width: 100%;
}

.component {
padding: var(--fds-spacing-10);
}

.libraryContent {
display: grid;
grid-template-columns: 0.7fr 3.3fr 1fr;
height: 100%;
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import React from 'react';
import { ContentLibrary } from './ContentLibrary';
import { render, screen } from '@testing-library/react';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { mockPagesConfig } from '../../mocks/mockPagesConfig';
import { textMock } from '@studio/testing/mocks/i18nMock';
import { RouterContext } from '../contexts/RouterContext';
import type { PageName } from '../types/PageName';
import { renderWithBrowserRouter } from '../../test-utils/renderWithBrowserRouter';

const navigateMock = jest.fn();

describe('ContentLibrary', () => {
it('renders the ContentLibrary with landingPage by default', () => {
renderContentLibrary();
const libraryHeader = screen.getByRole('heading', {
name: textMock('app_content_library.landing_page.page_name'),
name: textMock('app_content_library.library_heading'),
});
const landingPageTitle = screen.getByRole('heading', {
name: textMock('app_content_library.landing_page.title'),
Expand Down Expand Up @@ -51,7 +52,7 @@ describe('ContentLibrary', () => {
});

const renderContentLibrary = (currentPage: PageName = undefined) => {
render(
renderWithBrowserRouter(
<RouterContext.Provider value={{ currentPage, navigate: navigateMock }}>
<ContentLibrary pages={mockPagesConfig} />
</RouterContext.Provider>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ import type { PageComponent } from '../utils/router/RouterRouteMapper';
import { RouterRouteMapperImpl } from '../utils/router/RouterRouteMapper';
import type { PagePropsMap, PagesConfig } from '../types/PagesProps';
import classes from './ContentLibrary.module.css';
import { InfoBox } from './InfoBox';
import { PagesRouter } from './PagesRouter';
import { LibraryHeader } from './LibraryHeader';
import { StudioHeading } from '@studio/components';
import type { PageName } from '../types/PageName';
import { LibraryBody } from './LibraryBody';

type ContentLibraryProps = {
pages: PagesConfig;
Expand All @@ -19,37 +18,25 @@ export function ContentLibrary({ pages }: ContentLibraryProps): React.ReactEleme
return <ContentLibraryForPage pages={pages} currentPage={currentPage} />;
}

type ContentLibraryForPageProps<T extends PageName = 'landingPage'> = {
type ContentLibraryForPageProps<T extends PageName> = {
pages: PagesConfig;
currentPage: T;
};

function ContentLibraryForPage<T extends PageName = 'landingPage'>({
function ContentLibraryForPage<T extends PageName>({
pages,
currentPage,
}: ContentLibraryForPageProps<T>): React.ReactElement {
const router = new RouterRouteMapperImpl(pages);

const Component: PageComponent<Required<PagePropsMap>[T]> =
router.configuredRoutes.get(currentPage);
const Component: PageComponent<PagePropsMap<T>> = router.configuredRoutes.get(currentPage);
if (!Component) return <StudioHeading>404 Page Not Found</StudioHeading>; // Show the NotFound page from app-dev instead

const componentPropsAreExternal = currentPage !== 'landingPage';

const componentProps: Required<PagePropsMap>[T] =
componentPropsAreExternal && (pages[currentPage].props as Required<PagePropsMap>[T]);

return (
<div className={classes.libraryBackground}>
<div className={classes.libraryContainer}>
<LibraryHeader />
<div className={classes.libraryContent}>
<PagesRouter pageNames={Object.keys(pages) as PageName[]} />
<div className={classes.component}>
<Component {...componentProps} />
</div>
<InfoBox pageName={currentPage} />
</div>
<LibraryBody<T> Component={Component} pages={pages} currentPage={currentPage} />
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
.infoBoxContainer {
border-radius: var(--fds-border_radius-large);
border: 1px solid var(--fds-semantic-border-neutral-subtle);
width: 15vw;
overflow: hidden;
height: fit-content;
margin: var(--fds-spacing-10);
margin: var(--fds-spacing-8);
}

.infoBoxContainer img {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import { InfoBox } from './InfoBox';
import { render, screen } from '@testing-library/react';
import { textMock } from '@studio/testing/mocks/i18nMock';
import type { PageName } from '../../types/PageName';
import type { PageName } from '../../../types/PageName';
import { infoBoxConfigs } from './infoBoxConfigs';

const pageNameMock: PageName = 'codeList';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import classes from './InfoBox.module.css';
import { StudioParagraph } from '@studio/components';
import { useTranslation } from 'react-i18next';
import type { PageName } from '../../types/PageName';
import type { PageName } from '../../../types/PageName';
import { infoBoxConfigs } from './infoBoxConfigs';

type InfoBoxProps = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { PageName } from '../../types/PageName';
import type { InfoBoxProps } from '../../types/InfoBoxProps';
import type { PageName } from '../../../types/PageName';
import type { InfoBoxProps } from '../../../types/InfoBoxProps';

export type InfoBoxConfigs = Partial<{ [T in PageName]: InfoBoxProps }>;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.libraryContent {
display: grid;
grid-template-columns: 2fr 7fr 3fr;
height: 100%;
}

.component {
padding: var(--fds-spacing-10);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from 'react';
import classes from './LibraryBody.module.css';
import { PagesRouter } from './PagesRouter';
import { InfoBox } from './InfoBox';
import type { PagePropsMap, PagesConfig } from '../../types/PagesProps';
import type { PageName } from '../../types/PageName';
import type { PageComponent } from '../../utils/router/RouterRouteMapper';

type LibraryBodyProps<T extends PageName> = {
Component: PageComponent<PagePropsMap<T>>;
pages: PagesConfig;
currentPage: T;
};

export function LibraryBody<T extends PageName>({
Component,
pages,
currentPage,
}: LibraryBodyProps<T>) {
const componentProps: PagePropsMap<T> = getComponentProps(pages, currentPage);

return (
<div className={classes.libraryContent}>
<PagesRouter pageNames={getAllPageNamesFromPagesConfig(pages)} />
<Page<T> Component={Component} componentProps={componentProps} currentPage={currentPage} />
</div>
);
}

type PageProps<T extends PageName> = {
Component: PageComponent<PagePropsMap<T>>;
componentProps: PagePropsMap<T>;
currentPage: T;
};

function Page<T extends PageName>({ Component, componentProps, currentPage }: PageProps<T>) {
return (
<>
<div className={classes.component}>
<Component {...componentProps} />
</div>
<InfoBox pageName={currentPage} />
</>
);
}

const getComponentProps = <T extends PageName>(
pages: PagesConfig,
currentPage: T,
): PagePropsMap<T> => {
if (currentPage === 'landingPage') return {} as PagePropsMap<T>;
return pages[currentPage].props;
};

const getAllPageNamesFromPagesConfig = (pages: PagesConfig): PageName[] => {
const customPages = Object.keys(pages) as PageName[];
return ['landingPage', ...customPages];
};
Original file line number Diff line number Diff line change
@@ -1,41 +1,41 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { PagesRouter } from './PagesRouter';
import { textMock } from '@studio/testing/mocks/i18nMock';
import { RouterContext } from '../../contexts/RouterContext';
import type { PageName } from '../../types/PageName';
import { RouterContext } from '../../../contexts/RouterContext';
import type { PageName } from '../../../types/PageName';
import { renderWithBrowserRouter } from '../../../../test-utils/renderWithBrowserRouter';

const navigateMock = jest.fn();

describe('PagesRouter', () => {
it('renders the pages as navigation titles', () => {
renderPagesRouter();
const codeListNavTitle = screen.getByText(textMock('app_content_library.code_lists.page_name'));
const imagesNavTitle = screen.getByText(textMock('app_content_library.images.page_name'));
const codeListNavTitle = screen.getByRole('tab', {
name: textMock('app_content_library.code_lists.page_name'),
});
const imagesNavTitle = screen.getByRole('tab', {
name: textMock('app_content_library.images.page_name'),
});
expect(codeListNavTitle).toBeInTheDocument();
expect(imagesNavTitle).toBeInTheDocument();
});

it('calls navigate from RouterContext when clicking on a page that is not selected', async () => {
const user = userEvent.setup();
renderPagesRouter();
const imagesNavTitle = screen.getByText(textMock('app_content_library.images.page_name'));
const imagesNavTitle = screen.getByRole('tab', {
name: textMock('app_content_library.images.page_name'),
});
await user.click(imagesNavTitle);
expect(navigateMock).toHaveBeenCalledTimes(1);
expect(navigateMock).toHaveBeenCalledWith('images');
});

it('returns null if trying to render an unknown pageName', () => {
const pageName: string = 'unknownPageName';
renderPagesRouter([pageName as PageName]);
const navTitle = screen.queryByText(pageName);
expect(navTitle).not.toBeInTheDocument();
});
});

const renderPagesRouter = (pageNames: PageName[] = ['codeList', 'images']) => {
render(
renderWithBrowserRouter(
<RouterContext.Provider value={{ currentPage: 'codeList', navigate: navigateMock }}>
<PagesRouter pageNames={pageNames} />
</RouterContext.Provider>,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';
import { useRouterContext } from '../../../contexts/RouterContext';
import type { PageName } from '../../../types/PageName';
import { useContentTabs } from '../../../hooks/useLibraryMenuContentTabs';
import { StudioContentMenu } from '@studio/components';

type PagesRouterProps = {
pageNames: PageName[];
};

export function PagesRouter({ pageNames }: PagesRouterProps): React.ReactElement {
const { navigate, currentPage } = useRouterContext();
const contentTabs = useContentTabs();

const handleNavigation = (pageToNavigateTo: PageName) => {
navigate(pageToNavigateTo);
};

return (
<StudioContentMenu selectedTabId={currentPage} onChangeTab={handleNavigation}>
{pageNames.map((pageName) => (
<StudioContentMenu.LinkTab
key={contentTabs[pageName].tabId}
icon={contentTabs[pageName].icon}
tabId={contentTabs[pageName].tabId}
tabName={contentTabs[pageName].tabName}
renderTab={contentTabs[pageName].renderTab}
/>
))}
</StudioContentMenu>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { PagesRouter } from './PagesRouter';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { LibraryBody } from './LibraryBody';
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
.libraryHeading {
display: flex;
align-items: center;
gap: var(--fds-spacing-1);
padding: var(--fds-spacing-4);
border-bottom: solid 1px var(--fds-semantic-border-neutral-subtle);
}

.libraryLandingPageNavigation {
.headingIcon {
display: flex;
align-items: center;
gap: var(--fds-spacing-1);
cursor: pointer;
width: fit-content;
color: var(--fds-semantic-text-neutral-default);
font-size: var(--fds-sizing-10);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,19 @@ import React from 'react';
import { textMock } from '@studio/testing/mocks/i18nMock';
import { LibraryHeader } from './LibraryHeader';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { RouterContext } from '../../contexts/RouterContext';

const navigateMock = jest.fn();

describe('LibraryHeader', () => {
it('renders the landingPage header', () => {
it('renders the content library header', () => {
renderLibraryHeader();
const landingPageIcon = screen.getByRole('img');
const landingPageHeader = screen.getByRole('heading', {
name: textMock('app_content_library.landing_page.page_name'),
const libraryIcon = screen.getByRole('img');
const libraryHeader = screen.getByRole('heading', {
name: textMock('app_content_library.library_heading'),
});
expect(landingPageIcon).toBeInTheDocument();
expect(landingPageHeader).toBeInTheDocument();
});

it('calls navigate from useRouterContext when clicking on the header', async () => {
const user = userEvent.setup();
renderLibraryHeader();
const landingPageHeader = screen.getByRole('heading', {
name: textMock('app_content_library.landing_page.page_name'),
});
await user.click(landingPageHeader);
expect(navigateMock).toHaveBeenCalledTimes(1);
expect(navigateMock).toHaveBeenCalledWith('landingPage');
expect(libraryIcon).toBeInTheDocument();
expect(libraryHeader).toBeInTheDocument();
});
});

Expand Down
Loading