diff --git a/src/discussions/discussions-home/DiscussionsHome.jsx b/src/discussions/discussions-home/DiscussionsHome.jsx
index 61a21a5c..f454ce86 100644
--- a/src/discussions/discussions-home/DiscussionsHome.jsx
+++ b/src/discussions/discussions-home/DiscussionsHome.jsx
@@ -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}
/>
)}
+
+
{displayContentArea &&
}
diff --git a/src/discussions/tours/NotRespondedFilter.jsx b/src/discussions/tours/NotRespondedFilter.jsx
new file mode 100644
index 00000000..1c03a25a
--- /dev/null
+++ b/src/discussions/tours/NotRespondedFilter.jsx
@@ -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 (
+ <>
+
+ >
+ );
+};
diff --git a/src/discussions/tours/data/api.js b/src/discussions/tours/data/api.js
new file mode 100644
index 00000000..e01bafe5
--- /dev/null
+++ b/src/discussions/tours/data/api.js
@@ -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;
+}
diff --git a/src/discussions/tours/data/redux.test.js b/src/discussions/tours/data/redux.test.js
new file mode 100644
index 00000000..d05de496
--- /dev/null
+++ b/src/discussions/tours/data/redux.test.js
@@ -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({});
+ });
+});
diff --git a/src/discussions/tours/data/selectors.js b/src/discussions/tours/data/selectors.js
new file mode 100644
index 00000000..794ee8c0
--- /dev/null
+++ b/src/discussions/tours/data/selectors.js
@@ -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 || {};
+};
diff --git a/src/discussions/tours/data/slices.js b/src/discussions/tours/data/slices.js
new file mode 100644
index 00000000..33119a31
--- /dev/null
+++ b/src/discussions/tours/data/slices.js
@@ -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;
diff --git a/src/discussions/tours/data/thunks.js b/src/discussions/tours/data/thunks.js
new file mode 100644
index 00000000..733e2337
--- /dev/null
+++ b/src/discussions/tours/data/thunks.js
@@ -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);
+ }
+ };
+}
diff --git a/src/discussions/tours/data/tours.factory.js b/src/discussions/tours/data/tours.factory.js
new file mode 100644
index 00000000..b48a840d
--- /dev/null
+++ b/src/discussions/tours/data/tours.factory.js
@@ -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;
diff --git a/src/store.js b/src/store.js
index 3159f288..1ace5ff8 100644
--- a/src/store.js
+++ b/src/store.js
@@ -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,
});