;7Y9kb5@ZoGag~oOBrQeDl0eXCS;XTf*_4OppLiULaFNnZt
z!bB>rZu%@~f!!>gVvXXN%^_=XG4SjZmFxx{Oj2&B-%oMxjp9%^*2sc<^r}3H6?uDS
zUVOc_OEZIrq*~0ph^jG*Ib?Ud`Vvp@LK(N$-!K8{V|Y-Xl848f2_x@Z*8}GXEAz7L
z;VZsSy*WkBew`}$8|x_iTQobW$PD4>8acHXlT7?7x52>S<*y9V-hD894zbuL|kk
zekx?i6K2OIVMP@ngIjpq%DA34Y@Grk)`Dtv;SCphoR@q%2K|dF_x!mCD*4}vjbBXS
z%?NS|1oCK)`%9k`Ehq(WzIZ7HGU2Q@>qV4}#}}zUucM!HReT85UgmRd@wRYvigdOS
zs*49)4|CLR?}y*Ma;I0HKgQ6`JlY#Hjyi0?@I#=ZEcQUevkr&}<76Gv=Yl_J8U|>%
zD!s-Jl-3S4b&ss;X7vx)c_B|*O>11t)gq*R_{M~5onJP!z(DcPurFv;4}ar9%2+q%
zE)3dJ#}H~8J@_H$ugb)n%VyH^xmur|w+(Y-WMr}B7Y!$p&$OUX=I@o(8D-~+glJao
zN6eYLbh@s#HM07qE3ezTlo2zig2}>tyeB|9=nngKmYOx~+HOTT7mRIUA5LprF9BOS0lE4i)NPn0gzGv3IRWvAD!g
zyllcev;kY+w(rN;)FR-LU~{aU?OKlU*dSoJDqA3s=9m*a)Vo#ldtyvDDetv3VI29A
znyWcHN?iPv9QRTQ8wiB(A$+q6*hF+yxZNL9{^Qa_PKWS!-?HUlcCn1><^={9`n@&`
z-mNwD$;SD*f70>1Wpi_doP4*W!}5!E71Svo-llzmn*P!Uc*L-uWvXgI=6T#1b<~76
zd&p}@-s~)b;K#|mmBh;grQb3CfTyX_xunEz#FNYKmw#$QAL1RZhMBg(hylVI4n`|`VMywU^E5e4J6*-j2
z)diViE-9~H8)FUXH*U*Ygh8DLUK=2EIa1uh?A^&r6xi+E0_lZkSrZ0UKgx01qpyAoeq*DNn*a{Ed7wF#uH
zE$0S3x!N`6!^saA_R3E$6OU)P;EXz)=vUOp{DzE&X)VhS`Ajmy_j4IP_Y2N;oQf2b
zY*F>jAL+IV$B5O4X6p}dBULo3j0$vAahCBtz(OL~;d1*mY?Au>+To|C&p~R{g)6um
zOXKl{l%++K%H+8?vYr%VjR<}-7#mi({tlMa&*{5n9h)>I;E&a-4aRjUbX1T~)5(#-6YK%TW+WV#tx}pHfGV)cNeS1;LwgSimG$}W|O*ZJpV0%dL
zotGp@g%LVN{#S(yuS+oxtS&!tD0{$6VwnKX1Od7G
zk<=GvFN$SPcvQS9!a7rrY*&%$rh#a-Z(At85FZO&J8}R$`k87
zbbNElo_QZGMgPgcqy*nTFvPe41p=|`LG+FF>L3f*zX`)TvufGD`Ho!i{;
zl6R)aQA2I8!G4gxgJ}4gQHYmEif4gVH}?^R{E%I&LFgsWsNVY-{n6^Tct$htysEFL
z_EYnffd2bp{6i7?M-7Cgfn{|{Iwv`ft-))iOaf4ax$D(aUYaI@Lb1GuHP*s
z>>-r>jV%ZmbD(IOZ^j0GLSQ&qP_5e%1w!J@S(UNG-;uOl-VrL&D?o4>lT6q@QoXZv
zXfAekH?xUayh#YnTD939tFZmj7*MeKZMELk44vadAf9O)C7Xb@vi0;fLR%x)Xy{@x
zg#2y=xdxG1qmeezWY5;2D!Etp-WM;y{?tTch;K_aLtPM%N`fMP5&
ziVG$auw7}}zaF!mSn^d#V$!}IUmg(ga{~;^3o_qQ*5A>X+j}+)X(~EaF#ps4mozm><%Z{TvNo>n_vN)RbJn{=5~;`uo|7
z*mdX5MmL)vOnc2kZJ~yN{OOoxn4&~AOk#atOWvsJ!nVCVD_>=g!7zKHbs?RSZ=a5I
ze7dLkM&xXY$H&=e8Bgh;J3YO`UC#Z3*ZvLJ|7$
zxNM>Mi03_jkF=yiAuW^6qt-=1AXt?vDEO#XbW0o5|yveV1-F;N_5id3E_L5i#
zz}jXwl-MC2m2WX+H^O}n7JGclb5xdEN*acg^6>)!6Z=lZ5jNQ}()7|~HOc<$0Zs*n
z%j>>IUJ_)8^6z@L$B%liFGI(^Z6s#Qo^tF)I3#EvrZSg6{rT%0iJ{f?o1^j*@D^Ey
zwtkpW{*V6m0}<(p0_LuK^N&q{?R8*Q^%Gl?w!r5HJ{^!f)sAq1PfiScgCxb|+g&W*aE&qhPeTAMX%C_f$%8NLtk
z(rO3ejde377&w!r*KEq(#9==D^l_u}oMttPO7H>^qB{zQN1%JYqgB1Na7afKN*|R8
z+FziLZJGZ&OpMNsrhnd@W9k5dk%_^&f>#rzi1;ygndhQ*r
z0C_ZO;$}SkA=x1{%-Q6yjNAafLboaF4Cw9K``5O9v0UjqnOH{B!r#|R1`
zgT}t`hDKH%28;~h)4H~oAb5m*o}r$o&26z&vNDOP~~S>k=n&b^%@tUvyq2e`2zR
zuRW_?(N%i`OOv}|3C^{DlgWvJyQANb+Kl<~2*6<G3h+{}Qc#B{dh
zM=0T~EwFC|aw&rlr~hSj5+^ekI~Zwdi#=Y6R-(a}p*G3W&O8Av0Gl=we)00;*9
zx=%55x#^H8f5~~VM9rygwQ75(kK`wl8^{~I_SFEA1Eh|T1jC}MH?02Sc|Rw
zo+O3}5nZ(^CZ{V&Y>era&(@!B$9LON0#LzBqy_k$-XWEh?khvSb@}zKbEw658@x=3
zCj0dCEbX^@z2EE(#7)lDimy%pBM+NbzGzwd^IyGn%=so|-)~LNQi@+v&z1WLGdZTg
zA%AL%&zG2u49z`G4E=I*HB1WNT$|G6wAnK@&0WB`qxW#O1xjMup#`;gu4o2
zt=OuJr+aVKZZg8>0=_Hvhkh1D#p0z-y7(y93<4;|_``2B4sN)q(O*FptP~T$
zypJ|=)Qt;oDfaprit;y{HL*DHya;E&!i9b|ON$L6-p
zn?z>)#PYPGnvQ;rNpnnr&9e?|Orshw0pCN)BbS6Z^7!~R%D~FW%4yDH@LnXh)F_b0
zJ=IuQtGcU~YJfFOv8^r1_-AYnN~tEbjnf$
zJ>-kaos^+kD14-j?nCjU_33Y7kUY~*D|CzY#V6b&1!OHFcM6E(ibaJ}&FA+l?m04c
z%WqpLa*<)8=I-?Ku>#~Q+af5sF{vwBb8cNgjO7HioEb}(@ap^t(nysGNK`tw&&rEV
zx|gsVd;E@z(=iH$h%G`A63FR0W6h)ZnANPqZaYVvf4&sl_SjD&J*IZ9t$R{X$=YxNH-jdx`RnnFx;g
zO6{|X8*lx!!y_qg_~5tY{&^RY)24uuZ4Qjjw{j}ua^#IM{fp|$Soj~btA?j=)RF{C
znM*3u$F-MsDxQ=t(#s~)!KD>%px#n@+7;$p);i3_6>*Gl8WweUb)59-5H;N^HM-{O
zpn-Y%)VhoW>TgOt;QB_Ar>tQL-KP=9%L1N{-;dTqj|aIk6x;U?#mv5F{~2SB?`JK~
zq4^5UYE30as5hfzJ$6*D4FQAAxvg@wtg$hj_NZN6eLo?(r*B%(=uP@^&2~_VUYGdF
zH@aZH^w3qgW>N?flKWcFX^7Ec$2WWc!Soqg_{H%qknve#xKvifx4;;y
zdm#-LzDcR@v7{=C3^dN532E-p(?e&H5aqo*`>df%?d=FHdWj{cVd&Z=r+$2m-ln?G
zIFI95V~Qcxguu(sMm3-Aw48&xV`S2{uCZF*sAXsp}(+N1Nu
z+5JL&LZ>x$FHZ4iEuer|HvKvyfvs!Wd(nom4NVq55(W=j6_=N750`cO_5l2>9
zSl)IOii@M6b${3)y`1PC*we_bz-Yw#dE(UxJOWD~+2q10IS7?!g9X=F#12ZRHZyZl2VT^KDwGXF1z{ST*n^@n{fVf_nG+S2^lvnyWK
z1^rD2|EWX%|5^9X_bfg5|ETbP;@AS=*Ai=NfEGUgN=bw8f{o}OQq state.customPages.loadingStatus;
+export const getSavingStatus = (state) => state.customPages.savingStatus;
+export const getCustomPagesApiStatus = (state) => state.customPages.customPagesApiStatus;
+// export const getCourseAppSettingValue = (setting) => (state) => (
+// state.pagesAndResources.courseAppSettings[setting]?.value
+// );
diff --git a/src/custom-pages/data/slice.js b/src/custom-pages/data/slice.js
new file mode 100644
index 000000000..cc1f24728
--- /dev/null
+++ b/src/custom-pages/data/slice.js
@@ -0,0 +1,54 @@
+/* eslint-disable no-param-reassign */
+import { createSlice } from '@reduxjs/toolkit';
+
+import { RequestStatus } from '../../data/constants';
+
+const slice = createSlice({
+ name: 'customPages',
+ initialState: {
+ customPagesIds: [],
+ loadingStatus: RequestStatus.IN_PROGRESS,
+ savingStatus: '',
+ addingStatus: 'default',
+ deletingStatus: '',
+ customPagesApiStatus: {},
+ },
+ reducers: {
+ setPageIds: (state, { payload }) => {
+ state.customPagesIds = payload.customPagesIds;
+ },
+ updateLoadingStatus: (state, { payload }) => {
+ state.loadingStatus = payload.status;
+ },
+ updateSavingStatus: (state, { payload }) => {
+ state.savingStatus = payload.status;
+ },
+ updateAddingStatus: (state, { payload }) => {
+ state.addingStatus = payload.status;
+ },
+ updateDeletingStatus: (state, { payload }) => {
+ state.deletingStatus = payload.status;
+ },
+ deleteCustomPageSuccess: (state, { payload }) => {
+ state.customPagesIds = state.customPagesIds.filter(id => id !== payload.customPageId);
+ },
+ addCustomPageSuccess: (state, { payload }) => {
+ state.customPagesIds = [...state.customPagesIds, payload.customPageId];
+ },
+ },
+});
+
+export const {
+ setPageIds,
+ updateLoadingStatus,
+ updateSavingStatus,
+ updateCustomPagesApiStatus,
+ deleteCustomPageSuccess,
+ updateDeletingStatus,
+ addCustomPageSuccess,
+ updateAddingStatus,
+} = slice.actions;
+
+export const {
+ reducer,
+} = slice;
diff --git a/src/custom-pages/data/thunks.js b/src/custom-pages/data/thunks.js
new file mode 100644
index 000000000..620c68484
--- /dev/null
+++ b/src/custom-pages/data/thunks.js
@@ -0,0 +1,160 @@
+import { RequestStatus } from '../../data/constants';
+import {
+ addModel,
+ addModels,
+ removeModel,
+ updateModel,
+ updateModels,
+ } from '../../generic/model-store';
+import {
+ getCustomPages,
+ deleteCustomPage,
+ addCustomPage,
+ updateCustomPage,
+ updateCustomPageOrder,
+} from './api';
+import {
+ setPageIds,
+ updateCustomPagesApiStatus,
+ updateLoadingStatus,
+ updateSavingStatus,
+ updateAddingStatus,
+ updateDeletingStatus,
+ deleteCustomPageSuccess,
+ addCustomPageSuccess,
+} from './slice';
+
+/* eslint-disable import/prefer-default-export */
+export function fetchCustomPages(courseId) {
+ return async (dispatch) => {
+ dispatch(updateLoadingStatus({ courseId, status: RequestStatus.IN_PROGRESS }));
+
+ try {
+ const customPages = await getCustomPages(courseId);
+
+ dispatch(addModels({ modelType: 'customPages', models: customPages }));
+ dispatch(setPageIds({
+ customPagesIds: customPages.map(page => page.id),
+ }));
+ dispatch(updateLoadingStatus({ courseId, status: RequestStatus.SUCCESSFUL }));
+ } catch (error) {
+ if (error.response && error.response.status === 403) {
+ dispatch(updateCustomPagesApiStatus({ status: RequestStatus.DENIED }));
+ }
+ dispatch(updateLoadingStatus({ courseId, status: RequestStatus.FAILED }));
+ }
+ };
+}
+
+export function deleteSingleCustomPage({ blockId, closeConfirmation }) {
+ return async (dispatch) => {
+ dispatch(updateDeletingStatus({ status: RequestStatus.PENDING }));
+
+ try {
+ await deleteCustomPage(blockId);
+ dispatch(removeModel({ modelType: 'customPages', model: blockId }));
+ dispatch(deleteCustomPageSuccess({
+ customPageId: blockId,
+ }));
+ dispatch(updateDeletingStatus({ status: RequestStatus.SUCCESSFUL }));
+ } catch (error) {
+ if (error.response && error.response.status === 403) {
+ dispatch(updateDeletingStatus({ status: RequestStatus.DENIED }));
+ }
+ dispatch(updateDeletingStatus({ status: RequestStatus.FAILED }));
+ }
+ closeConfirmation();
+ };
+}
+
+export function addSingleCustomPage(courseId) {
+ return async (dispatch) => {
+ dispatch(updateAddingStatus({ status: RequestStatus.PENDING }));
+
+ try {
+ const pageData = await addCustomPage(courseId);
+ dispatch(addModel({
+ modelType: 'customPages',
+ model: {
+ id: pageData.locator,
+ courseStaffOnly: false,
+ ...pageData,
+ },
+ }));
+ dispatch(addCustomPageSuccess({
+ customPageId: pageData.locator,
+ }));
+ dispatch(updateAddingStatus({ courseId, status: RequestStatus.SUCCESSFUL }));
+ } catch (error) {
+ if (error.response && error.response.status === 403) {
+ dispatch(updateAddingStatus({ status: RequestStatus.DENIED }));
+ }
+ dispatch(updateAddingStatus({ status: RequestStatus.FAILED }));
+ }
+ };
+}
+
+export function updatePageOrder(courseId, pages) {
+ const tabs = [];
+ pages.forEach(page => {
+ const currentTab = {};
+ currentTab.tab_locator = page.id;
+ tabs.push(currentTab);
+ });
+ return async (dispatch) => {
+ try {
+ await updateCustomPageOrder(courseId, tabs);
+ dispatch(updateModels({ modelType: 'customPages', models: pages }));
+ dispatch(setPageIds({
+ customPagesIds: pages.map(page => page.id),
+ }));
+ dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
+ } catch (error) {
+ if (error.response && error.response.status === 403) {
+ dispatch(updateCustomPagesApiStatus({ status: RequestStatus.DENIED }));
+ }
+ dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
+ }
+ };
+}
+
+export function updateCustomPageVisibility({ blockId, metadata }) {
+ return async (dispatch) => {
+ dispatch(updateSavingStatus({ status: RequestStatus.IN_PROGRESS }));
+
+ try {
+ const pageData = await updateCustomPage({ blockId, metadata });
+ dispatch(updateModel({
+ modelType: 'customPages',
+ model: {
+ id: blockId,
+ courseStaffOnly: pageData.metadata.courseStaffOnly,
+ },
+ }));
+ dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
+ } catch (error) {
+ dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
+ }
+ };
+}
+
+export const updateSingleCustomPage = ({
+ blockId,
+ metadata,
+ setCurrentPage,
+}) => (dispatch) => {
+ dispatch(updateSavingStatus({ status: RequestStatus.IN_PROGRESS }));
+ try {
+ dispatch(updateModel({
+ modelType: 'customPages',
+ model: {
+ id: blockId,
+ name: metadata.displayName,
+ },
+ }));
+ setCurrentPage(null);
+ dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
+ } catch (error) {
+ dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
+ }
+};
diff --git a/src/custom-pages/factories/mockApiResponses.jsx b/src/custom-pages/factories/mockApiResponses.jsx
new file mode 100644
index 000000000..6671d13e8
--- /dev/null
+++ b/src/custom-pages/factories/mockApiResponses.jsx
@@ -0,0 +1,64 @@
+export const courseId = 'course-v1:edX+DemoX+Demo_Course';
+
+export const initialState = {
+ courseDetail: {
+ courseId,
+ status: 'sucessful',
+ },
+ customPages: {
+ customPagesIds: [
+ 'mOckID1',
+ ],
+ loadingStatus: 'successful',
+ savingStatus: '',
+ deletingStatus: '',
+ addingStatus: '',
+ customPagesApiStatus: {},
+ },
+ models: {
+ customPages: {
+ mOckID1: {
+ id: 'mOckID1',
+ name: 'test',
+ courseStaffOnly: false,
+ tabId: 'static_tab_1',
+ },
+ },
+ },
+};
+
+export const generateFetchPageApiResponse = () => ([{
+ type: 'static_tab',
+ title: null,
+ is_hideable: false,
+ is_hidden: false,
+ is_movable: true,
+ course_staff_only: false,
+ name: 'test',
+ tab_id: 'static_tab_1',
+ settings: {
+ url_slug: '1',
+ },
+ id: 'mOckID1',
+}]);
+
+export const generateXblockData = (
+ blockId,
+) => ({
+ id: blockId,
+ display_name: 'test',
+ data: 'test
',
+});
+
+export const generateUpdateVisiblityApiResponse = (
+ blockId,
+ visibility,
+) => ({
+ id: blockId,
+ metadata: { display_name: 'test', course_staff_only: visibility },
+});
+
+export const generateNewPageApiResponse = () => ({
+ locator: 'mOckID2',
+ courseKey: courseId,
+});
diff --git a/src/custom-pages/index.js b/src/custom-pages/index.js
new file mode 100644
index 000000000..0e4450041
--- /dev/null
+++ b/src/custom-pages/index.js
@@ -0,0 +1 @@
+export { default } from './CustomPages';
diff --git a/src/custom-pages/messages.js b/src/custom-pages/messages.js
new file mode 100644
index 000000000..af6b7ca6d
--- /dev/null
+++ b/src/custom-pages/messages.js
@@ -0,0 +1,110 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ heading: {
+ id: 'course-authoring.custom-pages.heading',
+ defaultMessage: 'Custom Pages',
+ },
+ errorAlertMessage: {
+ id: 'course-authoring.custom-pages.errorAlert.message',
+ defaultMessage: 'Unable to {actionName} page. Please try again.',
+ },
+ note: {
+ id: 'course-authoring.custom-pages.note',
+ defaultMessage: `Note: Pages are publicly visible. If users know the URL
+ of a page, they can view the page even if they are not registered for
+ or logged in to your course.`,
+ },
+ addPageHeaderLabel: {
+ id: 'course-authoring.custom-pages.header.addPage.label',
+ defaultMessage: 'New page',
+ },
+ viewLiveLabel: {
+ id: 'course-authoring.custom-pages.header.viewLive.label',
+ defaultMessage: 'View live',
+ },
+ pageExplanationHeader: {
+ id: 'course-authoring.custom-pages.pageExplanation.header',
+ defaultMessage: 'What are pages?',
+ },
+ pageExplanationBody: {
+ id: 'course-authoring.custom-pages.pageExplanation.body',
+ defaultMessage: `Pages are listed horizontally at the top of your course. Default pages (Home, Course, Discussion, Wiki, and Progress)
+ are followed by textbooks and custom pages that you create.`,
+ },
+ customPagesExplanationHeader: {
+ id: 'course-authoring.custom-pages.customPagesExplanation.header',
+ defaultMessage: 'Custom pages',
+ },
+ customPagesExplanationBody: {
+ id: 'course-authoring.custom-pages.customPagesExplanation.body',
+ defaultMessage: `You can create and edit custom pages to probide students with additional course content. For example, you can create
+ pages for the grading policy, course slide, and a course calendar.`,
+ },
+ studentViewExplanationHeader: {
+ id: 'course-authoring.custom-pages.studentViewExplanation.header',
+ defaultMessage: 'How do pages look to students in my course?',
+ },
+ studentViewExplanationBody: {
+ id: 'course-authoring.custom-pages.studentViewExplanation.body',
+ defaultMessage: 'Students see the default and custom pages at the top of your course and use the links to navigate.',
+ },
+ studentViewExampleButton: {
+ id: 'course-authoring.custom-pages.studentViewExampleButton.label',
+ defaultMessage: 'See an example',
+ },
+ studentViewModalTitle: {
+ id: 'course-authoring.custom-pages.studentViewModal.title',
+ defaultMessage: 'Pages in Your Course',
+ },
+ studentViewModalBody: {
+ id: 'course-authoring.custom-pages.studentViewModal.Body',
+ defaultMessage: "Pages appear in your course's top navigation bar. The default pages (Home, Course, Discussion, Wiki, and Progress) are followed by textbooks and custom pages.",
+ },
+ newPageTitle: {
+ id: 'course-authoring.custom-pages.page.newPage.title',
+ defaultMessage: 'Empty',
+ },
+ editTooltipContent: {
+ id: 'course-authoring.custom-pages.editTooltip.content',
+ defaultMessage: 'Edit',
+ },
+ deleteTooltipContent: {
+ id: 'course-authoring.custom-pages.deleteTooltip.content',
+ defaultMessage: 'Delete',
+ },
+ visibilityTooltipContent: {
+ id: 'course-authoring.custom-pages.visibilityTooltip.content',
+ defaultMessage: 'Hide/show page from learners',
+ },
+ addPageBodyLabel: {
+ id: 'course-authoring.custom-pages.body.addPage.label',
+ defaultMessage: 'Add a new page',
+ },
+ addingPageBodyLabel: {
+ id: 'course-authoring.custom-pages.body.addingPage.label',
+ defaultMessage: 'Adding a new page',
+ },
+ deleteConfirmationTitle: {
+ id: 'course-authoring.custom-pages..deleteConfirmation.title',
+ defaultMessage: 'Delete Page Confirmation',
+ },
+ deleteConfirmationMessage: {
+ id: 'course-authoring.custom-pages..deleteConfirmation.message',
+ defaultMessage: 'Are you sure you want to delete this page? This action cannot be undone.',
+ },
+ deletePageLabel: {
+ id: 'course-authoring.custom-pages.deleteConfirmation.deletePage.label',
+ defaultMessage: 'Ok',
+ },
+ deletingPageBodyLabel: {
+ id: 'course-authoring.custom-pages.deleteConfirmation.deletingPage.label',
+ defaultMessage: 'Deleting',
+ },
+ cancelButtonLabel: {
+ id: 'course-authoring.custom-pages.deleteConfirmation.cancelButton.label',
+ defaultMessage: 'Cancel',
+ },
+});
+
+export default messages;
diff --git a/src/data/constants.js b/src/data/constants.js
index eafcef4f9..42e156253 100644
--- a/src/data/constants.js
+++ b/src/data/constants.js
@@ -10,6 +10,7 @@ export const RequestStatus = {
SUCCESSFUL: 'successful',
FAILED: 'failed',
DENIED: 'denied',
+ PENDING: 'pending',
};
/**
diff --git a/src/data/slice.js b/src/data/slice.js
index 574ff318a..749e53f2b 100644
--- a/src/data/slice.js
+++ b/src/data/slice.js
@@ -1,9 +1,7 @@
/* eslint-disable no-param-reassign */
import { createSlice } from '@reduxjs/toolkit';
-export const LOADING = 'LOADING';
export const LOADED = 'LOADED';
-export const FAILED = 'FAILED';
const slice = createSlice({
name: 'courseDetail',
diff --git a/src/data/thunks.js b/src/data/thunks.js
index 3abe30df0..9a52d4d89 100644
--- a/src/data/thunks.js
+++ b/src/data/thunks.js
@@ -4,26 +4,24 @@ import { getCourseDetail } from './api';
import {
updateStatus,
updateCanChangeProviders,
- LOADING,
- LOADED,
- FAILED,
} from './slice';
+import { RequestStatus } from './constants';
/* eslint-disable import/prefer-default-export */
export function fetchCourseDetail(courseId) {
return async (dispatch) => {
- dispatch(updateStatus({ courseId, status: LOADING }));
+ dispatch(updateStatus({ courseId, status: RequestStatus.IN_PROGRESS }));
try {
const courseDetail = await getCourseDetail(courseId, getAuthenticatedUser().username);
- dispatch(updateStatus({ courseId, status: LOADED }));
+ dispatch(updateStatus({ courseId, status: RequestStatus.SUCCESSFUL }));
dispatch(addModel({ modelType: 'courseDetails', model: courseDetail }));
dispatch(updateCanChangeProviders({
canChangeProviders: getAuthenticatedUser().administrator || new Date(courseDetail.start) > new Date(),
}));
} catch (error) {
- dispatch(updateStatus({ courseId, status: FAILED }));
+ dispatch(updateStatus({ courseId, status: RequestStatus.FAILED }));
}
};
}
diff --git a/src/pages-and-resources/pages/PageCard.jsx b/src/pages-and-resources/pages/PageCard.jsx
index e8132d2f3..8d9f85a44 100644
--- a/src/pages-and-resources/pages/PageCard.jsx
+++ b/src/pages-and-resources/pages/PageCard.jsx
@@ -32,10 +32,21 @@ const PageCard = ({
}) => {
const { path: pagesAndResourcesPath } = useContext(PagesAndResourcesContext);
const isDesktop = useIsDesktop();
-
// eslint-disable-next-line react/no-unstable-nested-components
const SettingsButton = () => {
if (page.legacyLink) {
+ if (process.env.ENABLE_NEW_CUSTOM_PAGES && page.name === 'Custom pages') {
+ return (
+
+
+
+ );
+ }
return (
{
- it('will pass because it is an example', () => {
+import {
+ render,
+ queryAllByRole,
+} from '@testing-library/react';
+import { initializeMockApp } from '@edx/frontend-platform';
+import { AppProvider } from '@edx/frontend-platform/react';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
+
+import initializeStore from '../../store';
+import PageGrid from './PageGrid';
+
+import PagesAndResourcesProvider from '../PagesAndResourcesProvider';
+
+let container;
+let store;
+
+const renderComponent = () => {
+ const wrapper = render(
+
+
+
+
+
+
+ ,
+ );
+ container = wrapper.container;
+};
+
+describe('LiveSettings', () => {
+ beforeEach(async () => {
+ initializeMockApp({
+ authenticatedUser: {
+ userId: 3,
+ username: 'abc123',
+ administrator: false,
+ roles: [],
+ },
+ });
+ store = initializeStore({
+ courseDetail: {
+ courseId: 'id',
+ status: 'sucessful',
+ },
+ });
+ });
+
+ it('should render three cards', async () => {
+ renderComponent();
+ expect(queryAllByRole(container, 'button')).toHaveLength(3);
+ });
+ it('should navigate to custom-pages', async () => {
+ renderComponent();
+ const [customPagesSettingsButton] = queryAllByRole(container, 'link');
+ expect(customPagesSettingsButton).toHaveAttribute('href', 'custom-pages');
+ });
+ it('should navigate to legacyLink', async () => {
+ renderComponent();
+ const textbookSettingsButton = queryAllByRole(container, 'link')[1];
+ expect(textbookSettingsButton).toHaveAttribute('href', 'SomeUrl');
});
});
diff --git a/src/store.js b/src/store.js
index a4a53b9dd..6871e4202 100644
--- a/src/store.js
+++ b/src/store.js
@@ -4,12 +4,14 @@ import { reducer as modelsReducer } from './generic/model-store';
import { reducer as courseDetailReducer } from './data/slice';
import { reducer as discussionsReducer } from './pages-and-resources/discussions';
import { reducer as pagesAndResourcesReducer } from './pages-and-resources/data/slice';
+import { reducer as customPagesReducer } from './custom-pages/data/slice';
import { reducer as liveReducer } from './pages-and-resources/live/data/slice';
export default function initializeStore(preloadedState = undefined) {
return configureStore({
reducer: {
courseDetail: courseDetailReducer,
+ customPages: customPagesReducer,
discussions: discussionsReducer,
pagesAndResources: pagesAndResourcesReducer,
models: modelsReducer,