diff --git a/.env b/.env
index 9fc7c719..2edd8e4a 100644
--- a/.env
+++ b/.env
@@ -19,3 +19,4 @@ REFRESH_ACCESS_TOKEN_ENDPOINT=''
SEGMENT_KEY=''
SITE_NAME=''
USER_INFO_COOKIE_NAME=''
+FEEDER_PROJECT_ID=''
diff --git a/.env.development b/.env.development
index 5266dd5d..59b14cfc 100644
--- a/.env.development
+++ b/.env.development
@@ -20,3 +20,4 @@ REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
SEGMENT_KEY=''
SITE_NAME=localhost
USER_INFO_COOKIE_NAME='edx-user-info'
+FEEDER_PROJECT_ID=''
diff --git a/.env.test b/.env.test
index e6a9bc01..065e8ac4 100644
--- a/.env.test
+++ b/.env.test
@@ -18,3 +18,4 @@ REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
SEGMENT_KEY=''
SITE_NAME=localhost
USER_INFO_COOKIE_NAME='edx-user-info'
+FEEDER_PROJECT_ID=''
diff --git a/package-lock.json b/package-lock.json
index 603be7a9..ffb4bc6e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7808,6 +7808,11 @@
"integrity": "sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q==",
"dev": true
},
+ "feeder-react-feedback": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/feeder-react-feedback/-/feeder-react-feedback-0.0.6.tgz",
+ "integrity": "sha512-BdilgLncuOdoFFC0Da3wYMIYODSxZaH3vUS4662QUTzKOGrGrc63yDywRFw2x4b30Kie3kwD4z3rXcaTznq7Mg=="
+ },
"figures": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
diff --git a/package.json b/package.json
index d552dc26..8ecac840 100644
--- a/package.json
+++ b/package.json
@@ -45,6 +45,7 @@
"babel-polyfill": "6.26.0",
"classnames": "2.3.1",
"core-js": "3.19.1",
+ "feeder-react-feedback": "0.0.6",
"formik": "2.2.9",
"lodash.snakecase": "4.1.1",
"prop-types": "15.7.2",
diff --git a/src/components/Feedback/constants.js b/src/components/Feedback/constants.js
new file mode 100644
index 00000000..2d12c187
--- /dev/null
+++ b/src/components/Feedback/constants.js
@@ -0,0 +1,2 @@
+export const FeedbackTypes = ['Review', 'Bug', 'Idea'];
+export const primaryColor = '#00262b';
diff --git a/src/components/Feedback/feeder.scss b/src/components/Feedback/feeder.scss
new file mode 100644
index 00000000..5dd44fab
--- /dev/null
+++ b/src/components/Feedback/feeder.scss
@@ -0,0 +1,103 @@
+@import "feeder-react-feedback/dist/feeder-react-feedback.css";
+
+.frf-feedback-container {
+ position: unset;
+ bottom: unset;
+ z-index: unset;
+ width: unset;
+ right: unset;
+}
+
+#feedbackEmail {
+ display: none;
+}
+
+div[for="feedbackEmail"] {
+ display: none;
+}
+
+.frf-trigger-button {
+ position: unset;
+ box-shadow: unset;
+}
+
+.frf-feedback-icon::after {
+ content: "Give Feedback" !important;
+ margin-left: 8px !important;
+ font-size: unset !important;
+}
+
+.frf-feedback-icon-open:after {
+ content: "Close" !important;
+ font-size: inherit !important;
+}
+
+.frf-modal-container {
+ right: unset;
+ bottom: unset;
+}
+
+.frf-dialog-enter-active {
+ animation-name: unset;
+ animation-duration: unset;
+ opacity: unset;
+ transition: unset;
+}
+
+.frf-dialog-enter {
+ opacity: unset;
+}
+
+.frf-dialog-exit-active {
+ animation-name: unset;
+ animation-duration: unset;
+ opacity: unset;
+ transition: unset;
+}
+
+.frf-dialog-exit {
+ opacity: 0;
+}
+
+.frf-water {
+ display: none;
+}
+
+@media (hover: hover) {
+ .frf-trigger-button {
+ transition: unset;
+
+ &:hover {
+ .frf-feedback-icon:after {
+ content: unset;
+ margin-left: unset;
+ font-size: unset;
+ opacity: unset;
+ transition: unset;
+ }
+
+ .frf-feedback-icon-open:after {
+ content: unset;
+ font-size: unset;
+ opacity: unset;
+ transition: unset;
+ }
+ }
+
+ .frf-feedback-icon:after {
+ content: unset;
+ padding-left: unset;
+ font-size: unset;
+ opacity: unset;
+ transition: unset;
+ }
+
+ .frf-feedback-icon-open:after {
+ content: unset;
+ padding-left: unset;
+ font-size: unset;
+ opacity: unset;
+ transition: unset;
+ }
+ }
+}
diff --git a/src/components/Feedback/index.jsx b/src/components/Feedback/index.jsx
new file mode 100644
index 00000000..d202bcfe
--- /dev/null
+++ b/src/components/Feedback/index.jsx
@@ -0,0 +1,40 @@
+import React from 'react';
+
+import Feedback from 'feeder-react-feedback';
+
+import { ensureConfig, getConfig } from '@edx/frontend-platform';
+import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+
+import { FeedbackTypes, primaryColor } from './constants';
+import messages from './messages';
+
+import './feeder.scss';
+
+ensureConfig(['FEEDER_PROJECT_ID'], 'Feedback');
+
+const Index = ({ intl, ...props }) => {
+ const { email } = getAuthenticatedUser();
+ const projectId = getConfig().FEEDER_PROJECT_ID;
+
+ return (
+ projectId && (
+
+ )
+ );
+};
+
+Index.propTypes = {
+ intl: intlShape.isRequired,
+};
+
+export default injectIntl(Index);
diff --git a/src/components/Feedback/messages.js b/src/components/Feedback/messages.js
new file mode 100644
index 00000000..250858ed
--- /dev/null
+++ b/src/components/Feedback/messages.js
@@ -0,0 +1,16 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ submitButton: {
+ id: 'feeder.submit.button',
+ defaultMessage: 'Send Feedback',
+ description: 'Alt text for feedback submission button.',
+ },
+ postSubmitButton: {
+ id: 'feeder.post.submit.button',
+ defaultMessage: 'Thank You For The Feedback!',
+ description: 'Alt text for feedback post submission button',
+ },
+});
+
+export default messages;
diff --git a/src/discussions/discussions-home/DiscussionsHome.test.jsx b/src/discussions/discussions-home/DiscussionsHome.test.jsx
index 69317335..a57da1bf 100644
--- a/src/discussions/discussions-home/DiscussionsHome.test.jsx
+++ b/src/discussions/discussions-home/DiscussionsHome.test.jsx
@@ -3,7 +3,7 @@ import { IntlProvider } from 'react-intl';
import { Context as ResponsiveContext } from 'react-responsive';
import { MemoryRouter } from 'react-router';
-import { initializeMockApp } from '@edx/frontend-platform';
+import { getConfig, initializeMockApp, setConfig } from '@edx/frontend-platform';
import { AppProvider } from '@edx/frontend-platform/react';
import { initializeStore } from '../../store';
@@ -29,10 +29,16 @@ function renderComponent(location = `/${courseId}/`) {
describe('DiscussionsHome', () => {
beforeEach(async () => {
+ setConfig({
+ ...getConfig(),
+ FEEDER_PROJECT_ID: 'test-id',
+ });
+
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
+ email: 'abc123@example.com',
administrator: true,
roles: [],
},
@@ -50,7 +56,7 @@ describe('DiscussionsHome', () => {
await screen.findByTestId('topics-view');
});
- test('full view should show header and footer and hide close button', async () => {
+ test('full view should show header, footer and feedback button and hide close button', async () => {
renderComponent(`/${courseId}/topics`);
expect(screen.queryByText(navigationBarMessages.allTopics.defaultMessage))
.toBeInTheDocument();
@@ -65,9 +71,11 @@ describe('DiscussionsHome', () => {
// Footer should be visible
expect(screen.queryByRole('contentinfo'))
.toBeInTheDocument();
+
+ expect(screen.queryByTestId('feedback').childElementCount).toEqual(1);
});
- test('in-context view should hide header and footer and show close button', async () => {
+ test('in-context view should hide header and footer and show close and feedback button', async () => {
renderComponent(`/${courseId}/topics?inContext`);
expect(screen.queryByText(navigationBarMessages.allTopics.defaultMessage))
@@ -85,5 +93,6 @@ describe('DiscussionsHome', () => {
expect(screen.queryByRole('contentinfo'))
.not
.toBeInTheDocument();
+ expect(screen.queryByTestId('feedback').childElementCount).toEqual(1);
});
});
diff --git a/src/discussions/posts/post-actions-bar/PostActionsBar.jsx b/src/discussions/posts/post-actions-bar/PostActionsBar.jsx
index 4510953c..50f2336a 100644
--- a/src/discussions/posts/post-actions-bar/PostActionsBar.jsx
+++ b/src/discussions/posts/post-actions-bar/PostActionsBar.jsx
@@ -11,6 +11,7 @@ import {
} from '@edx/paragon';
import { Close } from '@edx/paragon/icons';
+import Feedback from '../../../components/Feedback';
import { Routes } from '../../../data/constants';
import { DiscussionContext } from '../../common/context';
import { discussionsPath } from '../../utils';
@@ -29,6 +30,9 @@ function PostActionsBar({
const handleCloseInContext = () => null;
return (
+
+
+
{!inContext && (
<>
+
+
+
+
+
+ ,
+ );
+}
+
+describe.each([
+ { inContext: false },
+ { inContext: true },
+])('PostActionsBar', ({ inContext }) => {
+ beforeEach(async () => {
+ setConfig({
+ ...getConfig(),
+ FEEDER_PROJECT_ID: 'test-id',
+ });
+
+ initializeMockApp({
+ authenticatedUser: {
+ userId: 3,
+ username: 'abc123',
+ email: 'abc123@example.com',
+ administrator: true,
+ roles: [],
+ },
+ });
+
+ store = initializeStore();
+ });
+
+ test(`'full view should show feedback, add post, ${inContext ? 'close button and title and hide searchbar'
+ : 'searchbar and hide title and close button'} when inContext is ${inContext}`, () => {
+ renderComponent(inContext);
+
+ expect(screen.queryByTestId('feedback').childElementCount).toEqual(1);
+ expect(screen.queryByRole('button', { name: 'Add a post' })).toBeInTheDocument();
+
+ if (inContext) {
+ expect(screen.queryByPlaceholderText(messages.searchAllPosts.defaultMessage)).not.toBeInTheDocument();
+ expect(screen.queryByText(messages.title.defaultMessage)).toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: 'Close' })).toBeInTheDocument();
+ } else {
+ expect(screen.queryByPlaceholderText(messages.searchAllPosts.defaultMessage)).toBeInTheDocument();
+ expect(screen.queryByText(messages.title.defaultMessage)).not.toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: 'Close' })).not.toBeInTheDocument();
+ }
+ });
+});
diff --git a/src/index.jsx b/src/index.jsx
index e5469974..4a40955a 100755
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -42,6 +42,7 @@ initialize({
config() {
mergeConfig({
POST_MARK_AS_READ_DELAY: process.env.POST_MARK_AS_READ_DELAY || 2000,
+ FEEDER_PROJECT_ID: process.env.FEEDER_PROJECT_ID || '',
});
},
},