Compare commits

..

4 Commits

Author SHA1 Message Date
adeel.tajamul
65a5fa2e37 fix: copy link not working in in-context discussion 2023-02-16 12:10:12 +05:00
Mehak Nasir
f8800de766 feat: added delay in post preview to avoid flashing of content 2023-02-15 17:06:48 +05:00
ayesha waris
f7740de54a fix: topic info is fixed from posts in incontext discussions (#433) 2023-02-14 18:17:53 +05:00
Mehak Nasir
4ca25c14d9 fix: restrict tiny mce from converting url 2023-02-14 17:35:37 +05:00
6 changed files with 36 additions and 203 deletions

View File

@@ -5,6 +5,8 @@ import DOMPurify from 'dompurify';
import { logError } from '@edx/frontend-platform/logging';
import { useDebounce } from '../discussions/data/hooks';
const defaultSanitizeOptions = {
USE_PROFILES: { html: true },
ADD_ATTR: ['columnalign'],
@@ -16,19 +18,21 @@ function HTMLLoader({
const sanitizedMath = DOMPurify.sanitize(htmlNode, { ...defaultSanitizeOptions });
const previewRef = useRef();
const debouncedPostContent = useDebounce(htmlNode, 500);
useEffect(() => {
let promise = Promise.resolve(); // Used to hold chain of typesetting calls
function typeset(code) {
promise = promise.then(() => window.MathJax?.typesetPromise(code()))
.catch((err) => logError(`Typeset failed: ${err.message}`));
return promise;
}
typeset(() => {
previewRef.current.innerHTML = sanitizedMath;
});
}, [htmlNode]);
if (debouncedPostContent) {
typeset(() => {
previewRef.current.innerHTML = sanitizedMath;
});
}
}, [debouncedPostContent]);
return (
<div ref={previewRef} className={cssClassName} id={componentId} data-testid={testId} />

View File

@@ -119,6 +119,7 @@ export default function TinyMCEEditor(props) {
content_css: false,
content_style: contentStyle,
body_class: 'm-2 text-editor',
convert_urls: false,
relative_urls: false,
default_link_target: '_blank',
target_list: false,

View File

@@ -224,3 +224,24 @@ export const useTourConfiguration = (intl) => {
}
));
};
export const useDebounce = (value, delay) => {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(
() => {
// Update debounced value after delay
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cancel the timeout if value changes (also on delay change or unmount)
// This is how we prevent debounced value from updating if value is changed ...
// .. within the delay period. Timeout gets cleared and restarted.
return () => {
clearTimeout(handler);
};
},
[value, delay], // Only re-call effect if value or delay changes
);
return debouncedValue;
};

View File

@@ -1,193 +0,0 @@
import {
fireEvent, render, screen, waitFor,
within,
} from '@testing-library/react';
import MockAdapter from 'axios-mock-adapter';
import { act } from 'react-dom/test-utils';
import { IntlProvider } from 'react-intl';
import { MemoryRouter, Route } from 'react-router';
import { Factory } from 'rosie';
import { initializeMockApp } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { AppProvider } from '@edx/frontend-platform/react';
import { initializeStore } from '../../store';
import { executeThunk } from '../../test-utils';
import { DiscussionContext } from '../common/context';
import { getCourseTopicsApiUrl } from './data/api';
import { selectCoursewareTopics, selectNonCoursewareTopics } from './data/selectors';
import { fetchCourseTopicsV3 } from './data/thunks';
import TopicsView from './TopicsView';
import './data/__factories__';
const courseId = 'course-v1:edX+DemoX+Demo_Course';
const topicsApiUrl = `${getCourseTopicsApiUrl()}`;
let store;
let axiosMock;
let lastLocation;
let component;
let debug;
function renderComponent() {
component = render(
<IntlProvider locale="en">
<AppProvider store={store}>
<DiscussionContext.Provider value={{ courseId }}>
<MemoryRouter initialEntries={[`/${courseId}/topics/`]}>
<Route path="/:courseId/topics/">
<TopicsView />
</Route>
<Route path="/:courseId/category/:category">
<TopicsView />
</Route>
<Route
render={({ location }) => {
lastLocation = location;
return null;
}}
/>
</MemoryRouter>
</DiscussionContext.Provider>
</AppProvider>
</IntlProvider>,
);
debug = component.debug;
}
describe('Legacy Topics View', () => {
let nonCoursewareTopics;
let coursewareTopics;
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
store = initializeStore({
config: { enableInContext: true, provider: 'openedx', hasModerationPrivileges: true },
});
Factory.resetAll();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
lastLocation = undefined;
});
async function setupMockResponse() {
axiosMock.onGet(`${topicsApiUrl}${courseId}`)
.reply(200, (Factory.buildList('topic', 1, null, {
topicPrefix: 'noncourseware-topic',
enabledInContext: true,
topicNamePrefix: 'general-topic',
usageKey: '',
courseware: false,
discussionCount: 1,
questionCount: 1,
}).concat(Factory.buildList('section', 2, null, { topicPrefix: 'courseware' })))
.concat(Factory.buildList('archived-topics', 2, null)));
await executeThunk(fetchCourseTopicsV3(courseId), store.dispatch, store.getState);
const state = store.getState();
nonCoursewareTopics = selectNonCoursewareTopics(state);
coursewareTopics = selectCoursewareTopics(state);
}
it('A non-courseware topic should be clickable and should have a title', async () => {
await setupMockResponse();
renderComponent();
nonCoursewareTopics.forEach(async topic => {
expect(screen.queryByText(topic.name)).toBeInTheDocument();
const noncoursewareTopic = await screen.findByText(topic.name);
fireEvent.click(noncoursewareTopic);
});
});
it('A non-courseware topic should be on the top of the list', async () => {
await setupMockResponse();
renderComponent();
const topicsList = await screen.getByRole('list');
const { firstChild } = topicsList;
expect(within(firstChild).queryByText(nonCoursewareTopics[0].name)).toBeInTheDocument();
expect(topicsList.children[1]).toBe(topicsList.querySelector('.divider'));
});
it('A non-Courseware topic should have 3 stats and should be hoverable', async () => {
await setupMockResponse();
renderComponent();
const topicsList = await screen.getByRole('list');
const { firstChild } = topicsList;
const statsList = await firstChild.getElementsByClassName('pgn__icon');
fireEvent.mouseOver(statsList[0]);
expect(statsList.length).toBe(3);
expect(screen.queryByText('1 Discussion')).toBeInTheDocument();
});
it('Section groups should be listed in the middle of the topics list.', async () => {
await setupMockResponse();
renderComponent();
const topicsList = await screen.getByRole('list');
const sectionGroups = await screen.getAllByTestId('section-group');
expect(topicsList.children[1]).toStrictEqual(topicsList.querySelector('.divider'));
expect(sectionGroups.length).toBe(2);
expect(topicsList.children[5]).toStrictEqual(topicsList.querySelector('.divider'));
});
it('A section group should have only a title and required subsections.', async () => {
await setupMockResponse();
renderComponent();
const sectionGroups = await screen.getAllByTestId('section-group');
coursewareTopics.forEach(async (topic, index) => {
const statsList = await sectionGroups[index].getElementsByClassName('pgn__icon');
const subsectionGroups = await within(sectionGroups[index]).getAllByTestId('subsection-group');
expect(within(sectionGroups[index]).queryByText(topic.displayName)).toBeInTheDocument();
expect(statsList).toHaveLength(0);
expect(subsectionGroups).toHaveLength(2);
});
});
it('The subsection should have a title name, be clickable, and not have the stats', async () => {
await setupMockResponse();
renderComponent();
const sectionGroups = await screen.getAllByTestId('section-group');
coursewareTopics.forEach(async (topic, index) => {
const subsectionGroups = await within(sectionGroups[index]).getAllByTestId('subsection-group');
topic.children.forEach(async (subsection, i) => {
const statsList = await subsectionGroups[i].getElementsByClassName('pgn__icon');
expect(await within(subsectionGroups[i]).queryByText(subsection.displayName)).toBeInTheDocument();
const subsectionTopic = await screen.findByText(subsection.displayName);
fireEvent.click(subsectionTopic);
expect(statsList).toHaveLength(0);
});
});
});
it('Subsection names should be clickable and redirected to the units lists', async () => {
await setupMockResponse();
renderComponent();
const subsectionGroup = await screen.getAllByTestId('subsection-group');
await act(async () => {
fireEvent.click(subsectionGroup[0]);
// debug();
});
await waitFor(async () => {
debug();
// expect(screen.queryByText(coursewareTopics[0].children[0].name)).toBeInTheDocument();
// const backButton = screen.getByRole('button', { type: 'button' });
// expect(backButton).toBeInTheDocument();
// const ele = await screen.getByLabelText('Back to topics list');
// expect(ele).toBeInTheDocument();
});
});
});

