diff --git a/frontend/src/components/interests/index.js b/frontend/src/components/interests/index.js
index 636cdeccc8..ac6d5d3a5c 100644
--- a/frontend/src/components/interests/index.js
+++ b/frontend/src/components/interests/index.js
@@ -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 (
@@ -25,7 +27,7 @@ export const InterestCard = ({ interest }) => {
);
};
-export const InterestsManagement = ({ interests, userDetails }) => {
+export const InterestsManagement = ({ interests, _userDetails, isInterestsFetched }) => {
return (
{
showAddButton={true}
managementView
>
- {interests.length ? (
- interests.map((i, n) => )
- ) : (
-
-
-
- )}
+
+ {interests?.length ? (
+ interests.map((i, n) => )
+ ) : (
+
+
+
+ )}
+
);
};
diff --git a/frontend/src/components/interests/tests/index.test.js b/frontend/src/components/interests/tests/index.test.js
new file mode 100644
index 0000000000..6074e2c5b2
--- /dev/null
+++ b/frontend/src/components/interests/tests/index.test.js
@@ -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(
+
+
+ ,
+ );
+ 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(
+
+
+ ,
+ );
+ expect(container.getElementsByClassName('show-loading-animation')).toHaveLength(0);
+ });
+
+ it('renders interests list card after API is fetched', async () => {
+ const { container, getByText } = render(
+
+
+ ,
+ );
+ 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);
+ });
+});
diff --git a/frontend/src/components/licenses/index.js b/frontend/src/components/licenses/index.js
index 9df751d22b..62dafaa8c5 100644
--- a/frontend/src/components/licenses/index.js
+++ b/frontend/src/components/licenses/index.js
@@ -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 (
@@ -25,7 +27,7 @@ export const LicenseCard = ({ license }) => {
);
};
-export const LicensesManagement = ({ licenses, userDetails }) => {
+export const LicensesManagement = ({ licenses, userDetails, isLicensesFetched }) => {
return (
{
showAddButton={true}
managementView
>
- {licenses.length ? (
- licenses.map((i, n) => )
- ) : (
-
-
-
- )}
+
+ {licenses?.length ? (
+ licenses.map((i, n) => )
+ ) : (
+
+
+
+ )}
+
);
};
diff --git a/frontend/src/components/licenses/licensesPlaceholder.js b/frontend/src/components/licenses/licensesPlaceholder.js
new file mode 100644
index 0000000000..2d53f92def
--- /dev/null
+++ b/frontend/src/components/licenses/licensesPlaceholder.js
@@ -0,0 +1,27 @@
+import React from 'react';
+import { TextRow } from 'react-placeholder/lib/placeholders';
+import { CopyrightIcon } from '../svgIcons';
+
+export const licenseCardPlaceholderTemplate = () => (_n, i) =>
+ (
+
+ );
+
+export const nCardPlaceholders = (N) => {
+ return [...Array(N).keys()].map(licenseCardPlaceholderTemplate());
+};
diff --git a/frontend/src/components/licenses/tests/licenses.test.js b/frontend/src/components/licenses/tests/licenses.test.js
index 810306584c..b4f9b96282 100644
--- a/frontend/src/components/licenses/tests/licenses.test.js
+++ b/frontend/src/components/licenses/tests/licenses.test.js
@@ -40,7 +40,7 @@ describe('Licenses Management', () => {
it('renders all licenses and button to add a new license', () => {
const { container } = render(
-
+
,
);
expect(container.querySelector('h3').innerHTML).toBe('Manage Licenses');
@@ -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(
+
+
+ ,
+ );
+ 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', () => {
diff --git a/frontend/src/components/teamsAndOrgs/campaigns.js b/frontend/src/components/teamsAndOrgs/campaigns.js
index 8ce11bb756..7649f76508 100644
--- a/frontend/src/components/teamsAndOrgs/campaigns.js
+++ b/frontend/src/components/teamsAndOrgs/campaigns.js
@@ -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 (
- {campaigns.length ? (
- campaigns.map((campaign, n) => )
- ) : (
-
-
-
- )}
+
+ {campaigns?.length ? (
+ campaigns.map((campaign, n) => )
+ ) : (
+
+
+
+ )}
+
);
}
diff --git a/frontend/src/components/teamsAndOrgs/campaignsPlaceholder.js b/frontend/src/components/teamsAndOrgs/campaignsPlaceholder.js
new file mode 100644
index 0000000000..17902f5790
--- /dev/null
+++ b/frontend/src/components/teamsAndOrgs/campaignsPlaceholder.js
@@ -0,0 +1,27 @@
+import React from 'react';
+import { TextRow } from 'react-placeholder/lib/placeholders';
+import { HashtagIcon } from '../svgIcons';
+
+export const campaignCardPlaceholderTemplate = () => (_n, i) =>
+ (
+
+ );
+
+export const nCardPlaceholders = (N) => {
+ return [...Array(N).keys()].map(campaignCardPlaceholderTemplate());
+};
diff --git a/frontend/src/components/teamsAndOrgs/organisations.js b/frontend/src/components/teamsAndOrgs/organisations.js
index 15e641f28d..10b782941e 100644
--- a/frontend/src/components/teamsAndOrgs/organisations.js
+++ b/frontend/src/components/teamsAndOrgs/organisations.js
@@ -15,6 +15,7 @@ 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,
@@ -22,6 +23,7 @@ export function OrgsManagement({
isAdmin,
userOrgsOnly,
setUserOrgsOnly,
+ isOrganisationsFetched,
}: Object) {
return (
- {isOrgManager ? (
- organisations.length ? (
- organisations.map((org, n) => )
+
+ {isOrgManager ? (
+ organisations?.length ? (
+ organisations.map((org, n) => )
+ ) : (
+
+
+
+ )
) : (
-
-
+
+
- )
- ) : (
-
-
-
- )}
+ )}
+
);
}
diff --git a/frontend/src/components/teamsAndOrgs/organisationsPlaceholder.js b/frontend/src/components/teamsAndOrgs/organisationsPlaceholder.js
new file mode 100644
index 0000000000..696f0cee5d
--- /dev/null
+++ b/frontend/src/components/teamsAndOrgs/organisationsPlaceholder.js
@@ -0,0 +1,35 @@
+import React from 'react';
+import { TextRow, RoundShape, RectShape } from 'react-placeholder/lib/placeholders';
+
+export const organisationCardPlaceholderTemplate = () => (_n, i) =>
+ (
+
+
+
+
+
+
+
+
+
+ {[...Array(2)].map((_, i) => (
+
+ ))}
+
+
+
+
+ );
+
+export const nCardPlaceholders = (N) => {
+ return [...Array(N).keys()].map(organisationCardPlaceholderTemplate());
+};
diff --git a/frontend/src/components/teamsAndOrgs/projects.js b/frontend/src/components/teamsAndOrgs/projects.js
index faae71b81e..c249501410 100644
--- a/frontend/src/components/teamsAndOrgs/projects.js
+++ b/frontend/src/components/teamsAndOrgs/projects.js
@@ -6,6 +6,7 @@ import ReactPlaceholder from 'react-placeholder';
import messages from './messages';
import { ProjectCard } from '../projectCard/projectCard';
import { AddButton, ViewAllLink } from './management';
+import { nCardPlaceholders } from '../projectCard/nCardPlaceholder';
export function Projects({
projects,
@@ -29,12 +30,10 @@ export function Projects({
{projects &&
projects.results &&
diff --git a/frontend/src/components/teamsAndOrgs/teams.js b/frontend/src/components/teamsAndOrgs/teams.js
index b91ec7c0a5..82f05ba5f2 100644
--- a/frontend/src/components/teamsAndOrgs/teams.js
+++ b/frontend/src/components/teamsAndOrgs/teams.js
@@ -11,6 +11,7 @@ import { UserAvatar, UserAvatarList } from '../user/avatar';
import { AddButton, ViewAllLink, Management, VisibilityBox, InviteOnlyBox } from './management';
import { SwitchToggle, RadioField, OrganisationSelectInput } from '../formInputs';
import { Button, EditButton } from '../button';
+import { nCardPlaceholders } from './teamsPlaceholder';
export function TeamsManagement({
teams,
@@ -18,6 +19,7 @@ export function TeamsManagement({
managementView,
userTeamsOnly,
setUserTeamsOnly,
+ isTeamsFetched,
}: Object) {
const isOrgManager = useSelector(
(state) => state.auth.get('organisations') && state.auth.get('organisations').length > 0,
@@ -42,13 +44,20 @@ export function TeamsManagement({
setUserOnly={setUserTeamsOnly}
userOnlyLabel={}
>
- {teams.length ? (
- teams.map((team, n) => )
- ) : (
-
-
-
- )}
+
+ {teams?.length ? (
+ teams.map((team, n) => )
+ ) : (
+
+
+
+ )}
+
);
}
@@ -67,14 +76,7 @@ export function Teams({ teams, viewAllQuery, showAddButton = false, isReady, bor
)}
{viewAllQuery && }
-
+
{teams && teams.slice(0, 6).map((team, n) => )}
{teams && teams.length === 0 && (
diff --git a/frontend/src/components/teamsAndOrgs/teamsPlaceholder.js b/frontend/src/components/teamsAndOrgs/teamsPlaceholder.js
new file mode 100644
index 0000000000..2f829e02be
--- /dev/null
+++ b/frontend/src/components/teamsAndOrgs/teamsPlaceholder.js
@@ -0,0 +1,35 @@
+import React, { Fragment } from 'react';
+
+import { TextRow, TextBlock, RoundShape, RectShape } from 'react-placeholder/lib/placeholders';
+
+export const teamCardPlaceholderTemplate = () => (_n, i) =>
+ (
+
+
+
+
+ {[...Array(2)].map((_, i) => (
+
+
+ {[...Array(2)].map((_, i) => (
+
+ ))}
+
+ ))}
+
+
+
+ );
+
+export const nCardPlaceholders = (N) => {
+ return [...Array(N).keys()].map(teamCardPlaceholderTemplate());
+};
diff --git a/frontend/src/components/teamsAndOrgs/tests/campaigns.test.js b/frontend/src/components/teamsAndOrgs/tests/campaigns.test.js
new file mode 100644
index 0000000000..cd32300faa
--- /dev/null
+++ b/frontend/src/components/teamsAndOrgs/tests/campaigns.test.js
@@ -0,0 +1,67 @@
+import { render, screen, waitFor } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import { IntlProviders } from '../../../utils/testWithIntl';
+import { CampaignsManagement } from '../campaigns';
+
+const dummyCampaigns = [
+ {
+ id: 1,
+ name: 'Campaign 1',
+ },
+ {
+ id: 2,
+ name: 'Campaign 2',
+ },
+];
+
+describe('CampaignsManagement component', () => {
+ it('renders loading placeholder when API is being fetched', () => {
+ const { container, getByRole } = render(
+
+
+ ,
+ );
+ expect(
+ screen.getByRole('heading', {
+ name: /manage campaigns/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(
+
+
+ ,
+ );
+ expect(container.getElementsByClassName('show-loading-animation')).toHaveLength(0);
+ });
+
+ it('renders campaigns list card after API is fetched', async () => {
+ const { container, getByText } = render(
+
+
+ ,
+ );
+ expect(
+ screen.getByRole('heading', {
+ name: /manage campaigns/i,
+ }),
+ ).toBeInTheDocument();
+ await waitFor(() => {
+ expect(getByText(/Campaign 1/i));
+ });
+ expect(getByText(/Campaign 2/i)).toBeInTheDocument();
+ expect(container.querySelectorAll('svg').length).toBe(3);
+ });
+});
diff --git a/frontend/src/components/teamsAndOrgs/tests/organisations.test.js b/frontend/src/components/teamsAndOrgs/tests/organisations.test.js
index 2e8e92a141..a78bcd7607 100644
--- a/frontend/src/components/teamsAndOrgs/tests/organisations.test.js
+++ b/frontend/src/components/teamsAndOrgs/tests/organisations.test.js
@@ -61,7 +61,12 @@ describe('OrgsManagement with', () => {
};
it('isOrgManager = false and isAdmin = false should NOT list organisations', () => {
const element = createComponentWithIntl(
- ,
+ ,
);
const testInstance = element.root;
expect(testInstance.findAllByType(FormattedMessage).map((i) => i.props.id)).toContain(
@@ -77,7 +82,12 @@ describe('OrgsManagement with', () => {
it('isOrgManager and isAdmin SHOULD list organisations and have a link to /new ', () => {
const element = createComponentWithIntl(
- ,
+ ,
);
const testInstance = element.root;
expect(testInstance.findByType(OrganisationCard).props.details).toStrictEqual(
@@ -99,7 +109,12 @@ describe('OrgsManagement with', () => {
it('OrgsManagement with isOrgManager = true and isAdmin = false SHOULD list organisations, but should NOT have an AddButton', () => {
const element = createComponentWithIntl(
- ,
+ ,
);
const testInstance = element.root;
expect(testInstance.findByType(OrganisationCard).props.details).toStrictEqual(
@@ -109,4 +124,30 @@ describe('OrgsManagement with', () => {
new Error('No instances found with node type: "AddButton"'),
);
});
+
+ it('renders loading placeholder when API is being fetched', () => {
+ const element = createComponentWithIntl(
+ ,
+ );
+ const testInstance = element.root;
+ expect(testInstance.findAllByProps({ className: 'show-loading-animation' }).length).toBe(4);
+ });
+
+ it('should not render loading placeholder after API is fetched', () => {
+ const element = createComponentWithIntl(
+ ,
+ );
+ const testInstance = element.root;
+ expect(testInstance.findAllByProps({ className: 'show-loading-animation' }).length).toBe(0);
+ });
});
diff --git a/frontend/src/components/teamsAndOrgs/tests/projects.test.js b/frontend/src/components/teamsAndOrgs/tests/projects.test.js
new file mode 100644
index 0000000000..5a503c9de5
--- /dev/null
+++ b/frontend/src/components/teamsAndOrgs/tests/projects.test.js
@@ -0,0 +1,18 @@
+import { render, screen } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import { IntlProviders } from '../../../utils/testWithIntl';
+import { Projects } from '../projects';
+
+it('renders loading placeholder when API is being fetched', () => {
+ const { container } = render(
+
+
+ ,
+ );
+ expect(
+ screen.getByRole('heading', {
+ name: /projects/i,
+ }),
+ ).toBeInTheDocument();
+ expect(container.getElementsByClassName('show-loading-animation')).toHaveLength(16);
+});
diff --git a/frontend/src/components/teamsAndOrgs/tests/teams.test.js b/frontend/src/components/teamsAndOrgs/tests/teams.test.js
index 2b3e3e576c..1f383f13ea 100644
--- a/frontend/src/components/teamsAndOrgs/tests/teams.test.js
+++ b/frontend/src/components/teamsAndOrgs/tests/teams.test.js
@@ -1,9 +1,10 @@
import React from 'react';
import TestRenderer from 'react-test-renderer';
+import '@testing-library/jest-dom';
+import { render, screen } from '@testing-library/react';
import { FormattedMessage } from 'react-intl';
-
-import { createComponentWithIntl } from '../../../utils/testWithIntl';
-import { TeamBox, TeamsBoxList } from '../teams';
+import { createComponentWithIntl, ReduxIntlProviders } from '../../../utils/testWithIntl';
+import { TeamBox, TeamsBoxList, TeamsManagement } from '../teams';
describe('test TeamBox', () => {
const element = TestRenderer.create(
@@ -98,3 +99,107 @@ describe('test TeamBoxList without mapping and validation teams', () => {
);
});
});
+
+describe('TeamsManagement component', () => {
+ it('renders loading placeholder when API is being fetched', async () => {
+ const { container, getByRole } = render(
+
+
+ ,
+ );
+ expect(
+ getByRole('button', {
+ name: /new/i,
+ }),
+ ).toBeInTheDocument();
+ expect(container.querySelectorAll('button')).toHaveLength(3);
+ expect(container.getElementsByClassName('show-loading-animation mb3')).toHaveLength(4);
+ });
+
+ it('does not render loading placeholder after API is fetched', () => {
+ const { container } = render(
+
+
+ ,
+ );
+ expect(container.getElementsByClassName('show-loading-animation mb3')).toHaveLength(0);
+ });
+
+ it("should not render 'Manage teams' but render 'My teams' text for non management view", () => {
+ render(
+
+
+ ,
+ );
+ expect(
+ screen.getByRole('heading', {
+ name: /manage teams/i,
+ }),
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByRole('heading', {
+ name: /my teams/i,
+ }),
+ ).not.toBeInTheDocument();
+ });
+
+ it('renders teams list card after API is fetched', async () => {
+ const dummyTeams = [
+ {
+ teamId: 3,
+ name: 'My Best Team',
+ role: 'PROJECT_MANAGER',
+ members: [
+ {
+ username: 'ram',
+ function: 'MEMBER',
+ active: true,
+ pictureUrl: null,
+ },
+ ],
+ },
+ ];
+ const { container, getByText } = render(
+
+
+ ,
+ );
+ expect(container.querySelectorAll('h3')[0].textContent).toBe('Manage Teams');
+ expect(container.querySelectorAll('article').length).toBe(1);
+ expect(getByText('My Best Team')).toBeInTheDocument();
+ expect(getByText('Managers')).toBeInTheDocument();
+ expect(getByText('Team members')).toBeInTheDocument();
+ expect(getByText('My Best Team').closest('a').href).toContain('/manage/teams/3/');
+ });
+
+ it('renders relevant text if user is not a member of any team', async () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByText(/you are not a member of a team yet\./i)).toBeInTheDocument();
+ });
+});
diff --git a/frontend/src/components/user/list.js b/frontend/src/components/user/list.js
index 4b05182d37..bcb7ec81d9 100644
--- a/frontend/src/components/user/list.js
+++ b/frontend/src/components/user/list.js
@@ -1,6 +1,7 @@
import React, { useEffect, useState, useRef } from 'react';
import { useSelector } from 'react-redux';
import { FormattedMessage } from 'react-intl';
+import ReactPlaceholder from 'react-placeholder';
import messages from './messages';
import { UserAvatar } from './avatar';
@@ -10,6 +11,7 @@ import { SearchIcon, CloseIcon } from '../svgIcons';
import { Dropdown } from '../dropdown';
import { SettingsIcon, CheckIcon } from '../svgIcons';
import Popup from 'reactjs-popup';
+import { nCardPlaceholders } from './usersPlaceholder';
const UserFilter = ({ filters, setFilters, updateFilters, intl }) => {
const inputRef = useRef(null);
@@ -146,12 +148,19 @@ export const UsersTable = ({ filters, setFilters }) => {
const [response, setResponse] = useState(null);
const userDetails = useSelector((state) => state.auth.get('userDetails'));
const [status, setStatus] = useState({ status: null, message: '' });
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
useEffect(() => {
const fetchUsers = async (filters) => {
+ setLoading(true);
const url = `users/?${filters}`;
- const res = await fetchLocalJSONAPI(url, token);
- setResponse(res);
+ fetchLocalJSONAPI(url, token)
+ .then((res) => {
+ setResponse(res);
+ setLoading(false);
+ })
+ .catch((err) => setError(err));
};
// Filter elements according to logic.
@@ -176,27 +185,35 @@ export const UsersTable = ({ filters, setFilters }) => {
fetchUsers(urlFilters);
}, [filters, token, status]);
- if (response === null) {
- return null;
- }
-
return (
-
-
-
+ {response?.users && (
+
+
+
+ )}
-
- {response.users.map((user) => (
-
- ))}
-
+
+
+ {response?.users.map((user) => (
+
+ ))}
+
+
{response === null || response.pagination.total === 0 ? null : (
{
+ it('renders user card', async () => {
+ const { container, getByText, getAllByRole } = render(
+
+
+ ,
+ );
+ await waitFor(() => {
+ expect(getByText(/Ram/i));
+ });
+ expect(getAllByRole('listitem')).toHaveLength(2);
+ expect(getByText(/total number of users: 220111/i)).toBeInTheDocument();
+ expect(screen.getByText('Ram').closest('a')).toHaveAttribute('href', '/users/Ram');
+ expect(screen.getAllByText('Mapper').length).toBe(2);
+ expect(getByText('Beginner')).toBeInTheDocument();
+ expect(getByText('Shyam')).toBeInTheDocument();
+ expect(screen.getByText('Shyam').closest('a')).toHaveAttribute('href', '/users/Shyam');
+ expect(screen.getByTitle(/Ram/i)).toHaveStyle(
+ `background-image: url(https://www.openstreetmap.org/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBNXQ2Q3c9PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--fe41f1b2a5d6cf492a7133f15c81f105dec06ff7/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBPZ2h3Ym1jNkZISmxjMmw2WlY5MGIxOXNhVzFwZEZzSGFXbHBhUT09IiwiZXhwIjpudWxsLCJwdXIiOiJ2YXJpYXRpb24ifX0=--058ac785867b32287d598a314311e2253bd879a3/unnamed.webp)`,
+ );
+ expect(container.querySelectorAll('svg').length).toBe(2);
+ });
+});
diff --git a/frontend/src/components/user/usersPlaceholder.js b/frontend/src/components/user/usersPlaceholder.js
new file mode 100644
index 0000000000..d301ab54f0
--- /dev/null
+++ b/frontend/src/components/user/usersPlaceholder.js
@@ -0,0 +1,40 @@
+import React from 'react';
+import { TextRow, RoundShape } from 'react-placeholder/lib/placeholders';
+
+export const userCardPlaceholderTemplate = () => (_n, i) =>
+ (
+
+ );
+
+export const nCardPlaceholders = (N) => {
+ return [...Array(N).keys()].map(userCardPlaceholderTemplate());
+};
diff --git a/frontend/src/network/tests/mockData/userList.js b/frontend/src/network/tests/mockData/userList.js
new file mode 100644
index 0000000000..0c217dfac3
--- /dev/null
+++ b/frontend/src/network/tests/mockData/userList.js
@@ -0,0 +1,29 @@
+export const usersList = {
+ users: [
+ {
+ id: 1,
+ username: 'Ram',
+ role: 'MAPPER',
+ mappingLevel: 'BEGINNER',
+ pictureUrl:
+ 'https://www.openstreetmap.org/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBNXQ2Q3c9PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--fe41f1b2a5d6cf492a7133f15c81f105dec06ff7/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBPZ2h3Ym1jNkZISmxjMmw2WlY5MGIxOXNhVzFwZEZzSGFXbHBhUT09IiwiZXhwIjpudWxsLCJwdXIiOiJ2YXJpYXRpb24ifX0=--058ac785867b32287d598a314311e2253bd879a3/unnamed.webp',
+ },
+ {
+ id: 2,
+ username: 'Shyam',
+ role: 'MAPPER',
+ mappingLevel: 'ADVANCED',
+ pictureUrl: null,
+ },
+ ],
+ pagination: {
+ hasNext: true,
+ hasPrev: false,
+ nextNum: 2,
+ page: 1,
+ pages: 11006,
+ prevNum: null,
+ perPage: 20,
+ total: 220111,
+ },
+};
diff --git a/frontend/src/network/tests/server-handlers.js b/frontend/src/network/tests/server-handlers.js
index 7cf28e25a1..998b0bd419 100644
--- a/frontend/src/network/tests/server-handlers.js
+++ b/frontend/src/network/tests/server-handlers.js
@@ -4,6 +4,7 @@ import { getProjectSummary, getProjectStats } from './mockData/projects';
import { featuredProjects } from './mockData/featuredProjects';
import { newUsersStats } from './mockData/userStats';
import { projectContributions, projectContributionsByDay } from './mockData/contributions';
+import { usersList } from './mockData/userList';
import tasksGeojson from '../../utils/tests/snippets/tasksGeometry';
import { API_URL } from '../../config';
@@ -34,6 +35,9 @@ const handlers = [
rest.get(API_URL + 'users/statistics/', async (req, res, ctx) => {
return res(ctx.json(newUsersStats));
}),
+ rest.get(API_URL + 'users', async (req, res, ctx) => {
+ return res(ctx.json(usersList));
+ }),
];
export { handlers };
diff --git a/frontend/src/views/campaigns.js b/frontend/src/views/campaigns.js
index e114e696e0..e3d8b12f1a 100644
--- a/frontend/src/views/campaigns.js
+++ b/frontend/src/views/campaigns.js
@@ -1,8 +1,7 @@
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import { Link, useNavigate } from '@reach/router';
-import ReactPlaceholder from 'react-placeholder';
-import { TextBlock, RectShape } from 'react-placeholder/lib/placeholders';
+
import { FormattedMessage } from 'react-intl';
import { Form } from 'react-final-form';
@@ -40,26 +39,14 @@ export function ListCampaigns() {
const userDetails = useSelector((state) => state.auth.get('userDetails'));
// TO DO: filter teams of current user
const [error, loading, campaigns] = useFetch(`campaigns/`);
-
- const placeHolder = (
-
- );
+ const isCampaignsFetched = !loading && !error;
return (
-
-
-
+
);
}
diff --git a/frontend/src/views/interests.js b/frontend/src/views/interests.js
index 895bc5e795..c7e54c461d 100644
--- a/frontend/src/views/interests.js
+++ b/frontend/src/views/interests.js
@@ -1,8 +1,6 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { useFetch } from '../hooks/UseFetch';
-import { TextBlock, RectShape } from 'react-placeholder/lib/placeholders';
-import ReactPlaceholder from 'react-placeholder';
import { Link, useNavigate } from '@reach/router';
import { Form } from 'react-final-form';
import { FormattedMessage } from 'react-intl';
@@ -76,26 +74,14 @@ export const ListInterests = () => {
const userDetails = useSelector((state) => state.auth.get('userDetails'));
// TO DO: filter teams of current user
const [error, loading, interests] = useFetch(`interests/`);
-
- const placeHolder = (
-
- );
+ const isInterestsFetched = !loading && !error;
return (
-
-
-
+
);
};
diff --git a/frontend/src/views/licenses.js b/frontend/src/views/licenses.js
index 1049a718fd..8e5b02f7c6 100644
--- a/frontend/src/views/licenses.js
+++ b/frontend/src/views/licenses.js
@@ -2,8 +2,6 @@ import React from 'react';
import { useSelector } from 'react-redux';
import { useFetch } from '../hooks/UseFetch';
import { useSetTitleTag } from '../hooks/UseMetaTags';
-import { TextBlock, RectShape } from 'react-placeholder/lib/placeholders';
-import ReactPlaceholder from 'react-placeholder';
import { Link, useNavigate } from '@reach/router';
import { Form } from 'react-final-form';
import { FormattedMessage } from 'react-intl';
@@ -49,26 +47,14 @@ export const ListLicenses = () => {
const userDetails = useSelector((state) => state.auth.get('userDetails'));
// TO DO: filter teams of current user
const [error, loading, licenses] = useFetch(`licenses/`);
-
- const placeHolder = (
-
- );
+ const isLicensesFetched = !loading && !error;
return (
-
-
-
+
);
};
diff --git a/frontend/src/views/organisationManagement.js b/frontend/src/views/organisationManagement.js
index b3a3eea953..4bc630033e 100644
--- a/frontend/src/views/organisationManagement.js
+++ b/frontend/src/views/organisationManagement.js
@@ -2,7 +2,6 @@ import React, { useState, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { Link, useNavigate } from '@reach/router';
import ReactPlaceholder from 'react-placeholder';
-import { RectShape } from 'react-placeholder/lib/placeholders';
import { FormattedMessage } from 'react-intl';
import { Form } from 'react-final-form';
@@ -32,37 +31,31 @@ export function ListOrganisations() {
);
const [organisations, setOrganisations] = useState(null);
const [userOrgsOnly, setUserOrgsOnly] = useState(userDetails.role === 'ADMIN' ? false : true);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
useEffect(() => {
if (token && userDetails && userDetails.id) {
+ setLoading(true);
const queryParam = `${userOrgsOnly ? `?manager_user_id=${userDetails.id}` : ''}`;
- fetchLocalJSONAPI(`organisations/${queryParam}`, token).then((orgs) =>
- setOrganisations(orgs.organisations),
- );
+ fetchLocalJSONAPI(`organisations/${queryParam}`, token)
+ .then((orgs) => {
+ setOrganisations(orgs.organisations);
+ setLoading(false);
+ })
+ .catch((err) => setError(err));
}
}, [userDetails, token, userOrgsOnly]);
- const placeHolder = (
-
-
-
-
- );
-
return (
-
-
-
+
);
}
diff --git a/frontend/src/views/teams.js b/frontend/src/views/teams.js
index eccbf453ca..19006c8a76 100644
--- a/frontend/src/views/teams.js
+++ b/frontend/src/views/teams.js
@@ -1,8 +1,6 @@
import React, { useState, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { Link, useNavigate } from '@reach/router';
-import ReactPlaceholder from 'react-placeholder';
-import { TextBlock, RectShape } from 'react-placeholder/lib/placeholders';
import { FormattedMessage } from 'react-intl';
import { Form } from 'react-final-form';
@@ -50,6 +48,8 @@ export function ListTeams({ managementView = false }: Object) {
const token = useSelector((state) => state.auth.get('token'));
const [teams, setTeams] = useState(null);
const [userTeamsOnly, setUserTeamsOnly] = useState(true);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
useEffect(() => {
if (token && userDetails && userDetails.id) {
@@ -59,36 +59,25 @@ export function ListTeams({ managementView = false }: Object) {
} else {
queryParam = `?member=${userDetails.id}`;
}
- fetchLocalJSONAPI(`teams/${queryParam}`, token).then((res) => setTeams(res.teams));
+ setLoading(true);
+ fetchLocalJSONAPI(`teams/${queryParam}`, token)
+ .then((res) => {
+ setTeams(res.teams);
+ setLoading(false);
+ })
+ .catch((err) => setError(err));
}
}, [userDetails, token, managementView, userTeamsOnly]);
- const placeHolder = (
-
- );
-
return (
-
-
-
+
);
}