diff --git a/src/authz-module/components/AuthZLayout.tsx b/src/authz-module/components/AuthZLayout.tsx new file mode 100644 index 0000000..9845b90 --- /dev/null +++ b/src/authz-module/components/AuthZLayout.tsx @@ -0,0 +1,27 @@ +import { ReactNode } from 'react'; +import { StudioHeader } from '@edx/frontend-component-header'; +import AuthZTitle, { AuthZTitleProps } from './AuthZTitle'; + +interface AuthZLayoutProps extends AuthZTitleProps { + children: ReactNode; + context: { + id: string; + org: string; + title: string; + } +} + +const AuthZLayout = ({ children, context, ...props }: AuthZLayoutProps) => ( + <> + + + {children} + + +); + +export default AuthZLayout; diff --git a/src/authz-module/components/AuthZTitle.test.tsx b/src/authz-module/components/AuthZTitle.test.tsx new file mode 100644 index 0000000..460cf88 --- /dev/null +++ b/src/authz-module/components/AuthZTitle.test.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import AuthZTitle, { AuthZTitleProps } from './AuthZTitle'; + +describe('AuthZTitle', () => { + const defaultProps: AuthZTitleProps = { + activeLabel: 'Current Page', + pageTitle: 'Page Title', + pageSubtitle: 'Page Subtitle', + }; + + it('renders without optional fields', () => { + render(); + expect(screen.getByText(defaultProps.activeLabel)).toBeInTheDocument(); + expect(screen.getByText(defaultProps.pageTitle)).toBeInTheDocument(); + expect(screen.getByText(defaultProps.pageSubtitle as string)).toBeInTheDocument(); + }); + + it('renders breadcrumb with links and active label', () => { + const navLinks = [ + { label: 'Root', to: '/' }, + { label: 'Section', to: '/section' }, + ]; + + render(); + + navLinks.forEach(({ label }) => { + expect(screen.getByText(label)).toBeInTheDocument(); + }); + + expect(screen.getByText(defaultProps.activeLabel)).toBeInTheDocument(); + }); + + it('renders page title', () => { + render(); + expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent(defaultProps.pageTitle); + }); + + it('renders page subtitle as ReactNode', () => { + const subtitleNode =
Custom Subtitle
; + render(); + expect(screen.getByTestId('custom-subtitle')).toBeInTheDocument(); + }); + + it('renders action buttons and triggers onClick', () => { + const onClick1 = jest.fn(); + const onClick2 = jest.fn(); + const actions = [ + { label: 'Save', onClick: onClick1 }, + { label: 'Cancel', onClick: onClick2 }, + ]; + + render(); + + actions.forEach(({ label, onClick }) => { + const button = screen.getByRole('button', { name: label }); + expect(button).toBeInTheDocument(); + fireEvent.click(button); + expect(onClick).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/authz-module/components/AuthZTitle.tsx b/src/authz-module/components/AuthZTitle.tsx new file mode 100644 index 0000000..6224ea9 --- /dev/null +++ b/src/authz-module/components/AuthZTitle.tsx @@ -0,0 +1,50 @@ +import { ReactNode } from 'react'; +import { + Breadcrumb, Col, Container, Row, Button, Badge, +} from '@openedx/paragon'; + +interface BreadcrumbLink { + label: string; + to?: string; +} + +interface Action { + label: string; + onClick: () => void; +} + +export interface AuthZTitleProps { + activeLabel: string; + pageTitle: string; + pageSubtitle: string | ReactNode; + navLinks?: BreadcrumbLink[]; + actions?: Action[]; +} + +const AuthZTitle = ({ + activeLabel, navLinks = [], pageTitle, pageSubtitle, actions = [], +}: AuthZTitleProps) => ( + + + + +

{pageTitle}

+ {typeof pageSubtitle === 'string' + ?

{pageSubtitle}

+ : pageSubtitle} + + +
+ { + actions.map(({ label, onClick }) => ) + } +
+ +
+
+); + +export default AuthZTitle; diff --git a/src/authz-module/index.scss b/src/authz-module/index.scss new file mode 100644 index 0000000..4aada25 --- /dev/null +++ b/src/authz-module/index.scss @@ -0,0 +1,6 @@ +.authz-libraries { + .pgn__breadcrumb li:first-child a { + color: var(--pgn-color-breadcrumb-active); + text-decoration: none; + } +}