Skip to content

Commit 6293659

Browse files
committed
feat: create layaut module components
1 parent 24806b7 commit 6293659

4 files changed

Lines changed: 145 additions & 0 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { ReactNode } from 'react';
2+
import { StudioHeader } from '@edx/frontend-component-header';
3+
import AuthZTitle, { AuthZTitleProps } from './AuthZTitle';
4+
5+
interface AuthZLayoutProps extends AuthZTitleProps {
6+
children: ReactNode;
7+
context: {
8+
id: string;
9+
org: string;
10+
title: string;
11+
}
12+
}
13+
14+
const AuthZLayout = ({ children, context, ...props }: AuthZLayoutProps) => (
15+
<>
16+
<StudioHeader
17+
number={context.id}
18+
org={context.org}
19+
title={context.title}
20+
/>
21+
<AuthZTitle {...props} />
22+
{children}
23+
</>
24+
25+
);
26+
27+
export default AuthZLayout;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import React from 'react';
2+
import { render, screen, fireEvent } from '@testing-library/react';
3+
import AuthZTitle, { AuthZTitleProps } from './AuthZTitle';
4+
5+
describe('AuthZTitle', () => {
6+
const defaultProps: AuthZTitleProps = {
7+
activeLabel: 'Current Page',
8+
pageTitle: 'Page Title',
9+
pageSubtitle: 'Page Subtitle',
10+
};
11+
12+
it('renders without optional fields', () => {
13+
render(<AuthZTitle {...defaultProps} />);
14+
expect(screen.getByText(defaultProps.activeLabel)).toBeInTheDocument();
15+
expect(screen.getByText(defaultProps.pageTitle)).toBeInTheDocument();
16+
expect(screen.getByText(defaultProps.pageSubtitle as string)).toBeInTheDocument();
17+
});
18+
19+
it('renders breadcrumb with links and active label', () => {
20+
const navLinks = [
21+
{ label: 'Root', to: '/' },
22+
{ label: 'Section', to: '/section' },
23+
];
24+
25+
render(<AuthZTitle {...defaultProps} navLinks={navLinks} />);
26+
27+
navLinks.forEach(({ label }) => {
28+
expect(screen.getByText(label)).toBeInTheDocument();
29+
});
30+
31+
expect(screen.getByText(defaultProps.activeLabel)).toBeInTheDocument();
32+
});
33+
34+
it('renders page title', () => {
35+
render(<AuthZTitle {...defaultProps} />);
36+
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent(defaultProps.pageTitle);
37+
});
38+
39+
it('renders page subtitle as ReactNode', () => {
40+
const subtitleNode = <div data-testid="custom-subtitle">Custom Subtitle</div>;
41+
render(<AuthZTitle {...defaultProps} pageSubtitle={subtitleNode} />);
42+
expect(screen.getByTestId('custom-subtitle')).toBeInTheDocument();
43+
});
44+
45+
it('renders action buttons and triggers onClick', () => {
46+
const onClick1 = jest.fn();
47+
const onClick2 = jest.fn();
48+
const actions = [
49+
{ label: 'Save', onClick: onClick1 },
50+
{ label: 'Cancel', onClick: onClick2 },
51+
];
52+
53+
render(<AuthZTitle {...defaultProps} actions={actions} />);
54+
55+
actions.forEach(({ label, onClick }) => {
56+
const button = screen.getByRole('button', { name: label });
57+
expect(button).toBeInTheDocument();
58+
fireEvent.click(button);
59+
expect(onClick).toHaveBeenCalled();
60+
});
61+
});
62+
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { ReactNode } from 'react';
2+
import {
3+
Breadcrumb, Col, Container, Row, Button, Badge,
4+
} from '@openedx/paragon';
5+
6+
interface BreadcrumbLink {
7+
label: string;
8+
to?: string;
9+
}
10+
11+
interface Action {
12+
label: string;
13+
onClick: () => void;
14+
}
15+
16+
export interface AuthZTitleProps {
17+
activeLabel: string;
18+
pageTitle: string;
19+
pageSubtitle: string | ReactNode;
20+
navLinks?: BreadcrumbLink[];
21+
actions?: Action[];
22+
}
23+
24+
const AuthZTitle = ({
25+
activeLabel, navLinks = [], pageTitle, pageSubtitle, actions = [],
26+
}: AuthZTitleProps) => (
27+
<Container className="p-5 bg-light-100">
28+
<Breadcrumb
29+
links={navLinks}
30+
activeLabel={activeLabel}
31+
/>
32+
<Row className="mt-4">
33+
<Col xs={12} md={8} className="mb-4">
34+
<h1 className="text-primary">{pageTitle}</h1>
35+
{typeof pageSubtitle === 'string'
36+
? <h3><Badge className="py-2 px-3 font-weight-normal" variant="light">{pageSubtitle}</Badge></h3>
37+
: pageSubtitle}
38+
</Col>
39+
<Col xs={12} md={4}>
40+
<div className="d-flex justify-content-md-end">
41+
{
42+
actions.map(({ label, onClick }) => <Button key={`authz-header-action-${label}`} onClick={onClick}>{label}</Button>)
43+
}
44+
</div>
45+
</Col>
46+
</Row>
47+
</Container>
48+
);
49+
50+
export default AuthZTitle;

src/authz-module/index.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.authz-libraries {
2+
.pgn__breadcrumb li:first-child a {
3+
color: var(--pgn-color-breadcrumb-active);
4+
text-decoration: none;
5+
}
6+
}

0 commit comments

Comments
 (0)