generated from openedx/frontend-template-application
-
Notifications
You must be signed in to change notification settings - Fork 4
feat(authz): create libraries team management view #2
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
Merged
Merged
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
4fbd975
test: add testing-library user event , utils and update path resolver
dcoa 75bd0d7
feat: create a loading page and the AuthZModule
dcoa 24806b7
feat: create app utilities for the API url resolution and permission …
dcoa 6293659
feat: create layaut module components
dcoa 7390387
feat: create the libraries context
dcoa cb49cfe
feat: create the table team management table
dcoa 6c67dc0
feat: add the libraries authz management view
dcoa a6f96e3
test: improve coverage
dcoa 39686a0
refactor: use a better name for the team management view
dcoa 3a27943
style: fix color for background tabs
dcoa 08a1f68
fix: convert data to camel case in the api return for getTeamMembers
dcoa fc62f15
style: fix linter issues and data location
dcoa 6b1db4a
refactor: create Query Keys factories
dcoa 80ad213
style: single quote imports
dcoa bb288f6
refactor: useLibrary directly in the componets to access the metadata
dcoa b2eb92b
style: update linter validation and fix lint issues
dcoa 7cb528b
fix: display username intead of name
dcoa File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) => ( | ||
| <> | ||
| <StudioHeader | ||
| number={context.id} | ||
| org={context.org} | ||
| title={context.title} | ||
| /> | ||
| <AuthZTitle {...props} /> | ||
| {children} | ||
| </> | ||
|
|
||
| ); | ||
|
|
||
| export default AuthZLayout; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(<AuthZTitle {...defaultProps} />); | ||
| 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(<AuthZTitle {...defaultProps} navLinks={navLinks} />); | ||
|
|
||
| navLinks.forEach(({ label }) => { | ||
| expect(screen.getByText(label)).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| expect(screen.getByText(defaultProps.activeLabel)).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('renders page title', () => { | ||
| render(<AuthZTitle {...defaultProps} />); | ||
| expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent(defaultProps.pageTitle); | ||
| }); | ||
|
|
||
| it('renders page subtitle as ReactNode', () => { | ||
| const subtitleNode = <div data-testid="custom-subtitle">Custom Subtitle</div>; | ||
| render(<AuthZTitle {...defaultProps} pageSubtitle={subtitleNode} />); | ||
| 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(<AuthZTitle {...defaultProps} actions={actions} />); | ||
|
|
||
| actions.forEach(({ label, onClick }) => { | ||
| const button = screen.getByRole('button', { name: label }); | ||
| expect(button).toBeInTheDocument(); | ||
| fireEvent.click(button); | ||
| expect(onClick).toHaveBeenCalled(); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) => ( | ||
| <Container className="p-5 bg-light-100"> | ||
| <Breadcrumb | ||
| links={navLinks} | ||
| activeLabel={activeLabel} | ||
| /> | ||
| <Row className="mt-4"> | ||
| <Col xs={12} md={8} className="mb-4"> | ||
| <h1 className="text-primary">{pageTitle}</h1> | ||
| {typeof pageSubtitle === 'string' | ||
| ? <h3><Badge className="py-2 px-3 font-weight-normal" variant="light">{pageSubtitle}</Badge></h3> | ||
| : pageSubtitle} | ||
| </Col> | ||
| <Col xs={12} md={4}> | ||
| <div className="d-flex justify-content-md-end"> | ||
| { | ||
| actions.map(({ label, onClick }) => <Button key={`authz-header-action-${label}`} onClick={onClick}>{label}</Button>) | ||
| } | ||
| </div> | ||
| </Col> | ||
| </Row> | ||
| </Container> | ||
| ); | ||
|
|
||
| export default AuthZTitle; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| export const ROUTES = { | ||
| LIBRARIES_TEAM_PATH: '/libraries/:libraryId', | ||
| LIBRARIES_USER_PATH: '/libraries/user/:username', | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; | ||
| import { LibraryMetadata, TeamMember } from '@src/types'; | ||
| import { camelCaseObject } from '@edx/frontend-platform'; | ||
| import { getApiUrl, getStudioApiUrl } from '@src/data/utils'; | ||
|
|
||
| export interface GetTeamMembersResponse { | ||
| members: TeamMember[]; | ||
| totalCount: number; | ||
| } | ||
|
|
||
| // TODO: replece api path once is created | ||
| export const getTeamMembers = async (object: string): Promise<TeamMember[]> => { | ||
| const { data } = await getAuthenticatedHttpClient().get(getApiUrl(`/api/authz/v1/roles/users?scope=${object}`)); | ||
| return camelCaseObject(data.results); | ||
| }; | ||
|
|
||
| // TODO: this should be replaced in the future with Console API | ||
| export const getLibrary = async (libraryId: string): Promise<LibraryMetadata> => { | ||
| const { data } = await getAuthenticatedHttpClient().get(getStudioApiUrl(`/api/libraries/v2/${libraryId}/`)); | ||
| return { | ||
| id: data.id, | ||
| org: data.org, | ||
| title: data.title, | ||
| slug: data.slug, | ||
| }; | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| import { ReactNode } from 'react'; | ||
| import { act, renderHook, waitFor } from '@testing-library/react'; | ||
| import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; | ||
| import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; | ||
| import { useLibrary, useTeamMembers } from './hooks'; | ||
|
|
||
| jest.mock('@edx/frontend-platform/auth', () => ({ | ||
| getAuthenticatedHttpClient: jest.fn(), | ||
| })); | ||
|
|
||
| const mockMembers = [ | ||
| { | ||
| displayName: 'Alice', | ||
| username: 'user1', | ||
| email: 'alice@example.com', | ||
| roles: ['admin', 'author'], | ||
| }, | ||
| { | ||
| displayName: 'Bob', | ||
| username: 'user2', | ||
| email: 'bob@example.com', | ||
| roles: ['collaborator'], | ||
| }, | ||
| ]; | ||
|
|
||
| const mockLibrary = { | ||
| id: 'lib:123', | ||
| org: 'demo-org', | ||
| title: 'Test Library', | ||
| slug: 'test-library', | ||
| }; | ||
|
|
||
| const createWrapper = () => { | ||
| const queryClient = new QueryClient({ | ||
| defaultOptions: { | ||
| queries: { | ||
| retry: false, | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| const wrapper = ({ children }: { children: ReactNode }) => ( | ||
| <QueryClientProvider | ||
| client={queryClient} | ||
| >{children} | ||
| </QueryClientProvider> | ||
| ); | ||
|
|
||
| return wrapper; | ||
| }; | ||
|
|
||
| describe('useTeamMembers', () => { | ||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
| }); | ||
|
|
||
| it('returns data when API call succeeds', async () => { | ||
| getAuthenticatedHttpClient.mockReturnValue({ | ||
| get: jest.fn().mockResolvedValue({ data: { results: mockMembers } }), | ||
| }); | ||
|
|
||
| const { result } = renderHook(() => useTeamMembers('lib:123'), { | ||
| wrapper: createWrapper(), | ||
| }); | ||
|
|
||
| await waitFor(() => expect(result.current.isSuccess).toBe(true)); | ||
|
|
||
| expect(getAuthenticatedHttpClient).toHaveBeenCalled(); | ||
| expect(result.current.data).toEqual(mockMembers); | ||
| }); | ||
|
|
||
| it('handles error when API call fails', async () => { | ||
| getAuthenticatedHttpClient.mockReturnValue({ | ||
| get: jest.fn().mockRejectedValue(new Error('API failure')), | ||
| }); | ||
|
|
||
| const { result } = renderHook(() => useTeamMembers('lib:123'), { | ||
| wrapper: createWrapper(), | ||
| }); | ||
|
|
||
| await waitFor(() => expect(result.current.isError).toBe(true)); | ||
|
|
||
| expect(getAuthenticatedHttpClient).toHaveBeenCalled(); | ||
| expect(result.current.error).toBeDefined(); | ||
| expect(result.current.data).toBeUndefined(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('useLibrary', () => { | ||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
| }); | ||
|
|
||
| it('returns metadata on success', async () => { | ||
| getAuthenticatedHttpClient.mockReturnValue({ | ||
| get: jest.fn().mockResolvedValueOnce({ data: mockLibrary }), | ||
| }); | ||
|
|
||
| const { result } = renderHook( | ||
| () => useLibrary('lib123'), | ||
| { wrapper: createWrapper() }, | ||
| ); | ||
| await waitFor(() => { | ||
| expect(result.current.data).toEqual(mockLibrary); | ||
| expect(getAuthenticatedHttpClient).toHaveBeenCalled(); | ||
| }); | ||
| }); | ||
|
|
||
| it('throws on error', () => { | ||
| getAuthenticatedHttpClient.mockReturnValue({ | ||
| get: jest.fn().mockRejectedValue(new Error('Not found')), | ||
| }); | ||
|
|
||
| const wrapper = createWrapper(); | ||
| try { | ||
| act(() => { | ||
| renderHook(() => useLibrary('lib123'), { wrapper }); | ||
| }); | ||
| } catch (e) { | ||
| expect(e).toEqual(new Error('Not found')); | ||
| } | ||
|
|
||
| expect(getAuthenticatedHttpClient).toHaveBeenCalled(); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is Studio header the right header for this MFE?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we believe the Studio header is the right choice for the MVP, given that the current functionality is focused solely on Library team management, which fits well within the Studio context. However, we will revisit the header choice in Phase 2, when we start integrating broader scopes and modules within the console.