From 95e04da05eec1772c6b442bddab512bb91824a53 Mon Sep 17 00:00:00 2001 From: Quentin Leonetti Date: Mon, 8 Apr 2024 15:44:55 +0200 Subject: [PATCH] new analytics productivity route and components (#4223) * new Productivity route and components * remove console.log * Add more tests * Add alumni and inactive badges * fix test * linting * linting * fix test * fix multiple display * add flag * add test * Update tests * camel to kebab routes * fix import order * fix type import * re-enable productivity flag * update badge labels * adjust table sizings * skip e2e tests --- .github/workflows/reusable-verify.yml | 1 + apps/crn-frontend/src/analytics/Routes.tsx | 80 +++++++-- .../src/analytics/__tests__/Routes.test.tsx | 90 ++++++++-- .../Leadership.tsx} | 31 ++-- .../__tests__/Leadership.test.tsx} | 15 +- .../{ => leadership}/__tests__/api.test.tsx | 4 +- .../src/analytics/{ => leadership}/api.ts | 0 .../src/analytics/{ => leadership}/state.ts | 2 +- .../analytics/productivity/Productivity.tsx | 116 +++++++++++++ .../__tests__/Productivity.test.tsx | 56 ++++++ apps/storybook/src/StateTag.stories.tsx | 4 +- packages/flags/src/index.ts | 4 +- .../react-components/src/atoms/TabLink.tsx | 53 +++--- .../src/icons/alumni-badge.tsx | 2 +- .../src/icons/inactive-badge.tsx | 14 +- packages/react-components/src/icons/index.ts | 3 +- .../react-components/src/icons/leadership.tsx | 4 +- .../src/icons/productivity.tsx | 32 ++++ packages/react-components/src/icons/props.tsx | 5 + packages/react-components/src/index.ts | 9 +- .../src/molecules/AssociationList.tsx | 6 +- .../src/molecules/EventOwner.tsx | 6 +- .../__tests__/AssociationList.test.tsx | 2 +- .../molecules/__tests__/EventOwner.test.tsx | 2 +- .../molecules/__tests__/EventTeams.test.tsx | 2 +- .../molecules/__tests__/MembersList.test.tsx | 4 +- .../molecules/__tests__/UsersList.test.tsx | 6 +- .../src/organisms/InterestGroupCard.tsx | 9 +- .../InterestGroupTeamsTabbedCard.tsx | 6 +- .../src/organisms/SpeakerList.tsx | 6 +- .../src/organisms/TeamCard.tsx | 4 +- .../src/organisms/TeamProductivityTable.tsx | 109 ++++++++++++ .../src/organisms/UserProductivityTable.tsx | 164 ++++++++++++++++++ .../src/organisms/UserTeamsTabbedCard.tsx | 4 +- .../__tests__/InterestGroupCard.test.tsx | 2 +- .../organisms/__tests__/PeopleCard.test.tsx | 4 +- .../organisms/__tests__/SpeakerList.test.tsx | 4 +- .../src/organisms/__tests__/TeamCard.test.tsx | 2 +- .../__tests__/TeamMembersTabbedCard.test.tsx | 2 +- .../__tests__/TeamProductivityTable.test.tsx | 32 ++++ .../__tests__/UserProductivityTable.test.tsx | 90 ++++++++++ .../react-components/src/organisms/index.ts | 4 + ...dy.tsx => AnalyticsLeadershipPageBody.tsx} | 10 +- .../src/templates/AnalyticsPageHeader.tsx | 25 ++- .../AnalyticsProductivityPageBody.tsx | 75 ++++++++ .../templates/InterestGroupProfileHeader.tsx | 9 +- .../src/templates/NetworkPageHeader.tsx | 36 ++-- .../src/templates/TeamProfileHeader.tsx | 8 +- .../AnalyticsLeadershipPageBody.test.tsx | 33 ++++ .../AnalyticsProductivityPageBody.test.tsx | 30 ++++ .../InterestGroupProfileHeader.test.tsx | 2 +- ...y.test.tsx => LeadershipPageBody.test.tsx} | 6 +- .../__tests__/TeamProfileHeader.test.tsx | 2 +- .../__tests__/UserProfileHeader.test.tsx | 4 +- .../react-components/src/templates/index.ts | 3 +- packages/routing/src/analytics.ts | 10 +- 56 files changed, 1083 insertions(+), 165 deletions(-) rename apps/crn-frontend/src/analytics/{Analytics.tsx => leadership/Leadership.tsx} (69%) rename apps/crn-frontend/src/analytics/{__tests__/Analytics.test.tsx => leadership/__tests__/Leadership.test.tsx} (86%) rename apps/crn-frontend/src/analytics/{ => leadership}/__tests__/api.test.tsx (96%) rename apps/crn-frontend/src/analytics/{ => leadership}/api.ts (100%) rename apps/crn-frontend/src/analytics/{ => leadership}/state.ts (97%) create mode 100644 apps/crn-frontend/src/analytics/productivity/Productivity.tsx create mode 100644 apps/crn-frontend/src/analytics/productivity/__tests__/Productivity.test.tsx create mode 100644 packages/react-components/src/icons/productivity.tsx create mode 100644 packages/react-components/src/icons/props.tsx create mode 100644 packages/react-components/src/organisms/TeamProductivityTable.tsx create mode 100644 packages/react-components/src/organisms/UserProductivityTable.tsx create mode 100644 packages/react-components/src/organisms/__tests__/TeamProductivityTable.test.tsx create mode 100644 packages/react-components/src/organisms/__tests__/UserProductivityTable.test.tsx rename packages/react-components/src/templates/{AnalyticsPageBody.tsx => AnalyticsLeadershipPageBody.tsx} (86%) create mode 100644 packages/react-components/src/templates/AnalyticsProductivityPageBody.tsx create mode 100644 packages/react-components/src/templates/__tests__/AnalyticsLeadershipPageBody.test.tsx create mode 100644 packages/react-components/src/templates/__tests__/AnalyticsProductivityPageBody.test.tsx rename packages/react-components/src/templates/__tests__/{AnalyticsPageBody.test.tsx => LeadershipPageBody.test.tsx} (76%) diff --git a/.github/workflows/reusable-verify.yml b/.github/workflows/reusable-verify.yml index e128d8542b..05ce633009 100644 --- a/.github/workflows/reusable-verify.yml +++ b/.github/workflows/reusable-verify.yml @@ -49,6 +49,7 @@ jobs: test-e2e: runs-on: ubuntu-latest + continue-on-error: true container: image: mcr.microsoft.com/playwright:focal steps: diff --git a/apps/crn-frontend/src/analytics/Routes.tsx b/apps/crn-frontend/src/analytics/Routes.tsx index 9b47fe428d..ee0dc176d8 100644 --- a/apps/crn-frontend/src/analytics/Routes.tsx +++ b/apps/crn-frontend/src/analytics/Routes.tsx @@ -1,32 +1,84 @@ +import { isEnabled } from '@asap-hub/flags'; import { SkeletonBodyFrame as Frame } from '@asap-hub/frontend-utils'; import { AnalyticsPage } from '@asap-hub/react-components'; -import { FC, lazy, useEffect } from 'react'; -import { Route, Switch, useRouteMatch } from 'react-router-dom'; +import { analytics } from '@asap-hub/routing'; +import { lazy, useEffect } from 'react'; +import { Redirect, Route, Switch, useRouteMatch } from 'react-router-dom'; -const loadAnalytics = () => - import(/* webpackChunkName: "analytics" */ './Analytics'); +const loadProductivity = () => + import(/* webpackChunkName: "productivity" */ './productivity/Productivity'); -const AnalyticsBody = lazy(loadAnalytics); +const loadLeadership = () => + import(/* webpackChunkName: "leadership" */ './leadership/Leadership'); -const About: FC> = () => { +const LeadershipBody = lazy(loadLeadership); +const ProductivityBody = lazy(loadProductivity); + +const Routes = () => { useEffect(() => { // eslint-disable-next-line @typescript-eslint/no-floating-promises - loadAnalytics(); + loadLeadership().then(loadLeadership); }, []); const { path } = useRouteMatch(); return ( - - - - - - + {isEnabled('DISPLAY_ANALYTICS_PRODUCTIVITY') && ( + + + + + + + + + + + + + )} + + + + + + + + + + + + {isEnabled('DISPLAY_ANALYTICS_PRODUCTIVITY') ? ( + + ) : ( + + )} ); }; -export default About; +export default Routes; diff --git a/apps/crn-frontend/src/analytics/__tests__/Routes.test.tsx b/apps/crn-frontend/src/analytics/__tests__/Routes.test.tsx index 0e3c1b23ff..04dc973f53 100644 --- a/apps/crn-frontend/src/analytics/__tests__/Routes.test.tsx +++ b/apps/crn-frontend/src/analytics/__tests__/Routes.test.tsx @@ -1,6 +1,7 @@ import { render, screen, + waitFor, waitForElementToBeRemoved, } from '@testing-library/react'; import { MemoryRouter, Route } from 'react-router-dom'; @@ -8,30 +9,30 @@ import { RecoilRoot } from 'recoil'; import { Suspense } from 'react'; import { mockConsoleError } from '@asap-hub/dom-test-utils'; import { analytics } from '@asap-hub/routing'; +import { disable, enable } from '@asap-hub/flags'; -import About from '../Routes'; -import { getAnalyticsLeadership } from '../api'; +import Analytics from '../Routes'; +import { getAnalyticsLeadership } from '../leadership/api'; import { Auth0Provider, WhenReady } from '../../auth/test-utils'; -jest.mock('../api'); +jest.mock('../leadership/api'); mockConsoleError(); afterEach(() => { jest.clearAllMocks(); }); -const mockGetMemberships = getAnalyticsLeadership as jest.MockedFunction< - typeof getAnalyticsLeadership ->; +const mockGetAnalyticsLeadership = + getAnalyticsLeadership as jest.MockedFunction; -const renderPage = async () => { - render( +const renderPage = async (path: string) => { + const { container } = render( - + - + @@ -40,13 +41,66 @@ const renderPage = async () => { , ); await waitForElementToBeRemoved(() => screen.queryByText(/loading/i)); + + return container; }; describe('Analytics page', () => { it('renders the Analytics Page successfully', async () => { - mockGetMemberships.mockResolvedValueOnce({ items: [], total: 0 }); + await renderPage( + analytics({}).productivity({}).metric({ metric: 'user' }).$, + ); + expect( + await screen.findByText(/Analytics/i, { + selector: 'h1', + }), + ).toBeVisible(); + }); + + it('redirects to user productivity page when flag is true', async () => { + enable('DISPLAY_ANALYTICS_PRODUCTIVITY'); + + await renderPage(analytics({}).$); + + expect( + await screen.findByText(/User Productivity/i, { + selector: 'h3', + }), + ).toBeVisible(); + }); + + it('redirects to working group page when productivity flag is false', async () => { + disable('DISPLAY_ANALYTICS_PRODUCTIVITY'); + mockGetAnalyticsLeadership.mockResolvedValueOnce({ items: [], total: 0 }); + await renderPage(analytics({}).$); + + expect( + await screen.findByText(/Working Group Leadership & Membership/i, { + selector: 'h3', + }), + ).toBeVisible(); + }); +}); + +describe('Productivity', () => { + it('renders the productivity tab', async () => { + await renderPage( + analytics({}).productivity({}).metric({ metric: 'team' }).$, + ); + expect( + await screen.findByText(/Analytics/i, { + selector: 'h1', + }), + ).toBeVisible(); + }); +}); +describe('Leadership & Membership', () => { + it('renders the Analytics Page successfully', async () => { + mockGetAnalyticsLeadership.mockResolvedValueOnce({ items: [], total: 0 }); - await renderPage(); + await renderPage( + analytics({}).leadership({}).metric({ metric: 'interest-group' }).$, + ); expect( await screen.findByText(/Analytics/i, { selector: 'h1', @@ -55,11 +109,17 @@ describe('Analytics page', () => { }); it('renders error message when the response is not a 2XX', async () => { - mockGetMemberships.mockRejectedValue(new Error('Failed to fetch')); + mockGetAnalyticsLeadership.mockRejectedValueOnce( + new Error('Failed to fetch'), + ); + await renderPage( + analytics({}).leadership({}).metric({ metric: 'interest-group' }).$, + ); - await renderPage(); + await waitFor(() => { + expect(mockGetAnalyticsLeadership).toHaveBeenCalled(); + }); - expect(mockGetMemberships).toHaveBeenCalled(); expect(screen.getByText(/Something went wrong/i)).toBeVisible(); }); }); diff --git a/apps/crn-frontend/src/analytics/Analytics.tsx b/apps/crn-frontend/src/analytics/leadership/Leadership.tsx similarity index 69% rename from apps/crn-frontend/src/analytics/Analytics.tsx rename to apps/crn-frontend/src/analytics/leadership/Leadership.tsx index 67d1775c0c..bc8ee08fc5 100644 --- a/apps/crn-frontend/src/analytics/Analytics.tsx +++ b/apps/crn-frontend/src/analytics/leadership/Leadership.tsx @@ -1,7 +1,10 @@ -import { FC, useState } from 'react'; -import { AnalyticsPageBody } from '@asap-hub/react-components'; +import { FC } from 'react'; +import { AnalyticsLeadershipPageBody } from '@asap-hub/react-components'; +import { useHistory, useParams } from 'react-router-dom'; +import { analytics } from '@asap-hub/routing'; + import { useAnalyticsLeadership } from './state'; -import { usePagination, usePaginationParams } from '../hooks'; +import { usePagination, usePaginationParams } from '../../hooks'; type MetricResponse = { id: string; @@ -19,9 +22,9 @@ type MetricResponse = { const getDataForMetric = ( data: MetricResponse[], - metric: 'workingGroup' | 'interestGroup', + metric: 'working-group' | 'interest-group', ) => { - if (metric === 'workingGroup') { + if (metric === 'working-group') { return data.map((row) => ({ id: row.id, name: row.displayName, @@ -41,21 +44,27 @@ const getDataForMetric = ( })); }; -const About: FC> = () => { - const [metric, setMetric] = useState<'workingGroup' | 'interestGroup'>( - 'workingGroup', - ); +const Leadership: FC> = () => { + const history = useHistory(); + const { metric } = useParams<{ + metric: 'working-group' | 'interest-group'; + }>(); + const setMetric = (newMetric: 'working-group' | 'interest-group') => + history.push(analytics({}).leadership({}).metric({ metric: newMetric }).$); + const { currentPage, pageSize } = usePaginationParams(); + const { items, total } = useAnalyticsLeadership({ currentPage, pageSize, searchQuery: '', filters: new Set(), }); + const { numberOfPages, renderPageHref } = usePagination(total, pageSize); return ( - > = () => { ); }; -export default About; +export default Leadership; diff --git a/apps/crn-frontend/src/analytics/__tests__/Analytics.test.tsx b/apps/crn-frontend/src/analytics/leadership/__tests__/Leadership.test.tsx similarity index 86% rename from apps/crn-frontend/src/analytics/__tests__/Analytics.test.tsx rename to apps/crn-frontend/src/analytics/leadership/__tests__/Leadership.test.tsx index 16111d8840..319e95ba70 100644 --- a/apps/crn-frontend/src/analytics/__tests__/Analytics.test.tsx +++ b/apps/crn-frontend/src/analytics/leadership/__tests__/Leadership.test.tsx @@ -6,10 +6,11 @@ import { import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Suspense } from 'react'; -import { MemoryRouter } from 'react-router-dom'; +import { MemoryRouter, Route } from 'react-router-dom'; import { RecoilRoot } from 'recoil'; +import { analytics } from '@asap-hub/routing'; -import Analytics from '../Analytics'; +import Leadership from '../Leadership'; import { getAnalyticsLeadership } from '../api'; import { analyticsLeadershipState } from '../state'; @@ -53,7 +54,9 @@ const data: ListAnalyticsTeamLeadershipResponse = { ], }; -const renderPage = async () => { +const renderPage = async ( + path = analytics({}).leadership({}).metric({ metric: 'working-group' }).$, +) => { const result = render( { @@ -68,8 +71,10 @@ const renderPage = async () => { - - + + + + diff --git a/apps/crn-frontend/src/analytics/__tests__/api.test.tsx b/apps/crn-frontend/src/analytics/leadership/__tests__/api.test.tsx similarity index 96% rename from apps/crn-frontend/src/analytics/__tests__/api.test.tsx rename to apps/crn-frontend/src/analytics/leadership/__tests__/api.test.tsx index ae202e5dd2..a34e32bfbf 100644 --- a/apps/crn-frontend/src/analytics/__tests__/api.test.tsx +++ b/apps/crn-frontend/src/analytics/leadership/__tests__/api.test.tsx @@ -3,9 +3,9 @@ import { AlgoliaSearchClient, ClientSearchResponse } from '@asap-hub/algolia'; import { GetListOptions } from '@asap-hub/frontend-utils'; import { AnalyticsTeamLeadershipResponse } from '@asap-hub/model'; import { getAnalyticsLeadership } from '../api'; -import { createAlgoliaResponse } from '../../__fixtures__/algolia'; +import { createAlgoliaResponse } from '../../../__fixtures__/algolia'; -jest.mock('../../config'); +jest.mock('../../../config'); afterEach(() => { nock.cleanAll(); diff --git a/apps/crn-frontend/src/analytics/api.ts b/apps/crn-frontend/src/analytics/leadership/api.ts similarity index 100% rename from apps/crn-frontend/src/analytics/api.ts rename to apps/crn-frontend/src/analytics/leadership/api.ts diff --git a/apps/crn-frontend/src/analytics/state.ts b/apps/crn-frontend/src/analytics/leadership/state.ts similarity index 97% rename from apps/crn-frontend/src/analytics/state.ts rename to apps/crn-frontend/src/analytics/leadership/state.ts index 3edc46a1c0..dc3e87652f 100644 --- a/apps/crn-frontend/src/analytics/state.ts +++ b/apps/crn-frontend/src/analytics/leadership/state.ts @@ -9,7 +9,7 @@ import { selectorFamily, useRecoilState, } from 'recoil'; -import { useAnalyticsAlgolia } from '../hooks/algolia'; +import { useAnalyticsAlgolia } from '../../hooks/algolia'; import { getAnalyticsLeadership } from './api'; const analyticsLeadershipIndexState = atomFamily< diff --git a/apps/crn-frontend/src/analytics/productivity/Productivity.tsx b/apps/crn-frontend/src/analytics/productivity/Productivity.tsx new file mode 100644 index 0000000000..e0f6106611 --- /dev/null +++ b/apps/crn-frontend/src/analytics/productivity/Productivity.tsx @@ -0,0 +1,116 @@ +import { useHistory, useParams } from 'react-router-dom'; +import { analytics } from '@asap-hub/routing'; +import { + AnalyticsProductivityPageBody, + TeamProductivityMetric, + UserProductivityMetric, +} from '@asap-hub/react-components'; +import { usePagination, usePaginationParams } from '../../hooks'; + +const Productivity = () => { + const history = useHistory(); + const { metric } = useParams<{ metric: 'user' | 'team' }>(); + const setMetric = (newMetric: 'user' | 'team') => + history.push( + analytics({}).productivity({}).metric({ metric: newMetric }).$, + ); + + const { currentPage, pageSize } = usePaginationParams(); + + const userItems: UserProductivityMetric[] = [ + { + id: '1', + name: 'User A', + alumni: false, + teams: [{ name: 'Team A', active: true }], + roles: ['Role A'], + asapOutput: 1, + asapPublicOutput: 2, + ratio: 1, + }, + { + id: '2', + name: 'User B', + alumni: true, + teams: [{ name: 'Team B', active: false }], + roles: ['Role B'], + asapOutput: 1, + asapPublicOutput: 2, + ratio: 1, + }, + { + id: '3', + name: 'User C', + alumni: false, + teams: [ + { name: 'Team A', active: true }, + { name: 'Team C', active: false }, + ], + roles: ['Role A', 'Role B'], + asapOutput: 1, + asapPublicOutput: 2, + ratio: 1, + }, + { + id: '4', + name: 'User D', + alumni: false, + teams: [], + roles: [], + asapOutput: 1, + asapPublicOutput: 2, + ratio: 1, + }, + ]; + const teamItems: TeamProductivityMetric[] = [ + { + id: '1', + name: 'Team A', + active: true, + articles: 1, + bioinformatics: 2, + datasets: 3, + labResources: 4, + protocols: 5, + }, + { + id: '2', + name: 'Team B', + active: true, + articles: 3, + bioinformatics: 2, + datasets: 2, + labResources: 4, + protocols: 3, + }, + { + id: '3', + name: 'Team C', + active: false, + articles: 2, + bioinformatics: 2, + datasets: 2, + labResources: 4, + protocols: 2, + }, + ]; + + const { numberOfPages, renderPageHref } = usePagination( + metric === 'user' ? userItems.length : teamItems.length, + pageSize, + ); + + return ( + + ); +}; + +export default Productivity; diff --git a/apps/crn-frontend/src/analytics/productivity/__tests__/Productivity.test.tsx b/apps/crn-frontend/src/analytics/productivity/__tests__/Productivity.test.tsx new file mode 100644 index 0000000000..bafb9c6a8d --- /dev/null +++ b/apps/crn-frontend/src/analytics/productivity/__tests__/Productivity.test.tsx @@ -0,0 +1,56 @@ +import { + Auth0Provider, + WhenReady, +} from '@asap-hub/crn-frontend/src/auth/test-utils'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { Suspense } from 'react'; +import { MemoryRouter, Route } from 'react-router-dom'; +import { analytics } from '@asap-hub/routing'; +import { RecoilRoot } from 'recoil'; + +import Productivity from '../Productivity'; + +const renderPage = async ( + path = analytics({}).productivity({}).metric({ metric: 'user' }).$, +) => { + const result = render( + + + + + + + + + + + + + , + , + ); + + await waitFor(() => + expect(result.queryByText(/loading/i)).not.toBeInTheDocument(), + ); + + return result; +}; + +it('renders with user data', async () => { + await renderPage(); + expect(screen.getAllByText('User Productivity').length).toBe(2); +}); + +it('renders with team data', async () => { + const label = 'Team Productivity'; + + await renderPage(); + const input = screen.getByRole('textbox', { hidden: false }); + + userEvent.click(input); + userEvent.click(screen.getByText(label)); + + expect(screen.getAllByText(label).length).toBe(2); +}); diff --git a/apps/storybook/src/StateTag.stories.tsx b/apps/storybook/src/StateTag.stories.tsx index 89057bd6c3..415289694b 100644 --- a/apps/storybook/src/StateTag.stories.tsx +++ b/apps/storybook/src/StateTag.stories.tsx @@ -1,6 +1,6 @@ import { alumniBadgeIcon, - inactiveBadgeIcon, + InactiveBadgeIcon, StateTag, } from '@asap-hub/react-components'; @@ -11,5 +11,5 @@ export default { export const Alumni = () => ; export const Inactive = () => ( - + } label="Inactive" /> ); diff --git a/packages/flags/src/index.ts b/packages/flags/src/index.ts index 47982a9e09..48e5f3f2e9 100644 --- a/packages/flags/src/index.ts +++ b/packages/flags/src/index.ts @@ -1,13 +1,15 @@ export type Flag = | 'PERSISTENT_EXAMPLE' | 'VERSION_RESEARCH_OUTPUT' - | 'DISPLAY_EVENTS'; + | 'DISPLAY_EVENTS' + | 'DISPLAY_ANALYTICS_PRODUCTIVITY'; export type Flags = Partial>; let overrides: Flags = { // flags already live in prod: // can also be used to manually disable a flag in development: DISPLAY_EVENTS: false, + DISPLAY_ANALYTICS_PRODUCTIVITY: false, }; const envDefaults: Record = { diff --git a/packages/react-components/src/atoms/TabLink.tsx b/packages/react-components/src/atoms/TabLink.tsx index f550d9d497..6747cc293b 100644 --- a/packages/react-components/src/atoms/TabLink.tsx +++ b/packages/react-components/src/atoms/TabLink.tsx @@ -1,12 +1,13 @@ /** @jsxImportSource @emotion/react */ -import { ReactNode } from 'react'; +import { ComponentType, ReactNode } from 'react'; import { NavLink } from 'react-router-dom'; import { css, Theme } from '@emotion/react'; import { layoutStyles } from '../text'; -import { rem } from '../pixels'; +import { perRem, rem } from '../pixels'; import { fern, lead, charcoal } from '../colors'; import { useHasRouter } from '../routing'; +import IconProps from '../icons/props'; const activeClassName = 'active-link'; const styles = css({ @@ -27,11 +28,37 @@ const activeStyles = ({ colors: { primary500 = fern } = {} }: Theme) => fontWeight: 'bold', }); +const iconStyles = css({ + display: 'inline-grid', + verticalAlign: 'middle', + paddingRight: `${6 / perRem}em`, +}); + interface TabLinkProps { readonly href: string; + readonly Icon?: ComponentType; readonly children: ReactNode; } -const TabLink: React.FC = ({ href, children }) => { +const TabLink: React.FC = ({ href, children, Icon }) => { + const active = + new URL(href, window.location.href).pathname === window.location.pathname; + + const inner = ( +

[ + layoutStyles, + components?.TabLink?.layoutStyles, + ]} + > + {Icon && ( + + + + )} + {children} +

