Compare commits
1 Commits
sundas/INF
...
saad/INF-7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3226c729b |
2
Makefile
2
Makefile
@@ -5,6 +5,8 @@ transifex_langs = "ar,fr,es_419,zh_CN,tr_TR,pl,fr_CA,fr_FR,de_DE,it_IT"
|
||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||
i18n = ./src/i18n
|
||||
transifex_input = $(i18n)/transifex_input.json
|
||||
tx_url1 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/translation/en/strings/
|
||||
tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/source/
|
||||
|
||||
# This directory must match .babelrc .
|
||||
transifex_temp = ./temp/babel-plugin-react-intl
|
||||
|
||||
@@ -72,7 +72,6 @@ function Search({ intl }) {
|
||||
<Icon
|
||||
src={SearchIcon}
|
||||
onClick={() => onSubmit(searchValue)}
|
||||
data-testid="search-icon"
|
||||
/>
|
||||
</span>
|
||||
</SearchField.Advanced>
|
||||
|
||||
@@ -27,7 +27,7 @@ const threadsApiUrl = getThreadsApiUrl();
|
||||
const discussionPostId = 'thread-1';
|
||||
const questionPostId = 'thread-2';
|
||||
const courseId = 'course-v1:edX+TestX+Test_Course';
|
||||
const reverseOrder = true;
|
||||
const reverseOrder = false;
|
||||
const enableInContextSidebar = false;
|
||||
let store;
|
||||
let axiosMock;
|
||||
|
||||
@@ -8,13 +8,14 @@ ensureConfig([
|
||||
], 'Posts API service');
|
||||
|
||||
export const getCourseConfigApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussion/v1/courses/`;
|
||||
export const getDiscussionsConfigUrl = (courseId) => `${getCourseConfigApiUrl()}${courseId}/`;
|
||||
|
||||
/**
|
||||
* Get discussions course config
|
||||
* @param {string} courseId
|
||||
*/
|
||||
export async function getDiscussionsConfig(courseId) {
|
||||
const { data } = await getAuthenticatedHttpClient().get(getDiscussionsConfigUrl(courseId));
|
||||
const url = `${getCourseConfigApiUrl()}${courseId}/`;
|
||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -23,7 +24,7 @@ export async function getDiscussionsConfig(courseId) {
|
||||
* @param {string} courseId
|
||||
*/
|
||||
export async function getDiscussionsSettings(courseId) {
|
||||
const url = `${getDiscussionsConfigUrl(courseId)}settings`;
|
||||
const url = `${getCourseConfigApiUrl()}${courseId}/settings`;
|
||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ export const useTourConfiguration = (intl) => {
|
||||
advanceButtonText: intl.formatMessage(messages.advanceButtonText),
|
||||
dismissButtonText: intl.formatMessage(messages.dismissButtonText),
|
||||
endButtonText: intl.formatMessage(messages.endButtonText),
|
||||
enabled: tour && Boolean(tour.enabled && tour.showTour && !enableInContextSidebar),
|
||||
enabled: tour && Boolean(tour.showTour && !enableInContextSidebar),
|
||||
onDismiss: () => dispatch(updateTourShowStatus(tour.id)),
|
||||
onEnd: () => dispatch(updateTourShowStatus(tour.id)),
|
||||
checkpoints: tourCheckpoints(intl)[camelToConstant(tour.tourName)],
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
fireEvent, render, screen, waitFor, within,
|
||||
} from '@testing-library/react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
@@ -13,13 +11,11 @@ import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import { PostActionsBar } from '../../components';
|
||||
import { initializeStore } from '../../store';
|
||||
import { executeThunk } from '../../test-utils';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import { getDiscussionsConfigUrl } from '../data/api';
|
||||
import { getCourseConfigApiUrl } from '../data/api';
|
||||
import { fetchCourseConfig } from '../data/thunks';
|
||||
import { getUserProfileApiUrl, learnersApiUrl } from './data/api';
|
||||
import { getCoursesApiUrl, getUserProfileApiUrl } from './data/api';
|
||||
import { fetchLearners } from './data/thunks';
|
||||
import LearnersView from './LearnersView';
|
||||
|
||||
@@ -27,31 +23,27 @@ import './data/__factories__';
|
||||
|
||||
let store;
|
||||
let axiosMock;
|
||||
const coursesApiUrl = getCoursesApiUrl();
|
||||
const courseConfigApiUrl = getCourseConfigApiUrl();
|
||||
const userProfileApiUrl = getUserProfileApiUrl();
|
||||
const courseId = 'course-v1:edX+TestX+Test_Course';
|
||||
let container;
|
||||
|
||||
function renderComponent() {
|
||||
const wrapper = render(
|
||||
return render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<DiscussionContext.Provider value={{
|
||||
page: 'learners',
|
||||
}}
|
||||
>
|
||||
<MemoryRouter initialEntries={[`/${courseId}/`]}>
|
||||
<Route path="/:courseId/">
|
||||
<PostActionsBar />
|
||||
<LearnersView />
|
||||
</Route>
|
||||
</MemoryRouter>
|
||||
</DiscussionContext.Provider>
|
||||
<MemoryRouter initialEntries={[`/${courseId}/`]}>
|
||||
<Route path="/:courseId/">
|
||||
<LearnersView />
|
||||
</Route>
|
||||
</MemoryRouter>
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
container = wrapper.container;
|
||||
}
|
||||
|
||||
describe('LearnersView', () => {
|
||||
const learnerCount = 3;
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
@@ -61,190 +53,41 @@ describe('LearnersView', () => {
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
|
||||
store = initializeStore();
|
||||
Factory.resetAll();
|
||||
});
|
||||
|
||||
async function setUpLearnerMockResponse(
|
||||
count = 3,
|
||||
pageSize = 6,
|
||||
page = 1,
|
||||
username = ['learner-1', 'learner-2', 'learner-3'],
|
||||
searchText,
|
||||
) {
|
||||
Factory.resetAll();
|
||||
const learnersData = Factory.build('learnersResult', {}, {
|
||||
count,
|
||||
pageSize,
|
||||
page,
|
||||
count: learnerCount,
|
||||
pageSize: 6,
|
||||
});
|
||||
axiosMock.onGet(learnersApiUrl(courseId))
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
axiosMock.onGet(`${coursesApiUrl}${courseId}/activity_stats/`)
|
||||
.reply(() => [200, learnersData]);
|
||||
|
||||
axiosMock.onGet(`${getUserProfileApiUrl()}?username=${username.join()}`)
|
||||
.reply(() => [200, Factory.build('learnersProfile', {}, {
|
||||
username,
|
||||
}).profiles]);
|
||||
await executeThunk(fetchLearners(courseId, { usernameSearch: searchText }), store.dispatch, store.getState);
|
||||
}
|
||||
|
||||
async function assignPrivilages() {
|
||||
axiosMock.onGet(getDiscussionsConfigUrl(courseId)).reply(200, {
|
||||
learners_tab_enabled: true,
|
||||
user_is_privileged: true,
|
||||
const learnersProfile = Factory.build('learnersProfile', {}, {
|
||||
username: ['learner-1', 'learner-2', 'learner-3'],
|
||||
});
|
||||
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
}
|
||||
|
||||
test('Learners tab is disabled by default', async () => {
|
||||
await setUpLearnerMockResponse();
|
||||
await renderComponent();
|
||||
|
||||
expect(screen.queryByText('learner-1')).not.toBeInTheDocument();
|
||||
axiosMock.onGet(`${userProfileApiUrl}?username=learner-1,learner-2,learner-3`)
|
||||
.reply(() => [200, learnersProfile.profiles]);
|
||||
await executeThunk(fetchLearners(courseId), store.dispatch, store.getState);
|
||||
});
|
||||
|
||||
test('Learners tab is enabled', async () => {
|
||||
await setUpLearnerMockResponse();
|
||||
await assignPrivilages();
|
||||
await waitFor(() => {
|
||||
renderComponent();
|
||||
});
|
||||
|
||||
expect(screen.queryByText('learner-1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Most activity should be selected by default for the non-moderator role.', async () => {
|
||||
await setUpLearnerMockResponse();
|
||||
await renderComponent();
|
||||
|
||||
const filterBar = container.querySelector('.collapsible-trigger');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(filterBar);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const mostActivity = screen.getByTestId('activity selected');
|
||||
|
||||
expect(mostActivity).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ searchBy: 'sort-recency', result: 0 },
|
||||
{ searchBy: 'sort-activity', result: 3 },
|
||||
])('successfully display learners by %s.', async ({ searchBy, result }) => {
|
||||
await setUpLearnerMockResponse();
|
||||
await assignPrivilages();
|
||||
await renderComponent();
|
||||
|
||||
const filterBar = container.querySelector('.collapsible-trigger');
|
||||
await act(async () => {
|
||||
fireEvent.click(filterBar);
|
||||
});
|
||||
|
||||
await waitFor(async () => {
|
||||
const activity = container.querySelector(`#${searchBy}`);
|
||||
|
||||
describe('Basic', () => {
|
||||
test('Learners tab is disabled by default', async () => {
|
||||
await act(async () => {
|
||||
fireEvent.click(activity);
|
||||
await renderComponent();
|
||||
});
|
||||
await waitFor(() => {
|
||||
const learners = container.querySelectorAll('.discussion-post');
|
||||
|
||||
expect(learners).toHaveLength(result);
|
||||
expect(screen.queryByText(/Last active/i)).toBeFalsy();
|
||||
});
|
||||
test('Learners tab is enabled', async () => {
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`).reply(200, {
|
||||
learners_tab_enabled: true,
|
||||
user_is_privileged: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should display a learner\'s list.', async () => {
|
||||
await setUpLearnerMockResponse();
|
||||
await assignPrivilages();
|
||||
await waitFor(() => {
|
||||
renderComponent();
|
||||
});
|
||||
|
||||
const learners = await container.querySelectorAll('.discussion-post');
|
||||
const firstLearner = learners.item(0);
|
||||
const learnerAvatar = firstLearner.querySelector('[alt=learner-1]');
|
||||
const learnerTitle = within(firstLearner).queryByText('learner-1');
|
||||
const stats = firstLearner.querySelectorAll('.icon-size');
|
||||
|
||||
expect(learners).toHaveLength(3);
|
||||
expect(learnerAvatar).toBeInTheDocument();
|
||||
expect(learnerTitle).toBeInTheDocument();
|
||||
expect(stats).toHaveLength(2);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
searchText: 'hello world',
|
||||
output: 'Showing 0 results for',
|
||||
learnersCount: 0,
|
||||
username: [],
|
||||
},
|
||||
{
|
||||
searchText: 'learner',
|
||||
output: 'Showing 2 results for',
|
||||
learnersCount: 2,
|
||||
username:
|
||||
['learner-1', 'learner-2'],
|
||||
},
|
||||
])('should have a search bar with a clear button and \'$output\' results found text.',
|
||||
async ({
|
||||
searchText, output, learnersCount, username,
|
||||
}) => {
|
||||
await setUpLearnerMockResponse();
|
||||
await assignPrivilages();
|
||||
await renderComponent();
|
||||
|
||||
const searchField = within(container).getByPlaceholderText('Search learners');
|
||||
const searchButton = within(container).getByTestId('search-icon');
|
||||
|
||||
await fireEvent.change(searchField, { target: { value: searchText } });
|
||||
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/settings`).reply(200, {});
|
||||
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
|
||||
await act(async () => {
|
||||
fireEvent.click(searchButton);
|
||||
setUpLearnerMockResponse(learnersCount, learnersCount, 1, username, searchText);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const clearButton = within(container).queryByText('Clear results');
|
||||
const searchMessage = within(container).queryByText(`${output} "${searchText}"`);
|
||||
const leaners = container.querySelectorAll('.discussion-post') ?? [];
|
||||
|
||||
expect(searchMessage).toBeInTheDocument();
|
||||
expect(clearButton).toBeInTheDocument();
|
||||
expect(leaners).toHaveLength(learnersCount);
|
||||
await renderComponent();
|
||||
});
|
||||
});
|
||||
|
||||
test('When click on the clear button it should move to a list of all learners.', async () => {
|
||||
await setUpLearnerMockResponse();
|
||||
await assignPrivilages();
|
||||
await renderComponent();
|
||||
|
||||
const searchField = within(container).getByPlaceholderText('Search learners');
|
||||
const searchButton = within(container).getByTestId('search-icon');
|
||||
let clearButton;
|
||||
|
||||
await fireEvent.change(searchField, { target: { value: 'learner' } });
|
||||
await act(async () => {
|
||||
fireEvent.click(searchButton);
|
||||
setUpLearnerMockResponse(2, 2, 1, ['learner-1', 'learner-2'], 'learner');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
clearButton = within(container).queryByText('Clear results');
|
||||
});
|
||||
await act(async () => fireEvent.click(clearButton));
|
||||
await waitFor(() => {
|
||||
setUpLearnerMockResponse();
|
||||
});
|
||||
|
||||
const learners = container.querySelectorAll('.discussion-post');
|
||||
|
||||
expect(learners).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,9 +13,9 @@ Factory.define('learner')
|
||||
});
|
||||
|
||||
Factory.define('learnersResult')
|
||||
.option('count', null)
|
||||
.option('page', null)
|
||||
.option('pageSize', null)
|
||||
.option('count', null, 3)
|
||||
.option('page', null, 1)
|
||||
.option('pageSize', null, 5)
|
||||
.option('courseId', null, 'course-v1:Test+TestX+Test_Course')
|
||||
.option('activeFlags', null, 0)
|
||||
.attr(
|
||||
|
||||
@@ -24,7 +24,7 @@ const ActionItem = ({
|
||||
<label
|
||||
htmlFor={id}
|
||||
className="focus border-bottom-0 d-flex align-items-center w-100 py-2 m-0 font-weight-500 filter-menu"
|
||||
data-testid={value === selected ? `${value} selected` : null}
|
||||
data-testid={value === selected ? 'selected' : null}
|
||||
style={{ cursor: 'pointer' }}
|
||||
aria-checked={value === selected}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
|
||||
@@ -9,7 +10,9 @@ import {
|
||||
} from '@edx/paragon';
|
||||
import { ArrowBack } from '@edx/paragon/icons';
|
||||
|
||||
import { EndorsementStatus, PostsPages, ThreadType } from '../../data/constants';
|
||||
import {
|
||||
EndorsementStatus, PostsPages, RequestStatus, ThreadType,
|
||||
} from '../../data/constants';
|
||||
import { useDispatchWithState } from '../../data/hooks';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import { useIsOnDesktop } from '../data/hooks';
|
||||
@@ -21,12 +24,14 @@ import { ResponseEditor } from './comments/comment';
|
||||
import CommentsSort from './comments/CommentsSort';
|
||||
import CommentsView from './comments/CommentsView';
|
||||
import { useCommentsCount, usePost } from './data/hooks';
|
||||
import { selectCommentsStatus } from './data/selectors';
|
||||
import messages from './messages';
|
||||
|
||||
function PostCommentsView({ intl }) {
|
||||
const [isLoading, submitDispatch] = useDispatchWithState();
|
||||
const { postId } = useParams();
|
||||
const thread = usePost(postId);
|
||||
const commentsStatus = useSelector(selectCommentsStatus);
|
||||
const commentsCount = useCommentsCount(postId);
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
@@ -35,6 +40,7 @@ function PostCommentsView({ intl }) {
|
||||
const {
|
||||
courseId, learnerUsername, category, topicId, page, enableInContextSidebar,
|
||||
} = useContext(DiscussionContext);
|
||||
const enableCommentsSort = false;
|
||||
|
||||
useEffect(() => {
|
||||
if (!thread) { submitDispatch(fetchThread(postId, courseId, true)); }
|
||||
@@ -104,7 +110,7 @@ function PostCommentsView({ intl }) {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{!!commentsCount && <CommentsSort />}
|
||||
{!!commentsCount && commentsStatus === RequestStatus.SUCCESSFUL && enableCommentsSort && <CommentsSort />}
|
||||
{thread.type === ThreadType.DISCUSSION && (
|
||||
<CommentsView
|
||||
postId={postId}
|
||||
|
||||
@@ -20,12 +20,7 @@ import DiscussionContent from '../discussions-home/DiscussionContent';
|
||||
import { getThreadsApiUrl } from '../posts/data/api';
|
||||
import { fetchThread, fetchThreads } from '../posts/data/thunks';
|
||||
import { fetchCourseTopics } from '../topics/data/thunks';
|
||||
import { getDiscussionTourUrl } from '../tours/data/api';
|
||||
import { selectTours } from '../tours/data/selectors';
|
||||
import { fetchDiscussionTours } from '../tours/data/thunks';
|
||||
import discussionTourFactory from '../tours/data/tours.factory';
|
||||
import { getCommentsApiUrl } from './data/api';
|
||||
import { removeComment } from './data/thunks';
|
||||
|
||||
import '../posts/data/__factories__';
|
||||
import './data/__factories__';
|
||||
@@ -39,13 +34,11 @@ const questionPostId = 'thread-2';
|
||||
const closedPostId = 'thread-2';
|
||||
const courseId = 'course-v1:edX+TestX+Test_Course';
|
||||
const topicsApiUrl = `${getApiBaseUrl()}/api/discussion/v1/course_topics/${courseId}`;
|
||||
const reverseOrder = true;
|
||||
const reverseOrder = false;
|
||||
const enableInContextSidebar = false;
|
||||
let store;
|
||||
let axiosMock;
|
||||
let testLocation;
|
||||
let container;
|
||||
let unmount;
|
||||
|
||||
function mockAxiosReturnPagedComments() {
|
||||
[null, false, true].forEach(endorsed => {
|
||||
@@ -82,7 +75,6 @@ function mockAxiosReturnPagedCommentsResponses() {
|
||||
page: undefined,
|
||||
page_size: undefined,
|
||||
requested_fields: 'profile_image',
|
||||
reverse_order: true,
|
||||
};
|
||||
|
||||
for (let page = 1; page <= 2; page++) {
|
||||
@@ -124,8 +116,7 @@ function renderComponent(postId) {
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
container = wrapper.container;
|
||||
unmount = wrapper.unmount;
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
describe('PostView', () => {
|
||||
@@ -741,87 +732,19 @@ describe('ThreadView', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('For comments replies', () => {
|
||||
describe('for comments replies', () => {
|
||||
it('shows delete confirmation modal', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
|
||||
const reply = await waitFor(() => screen.findByTestId('reply-comment-7'));
|
||||
await act(async () => { fireEvent.click(within(reply).getByRole('button', { name: /actions menu/i })); });
|
||||
await act(async () => { fireEvent.click(screen.queryByRole('button', { name: /Delete/i })); });
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
within(reply).getByRole('button', { name: /actions menu/i }),
|
||||
);
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.queryByRole('button', { name: /Delete/i }));
|
||||
});
|
||||
expect(screen.queryByRole('dialog', { name: /Delete/i, exact: false })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('for comments sort', () => {
|
||||
const getCommentSortDropdown = async () => {
|
||||
renderComponent(discussionPostId);
|
||||
|
||||
await waitFor(() => screen.findByTestId('comment-comment-1'));
|
||||
await act(async () => { fireEvent.click(screen.getByRole('button', { name: /Newest first/i })); });
|
||||
return waitFor(() => screen.findByTestId('comment-sort-dropdown-modal-popup'));
|
||||
};
|
||||
|
||||
it('should show sort dropdown if there are endorse or unendorsed comments', async () => {
|
||||
renderComponent(discussionPostId);
|
||||
|
||||
const comment = await waitFor(() => screen.findByTestId('comment-comment-1'));
|
||||
const sortWrapper = container.querySelector('.comments-sort');
|
||||
const sortDropDown = within(sortWrapper).getByRole('button', { name: /Newest first/i });
|
||||
|
||||
expect(comment).toBeInTheDocument();
|
||||
expect(sortDropDown).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not show sort dropdown if there is no response', async () => {
|
||||
const commentId = 'comment-1';
|
||||
renderComponent(discussionPostId);
|
||||
|
||||
await waitFor(() => screen.findByTestId('comment-comment-1'));
|
||||
axiosMock.onDelete(`${commentsApiUrl}${commentId}/`).reply(201);
|
||||
await executeThunk(removeComment(commentId, discussionPostId), store.dispatch, store.getState);
|
||||
|
||||
expect(await waitFor(() => screen.findByText('No responses', { exact: true }))).toBeInTheDocument();
|
||||
expect(container.querySelector('.comments-sort')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have only two options', async () => {
|
||||
const dropdown = await getCommentSortDropdown();
|
||||
|
||||
expect(dropdown).toBeInTheDocument();
|
||||
expect(await within(dropdown).getAllByRole('button')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should be selected Newest first and auto focus', async () => {
|
||||
const dropdown = await getCommentSortDropdown();
|
||||
|
||||
expect(within(dropdown).getByRole('button', { name: /Newest first/i })).toBeInTheDocument();
|
||||
expect(within(dropdown).getByRole('button', { name: /Newest first/i })).toHaveFocus();
|
||||
expect(within(dropdown).getByRole('button', { name: /Oldest first/i })).not.toHaveFocus();
|
||||
});
|
||||
|
||||
test('successfully handles sort state update', async () => {
|
||||
const dropdown = await getCommentSortDropdown();
|
||||
|
||||
expect(store.getState().comments.sortOrder).toBeTruthy();
|
||||
await act(async () => { fireEvent.click(within(dropdown).getByRole('button', { name: /Oldest first/i })); });
|
||||
|
||||
expect(store.getState().comments.sortOrder).toBeFalsy();
|
||||
});
|
||||
|
||||
test('successfully handles tour state update', async () => {
|
||||
const tourName = 'response_sort';
|
||||
await axiosMock.onGet(getDiscussionTourUrl(), {}).reply(200, [discussionTourFactory.build({ tourName })]);
|
||||
await executeThunk(fetchDiscussionTours(), store.dispatch, store.getState);
|
||||
|
||||
renderComponent(discussionPostId);
|
||||
|
||||
await waitFor(() => screen.findByTestId('comment-comment-1'));
|
||||
const responseSortTour = () => selectTours(store.getState()).find(item => item.tourName === 'response_sort');
|
||||
|
||||
expect(responseSortTour().enabled).toBeTruthy();
|
||||
await unmount();
|
||||
expect(responseSortTour().enabled).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
@@ -6,43 +6,31 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Button, Dropdown, ModalPopup, useToggle,
|
||||
} from '@edx/paragon';
|
||||
import { ExpandLess, ExpandMore } from '@edx/paragon/icons';
|
||||
import {
|
||||
ExpandLess, ExpandMore,
|
||||
} from '@edx/paragon/icons';
|
||||
|
||||
import { updateUserDiscussionsTourByName } from '../../tours/data';
|
||||
import { selectCommentSortOrder } from '../data/selectors';
|
||||
import { setCommentSortOrder } from '../data/slices';
|
||||
import messages from '../messages';
|
||||
|
||||
function CommentSortDropdown({ intl }) {
|
||||
function CommentSortDropdown({
|
||||
intl,
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
const sortedOrder = useSelector(selectCommentSortOrder);
|
||||
const [isOpen, open, close] = useToggle(false);
|
||||
const [target, setTarget] = useState(null);
|
||||
|
||||
const handleActions = (reverseOrder) => {
|
||||
close();
|
||||
dispatch(setCommentSortOrder(reverseOrder));
|
||||
};
|
||||
|
||||
const enableCommentsSortTour = useCallback((enabled) => {
|
||||
const data = {
|
||||
enabled,
|
||||
tourName: 'response_sort',
|
||||
};
|
||||
dispatch(updateUserDiscussionsTourByName(data));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
enableCommentsSortTour(true);
|
||||
return () => {
|
||||
enableCommentsSortTour(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="comments-sort d-flex justify-content-end mx-4 mt-2">
|
||||
<Button
|
||||
id="comment-sort"
|
||||
alt={intl.formatMessage(messages.actionsAlt)}
|
||||
ref={setTarget}
|
||||
variant="tertiary"
|
||||
|
||||
@@ -19,12 +19,7 @@ import { useUserCanAddThreadInBlackoutDate } from '../../../data/hooks';
|
||||
import { fetchThread } from '../../../posts/data/thunks';
|
||||
import LikeButton from '../../../posts/post/LikeButton';
|
||||
import { useActions } from '../../../utils';
|
||||
import {
|
||||
selectCommentCurrentPage,
|
||||
selectCommentHasMorePages,
|
||||
selectCommentResponses,
|
||||
selectCommentSortOrder,
|
||||
} from '../../data/selectors';
|
||||
import { selectCommentCurrentPage, selectCommentHasMorePages, selectCommentResponses } from '../../data/selectors';
|
||||
import { editComment, fetchCommentResponses, removeComment } from '../../data/thunks';
|
||||
import messages from '../../messages';
|
||||
import CommentEditor from './CommentEditor';
|
||||
@@ -52,17 +47,13 @@ function Comment({
|
||||
const currentPage = useSelector(selectCommentCurrentPage(comment.id));
|
||||
const userCanAddThreadInBlackoutDate = useUserCanAddThreadInBlackoutDate();
|
||||
const { courseId } = useContext(DiscussionContext);
|
||||
const sortedOrder = useSelector(selectCommentSortOrder);
|
||||
|
||||
useEffect(() => {
|
||||
// If the comment has a parent comment, it won't have any children, so don't fetch them.
|
||||
if (hasChildren && showFullThread) {
|
||||
dispatch(fetchCommentResponses(comment.id, {
|
||||
page: 1,
|
||||
reverseOrder: sortedOrder,
|
||||
}));
|
||||
if (hasChildren && !currentPage && showFullThread) {
|
||||
dispatch(fetchCommentResponses(comment.id, { page: 1 }));
|
||||
}
|
||||
}, [comment.id, sortedOrder]);
|
||||
}, [comment.id]);
|
||||
|
||||
const actions = useActions({
|
||||
...comment,
|
||||
@@ -99,10 +90,7 @@ function Comment({
|
||||
}), [showDeleteConfirmation, dispatch, comment.id, comment.endorsed, comment.threadId, courseId, handleAbusedFlag]);
|
||||
|
||||
const handleLoadMoreComments = () => (
|
||||
dispatch(fetchCommentResponses(comment.id, {
|
||||
page: currentPage + 1,
|
||||
reverseOrder: sortedOrder,
|
||||
}))
|
||||
dispatch(fetchCommentResponses(comment.id, { page: currentPage + 1 }))
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -55,7 +55,6 @@ export async function getCommentResponses(
|
||||
commentId, {
|
||||
page,
|
||||
pageSize,
|
||||
reverseOrder,
|
||||
} = {},
|
||||
) {
|
||||
const url = `${getCommentsApiUrl()}${commentId}/`;
|
||||
@@ -63,7 +62,6 @@ export async function getCommentResponses(
|
||||
page,
|
||||
pageSize,
|
||||
requestedFields: 'profile_image',
|
||||
reverseOrder,
|
||||
});
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(url, { params });
|
||||
|
||||
@@ -22,7 +22,7 @@ const commentsSlice = createSlice({
|
||||
postStatus: RequestStatus.SUCCESSFUL,
|
||||
pagination: {},
|
||||
responsesPagination: {},
|
||||
sortOrder: true,
|
||||
sortOrder: false,
|
||||
},
|
||||
reducers: {
|
||||
fetchCommentsRequest: (state) => {
|
||||
@@ -75,16 +75,12 @@ const commentsSlice = createSlice({
|
||||
},
|
||||
fetchCommentResponsesSuccess: (state, { payload }) => {
|
||||
state.status = RequestStatus.SUCCESSFUL;
|
||||
if (payload.page === 1) {
|
||||
state.commentsInComments[payload.commentId] = payload.commentsInComments[payload.commentId] || [];
|
||||
} else {
|
||||
state.commentsInComments[payload.commentId] = [
|
||||
...new Set([
|
||||
...(state.commentsInComments[payload.commentId] || []),
|
||||
...(payload.commentsInComments[payload.commentId] || []),
|
||||
]),
|
||||
];
|
||||
}
|
||||
state.commentsInComments[payload.commentId] = [
|
||||
...new Set([
|
||||
...(state.commentsInComments[payload.commentId] || []),
|
||||
...(payload.commentsInComments[payload.commentId] || []),
|
||||
]),
|
||||
];
|
||||
state.commentsById = { ...state.commentsById, ...payload.commentsById };
|
||||
state.responsesPagination[payload.commentId] = {
|
||||
currentPage: payload.page,
|
||||
|
||||
@@ -106,11 +106,11 @@ export function fetchThreadComments(
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchCommentResponses(commentId, { page = 1, reverseOrder = true } = {}) {
|
||||
export function fetchCommentResponses(commentId, { page = 1 } = {}) {
|
||||
return async (dispatch) => {
|
||||
try {
|
||||
dispatch(fetchCommentResponsesRequest({ commentId }));
|
||||
const data = await getCommentResponses(commentId, { page, reverseOrder });
|
||||
const data = await getCommentResponses(commentId, { page });
|
||||
dispatch(fetchCommentResponsesSuccess({
|
||||
...normaliseComments(camelCaseObject(data)),
|
||||
page,
|
||||
|
||||
@@ -10,13 +10,5 @@ export default function tourCheckpoints(intl) {
|
||||
title: intl.formatMessage(messages.notRespondedFilterTourTitle),
|
||||
},
|
||||
],
|
||||
RESPONSE_SORT: [
|
||||
{
|
||||
body: intl.formatMessage(messages.responseSortTourBody),
|
||||
placement: 'left',
|
||||
target: '#comment-sort',
|
||||
title: intl.formatMessage(messages.responseSortTourTitle),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,25 +5,29 @@ import { initializeMockApp } from '@edx/frontend-platform/testing';
|
||||
|
||||
import { RequestStatus } from '../../../data/constants';
|
||||
import { initializeStore } from '../../../store';
|
||||
import { executeThunk } from '../../../test-utils';
|
||||
import { getDiscussionTourUrl } from './api';
|
||||
import { selectTours } from './selectors';
|
||||
import {
|
||||
discussionsTourRequest,
|
||||
discussionsToursRequestError,
|
||||
fetchUserDiscussionsToursSuccess,
|
||||
updateUserDiscussionsTourByName,
|
||||
toursReducer,
|
||||
updateUserDiscussionsTourSuccess,
|
||||
} from './slices';
|
||||
import { fetchDiscussionTours, updateTourShowStatus } from './thunks';
|
||||
import discussionTourFactory from './tours.factory';
|
||||
|
||||
const url = getDiscussionTourUrl();
|
||||
let actualActions;
|
||||
let mockAxios;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let store;
|
||||
const url = getDiscussionTourUrl();
|
||||
describe('DiscussionToursThunk', () => {
|
||||
let actualActions;
|
||||
|
||||
const dispatch = (action) => {
|
||||
actualActions.push(action);
|
||||
};
|
||||
|
||||
describe('DiscussionTours data layer', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
@@ -42,147 +46,168 @@ describe('DiscussionTours data layer', () => {
|
||||
mockAxios.reset();
|
||||
});
|
||||
|
||||
describe('DiscussionToursThunk', () => {
|
||||
const dispatch = (action) => {
|
||||
actualActions.push(action);
|
||||
};
|
||||
it('dispatches get request, success actions', async () => {
|
||||
const mockData = discussionTourFactory.buildList(2);
|
||||
mockAxios.onGet(url)
|
||||
.reply(200, mockData);
|
||||
|
||||
const getExpectedAction = (mockData) => ({
|
||||
request: {
|
||||
const expectedActions = [
|
||||
{
|
||||
payload: undefined,
|
||||
type: 'userDiscussionsTours/discussionsTourRequest',
|
||||
},
|
||||
fetch: {
|
||||
{
|
||||
type: 'userDiscussionsTours/fetchUserDiscussionsToursSuccess',
|
||||
payload: mockData,
|
||||
},
|
||||
update: {
|
||||
];
|
||||
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,
|
||||
},
|
||||
error: {
|
||||
payload: undefined,
|
||||
type: 'userDiscussionsTours/discussionsToursRequestError',
|
||||
},
|
||||
});
|
||||
|
||||
it('dispatches get request, success actions', async () => {
|
||||
const mockData = discussionTourFactory.buildList(2);
|
||||
mockAxios.onGet(url).reply(200, mockData);
|
||||
const expectedActions = [getExpectedAction().request, getExpectedAction(mockData).fetch];
|
||||
|
||||
await fetchDiscussionTours()(dispatch);
|
||||
expect(actualActions).toEqual(expectedActions);
|
||||
});
|
||||
|
||||
it('dispatches request, and error actions', async () => {
|
||||
mockAxios.onGet('/api/discussion-tours/').reply(500);
|
||||
const expectedActions = [getExpectedAction().request, getExpectedAction().error];
|
||||
|
||||
await fetchDiscussionTours()(dispatch);
|
||||
expect(actualActions).toEqual(expectedActions);
|
||||
});
|
||||
|
||||
it('dispatches put request, success actions', async () => {
|
||||
const mockData = discussionTourFactory.build();
|
||||
mockAxios.onPut(`${url}${1}`).reply(200, mockData);
|
||||
const expectedActions = [getExpectedAction().request, getExpectedAction(mockData).update];
|
||||
|
||||
await updateTourShowStatus(1)(dispatch);
|
||||
expect(actualActions).toEqual(expectedActions);
|
||||
});
|
||||
|
||||
it('dispatches update request, and error actions', async () => {
|
||||
mockAxios.onPut(`${url}${1}`).reply(500);
|
||||
const expectedActions = [getExpectedAction().request, getExpectedAction().error];
|
||||
|
||||
await updateTourShowStatus(1)(dispatch);
|
||||
expect(actualActions).toEqual(expectedActions);
|
||||
});
|
||||
];
|
||||
await updateTourShowStatus(1)(dispatch);
|
||||
expect(actualActions)
|
||||
.toEqual(expectedActions);
|
||||
});
|
||||
|
||||
describe('toursReducer', () => {
|
||||
it('handles the discussionsToursRequest action', async () => {
|
||||
store.dispatch(discussionsTourRequest());
|
||||
const { tours } = store.getState();
|
||||
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',
|
||||
}];
|
||||
|
||||
expect(tours.tours).toEqual([]);
|
||||
expect(tours.error).toBeNull();
|
||||
expect(tours.loading).toEqual(RequestStatus.IN_PROGRESS);
|
||||
});
|
||||
await updateTourShowStatus(1)(dispatch);
|
||||
expect(actualActions)
|
||||
.toEqual(errorAction);
|
||||
});
|
||||
});
|
||||
|
||||
it('handles the fetchUserDiscussionsToursSuccess action', async () => {
|
||||
const mockData = [{ id: 1 }, { id: 2 }];
|
||||
await store.dispatch(fetchUserDiscussionsToursSuccess(mockData));
|
||||
const { tours } = store.getState();
|
||||
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,
|
||||
});
|
||||
});
|
||||
|
||||
expect(tours).toEqual({
|
||||
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', async () => {
|
||||
const updatedTour = { id: 2, name: 'Updated Tour' };
|
||||
await store.dispatch(fetchUserDiscussionsToursSuccess([{ id: 1 }, { id: 2 }]));
|
||||
await store.dispatch(updateUserDiscussionsTourSuccess(updatedTour));
|
||||
const { tours } = store.getState();
|
||||
it('handles the updateUserDiscussionsTourSuccess action', () => {
|
||||
const initialState = {
|
||||
tours: [
|
||||
{ id: 1 },
|
||||
{ id: 2 },
|
||||
],
|
||||
};
|
||||
const updatedTour = {
|
||||
id: 2,
|
||||
name: 'Updated Tour',
|
||||
};
|
||||
const state = toursReducer(initialState, updateUserDiscussionsTourSuccess(updatedTour));
|
||||
expect(state.tours)
|
||||
.toEqual([{ id: 1 }, updatedTour]);
|
||||
});
|
||||
|
||||
expect(tours.tours).toEqual([{ id: 1 }, updatedTour]);
|
||||
});
|
||||
|
||||
it('handles the discussionsToursRequestError action', async () => {
|
||||
const errorMessage = 'Something went wrong';
|
||||
await store.dispatch(discussionsToursRequestError(errorMessage));
|
||||
const { tours } = store.getState();
|
||||
|
||||
expect(tours).toEqual({
|
||||
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: errorMessage,
|
||||
error: mockError,
|
||||
});
|
||||
});
|
||||
|
||||
it('handles the updateUserDiscussionsTourByName action', async () => {
|
||||
const tourName = 'response_sort';
|
||||
const updatedTour = {
|
||||
tourName: 'response_sort',
|
||||
enabled: false,
|
||||
};
|
||||
|
||||
await mockAxios.onGet(getDiscussionTourUrl(), {}).reply(200, [discussionTourFactory.build({ tourName })]);
|
||||
await executeThunk(fetchDiscussionTours(), store.dispatch, store.getState);
|
||||
store.dispatch(updateUserDiscussionsTourByName(updatedTour));
|
||||
|
||||
expect(store.getState().tours.tours).toEqual([{
|
||||
id: 4,
|
||||
tourName: 'response_sort',
|
||||
enabled: false,
|
||||
description: 'This is the description for Discussion Tour 4.',
|
||||
}]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tourSelector', () => {
|
||||
it('returns the tours list from state', async () => {
|
||||
await mockAxios.onGet(getDiscussionTourUrl(), {}).reply(200, [
|
||||
discussionTourFactory.build({ tourName: 'other_filter' }),
|
||||
]);
|
||||
await executeThunk(fetchDiscussionTours(), store.dispatch, store.getState);
|
||||
|
||||
expect(selectTours(store.getState())).toEqual([{
|
||||
id: 5,
|
||||
tourName: 'other_filter',
|
||||
description: 'This is the description for Discussion Tour 5.',
|
||||
enabled: true,
|
||||
}]);
|
||||
});
|
||||
|
||||
it('returns an empty list if the tours state is not defined', async () => {
|
||||
await executeThunk(fetchDiscussionTours(), store.dispatch, store.getState);
|
||||
|
||||
expect(selectTours(store.getState())).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,12 +31,6 @@ const userDiscussionsToursSlice = createSlice({
|
||||
state.loading = RequestStatus.SUCCESSFUL;
|
||||
state.error = null;
|
||||
},
|
||||
updateUserDiscussionsTourByName: (state, action) => {
|
||||
const tourIndex = state.tours.findIndex(tour => tour.tourName === action.payload.tourName);
|
||||
state.tours[tourIndex] = { ...state.tours[tourIndex], ...action.payload };
|
||||
state.loading = RequestStatus.SUCCESSFUL;
|
||||
state.error = null;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -45,7 +39,6 @@ export const {
|
||||
fetchUserDiscussionsToursSuccess,
|
||||
discussionsToursRequestError,
|
||||
updateUserDiscussionsTourSuccess,
|
||||
updateUserDiscussionsTourByName,
|
||||
} = userDiscussionsToursSlice.actions;
|
||||
|
||||
export const toursReducer = userDiscussionsToursSlice.reducer;
|
||||
|
||||
@@ -9,10 +9,6 @@ import {
|
||||
updateUserDiscussionsTourSuccess,
|
||||
} from './slices';
|
||||
|
||||
function normaliseTourData(data) {
|
||||
return data.map(tour => ({ ...tour, enabled: true }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Action thunk to fetch the list of discussion tours for the current user.
|
||||
* @returns {function} - Thunk that dispatches the request, success, and error actions.
|
||||
@@ -22,7 +18,7 @@ export function fetchDiscussionTours() {
|
||||
try {
|
||||
dispatch(discussionsTourRequest());
|
||||
const data = await getDiscssionTours();
|
||||
dispatch(fetchUserDiscussionsToursSuccess(camelCaseObject(normaliseTourData(data))));
|
||||
dispatch(fetchUserDiscussionsToursSuccess(camelCaseObject(data)));
|
||||
} catch (error) {
|
||||
dispatch(discussionsToursRequestError());
|
||||
logError(error);
|
||||
|
||||
@@ -2,8 +2,7 @@ import { Factory } from 'rosie';
|
||||
|
||||
const discussionTourFactory = new Factory()
|
||||
.sequence('id')
|
||||
.attr('tourName', ['id'], (id) => `Discussion Tour ${id}`)
|
||||
.attr('description', ['id'], (id) => `This is the description for Discussion Tour ${id}.`)
|
||||
.attr('enabled', ['id'], true);
|
||||
.attr('name', ['id'], (id) => `Discussion Tour ${id}`)
|
||||
.attr('description', ['id'], (id) => `This is the description for Discussion Tour ${id}.`);
|
||||
|
||||
export default discussionTourFactory;
|
||||
|
||||
@@ -26,16 +26,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'New filtering option!',
|
||||
description: 'Title of the tour for the not responded filter',
|
||||
},
|
||||
responseSortTourBody: {
|
||||
id: 'tour.body.responseSortTour',
|
||||
defaultMessage: 'Responses and comments are now sorted by newest first. Please use this option to change the sort order',
|
||||
description: 'Body of the tour for the response sort',
|
||||
},
|
||||
responseSortTourTitle: {
|
||||
id: 'tour.title.responseSortTour',
|
||||
defaultMessage: 'Sort Responses!',
|
||||
description: 'Title of the tour for the response sort',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -208,7 +208,5 @@
|
||||
"tour.action.dismiss": "Dismiss",
|
||||
"tour.action.end": "Okay",
|
||||
"tour.body.notRespondedFilter": "Now you can filter discussions to find posts with no response.",
|
||||
"tour.title.notRespondedFilter": "New filtering option!",
|
||||
"tour.body.responseSortTour": "Responses and comments are now sorted by newest first. Please use this option to change the sort order",
|
||||
"tour.title.responseSortTour": "Sort Responses!"
|
||||
"tour.title.notRespondedFilter": "New filtering option!"
|
||||
}
|
||||
@@ -208,7 +208,5 @@
|
||||
"tour.action.dismiss": "Abgewiesen",
|
||||
"tour.action.end": "okay",
|
||||
"tour.body.notRespondedFilter": "Jetzt können Sie Diskussionen filtern, um Beiträge ohne Antwort zu finden.",
|
||||
"tour.title.notRespondedFilter": "Neue Filteroption!",
|
||||
"tour.body.responseSortTour": "Responses and comments are now sorted by newest first. Please use this option to change the sort order",
|
||||
"tour.title.responseSortTour": "Sort Responses!"
|
||||
"tour.title.notRespondedFilter": "Neue Filteroption!"
|
||||
}
|
||||
@@ -208,7 +208,5 @@
|
||||
"tour.action.dismiss": "Descartar",
|
||||
"tour.action.end": "Okey",
|
||||
"tour.body.notRespondedFilter": "Ahora puede filtrar debates para encontrar publicaciones sin respuesta.",
|
||||
"tour.title.notRespondedFilter": "¡Nueva opción de filtrado!",
|
||||
"tour.body.responseSortTour": "Responses and comments are now sorted by newest first. Please use this option to change the sort order",
|
||||
"tour.title.responseSortTour": "Sort Responses!"
|
||||
"tour.title.notRespondedFilter": "¡Nueva opción de filtrado!"
|
||||
}
|
||||
@@ -208,7 +208,5 @@
|
||||
"tour.action.dismiss": "Dismiss",
|
||||
"tour.action.end": "Okay",
|
||||
"tour.body.notRespondedFilter": "Now you can filter discussions to find posts with no response.",
|
||||
"tour.title.notRespondedFilter": "New filtering option!",
|
||||
"tour.body.responseSortTour": "Responses and comments are now sorted by newest first. Please use this option to change the sort order",
|
||||
"tour.title.responseSortTour": "Sort Responses!"
|
||||
"tour.title.notRespondedFilter": "New filtering option!"
|
||||
}
|
||||
@@ -208,7 +208,5 @@
|
||||
"tour.action.dismiss": "Rejeter",
|
||||
"tour.action.end": "Okay",
|
||||
"tour.body.notRespondedFilter": "Vous pouvez maintenant filtrer les discussions pour trouver les messages sans réponse.",
|
||||
"tour.title.notRespondedFilter": "Nouvelle option de filtrage!",
|
||||
"tour.body.responseSortTour": "Les réponses et les commentaires sont désormais triés par les plus récents en premier. Veuillez utiliser cette option pour modifier l'ordre de tri",
|
||||
"tour.title.responseSortTour": "Triez les réponses !"
|
||||
"tour.title.notRespondedFilter": "Nouvelle option de filtrage!"
|
||||
}
|
||||
@@ -208,7 +208,5 @@
|
||||
"tour.action.dismiss": "Dismiss",
|
||||
"tour.action.end": "Okay",
|
||||
"tour.body.notRespondedFilter": "Now you can filter discussions to find posts with no response.",
|
||||
"tour.title.notRespondedFilter": "New filtering option!",
|
||||
"tour.body.responseSortTour": "Responses and comments are now sorted by newest first. Please use this option to change the sort order",
|
||||
"tour.title.responseSortTour": "Sort Responses!"
|
||||
"tour.title.notRespondedFilter": "New filtering option!"
|
||||
}
|
||||
@@ -208,7 +208,5 @@
|
||||
"tour.action.dismiss": "Dismiss",
|
||||
"tour.action.end": "Okay",
|
||||
"tour.body.notRespondedFilter": "Now you can filter discussions to find posts with no response.",
|
||||
"tour.title.notRespondedFilter": "New filtering option!",
|
||||
"tour.body.responseSortTour": "Responses and comments are now sorted by newest first. Please use this option to change the sort order",
|
||||
"tour.title.responseSortTour": "Sort Responses!"
|
||||
"tour.title.notRespondedFilter": "New filtering option!"
|
||||
}
|
||||
@@ -208,7 +208,5 @@
|
||||
"tour.action.dismiss": "Dismiss",
|
||||
"tour.action.end": "Okay",
|
||||
"tour.body.notRespondedFilter": "Now you can filter discussions to find posts with no response.",
|
||||
"tour.title.notRespondedFilter": "New filtering option!",
|
||||
"tour.body.responseSortTour": "Responses and comments are now sorted by newest first. Please use this option to change the sort order",
|
||||
"tour.title.responseSortTour": "Sort Responses!"
|
||||
"tour.title.notRespondedFilter": "New filtering option!"
|
||||
}
|
||||
@@ -208,7 +208,5 @@
|
||||
"tour.action.dismiss": "İptal",
|
||||
"tour.action.end": "Tamam",
|
||||
"tour.body.notRespondedFilter": "Artık yanıt vermeyen iletileri bulmak için tartışmaları filtreleyebilirsiniz.",
|
||||
"tour.title.notRespondedFilter": "Yeni filtreleme seçeneği!",
|
||||
"tour.body.responseSortTour": "Responses and comments are now sorted by newest first. Please use this option to change the sort order",
|
||||
"tour.title.responseSortTour": "Sort Responses!"
|
||||
"tour.title.notRespondedFilter": "Yeni filtreleme seçeneği!"
|
||||
}
|
||||
@@ -208,7 +208,5 @@
|
||||
"tour.action.dismiss": "Dismiss",
|
||||
"tour.action.end": "Okay",
|
||||
"tour.body.notRespondedFilter": "Now you can filter discussions to find posts with no response.",
|
||||
"tour.title.notRespondedFilter": "New filtering option!",
|
||||
"tour.body.responseSortTour": "Responses and comments are now sorted by newest first. Please use this option to change the sort order",
|
||||
"tour.title.responseSortTour": "Sort Responses!"
|
||||
"tour.title.notRespondedFilter": "New filtering option!"
|
||||
}
|
||||
Reference in New Issue
Block a user