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:
89
src/certificates/data/api.js
Normal file
89
src/certificates/data/api.js
Normal 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);
|
||||
}
|
||||
12
src/certificates/data/constants.js
Normal file
12
src/certificates/data/constants.js
Normal 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',
|
||||
};
|
||||
21
src/certificates/data/selectors.js
Normal file
21
src/certificates/data/selectors.js
Normal 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,
|
||||
);
|
||||
59
src/certificates/data/slice.js
Normal file
59
src/certificates/data/slice.js
Normal 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;
|
||||
120
src/certificates/data/thunks.js
Normal file
120
src/certificates/data/thunks.js
Normal 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());
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user