Skip to content

Commit

Permalink
Task/FP-1375: Admin controlled messages (#615)
Browse files Browse the repository at this point in the history
* Add CustomMessages and CustomMessageTemplate models.

* Add sagas and reducers for custom messages.

* Add frontend components for CustomMessage.

* Add  tests for custom messages.

* Fix tests.

* Fix bug for template state.

* Fix style

* Correct dismissible spelling.

* Refactor to embed template inside message.

* Fix tests.

* Remove TODO comments.

* Revert whitespace changes.

* Revert whitespace changes.

* Allow info messages to have dismiss button.

* Fix linting.

* Remove unused css import.

* Change TextField to CharField for sanity.

* Use class selector instead of attribute selector.

* Refactor messages.

* Fix prettier linting.

* Fix server linting.

* Fix server testing.

* Rename message to portal_messages.

* Fix model.

* Refactor put request for custom messages.

* Refactor introMessageName to messageName.

* Fix prettier linting.

* Fix incorrect import.

* Fix test.

* Simplify custom message payload.

* Rename messageName to introMessageName.

* Fix linting.

* Rename introMessages to introMessageComponents.

* Fix linting.

* Fix backend testing.

Co-authored-by: Sal Tijerina <r.sal.tijerina@gmail.com>
  • Loading branch information
duckonomy and rstijerina committed Jun 2, 2022
1 parent b3aa730 commit 6040fa4
Show file tree
Hide file tree
Showing 47 changed files with 752 additions and 298 deletions.
2 changes: 1 addition & 1 deletion client/src/components/Allocations/AllocationsLayout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export const Layout = ({ page }) => {
return (
<Section
bodyClassName="has-loaded-allocations"
introMessageName="ALLOCATIONS"
messageComponentName="ALLOCATIONS"
header={<Header page={page} />}
headerClassName="allocations-header"
headerActions={<Actions page={page} />}
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/Applications/AppLayout/AppLayout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const AppsRoutes = () => {
return (
<Section
bodyClassName="has-loaded-applications"
introMessageName="APPLICATIONS"
messageComponentName="APPLICATIONS"
header={
<Route path={`${path}/:appId?`}>
<AppsHeader categoryDict={categoryDict} />
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/Dashboard/Dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function Dashboard() {
return (
<Section
bodyClassName="has-loaded-dashboard"
introMessageName="DASHBOARD"
messageComponentName="DASHBOARD"
messages={<BrowserChecker />}
header="Dashboard"
headerActions={
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/DataFiles/DataFiles.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ const DataFiles = () => {
return (
<Section
bodyClassName="has-loaded-datafiles"
introMessageName={
messageComponentName={
listingParams.system === noPHISystem ? 'UNPROTECTED' : 'DATA'
}
header={
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/History/History.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ const Layout = () => {
return (
<Section
bodyClassName="has-loaded-history"
introMessageName="HISTORY"
messageComponentName="HISTORY"
header={`History / ${historyType}`}
headerClassName={styles['header']}
headerActions={<Actions />}
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/ManageAccount/ManageAccount.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const ManageAccountView = () => {
return (
<Section
bodyClassName="has-loaded-account"
introMessageName="ACCOUNT"
messageComponentName="ACCOUNT"
header="Manage Account"
messages={[
!isLoading && (errors.data || errors.fields) && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { initialState as profile } from '../../../redux/reducers/profile.reducer
import { initialState as workbench } from '../../../redux/reducers/workbench.reducers';
import { initialState as notifications } from '../../../redux/reducers/notifications.reducers';
import { initialTicketCreateState as ticketCreate } from '../../../redux/reducers/tickets.reducers';
import introMessages from '../../../redux/reducers/intro.reducers';
import introMessageComponents from '../../../redux/reducers/portalMessages.reducers';
import ManageAccountPage from '../index';

const mockStore = configureStore();
Expand All @@ -24,7 +24,7 @@ describe('Manage Account Page', () => {
config: { hideDataFiles: false },
},
notifications,
introMessages,
introMessageComponents,
ticketCreate,
})}
>
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/Onboarding/OnboardingUser.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const OnboardingUser = () => {

return (
<Section
introMessageName="ONBOARDING"
messageComponentName="ONBOARDING"
header={
isStaff
? `Onboarding Administration for ${user.username} - ${user.lastName}, ${user.firstName}`
Expand Down
6 changes: 4 additions & 2 deletions client/src/components/Tickets/TicketStandaloneCreate.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ function TicketStandaloneCreate() {
const authenticatedUser = useSelector(
(state) => state.authenticatedUser.user
);
const introMessages = useSelector((state) => state.introMessages);
const introMessageComponents = useSelector(
(state) => state.introMessageComponents
);
return (
<>
<Navbar className="ticket-unauthenticated-title">Add Ticket</Navbar>

<div className="ticket-unauthenticated-create-form">
<Alert
isOpen={introMessages.TICKETS}
isOpen={introMessageComponents.TICKETS}
color="secondary"
className="introMessageGeneral"
>
Expand Down
12 changes: 9 additions & 3 deletions client/src/components/Tickets/TicketStandaloneCreate.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import renderComponent from 'utils/testing';
import TicketStandaloneCreate from './TicketStandaloneCreate';
import { initialTicketCreateState as ticketCreate } from '../../redux/reducers/tickets.reducers';
import { initialState as workbench } from '../../redux/reducers/workbench.reducers';
import initialIntroMessages from '../../redux/reducers/intro.reducers';
import initialIntroMessageComponents from '../../redux/reducers/portalMessages.reducers';
import { initialState as user } from '../../redux/reducers/authenticated_user.reducer';

const mockStore = configureStore();
Expand All @@ -15,7 +15,10 @@ describe('TicketStandaloneCreate', () => {
const store = mockStore({
ticketCreate,
authenticatedUser: user,
introMessages: initialIntroMessages,
introMessageComponents: {
...initialIntroMessageComponents,
TICKETS: true,
},
workbench,
});

Expand All @@ -29,7 +32,10 @@ describe('TicketStandaloneCreate', () => {
const store = mockStore({
ticketCreate,
authenticatedUser: user,
introMessages: { ...initialIntroMessages, TICKETS: false },
introMessageComponents: {
...initialIntroMessageComponents,
TICKETS: false,
},
workbench,
});

Expand Down
2 changes: 1 addition & 1 deletion client/src/components/UIPatterns/UIPatterns.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import UIPatternsSidebar from './UIPatternsSidebar';
function UIPatterns() {
return (
<Section
introMessageName="UI"
messageComponentName="UI"
className={styles.container}
header="UI Patterns"
content={
Expand Down
1 change: 1 addition & 0 deletions client/src/components/Workbench/AppRouter.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ function AppRouter() {
useEffect(() => {
if (authenticatedUser) {
dispatch({ type: 'FETCH_INTRO' });
dispatch({ type: 'FETCH_CUSTOM_MESSAGES' });
}
}, [authenticatedUser]);
return (
Expand Down
4 changes: 2 additions & 2 deletions client/src/components/Workbench/Workbench.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
} from '../../redux/reducers/tickets.reducers';
import { initialState as authenticatedUser } from '../../redux/reducers/authenticated_user.reducer';
import { initialState as systemMonitor } from '../../redux/reducers/systemMonitor.reducers';
import { initialIntroMessages as introMessages } from '../../redux/reducers/intro.reducers';
import { initialIntroMessageComponents as introMessageComponents } from '../../redux/reducers/portalMessages.reducers';
import { initialSystemState as systems } from '../../redux/reducers/datafiles.reducers';

import * as introMessageText from '../../constants/messages';
Expand All @@ -25,7 +25,7 @@ const state = {
workbench,
onboarding,
notifications,
introMessages,
introMessageComponents,
jobs,
systemMonitor,
ticketList,
Expand Down
1 change: 1 addition & 0 deletions client/src/components/Workbench/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe('AppRouter', () => {
{ type: 'FETCH_WORKBENCH' },
{ type: 'FETCH_SYSTEMS' },
{ type: 'FETCH_INTRO' },
{ type: 'FETCH_CUSTOM_MESSAGES' },
]);
});
});
64 changes: 64 additions & 0 deletions client/src/components/_common/CustomMessage/CustomMessage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';
import PropTypes from 'prop-types';
import { SectionMessage } from '_common';
import { useDispatch, useSelector } from 'react-redux';
import styles from './CustomMessage.module.scss';

/**
* A message which, is created by the admin. Like IntroMessage, when dismissed, will not appear again.
*
* _This message is designed for custom messages from the admin.
*
* @example
* // message with identifier
* <CustomMessage
* messageComponentName={identifierForMessageLikeRouteName}
* >
* </CustomMessage>
*/
function CustomMessage({ messageComponentName }) {
const dispatch = useDispatch();
const messages = useSelector((state) => {
return state.customMessages
? state.customMessages.messages.filter((message) => {
return (
message.unread &&
message.template.component === messageComponentName
);
})
: [];
});

function onDismiss(dismissMessage) {
const payload = {
templateId: dismissMessage.template.id,
unread: false,
};
dispatch({
type: 'SAVE_CUSTOM_MESSAGES',
payload,
});
}

return messages.map((message) => {
const template = message.template;
return (
<div key={template.id} className={styles.message}>
<SectionMessage
type={template.message_type}
canDismiss={template.dismissible}
onDismiss={() => onDismiss(message)}
>
{template.message}
</SectionMessage>
</div>
);
});
}

CustomMessage.propTypes = {
/** A unique identifier for the message */
messageComponentName: PropTypes.string.isRequired,
};

export default CustomMessage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.message {
margin-top: 12px;
margin-bottom: 12px;
}
41 changes: 41 additions & 0 deletions client/src/components/_common/CustomMessage/CustomMessage.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';
import { render } from '@testing-library/react';
import configureStore from 'redux-mock-store';
import { Provider } from 'react-redux';

import CustomMessage from './CustomMessage';

const mockStore = configureStore();
const store = mockStore({
customMessages: {
messages: [
{
template: {
id: 1,
component: 'TEST',
message_type: 'warning',
dismissible: true,
message: 'Test Message',
},
unread: true,
},
],
},
});

describe('CustomMessage', () => {
describe('elements', () => {
it('renders message text, message type, and dismissability correctly', () => {
const { container, getByText } = render(
<Provider store={store}>
<CustomMessage messageComponentName="TEST"></CustomMessage>
</Provider>
);
expect(container.getElementsByClassName('is-warn').length).toEqual(1);
expect(container.getElementsByClassName('close-button').length).toEqual(
1
);
expect(getByText('Test Message')).not.toEqual(null);
});
});
});
1 change: 1 addition & 0 deletions client/src/components/_common/CustomMessage/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './CustomMessage';
28 changes: 16 additions & 12 deletions client/src/components/_common/IntroMessage/IntroMessage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,39 +8,43 @@ import styles from './IntroMessage.module.css';

/**
* Whether the name is of a known intro message
* @param {String} messageName - The name of the message to check
* @param {String} messageComponentName - The name of the component that contains the message
*/
export function isKnownMessage(messageName) {
const introMessages = useSelector((state) => state.introMessages);
export function isKnownMessage(messageComponentName) {
const introMessageComponents = useSelector(
(state) => state.introMessageComponents
);

return introMessages && introMessages[messageName];
return introMessageComponents && introMessageComponents[messageComponentName];
}

/**
* A message which, when dismissed, will not appear again unless browser storage is cleared
*
* _This message is designed for user introduction to sections, but can be abstracted further into a `<DismissableMessage>` or abstracted less such that a message need not be passed in._
* _This message is designed for user introduction to sections, but can be abstracted further into a `<DismissibleMessage>` or abstracted less such that a message need not be passed in._
*
* @example
* // message with custom text, class, and identifier
* <IntroMessage
* className="external-message-class"
* messageName={identifierForMessageLikeRouteName}
* messageComponentName={identifierForMessageLikeRouteName}
* >
* Introductory text (defined externally).
* </IntroMessage>
*/
function IntroMessage({ children, className, messageName }) {
function IntroMessage({ children, className, messageComponentName }) {
const dispatch = useDispatch();
const introMessages = useSelector((state) => state.introMessages);
const shouldShow = isKnownMessage(messageName);
const introMessageComponents = useSelector(
(state) => state.introMessageComponents
);
const shouldShow = isKnownMessage(messageComponentName);
const [isVisible, setIsVisible] = useState(shouldShow);

// Manage visibility
const onDismiss = useCallback(() => {
const newMessagesState = {
...introMessages,
[messageName]: false,
...introMessageComponents,
[messageComponentName]: false,
};
dispatch({ type: 'SAVE_INTRO', payload: newMessagesState });

Expand All @@ -49,7 +53,7 @@ function IntroMessage({ children, className, messageName }) {

return (
<SectionMessage
aria-label={messageName}
aria-label={messageComponentName}
type="info"
canDismiss
className={`${styles.root} ${className}`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe('IntroMessage', () => {
it('includes class, message, and role appropriately', () => {
const { container, getByRole, getByText } = render(
<Provider store={store}>
<IntroMessage className="test-class" messageName="TEST">
<IntroMessage className="test-class" messageComponentName="TEST">
<p>Test Message</p>
</IntroMessage>
</Provider>
Expand Down
Loading

0 comments on commit 6040fa4

Please sign in to comment.