feat: [FC-0044] Certificates page (#872)

* feat: [FC-0044]  Certificates page

* feat: add descriptions for details, signatories, sidebar i18n messages

---------

Co-authored-by: Kyrylo Hudym-Levkovych <kyr.hudym@kyrs-MacBook-Pro.local>
This commit is contained in:
Kyr
2024-04-04 20:28:04 +03:00
committed by GitHub
parent b61cb5c7cd
commit e306b62dd1
68 changed files with 4255 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
/* eslint-disable import/prefer-default-export */
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { prepareCertificatePayload } from '../utils';
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
export const getCertificatesApiUrl = (courseId) => `${getApiBaseUrl()}/api/contentstore/v1/certificates/${courseId}`;
export const getCertificateApiUrl = (courseId) => `${getApiBaseUrl()}/certificates/${courseId}`;
export const getUpdateCertificateApiUrl = (courseId, certificateId) => `${getCertificateApiUrl(courseId)}/${certificateId}`;
export const getUpdateCertificateActiveStatusApiUrl = (path) => `${getApiBaseUrl()}${path}`;
/**
* Gets certificates for a course.
* @param {string} courseId
* @returns {Promise<Object>}
*/
export async function getCertificates(courseId) {
const { data } = await getAuthenticatedHttpClient()
.get(getCertificatesApiUrl(courseId));
return camelCaseObject(data);
}
/**
* Create course certificate.
* @param {string} courseId
* @param {object} certificatesData
* @returns {Promise<Object>}
*/
export async function createCertificate(courseId, certificatesData) {
const { data } = await getAuthenticatedHttpClient()
.post(
getCertificateApiUrl(courseId),
prepareCertificatePayload(certificatesData),
);
return camelCaseObject(data);
}
/**
* Update course certificate.
* @param {string} courseId
* @param {object} certificateData
* @returns {Promise<Object>}
*/
export async function updateCertificate(courseId, certificateData) {
const { data } = await getAuthenticatedHttpClient()
.post(
getUpdateCertificateApiUrl(courseId, certificateData.id),
prepareCertificatePayload(certificateData),
);
return camelCaseObject(data);
}
/**
* Delete course certificate.
* @param {string} courseId
* @param {object} certificateId
* @returns {Promise<Object>}
*/
export async function deleteCertificate(courseId, certificateId) {
const { data } = await getAuthenticatedHttpClient()
.delete(
getUpdateCertificateApiUrl(courseId, certificateId),
);
return data;
}
/**
* Activate/deactivate course certificate.
* @param {string} courseId
* @param {object} activationStatus
* @returns {Promise<Object>}
*/
export async function updateActiveStatus(path, activationStatus) {
const body = {
is_active: activationStatus,
};
const { data } = await getAuthenticatedHttpClient()
.post(
getUpdateCertificateActiveStatusApiUrl(path),
body,
);
return camelCaseObject(data);
}

View File

@@ -0,0 +1,12 @@
export const MODE_STATES = {
noModes: 'no_modes',
noCertificates: 'no_certificates',
view: 'view',
editAll: 'edit_all',
create: 'create',
};
export const ACTIVATION_MESSAGES = {
activating: 'Activating',
deactivating: 'Deactivating',
};

View File

@@ -0,0 +1,21 @@
import { createSelector } from '@reduxjs/toolkit';
export const getLoadingStatus = (state) => state.certificates.loadingStatus;
export const getSavingStatus = (state) => state.certificates.savingStatus;
export const getSavingImageStatus = (state) => state.certificates.savingImageStatus;
export const getSendRequestErrors = (state) => state.certificates.sendRequestErrors.developer_message;
export const getCertificates = state => state.certificates.certificatesData.certificates;
export const getHasCertificateModes = state => state.certificates.certificatesData.hasCertificateModes;
export const getCourseModes = state => state.certificates.certificatesData.courseModes;
export const getCertificateActivationUrl = state => state.certificates.certificatesData.certificateActivationHandlerUrl;
export const getCertificateWebViewUrl = state => state.certificates.certificatesData.certificateWebViewUrl;
export const getIsCertificateActive = state => state.certificates.certificatesData.isActive;
export const getComponentMode = state => state.certificates.componentMode;
export const getCourseNumber = state => state.certificates.certificatesData.courseNumber;
export const getCourseNumberOverride = state => state.certificates.certificatesData.courseNumberOverride;
export const getCourseTitle = state => state.certificates.certificatesData.courseTitle;
export const getHasCertificates = createSelector(
[getCertificates],
(certificates) => certificates && certificates.length > 0,
);

View File

@@ -0,0 +1,59 @@
/* eslint-disable no-param-reassign */
import { createSlice } from '@reduxjs/toolkit';
import { RequestStatus } from '../../data/constants';
import { MODE_STATES } from './constants';
const slice = createSlice({
name: 'certificates',
initialState: {
certificatesData: {},
componentMode: MODE_STATES.noModes,
loadingStatus: RequestStatus.PENDING,
savingStatus: '',
savingImageStatus: '',
},
reducers: {
updateSavingStatus: (state, { payload }) => {
state.savingStatus = payload.status;
},
updateSavingImageStatus: (state, { payload }) => {
state.savingImageStatus = payload.status;
},
updateLoadingStatus: (state, { payload }) => {
state.loadingStatus = payload.status;
},
fetchCertificatesSuccess: (state, { payload }) => {
Object.assign(state.certificatesData, payload);
},
createCertificateSuccess: (state, action) => {
state.certificatesData.certificates.push(action.payload);
},
updateCertificateSuccess: (state, action) => {
const index = state.certificatesData.certificates.findIndex(c => c.id === action.payload.id);
if (index !== -1) {
state.certificatesData.certificates[index] = action.payload;
}
},
setMode: (state, action) => {
state.componentMode = action.payload;
},
deleteCertificateSuccess: (state) => {
state.certificatesData.certificates = [];
},
},
});
export const {
setMode,
updateSavingStatus,
updateLoadingStatus,
updateSavingImageStatus,
fetchCertificatesSuccess,
createCertificateSuccess,
updateCertificateSuccess,
deleteCertificateSuccess,
} = slice.actions;
export const { reducer } = slice;

View File

@@ -0,0 +1,120 @@
import { RequestStatus } from '../../data/constants';
import {
hideProcessingNotification,
showProcessingNotification,
} from '../../generic/processing-notification/data/slice';
import { NOTIFICATION_MESSAGES } from '../../constants';
import {
getCertificates,
createCertificate,
updateCertificate,
deleteCertificate,
updateActiveStatus,
} from './api';
import {
fetchCertificatesSuccess,
updateLoadingStatus,
updateSavingStatus,
createCertificateSuccess,
updateCertificateSuccess,
deleteCertificateSuccess,
} from './slice';
import { ACTIVATION_MESSAGES } from './constants';
export function fetchCertificates(courseId) {
return async (dispatch) => {
dispatch(updateLoadingStatus({ status: RequestStatus.IN_PROGRESS }));
try {
const certificates = await getCertificates(courseId);
dispatch(fetchCertificatesSuccess(certificates));
dispatch(updateLoadingStatus({ status: RequestStatus.SUCCESSFUL }));
} catch (error) {
if (error.response && error.response.status === 403) {
dispatch(updateLoadingStatus({ courseId, status: RequestStatus.DENIED }));
} else {
dispatch(updateLoadingStatus({ courseId, status: RequestStatus.FAILED }));
}
}
};
}
export function createCourseCertificate(courseId, certificate) {
return async (dispatch) => {
dispatch(updateSavingStatus({ status: RequestStatus.PENDING }));
dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.saving));
try {
const certificateValues = await createCertificate(courseId, certificate);
dispatch(createCertificateSuccess(certificateValues));
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
return true;
} catch (error) {
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
return false;
} finally {
dispatch(hideProcessingNotification());
}
};
}
export function updateCourseCertificate(courseId, certificate) {
return async (dispatch) => {
dispatch(updateSavingStatus({ status: RequestStatus.PENDING }));
dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.saving));
try {
const certificatesValues = await updateCertificate(courseId, certificate);
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
dispatch(updateCertificateSuccess(certificatesValues));
return true;
} catch (error) {
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
return false;
} finally {
dispatch(hideProcessingNotification());
}
};
}
export function deleteCourseCertificate(courseId, certificateId) {
return async (dispatch) => {
dispatch(updateSavingStatus({ status: RequestStatus.PENDING }));
dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.deleting));
try {
const certificatesValues = await deleteCertificate(courseId, certificateId);
dispatch(deleteCertificateSuccess(certificatesValues));
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
return true;
} catch (error) {
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
return false;
} finally {
dispatch(hideProcessingNotification());
}
};
}
export function updateCertificateActiveStatus(courseId, path, activationStatus) {
return async (dispatch) => {
dispatch(updateSavingStatus({ status: RequestStatus.PENDING }));
dispatch(showProcessingNotification(
activationStatus ? ACTIVATION_MESSAGES.activating : ACTIVATION_MESSAGES.deactivating,
));
try {
await updateActiveStatus(path, activationStatus);
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
dispatch(fetchCertificates(courseId));
return true;
} catch (error) {
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
return false;
} finally {
dispatch(hideProcessingNotification());
}
};
}