feat: added tour for not responded filter
This commit is contained in:
@@ -29,6 +29,7 @@ import BlackoutInformationBanner from './BlackoutInformationBanner';
|
||||
import DiscussionContent from './DiscussionContent';
|
||||
import DiscussionSidebar from './DiscussionSidebar';
|
||||
import InformationBanner from './InformationBanner';
|
||||
import NotRespondedFilter from '../tours/NotRespondedFilter';
|
||||
|
||||
export default function DiscussionsHome() {
|
||||
const location = useLocation();
|
||||
@@ -101,6 +102,8 @@ export default function DiscussionsHome() {
|
||||
component={LegacyBreadcrumbMenu}
|
||||
/>
|
||||
)}
|
||||
<NotRespondedFilter />
|
||||
|
||||
<div className="d-flex flex-row">
|
||||
<DiscussionSidebar displaySidebar={displaySidebar} postActionBarRef={postActionBarRef} />
|
||||
{displayContentArea && <DiscussionContent />}
|
||||
|
||||
42
src/discussions/tours/NotRespondedFilter.jsx
Normal file
42
src/discussions/tours/NotRespondedFilter.jsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { ProductTour } from '@edx/paragon';
|
||||
|
||||
import { fetchDiscussionTours, updateTourShowStatus } from './data/thunks';
|
||||
import { notRespondedFilterTour } from './data/selectors';
|
||||
|
||||
export default () => {
|
||||
const dispatch = useDispatch();
|
||||
useEffect(() => {
|
||||
dispatch(fetchDiscussionTours());
|
||||
}, []);
|
||||
const tourData = useSelector(notRespondedFilterTour);
|
||||
const config = {
|
||||
tourId: 'notRespondedTour',
|
||||
advanceButtonText: 'Next',
|
||||
dismissButtonText: 'Dismiss',
|
||||
endButtonText: 'Okay',
|
||||
enabled: tourData ? tourData.showTour : false,
|
||||
onDismiss: () => dispatch(updateTourShowStatus(tourData.id)),
|
||||
onEnd: () => dispatch(updateTourShowStatus(tourData.id)),
|
||||
checkpoints: [
|
||||
{
|
||||
body: 'Now you can filter discussions .',
|
||||
placement: 'right',
|
||||
target: '#icon-tune',
|
||||
title: 'New filtering option!',
|
||||
},
|
||||
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProductTour
|
||||
tours={[config]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
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;
|
||||
}
|
||||
252
src/discussions/tours/data/redux.test.js
Normal file
252
src/discussions/tours/data/redux.test.js
Normal file
@@ -0,0 +1,252 @@
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { initializeMockApp } from '@edx/frontend-platform/testing';
|
||||
|
||||
import { initializeStore } from '../../../store';
|
||||
import { getDiscussionTourUrl } from './api';
|
||||
import {
|
||||
fetchUserDiscussionsToursError,
|
||||
fetchUserDiscussionsToursRequest,
|
||||
fetchUserDiscussionsToursSuccess,
|
||||
toursReducer, updateUserDiscussionsTourError, updateUserDiscussionsTourRequest, updateUserDiscussionsTourSuccess
|
||||
} from './slices';
|
||||
import { fetchDiscussionTours, updateTourShowStatus } from './thunks';
|
||||
import discussionTourFactory from './tours.factory';
|
||||
|
||||
let mockAxios;
|
||||
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/fetchUserDiscussionsToursRequest',
|
||||
},
|
||||
{
|
||||
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/fetchUserDiscussionsToursRequest',
|
||||
}, {
|
||||
payload: undefined,
|
||||
type: 'userDiscussionsTours/fetchUserDiscussionsToursError',
|
||||
}];
|
||||
|
||||
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/updateUserDiscussionsTourRequest',
|
||||
},
|
||||
{
|
||||
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/updateUserDiscussionsTourRequest',
|
||||
}, {
|
||||
payload: undefined,
|
||||
type: 'userDiscussionsTours/updateUserDiscussionsTourError',
|
||||
}];
|
||||
|
||||
await updateTourShowStatus(1)(dispatch);
|
||||
expect(actualActions)
|
||||
.toEqual(errorAction);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toursReducer', () => {
|
||||
it('handles the fetchUserDiscussionsToursRequest action', () => {
|
||||
const initialState = {
|
||||
tours: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
};
|
||||
const state = toursReducer(initialState, fetchUserDiscussionsToursRequest());
|
||||
expect(state)
|
||||
.toEqual({
|
||||
tours: [],
|
||||
loading: true,
|
||||
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: false,
|
||||
error: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('handles the fetchUserDiscussionsToursError action', () => {
|
||||
const initialState = {
|
||||
tours: [],
|
||||
loading: true,
|
||||
error: null,
|
||||
};
|
||||
const mockError = new Error('Something went wrong');
|
||||
const state = toursReducer(initialState, fetchUserDiscussionsToursError(mockError));
|
||||
expect(state)
|
||||
.toEqual({
|
||||
tours: [],
|
||||
loading: false,
|
||||
error: mockError,
|
||||
});
|
||||
});
|
||||
|
||||
it('handles the updateUserDiscussionsTourRequest action', () => {
|
||||
const initialState = {
|
||||
tours: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
};
|
||||
const state = toursReducer(initialState, updateUserDiscussionsTourRequest());
|
||||
expect(state)
|
||||
.toEqual({
|
||||
tours: [],
|
||||
loading: true,
|
||||
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: false,
|
||||
error: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('handles the updateUserDiscussionsTourError action', () => {
|
||||
const initialState = {
|
||||
tours: [],
|
||||
loading: true,
|
||||
error: null,
|
||||
};
|
||||
const mockError = new Error('Something went wrong');
|
||||
const state = toursReducer(initialState, updateUserDiscussionsTourError(mockError));
|
||||
expect(state)
|
||||
.toEqual({
|
||||
tours: [],
|
||||
loading: false,
|
||||
error: mockError,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
import { notRespondedFilterTour } from './selectors';
|
||||
|
||||
describe('notRespondedFilterTour', () => {
|
||||
it('filters the tours list by the "not_responded_filter" tour name', () => {
|
||||
const state = {
|
||||
tours: {
|
||||
tours: [
|
||||
{ id: 1, tourName: 'not_responded_filter' },
|
||||
{ id: 2, tourName: 'other_filter' },
|
||||
],
|
||||
},
|
||||
};
|
||||
const expectedResult = { id: 1, tourName: 'not_responded_filter' }
|
||||
;
|
||||
expect(notRespondedFilterTour(state)).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('returns an empty object if the tours state is not defined', () => {
|
||||
const state = {};
|
||||
expect(notRespondedFilterTour(state)).toEqual({});
|
||||
});
|
||||
|
||||
it('returns an empty object if the tours state does not contain not_responded_filter', () => {
|
||||
const state = {
|
||||
tours: {
|
||||
tours: [
|
||||
{ id: 1, tourName: 'other_data' },
|
||||
{ id: 2, tourName: 'other_data_1' },
|
||||
],
|
||||
},
|
||||
};
|
||||
expect(notRespondedFilterTour(state)).toEqual({});
|
||||
});
|
||||
});
|
||||
10
src/discussions/tours/data/selectors.js
Normal file
10
src/discussions/tours/data/selectors.js
Normal file
@@ -0,0 +1,10 @@
|
||||
export const notRespondedFilterTour = ({ tours }) => {
|
||||
// This function filters the tours list in the state by the tour_name 'not_responded_filter'
|
||||
// and returns the filtered list. This can be useful for displaying only the 'not_responded_filter'
|
||||
// tours to the user, for example in a list or table.
|
||||
if (!tours) {
|
||||
return {};
|
||||
}
|
||||
const response = tours.tours.find(tour => tour.tourName === 'not_responded_filter');
|
||||
return response || {};
|
||||
};
|
||||
52
src/discussions/tours/data/slices.js
Normal file
52
src/discussions/tours/data/slices.js
Normal file
@@ -0,0 +1,52 @@
|
||||
/* eslint-disable no-param-reassign,import/prefer-default-export */
|
||||
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
const userDiscussionsToursSlice = createSlice({
|
||||
name: 'userDiscussionsTours',
|
||||
initialState: {
|
||||
tours: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
},
|
||||
reducers: {
|
||||
fetchUserDiscussionsToursRequest: (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
},
|
||||
fetchUserDiscussionsToursSuccess: (state, action) => {
|
||||
state.tours = action.payload;
|
||||
state.loading = false;
|
||||
state.error = null;
|
||||
},
|
||||
fetchUserDiscussionsToursError: (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload;
|
||||
},
|
||||
updateUserDiscussionsTourRequest: (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
},
|
||||
updateUserDiscussionsTourSuccess: (state, action) => {
|
||||
const tourIndex = state.tours.tours.findIndex(tour => tour.id === action.payload.id);
|
||||
state.tours.tours[tourIndex] = action.payload;
|
||||
state.tours.loading = false;
|
||||
state.tours.error = null;
|
||||
},
|
||||
updateUserDiscussionsTourError: (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
fetchUserDiscussionsToursRequest,
|
||||
fetchUserDiscussionsToursSuccess,
|
||||
fetchUserDiscussionsToursError,
|
||||
updateUserDiscussionsTourRequest,
|
||||
updateUserDiscussionsTourSuccess,
|
||||
updateUserDiscussionsTourError,
|
||||
} = userDiscussionsToursSlice.actions;
|
||||
|
||||
export const toursReducer = userDiscussionsToursSlice.reducer;
|
||||
48
src/discussions/tours/data/thunks.js
Normal file
48
src/discussions/tours/data/thunks.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import { camelCaseObject } from '@edx/frontend-platform';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
|
||||
import { getDiscssionTours, updateDiscussionTour } from './api';
|
||||
import {
|
||||
fetchUserDiscussionsToursError,
|
||||
fetchUserDiscussionsToursRequest,
|
||||
fetchUserDiscussionsToursSuccess,
|
||||
updateUserDiscussionsTourError,
|
||||
updateUserDiscussionsTourRequest,
|
||||
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(fetchUserDiscussionsToursRequest());
|
||||
const data = await getDiscssionTours();
|
||||
dispatch(fetchUserDiscussionsToursSuccess(camelCaseObject(data)));
|
||||
} catch (error) {
|
||||
dispatch(fetchUserDiscussionsToursError());
|
||||
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(updateUserDiscussionsTourRequest());
|
||||
const data = await updateDiscussionTour(tourId);
|
||||
dispatch(updateUserDiscussionsTourSuccess(camelCaseObject(data)));
|
||||
} catch (error) {
|
||||
dispatch(updateUserDiscussionsTourError());
|
||||
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;
|
||||
@@ -9,6 +9,7 @@ import { inContextTopicsReducer } from './discussions/in-context-topics/data';
|
||||
import { learnersReducer } from './discussions/learners/data';
|
||||
import { threadsReducer } from './discussions/posts/data';
|
||||
import { topicsReducer } from './discussions/topics/data';
|
||||
import { toursReducer } from './discussions/tours/data/slices';
|
||||
|
||||
export function initializeStore(preloadedState = undefined) {
|
||||
return configureStore({
|
||||
@@ -22,6 +23,7 @@ export function initializeStore(preloadedState = undefined) {
|
||||
blocks: blocksReducer,
|
||||
learners: learnersReducer,
|
||||
courseTabs: courseTabsReducer,
|
||||
tours: toursReducer,
|
||||
},
|
||||
preloadedState,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user