.label.label-id-0.selected{background-color:undefined}
- .label.label-id-0.selected:not(.no-hover):hover,
+ .label.label-id-0.selected:not(.disabled):not(.no-hover):hover,
.label.label-id-0:not(.selected)
{background-color:white;border-color:undefined;color:undefined}
- .label.label-id-0:not(.no-hover):hover{background-color:black;color:white}
+ .label.label-id-0:not(.disabled):not(.no-hover):hover
+ {background-color:black;color:white}
.label.label-id-4.selected{background-color:undefined}
- .label.label-id-4.selected:not(.no-hover):hover,
+ .label.label-id-4.selected:not(.disabled):not(.no-hover):hover,
.label.label-id-4:not(.selected)
{background-color:white;border-color:undefined;color:undefined}
- .label.label-id-4:not(.no-hover):hover{background-color:black;color:white}
+ .label.label-id-4:not(.disabled):not(.no-hover):hover
+ {background-color:black;color:white}
.label.label-id-3.selected{background-color:undefined}
- .label.label-id-3.selected:not(.no-hover):hover,
+ .label.label-id-3.selected:not(.disabled):not(.no-hover):hover,
.label.label-id-3:not(.selected)
{background-color:white;border-color:undefined;color:undefined}
- .label.label-id-3:not(.no-hover):hover{background-color:black;color:white}
+ .label.label-id-3:not(.disabled):not(.no-hover):hover
+ {background-color:black;color:white}
.label.label-id-2.selected{background-color:undefined}
- .label.label-id-2.selected:not(.no-hover):hover,
+ .label.label-id-2.selected:not(.disabled):not(.no-hover):hover,
.label.label-id-2:not(.selected)
{background-color:white;border-color:undefined;color:undefined}
- .label.label-id-2:not(.no-hover):hover{background-color:black;color:white}
+ .label.label-id-2:not(.disabled):not(.no-hover):hover
+ {background-color:black;color:white}
.label.label-id-1.selected{background-color:undefined}
- .label.label-id-1.selected:not(.no-hover):hover,
+ .label.label-id-1.selected:not(.disabled):not(.no-hover):hover,
.label.label-id-1:not(.selected)
{background-color:white;border-color:undefined;color:undefined}
- .label.label-id-1:not(.no-hover):hover{background-color:black;color:white}
+ .label.label-id-1:not(.disabled):not(.no-hover):hover
+ {background-color:black;color:white}
`;
-
-exports[`LabelPane with selected labels 1`] = `
-
-
-
-
-
-
-
- first
-
-
-
-
-
- second
-
-
-
-`;
diff --git a/src/__test__/add-edit-page.test.jsx b/src/__test__/add-edit-page.test.jsx
index b4376eb..80da549 100644
--- a/src/__test__/add-edit-page.test.jsx
+++ b/src/__test__/add-edit-page.test.jsx
@@ -5,45 +5,68 @@ import moment from 'moment';
import AddEditEventPage from '../pages/add-edit/add-edit-page';
describe('AddEditEventPage', () => {
- // TODO: test the snapshot. This requires mocking moment or
- // EventDateTimeSelector, or modifying the latter to accept the datetime as
- // a property.
- test.skip('matches snapshot', () => {
- const event = {
- title: 'An event title',
- labels: ['label'],
- start: moment('2018-05-04T09:00:00Z'),
- end: moment('2018-05-04T10:00:00Z'),
- };
+ const userAccount = {
+ scope: new Map(),
+ };
+ const adminAccount = {
+ scope: new Map(),
+ };
+ const event = {
+ title: 'An event title',
+ description: 'some *markdown*',
+ labels: ['label'],
+ start: moment('2018-05-04T09:00:00Z'),
+ end: moment('2018-05-04T10:00:00Z'),
+ };
+ const sidebarFunctions = {
+ setPageTitlePrefix: () => undefined,
+ setSidebarMode: () => undefined,
+ toggleSidebarCollapsed: () => undefined,
+ };
+
+ test('new event', () => {
+ const component = renderer.create(
);
+ const tree = component.toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+ test('signed-in user', () => {
+ const component = renderer.create(
);
+ const tree = component.toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+ test('admin', () => {
const component = renderer.create(
undefined}
- setSidebarMode={() => undefined}
- toggleSidebarCollapsed={() => undefined}
+ {...sidebarFunctions}
/>);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
test('validateInput', () => {
- const event = {
- title: 'An event title',
- labels: [''],
- start: moment('2018-05-04T09:00:00Z'),
- end: moment('2018-05-04T10:00:00Z'),
- };
- const wrapper = shallow( undefined}
- setSidebarMode={() => undefined}
- toggleSidebarCollapsed={() => undefined}
- />);
- expect(wrapper.instance().validateInput()).toBe(false);
- wrapper.instance().titleChanged({ currentTarget: { value: 'Title' } });
- expect(wrapper.instance().validateInput()).toBe(false);
- wrapper.instance().labelToggled('label-1');
- expect(wrapper.instance().validateInput()).toBe(true);
+ {...sidebarFunctions}
+ />).instance();
+ expect(instance.validateInput()).toBe(false);
+ instance.titleChanged({ currentTarget: { value: 'Title' } });
+ expect(instance.validateInput()).toBe(false);
+ instance.labelToggled('label-1');
+ expect(instance.validateInput()).toBe(true);
+ instance.saveButtonClicked();
});
});
diff --git a/src/__test__/auth.test.js b/src/__test__/auth.test.js
index 831b259..f08b435 100644
--- a/src/__test__/auth.test.js
+++ b/src/__test__/auth.test.js
@@ -1,4 +1,19 @@
-import { removeOauthParams } from '../data/auth';
+import {
+ canSignOut,
+ initializeAccessToken,
+ clearAccessToken,
+ removeOauthParams,
+} from '../data/auth';
+
+describe('access tokens', () => {
+ test('sequence', () => {
+ localStorage.abeAccessToken = 'token';
+ initializeAccessToken();
+ expect(canSignOut()).toEqual(true);
+ clearAccessToken();
+ expect(canSignOut()).toEqual(true);
+ });
+});
/* eslint-disable max-len */
describe('removeOauthParams', () => {
diff --git a/src/__test__/calendar-container.test.js b/src/__test__/calendar-container.test.js
new file mode 100644
index 0000000..24c1062
--- /dev/null
+++ b/src/__test__/calendar-container.test.js
@@ -0,0 +1,85 @@
+import { getVisibleEvents } from '../containers/calendar-container';
+
+describe('getVisibleEvents', () => {
+ const events = [
+ { name: 'A', id: 1, labels: ['a'] },
+ { name: 'B', id: 2, labels: ['b'] },
+ { name: 'C', sid: 3, labels: ['a', 'b'] },
+ ];
+ const allLabels = {
+ a: { color: 'red' },
+ b: { color: 'green' },
+ c: { color: 'blue' },
+ };
+ const labels = Object.keys(allLabels);
+ test('waits for data', () => {
+ expect(getVisibleEvents(null, labels, allLabels)).toEqual(null);
+ expect(getVisibleEvents(events, null, allLabels)).toEqual(null);
+ expect(getVisibleEvents(events, labels, null)).toEqual(null);
+ });
+ test('assigns ids', () => {
+ const es = getVisibleEvents(events, labels, allLabels);
+ expect(es[0].id).toEqual(1);
+ expect(es[1].id).toEqual(2);
+ expect(es[2].id).toEqual(3);
+ });
+ test('assigns color', () => {
+ const es = getVisibleEvents(events, labels, allLabels);
+ expect(es[0].color).toEqual('red');
+ expect(es[1].color).toEqual('green');
+ expect(es[2].color).toEqual('red');
+ });
+ test('filters labels', () => {
+ const es1 = getVisibleEvents(events, ['a'], allLabels);
+ expect(es1.length).toEqual(2);
+ expect(es1[0].id).toEqual(1);
+ expect(es1[1].id).toEqual(3);
+
+ const es2 = getVisibleEvents(events, ['b'], allLabels);
+ expect(es2.length).toEqual(2);
+ expect(es2[0].id).toEqual(2);
+ expect(es2[1].id).toEqual(3);
+ });
+});
+
+describe('getVisibleEvents', () => {
+ const events = [
+ { name: 'A', id: 1, labels: ['a'] },
+ { name: 'B', id: 2, labels: ['b'] },
+ { name: 'C', sid: 3, labels: ['a', 'b'] },
+ ];
+ const allLabels = {
+ a: { color: 'red' },
+ b: { color: 'green' },
+ c: { color: 'blue' },
+ };
+ const labels = Object.keys(allLabels);
+ test('waits for data', () => {
+ expect(getVisibleEvents(null, labels, allLabels)).toEqual(null);
+ expect(getVisibleEvents(events, null, allLabels)).toEqual(null);
+ expect(getVisibleEvents(events, labels, null)).toEqual(null);
+ });
+ test('assigns ids', () => {
+ const es = getVisibleEvents(events, labels, allLabels);
+ expect(es[0].id).toEqual(1);
+ expect(es[1].id).toEqual(2);
+ expect(es[2].id).toEqual(3);
+ });
+ test('assigns color', () => {
+ const es = getVisibleEvents(events, labels, allLabels);
+ expect(es[0].color).toEqual('red');
+ expect(es[1].color).toEqual('green');
+ expect(es[2].color).toEqual('red');
+ });
+ test('filters labels', () => {
+ const es1 = getVisibleEvents(events, ['a'], allLabels);
+ expect(es1.length).toEqual(2);
+ expect(es1[0].id).toEqual(1);
+ expect(es1[1].id).toEqual(3);
+
+ const es2 = getVisibleEvents(events, ['b'], allLabels);
+ expect(es2.length).toEqual(2);
+ expect(es2[0].id).toEqual(2);
+ expect(es2[1].id).toEqual(3);
+ });
+});
diff --git a/src/__test__/event-details-page.test.jsx b/src/__test__/event-details-page.test.jsx
index 2e15f37..82e591e 100644
--- a/src/__test__/event-details-page.test.jsx
+++ b/src/__test__/event-details-page.test.jsx
@@ -4,7 +4,6 @@ import renderer from 'react-test-renderer';
import EventDetailsPage from '../pages/details/event-details-page';
describe('EventDetailsPage', () => {
- moment.tz.setDefault('EST');
test('loading', () => {
const jsx = undefined} />;
const component = renderer.create(jsx);
diff --git a/src/__test__/label-pane.test.jsx b/src/__test__/label-pane.test.jsx
index 61cbb1c..d85b8c4 100644
--- a/src/__test__/label-pane.test.jsx
+++ b/src/__test__/label-pane.test.jsx
@@ -15,32 +15,37 @@ describe('LabelPane', () => {
id: 'label-2',
color: 'green',
description: 'second label',
+ protected: true,
},
};
- test('matches snapshot', () => {
- const component = renderer.create();
+ test('matches baseline', () => {
+ const jsx = ;
+ const component = renderer.create(jsx);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
- test('with selected labels', () => {
- const component = renderer.create();
+ test('shows selected labels', () => {
+ const jsx = ;
+ const component = renderer.create(jsx);
+ const tree = component.toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+ test('hides unselected labels', () => {
+ const jsx = (
+
+ );
+ const component = renderer.create(jsx);
+ const tree = component.toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+ test('disables protected labels', () => {
+ const jsx = ;
+ const component = renderer.create(jsx);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
test('not editable', () => {
- const component = renderer.create();
+ const component = renderer.create();
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
diff --git a/src/__test__/setup.js b/src/__test__/setup.js
index d55298f..e5c9d56 100644
--- a/src/__test__/setup.js
+++ b/src/__test__/setup.js
@@ -8,3 +8,27 @@ jest.mock('moment', () => {
moment.tz.setDefault('America/New_York');
return moment;
});
+
+class LocalStorageMock {
+ constructor() {
+ this.store = {};
+ }
+
+ clear() {
+ this.store = {};
+ }
+
+ getItem(key) {
+ return this.store[key] || null;
+ }
+
+ setItem(key, value) {
+ this.store[key] = value.toString();
+ }
+
+ removeItem(key) {
+ delete this.store[key];
+ }
+}
+
+global.localStorage = new LocalStorageMock();
diff --git a/src/components/label-pane.jsx b/src/components/label-pane.jsx
index 8b929cb..4049f90 100644
--- a/src/components/label-pane.jsx
+++ b/src/components/label-pane.jsx
@@ -13,8 +13,11 @@ export default class LabelPane extends React.Component {
}
render() {
- const { editable, selectedLabels, showUnselected } = this.props;
- const enableHoverStyle = !this.props.general.isMobile && this.props.editable;
+ const { props } = this;
+ const {
+ disableProtectedLabels, editable, selectedLabels, showUnselected,
+ } = this.props;
+ const enableHoverStyle = !this.props.general.isMobile && props.editable;
const noHoverClass = enableHoverStyle ? '' : 'no-hover';
// sort Featured first; then remaining default labels, alphabetically within
// each section
@@ -23,16 +26,30 @@ export default class LabelPane extends React.Component {
.sortBy(label => !label.default)
.sortBy(label => !initialLabels.includes(label.name.toLowerCase()))
.value();
+ if (disableProtectedLabels) {
+ labels = _.sortBy(labels, label => Boolean(label.protected));
+ }
if (!showUnselected) {
labels = labels.filter(({ name }) => selectedLabels.includes(name));
}
- const labelClicked = labelName => this.props.labelToggled(labelName);
+ const labelClicked = labelName => props.labelToggled(labelName);
+ const visibilityIcons = {
+ public: 'ion-android-people',
+ olin: null,
+ students: 'ion-university',
+ };
function renderLabel(label) {
const { description: tooltip, name, id } = label;
const selected = selectedLabels.includes(name);
const cssId = `label-${id}`;
- const classes = `label ${cssId} ${selected ? 'selected' : ''}`;
+ const disabled = disableProtectedLabels && label.protected;
+ const classes = `label ${cssId} ${selected ? 'selected' : ''} ${disabled ? 'disabled' : ''}`;
+ const iconClass =
+ disableProtectedLabels && label.protected
+ ? 'ion-android-lock'
+ : visibilityIcons[label.visibility] || 'ion-pricetag';
+ const icon = ;
return editable ? (
labelClicked(name)}
+ onClick={() => !disabled && labelClicked(name)}
>
-
+ {icon}
{name}
) : (
-
+ {icon}
{name}
);
@@ -63,10 +80,11 @@ export default class LabelPane extends React.Component {
// snapshots.
return [
`${sel}.selected{background-color:${color}}`,
- `${sel}.selected:not(.no-hover):hover,`,
+ `${sel}.selected:not(.disabled):not(.no-hover):hover,`,
`${sel}:not(.selected)`,
`{background-color:white;border-color:${color};color:${color}}`,
- `${sel}:not(.no-hover):hover{background-color:${color || 'black'};color:white}`,
+ `${sel}:not(.disabled):not(.no-hover):hover`,
+ `{background-color:${color || 'black'};color:white}`,
].join('\n');
}
@@ -83,10 +101,13 @@ export default class LabelPane extends React.Component {
LabelPane.propTypes = {
general: PropTypes.shape({ isMobile: PropTypes.bool }),
editable: PropTypes.bool,
+ disableProtectedLabels: PropTypes.bool,
showUnselected: PropTypes.bool,
possibleLabels: PropTypes.objectOf(PropTypes.shape({
color: PropTypes.string,
description: PropTypes.string,
+ protected: PropTypes.bool,
+ visbility: PropTypes.string,
})),
selectedLabels: PropTypes.arrayOf(PropTypes.string),
contentClass: PropTypes.string,
@@ -96,6 +117,7 @@ LabelPane.propTypes = {
LabelPane.defaultProps = {
general: {},
editable: true,
+ disableProtectedLabels: false,
possibleLabels: {},
selectedLabels: [],
showUnselected: true,
diff --git a/src/containers/add-edit-container.js b/src/containers/add-edit-container.js
index 62dc51b..c117397 100644
--- a/src/containers/add-edit-container.js
+++ b/src/containers/add-edit-container.js
@@ -15,6 +15,7 @@ import AddEditEventPage from '../pages/add-edit/add-edit-page';
// This function passes values/objects from the Redux state to the React component as props
const mapStateToProps = state => ({
+ account: state.account,
general: state.general,
eventData: state.events.current,
possibleLabels: state.labels.labelList,
diff --git a/src/containers/calendar-container.js b/src/containers/calendar-container.js
index 82ceedc..c1fa8be 100644
--- a/src/containers/calendar-container.js
+++ b/src/containers/calendar-container.js
@@ -1,5 +1,6 @@
// This container is a sort of middleware between the React page and the Redux data store
+import _ from 'lodash';
import { connect } from 'react-redux';
import {
page,
@@ -15,20 +16,18 @@ import {
import CalendarPage from '../pages/calendar/calendar-page';
import withServerData from './with-server-data';
-const getVisibleEvents = (events, visibleLabels, allLabels) => {
+export const getVisibleEvents = (events, visibleLabels, allLabels) => {
// Filter out events that are not labeled with currently visible labels
if (!events || !visibleLabels || !allLabels) return null;
- return Object.values(events).filter((event) => {
- for (let i = 0; i < event.labels.length; ++i) {
- const indexOfLabel = visibleLabels.indexOf(event.labels[i]);
- if (indexOfLabel > -1) {
- event.color = allLabels[visibleLabels[indexOfLabel]].color;
- event.id = event.id || event.sid; // Make sure all events have an id attribute
- return true; // Event has at least one visible label
- }
- }
- return false; // No labels on this event are currently visible, so don't show
- });
+ const visibleLabelSet = new Set(visibleLabels);
+ const firstVisibleLabel = event => event.labels.find(name => visibleLabelSet.has(name));
+ return Object.values(events)
+ .filter(event => firstVisibleLabel(event))
+ .map(event =>
+ _.merge({}, event, {
+ color: allLabels[firstVisibleLabel(event)].color,
+ id: event.id || event.sid, // Make sure all events have an id attribute
+ }));
};
// This function passes values/objects from the Redux state to the React component as props
diff --git a/src/pages/add-edit/add-edit-page.jsx b/src/pages/add-edit/add-edit-page.jsx
index b8653dd..0676990 100644
--- a/src/pages/add-edit/add-edit-page.jsx
+++ b/src/pages/add-edit/add-edit-page.jsx
@@ -4,6 +4,7 @@ import axios from 'axios';
import deepcopy from 'deepcopy';
import _ from 'lodash';
import moment from 'moment';
+import PropTypes from 'prop-types';
import * as React from 'react';
import DateTimeSelector from '../../components/date-time-selector';
import LabelPane from '../../components/label-pane';
@@ -111,11 +112,6 @@ export default class AddEditEventPage extends React.Component {
setEnd = end => this.updateEventDatum({ end });
validateInput = () => {
- if (this.state.eventData.title.length === 0) {
- alert('Event Title is required');
- return false;
- }
-
if (this.state.eventData.title.length === 0) {
alert('Event must have a title');
return false;
@@ -243,7 +239,9 @@ export default class AddEditEventPage extends React.Component {
);
}
+ const { scope } = this.props.account;
const editingExisting = this.state.eventData.id || this.state.eventData.sid;
+ const requiredScope = editingExisting ? 'create:protected_events' : 'edit:protected_events';
const pageTitle = editingExisting ? 'Edit Event' : 'Add Event';
const submitButtonText = editingExisting ? 'Update Event' : 'Add Event';
const formUrl = 'https://goo.gl/forms/2cqVijokICZ5S20R2';
@@ -307,7 +305,7 @@ export default class AddEditEventPage extends React.Component {