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 || '', }); }, },