Skip to content

Commit

Permalink
Improve management section placeholders (hotosm#5156)
Browse files Browse the repository at this point in the history
* Use explore projects placeholder for mgmt projects

* Add teams placeholder in manage landing page

* Add placeholder for organisations

* Add placeholder for teams tab

* Add campaigns placeholder

* Add placeholder for categories tab

* Add placeholder for users tab

* Add placeholder for licenses tab

* Show manage title when organization list is loading

* Display placeholder when API is being fetched

* Update test cases
  • Loading branch information
HelNershingThapa committed Jun 6, 2022
1 parent 93aa002 commit 4dcdc39
Show file tree
Hide file tree
Showing 26 changed files with 730 additions and 194 deletions.
25 changes: 17 additions & 8 deletions frontend/src/components/interests/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import React from 'react';
import { Link } from '@reach/router';
import { Form, Field } from 'react-final-form';
import { FormattedMessage } from 'react-intl';
import ReactPlaceholder from 'react-placeholder';

import messages from '../teamsAndOrgs/messages';
import { Management } from '../teamsAndOrgs/management';
import { HashtagIcon } from '../svgIcons';
import { Button } from '../button';
import { nCardPlaceholders } from '../teamsAndOrgs/campaignsPlaceholder';

export const InterestCard = ({ interest }) => {
return (
Expand All @@ -25,7 +27,7 @@ export const InterestCard = ({ interest }) => {
);
};

export const InterestsManagement = ({ interests, userDetails }) => {
export const InterestsManagement = ({ interests, _userDetails, isInterestsFetched }) => {
return (
<Management
title={
Expand All @@ -37,13 +39,20 @@ export const InterestsManagement = ({ interests, userDetails }) => {
showAddButton={true}
managementView
>
{interests.length ? (
interests.map((i, n) => <InterestCard interest={i} />)
) : (
<div>
<FormattedMessage {...messages.noCategories} />
</div>
)}
<ReactPlaceholder
showLoadingAnimation={true}
customPlaceholder={nCardPlaceholders(4)}
delay={10}
ready={isInterestsFetched}
>
{interests?.length ? (
interests.map((i, n) => <InterestCard key={n} interest={i} />)
) : (
<div>
<FormattedMessage {...messages.noCategories} />
</div>
)}
</ReactPlaceholder>
</Management>
);
};
Expand Down
63 changes: 63 additions & 0 deletions frontend/src/components/interests/tests/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { render, screen, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import { IntlProviders } from '../../../utils/testWithIntl';
import { InterestsManagement } from '../index';

const dummyInterests = [
{
id: 1,
name: 'Interest 1',
},
{
id: 2,
name: 'Interest 2',
},
];

describe('InterestsManagement component', () => {
it('renders loading placeholder when API is being fetched', () => {
const { container, getByRole } = render(
<IntlProviders>
<InterestsManagement isInterestsFetched={false} />
</IntlProviders>,
);
expect(
screen.getByRole('heading', {
name: /manage categories/i,
}),
).toBeInTheDocument();
expect(container.getElementsByClassName('show-loading-animation')).toHaveLength(4);
expect(
getByRole('button', {
name: /new/i,
}),
).toBeInTheDocument();
});

it('does not render loading placeholder after API is fetched', () => {
const { container } = render(
<IntlProviders>
<InterestsManagement isInterestsFetched={true} />
</IntlProviders>,
);
expect(container.getElementsByClassName('show-loading-animation')).toHaveLength(0);
});

it('renders interests list card after API is fetched', async () => {
const { container, getByText } = render(
<IntlProviders>
<InterestsManagement interests={dummyInterests} isInterestsFetched={true} />
</IntlProviders>,
);
expect(
screen.getByRole('heading', {
name: /manage categories/i,
}),
).toBeInTheDocument();
await waitFor(() => {
expect(getByText(/Interest 1/i));
});
expect(getByText(/Interest 2/i)).toBeInTheDocument();
expect(container.querySelectorAll('svg').length).toBe(3);
});
});
25 changes: 17 additions & 8 deletions frontend/src/components/licenses/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import React from 'react';
import { Link } from '@reach/router';
import { Form, Field } from 'react-final-form';
import { FormattedMessage } from 'react-intl';
import ReactPlaceholder from 'react-placeholder';

import messages from '../teamsAndOrgs/messages';
import { Management } from '../teamsAndOrgs/management';
import { CopyrightIcon } from '../svgIcons';
import { Button } from '../button';
import { nCardPlaceholders } from './licensesPlaceholder';

export const LicenseCard = ({ license }) => {
return (
Expand All @@ -25,7 +27,7 @@ export const LicenseCard = ({ license }) => {
);
};

export const LicensesManagement = ({ licenses, userDetails }) => {
export const LicensesManagement = ({ licenses, userDetails, isLicensesFetched }) => {
return (
<Management
title={
Expand All @@ -37,13 +39,20 @@ export const LicensesManagement = ({ licenses, userDetails }) => {
showAddButton={true}
managementView
>
{licenses.length ? (
licenses.map((i, n) => <LicenseCard key={n} license={i} />)
) : (
<div className="pv3">
<FormattedMessage {...messages.noLicenses} />
</div>
)}
<ReactPlaceholder
showLoadingAnimation={true}
customPlaceholder={nCardPlaceholders(4)}
delay={10}
ready={isLicensesFetched}
>
{licenses?.length ? (
licenses.map((i, n) => <LicenseCard key={n} license={i} />)
) : (
<div className="pv3">
<FormattedMessage {...messages.noLicenses} />
</div>
)}
</ReactPlaceholder>
</Management>
);
};
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/components/licenses/licensesPlaceholder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import { TextRow } from 'react-placeholder/lib/placeholders';
import { CopyrightIcon } from '../svgIcons';

export const licenseCardPlaceholderTemplate = () => (_n, i) =>
(
<div className="w-50-ns w-100 fl pr3" key={i}>
<div className="cf bg-white blue-dark br1 mv2 pv4 ph3 ba br1 b--grey-light shadow-hover">
<div className="dib v-mid pr3">
<div className="z-1 fl br-100 tc h2 w2 bg-blue-light white">
<span className="relative w-50 dib">
<CopyrightIcon style={{ paddingTop: '0.475rem' }} />
</span>
</div>
</div>
<TextRow
className="show-loading-animation f3 mv0 dib v-mid"
color="#CCC"
style={{ width: '55%', marginTop: 0 }}
/>
</div>
</div>
);

export const nCardPlaceholders = (N) => {
return [...Array(N).keys()].map(licenseCardPlaceholderTemplate());
};
14 changes: 13 additions & 1 deletion frontend/src/components/licenses/tests/licenses.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe('Licenses Management', () => {
it('renders all licenses and button to add a new license', () => {
const { container } = render(
<IntlProviders>
<LicensesManagement licenses={licenses} />
<LicensesManagement licenses={licenses} isLicensesFetched={true} />
</IntlProviders>,
);
expect(container.querySelector('h3').innerHTML).toBe('Manage Licenses');
Expand All @@ -53,6 +53,18 @@ describe('Licenses Management', () => {
const license2 = screen.getByText(/NextView/);
expect(license2.closest('a').href).toContain('/2');
});

it('renders placeholder and not licenses when API is being fetched', () => {
const { container } = render(
<IntlProviders>
<LicensesManagement licenses={licenses} isLicensesFetched={false} />
</IntlProviders>,
);
expect(screen.queryByText(/HOT Licence/)).not.toBeInTheDocument();
expect(screen.queryByText(/NextView/)).not.toBeInTheDocument();
expect(container.querySelectorAll('svg').length).toBe(5); // 4 plus the new icon svg
expect(container.querySelector('.show-loading-animation')).toBeInTheDocument();
});
});

describe('LicenseForm', () => {
Expand Down
25 changes: 17 additions & 8 deletions frontend/src/components/teamsAndOrgs/campaigns.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import React from 'react';
import { Link } from '@reach/router';
import { FormattedMessage } from 'react-intl';
import { Form, Field } from 'react-final-form';
import ReactPlaceholder from 'react-placeholder';

import { nCardPlaceholders } from './campaignsPlaceholder';
import messages from './messages';
import { Management } from './management';
import { Button } from '../button';
import { HashtagIcon } from '../svgIcons';

export function CampaignsManagement({ campaigns, userDetails }: Object) {
export function CampaignsManagement({ campaigns, userDetails, isCampaignsFetched }: Object) {
return (
<Management
title={
Expand All @@ -20,13 +22,20 @@ export function CampaignsManagement({ campaigns, userDetails }: Object) {
showAddButton={userDetails.role === 'ADMIN'}
managementView
>
{campaigns.length ? (
campaigns.map((campaign, n) => <CampaignCard campaign={campaign} key={n} />)
) : (
<div>
<FormattedMessage {...messages.noCampaigns} />
</div>
)}
<ReactPlaceholder
showLoadingAnimation={true}
customPlaceholder={nCardPlaceholders(4)}
delay={10}
ready={isCampaignsFetched}
>
{campaigns?.length ? (
campaigns.map((campaign, n) => <CampaignCard campaign={campaign} key={n} />)
) : (
<div>
<FormattedMessage {...messages.noCampaigns} />
</div>
)}
</ReactPlaceholder>
</Management>
);
}
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/components/teamsAndOrgs/campaignsPlaceholder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import { TextRow } from 'react-placeholder/lib/placeholders';
import { HashtagIcon } from '../svgIcons';

export const campaignCardPlaceholderTemplate = () => (_n, i) =>
(
<div className="w-50-ns w-100 fl pr3" key={i}>
<div className="cf bg-white blue-dark br1 mv2 pv4 ph3 ba br1 b--grey-light shadow-hover">
<div className="dib v-mid pr3">
<div className="z-1 fl br-100 tc h2 w2 bg-blue-light white">
<span className="relative w-50 dib">
<HashtagIcon style={{ paddingTop: '0.4175rem' }} />
</span>
</div>
</div>
<TextRow
className="show-loading-animation f3 mv0 dib v-mid"
color="#CCC"
style={{ width: '55%', marginTop: 0 }}
/>
</div>
</div>
);

export const nCardPlaceholders = (N) => {
return [...Array(N).keys()].map(campaignCardPlaceholderTemplate());
};
31 changes: 20 additions & 11 deletions frontend/src/components/teamsAndOrgs/organisations.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ import { Management } from './management';
import { InternalLinkIcon, ClipboardIcon } from '../svgIcons';
import { Button } from '../button';
import { UserAvatarList } from '../user/avatar';
import { nCardPlaceholders } from './organisationsPlaceholder';

export function OrgsManagement({
organisations,
isOrgManager,
isAdmin,
userOrgsOnly,
setUserOrgsOnly,
isOrganisationsFetched,
}: Object) {
return (
<Management
Expand All @@ -38,19 +40,26 @@ export function OrgsManagement({
setUserOnly={setUserOrgsOnly}
isAdmin={isAdmin}
>
{isOrgManager ? (
organisations.length ? (
organisations.map((org, n) => <OrganisationCard details={org} key={n} />)
<ReactPlaceholder
showLoadingAnimation={true}
customPlaceholder={nCardPlaceholders(4)}
delay={10}
ready={isOrganisationsFetched}
>
{isOrgManager ? (
organisations?.length ? (
organisations.map((org, n) => <OrganisationCard details={org} key={n} />)
) : (
<div className="pb5">
<FormattedMessage {...messages.noOrganisationsFound} />
</div>
)
) : (
<div className="pb5">
<FormattedMessage {...messages.noOrganisationsFound} />
<div>
<FormattedMessage {...messages.notAllowed} />
</div>
)
) : (
<div>
<FormattedMessage {...messages.notAllowed} />
</div>
)}
)}
</ReactPlaceholder>
</Management>
);
}
Expand Down
35 changes: 35 additions & 0 deletions frontend/src/components/teamsAndOrgs/organisationsPlaceholder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';
import { TextRow, RoundShape, RectShape } from 'react-placeholder/lib/placeholders';

export const organisationCardPlaceholderTemplate = () => (_n, i) =>
(
<div className="w-50-l w-100 fl pr3" key={i}>
<div className="bg-white blue-dark mv2 pb4 dib w-100 ba br1 b--grey-light shadow-hover">
<div className="w-25 h4 fl pa3">
<RectShape
className="show-loading-animation"
style={{ width: '100%', height: 100 }}
color="#DDD"
/>
</div>
<div className="w-75 fl pl3">
<TextRow className="show-loading-animation mb4" color="#CCC" style={{ width: '50%' }} />
<TextRow className="show-loading-animation mb2" color="#CCC" style={{ width: '50%' }} />
<div className="dib">
{[...Array(2)].map((_, i) => (
<RoundShape
key={i}
className="show-loading-animation dib mt1"
style={{ width: 25, height: 25 }}
color="#DDD"
/>
))}
</div>
</div>
</div>
</div>
);

export const nCardPlaceholders = (N) => {
return [...Array(N).keys()].map(organisationCardPlaceholderTemplate());
};
Loading

0 comments on commit 4dcdc39

Please sign in to comment.