View File

@@ -8,7 +8,7 @@ Factory.define('topic')
.sequence('name', ['topicNamePrefix'], (idx, topicNamePrefix) => `${topicNamePrefix}-${idx}`)
.sequence('usage-key', ['usageKey'], (idx, usageKey) => usageKey)
.sequence('courseware', ['courseware'], (idx, courseware) => courseware)
.attr('activeFlags', null, true)
.attr('thread_counts', ['discussionCount', 'questionCount'], (discCount, questCount) => {
Factory.reset('thread-counts');
return Factory.build('thread-counts', null, { discussionCount: discCount, questionCount: questCount });

View File

@@ -33,7 +33,7 @@ function Post({
const history = useHistory();
const dispatch = useDispatch();
const { enableInContextSidebar } = useContext(DiscussionContext);
const { courseId } = useSelector((state) => state.courseTabs);
const courseId = useSelector((state) => state.config.id);
const topic = useSelector(selectTopic(post.topicId));
const getTopicSubsection = useSelector(selectorForUnitSubsection);
const topicContext = useSelector(selectTopicContext(post.topicId));
@@ -126,7 +126,7 @@ function Post({
<div className="d-flex mt-14px text-break font-style text-primary-500">
<HTMLLoader htmlNode={post.renderedBody} componentId="post" cssClassName="html-loader" testId={post.id} />
</div>
{topicContext && topic && (
{topicContext && (
<div
className={classNames('mt-14px mb-1 font-style font-size-12',
{ 'w-100': enableInContextSidebar })}
@@ -137,7 +137,7 @@ function Post({
destination={topicContext.unitLink}
target="_top"
>
{enableInContextSidebar
{(topicContext && !topic)
? (
<>
<span className="w-auto">{topicContext.chapterName}</span>