+ ); + if (useHasRouter()) { return ( = ({ href, children }) => { { [`&.${activeClassName}`]: activeStyles(theme) }, ]} > -

[ - layoutStyles, - components?.TabLink?.layoutStyles, - ]} - > - {children} -

+ {inner}
); } - const active = - new URL(href, window.location.href).pathname === window.location.pathname; return ( = ({ href, children }) => { active && activeStyles(theme), ]} > -

[ - layoutStyles, - components?.TabLink?.layoutStyles, - ]} - > - {children} -

+ {inner}
); }; diff --git a/packages/react-components/src/icons/alumni-badge.tsx b/packages/react-components/src/icons/alumni-badge.tsx index 3451338151..3ac33f628b 100644 --- a/packages/react-components/src/icons/alumni-badge.tsx +++ b/packages/react-components/src/icons/alumni-badge.tsx @@ -6,7 +6,7 @@ const alumniBadge = ( fill="none" xmlns="http://www.w3.org/2000/svg" > - Alumni Badge + Alumni Member = ({ + entityName = 'Team', +}) => ( - Inactive + Inactive {entityName} ); -export default inactiveBadgeIcon; +export default InactiveBadgeIcon; diff --git a/packages/react-components/src/icons/index.ts b/packages/react-components/src/icons/index.ts index 8bbf0a6252..e66cc29735 100644 --- a/packages/react-components/src/icons/index.ts +++ b/packages/react-components/src/icons/index.ts @@ -51,7 +51,7 @@ export { default as gp2Logo } from './gp2-logo'; export { default as grantDocument } from './grantDocument'; export { default as InterestGroupsIcon } from './interest-groups'; export { default as hidePasswordIcon } from './hide-password'; -export { default as inactiveBadgeIcon } from './inactive-badge'; +export { default as InactiveBadgeIcon } from './inactive-badge'; export { default as infoCircleIcon } from './info-circle'; export { default as infoCircleYellowIcon } from './info-circle-yellow'; export { default as informationIcon } from './information'; @@ -84,6 +84,7 @@ export { default as placeholderIcon } from './placeholder'; export { default as plusIcon } from './plus'; export { default as policyIcon } from './policy'; export { default as previousPageIcon } from './previous-page'; +export { default as ProductivityIcon } from './productivity'; export { default as protocol } from './protocol'; export { default as protocolsIcon } from './protocols'; export { default as reportIcon } from './report'; diff --git a/packages/react-components/src/icons/leadership.tsx b/packages/react-components/src/icons/leadership.tsx index b0ab51d8b1..8289619d57 100644 --- a/packages/react-components/src/icons/leadership.tsx +++ b/packages/react-components/src/icons/leadership.tsx @@ -1,11 +1,13 @@ /* istanbul ignore file */ import { FC } from 'react'; +import IconProps from './props'; -interface LeadershipIconProps { +interface LeadershipIconProps extends IconProps { readonly color?: string; readonly size?: number; } + const Leadership: FC = ({ color = '#4D646B', size = 24, diff --git a/packages/react-components/src/icons/productivity.tsx b/packages/react-components/src/icons/productivity.tsx new file mode 100644 index 0000000000..18f093038e --- /dev/null +++ b/packages/react-components/src/icons/productivity.tsx @@ -0,0 +1,32 @@ +/* istanbul ignore file */ + +import { FC } from 'react'; +import IconProps from './props'; + +interface ProductivityIconProps extends IconProps { + readonly color?: string; + readonly size?: number; +} + +const Productivity: FC = ({ + color = '#4D646B', + size = 24, +}) => ( + + Productivity + + +); + +export default Productivity; diff --git a/packages/react-components/src/icons/props.tsx b/packages/react-components/src/icons/props.tsx new file mode 100644 index 0000000000..908cad8e79 --- /dev/null +++ b/packages/react-components/src/icons/props.tsx @@ -0,0 +1,5 @@ +interface IconProps { + color?: string; +} + +export default IconProps; diff --git a/packages/react-components/src/index.ts b/packages/react-components/src/index.ts index e9ae907a66..bdf73b3d14 100644 --- a/packages/react-components/src/index.ts +++ b/packages/react-components/src/index.ts @@ -186,14 +186,21 @@ export { UserTeamsTabbedCard, WorkingGroupCard, WorkingGroupMembers, + UserProductivityTable, + TeamProductivityTable, WorkingGroupsTabbedCard, } from './organisms'; +export type { + UserProductivityMetric, + TeamProductivityMetric, +} from './organisms'; export { AboutPage, AboutPageBody, AboutPageHeader, AnalyticsPage, - AnalyticsPageBody, + AnalyticsProductivityPageBody, + AnalyticsLeadershipPageBody, AnalyticsPageHeader, BasicLayout, BiographyModal, diff --git a/packages/react-components/src/molecules/AssociationList.tsx b/packages/react-components/src/molecules/AssociationList.tsx index 6fcc184fff..6a7038156f 100644 --- a/packages/react-components/src/molecules/AssociationList.tsx +++ b/packages/react-components/src/molecules/AssociationList.tsx @@ -5,7 +5,7 @@ import { network } from '@asap-hub/routing'; import { LabIcon, TeamIcon, - inactiveBadgeIcon, + InactiveBadgeIcon, WorkingGroupsIcon, } from '../icons'; import { Avatar, Link } from '../atoms'; @@ -134,7 +134,9 @@ const AssociationList: FC = ({ {type} {displayName} {inactiveSince && ( - {inactiveBadgeIcon} + + + )} )} diff --git a/packages/react-components/src/molecules/EventOwner.tsx b/packages/react-components/src/molecules/EventOwner.tsx index 8b088c2793..12227575e7 100644 --- a/packages/react-components/src/molecules/EventOwner.tsx +++ b/packages/react-components/src/molecules/EventOwner.tsx @@ -6,7 +6,7 @@ import { Link } from '../atoms'; import { neutral900 } from '../colors'; import { InterestGroupsIcon, - inactiveBadgeIcon, + InactiveBadgeIcon, WorkingGroupsIcon, } from '../icons'; import { rem } from '../pixels'; @@ -51,7 +51,9 @@ const EventOwner: React.FC< {interestGroup.name} {!interestGroup.active && ( - {inactiveBadgeIcon} + + + )} ) : workingGroup ? ( diff --git a/packages/react-components/src/molecules/__tests__/AssociationList.test.tsx b/packages/react-components/src/molecules/__tests__/AssociationList.test.tsx index 79067a0659..36eb26bff1 100644 --- a/packages/react-components/src/molecules/__tests__/AssociationList.test.tsx +++ b/packages/react-components/src/molecules/__tests__/AssociationList.test.tsx @@ -47,7 +47,7 @@ describe('Teams', () => { ]} />, ); - expect(getByTitle('Inactive')).toBeInTheDocument(); + expect(getByTitle('Inactive Team')).toBeInTheDocument(); }); it('display type in plural if there is more than one', () => { diff --git a/packages/react-components/src/molecules/__tests__/EventOwner.test.tsx b/packages/react-components/src/molecules/__tests__/EventOwner.test.tsx index e48cef37b5..3021c96b89 100644 --- a/packages/react-components/src/molecules/__tests__/EventOwner.test.tsx +++ b/packages/react-components/src/molecules/__tests__/EventOwner.test.tsx @@ -52,5 +52,5 @@ it('displays inactive badge when a group is inactive', () => { />, ); - expect(screen.getByTitle('Inactive')).toBeInTheDocument(); + expect(screen.getByTitle('Inactive Interest Group')).toBeInTheDocument(); }); diff --git a/packages/react-components/src/molecules/__tests__/EventTeams.test.tsx b/packages/react-components/src/molecules/__tests__/EventTeams.test.tsx index b030c1a16f..86de2f9f2f 100644 --- a/packages/react-components/src/molecules/__tests__/EventTeams.test.tsx +++ b/packages/react-components/src/molecules/__tests__/EventTeams.test.tsx @@ -90,5 +90,5 @@ it('displays inactive badge when a team is inactive', () => { />, ); - expect(screen.getByTitle('Inactive')).toBeInTheDocument(); + expect(screen.getByTitle('Inactive Team')).toBeInTheDocument(); }); diff --git a/packages/react-components/src/molecules/__tests__/MembersList.test.tsx b/packages/react-components/src/molecules/__tests__/MembersList.test.tsx index 64dd1d92d5..4a305d0c13 100644 --- a/packages/react-components/src/molecules/__tests__/MembersList.test.tsx +++ b/packages/react-components/src/molecules/__tests__/MembersList.test.tsx @@ -115,7 +115,7 @@ it('renders alumni badge when there is an alumni member', async () => { />, ); - expect(queryByTitle('Alumni Badge')).toBeInTheDocument(); + expect(queryByTitle('Alumni Member')).toBeInTheDocument(); rerender( { />, ); - expect(queryByTitle('Alumni Badge')).not.toBeInTheDocument(); + expect(queryByTitle('Alumni Member')).not.toBeInTheDocument(); }); it('overrides user link based on based on the overrideUserRoute prop fn', () => { diff --git a/packages/react-components/src/molecules/__tests__/UsersList.test.tsx b/packages/react-components/src/molecules/__tests__/UsersList.test.tsx index 2a0540f74c..c9bc13d117 100644 --- a/packages/react-components/src/molecules/__tests__/UsersList.test.tsx +++ b/packages/react-components/src/molecules/__tests__/UsersList.test.tsx @@ -66,7 +66,7 @@ describe('alumni badge', () => { ]} />, ); - expect(getByText('Alumni Badge')).toBeInTheDocument(); + expect(getByText('Alumni Member')).toBeInTheDocument(); }); it('does not show alumni badge if user is not alumni', () => { const { queryByText } = render( @@ -79,7 +79,7 @@ describe('alumni badge', () => { ]} />, ); - expect(queryByText('Alumni Badge')).not.toBeInTheDocument(); + expect(queryByText('Alumni Member')).not.toBeInTheDocument(); }); it('does not show alumni badge for external authors', () => { const { queryByText } = render( @@ -93,7 +93,7 @@ describe('alumni badge', () => { ]} />, ); - expect(queryByText('Alumni Badge')).not.toBeInTheDocument(); + expect(queryByText('Alumni Member')).not.toBeInTheDocument(); }); }); diff --git a/packages/react-components/src/organisms/InterestGroupCard.tsx b/packages/react-components/src/organisms/InterestGroupCard.tsx index 39bd9d9c75..7fd2625cca 100644 --- a/packages/react-components/src/organisms/InterestGroupCard.tsx +++ b/packages/react-components/src/organisms/InterestGroupCard.tsx @@ -3,7 +3,7 @@ import { InterestGroupResponse } from '@asap-hub/model'; import { network } from '@asap-hub/routing'; import { Paragraph, StateTag } from '../atoms'; -import { inactiveBadgeIcon, TeamIcon } from '../icons'; +import { InactiveBadgeIcon, TeamIcon } from '../icons'; import { rem } from '../pixels'; import EntityCard from './EntityCard'; @@ -47,7 +47,12 @@ const InterestGroupCard: React.FC = ({ footer={footer} googleDrive={googleDrive} href={href} - inactiveBadge={} + inactiveBadge={ + } + label="Inactive" + /> + } tags={tags.map((tag) => tag.name)} text={description} title={name} diff --git a/packages/react-components/src/organisms/InterestGroupTeamsTabbedCard.tsx b/packages/react-components/src/organisms/InterestGroupTeamsTabbedCard.tsx index 4fcd1925cf..cb904f4b78 100644 --- a/packages/react-components/src/organisms/InterestGroupTeamsTabbedCard.tsx +++ b/packages/react-components/src/organisms/InterestGroupTeamsTabbedCard.tsx @@ -3,7 +3,7 @@ import { network } from '@asap-hub/routing'; import { css } from '@emotion/react'; import React, { ComponentProps } from 'react'; import { Link, Paragraph } from '../atoms'; -import { inactiveBadgeIcon, TeamIcon } from '../icons'; +import { InactiveBadgeIcon, TeamIcon } from '../icons'; import { TabbedCard } from '../molecules'; import { perRem, rem, tabletScreen } from '../pixels'; import { splitListBy } from '../utils'; @@ -90,7 +90,9 @@ const GroupTeamsTabbedCard: React.FC = ({ {displayName} {inactiveSince && ( - {inactiveBadgeIcon} + + + )} ))} diff --git a/packages/react-components/src/organisms/SpeakerList.tsx b/packages/react-components/src/organisms/SpeakerList.tsx index f11256940c..76eb06b7ad 100644 --- a/packages/react-components/src/organisms/SpeakerList.tsx +++ b/packages/react-components/src/organisms/SpeakerList.tsx @@ -9,7 +9,7 @@ import { chevronCircleDownIcon, chevronCircleUpIcon, alumniBadgeIcon, - inactiveBadgeIcon, + InactiveBadgeIcon, } from '../icons'; import { useDateHasPassed } from '../date'; import { considerEndedAfter } from '../utils'; @@ -199,7 +199,9 @@ const SpeakerList: React.FC = ({ speakers, endDate }) => { <> {speaker.team.displayName} {speaker.team.inactiveSince && ( - {inactiveBadgeIcon} + + + )} diff --git a/packages/react-components/src/organisms/TeamCard.tsx b/packages/react-components/src/organisms/TeamCard.tsx index a926f43fc3..6078223b6c 100644 --- a/packages/react-components/src/organisms/TeamCard.tsx +++ b/packages/react-components/src/organisms/TeamCard.tsx @@ -5,7 +5,7 @@ import { network } from '@asap-hub/routing'; import { StateTag } from '../atoms'; import { mobileScreen, rem } from '../pixels'; import { lead } from '../colors'; -import { TeamIcon, LabIcon, inactiveBadgeIcon } from '../icons'; +import { TeamIcon, LabIcon, InactiveBadgeIcon } from '../icons'; import { getCounterString } from '../utils'; import { EntityCard } from '.'; @@ -63,7 +63,7 @@ const TeamCard: React.FC = ({ active={!inactiveSince} footer={footer} href={href} - inactiveBadge={} + inactiveBadge={} label="Inactive" />} tags={tags.map(({ name }) => name)} text={projectTitle} title={`Team ${displayName}`} diff --git a/packages/react-components/src/organisms/TeamProductivityTable.tsx b/packages/react-components/src/organisms/TeamProductivityTable.tsx new file mode 100644 index 0000000000..bc234d7086 --- /dev/null +++ b/packages/react-components/src/organisms/TeamProductivityTable.tsx @@ -0,0 +1,109 @@ +import { css } from '@emotion/react'; +import { Card } from '../atoms'; +import { charcoal, neutral200, steel } from '../colors'; +import { rem, tabletScreen } from '../pixels'; +import { borderRadius } from '../card'; +import { InactiveBadgeIcon } from '../icons'; + +const container = css({ + display: 'grid', + paddingTop: rem(32), +}); + +const gridTitleStyles = css({ + display: 'none', + [`@media (min-width: ${tabletScreen.min}px)`]: { + display: 'inherit', + paddingBottom: rem(16), + }, +}); + +const rowTitleStyles = css({ + paddingTop: rem(32), + paddingBottom: rem(16), + ':first-of-type': { paddingTop: 0 }, + [`@media (min-width: ${tabletScreen.min}px)`]: { display: 'none' }, +}); + +const rowStyles = css({ + display: 'grid', + padding: `${rem(20)} ${rem(24)} 0`, + borderBottom: `1px solid ${steel.rgb}`, + ':first-of-type': { + borderBottom: 'none', + }, + ':nth-of-type(2n+3)': { + background: neutral200.rgb, + }, + ':last-child': { + borderBottom: 'none', + marginBottom: 0, + paddingBottom: rem(15), + borderRadius: rem(borderRadius), + }, + [`@media (min-width: ${tabletScreen.min}px)`]: { + gridTemplateColumns: '1fr 1fr 1fr 1fr 1fr 1fr', + columnGap: rem(15), + paddingTop: 0, + paddingBottom: 0, + borderBottom: `1px solid ${steel.rgb}`, + }, +}); + +const titleStyles = css({ fontWeight: 'bold', color: charcoal.rgb }); + +const iconStyles = css({ + display: 'flex', + gap: rem(3), +}); + +export type TeamProductivityMetric = { + id: string; + name: string; + active: boolean; + articles: number; + bioinformatics: number; + datasets: number; + labResources: number; + protocols: number; +}; +interface TeamProductivityTableProps { + data: TeamProductivityMetric[]; +} + +const TeamProductivityTable: React.FC = ({ + data, +}) => ( + +
+
+ Team + Articles + Bioinformatics + Datasets + Lab Resources + Protocols +
+ {data.map((row) => ( +
+ Team +

+ {row.name} {!row.active && } +

+ Articles +

{row.articles}

+ Bioinformatics +

{row.bioinformatics}

+ Datasets +

{row.datasets}

+ Lab Resources +

{row.labResources}

+ Protocols +

{row.protocols}

+
+ ))} +
+
+); + +export default TeamProductivityTable; diff --git a/packages/react-components/src/organisms/UserProductivityTable.tsx b/packages/react-components/src/organisms/UserProductivityTable.tsx new file mode 100644 index 0000000000..c12055b683 --- /dev/null +++ b/packages/react-components/src/organisms/UserProductivityTable.tsx @@ -0,0 +1,164 @@ +import { css } from '@emotion/react'; +import { Card } from '../atoms'; +import { charcoal, lead, neutral200, steel } from '../colors'; +import { rem, tabletScreen } from '../pixels'; +import { borderRadius } from '../card'; +import { alumniBadgeIcon, InactiveBadgeIcon } from '../icons'; + +const container = css({ + display: 'grid', + paddingTop: rem(32), +}); + +const gridTitleStyles = css({ + display: 'none', + [`@media (min-width: ${tabletScreen.min}px)`]: { + display: 'inherit', + paddingBottom: rem(16), + }, +}); + +const rowTitleStyles = css({ + paddingTop: rem(32), + paddingBottom: rem(16), + ':first-of-type': { paddingTop: 0 }, + [`@media (min-width: ${tabletScreen.min}px)`]: { display: 'none' }, +}); + +const rowStyles = css({ + display: 'grid', + padding: `${rem(20)} ${rem(24)} 0`, + borderBottom: `1px solid ${steel.rgb}`, + ':first-of-type': { + borderBottom: 'none', + }, + ':nth-of-type(2n+3)': { + background: neutral200.rgb, + }, + ':last-child': { + borderBottom: 'none', + marginBottom: 0, + paddingBottom: rem(15), + borderRadius: rem(borderRadius), + }, + [`@media (min-width: ${tabletScreen.min}px)`]: { + gridTemplateColumns: '1fr 1fr 1fr 1fr 1.1fr 0.5fr', + columnGap: rem(15), + paddingTop: 0, + paddingBottom: 0, + borderBottom: `1px solid ${steel.rgb}`, + }, +}); + +const titleStyles = css({ fontWeight: 'bold', color: charcoal.rgb }); +const counterStyle = css({ + display: 'inline-flex', + color: lead.rgb, + marginLeft: rem(9), + textAlign: 'center', + minWidth: rem(24), + border: `1px solid ${steel.rgb}`, + borderRadius: '100%', + fontSize: '14px', + fontWeight: 'bold', + + justifyContent: 'center', + alignItems: 'center', + width: rem(24), + height: rem(24), +}); + +const iconStyles = css({ + display: 'flex', + gap: rem(3), +}); + +type Team = { + name: string; + active: boolean; +}; + +const displayTeams = (items: { name: string; active: boolean }[]) => { + if (items.length === 0) { + return `No team`; + } + if (items.length === 1) { + return items[0]?.active ? ( + items[0].name + ) : ( + + {items[0]?.name} + + ); + } + return ( + <> + Multiple teams{items.length} + + ); +}; + +const displayRoles = (items: string[]) => { + if (items.length === 0) { + return `No role`; + } + if (items.length === 1) { + return items[0]; + } + return ( + <> + Multiple roles{items.length} + + ); +}; + +export type UserProductivityMetric = { + id: string; + alumni: boolean; + name: string; + teams: Team[]; + roles: string[]; + asapOutput: number; + asapPublicOutput: number; + ratio: number; +}; +interface UserProductivityTableProps { + data: UserProductivityMetric[]; +} + +const UserProductivityTable: React.FC = ({ + data, +}) => ( + +
+
+ User + Team + Role + ASAP Output + ASAP Public Output + Ratio +
+ {data.map((row) => ( +
+ User +

+ {row.name} {row.alumni && alumniBadgeIcon} +

+ Team +

{displayTeams(row.teams)}

+ Role +

{displayRoles(row.roles)}

+ ASAP Output +

{row.asapOutput}

+ ASAP Public Output +

{row.asapPublicOutput}

+ Ratio +

{row.ratio}

+
+ ))} +
+
+); + +export default UserProductivityTable; diff --git a/packages/react-components/src/organisms/UserTeamsTabbedCard.tsx b/packages/react-components/src/organisms/UserTeamsTabbedCard.tsx index 161c7b0584..da598de080 100644 --- a/packages/react-components/src/organisms/UserTeamsTabbedCard.tsx +++ b/packages/react-components/src/organisms/UserTeamsTabbedCard.tsx @@ -3,7 +3,7 @@ import { network } from '@asap-hub/routing'; import { css } from '@emotion/react'; import React, { Fragment } from 'react'; import { Divider, Link, Paragraph } from '../atoms'; -import { inactiveBadgeIcon } from '../icons'; +import { InactiveBadgeIcon } from '../icons'; import { TabbedCard } from '../molecules'; import { perRem, rem, tabletScreen } from '../pixels'; import { splitListBy } from '../utils'; @@ -148,7 +148,7 @@ const UserTeamsTabbedCard: React.FC = ({ Team {displayName} {teamInactiveSince && ( - {inactiveBadgeIcon} + )} diff --git a/packages/react-components/src/organisms/__tests__/InterestGroupCard.test.tsx b/packages/react-components/src/organisms/__tests__/InterestGroupCard.test.tsx index 21b01fe019..abbd96b66d 100644 --- a/packages/react-components/src/organisms/__tests__/InterestGroupCard.test.tsx +++ b/packages/react-components/src/organisms/__tests__/InterestGroupCard.test.tsx @@ -30,7 +30,7 @@ it('renders the state tag for a inactive group', () => { , ); expect(getByText('Inactive', { selector: 'span' })).toBeVisible(); - expect(getByTitle('Inactive')).toBeInTheDocument(); + expect(getByTitle('Inactive Interest Group')).toBeInTheDocument(); rerender(); expect(queryByText('Inactive')).not.toBeInTheDocument(); }); diff --git a/packages/react-components/src/organisms/__tests__/PeopleCard.test.tsx b/packages/react-components/src/organisms/__tests__/PeopleCard.test.tsx index 809292d68a..092ae68fa5 100644 --- a/packages/react-components/src/organisms/__tests__/PeopleCard.test.tsx +++ b/packages/react-components/src/organisms/__tests__/PeopleCard.test.tsx @@ -58,12 +58,12 @@ describe('alumni badge', () => { render(); expect(screen.getByText('Alumni')).toBeInTheDocument(); - expect(screen.getByTitle('Alumni Badge')).toBeInTheDocument(); + expect(screen.getByTitle('Alumni Member')).toBeInTheDocument(); }); it('does not render alumni badge for non alumni', () => { render(); expect(screen.queryByText('Alumni')).not.toBeInTheDocument(); - expect(screen.queryByTitle('Alumni Badge')).not.toBeInTheDocument(); + expect(screen.queryByTitle('Alumni Member')).not.toBeInTheDocument(); }); }); diff --git a/packages/react-components/src/organisms/__tests__/SpeakerList.test.tsx b/packages/react-components/src/organisms/__tests__/SpeakerList.test.tsx index 841e1cf9fe..b2ed484ce7 100644 --- a/packages/react-components/src/organisms/__tests__/SpeakerList.test.tsx +++ b/packages/react-components/src/organisms/__tests__/SpeakerList.test.tsx @@ -77,7 +77,7 @@ describe('When rendering the speaker list', () => { }; render(); - expect(screen.getByTitle('Alumni Badge')).toBeInTheDocument(); + expect(screen.getByTitle('Alumni Member')).toBeInTheDocument(); }); it('Renders an announced user and checks the grid labels and role', async () => { @@ -196,7 +196,7 @@ describe('When rendering the speaker list', () => { />, ); - expect(screen.getByTitle('Inactive')).toBeInTheDocument(); + expect(screen.getByTitle('Inactive Team')).toBeInTheDocument(); }); }); }); diff --git a/packages/react-components/src/organisms/__tests__/TeamCard.test.tsx b/packages/react-components/src/organisms/__tests__/TeamCard.test.tsx index 1917ba4a7d..cdc74258ce 100644 --- a/packages/react-components/src/organisms/__tests__/TeamCard.test.tsx +++ b/packages/react-components/src/organisms/__tests__/TeamCard.test.tsx @@ -33,7 +33,7 @@ it('renders the state tag for a inactive group', () => { , ); expect(getByText('Inactive', { selector: 'span' })).toBeVisible(); - expect(getByTitle('Inactive')).toBeInTheDocument(); + expect(getByTitle('Inactive Team')).toBeInTheDocument(); rerender(); expect(queryByText('Inactive')).not.toBeInTheDocument(); }); diff --git a/packages/react-components/src/organisms/__tests__/TeamMembersTabbedCard.test.tsx b/packages/react-components/src/organisms/__tests__/TeamMembersTabbedCard.test.tsx index 22e2bd4adc..af0f0bddc1 100644 --- a/packages/react-components/src/organisms/__tests__/TeamMembersTabbedCard.test.tsx +++ b/packages/react-components/src/organisms/__tests__/TeamMembersTabbedCard.test.tsx @@ -48,7 +48,7 @@ it('renders the members tabbed card', () => { expect(screen.getByText(member.role)).toBeInTheDocument(); }); - expect(screen.getAllByTitle(/Alumni Badge/i)).toHaveLength(1); + expect(screen.getAllByTitle(/Alumni Member/i)).toHaveLength(1); expect(screen.getByText(`${labName} Lab`)).toBeInTheDocument(); }); diff --git a/packages/react-components/src/organisms/__tests__/TeamProductivityTable.test.tsx b/packages/react-components/src/organisms/__tests__/TeamProductivityTable.test.tsx new file mode 100644 index 0000000000..8348019882 --- /dev/null +++ b/packages/react-components/src/organisms/__tests__/TeamProductivityTable.test.tsx @@ -0,0 +1,32 @@ +import { render } from '@testing-library/react'; +import TeamProductivityTable from '../TeamProductivityTable'; + +describe('TeamProductivityTable', () => { + const team = { + id: '1', + name: 'Test Team', + active: true, + articles: 1, + bioinformatics: 2, + datasets: 3, + labResources: 4, + protocols: 5, + }; + + it('renders data', () => { + const data = [team]; + const { getByText } = render(); + expect(getByText('Test Team')).toBeInTheDocument(); + }); + + it('renders inactive badge', () => { + const data = [ + { + ...team, + active: false, + }, + ]; + const { getByTitle } = render(); + expect(getByTitle('Inactive Team')).toBeInTheDocument(); + }); +}); diff --git a/packages/react-components/src/organisms/__tests__/UserProductivityTable.test.tsx b/packages/react-components/src/organisms/__tests__/UserProductivityTable.test.tsx new file mode 100644 index 0000000000..fcf9bd2caa --- /dev/null +++ b/packages/react-components/src/organisms/__tests__/UserProductivityTable.test.tsx @@ -0,0 +1,90 @@ +import { render } from '@testing-library/react'; +import UserProductivityTable from '../UserProductivityTable'; + +describe('UserProductivityTable', () => { + const user = { + id: '1', + name: 'Test User', + alumni: false, + teams: [{ name: 'Team A', active: true }], + roles: ['Role A'], + asapOutput: 1, + asapPublicOutput: 2, + ratio: 1, + }; + + it('renders data', () => { + const data = [user]; + const { getByText } = render(); + expect(getByText('Test User')).toBeInTheDocument(); + }); + + it('displays alumni badge', () => { + const data = [ + { + ...user, + alumni: true, + }, + ]; + const { getByTitle } = render(); + expect(getByTitle('Alumni Member')).toBeInTheDocument(); + }); + + it('displays inactive badge', () => { + const data = [ + { + ...user, + teams: [{ name: 'Team A', active: false }], + }, + ]; + const { getByTitle } = render(); + expect(getByTitle('Inactive Team')).toBeInTheDocument(); + }); + + it('handles multiple teams', () => { + const data = [ + { + ...user, + teams: [ + { name: 'Team A', active: true }, + { name: 'Team B', active: true }, + ], + }, + ]; + const { getByText } = render(); + expect(getByText('Multiple teams')).toBeInTheDocument(); + }); + + it('handles multiple roles', () => { + const data = [ + { + ...user, + roles: ['Role A', 'Role B'], + }, + ]; + const { getByText } = render(); + expect(getByText('Multiple roles')).toBeInTheDocument(); + }); + + it('display no team', () => { + const data = [ + { + ...user, + teams: [], + }, + ]; + const { getByText } = render(); + expect(getByText('No team')).toBeInTheDocument(); + }); + + it('display no role', () => { + const data = [ + { + ...user, + roles: [], + }, + ]; + const { getByText } = render(); + expect(getByText('No role')).toBeInTheDocument(); + }); +}); diff --git a/packages/react-components/src/organisms/index.ts b/packages/react-components/src/organisms/index.ts index bb11258db5..679cdf2cf8 100644 --- a/packages/react-components/src/organisms/index.ts +++ b/packages/react-components/src/organisms/index.ts @@ -55,6 +55,10 @@ export { default as ResultList } from './ResultList'; export { default as RichText } from './RichText'; export { default as RichTextCard } from './RichTextCard'; export { default as SearchAndFilter } from './SearchAndFilter'; +export { default as TeamProductivityTable } from './TeamProductivityTable'; +export { default as UserProductivityTable } from './UserProductivityTable'; +export type { TeamProductivityMetric } from './TeamProductivityTable'; +export type { UserProductivityMetric } from './UserProductivityTable'; export { default as SharedOutputDropdown, SharedOutputDropdownBase, diff --git a/packages/react-components/src/templates/AnalyticsPageBody.tsx b/packages/react-components/src/templates/AnalyticsLeadershipPageBody.tsx similarity index 86% rename from packages/react-components/src/templates/AnalyticsPageBody.tsx rename to packages/react-components/src/templates/AnalyticsLeadershipPageBody.tsx index a08aa25829..bd5fd27b62 100644 --- a/packages/react-components/src/templates/AnalyticsPageBody.tsx +++ b/packages/react-components/src/templates/AnalyticsLeadershipPageBody.tsx @@ -5,7 +5,7 @@ import { Dropdown, Headline3, Paragraph, Subtitle } from '../atoms'; import { LeadershipMembershipTable } from '../organisms'; import { perRem } from '../pixels'; -type MetricOption = 'workingGroup' | 'interestGroup'; +type MetricOption = 'working-group' | 'interest-group'; type MetricData = { id: string; name: string; @@ -16,8 +16,8 @@ type MetricData = { }; const metricOptions: Record = { - workingGroup: 'Working Group Leadership & Membership', - interestGroup: 'Interest Group Leadership & Membership', + 'working-group': 'Working Group Leadership & Membership', + 'interest-group': 'Interest Group Leadership & Membership', }; const metricOptionList = Object.keys(metricOptions).map((value) => ({ @@ -47,7 +47,7 @@ const pageControlsStyles = css({ paddingBottom: `${36 / perRem}em`, }); -const AnalyticsPageBody: React.FC = ({ +const LeadershipPageBody: React.FC = ({ metric, setMetric, data, @@ -77,4 +77,4 @@ const AnalyticsPageBody: React.FC = ({ ); -export default AnalyticsPageBody; +export default LeadershipPageBody; diff --git a/packages/react-components/src/templates/AnalyticsPageHeader.tsx b/packages/react-components/src/templates/AnalyticsPageHeader.tsx index 8abc6f4b6a..88b6bf45a8 100644 --- a/packages/react-components/src/templates/AnalyticsPageHeader.tsx +++ b/packages/react-components/src/templates/AnalyticsPageHeader.tsx @@ -1,12 +1,12 @@ import { css } from '@emotion/react'; - +import { isEnabled } from '@asap-hub/flags'; import { analytics } from '@asap-hub/routing'; import { Display, Paragraph, TabLink } from '../atoms'; import { perRem } from '../pixels'; -import { charcoal, paper, steel } from '../colors'; +import { paper, steel } from '../colors'; import { defaultPageLayoutPaddingStyle } from '../layout'; import TabNav from '../molecules/TabNav'; -import { LeadershipIcon } from '../icons'; +import { LeadershipIcon, ProductivityIcon } from '../icons'; const visualHeaderStyles = css({ padding: `${defaultPageLayoutPaddingStyle} 0`, @@ -18,12 +18,6 @@ const textStyles = css({ maxWidth: `${610 / perRem}em`, }); -const iconStyles = css({ - display: 'inline-grid', - verticalAlign: 'middle', - paddingRight: `${6 / perRem}em`, -}); - const AnalyticsPageHeader: React.FC = () => (
@@ -34,10 +28,15 @@ const AnalyticsPageHeader: React.FC = () => (
- - - - + {isEnabled('DISPLAY_ANALYTICS_PRODUCTIVITY') && ( + + Resource & Data Sharing + + )} + Leadership & Membership diff --git a/packages/react-components/src/templates/AnalyticsProductivityPageBody.tsx b/packages/react-components/src/templates/AnalyticsProductivityPageBody.tsx new file mode 100644 index 0000000000..0a87b1a16a --- /dev/null +++ b/packages/react-components/src/templates/AnalyticsProductivityPageBody.tsx @@ -0,0 +1,75 @@ +import { css } from '@emotion/react'; +import { ComponentProps } from 'react'; +import { PageControls } from '..'; +import { Dropdown, Headline3, Paragraph, Subtitle } from '../atoms'; +import { TeamProductivityTable, UserProductivityTable } from '../organisms'; +import { TeamProductivityMetric } from '../organisms/TeamProductivityTable'; +import { UserProductivityMetric } from '../organisms/UserProductivityTable'; +import { perRem } from '../pixels'; + +type MetricOption = 'user' | 'team'; + +const metricOptions: Record = { + user: 'User Productivity', + team: 'Team Productivity', +}; + +const metricOptionList = Object.keys(metricOptions).map((value) => ({ + value: value as MetricOption, + label: metricOptions[value as MetricOption], +})); + +type LeadershipAndMembershipAnalyticsProps = ComponentProps< + typeof PageControls +> & { + metric: MetricOption; + setMetric: (option: MetricOption) => void; + userData: UserProductivityMetric[]; + teamData: TeamProductivityMetric[]; +}; + +const metricDropdownStyles = css({ + marginBottom: `${48 / perRem}em`, +}); + +const tableHeaderStyles = css({ + paddingBottom: `${24 / perRem}em`, +}); + +const pageControlsStyles = css({ + justifySelf: 'center', + paddingTop: `${36 / perRem}em`, + paddingBottom: `${36 / perRem}em`, +}); + +const AnalyticsProductivityPageBody: React.FC< + LeadershipAndMembershipAnalyticsProps +> = ({ metric, setMetric, userData, teamData, ...pageControlProps }) => ( +
+
+ Metric + +
+
+ {metricOptions[metric]} + + Overview of ASAP outputs shared on the CRN Hub by {metric}. + +
+ {metric === 'user' ? ( + + ) : ( + + )} +
+ +
+
+); + +export default AnalyticsProductivityPageBody; diff --git a/packages/react-components/src/templates/InterestGroupProfileHeader.tsx b/packages/react-components/src/templates/InterestGroupProfileHeader.tsx index 47bfab44b3..290460f97f 100644 --- a/packages/react-components/src/templates/InterestGroupProfileHeader.tsx +++ b/packages/react-components/src/templates/InterestGroupProfileHeader.tsx @@ -13,7 +13,7 @@ import { import { CopyButton, Display, Link, StateTag, TabLink } from '../atoms'; import { googleDriveIcon, - inactiveBadgeIcon, + InactiveBadgeIcon, systemCalendarIcon, TeamIcon, } from '../icons'; @@ -113,7 +113,12 @@ const InterestGroupProfileHeader: React.FC = ({
{name} - {!active && } + {!active && ( + } + label="Inactive" + /> + )}
{active && contactEmails.length !== 0 && (
diff --git a/packages/react-components/src/templates/NetworkPageHeader.tsx b/packages/react-components/src/templates/NetworkPageHeader.tsx index 8e80e748b5..276dcdd151 100644 --- a/packages/react-components/src/templates/NetworkPageHeader.tsx +++ b/packages/react-components/src/templates/NetworkPageHeader.tsx @@ -10,7 +10,7 @@ import { network } from '@asap-hub/routing'; import { Display, Paragraph, TabLink } from '../atoms'; import { perRem } from '../pixels'; -import { charcoal, lead, paper, steel } from '../colors'; +import { paper, steel } from '../colors'; import { networkPageLayoutPaddingStyle, defaultPageLayoutPaddingStyle, @@ -31,14 +31,10 @@ const visualHeaderStyles = css({ background: paper.rgb, boxShadow: `0 2px 4px -2px ${steel.rgb}`, }); + const textStyles = css({ maxWidth: `${610 / perRem}em`, }); -const iconStyles = css({ - display: 'inline-grid', - verticalAlign: 'middle', - paddingRight: `${6 / perRem}em`, -}); const controlsStyles = css({ padding: `${networkPageLayoutPaddingStyle} 0`, @@ -139,38 +135,30 @@ const NetworkPageHeader: React.FC = ({
- - - - + People - - - - + Teams - - - Interest Groups - - - Working Groups diff --git a/packages/react-components/src/templates/TeamProfileHeader.tsx b/packages/react-components/src/templates/TeamProfileHeader.tsx index 9df71b2846..5dd2909ef9 100644 --- a/packages/react-components/src/templates/TeamProfileHeader.tsx +++ b/packages/react-components/src/templates/TeamProfileHeader.tsx @@ -10,7 +10,7 @@ import { bioinformatics, crnReportIcon, dataset, - inactiveBadgeIcon, + InactiveBadgeIcon, LabIcon, labResource, plusIcon, @@ -74,7 +74,7 @@ const createSectionStyles = css({ [`@media (min-width: ${mobileScreen.max}px)`]: { grid: ` - "contact members create" + "contact members create" "lab lab lab"/ auto auto 1fr `, }, @@ -156,7 +156,9 @@ const TeamProfileHeader: React.FC = ({
Team {displayName} - {!isActive && } + {!isActive && ( + } label="Inactive" /> + )}
{ + const props: ComponentProps = { + numberOfPages: 1, + currentPageIndex: 0, + renderPageHref: () => '', + setMetric: () => {}, + data: [], + metric: 'interest-group', + }; + it('renders interest group tab', () => { + const { getAllByText } = render( + , + ); + + expect(getAllByText('Interest Group Leadership & Membership').length).toBe( + 2, + ); + }); + + it('renders working group tab', () => { + const { getAllByText } = render( + , + ); + + expect(getAllByText('Working Group Leadership & Membership').length).toBe( + 2, + ); + }); +}); diff --git a/packages/react-components/src/templates/__tests__/AnalyticsProductivityPageBody.test.tsx b/packages/react-components/src/templates/__tests__/AnalyticsProductivityPageBody.test.tsx new file mode 100644 index 0000000000..3268229c2b --- /dev/null +++ b/packages/react-components/src/templates/__tests__/AnalyticsProductivityPageBody.test.tsx @@ -0,0 +1,30 @@ +import { render } from '@testing-library/react'; +import { ComponentProps } from 'react'; +import { AnalyticsProductivityPageBody } from '..'; + +describe('AnalyticsProductivityPageBody', () => { + const props: ComponentProps = { + numberOfPages: 1, + currentPageIndex: 0, + renderPageHref: () => '', + setMetric: () => null, + userData: [], + teamData: [], + metric: 'user', + }; + it('renders user tab', () => { + const { getAllByText } = render( + , + ); + + expect(getAllByText('User Productivity').length).toBe(2); + }); + + it('renders team tab', () => { + const { getAllByText } = render( + , + ); + + expect(getAllByText('Team Productivity').length).toBe(2); + }); +}); diff --git a/packages/react-components/src/templates/__tests__/InterestGroupProfileHeader.test.tsx b/packages/react-components/src/templates/__tests__/InterestGroupProfileHeader.test.tsx index d98b74f40f..1829e6d398 100644 --- a/packages/react-components/src/templates/__tests__/InterestGroupProfileHeader.test.tsx +++ b/packages/react-components/src/templates/__tests__/InterestGroupProfileHeader.test.tsx @@ -30,7 +30,7 @@ it('renders the tag for inactive groups', () => { , ); expect(screen.getByText('Inactive', { selector: 'span' })).toBeVisible(); - expect(screen.getByTitle('Inactive')).toBeInTheDocument(); + expect(screen.getByTitle('Inactive Interest Group')).toBeInTheDocument(); }); it('renders group google drive link if present', () => { diff --git a/packages/react-components/src/templates/__tests__/AnalyticsPageBody.test.tsx b/packages/react-components/src/templates/__tests__/LeadershipPageBody.test.tsx similarity index 76% rename from packages/react-components/src/templates/__tests__/AnalyticsPageBody.test.tsx rename to packages/react-components/src/templates/__tests__/LeadershipPageBody.test.tsx index b189d14dce..238182998a 100644 --- a/packages/react-components/src/templates/__tests__/AnalyticsPageBody.test.tsx +++ b/packages/react-components/src/templates/__tests__/LeadershipPageBody.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react'; -import AnalyticsPageBody from '../AnalyticsPageBody'; +import AnalyticsLeadershipPageBody from '../AnalyticsLeadershipPageBody'; const pageControlProps = { numberOfPages: 1, @@ -10,8 +10,8 @@ const pageControlProps = { }; it('renders the selected metric', () => { render( - {}} {...pageControlProps} diff --git a/packages/react-components/src/templates/__tests__/TeamProfileHeader.test.tsx b/packages/react-components/src/templates/__tests__/TeamProfileHeader.test.tsx index 88dd9e82e2..2f22825285 100644 --- a/packages/react-components/src/templates/__tests__/TeamProfileHeader.test.tsx +++ b/packages/react-components/src/templates/__tests__/TeamProfileHeader.test.tsx @@ -35,7 +35,7 @@ it('renders the tag for inactive teams', () => { />, ); expect(screen.getByText('Inactive', { selector: 'span' })).toBeVisible(); - expect(screen.getByTitle('Inactive')).toBeInTheDocument(); + expect(screen.getByTitle('Inactive Team')).toBeInTheDocument(); }); it('does not render the tag for active teams', () => { diff --git a/packages/react-components/src/templates/__tests__/UserProfileHeader.test.tsx b/packages/react-components/src/templates/__tests__/UserProfileHeader.test.tsx index fc67ec419e..819e7959fe 100644 --- a/packages/react-components/src/templates/__tests__/UserProfileHeader.test.tsx +++ b/packages/react-components/src/templates/__tests__/UserProfileHeader.test.tsx @@ -140,7 +140,7 @@ describe('alumni', () => { , ); expect(queryByText('Alumni')).toBeInTheDocument(); - expect(queryByTitle('Alumni Badge')).toBeInTheDocument(); + expect(queryByTitle('Alumni Member')).toBeInTheDocument(); rerender( @@ -148,7 +148,7 @@ describe('alumni', () => { , ); expect(queryByText('Alumni')).not.toBeInTheDocument(); - expect(queryByTitle('Alumni Badge')).not.toBeInTheDocument(); + expect(queryByTitle('Alumni Member')).not.toBeInTheDocument(); }); it('shows the proper alumni toast message when user is alumni', () => { diff --git a/packages/react-components/src/templates/index.ts b/packages/react-components/src/templates/index.ts index 7861a5b82d..55abdb2a30 100644 --- a/packages/react-components/src/templates/index.ts +++ b/packages/react-components/src/templates/index.ts @@ -2,7 +2,8 @@ export { default as AboutPage } from './AboutPage'; export { default as AboutPageBody } from './AboutPageBody'; export { default as AboutPageHeader } from './AboutPageHeader'; export { default as AnalyticsPage } from './AnalyticsPage'; -export { default as AnalyticsPageBody } from './AnalyticsPageBody'; +export { default as AnalyticsLeadershipPageBody } from './AnalyticsLeadershipPageBody'; +export { default as AnalyticsProductivityPageBody } from './AnalyticsProductivityPageBody'; export { default as AnalyticsPageHeader } from './AnalyticsPageHeader'; export { default as BasicLayout } from './BasicLayout'; export { default as BiographyModal } from './BiographyModal'; diff --git a/packages/routing/src/analytics.ts b/packages/routing/src/analytics.ts index 6f401ca77f..7c56b2560c 100644 --- a/packages/routing/src/analytics.ts +++ b/packages/routing/src/analytics.ts @@ -1,3 +1,9 @@ -import { route } from 'typesafe-routes'; +import { route, stringParser } from 'typesafe-routes'; -export default route('/analytics', {}, {}); +const metric = route('/:metric', { metric: stringParser }, {}); + +const leadership = route('/leadership', {}, { metric }); + +const productivity = route('/productivity', {}, { metric }); + +export default route('/analytics', {}, { leadership, productivity });