Compare commits
8 Commits
dependabot
...
ahtisham/I
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b3fc507bf | ||
|
|
a1a9e3b21e | ||
|
|
6f96b0d6ef | ||
|
|
087adf6562 | ||
|
|
bb8f4b1c50 | ||
|
|
5c75651481 | ||
|
|
5f563ab702 | ||
|
|
e6d560733b |
@@ -1,6 +1,9 @@
|
|||||||
/* eslint-disable import/prefer-default-export */
|
/* eslint-disable import/prefer-default-export */
|
||||||
import {
|
import {
|
||||||
useContext, useEffect, useRef, useState,
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
@@ -17,6 +20,10 @@ import { DiscussionContext } from '../common/context';
|
|||||||
import { clearRedirect } from '../posts/data';
|
import { clearRedirect } from '../posts/data';
|
||||||
import { threadsLoadingStatus } from '../posts/data/selectors';
|
import { threadsLoadingStatus } from '../posts/data/selectors';
|
||||||
import { selectTopics } from '../topics/data/selectors';
|
import { selectTopics } from '../topics/data/selectors';
|
||||||
|
import tourCheckpoints from '../tours/constants';
|
||||||
|
import { selectTours } from '../tours/data/selectors';
|
||||||
|
import { updateTourShowStatus } from '../tours/data/thunks';
|
||||||
|
import messages from '../tours/messages';
|
||||||
import { discussionsPath, inBlackoutDateRange } from '../utils';
|
import { discussionsPath, inBlackoutDateRange } from '../utils';
|
||||||
import {
|
import {
|
||||||
selectAreThreadsFiltered,
|
selectAreThreadsFiltered,
|
||||||
@@ -40,10 +47,11 @@ export function useTotalTopicThreadCount() {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.keys(topics).reduce((total, topicId) => {
|
return Object.keys(topics)
|
||||||
const topic = topics[topicId];
|
.reduce((total, topicId) => {
|
||||||
return total + topic.threadCounts.discussion + topic.threadCounts.question;
|
const topic = topics[topicId];
|
||||||
}, 0);
|
return total + topic.threadCounts.discussion + topic.threadCounts.question;
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSidebarVisible = () => {
|
export const useSidebarVisible = () => {
|
||||||
@@ -192,3 +200,27 @@ export const useUserCanAddThreadInBlackoutDate = () => {
|
|||||||
return (!(isInBlackoutDateRange)
|
return (!(isInBlackoutDateRange)
|
||||||
|| (isUserAdmin || userHasModerationPrivilages || isUserGroupTA || isCourseAdmin || isCourseStaff));
|
|| (isUserAdmin || userHasModerationPrivilages || isUserGroupTA || isCourseAdmin || isCourseStaff));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function camelToConstant(string) {
|
||||||
|
return string.replace(/[A-Z]/g, (match) => `_${match}`).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTourConfiguration = (intl) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const { enableInContextSidebar } = useContext(DiscussionContext);
|
||||||
|
const tours = useSelector(selectTours);
|
||||||
|
|
||||||
|
return tours.map((tour) => (
|
||||||
|
{
|
||||||
|
tourId: tour.tourName,
|
||||||
|
advanceButtonText: intl.formatMessage(messages.advanceButtonText),
|
||||||
|
dismissButtonText: intl.formatMessage(messages.dismissButtonText),
|
||||||
|
endButtonText: intl.formatMessage(messages.endButtonText),
|
||||||
|
enabled: tour && Boolean(tour.showTour && !enableInContextSidebar),
|
||||||
|
onDismiss: () => dispatch(updateTourShowStatus(tour.id)),
|
||||||
|
onEnd: () => dispatch(updateTourShowStatus(tour.id)),
|
||||||
|
checkpoints: tourCheckpoints(intl)[camelToConstant(tour.tourName)],
|
||||||
|
}
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { EmptyTopic as InContextEmptyTopics } from '../in-context-topics/compone
|
|||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
import { LegacyBreadcrumbMenu, NavigationBar } from '../navigation';
|
import { LegacyBreadcrumbMenu, NavigationBar } from '../navigation';
|
||||||
import { selectPostEditorVisible } from '../posts/data/selectors';
|
import { selectPostEditorVisible } from '../posts/data/selectors';
|
||||||
|
import DiscussionsProductTour from '../tours/DiscussionsProductTour';
|
||||||
import { postMessageToParent } from '../utils';
|
import { postMessageToParent } from '../utils';
|
||||||
import BlackoutInformationBanner from './BlackoutInformationBanner';
|
import BlackoutInformationBanner from './BlackoutInformationBanner';
|
||||||
import DiscussionContent from './DiscussionContent';
|
import DiscussionContent from './DiscussionContent';
|
||||||
@@ -101,6 +102,7 @@ export default function DiscussionsHome() {
|
|||||||
component={LegacyBreadcrumbMenu}
|
component={LegacyBreadcrumbMenu}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="d-flex flex-row">
|
<div className="d-flex flex-row">
|
||||||
<DiscussionSidebar displaySidebar={displaySidebar} postActionBarRef={postActionBarRef} />
|
<DiscussionSidebar displaySidebar={displaySidebar} postActionBarRef={postActionBarRef} />
|
||||||
{displayContentArea && <DiscussionContent />}
|
{displayContentArea && <DiscussionContent />}
|
||||||
@@ -122,6 +124,7 @@ export default function DiscussionsHome() {
|
|||||||
</Switch>
|
</Switch>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<DiscussionsProductTour />
|
||||||
</main>
|
</main>
|
||||||
{!enableInContextSidebar && <Footer />}
|
{!enableInContextSidebar && <Footer />}
|
||||||
</DiscussionContext.Provider>
|
</DiscussionContext.Provider>
|
||||||
|
|||||||
@@ -148,12 +148,15 @@ function PostFilterBar({
|
|||||||
cohort: capitalize(selectedCohort?.name),
|
cohort: capitalize(selectedCohort?.name),
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
<Collapsible.Visible whenClosed>
|
<span id="icon-tune">
|
||||||
<Icon src={Tune} />
|
<Collapsible.Visible whenClosed>
|
||||||
</Collapsible.Visible>
|
<Icon src={Tune} />
|
||||||
<Collapsible.Visible whenOpen>
|
</Collapsible.Visible>
|
||||||
<Icon src={Tune} />
|
<Collapsible.Visible whenOpen>
|
||||||
</Collapsible.Visible>
|
<Icon src={Tune} />
|
||||||
|
</Collapsible.Visible>
|
||||||
|
</span>
|
||||||
|
|
||||||
</Collapsible.Trigger>
|
</Collapsible.Trigger>
|
||||||
<Collapsible.Body className="collapsible-body px-4 pb-3 pt-0">
|
<Collapsible.Body className="collapsible-body px-4 pb-3 pt-0">
|
||||||
<Form>
|
<Form>
|
||||||
|
|||||||
34
src/discussions/tours/DiscussionsProductTour.jsx
Normal file
34
src/discussions/tours/DiscussionsProductTour.jsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import isEmpty from 'lodash/isEmpty';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
import { ProductTour } from '@edx/paragon';
|
||||||
|
|
||||||
|
import { useTourConfiguration } from '../data/hooks';
|
||||||
|
import { fetchDiscussionTours } from './data/thunks';
|
||||||
|
|
||||||
|
function DiscussionsProductTour({ intl }) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const config = useTourConfiguration(intl);
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchDiscussionTours());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!isEmpty(config) && (
|
||||||
|
<ProductTour
|
||||||
|
tours={config}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DiscussionsProductTour.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(DiscussionsProductTour);
|
||||||
14
src/discussions/tours/constants.js
Normal file
14
src/discussions/tours/constants.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
export default function tourCheckpoints(intl) {
|
||||||
|
return {
|
||||||
|
NOT_RESPONDED_FILTER: [
|
||||||
|
{
|
||||||
|
body: intl.formatMessage(messages.notRespondedFilterTourBody),
|
||||||
|
placement: 'right',
|
||||||
|
target: '#icon-tune',
|
||||||
|
title: intl.formatMessage(messages.notRespondedFilterTourTitle),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
30
src/discussions/tours/data/api.js
Normal file
30
src/discussions/tours/data/api.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
|
|
||||||
|
// create constant for the API URL
|
||||||
|
export const getDiscussionTourUrl = () => `${getConfig().LMS_BASE_URL}/api/user_tours/discussion_tours/`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getDiscussionTours
|
||||||
|
* This function makes an HTTP GET request to the API to retrieve a list of tours for the authenticated user.
|
||||||
|
* @returns {Promise} - A promise that resolves to the API response data.
|
||||||
|
*/
|
||||||
|
export async function getDiscssionTours() {
|
||||||
|
const { data } = await getAuthenticatedHttpClient()
|
||||||
|
.get(getDiscussionTourUrl());
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* updateDiscussionTour
|
||||||
|
* This function makes an HTTP PUT request to the API to update a specific tour for the authenticated user.
|
||||||
|
* @param {number} tourId - The ID of the tour to be updated.
|
||||||
|
* @returns {Promise} - A promise that resolves to the API response data.
|
||||||
|
*/
|
||||||
|
export async function updateDiscussionTour(tourId) {
|
||||||
|
const { data } = await getAuthenticatedHttpClient()
|
||||||
|
.put(`${getDiscussionTourUrl()}${tourId}`, {
|
||||||
|
show_tour: false,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
1
src/discussions/tours/data/index.js
Normal file
1
src/discussions/tours/data/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './slices';
|
||||||
218
src/discussions/tours/data/redux.test.js
Normal file
218
src/discussions/tours/data/redux.test.js
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
|
|
||||||
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
|
import { initializeMockApp } from '@edx/frontend-platform/testing';
|
||||||
|
|
||||||
|
import { RequestStatus } from '../../../data/constants';
|
||||||
|
import { initializeStore } from '../../../store';
|
||||||
|
import { getDiscussionTourUrl } from './api';
|
||||||
|
import { selectTours } from './selectors';
|
||||||
|
import {
|
||||||
|
discussionsTourRequest,
|
||||||
|
discussionsToursRequestError,
|
||||||
|
fetchUserDiscussionsToursSuccess,
|
||||||
|
toursReducer,
|
||||||
|
updateUserDiscussionsTourSuccess,
|
||||||
|
} from './slices';
|
||||||
|
import { fetchDiscussionTours, updateTourShowStatus } from './thunks';
|
||||||
|
import discussionTourFactory from './tours.factory';
|
||||||
|
|
||||||
|
let mockAxios;
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
let store;
|
||||||
|
const url = getDiscussionTourUrl();
|
||||||
|
describe('DiscussionToursThunk', () => {
|
||||||
|
let actualActions;
|
||||||
|
|
||||||
|
const dispatch = (action) => {
|
||||||
|
actualActions.push(action);
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
initializeMockApp({
|
||||||
|
authenticatedUser: {
|
||||||
|
userId: 3,
|
||||||
|
username: 'abc123',
|
||||||
|
administrator: true,
|
||||||
|
roles: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
mockAxios = new MockAdapter(getAuthenticatedHttpClient());
|
||||||
|
store = initializeStore();
|
||||||
|
actualActions = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mockAxios.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dispatches get request, success actions', async () => {
|
||||||
|
const mockData = discussionTourFactory.buildList(2);
|
||||||
|
mockAxios.onGet(url)
|
||||||
|
.reply(200, mockData);
|
||||||
|
|
||||||
|
const expectedActions = [
|
||||||
|
{
|
||||||
|
payload: undefined,
|
||||||
|
type: 'userDiscussionsTours/discussionsTourRequest',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'userDiscussionsTours/fetchUserDiscussionsToursSuccess',
|
||||||
|
payload: mockData,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
await fetchDiscussionTours()(dispatch);
|
||||||
|
expect(actualActions)
|
||||||
|
.toEqual(expectedActions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dispatches request, and error actions', async () => {
|
||||||
|
mockAxios.onGet('/api/discussion-tours/')
|
||||||
|
.reply(500);
|
||||||
|
const errorAction = [{
|
||||||
|
payload: undefined,
|
||||||
|
type: 'userDiscussionsTours/discussionsTourRequest',
|
||||||
|
}, {
|
||||||
|
payload: undefined,
|
||||||
|
type: 'userDiscussionsTours/discussionsToursRequestError',
|
||||||
|
}];
|
||||||
|
|
||||||
|
await fetchDiscussionTours()(dispatch);
|
||||||
|
expect(actualActions)
|
||||||
|
.toEqual(errorAction);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dispatches put request, success actions', async () => {
|
||||||
|
const mockData = discussionTourFactory.build();
|
||||||
|
mockAxios.onPut(`${url}${1}`)
|
||||||
|
.reply(200, mockData);
|
||||||
|
|
||||||
|
const expectedActions = [
|
||||||
|
{
|
||||||
|
payload: undefined,
|
||||||
|
type: 'userDiscussionsTours/discussionsTourRequest',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'userDiscussionsTours/updateUserDiscussionsTourSuccess',
|
||||||
|
payload: mockData,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
await updateTourShowStatus(1)(dispatch);
|
||||||
|
expect(actualActions)
|
||||||
|
.toEqual(expectedActions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dispatches update request, and error actions', async () => {
|
||||||
|
mockAxios.onPut(`${url}${1}`)
|
||||||
|
.reply(500);
|
||||||
|
const errorAction = [{
|
||||||
|
payload: undefined,
|
||||||
|
type: 'userDiscussionsTours/discussionsTourRequest',
|
||||||
|
}, {
|
||||||
|
payload: undefined,
|
||||||
|
type: 'userDiscussionsTours/discussionsToursRequestError',
|
||||||
|
}];
|
||||||
|
|
||||||
|
await updateTourShowStatus(1)(dispatch);
|
||||||
|
expect(actualActions)
|
||||||
|
.toEqual(errorAction);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toursReducer', () => {
|
||||||
|
it('handles the discussionsToursRequest action', () => {
|
||||||
|
const initialState = {
|
||||||
|
tours: [],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
const state = toursReducer(initialState, discussionsTourRequest());
|
||||||
|
expect(state)
|
||||||
|
.toEqual({
|
||||||
|
tours: [],
|
||||||
|
loading: RequestStatus.IN_PROGRESS,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles the fetchUserDiscussionsToursSuccess action', () => {
|
||||||
|
const initialState = {
|
||||||
|
tours: [],
|
||||||
|
loading: true,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
const mockData = [{ id: 1 }, { id: 2 }];
|
||||||
|
const state = toursReducer(initialState, fetchUserDiscussionsToursSuccess(mockData));
|
||||||
|
expect(state)
|
||||||
|
.toEqual({
|
||||||
|
tours: mockData,
|
||||||
|
loading: RequestStatus.SUCCESSFUL,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles the updateUserDiscussionsTourSuccess action', () => {
|
||||||
|
const initialState = {
|
||||||
|
tours: {
|
||||||
|
tours: [{ id: 1 }, { id: 2 }],
|
||||||
|
loading: true,
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const updatedTour = {
|
||||||
|
id: 2,
|
||||||
|
name: 'Updated Tour',
|
||||||
|
};
|
||||||
|
const state = toursReducer(initialState, updateUserDiscussionsTourSuccess(updatedTour));
|
||||||
|
expect(state.tours)
|
||||||
|
.toEqual({
|
||||||
|
tours: [{ id: 1 }, updatedTour],
|
||||||
|
loading: RequestStatus.SUCCESSFUL,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles the discussionsToursRequestError action', () => {
|
||||||
|
const initialState = {
|
||||||
|
tours: [],
|
||||||
|
loading: true,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
const mockError = new Error('Something went wrong');
|
||||||
|
const state = toursReducer(initialState, discussionsToursRequestError(mockError));
|
||||||
|
expect(state)
|
||||||
|
.toEqual({
|
||||||
|
tours: [],
|
||||||
|
loading: RequestStatus.FAILED,
|
||||||
|
error: mockError,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('tourSelector', () => {
|
||||||
|
it('returns the tours list from state', () => {
|
||||||
|
const state = {
|
||||||
|
tours: {
|
||||||
|
tours: [
|
||||||
|
{ id: 1, tourName: 'not_responded_filter' },
|
||||||
|
{ id: 2, tourName: 'other_filter' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const expectedResult = [
|
||||||
|
{ id: 1, tourName: 'not_responded_filter' },
|
||||||
|
{ id: 2, tourName: 'other_filter' },
|
||||||
|
];
|
||||||
|
expect(selectTours(state)).toEqual(expectedResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an empty list if the tours state is not defined', () => {
|
||||||
|
const state = {
|
||||||
|
tours: {
|
||||||
|
tours: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(selectTours(state))
|
||||||
|
.toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
2
src/discussions/tours/data/selectors.js
Normal file
2
src/discussions/tours/data/selectors.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
|
export const selectTours = (state) => state.tours.tours;
|
||||||
44
src/discussions/tours/data/slices.js
Normal file
44
src/discussions/tours/data/slices.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/* eslint-disable no-param-reassign,import/prefer-default-export */
|
||||||
|
|
||||||
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import { RequestStatus } from '../../../data/constants';
|
||||||
|
|
||||||
|
const userDiscussionsToursSlice = createSlice({
|
||||||
|
name: 'userDiscussionsTours',
|
||||||
|
initialState: {
|
||||||
|
tours: [],
|
||||||
|
loading: RequestStatus.SUCCESSFUL,
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
reducers: {
|
||||||
|
discussionsTourRequest: (state) => {
|
||||||
|
state.loading = RequestStatus.IN_PROGRESS;
|
||||||
|
state.error = null;
|
||||||
|
},
|
||||||
|
fetchUserDiscussionsToursSuccess: (state, action) => {
|
||||||
|
state.tours = action.payload;
|
||||||
|
state.loading = RequestStatus.SUCCESSFUL;
|
||||||
|
state.error = null;
|
||||||
|
},
|
||||||
|
discussionsToursRequestError: (state, action) => {
|
||||||
|
state.loading = RequestStatus.FAILED;
|
||||||
|
state.error = action.payload;
|
||||||
|
},
|
||||||
|
updateUserDiscussionsTourSuccess: (state, action) => {
|
||||||
|
const tourIndex = state.tours.tours.findIndex(tour => tour.id === action.payload.id);
|
||||||
|
state.tours.tours[tourIndex] = action.payload;
|
||||||
|
state.tours.loading = RequestStatus.SUCCESSFUL;
|
||||||
|
state.tours.error = null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {
|
||||||
|
discussionsTourRequest,
|
||||||
|
fetchUserDiscussionsToursSuccess,
|
||||||
|
discussionsToursRequestError,
|
||||||
|
updateUserDiscussionsTourSuccess,
|
||||||
|
} = userDiscussionsToursSlice.actions;
|
||||||
|
|
||||||
|
export const toursReducer = userDiscussionsToursSlice.reducer;
|
||||||
46
src/discussions/tours/data/thunks.js
Normal file
46
src/discussions/tours/data/thunks.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { camelCaseObject } from '@edx/frontend-platform';
|
||||||
|
import { logError } from '@edx/frontend-platform/logging';
|
||||||
|
|
||||||
|
import { getDiscssionTours, updateDiscussionTour } from './api';
|
||||||
|
import {
|
||||||
|
discussionsTourRequest,
|
||||||
|
discussionsToursRequestError,
|
||||||
|
fetchUserDiscussionsToursSuccess,
|
||||||
|
updateUserDiscussionsTourSuccess,
|
||||||
|
} from './slices';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action thunk to fetch the list of discussion tours for the current user.
|
||||||
|
* @returns {function} - Thunk that dispatches the request, success, and error actions.
|
||||||
|
*/
|
||||||
|
export function fetchDiscussionTours() {
|
||||||
|
return async (dispatch) => {
|
||||||
|
try {
|
||||||
|
dispatch(discussionsTourRequest());
|
||||||
|
const data = await getDiscssionTours();
|
||||||
|
dispatch(fetchUserDiscussionsToursSuccess(camelCaseObject(data)));
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(discussionsToursRequestError());
|
||||||
|
logError(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action thunk to update the show_tour field for a specific discussion tour for the current user.
|
||||||
|
* @param {number} tourId - The ID of the tour to update.
|
||||||
|
* @returns {function} - Thunk that dispatches the request, success, and error actions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function updateTourShowStatus(tourId) {
|
||||||
|
return async (dispatch) => {
|
||||||
|
try {
|
||||||
|
dispatch(discussionsTourRequest());
|
||||||
|
const data = await updateDiscussionTour(tourId);
|
||||||
|
dispatch(updateUserDiscussionsTourSuccess(camelCaseObject(data)));
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(discussionsToursRequestError());
|
||||||
|
logError(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
8
src/discussions/tours/data/tours.factory.js
Normal file
8
src/discussions/tours/data/tours.factory.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { Factory } from 'rosie';
|
||||||
|
|
||||||
|
const discussionTourFactory = new Factory()
|
||||||
|
.sequence('id')
|
||||||
|
.attr('name', ['id'], (id) => `Discussion Tour ${id}`)
|
||||||
|
.attr('description', ['id'], (id) => `This is the description for Discussion Tour ${id}.`);
|
||||||
|
|
||||||
|
export default discussionTourFactory;
|
||||||
31
src/discussions/tours/messages.js
Normal file
31
src/discussions/tours/messages.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
advanceButtonText: {
|
||||||
|
id: 'tour.action.advance',
|
||||||
|
defaultMessage: 'Next',
|
||||||
|
description: 'Action to go to next step of tour',
|
||||||
|
},
|
||||||
|
dismissButtonText: {
|
||||||
|
id: 'tour.action.dismiss',
|
||||||
|
defaultMessage: 'Dismiss',
|
||||||
|
description: 'Action to dismiss current tour',
|
||||||
|
},
|
||||||
|
endButtonText: {
|
||||||
|
id: 'tour.action.end',
|
||||||
|
defaultMessage: 'Okay',
|
||||||
|
description: 'Action to end current tour',
|
||||||
|
},
|
||||||
|
notRespondedFilterTourBody: {
|
||||||
|
id: 'tour.body.notRespondedFilter',
|
||||||
|
defaultMessage: 'Now you can filter discussions to find posts with no response.',
|
||||||
|
description: 'Body of the tour for the not responded filter',
|
||||||
|
},
|
||||||
|
notRespondedFilterTourTitle: {
|
||||||
|
id: 'tour.title.notRespondedFilter',
|
||||||
|
defaultMessage: 'New filtering option!',
|
||||||
|
description: 'Title of the tour for the not responded filter',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default messages;
|
||||||
@@ -283,3 +283,6 @@ header {
|
|||||||
padding: 1px 5px !important;
|
padding: 1px 5px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pgn__checkpoint {
|
||||||
|
max-width: 340px !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { inContextTopicsReducer } from './discussions/in-context-topics/data';
|
|||||||
import { learnersReducer } from './discussions/learners/data';
|
import { learnersReducer } from './discussions/learners/data';
|
||||||
import { threadsReducer } from './discussions/posts/data';
|
import { threadsReducer } from './discussions/posts/data';
|
||||||
import { topicsReducer } from './discussions/topics/data';
|
import { topicsReducer } from './discussions/topics/data';
|
||||||
|
import { toursReducer } from './discussions/tours/data';
|
||||||
|
|
||||||
export function initializeStore(preloadedState = undefined) {
|
export function initializeStore(preloadedState = undefined) {
|
||||||
return configureStore({
|
return configureStore({
|
||||||
@@ -22,6 +23,7 @@ export function initializeStore(preloadedState = undefined) {
|
|||||||
blocks: blocksReducer,
|
blocks: blocksReducer,
|
||||||
learners: learnersReducer,
|
learners: learnersReducer,
|
||||||
courseTabs: courseTabsReducer,
|
courseTabs: courseTabsReducer,
|
||||||
|
tours: toursReducer,
|
||||||
},
|
},
|
||||||
preloadedState,
|
preloadedState,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user