From fecb788b468b9c23edb45a9d565d8efa415bf925 Mon Sep 17 00:00:00 2001 From: Oliver Steele Date: Thu, 17 May 2018 17:06:52 -0400 Subject: [PATCH 1/4] Rewrite getVisibleEvents to match style guide --- .eslintrc.yml | 2 - src/__test__/calendar-container.test.js | 86 +++++++++++++++++++++++++ src/containers/calendar-container.js | 23 ++++--- src/pages/add-edit/location-field.jsx | 7 +- 4 files changed, 98 insertions(+), 20 deletions(-) create mode 100644 src/__test__/calendar-container.test.js diff --git a/.eslintrc.yml b/.eslintrc.yml index 2172f25..97f4500 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -13,7 +13,6 @@ globals: rules: # Intentional exceptions to Airbnb rules: no-alert: off # currently used in the UI - no-plusplus: [error, allowForLoopAfterthoughts: true] no-underscore-dangle: [error, allow: [__REDUX_DEVTOOLS_EXTENSION_COMPOSE__]] no-unused-vars: [error, argsIgnorePattern: "^_" ] react/jsx-no-target-blank: off @@ -24,7 +23,6 @@ rules: no-restricted-globals: warn react/no-find-dom-node: warn react/prop-types: off # TODO: #123 - react/require-default-props: warn # TODO: #118 accessibility jsx-a11y/anchor-is-valid: off diff --git a/src/__test__/calendar-container.test.js b/src/__test__/calendar-container.test.js new file mode 100644 index 0000000..dbcbcad --- /dev/null +++ b/src/__test__/calendar-container.test.js @@ -0,0 +1,86 @@ +import { getVisibleEvents } from '../containers/calendar-container'; +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/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/location-field.jsx b/src/pages/add-edit/location-field.jsx index f734c39..9ee21c2 100644 --- a/src/pages/add-edit/location-field.jsx +++ b/src/pages/add-edit/location-field.jsx @@ -7,12 +7,7 @@ import * as React from 'react'; export default class LocationField extends React.Component { static stringMatches(str, substrings) { const s = str.trim(); - for (let i = 0; i < substrings.length; ++i) { - if (s === substrings[i]) { - return true; - } - } - return false; + return substrings.some(substr => substr === s); } constructor(props) { From 389695deaff754104e45b46c088625d0c520d283 Mon Sep 17 00:00:00 2001 From: Oliver Steele Date: Fri, 18 May 2018 15:47:26 -0400 Subject: [PATCH 2/4] Teach UI about protected labels Fixes ##177. --- public/css/app.css | 6 +- .../__snapshots__/label-pane.test.jsx.snap | 77 +++++++++++-------- src/__test__/calendar-container.test.js | 1 - src/components/label-pane.jsx | 40 +++++++--- src/pages/add-edit/add-edit-page.jsx | 1 + src/pages/details/event-details-page.jsx | 2 +- 6 files changed, 80 insertions(+), 47 deletions(-) diff --git a/public/css/app.css b/public/css/app.css index 4ac4fa5..8d64b62 100755 --- a/public/css/app.css +++ b/public/css/app.css @@ -343,7 +343,7 @@ span.label { margin: 0.3rem 0.2em; } -.button.label:hover:not(.no-hover) { +.button.label:hover:not(.disabled):not(.no-hover) { background-color: #14679e; color: #fefefe; cursor: pointer; @@ -354,13 +354,13 @@ span.label { } .add-edit-filters .button.label, -.add-edit-filters .button.label:not(.selected):not(.no-hover):hover { +.add-edit-filters .button.label:not(.selected):not(.disabled):not(.no-hover):hover { background-color: #14679e !important; color: #fefefe !important; } .add-edit-filters .button.label:not(.selected), -.add-edit-filters .button.label.selected:not(.no-hover):hover { +.add-edit-filters .button.label.selected:not(.disabled):not(.no-hover):hover { background-color: white !important; border-color: #14679e !important; color: #14679e !important; diff --git a/src/__test__/__snapshots__/label-pane.test.jsx.snap b/src/__test__/__snapshots__/label-pane.test.jsx.snap index 6131cf9..9b04db0 100644 --- a/src/__test__/__snapshots__/label-pane.test.jsx.snap +++ b/src/__test__/__snapshots__/label-pane.test.jsx.snap @@ -8,21 +8,23 @@ exports[`LabelPane matches snapshot 1`] = ` type="text/css" > .label.label-label-1.selected{background-color:red} - .label.label-label-1.selected:not(.no-hover):hover, + .label.label-label-1.selected:not(.disabled):not(.no-hover):hover, .label.label-label-1:not(.selected) {background-color:white;border-color:red;color:red} - .label.label-label-1:not(.no-hover):hover{background-color:red;color:white} + .label.label-label-1:not(.disabled):not(.no-hover):hover + {background-color:red;color:white} .label.label-label-2.selected{background-color:green} - .label.label-label-2.selected:not(.no-hover):hover, + .label.label-label-2.selected:not(.disabled):not(.no-hover):hover, .label.label-label-2:not(.selected) {background-color:white;border-color:green;color:green} - .label.label-label-2:not(.no-hover):hover{background-color:green;color:white} + .label.label-label-2:not(.disabled):not(.no-hover):hover + {background-color:green;color:white}
) : ( -   + {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/pages/add-edit/add-edit-page.jsx b/src/pages/add-edit/add-edit-page.jsx index b8653dd..5926972 100644 --- a/src/pages/add-edit/add-edit-page.jsx +++ b/src/pages/add-edit/add-edit-page.jsx @@ -316,6 +316,7 @@ export default class AddEditEventPage extends React.Component { /> {event.location}

- + From 09cccea464d8b35670510b5ed2395774b3db40c1 Mon Sep 17 00:00:00 2001 From: Oliver Steele Date: Fri, 18 May 2018 15:57:11 -0400 Subject: [PATCH 3/4] Disallow editing a protected event Fixes #178 --- src/pages/add-edit/add-edit-page.jsx | 2 +- src/sidebar/sidebar.jsx | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/pages/add-edit/add-edit-page.jsx b/src/pages/add-edit/add-edit-page.jsx index 5926972..3dd6ee0 100644 --- a/src/pages/add-edit/add-edit-page.jsx +++ b/src/pages/add-edit/add-edit-page.jsx @@ -307,7 +307,7 @@ export default class AddEditEventPage extends React.Component { + event && event.labels.some(label => (possibleLabels[label] || {}).protected); + const Sidebar = (props) => { const { account: { scope }, @@ -23,7 +26,7 @@ const Sidebar = (props) => { const oauthUrl = `${oauthBaseUrl}?redirect_uri=${encodeURIComponent(window.location.href)}`; const content = (
- {!scope.has('community_events:read') && ( + {!scope.has('read:all_events') && (

You are viewing the public calendar.

@@ -35,7 +38,7 @@ const Sidebar = (props) => { )} {mode.LINK_PANE && - scope.has('events:create') && ( + scope.has('create:events') && ( { )} {mode.EVENT_ACTIONS && - scope.has('events:edit') && ( + scope.has('edit:events') && + (scope.has('edit:protected_events') || + !isProtectedEvent(props.currentEvent, props.possibleLabels)) && ( )} From 73381d807229fd63e338e678b19600db1dddef37 Mon Sep 17 00:00:00 2001 From: Oliver Steele Date: Fri, 18 May 2018 16:10:11 -0400 Subject: [PATCH 4/4] Admins can edit protected events --- src/containers/add-edit-container.js | 1 + src/pages/add-edit/add-edit-page.jsx | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) 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/pages/add-edit/add-edit-page.jsx b/src/pages/add-edit/add-edit-page.jsx index 3dd6ee0..14c0257 100644 --- a/src/pages/add-edit/add-edit-page.jsx +++ b/src/pages/add-edit/add-edit-page.jsx @@ -243,7 +243,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'; @@ -316,7 +318,7 @@ export default class AddEditEventPage extends React.Component { />