You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by be...@apache.org on 2016/05/18 21:58:34 UTC
fauxton commit: updated refs/heads/master to 0518374
Repository: couchdb-fauxton
Updated Branches:
refs/heads/master 5b718dd71 -> 05183749e
Notifications Update
This updates the global notifications to use the same store
as the notification panel and drop Backbone.
Project: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/commit/05183749
Tree: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/tree/05183749
Diff: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/diff/05183749
Branch: refs/heads/master
Commit: 05183749e53f173b8f2fff52c9c2d93a053c3bca
Parents: 5b718dd
Author: Ben Keen <be...@gmail.com>
Authored: Wed Mar 30 14:35:50 2016 -0700
Committer: Ben Keen <be...@gmail.com>
Committed: Wed May 18 08:50:55 2016 -0700
----------------------------------------------------------------------
.../databases/tests/nightwatch/zeroclipboard.js | 3 +-
.../tests/nightwatch/uploadAttachment.js | 4 +-
app/addons/fauxton/base.js | 111 +-------
app/addons/fauxton/notifications/actions.js | 27 +-
app/addons/fauxton/notifications/actiontypes.js | 5 +-
.../notifications/notifications.react.jsx | 282 ++++++++++++++++---
app/addons/fauxton/notifications/stores.js | 46 +++
.../fauxton/notifications/tests/actionsSpec.jsx | 58 ++++
.../tests/componentsSpec.react.jsx | 115 +++++---
app/addons/fauxton/templates/notification.html | 19 --
app/addons/fauxton/tests/baseSpec.js | 62 ----
assets/index.underscore | 3 +-
assets/less/fauxton.less | 3 +-
assets/less/templates.less | 7 +-
package.json | 1 +
.../custom-commands/closeNotification.js | 2 +-
16 files changed, 464 insertions(+), 284 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/05183749/app/addons/databases/tests/nightwatch/zeroclipboard.js
----------------------------------------------------------------------
diff --git a/app/addons/databases/tests/nightwatch/zeroclipboard.js b/app/addons/databases/tests/nightwatch/zeroclipboard.js
index 708bb64..cd83582 100644
--- a/app/addons/databases/tests/nightwatch/zeroclipboard.js
+++ b/app/addons/databases/tests/nightwatch/zeroclipboard.js
@@ -24,10 +24,9 @@ module.exports = {
}
client
+ .deleteDatabase(newDatabaseName)
.loginToGUI()
- .deleteDatabase(newDatabaseName) //need to delete the automatic database 'fauxton-selenium-tests' that has been set up before each test
.url(baseUrl)
-
.clickWhenVisible('.control-toggle-api-url')
.pause(1000) // needed for reliability. The tray slides in from the top so the pos of the copy button changes
.waitForElementVisible('.copy-button', waitTime, false)
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/05183749/app/addons/documents/tests/nightwatch/uploadAttachment.js
----------------------------------------------------------------------
diff --git a/app/addons/documents/tests/nightwatch/uploadAttachment.js b/app/addons/documents/tests/nightwatch/uploadAttachment.js
index 4005984..b534823 100644
--- a/app/addons/documents/tests/nightwatch/uploadAttachment.js
+++ b/app/addons/documents/tests/nightwatch/uploadAttachment.js
@@ -27,8 +27,8 @@ module.exports = {
.waitForElementVisible('input[name="_attachments"]', waitTime)
.setValue('input[name="_attachments"]', require('path').resolve(__dirname + '/uploadAttachment.js'))
.clickWhenVisible('#upload-btn')
- .waitForElementVisible('#global-notification-id', waitTime, false)
- .getText('#global-notification-id', function (result) {
+ .waitForElementVisible('.global-notification', waitTime, false)
+ .getText('.global-notification', function (result) {
var data = result.value;
this.verify.ok(data, 'Document saved successfully.');
})
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/05183749/app/addons/fauxton/base.js
----------------------------------------------------------------------
diff --git a/app/addons/fauxton/base.js b/app/addons/fauxton/base.js
index 83fb727..7d22bb1 100644
--- a/app/addons/fauxton/base.js
+++ b/app/addons/fauxton/base.js
@@ -29,17 +29,14 @@ function (app, FauxtonAPI, Components, NotificationComponents, Actions, NavbarRe
var Fauxton = FauxtonAPI.addon();
FauxtonAPI.addNotification = function (options) {
options = _.extend({
- msg: "Notification Event Triggered!",
- type: "info",
- selector: "#global-notifications",
- escape: true
+ msg: 'Notification Event Triggered!',
+ type: 'info',
+ escape: true,
+ clear: false
}, options);
// log all notifications in a store
Actions.addNotification(options);
-
- var view = new Fauxton.Notification(options);
- return view.renderNotification();
};
FauxtonAPI.UUID = FauxtonAPI.Model.extend({
@@ -103,11 +100,10 @@ function (app, FauxtonAPI, Components, NotificationComponents, Actions, NavbarRe
NavbarReactComponents.renderNavBar(primaryNavBarEl);
}
- var notificationCenterEl = $('#notification-center')[0];
- if (notificationCenterEl) {
- NotificationComponents.renderNotificationCenter(notificationCenterEl);
+ var notificationEl = $('#notifications')[0];
+ if (notificationEl) {
+ NotificationComponents.renderNotificationController(notificationEl);
}
-
var versionInfo = new Fauxton.VersionInfo();
versionInfo.fetch().then(function () {
@@ -121,98 +117,5 @@ function (app, FauxtonAPI, Components, NotificationComponents, Actions, NavbarRe
}
});
- Fauxton.Notification = FauxtonAPI.View.extend({
- animationTimer: 8000,
- id: 'global-notification-id',
- events: {
- 'click .js-dismiss': 'onClickRemoveWithAnimation'
- },
-
- initialize: function (options) {
- this.htmlToRender = options.msg;
- // escape always, except the value is false
- if (options.escape !== false) {
- this.htmlToRender = _.escape(this.htmlToRender);
- }
- this.type = options.type || "info";
- this.selector = options.selector;
- this.fade = options.fade === undefined ? true : options.fade;
- this.data = options.data || "";
- this.template = options.template || "addons/fauxton/templates/notification";
- },
-
- serialize: function () {
- var icon;
-
- switch (this.type) {
- case 'error':
- icon = 'fonticon-attention-circled';
- break;
- case 'info':
- icon = 'fonticon-info-circled';
- break;
- case 'success':
- icon = 'fonticon-ok-circled';
- break;
- default:
- icon = 'fonticon-info-circled';
- break;
- }
-
- return {
- icon: icon,
- data: this.data,
- htmlToRender: this.htmlToRender,
- type: this.type
- };
- },
-
- onClickRemoveWithAnimation: function (event) {
- event.preventDefault();
- window.clearTimeout(this.timeout);
- this.removeWithAnimation();
- },
-
- removeWithAnimation: function () {
- this.$el.velocity('reverse', FauxtonAPI.constants.MISC.TRAY_TOGGLE_SPEED, function () {
- this.$el.remove();
- this.removeCloseListener();
- }.bind(this));
- },
-
- addCloseListener: function () {
- $(document).on('keydown.notificationClose', this.onKeyDown.bind(this));
- },
-
- onKeyDown: function (e) {
- var code = e.keyCode || e.which;
- if (code === 27) { // ESC key
- this.removeWithAnimation();
- }
- },
-
- removeCloseListener: function () {
- $(document).off('keydown.notificationClose', this.removeWithAnimation);
- },
-
- delayedRemoval: function () {
- this.timeout = setTimeout(function () {
- this.removeWithAnimation();
- }.bind(this), this.animationTimer);
- },
-
- renderNotification: function (selector) {
- selector = selector || this.selector;
- if (this.clear) {
- $(selector).html('');
- }
- this.render().$el.appendTo(selector);
- this.$el.velocity('transition.slideDownIn', FauxtonAPI.constants.MISC.TRAY_TOGGLE_SPEED);
- this.delayedRemoval();
- this.addCloseListener();
- return this;
- }
- });
-
return Fauxton;
});
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/05183749/app/addons/fauxton/notifications/actions.js
----------------------------------------------------------------------
diff --git a/app/addons/fauxton/notifications/actions.js b/app/addons/fauxton/notifications/actions.js
index 38b9255..91fa822 100644
--- a/app/addons/fauxton/notifications/actions.js
+++ b/app/addons/fauxton/notifications/actions.js
@@ -55,13 +55,38 @@ function (FauxtonAPI, ActionTypes) {
});
}
+ function startHidingNotification (notificationId) {
+ FauxtonAPI.dispatch({
+ type: ActionTypes.START_HIDING_NOTIFICATION,
+ options: {
+ notificationId: notificationId
+ }
+ });
+ }
+
+ function hideNotification (notificationId) {
+ FauxtonAPI.dispatch({
+ type: ActionTypes.HIDE_NOTIFICATION,
+ options: {
+ notificationId: notificationId
+ }
+ });
+ }
+
+ function hideAllVisibleNotifications () {
+ FauxtonAPI.dispatch({ type: ActionTypes.HIDE_ALL_NOTIFICATIONS });
+ }
+
return {
addNotification: addNotification,
showNotificationCenter: showNotificationCenter,
hideNotificationCenter: hideNotificationCenter,
clearAllNotifications: clearAllNotifications,
clearSingleNotification: clearSingleNotification,
- selectNotificationFilter: selectNotificationFilter
+ selectNotificationFilter: selectNotificationFilter,
+ startHidingNotification: startHidingNotification,
+ hideNotification: hideNotification,
+ hideAllVisibleNotifications: hideAllVisibleNotifications
};
});
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/05183749/app/addons/fauxton/notifications/actiontypes.js
----------------------------------------------------------------------
diff --git a/app/addons/fauxton/notifications/actiontypes.js b/app/addons/fauxton/notifications/actiontypes.js
index 103ec58..e346c24 100644
--- a/app/addons/fauxton/notifications/actiontypes.js
+++ b/app/addons/fauxton/notifications/actiontypes.js
@@ -17,6 +17,9 @@ define([], function () {
HIDE_NOTIFICATION_CENTER: 'HIDE_NOTIFICATION_CENTER',
CLEAR_SINGLE_NOTIFICATION: 'CLEAR_SINGLE_NOTIFICATION',
CLEAR_ALL_NOTIFICATIONS: 'CLEAR_ALL_NOTIFICATIONS',
- SELECT_NOTIFICATION_FILTER: 'SELECT_NOTIFICATION_FILTER'
+ SELECT_NOTIFICATION_FILTER: 'SELECT_NOTIFICATION_FILTER',
+ START_HIDING_NOTIFICATION: 'START_HIDING_NOTIFICATION',
+ HIDE_NOTIFICATION: 'HIDE_NOTIFICATION',
+ HIDE_ALL_NOTIFICATIONS: 'HIDE_ALL_NOTIFICATIONS'
};
});
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/05183749/app/addons/fauxton/notifications/notifications.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/fauxton/notifications/notifications.react.jsx b/app/addons/fauxton/notifications/notifications.react.jsx
index 2e53ebb..cd88fe5 100644
--- a/app/addons/fauxton/notifications/notifications.react.jsx
+++ b/app/addons/fauxton/notifications/notifications.react.jsx
@@ -19,81 +19,268 @@ define([
'./stores',
'../components.react',
- // needed to run the test individually. Don't remove
+ 'velocity-react',
"velocity-animate/velocity",
"velocity-animate/velocity.ui"
],
-function (app, FauxtonAPI, React, ReactDOM, Actions, Stores, Components) {
+function (app, FauxtonAPI, React, ReactDOM, Actions, Stores, Components, VelocityReact) {
- var notificationStore = Stores.notificationStore;
+ var store = Stores.notificationStore;
var Clipboard = Components.Clipboard;
+ var VelocityComponent = VelocityReact.VelocityComponent;
- var NotificationCenterButton = React.createClass({
+ // The one-stop-shop for Fauxton notifications. This controller handler the header notifications and the rightmost
+ // notification center panel
+ var NotificationController = React.createClass({
+
getInitialState: function () {
+ return this.getStoreState();
+ },
+
+ getStoreState: function () {
return {
- visible: true
+ notificationCenterVisible: store.isNotificationCenterVisible(),
+ notificationCenterFilter: store.getNotificationFilter(),
+ notifications: store.getNotifications()
};
},
- hide: function () {
- this.setState({ visible: false });
+ componentDidMount: function () {
+ store.on('change', this.onChange, this);
},
- show: function () {
- this.setState({ visible: true });
+ componentWillUnmount: function () {
+ store.off('change', this.onChange);
+ },
+
+ onChange: function () {
+ if (this.isMounted()) {
+ this.setState(this.getStoreState());
+ }
},
render: function () {
- var classes = 'fonticon fonticon-bell' + ((!this.state.visible) ? ' hide' : '');
return (
- <div className={classes} onClick={Actions.showNotificationCenter}></div>
+ <div>
+ <GlobalNotifications
+ notifications={this.state.notifications} />
+ <NotificationCenterPanel
+ visible={this.state.notificationCenterVisible}
+ filter={this.state.notificationCenterFilter}
+ notifications={this.state.notifications} />
+ </div>
);
}
});
- var NotificationCenterPanel = React.createClass({
- getInitialState: function () {
- return this.getStoreState();
+ var GlobalNotifications = React.createClass({
+ propTypes: {
+ notifications: React.PropTypes.array.isRequired
},
- getStoreState: function () {
+ componentDidMount: function () {
+ $(document).on('keydown.notificationClose', this.onKeyDown);
+ },
+
+ componentWillUnmount: function () {
+ $(document).off('keydown.notificationClose', this.onKeyDown);
+ },
+
+ onKeyDown: function (e) {
+ var code = e.keyCode || e.which;
+ if (code === 27) {
+ Actions.hideAllVisibleNotifications();
+ }
+ },
+
+ getNotifications: function () {
+ if (!this.props.notifications.length) {
+ return null;
+ }
+
+ return _.map(this.props.notifications, function (notification, index) {
+
+ // notifications are completely removed from the DOM once they're
+ if (!notification.visible) {
+ return;
+ }
+
+ return (
+ <Notification
+ notificationId={notification.notificationId}
+ isHiding={notification.isHiding}
+ key={index}
+ msg={notification.msg}
+ type={notification.type}
+ escape={notification.escape}
+ onStartHide={Actions.startHidingNotification}
+ onHideComplete={Actions.hideNotification} />
+ );
+ }, this);
+ },
+
+ render: function () {
+ return (
+ <div id="global-notifications">
+ {this.getNotifications()}
+ </div>
+ );
+ }
+ });
+
+
+ var Notification = React.createClass({
+ propTypes: {
+ msg: React.PropTypes.string.isRequired,
+ onStartHide: React.PropTypes.func.isRequired,
+ onHideComplete: React.PropTypes.func.isRequired,
+ type: React.PropTypes.oneOf(['error', 'info', 'success']),
+ escape: React.PropTypes.bool,
+ isHiding: React.PropTypes.bool.isRequired,
+ visibleTime: React.PropTypes.number
+ },
+
+ getDefaultProps: function () {
+ return {
+ type: 'info',
+ visibleTime: 8000,
+ escape: true,
+ slideInTime: 200,
+ slideOutTime: 200
+ };
+ },
+
+ componentWillUnmount: function () {
+ if (this.timeout) {
+ window.clearTimeout(this.timeout);
+ }
+ },
+
+ getInitialState: function () {
return {
- isVisible: notificationStore.isNotificationCenterVisible(),
- filter: notificationStore.getNotificationFilter(),
- notifications: notificationStore.getNotifications()
+ animation: { opacity: 0, minHeight: 0 }
};
},
componentDidMount: function () {
- notificationStore.on('change', this.onChange, this);
+ this.setState({
+ animation: {
+ opacity: (this.props.isHiding) ? 0 : 1,
+ minHeight: (this.props.isHiding) ? 0 : ReactDOM.findDOMNode(this.refs.notification).offsetHeight
+ }
+ });
+
+ this.timeout = setTimeout(function () {
+ this.hide();
+ }.bind(this), this.props.visibleTime);
},
- componentWillUnmount: function () {
- notificationStore.off('change', this.onChange);
+ componentDidUpdate: function (prevProps) {
+ if (!prevProps.isHiding && this.props.isHiding) {
+ this.setState({
+ animation: {
+ opacity: 0,
+ minHeight: 0
+ }
+ });
+ }
},
- onChange: function () {
- if (this.isMounted()) {
- this.setState(this.getStoreState());
+ getHeight: function () {
+ return $(ReactDOM.findDOMNode(this)).outerHeight(true);
+ },
+
+ hide: function (e) {
+ if (e) {
+ e.preventDefault();
}
+ this.props.onStartHide(this.props.notificationId);
+ },
+
+ // many messages contain HTML, hence the need for dangerouslySetInnerHTML
+ getMsg: function () {
+ var msg = (this.props.escape) ? _.escape(this.props.msg) : this.props.msg;
+ return {
+ __html: msg
+ };
+ },
+
+ onAnimationComplete: function () {
+ if (this.props.isHiding) {
+ this.props.onHideComplete(this.props.notificationId);
+ }
+ },
+
+ render: function () {
+ var iconMap = {
+ error: 'fonticon-attention-circled',
+ info: 'fonticon-info-circled',
+ success: 'fonticon-ok-circled'
+ };
+
+ return (
+ <VelocityComponent animation={this.state.animation}
+ runOnMount={true} duration={this.props.slideInTime} complete={this.onAnimationComplete}>
+ <div className="notification-wrapper">
+ <div className={'global-notification alert alert-' + this.props.type} ref="notification">
+ <a data-bypass href="#" onClick={this.hide}><i className="pull-right fonticon-cancel" /></a>
+ <i className={'notification-icon ' + iconMap[this.props.type]} />
+ <span dangerouslySetInnerHTML={this.getMsg()}></span>
+ </div>
+ </div>
+ </VelocityComponent>
+ );
+ }
+ });
+
+
+ var NotificationCenterButton = React.createClass({
+ getInitialState: function () {
+ return {
+ visible: true
+ };
+ },
+
+ hide: function () {
+ this.setState({ visible: false });
+ },
+
+ show: function () {
+ this.setState({ visible: true });
+ },
+
+ render: function () {
+ var classes = 'fonticon fonticon-bell' + ((!this.state.visible) ? ' hide' : '');
+ return (
+ <div className={classes} onClick={Actions.showNotificationCenter}></div>
+ );
+ }
+ });
+
+
+ var NotificationCenterPanel = React.createClass({
+ propTypes: {
+ visible: React.PropTypes.bool.isRequired,
+ filter: React.PropTypes.string.isRequired,
+ notifications: React.PropTypes.array.isRequired
},
getNotifications: function () {
- if (!this.state.notifications.length) {
+ if (!this.props.notifications.length) {
return (
<li className="no-notifications">No notifications.</li>
);
}
- return _.map(this.state.notifications, function (notification, i) {
+ return _.map(this.props.notifications, function (notification) {
return (
- <NotificationRow
- isVisible={this.state.isVisible}
+ <NotificationPanelRow
+ isVisible={this.props.visible}
item={notification}
- filter={this.state.filter}
+ filter={this.props.filter}
key={notification.notificationId}
/>
);
@@ -102,7 +289,7 @@ function (app, FauxtonAPI, React, ReactDOM, Actions, Stores, Components) {
render: function () {
var panelClasses = 'notification-center-panel flex-layout flex-col';
- if (this.state.isVisible) {
+ if (this.props.visible) {
panelClasses += ' visible';
}
@@ -112,39 +299,39 @@ function (app, FauxtonAPI, React, ReactDOM, Actions, Stores, Components) {
error: 'flex-body',
info: 'flex-body'
};
- filterClasses[this.state.filter] += ' selected';
+ filterClasses[this.props.filter] += ' selected';
- var maskClasses = 'notification-page-mask' + ((this.state.isVisible) ? ' visible' : '');
+ var maskClasses = 'notification-page-mask' + ((this.props.visible) ? ' visible' : '');
return (
- <div>
+ <div id="notification-center">
<div className={panelClasses}>
<header className="flex-layout flex-row">
- <span className="fonticon fonticon-bell"></span>
+ <span className="fonticon fonticon-bell" />
<h1 className="flex-body">Notifications</h1>
<button type="button" onClick={Actions.hideNotificationCenter}>�</button>
</header>
<ul className="notification-filter flex-layout flex-row">
<li className={filterClasses.all} title="All notifications" data-filter="all"
- onClick={Actions.selectNotificationFilter.bind(this, 'all')}>All</li>
+ onClick={Actions.selectNotificationFilter.bind(this, 'all')}>All</li>
<li className={filterClasses.success} title="Success notifications" data-filter="success"
- onClick={Actions.selectNotificationFilter.bind(this, 'success')}>
- <span className="fonticon fonticon-ok-circled"></span>
+ onClick={Actions.selectNotificationFilter.bind(this, 'success')}>
+ <span className="fonticon fonticon-ok-circled" />
</li>
<li className={filterClasses.error} title="Error notifications" data-filter="error"
- onClick={Actions.selectNotificationFilter.bind(this, 'error')}>
- <span className="fonticon fonticon-attention-circled"></span>
+ onClick={Actions.selectNotificationFilter.bind(this, 'error')}>
+ <span className="fonticon fonticon-attention-circled" />
</li>
<li className={filterClasses.info} title="Info notifications" data-filter="info"
- onClick={Actions.selectNotificationFilter.bind(this, 'info')}>
- <span className="fonticon fonticon-info-circled"></span>
+ onClick={Actions.selectNotificationFilter.bind(this, 'info')}>
+ <span className="fonticon fonticon-info-circled" />
</li>
</ul>
<div className="flex-body">
<ul className="notification-list">
- {this.getNotifications()}
+ {this.getNotifications()}
</ul>
</div>
@@ -159,7 +346,8 @@ function (app, FauxtonAPI, React, ReactDOM, Actions, Stores, Components) {
}
});
- var NotificationRow = React.createClass({
+
+ var NotificationPanelRow = React.createClass({
propTypes: {
item: React.PropTypes.object.isRequired,
filter: React.PropTypes.string.isRequired,
@@ -252,12 +440,14 @@ function (app, FauxtonAPI, React, ReactDOM, Actions, Stores, Components) {
return {
+ NotificationController: NotificationController,
NotificationCenterButton: NotificationCenterButton,
NotificationCenterPanel: NotificationCenterPanel,
- NotificationRow: NotificationRow,
+ NotificationPanelRow: NotificationPanelRow,
+ Notification: Notification,
- renderNotificationCenter: function (el) {
- return ReactDOM.render(<NotificationCenterPanel />, el);
+ renderNotificationController: function (el) {
+ return ReactDOM.render(<NotificationController />, el);
}
};
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/05183749/app/addons/fauxton/notifications/stores.js
----------------------------------------------------------------------
diff --git a/app/addons/fauxton/notifications/stores.js b/app/addons/fauxton/notifications/stores.js
index 028bb2f..08becfb 100644
--- a/app/addons/fauxton/notifications/stores.js
+++ b/app/addons/fauxton/notifications/stores.js
@@ -61,6 +61,18 @@ function (FauxtonAPI, app, ActionTypes, moment) {
info.cleanMsg = app.utils.stripHTML(info.msg);
info.time = moment();
+ // all new notifications are visible by default. They get hidden after their time expires, by the component
+ info.visible = true;
+ info.isHiding = false;
+
+ // clear: true causes all visible messages to be hidden
+ if (info.clear) {
+ this._notifications.forEach(function (notification) {
+ if (notification.visible) {
+ notification.isHiding = true;
+ }
+ });
+ }
this._notifications.unshift(info);
},
@@ -76,6 +88,25 @@ function (FauxtonAPI, app, ActionTypes, moment) {
this._notifications = [];
},
+ hideNotification: function (notificationId) {
+ var notification = _.findWhere(this._notifications, { notificationId: notificationId });
+ notification.visible = false;
+ notification.isHiding = false;
+ },
+
+ hideAllNotifications: function () {
+ this._notifications.forEach(function (notification) {
+ if (notification.visible) {
+ notification.isHiding = true;
+ }
+ });
+ },
+
+ startHidingNotification: function (notificationId) {
+ var notification = _.findWhere(this._notifications, { notificationId: notificationId });
+ notification.isHiding = true;
+ },
+
getNotificationFilter: function () {
return this._selectedNotificationFilter;
},
@@ -104,6 +135,21 @@ function (FauxtonAPI, app, ActionTypes, moment) {
this.clearNotification(action.options.notificationId);
break;
+ case ActionTypes.START_HIDING_NOTIFICATION:
+ this.startHidingNotification(action.options.notificationId);
+ this.triggerChange();
+ break;
+
+ case ActionTypes.HIDE_NOTIFICATION:
+ this.hideNotification(action.options.notificationId);
+ this.triggerChange();
+ break;
+
+ case ActionTypes.HIDE_ALL_NOTIFICATIONS:
+ this.hideAllNotifications();
+ this.triggerChange();
+ break;
+
case ActionTypes.SHOW_NOTIFICATION_CENTER:
this._notificationCenterVisible = true;
this.triggerChange();
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/05183749/app/addons/fauxton/notifications/tests/actionsSpec.jsx
----------------------------------------------------------------------
diff --git a/app/addons/fauxton/notifications/tests/actionsSpec.jsx b/app/addons/fauxton/notifications/tests/actionsSpec.jsx
new file mode 100644
index 0000000..7121143
--- /dev/null
+++ b/app/addons/fauxton/notifications/tests/actionsSpec.jsx
@@ -0,0 +1,58 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+define([
+ '../../../../core/api',
+ '../notifications.react',
+ '../stores',
+ '../../../../../test/mocha/testUtils',
+ 'react',
+ 'react-dom',
+ 'moment',
+ 'react-addons-test-utils',
+ 'sinon'
+], function (FauxtonAPI, Views, Stores, utils, React, ReactDOM, moment, TestUtils) {
+ var assert = utils.assert;
+ var store = Stores.notificationStore;
+
+
+ describe('NotificationPanel', function () {
+ var container;
+
+ beforeEach(function () {
+ container = document.createElement('div');
+ store.reset();
+ });
+
+ afterEach(function () {
+ ReactDOM.unmountComponentAtNode(container);
+ });
+
+ it('clear all action fires', function () {
+ var panelEl = TestUtils.renderIntoDocument(<Views.NotificationPanel />, container);
+
+ var stub = sinon.stub(Actions, 'clearAllNotifications');
+ TestUtils.Simulate.click($(ReactDOM.findDOMNode(panelEl)).find('footer input')[0]);
+ assert.ok(stub.calledOnce);
+ Actions.clearAllNotifications.restore();
+ });
+
+ it('switch filter action fires', function () {
+ var panelEl = TestUtils.renderIntoDocument(<Views.NotificationPanel />, container);
+
+ var stub = sinon.stub(Actions, 'clearAllNotifications');
+ TestUtils.Simulate.click($(ReactDOM.findDOMNode(panelEl)).find('footer input')[0]);
+ assert.ok(stub.calledOnce);
+ Actions.clearAllNotifications.restore();
+ });
+
+ });
+});
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/05183749/app/addons/fauxton/notifications/tests/componentsSpec.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/fauxton/notifications/tests/componentsSpec.react.jsx b/app/addons/fauxton/notifications/tests/componentsSpec.react.jsx
index ab9f9a7..0f4d72a 100644
--- a/app/addons/fauxton/notifications/tests/componentsSpec.react.jsx
+++ b/app/addons/fauxton/notifications/tests/componentsSpec.react.jsx
@@ -13,17 +13,44 @@ define([
'../../../../core/api',
'../notifications.react',
'../stores',
+ '../actions',
'../../../../../test/mocha/testUtils',
'react',
'react-dom',
'moment',
'react-addons-test-utils',
'sinon'
-], function (FauxtonAPI, Views, Stores, utils, React, ReactDOM, moment, TestUtils) {
+], function (FauxtonAPI, Views, Stores, Actions, utils, React, ReactDOM, moment, TestUtils) {
var assert = utils.assert;
var store = Stores.notificationStore;
- describe('NotificationRow', function () {
+
+ describe('NotificationController', function () {
+ var container;
+
+ beforeEach(function () {
+ container = document.createElement('div');
+ store.reset();
+ });
+
+ afterEach(function () {
+ ReactDOM.unmountComponentAtNode(container);
+ });
+
+ it('notifications should be escaped by default', function () {
+ var component = TestUtils.renderIntoDocument(<Views.NotificationController />, container);
+ FauxtonAPI.addNotification({ msg: '<script>window.whatever=1;</script>' });
+ assert.ok(/<script>window.whatever=1;<\/script>/.test(ReactDOM.findDOMNode(component).innerHTML));
+ });
+
+ it('notifications should be able to render unescaped', function () {
+ var component = TestUtils.renderIntoDocument(<Views.NotificationController />, container);
+ FauxtonAPI.addNotification({ msg: '<script>window.whatever=1;</script>', escape: false });
+ assert.ok(/<script>window.whatever=1;<\/script>/.test(ReactDOM.findDOMNode(component).innerHTML));
+ });
+ });
+
+ describe('NotificationPanelRow', function () {
var container;
var notifications = {
@@ -57,21 +84,21 @@ define([
it('shows all notification types when "all" filter applied', function () {
var row1 = TestUtils.renderIntoDocument(
- <Views.NotificationRow filter="all" item={notifications.success} />,
+ <Views.NotificationPanelRow filter="all" item={notifications.success}/>,
container
);
assert.equal($(ReactDOM.findDOMNode(row1)).attr('aria-hidden'), 'false');
ReactDOM.unmountComponentAtNode(container);
var row2 = TestUtils.renderIntoDocument(
- <Views.NotificationRow filter="all" item={notifications.error} />,
+ <Views.NotificationPanelRow filter="all" item={notifications.error}/>,
container
);
assert.equal($(ReactDOM.findDOMNode(row2)).attr('aria-hidden'), 'false');
ReactDOM.unmountComponentAtNode(container);
var row3 = TestUtils.renderIntoDocument(
- <Views.NotificationRow filter="all" item={notifications.info} />,
+ <Views.NotificationPanelRow filter="all" item={notifications.info}/>,
container
);
assert.equal($(ReactDOM.findDOMNode(row3)).attr('aria-hidden'), 'false');
@@ -80,7 +107,7 @@ define([
it('hides notification when filter doesn\'t match', function () {
var rowEl = TestUtils.renderIntoDocument(
- <Views.NotificationRow filter="success" item={notifications.info} />,
+ <Views.NotificationPanelRow filter="success" item={notifications.info}/>,
container
);
assert.equal($(ReactDOM.findDOMNode(rowEl)).attr('aria-hidden'), 'true');
@@ -88,12 +115,11 @@ define([
it('shows notification when filter exact match', function () {
var rowEl = TestUtils.renderIntoDocument(
- <Views.NotificationRow filter="info" item={notifications.info} />,
+ <Views.NotificationPanelRow filter="info" item={notifications.info}/>,
container
);
assert.equal($(ReactDOM.findDOMNode(rowEl)).attr('aria-hidden'), 'false');
});
-
});
@@ -110,51 +136,56 @@ define([
});
it('shows all notifications by default', function () {
- store.addNotification({ type: 'success', msg: 'Success are okay' });
- store.addNotification({ type: 'success', msg: 'another success.' });
- store.addNotification({ type: 'info', msg: 'A single info message' });
- store.addNotification({ type: 'error', msg: 'Error #1' });
- store.addNotification({ type: 'error', msg: 'Error #2' });
- store.addNotification({ type: 'error', msg: 'Error #3' });
-
- var panelEl = TestUtils.renderIntoDocument(<Views.NotificationCenterPanel />, container);
+ store.addNotification({type: 'success', msg: 'Success are okay'});
+ store.addNotification({type: 'success', msg: 'another success.'});
+ store.addNotification({type: 'info', msg: 'A single info message'});
+ store.addNotification({type: 'error', msg: 'Error #1'});
+ store.addNotification({type: 'error', msg: 'Error #2'});
+ store.addNotification({type: 'error', msg: 'Error #3'});
+
+ var panelEl = TestUtils.renderIntoDocument(
+ <Views.NotificationCenterPanel
+ filter="all"
+ notifications={store.getNotifications()}
+ />, container);
+
assert.equal($(ReactDOM.findDOMNode(panelEl)).find('.notification-list li[aria-hidden=false]').length, 6);
});
- it('clicking on a filter icon filters applies appropriate filter', function () {
- store.addNotification({ type: 'success', msg: 'Success are okay' });
- store.addNotification({ type: 'success', msg: 'another success.' });
- store.addNotification({ type: 'info', msg: 'A single info message' });
- store.addNotification({ type: 'error', msg: 'Error #1' });
- store.addNotification({ type: 'error', msg: 'Error #2' });
- store.addNotification({ type: 'error', msg: 'Error #3' });
+ it('appropriate filters are applied - 1', function () {
+ store.addNotification({type: 'success', msg: 'Success are okay'});
+ store.addNotification({type: 'success', msg: 'another success.'});
+ store.addNotification({type: 'info', msg: 'A single info message'});
+ store.addNotification({type: 'error', msg: 'Error #1'});
+ store.addNotification({type: 'error', msg: 'Error #2'});
+ store.addNotification({type: 'error', msg: 'Error #3'});
- var panelEl = TestUtils.renderIntoDocument(<Views.NotificationCenterPanel />, container);
+ var panelEl = TestUtils.renderIntoDocument(
+ <Views.NotificationCenterPanel
+ filter="success"
+ notifications={store.getNotifications()}
+ />, container);
// there are 2 success messages
- TestUtils.Simulate.click($(ReactDOM.findDOMNode(panelEl)).find('.notification-filter li[data-filter="success"]')[0]);
assert.equal($(ReactDOM.findDOMNode(panelEl)).find('.notification-list li[aria-hidden=false]').length, 2);
-
- // 3 errors
- TestUtils.Simulate.click($(ReactDOM.findDOMNode(panelEl)).find('.notification-filter li[data-filter="error"]')[0]);
- assert.equal($(ReactDOM.findDOMNode(panelEl)).find('.notification-list li[aria-hidden=false]').length, 3);
-
- // 1 info
- TestUtils.Simulate.click($(ReactDOM.findDOMNode(panelEl)).find('.notification-filter li[data-filter="info"]')[0]);
- assert.equal($(ReactDOM.findDOMNode(panelEl)).find('.notification-list li[aria-hidden=false]').length, 1);
});
- it('clear all clears all notifications', function () {
- store.addNotification({ type: 'success', msg: 'Success are okay' });
- store.addNotification({ type: 'info', msg: 'A single info message' });
- store.addNotification({ type: 'error', msg: 'Error #2' });
- store.addNotification({ type: 'error', msg: 'Error #3' });
+ it('appropriate filters are applied - 2', function () {
+ store.addNotification({type: 'success', msg: 'Success are okay'});
+ store.addNotification({type: 'success', msg: 'another success.'});
+ store.addNotification({type: 'info', msg: 'A single info message'});
+ store.addNotification({type: 'error', msg: 'Error #1'});
+ store.addNotification({type: 'error', msg: 'Error #2'});
+ store.addNotification({type: 'error', msg: 'Error #3'});
- var panelEl = TestUtils.renderIntoDocument(<Views.NotificationCenterPanel />, container);
- assert.equal($(ReactDOM.findDOMNode(panelEl)).find('.notification-list li[aria-hidden=false]').length, 4);
- TestUtils.Simulate.click($(ReactDOM.findDOMNode(panelEl)).find('footer input')[0]);
+ var panelEl = TestUtils.renderIntoDocument(
+ <Views.NotificationCenterPanel
+ filter="error"
+ notifications={store.getNotifications()}
+ />, container);
- assert.equal($(ReactDOM.findDOMNode(panelEl)).find('.notification-list li[aria-hidden=false]').length, 0);
+ // 3 errors
+ assert.equal($(ReactDOM.findDOMNode(panelEl)).find('.notification-list li[aria-hidden=false]').length, 3);
});
});
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/05183749/app/addons/fauxton/templates/notification.html
----------------------------------------------------------------------
diff --git a/app/addons/fauxton/templates/notification.html b/app/addons/fauxton/templates/notification.html
deleted file mode 100644
index 84ac7ac..0000000
--- a/app/addons/fauxton/templates/notification.html
+++ /dev/null
@@ -1,19 +0,0 @@
-<%/*
-Licensed under the Apache License, Version 2.0 (the "License"); you may not
-use this file except in compliance with the License. You may obtain a copy of
-the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-License for the specific language governing permissions and limitations under
-the License.
-*/%>
-
-<div class="global-notification alert alert-<%- type %>">
- <a data-bypass href="#" class="js-dismiss"><i class="pull-right fonticon-cancel"></i></a>
- <i class="notification-icon <%- icon %>"></i>
- <%= htmlToRender %><!-- every caller has to escape on it's own -->
-</div>
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/05183749/app/addons/fauxton/tests/baseSpec.js
----------------------------------------------------------------------
diff --git a/app/addons/fauxton/tests/baseSpec.js b/app/addons/fauxton/tests/baseSpec.js
index 55d0dca..3e4638f 100644
--- a/app/addons/fauxton/tests/baseSpec.js
+++ b/app/addons/fauxton/tests/baseSpec.js
@@ -66,8 +66,6 @@ define([
setView: function () {}
}
};
-
-
});
after(function () {
@@ -85,66 +83,6 @@ define([
testRouteObject.renderWith('the-route', mockLayout, 'args');
assert.ok(setViewCalled, 'Set Breadcrumbs was called');
});
-
});
- describe('Fauxton Notifications', function () {
-
- it('should escape by default', function () {
- window.fauxton_xss_test_escaped = true;
- var view = FauxtonAPI.addNotification({
- msg: '<script>window.fauxton_xss_test_escaped = false;</script>',
- selector: 'body'
- });
- view.$el.remove();
- assert.ok(window.fauxton_xss_test_escaped);
- delete window.fauxton_xss_test_escaped;
- });
-
- it('should be able to render unescaped', function (done) {
- var view = FauxtonAPI.addNotification({
- msg: '<script>window.fauxton_xss_test_unescaped = true;</script>',
- selector: 'body',
- escape: false
- });
-
- view.promise().then(function () {
- view.$el.remove();
- assert.ok(window.fauxton_xss_test_unescaped);
- delete window.fauxton_xss_test_unescaped;
- done();
- });
- });
-
- it('should render escaped if the escape value is not explicitly false, ' +
- 'e.g. was forgotten in a direct call', function () {
-
- window.fauxton_xss_test2_escaped = true;
- var view = new Base.Notification({
- msg: '<script>window.fauxton_xss_test2_escaped = false;</script>',
- selector: 'body'
- }).render();
-
- view.$el.remove();
- assert.ok(window.fauxton_xss_test2_escaped);
- delete window.fauxton_xss_test2_escaped;
- });
-
- it('should close notification when ESCAPE key used', function () {
- var notification = FauxtonAPI.addNotification({
- msg: 'Close me!',
- selector: 'body'
- });
- var removeWithAnimationSpy = sinon.spy(notification, 'removeWithAnimation');
-
- notification.render();
-
- // manually trigger an ESCAPE key click
- $(document).trigger($.Event("keydown", { keyCode: 27 }));
-
- // confirm the remove method has now been called
- assert.ok(removeWithAnimationSpy.calledOnce);
- });
-
- });
});
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/05183749/assets/index.underscore
----------------------------------------------------------------------
diff --git a/assets/index.underscore b/assets/index.underscore
index c179ec4..782ca96 100644
--- a/assets/index.underscore
+++ b/assets/index.underscore
@@ -29,8 +29,7 @@
<body id="home">
- <div id="global-notifications" class="container errors-container"></div>
- <div id="notification-center"></div>
+ <div id="notifications"></div>
<!-- Main container. -->
<div role="main" id="main">
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/05183749/assets/less/fauxton.less
----------------------------------------------------------------------
diff --git a/assets/less/fauxton.less b/assets/less/fauxton.less
index f6dae31..5ed50d9 100644
--- a/assets/less/fauxton.less
+++ b/assets/less/fauxton.less
@@ -184,7 +184,8 @@ table.databases {
text-shadow: none;
.box-shadow(0px 4px 0px rgba(0,0,0,0.45));
.border-radius(0);
- border-bottom: 1px solid #3a2c2b;�
+ border-bottom: 1px solid #3a2c2b;
+ box-shadow: 0 4px 0 0 rgba(0, 0, 0, 0.4);
a, a:hover {
color: #cecece;
text-decoration: underline;
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/05183749/assets/less/templates.less
----------------------------------------------------------------------
diff --git a/assets/less/templates.less b/assets/less/templates.less
index 172de0f..ef1aca5 100644
--- a/assets/less/templates.less
+++ b/assets/less/templates.less
@@ -26,7 +26,7 @@
#global-notifications {
position: fixed;
- top: 0px;
+ top: 0;
display: block;
z-index: 100000;
width: 100%;
@@ -513,3 +513,8 @@ with_tabs_sidebar.html
}
}
}
+
+.notification-wrapper {
+ opacity: 0;
+ height: 0;
+}
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/05183749/package.json
----------------------------------------------------------------------
diff --git a/package.json b/package.json
index 05a8310..9265a0d 100644
--- a/package.json
+++ b/package.json
@@ -89,6 +89,7 @@
"urls": "~0.0.3",
"velocity-animate": "^1.2.3",
"visualizeRevTree": "git+https://github.com/neojski/visualizeRevTree.git#gh-pages",
+ "velocity-react": "^1.1.4",
"webpack": "^1.12.12",
"webpack-dev-server": "^1.14.1",
"zeroclipboard": "^2.2.0"
http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/05183749/test/nightwatch_tests/custom-commands/closeNotification.js
----------------------------------------------------------------------
diff --git a/test/nightwatch_tests/custom-commands/closeNotification.js b/test/nightwatch_tests/custom-commands/closeNotification.js
index 04120bc..5c29d56 100644
--- a/test/nightwatch_tests/custom-commands/closeNotification.js
+++ b/test/nightwatch_tests/custom-commands/closeNotification.js
@@ -14,7 +14,7 @@ var helpers = require('../helpers/helpers.js');
exports.command = function () {
var client = this,
- dismissSelector = '#global-notifications .js-dismiss';
+ dismissSelector = '#global-notifications .fonticon-cancel';
client
.waitForElementPresent(dismissSelector, helpers.maxWaitTime, false)