feat: added filter options for learner posts (#340)
Co-authored-by: adeel.tajamul <adeel.tajamul@arbisoft.com>
This commit is contained in:
committed by
GitHub
parent
12dd08d97f
commit
7ced4b292c
201
src/components/FilterBar.jsx
Normal file
201
src/components/FilterBar.jsx
Normal file
@@ -0,0 +1,201 @@
|
||||
/* eslint-disable react/forbid-prop-types */
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { capitalize, toString } from 'lodash';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Collapsible, Form, Icon, Spinner,
|
||||
} from '@edx/paragon';
|
||||
import { Tune } from '@edx/paragon/icons';
|
||||
|
||||
import {
|
||||
PostsStatusFilter, RequestStatus,
|
||||
ThreadOrdering, ThreadType,
|
||||
} from '../data/constants';
|
||||
import { selectCourseCohorts } from '../discussions/cohorts/data/selectors';
|
||||
import messages from '../discussions/posts/post-filter-bar/messages';
|
||||
import { ActionItem } from '../discussions/posts/post-filter-bar/PostFilterBar';
|
||||
|
||||
function FilterBar({
|
||||
intl,
|
||||
filters,
|
||||
selectedFilters,
|
||||
onFilterChange,
|
||||
showCohortsFilter,
|
||||
}) {
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
const cohorts = useSelector(selectCourseCohorts);
|
||||
const { status } = useSelector(state => state.cohorts);
|
||||
const selectedCohort = useMemo(() => cohorts.find(cohort => (
|
||||
toString(cohort.id) === selectedFilters.cohort)),
|
||||
[selectedFilters.cohort]);
|
||||
|
||||
const allFilters = [
|
||||
{
|
||||
id: 'type-all',
|
||||
label: intl.formatMessage(messages.allPosts),
|
||||
value: ThreadType.ALL,
|
||||
},
|
||||
{
|
||||
id: 'type-discussions',
|
||||
label: intl.formatMessage(messages.filterDiscussions),
|
||||
value: ThreadType.DISCUSSION,
|
||||
},
|
||||
{
|
||||
id: 'type-questions',
|
||||
label: intl.formatMessage(messages.filterQuestions),
|
||||
value: ThreadType.QUESTION,
|
||||
},
|
||||
{
|
||||
id: 'status-any',
|
||||
label: intl.formatMessage(messages.filterAnyStatus),
|
||||
value: PostsStatusFilter.ALL,
|
||||
},
|
||||
{
|
||||
id: 'status-unread',
|
||||
label: intl.formatMessage(messages.filterUnread),
|
||||
value: PostsStatusFilter.UNREAD,
|
||||
},
|
||||
{
|
||||
id: 'status-reported',
|
||||
label: intl.formatMessage(messages.filterReported),
|
||||
value: PostsStatusFilter.REPORTED,
|
||||
},
|
||||
{
|
||||
id: 'status-unanswered',
|
||||
label: intl.formatMessage(messages.filterUnanswered),
|
||||
value: PostsStatusFilter.UNANSWERED,
|
||||
},
|
||||
{
|
||||
id: 'status-unresponded',
|
||||
label: intl.formatMessage(messages.filterUnresponded),
|
||||
value: PostsStatusFilter.UNRESPONDED,
|
||||
},
|
||||
{
|
||||
id: 'sort-activity',
|
||||
label: intl.formatMessage(messages.lastActivityAt),
|
||||
value: ThreadOrdering.BY_LAST_ACTIVITY,
|
||||
},
|
||||
{
|
||||
id: 'sort-comments',
|
||||
label: intl.formatMessage(messages.commentCount),
|
||||
value: ThreadOrdering.BY_COMMENT_COUNT,
|
||||
},
|
||||
{
|
||||
id: 'sort-votes',
|
||||
label: intl.formatMessage(messages.voteCount),
|
||||
value: ThreadOrdering.BY_VOTE_COUNT,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Collapsible.Advanced
|
||||
open={isOpen}
|
||||
onToggle={() => setOpen(!isOpen)}
|
||||
className="filter-bar collapsible-card-lg border-0"
|
||||
>
|
||||
<Collapsible.Trigger className="collapsible-trigger border-0">
|
||||
<span className="text-primary-700 pr-4">
|
||||
{intl.formatMessage(messages.sortFilterStatus, {
|
||||
own: false,
|
||||
type: selectedFilters.type,
|
||||
sort: selectedFilters.orderBy,
|
||||
status: selectedFilters.status,
|
||||
cohortType: selectedCohort?.name ? 'group' : 'all',
|
||||
cohort: capitalize(selectedCohort?.name),
|
||||
})}
|
||||
</span>
|
||||
<Collapsible.Visible whenClosed>
|
||||
<Icon src={Tune} />
|
||||
</Collapsible.Visible>
|
||||
<Collapsible.Visible whenOpen>
|
||||
<Icon src={Tune} />
|
||||
</Collapsible.Visible>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Body className="collapsible-body px-4 pb-3 pt-0">
|
||||
<Form>
|
||||
<div className="d-flex flex-row py-2 justify-content-between">
|
||||
{filters.map((value) => (
|
||||
<Form.RadioSet
|
||||
name={value.name}
|
||||
className="d-flex flex-column list-group list-group-flush"
|
||||
value={selectedFilters[value.name]}
|
||||
onChange={onFilterChange}
|
||||
>
|
||||
{
|
||||
value.filters.map(filterName => {
|
||||
const element = allFilters.find(obj => obj.id === filterName);
|
||||
if (element) {
|
||||
return (
|
||||
<ActionItem
|
||||
id={element.id}
|
||||
label={element.label}
|
||||
value={element.value}
|
||||
selected={selectedFilters[value.name]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return false;
|
||||
})
|
||||
}
|
||||
|
||||
</Form.RadioSet>
|
||||
))}
|
||||
</div>
|
||||
{showCohortsFilter && (
|
||||
<>
|
||||
<div className="border-bottom my-2" />
|
||||
{status === RequestStatus.IN_PROGRESS ? (
|
||||
<div className="d-flex justify-content-center p-4">
|
||||
<Spinner animation="border" variant="primary" size="lg" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="d-flex flex-row pt-2">
|
||||
<Form.RadioSet
|
||||
name="cohort"
|
||||
className="d-flex flex-column list-group list-group-flush w-100"
|
||||
value={selectedFilters.cohort}
|
||||
onChange={onFilterChange}
|
||||
>
|
||||
<ActionItem
|
||||
id="all-groups"
|
||||
label="All groups"
|
||||
value=""
|
||||
selected={selectedFilters.cohort}
|
||||
/>
|
||||
{cohorts.map(cohort => (
|
||||
<ActionItem
|
||||
key={toString(cohort.id)}
|
||||
id={toString(cohort.id)}
|
||||
label={capitalize(cohort.name)}
|
||||
value={toString(cohort.id)}
|
||||
selected={selectedFilters.cohort}
|
||||
/>
|
||||
))}
|
||||
</Form.RadioSet>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
);
|
||||
}
|
||||
|
||||
FilterBar.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
filters: PropTypes.array.isRequired,
|
||||
selectedFilters: PropTypes.object.isRequired,
|
||||
onFilterChange: PropTypes.func.isRequired,
|
||||
showCohortsFilter: PropTypes.bool,
|
||||
};
|
||||
|
||||
FilterBar.defaultProps = {
|
||||
showCohortsFilter: false,
|
||||
};
|
||||
|
||||
export default injectIntl(FilterBar);
|
||||
@@ -111,6 +111,7 @@ export const PostsStatusFilter = {
|
||||
FOLLOWING: 'statusFollowing',
|
||||
REPORTED: 'statusReported',
|
||||
UNANSWERED: 'statusUnanswered',
|
||||
UNRESPONDED: 'statusUnresponded',
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, {
|
||||
useCallback, useContext, useEffect, useMemo,
|
||||
} from 'react';
|
||||
|
||||
import snakeCase from 'lodash.snakecase';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
@@ -12,7 +13,12 @@ import {
|
||||
} from '@edx/paragon';
|
||||
import { ArrowBack } from '@edx/paragon/icons';
|
||||
|
||||
import { RequestStatus, Routes } from '../../data/constants';
|
||||
import {
|
||||
PostsStatusFilter,
|
||||
RequestStatus,
|
||||
Routes,
|
||||
ThreadType,
|
||||
} from '../../data/constants';
|
||||
import { DiscussionContext } from '../common/context';
|
||||
import { selectUserHasModerationPrivileges, selectUserIsStaff } from '../data/selectors';
|
||||
import {
|
||||
@@ -20,10 +26,12 @@ import {
|
||||
selectThreadNextPage,
|
||||
threadsLoadingStatus,
|
||||
} from '../posts/data/selectors';
|
||||
import { clearPostsPages } from '../posts/data/slices';
|
||||
import NoResults from '../posts/NoResults';
|
||||
import { PostLink } from '../posts/post';
|
||||
import { discussionsPath, filterPosts } from '../utils';
|
||||
import { fetchUserPosts } from './data/thunks';
|
||||
import LearnerPostFilterBar from './learner-post-filter-bar/LearnerPostFilterBar';
|
||||
import messages from './messages';
|
||||
|
||||
function LearnerPostsView({ intl }) {
|
||||
@@ -33,21 +41,49 @@ function LearnerPostsView({ intl }) {
|
||||
|
||||
const posts = useSelector(selectAllThreads);
|
||||
const loadingStatus = useSelector(threadsLoadingStatus());
|
||||
const postFilter = useSelector(state => state.learners.postFilter);
|
||||
const { courseId, learnerUsername: username } = useContext(DiscussionContext);
|
||||
const nextPage = useSelector(selectThreadNextPage());
|
||||
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
|
||||
const userIsStaff = useSelector(selectUserIsStaff);
|
||||
const countFlagged = userHasModerationPrivileges || userIsStaff;
|
||||
const params = {
|
||||
orderBy: snakeCase(postFilter.orderBy),
|
||||
username,
|
||||
page: 1,
|
||||
};
|
||||
if (postFilter.type !== ThreadType.ALL) {
|
||||
params.threadType = postFilter.type;
|
||||
}
|
||||
if (postFilter.status !== PostsStatusFilter.ALL) {
|
||||
const statusMapping = {
|
||||
statusUnread: 'unread',
|
||||
statusReported: 'flagged',
|
||||
statusUnanswered: 'unanswered',
|
||||
statusUnresponded: 'unresponded',
|
||||
};
|
||||
params.status = statusMapping[postFilter.status];
|
||||
}
|
||||
if (postFilter.cohort !== '') {
|
||||
params.groupId = postFilter.cohort;
|
||||
}
|
||||
if (countFlagged) {
|
||||
params.countFlagged = countFlagged;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchUserPosts(courseId, { username, countFlagged }));
|
||||
dispatch(fetchUserPosts(courseId, params));
|
||||
}, [courseId, username]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(clearPostsPages());
|
||||
dispatch(fetchUserPosts(courseId, params));
|
||||
}, [postFilter]);
|
||||
|
||||
const loadMorePosts = () => (
|
||||
dispatch(fetchUserPosts(courseId, {
|
||||
username,
|
||||
...params,
|
||||
page: nextPage,
|
||||
countFlagged,
|
||||
}))
|
||||
);
|
||||
|
||||
@@ -84,6 +120,7 @@ function LearnerPostsView({ intl }) {
|
||||
<div style={{ padding: '18px' }} />
|
||||
</div>
|
||||
<div className="bg-light-400 border border-light-300" />
|
||||
<LearnerPostFilterBar />
|
||||
<div className="list-group list-group-flush">
|
||||
{postInstances(pinnedPosts)}
|
||||
{postInstances(unpinnedPosts)}
|
||||
|
||||
@@ -45,11 +45,10 @@ export async function getUserProfiles(usernames) {
|
||||
* pagination: {count, num_pages, next, previous}
|
||||
* }
|
||||
*/
|
||||
export async function getUserPosts(courseId, { username, page, countFlagged }) {
|
||||
export async function getUserPosts(courseId, params) {
|
||||
const learnerPostsApiUrl = `${coursesApiUrl}${courseId}/learner/`;
|
||||
|
||||
const params = snakeCaseObject({ username, page, countFlagged });
|
||||
const snakeCaseParams = snakeCaseObject(params);
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(learnerPostsApiUrl, { params });
|
||||
.get(learnerPostsApiUrl, { params: snakeCaseParams });
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@ import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
import {
|
||||
LearnersOrdering,
|
||||
PostsStatusFilter,
|
||||
RequestStatus,
|
||||
ThreadOrdering,
|
||||
ThreadType,
|
||||
} from '../../../data/constants';
|
||||
|
||||
const learnersSlice = createSlice({
|
||||
@@ -16,6 +19,12 @@ const learnersSlice = createSlice({
|
||||
totalPages: null,
|
||||
totalLearners: null,
|
||||
sortedBy: LearnersOrdering.BY_LAST_ACTIVITY,
|
||||
postFilter: {
|
||||
type: ThreadType.ALL,
|
||||
status: PostsStatusFilter.ALL,
|
||||
orderBy: ThreadOrdering.BY_LAST_ACTIVITY,
|
||||
cohort: '',
|
||||
},
|
||||
usernameSearch: null,
|
||||
},
|
||||
reducers: {
|
||||
@@ -47,6 +56,10 @@ const learnersSlice = createSlice({
|
||||
state.usernameSearch = payload;
|
||||
state.pages = [];
|
||||
},
|
||||
setPostFilter: (state, { payload }) => {
|
||||
state.pages = [];
|
||||
state.postFilter = payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -57,6 +70,7 @@ export const {
|
||||
fetchLearnersDenied,
|
||||
setSortedBy,
|
||||
setUsernameSearch,
|
||||
setPostFilter,
|
||||
} = learnersSlice.actions;
|
||||
|
||||
export const learnersReducer = learnersSlice.reducer;
|
||||
|
||||
@@ -67,15 +67,17 @@ export function fetchLearners(courseId, {
|
||||
* @param page
|
||||
* @returns a promise that will update the state with the learner's posts
|
||||
*/
|
||||
export function fetchUserPosts(courseId, { username, page = 1, countFlagged = false } = {}) {
|
||||
export function fetchUserPosts(courseId, params) {
|
||||
return async (dispatch) => {
|
||||
try {
|
||||
dispatch(fetchLearnerThreadsRequest({ courseId, author: username }));
|
||||
|
||||
const data = await getUserPosts(courseId, { username, page, countFlagged });
|
||||
dispatch(fetchLearnerThreadsRequest({ courseId, author: params?.username }));
|
||||
const data = await getUserPosts(courseId, params);
|
||||
const normalisedData = normaliseThreads(camelCaseObject(data));
|
||||
|
||||
dispatch(fetchThreadsSuccess({ ...normalisedData, page, author: username }));
|
||||
dispatch(fetchThreadsSuccess({
|
||||
...normalisedData,
|
||||
page: params.page,
|
||||
author: params.username,
|
||||
}));
|
||||
} catch (error) {
|
||||
if (getHttpErrorStatus(error) === 403) {
|
||||
dispatch(fetchThreadsDenied());
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import FilterBar from '../../../components/FilterBar';
|
||||
import { selectCourseCohorts } from '../../cohorts/data/selectors';
|
||||
import { fetchCourseCohorts } from '../../cohorts/data/thunks';
|
||||
import { selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../../data/selectors';
|
||||
import { setPostFilter } from '../data/slices';
|
||||
|
||||
function LearnerPostFilterBar() {
|
||||
const dispatch = useDispatch();
|
||||
const { courseId } = useParams();
|
||||
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
|
||||
const userIsGroupTa = useSelector(selectUserIsGroupTa);
|
||||
const cohorts = useSelector(selectCourseCohorts);
|
||||
const postFilter = useSelector(state => state.learners.postFilter);
|
||||
|
||||
const filtersToShow = [
|
||||
{
|
||||
name: 'type',
|
||||
filters: ['type-all', 'type-discussions', 'type-questions'],
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
filters: ['status-any', 'status-unread', 'status-unanswered', 'status-unresponded'],
|
||||
},
|
||||
{
|
||||
name: 'orderBy',
|
||||
filters: ['sort-activity', 'sort-comments', 'sort-votes'],
|
||||
},
|
||||
];
|
||||
|
||||
if (userHasModerationPrivileges || userIsGroupTa) {
|
||||
filtersToShow[1].filters.splice(2, 0, 'status-reported');
|
||||
}
|
||||
|
||||
const handleFilterChange = (event) => {
|
||||
const { name, value } = event.currentTarget;
|
||||
if (name === 'type') {
|
||||
if (postFilter.type !== value) {
|
||||
dispatch(setPostFilter({
|
||||
...postFilter,
|
||||
type: value,
|
||||
}));
|
||||
}
|
||||
} else if (name === 'status') {
|
||||
if (postFilter.status !== value) {
|
||||
dispatch(setPostFilter({
|
||||
...postFilter,
|
||||
status: value,
|
||||
}));
|
||||
}
|
||||
} else if (name === 'orderBy') {
|
||||
if (postFilter.orderBy !== value) {
|
||||
dispatch(setPostFilter({
|
||||
...postFilter,
|
||||
orderBy: value,
|
||||
}));
|
||||
}
|
||||
} else if (name === 'cohort') {
|
||||
if (postFilter.cohort !== value) {
|
||||
dispatch(setPostFilter({
|
||||
...postFilter,
|
||||
cohort: value,
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (userHasModerationPrivileges && isEmpty(cohorts)) {
|
||||
dispatch(fetchCourseCohorts(courseId));
|
||||
}
|
||||
}, [courseId, userHasModerationPrivileges]);
|
||||
|
||||
return (
|
||||
<FilterBar
|
||||
filters={filtersToShow}
|
||||
selectedFilters={postFilter}
|
||||
onFilterChange={handleFilterChange}
|
||||
showCohortsFilter={userHasModerationPrivileges || userIsGroupTa}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default LearnerPostFilterBar;
|
||||
@@ -0,0 +1,108 @@
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { generatePath, MemoryRouter, Route } from 'react-router';
|
||||
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import { Routes } from '../../../data/constants';
|
||||
import { initializeStore } from '../../../store';
|
||||
import { DiscussionContext } from '../../common/context';
|
||||
import LearnerPostFilterBar from './LearnerPostFilterBar';
|
||||
|
||||
let store;
|
||||
const username = 'abc123';
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const path = generatePath(
|
||||
Routes.LEARNERS.POSTS,
|
||||
{ courseId, learnerUsername: username },
|
||||
);
|
||||
|
||||
function renderComponent() {
|
||||
return render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<DiscussionContext.Provider
|
||||
value={{
|
||||
learnerUsername: username,
|
||||
courseId,
|
||||
}}
|
||||
>
|
||||
<MemoryRouter initialEntries={[path]}>
|
||||
<Route
|
||||
path={Routes.LEARNERS.POSTS}
|
||||
component={LearnerPostFilterBar}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</DiscussionContext.Provider>
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('LearnerPostFilterBar', () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username,
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
const initialData = {
|
||||
config: {
|
||||
hasModerationPrivileges: true,
|
||||
isGroupTa: false,
|
||||
},
|
||||
cohorts: {
|
||||
status: 'successful',
|
||||
cohorts: [
|
||||
{
|
||||
name: 'Default Group',
|
||||
id: 1,
|
||||
userCount: 1,
|
||||
groupId: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
store = initializeStore(initialData);
|
||||
});
|
||||
|
||||
test('checks if all filters are visible', async () => {
|
||||
const { queryAllByRole } = await renderComponent();
|
||||
await act(async () => {
|
||||
fireEvent.click(queryAllByRole('button')[0]);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(queryAllByRole('radiogroup')).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
|
||||
test('checks if default values are selected', async () => {
|
||||
const { queryAllByRole } = await renderComponent();
|
||||
await act(async () => {
|
||||
fireEvent.click(queryAllByRole('button')[0]);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
queryAllByRole('radiogroup')[0].querySelector('input[value="all"]'),
|
||||
).toBeChecked();
|
||||
expect(
|
||||
queryAllByRole('radiogroup')[1].querySelector('input[value="statusAll"]'),
|
||||
).toBeChecked();
|
||||
expect(
|
||||
queryAllByRole('radiogroup')[2].querySelector('input[value="lastActivityAt"]'),
|
||||
).toBeChecked();
|
||||
expect(
|
||||
queryAllByRole('radiogroup')[3].querySelector('input[value=""]'),
|
||||
).toBeChecked();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -61,6 +61,9 @@ const threadsSlice = createSlice({
|
||||
}
|
||||
state.status = RequestStatus.IN_PROGRESS;
|
||||
},
|
||||
clearPostsPages: (state) => {
|
||||
state.pages = [];
|
||||
},
|
||||
fetchThreadsRequest: (state) => {
|
||||
state.status = RequestStatus.IN_PROGRESS;
|
||||
},
|
||||
@@ -242,6 +245,7 @@ export const {
|
||||
showPostEditor,
|
||||
hidePostEditor,
|
||||
clearRedirect,
|
||||
clearPostsPages,
|
||||
} = threadsSlice.actions;
|
||||
|
||||
export const threadsReducer = threadsSlice.reducer;
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
import { selectThreadFilters, selectThreadSorting } from '../data/selectors';
|
||||
import messages from './messages';
|
||||
|
||||
const ActionItem = ({
|
||||
export const ActionItem = ({
|
||||
id,
|
||||
label,
|
||||
value,
|
||||
|
||||
@@ -46,6 +46,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Unanswered',
|
||||
description: 'Option in dropdown to filter to unanswered posts',
|
||||
},
|
||||
filterUnresponded: {
|
||||
id: 'discussions.posts.status.filter.unresponded',
|
||||
defaultMessage: 'Unresponded',
|
||||
description: 'Option in dropdown to filter to unresponded posts',
|
||||
},
|
||||
myPosts: {
|
||||
id: 'discussions.posts.filter.myPosts',
|
||||
defaultMessage: 'My posts',
|
||||
@@ -93,6 +98,7 @@ const messages = defineMessages({
|
||||
statusFollowing {followed}
|
||||
statusReported {reported}
|
||||
statusUnanswered {unanswered}
|
||||
statusUnresponded {unresponded}
|
||||
other {{status}}
|
||||
} {type, select,
|
||||
discussion {discussions}
|
||||
|
||||
Reference in New Issue
Block a user