Skip to content

Commit

Permalink
feat: display course credentials on VC page
Browse files Browse the repository at this point in the history
  • Loading branch information
kyrylo-kh committed Jan 27, 2025
1 parent db6fc53 commit 46049a6
Show file tree
Hide file tree
Showing 23 changed files with 2,239 additions and 3,797 deletions.
5,703 changes: 2,041 additions & 3,662 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
"@fortawesome/react-fontawesome": "0.2.2",
"@openedx/frontend-plugin-framework": "^1.2.0",
"@openedx/paragon": "^22.2.2",
"ajv": "^8.12.0",
"ajv-keywords": "^5.1.0",
"babel-polyfill": "6.26.0",
"core-js": "3.40.0",
"js-cookie": "3.0.5",
Expand All @@ -59,7 +61,8 @@
"react-router": "6.28.2",
"react-router-dom": "6.28.2",
"redux": "4.2.1",
"regenerator-runtime": "0.14.1"
"regenerator-runtime": "0.14.1",
"schema-utils": "^4.2.0"
},
"devDependencies": {
"@edx/browserslist-config": "^1.2.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import {
import { Hyperlink, DropdownButton, Dropdown } from '@openedx/paragon';
import messages from './messages';

function ProgramCertificate({
function Certificate({
intl,
program_title: programTitle,
program_org: programOrg,
type,
credential_title: certificateTitle,
credential_org: certificateOrg,
modified_date: modifiedDate,
uuid,
handleCreate,
Expand Down Expand Up @@ -48,15 +49,17 @@ function ProgramCertificate({
<div className="card-body d-flex flex-column">
<div className="card-title">
<p className="small mb-0">
{intl.formatMessage(messages.certificateCardName)}
{type === 'program'
? intl.formatMessage(messages.programCertificateCardName)
: intl.formatMessage(messages.courseCertificateCardName)}
</p>
<h4 className="certificate-title">{programTitle}</h4>
<h4 className="certificate-title">{certificateTitle}</h4>
</div>
<p className="small mb-0">
<p className="small mb-0 mt-auto">
{intl.formatMessage(messages.certificateCardOrgLabel)}
</p>
<p className="h6 mb-4">
{programOrg
{certificateOrg
|| intl.formatMessage(messages.certificateCardNoOrgText)}
</p>
<p className="small mb-2">
Expand All @@ -71,10 +74,11 @@ function ProgramCertificate({
);
}

ProgramCertificate.propTypes = {
Certificate.propTypes = {
intl: intlShape.isRequired,
program_title: PropTypes.string.isRequired,
program_org: PropTypes.string.isRequired,
type: PropTypes.oneOf(['program', 'course']),
credential_title: PropTypes.string.isRequired,
credential_org: PropTypes.string.isRequired,
modified_date: PropTypes.string.isRequired,
uuid: PropTypes.string.isRequired,
handleCreate: PropTypes.func.isRequired,
Expand All @@ -86,4 +90,4 @@ ProgramCertificate.propTypes = {
).isRequired,
};

export default injectIntl(ProgramCertificate);
export default injectIntl(Certificate);
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// eslint-disable-next-line no-restricted-exports
export { default } from './ProgramCertificate';
export { default } from './Certificate';
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { defineMessages } from '@edx/frontend-platform/i18n';

const messages = defineMessages({
certificateCardName: {
id: 'certificate.card.name',
programCertificateCardName: {
id: 'certificate.program.card.name',
defaultMessage: 'Program Certificate',
description: 'A title text of the available program certificate item.',
},
courseCertificateCardName: {
id: 'certificate.course.card.name',
defaultMessage: 'Course Certificate',
description: 'A title text of the available course certificate item.',
},
certificateCardOrgLabel: {
id: 'certificate.card.organization.label',
defaultMessage: 'From',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import React from 'react';
import {
render, screen, cleanup, initializeMockApp, fireEvent,
} from '../../../setupTest';
import ProgramCertificate from '..';
import Certificate from '../Certificate';

describe('program-certificate', () => {
beforeAll(async () => {
Expand All @@ -15,30 +15,39 @@ describe('program-certificate', () => {
afterEach(cleanup);

const props = {
program_title: 'Program name',
program_org: 'Test org',
credential_title: 'Certificate title',
credential_org: 'Test org',
modified_date: '2023-02-02',
storages: [{ id: 'storageId', name: 'storageName' }],
handleCreate: jest.fn(),
};

it('renders the component', () => {
render(<ProgramCertificate {...props} />);
it('renders the component with type programm', () => {
render(<Certificate {...props} type="program" />);

expect(screen.getByText('Program Certificate')).toBeTruthy();
expect(screen.getByText(props.program_title)).toBeTruthy();
expect(screen.getByText(props.program_org)).toBeTruthy();
expect(screen.getByText(props.credential_title)).toBeTruthy();
expect(screen.getByText(props.credential_org)).toBeTruthy();
expect(screen.getByText('Awarded on 2/2/2023')).toBeTruthy();
});

it('should display "No organization" if Program Organization wasn\'t set', () => {
render(<ProgramCertificate {...props} program_org="" />);
it('renders the component with type course', () => {
render(<Certificate {...props} type="course" />);

expect(screen.getByText('Course Certificate')).toBeTruthy();
expect(screen.getByText(props.credential_title)).toBeTruthy();
expect(screen.getByText(props.credential_org)).toBeTruthy();
expect(screen.getByText('Awarded on 2/2/2023')).toBeTruthy();
});

it('should display "No organization" if Organization wasn\'t set', () => {
render(<Certificate {...props} credential_org="" />);

expect(screen.getByText('No organization')).toBeTruthy();
});

it('renders modal by clicking on a create button', () => {
render(<ProgramCertificate {...props} />);
render(<Certificate {...props} />);
fireEvent.click(screen.getByText('Create'));

expect(screen.findByTitle('Verifiable credential')).toBeTruthy();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import messages from './messages';
import appStoreImg from '../../assets/images/appStore.png';
import googlePlayImg from '../../assets/images/googleplay.png';

function ProgramCertificateModal({
function CertificateModal({
intl, isOpen, close, data,
}) {
const {
Expand Down Expand Up @@ -171,11 +171,11 @@ function ProgramCertificateModal({
);
}

ProgramCertificateModal.propTypes = {
CertificateModal.propTypes = {
intl: intlShape.isRequired,
isOpen: PropTypes.bool.isRequired,
close: PropTypes.func.isRequired,
data: PropTypes.shape.isRequired,
};

export default injectIntl(ProgramCertificateModal);
export default injectIntl(CertificateModal);
2 changes: 2 additions & 0 deletions src/components/CertificateModal/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// eslint-disable-next-line no-restricted-exports
export { default } from './CertificateModal';
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const messages = defineMessages({
id: 'credentials.modal.mobile.title',
defaultMessage:
'To download a verifiable credential to your mobile wallet application, please follow the instructions below.',
description: 'Text for a mobile dialog of the program certificate.',
description: 'Text for a mobile dialog of the certificate.',
},
certificateModalAppStoreBtn: {
id: 'credentials.modal.instruction.appStore.button',
Expand Down Expand Up @@ -84,9 +84,9 @@ const messages = defineMessages({
credentialsModalError: {
id: 'credentials.modal.error',
defaultMessage:
'An error occurred attempting to retrieve your program certificate. Please try again later.',
'An error occurred attempting to retrieve your certificate. Please try again later.',
description:
"An error message indicating there is a problem retrieving the user's program certificate data",
"An error message indicating there is a problem retrieving the user's certificate data",
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* @jest-environment jsdom
*/
import React from 'react';
import ProgramCertificateModal from '..';
import CertificateModal from '..';
import {
render, screen, cleanup, initializeMockApp,
} from '../../../setupTest';
Expand All @@ -18,15 +18,15 @@ const props = {
},
};

describe('program-certificate-modal', () => {
describe('certificate-modal', () => {
beforeAll(async () => {
await initializeMockApp();
});
beforeEach(() => jest.resetModules);
afterEach(cleanup);

it('renders the component', () => {
render(<ProgramCertificateModal {...props} />);
render(<CertificateModal {...props} />);
expect(screen.getByText('Verifiable credential')).toBeTruthy();
expect(screen.getByText('Close modal window')).toBeTruthy();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,41 +9,44 @@ import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { getConfig } from '@edx/frontend-platform/config';
import { logError } from '@edx/frontend-platform/logging';

import ProgramCertificate from '../ProgramCertificate';
import NavigationBar from '../NavigationBar';
import {
getProgramCertificates,
getCertificates,
getAvailableStorages,
initVerifiableCredentialIssuance,
} from './data/service';
import messages from './messages';
import ProgramCertificateModal from '../ProgramCertificateModal';
import CertificateModal from '../CertificateModal';
import Certificate from '../Certificate';

function ProgramCertificatesList({ intl }) {
function CertificatesList({ intl }) {
const [certificatesAreLoaded, setCertificatesAreLoaded] = useState(false);
const [dataLoadingIssue, setDataLoadingIssue] = useState('');
const [certificates, setCertificates] = useState([]);
const [certificates, setCertificates] = useState({
program_credentials: [],
course_credentials: [],
});

const [storagesIsLoaded, setStoragesIsLoaded] = useState(false);
const [storages, setStorages] = useState([]);

const [modalIsOpen, openModal, closeModal] = useToggle(false);

const isCertificatesEmpty = !certificates.program_credentials.length && !certificates.course_credentials.length;

const [
verifiableCredentialIssuanceData,
setVerifiableCredentialIssuanceData,
] = useState({});

useEffect(() => {
getProgramCertificates()
getCertificates()
.then((data) => {
setCertificates(data.program_credentials);
setCertificates(data);
setCertificatesAreLoaded(true);
})
.catch((error) => {
const errorMessage = intl.formatMessage(
messages.errorProgramCertificatesLoading,
);
const errorMessage = intl.formatMessage(messages.errorCertificatesLoading);

Check warning on line 49 in src/components/CertificatesList/CertificatesList.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/CertificatesList/CertificatesList.jsx#L49

Added line #L49 was not covered by tests
setDataLoadingIssue(errorMessage);
logError(errorMessage + error.message);
});
Expand Down Expand Up @@ -112,35 +115,38 @@ function ProgramCertificatesList({ intl }) {
</p>
);

const renderProgramCertificates = () => (
<section id="program-certificates-list" className="pl-3 pr-3 pb-3">
<p>{intl.formatMessage(messages.credentialsDescription)}</p>
<Row className="mt-4">
{certificates.map((certificate) => (
<ProgramCertificate
key={certificate.uuid}
storages={storages}
handleCreate={handleCreate}
{...certificate}
/>
))}
</Row>
</section>
);

const renderData = () => {
if (dataLoadingIssue) {
return renderCredentialsServiceIssueAlert({
message: dataLoadingIssue,
});
}
if (!certificates.length) {
return renderEmpty();
const renderCertificates = (type) => {
if (!certificates[`${type}_credentials`].length) {
return null;

Check warning on line 120 in src/components/CertificatesList/CertificatesList.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/CertificatesList/CertificatesList.jsx#L120

Added line #L120 was not covered by tests
}

return (
<section id={`${type}-certificates-list`} className="pl-3 pr-3 pb-3">
<p>
{type === 'program'
? intl.formatMessage(messages.programCredentialsDescription)
: intl.formatMessage(messages.courseCredentialsDescription)}
</p>
<Row className="mt-4">
{certificates[`${type}_credentials`].map((certificate) => (
<Certificate
type={type}
key={certificate.uuid}
storages={storages}
handleCreate={handleCreate}
{...certificate}
/>
))}
</Row>
</section>
);
};

const renderData = (type) => {
if (!certificatesAreLoaded || !storagesIsLoaded) {
return null;
}
return renderProgramCertificates();
return renderCertificates(type);
};

const renderHelp = () => (
Expand All @@ -159,6 +165,24 @@ function ProgramCertificatesList({ intl }) {
</div>
);

const renderContent = () => {
if (dataLoadingIssue) {
return renderCredentialsServiceIssueAlert({

Check warning on line 170 in src/components/CertificatesList/CertificatesList.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/CertificatesList/CertificatesList.jsx#L170

Added line #L170 was not covered by tests
message: dataLoadingIssue,
});
}
if (isCertificatesEmpty) {
return renderEmpty();
}

return (
<>
{renderData('program')}
{renderData('course')}
</>
);
};

return (
<main id="main-content" className="pt-5 pb-5 pl-4 pr-4" tabIndex="-1">
<div className="container-fluid">
Expand All @@ -167,9 +191,9 @@ function ProgramCertificatesList({ intl }) {
<h1 className="h3 pl-3 pr-3 mb-4">
{intl.formatMessage(messages.credentialsHeader)}
</h1>
{renderData()}
{renderContent()}
{renderHelp()}
<ProgramCertificateModal
<CertificateModal
isOpen={modalIsOpen}
close={closeModal}
data={verifiableCredentialIssuanceData}
Expand All @@ -179,8 +203,8 @@ function ProgramCertificatesList({ intl }) {
);
}

ProgramCertificatesList.propTypes = {
CertificatesList.propTypes = {
intl: intlShape.isRequired,
};

export default injectIntl(ProgramCertificatesList);
export default injectIntl(CertificatesList);
Loading

0 comments on commit 46049a6

Please sign in to comment.