You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@couchdb.apache.org by benkeen <gi...@git.apache.org> on 2015/10/01 22:10:38 UTC

[GitHub] couchdb-fauxton pull request: Notification Center

GitHub user benkeen opened a pull request:

    https://github.com/apache/couchdb-fauxton/pull/544

    Notification Center

    This PR adds a new notification center panel. A bell icon now
    appears at the top right of all pages which when clicked,
    opens the notification center. That shows you a log of all
    notifications that have occured during your session with the
    option to clear single ones, clear all, and view by group.

You can merge this pull request into a Git repository by running:

    $ git pull https://github.com/benkeen/couchdb-fauxton notification-center

Alternatively you can review and apply these changes as the patch at:

    https://github.com/apache/couchdb-fauxton/pull/544.patch

To close this pull request, make a commit to your master/trunk branch
with (at least) the following in the commit message:

    This closes #544
    
----
commit 122fa2d2d472cef4ce0c25674101e473230be368
Author: Ben Keen <be...@gmail.com>
Date:   2015-09-28T20:41:53Z

    Notification Center
    
    This PR adds a new notification center panel. A bell icon now
    appears at the top right of all pages which when clicked,
    opens the notification center. That shows you a log of all
    notifications that have occured during your session with the
    option to clear single ones, clear all, and view by group.

----


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] couchdb-fauxton pull request: Notification Center

Posted by michellephung <gi...@git.apache.org>.
Github user michellephung commented on the pull request:

    https://github.com/apache/couchdb-fauxton/pull/544#issuecomment-146129565
  
    Looks great! can we make the old notifications smaller at least?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] couchdb-fauxton pull request: Notification Center

Posted by robertkowalski <gi...@git.apache.org>.
Github user robertkowalski commented on a diff in the pull request:

    https://github.com/apache/couchdb-fauxton/pull/544#discussion_r41138917
  
    --- Diff: app/addons/fauxton/components.react.jsx ---
    @@ -42,6 +54,14 @@ function (app, FauxtonAPI, React, ZeroClipboard) {
           ZeroClipboard.config({ moviePath: getZeroClipboardSwfPath() });
         },
     
    +    getClipboardElement: function () {
    +      if (this.props.displayType === 'icon') {
    +        return (<i className="fonticon-clipboard"></i>);
    +      } else {
    --- End diff --
    
    you can use an early return here


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] couchdb-fauxton pull request: Notification Center

Posted by robertkowalski <gi...@git.apache.org>.
Github user robertkowalski commented on a diff in the pull request:

    https://github.com/apache/couchdb-fauxton/pull/544#discussion_r41140262
  
    --- Diff: app/addons/fauxton/tests/componentsSpec.react.jsx ---
    @@ -204,5 +207,169 @@ define([
     
       });
     
    -});
     
    +  describe('Clipboard', function () {
    +    var container;
    +    beforeEach(function () {
    +      container = document.createElement('div');
    +    });
    +
    +    afterEach(function () {
    +      React.unmountComponentAtNode(container);
    +    });
    +
    +    it('shows a clipboard icon by default', function () {
    +      var clipboard = React.render(<Views.Clipboard text="copy me" />, container);
    +      assert.equal($(clipboard.getDOMNode()).find('.fonticon-clipboard').length, 1);
    +    });
    +
    +    it('shows text if specified', function () {
    +      var clipboard = React.render(<Views.Clipboard displayType="text" text="copy me" />, container);
    +      assert.equal($(clipboard.getDOMNode()).find('.fonticon-clipboard').length, 0);
    +    });
    +
    +    it('shows custom text if specified ', function () {
    +      var clipboard = React.render(<Views.Clipboard displayType="text" textDisplay='booyah!' text="copy me" />, container);
    +      assert.ok(/booyah!/.test($(clipboard.getDOMNode())[0].outerHTML));
    +    });
    +
    +  });
    +
    +
    +  describe('NotificationRow', function () {
    +    var container;
    +
    +    var notifications = {
    +      success: {
    +        notificationId: 1,
    +        type: 'success',
    +        msg: 'Success!',
    +        time: moment()
    +      },
    +      info: {
    +        notificationId: 2,
    +        type: 'info',
    +        msg: 'Error!',
    +        time: moment()
    +      },
    +      error: {
    +        notificationId: 3,
    +        type: 'error',
    +        msg: 'Error!',
    +        time: moment()
    +      }
    +    };
    +
    +    beforeEach(function () {
    +      container = document.createElement('div');
    +    });
    +
    +    afterEach(function () {
    +      React.unmountComponentAtNode(container);
    +    });
    +
    +    it('shows all notification types when "all" filter applied', function () {
    +      var row1 = React.render(
    +        <Views.NotificationRow filter="all" item={notifications.success} transitionSpeed={0} />,
    +        container
    +      );
    +      assert.equal($(row1.getDOMNode()).data('visible'), true);
    +      React.unmountComponentAtNode(container);
    +
    +      var row2 = React.render(
    +        <Views.NotificationRow filter="all" item={notifications.error} transitionSpeed={0} />,
    +        container
    +      );
    +      assert.equal($(row2.getDOMNode()).data('visible'), true);
    +      React.unmountComponentAtNode(container);
    +
    +      var row3 = React.render(
    +        <Views.NotificationRow filter="all" item={notifications.info} transitionSpeed={0} />,
    +        container
    +      );
    +      assert.equal($(row3.getDOMNode()).data('visible'), true);
    +      React.unmountComponentAtNode(container);
    +    });
    +
    +    it('hides notification when filter doesn\'t match', function () {
    +      var rowEl = React.render(
    +        <Views.NotificationRow filter="success" item={notifications.info} transitionSpeed={0} />,
    +        container
    +      );
    +      assert.equal($(rowEl.getDOMNode()).data('visible'), false);
    +    });
    +
    +    it('shows notification when filter exact match', function () {
    +      var rowEl = React.render(
    +        <Views.NotificationRow filter="info" item={notifications.info} transitionSpeed={0} />,
    +        container
    +      );
    +      assert.equal($(rowEl.getDOMNode()).data('visible'), true);
    +    });
    +
    +  });
    +
    +
    +  describe('NotificationCenterPanel', function () {
    +    var container;
    +
    +    beforeEach(function () {
    +      container = document.createElement('div');
    +      store.reset();
    +    });
    +
    +    afterEach(function () {
    +      React.unmountComponentAtNode(container);
    +    });
    +
    +    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 = React.render(<Views.NotificationCenterPanel />, container);
    +      assert.equal($(panelEl.getDOMNode()).find('.notification-list li[data-visible=true]').length, 6);
    --- End diff --
    
    what is that `data-visible` thing?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] couchdb-fauxton pull request: Notification Center

Posted by robertkowalski <gi...@git.apache.org>.
Github user robertkowalski commented on a diff in the pull request:

    https://github.com/apache/couchdb-fauxton/pull/544#discussion_r41139666
  
    --- Diff: app/addons/fauxton/components.react.jsx ---
    @@ -334,13 +354,246 @@ function (app, FauxtonAPI, React, ZeroClipboard) {
       });
     
     
    +  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({
    +
    +    getInitialState: function () {
    +      return this.getStoreState();
    +    },
    +
    +    getStoreState: function () {
    +      return {
    +        isVisible: notificationStore.isNotificationCenterVisible(),
    +        filter: notificationStore.getNotificationFilter(),
    +        notifications: notificationStore.getNotifications()
    +      };
    +    },
    +
    +    componentDidMount: function () {
    +      notificationStore.on('change', this.onChange, this);
    +    },
    +
    +    componentWillUnmount: function () {
    +      notificationStore.off('change', this.onChange);
    +    },
    +
    +    onChange: function () {
    +      if (this.isMounted()) {
    +        this.setState(this.getStoreState());
    +      }
    +    },
    +
    +    getNotifications: function () {
    +      if (!this.state.notifications.length) {
    +        return (
    +          <li className="no-notifications">No notifications.</li>
    +        );
    +      }
    +
    +      return _.map(this.state.notifications, function (notification, i) {
    +        return (
    +          <NotificationRow
    +            isVisible={this.state.isVisible}
    +            item={notification}
    +            filter={this.state.filter}
    +            key={notification.notificationId}
    +          />
    +        );
    +      }, this);
    +    },
    +
    +    selectFilter: function (e) {
    +      var filter = $(e.target).closest('li').data('filter');
    +      Actions.selectNotificationFilter(filter);
    +    },
    +
    +    render: function () {
    +      var panelClasses = 'notification-center-panel flex-layout flex-col';
    +      if (this.state.isVisible) {
    +        panelClasses += ' visible';
    +      }
    +
    +      var filterClasses = {
    +        all: 'flex-body',
    +        success: 'flex-body',
    +        error: 'flex-body',
    +        info: 'flex-body'
    +      };
    +      filterClasses[this.state.filter] += ' selected';
    +
    +      var maskClasses = 'notification-page-mask' + ((this.state.isVisible) ? ' visible' : '');
    +      return (
    +        <div>
    +          <div className={panelClasses}>
    +
    +            <header className="flex-layout flex-row">
    +              <span className="fonticon fonticon-bell"></span>
    +              <h1 className="flex-body">Notifications</h1>
    +              <button type="button" aria-hidden="true" onClick={Actions.hideNotificationCenter}>×</button>
    +            </header>
    +
    +            <ul className="notification-filter flex-layout flex-row" onClick={this.selectFilter}>
    +              <li className={filterClasses.all} data-filter="all" title="All notifications">All</li>
    +              <li className={filterClasses.success} data-filter="success" title="Success notifications">
    +                <span className="fonticon fonticon-ok-circled"></span>
    +              </li>
    +              <li className={filterClasses.error} data-filter="error" title="Error notifications">
    +                <span className="fonticon fonticon-attention-circled"></span>
    +              </li>
    +              <li className={filterClasses.info} data-filter="info" title="Info notifications">
    +                <span className="fonticon fonticon-info-circled"></span>
    +              </li>
    +            </ul>
    +
    +            <div className="flex-body">
    +              <ul className="notification-list">
    +                {this.getNotifications()}
    +              </ul>
    +            </div>
    +
    +            <footer>
    +              <input type="button" value="Clear All" className="btn btn-small btn-info" onClick={Actions.clearAllNotifications} />
    +            </footer>
    +          </div>
    +
    +          <div className={maskClasses} onClick={Actions.hideNotificationCenter}></div>
    +        </div>
    +      );
    +    }
    +  });
    +
    +  var NotificationRow = React.createClass({
    +    propTypes: {
    +      item: React.PropTypes.object.isRequired,
    +      filter: React.PropTypes.string.isRequired,
    +      transitionSpeed: React.PropTypes.number
    +    },
    +
    +    getDefaultProps: function () {
    +      return {
    +        transitionSpeed: 300
    +      };
    +    },
    +
    +    clearNotification: function () {
    +      var notificationId = this.props.item.notificationId;
    +      this.hide(function () {
    +        Actions.clearSingleNotification(notificationId);
    +      });
    +    },
    +
    +    componentDidMount: function () {
    +      this.setState({
    +        elementHeight: this.getHeight()
    +      });
    +    },
    +
    +    componentDidUpdate: function (prevProps) {
    +      // in order for the nice slide effects to work we need a concrete element height to slide to and from.
    +      // $.outerHeight() only works reliably on visible elements, hence this additional setState here
    +      if (!prevProps.isVisible && this.props.isVisible) {
    +        this.setState({
    +          elementHeight: this.getHeight()
    +        });
    +      }
    +
    +      var show = true;
    +      if (this.props.filter !== 'all') {
    +        show = this.props.item.type === this.props.filter;
    +      }
    +      if (show) {
    +        console.log(this.state.elementHeight);
    --- End diff --
    
    debug?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] couchdb-fauxton pull request: Notification Center

Posted by asfgit <gi...@git.apache.org>.
Github user asfgit closed the pull request at:

    https://github.com/apache/couchdb-fauxton/pull/544


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] couchdb-fauxton pull request: Notification Center

Posted by benkeen <gi...@git.apache.org>.
Github user benkeen commented on a diff in the pull request:

    https://github.com/apache/couchdb-fauxton/pull/544#discussion_r41405237
  
    --- Diff: app/addons/fauxton/components.react.jsx ---
    @@ -334,13 +354,246 @@ function (app, FauxtonAPI, React, ZeroClipboard) {
       });
     
     
    +  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({
    +
    +    getInitialState: function () {
    +      return this.getStoreState();
    +    },
    +
    +    getStoreState: function () {
    +      return {
    +        isVisible: notificationStore.isNotificationCenterVisible(),
    +        filter: notificationStore.getNotificationFilter(),
    +        notifications: notificationStore.getNotifications()
    +      };
    +    },
    +
    +    componentDidMount: function () {
    +      notificationStore.on('change', this.onChange, this);
    +    },
    +
    +    componentWillUnmount: function () {
    +      notificationStore.off('change', this.onChange);
    +    },
    +
    +    onChange: function () {
    +      if (this.isMounted()) {
    +        this.setState(this.getStoreState());
    +      }
    +    },
    +
    +    getNotifications: function () {
    +      if (!this.state.notifications.length) {
    +        return (
    +          <li className="no-notifications">No notifications.</li>
    +        );
    +      }
    +
    +      return _.map(this.state.notifications, function (notification, i) {
    +        return (
    +          <NotificationRow
    +            isVisible={this.state.isVisible}
    +            item={notification}
    +            filter={this.state.filter}
    +            key={notification.notificationId}
    +          />
    +        );
    +      }, this);
    +    },
    +
    +    selectFilter: function (e) {
    +      var filter = $(e.target).closest('li').data('filter');
    +      Actions.selectNotificationFilter(filter);
    +    },
    +
    +    render: function () {
    +      var panelClasses = 'notification-center-panel flex-layout flex-col';
    +      if (this.state.isVisible) {
    +        panelClasses += ' visible';
    +      }
    +
    +      var filterClasses = {
    +        all: 'flex-body',
    +        success: 'flex-body',
    +        error: 'flex-body',
    +        info: 'flex-body'
    +      };
    +      filterClasses[this.state.filter] += ' selected';
    +
    +      var maskClasses = 'notification-page-mask' + ((this.state.isVisible) ? ' visible' : '');
    +      return (
    +        <div>
    +          <div className={panelClasses}>
    +
    +            <header className="flex-layout flex-row">
    +              <span className="fonticon fonticon-bell"></span>
    +              <h1 className="flex-body">Notifications</h1>
    +              <button type="button" aria-hidden="true" onClick={Actions.hideNotificationCenter}>×</button>
    +            </header>
    +
    +            <ul className="notification-filter flex-layout flex-row" onClick={this.selectFilter}>
    +              <li className={filterClasses.all} data-filter="all" title="All notifications">All</li>
    +              <li className={filterClasses.success} data-filter="success" title="Success notifications">
    +                <span className="fonticon fonticon-ok-circled"></span>
    +              </li>
    +              <li className={filterClasses.error} data-filter="error" title="Error notifications">
    +                <span className="fonticon fonticon-attention-circled"></span>
    +              </li>
    +              <li className={filterClasses.info} data-filter="info" title="Info notifications">
    +                <span className="fonticon fonticon-info-circled"></span>
    +              </li>
    +            </ul>
    +
    +            <div className="flex-body">
    +              <ul className="notification-list">
    +                {this.getNotifications()}
    +              </ul>
    +            </div>
    +
    +            <footer>
    +              <input type="button" value="Clear All" className="btn btn-small btn-info" onClick={Actions.clearAllNotifications} />
    +            </footer>
    +          </div>
    +
    +          <div className={maskClasses} onClick={Actions.hideNotificationCenter}></div>
    +        </div>
    +      );
    +    }
    +  });
    +
    +  var NotificationRow = React.createClass({
    +    propTypes: {
    +      item: React.PropTypes.object.isRequired,
    +      filter: React.PropTypes.string.isRequired,
    +      transitionSpeed: React.PropTypes.number
    +    },
    +
    +    getDefaultProps: function () {
    +      return {
    +        transitionSpeed: 300
    +      };
    +    },
    +
    +    clearNotification: function () {
    +      var notificationId = this.props.item.notificationId;
    +      this.hide(function () {
    +        Actions.clearSingleNotification(notificationId);
    +      });
    +    },
    +
    +    componentDidMount: function () {
    +      this.setState({
    +        elementHeight: this.getHeight()
    +      });
    +    },
    +
    +    componentDidUpdate: function (prevProps) {
    +      // in order for the nice slide effects to work we need a concrete element height to slide to and from.
    +      // $.outerHeight() only works reliably on visible elements, hence this additional setState here
    +      if (!prevProps.isVisible && this.props.isVisible) {
    +        this.setState({
    +          elementHeight: this.getHeight()
    +        });
    +      }
    +
    +      var show = true;
    +      if (this.props.filter !== 'all') {
    +        show = this.props.item.type === this.props.filter;
    +      }
    +      if (show) {
    +        console.log(this.state.elementHeight);
    +        $(this.getDOMNode()).velocity({ opacity: 1, height: this.state.elementHeight }, this.props.transitionSpeed);
    +      } else {
    +        this.hide();
    +      }
    +    },
    +
    +    getHeight: function () {
    +      return $(this.getDOMNode()).outerHeight(true);
    +    },
    +
    +    hide: function (onHidden) {
    +      $(this.getDOMNode()).velocity({ opacity: 0, height: 0 }, this.props.transitionSpeed, function () {
    +        if (onHidden) {
    +          onHidden();
    +        }
    +      });
    +    },
    +
    +    render: function () {
    +      var iconMap = {
    +        success: 'fonticon-ok-circled',
    +        error: 'fonticon-attention-circled',
    +        info: 'fonticon-info-circled'
    +      };
    +
    +      var timeElapsed = this.props.item.time.fromNow();
    +
    +      // we can safely do this because the store ensures all notifications are of known types
    +      var rowIconClasses = 'fonticon ' + iconMap[this.props.item.type];
    +      var classes = 'flex-layout flex-row';
    +
    +      // for testing purposes
    +      var visible = (this.props.filter === 'all' || this.props.filter === this.props.item.type) ? 'true' : 'false';
    +
    +      // N.B. wrapper <div> needed to ensure smooth hide/show transitions
    +      return (
    +        <li data-visible={visible}>
    --- End diff --
    
    Neither would work in this instance. The visibility is done by velocity sliding up/down the component, not by adding a class. I can add a class if you'd prefer that, but it seems the same as the data-role, only more confusing because people would think it had something to do with setting the visibility.
    
    After the end of the action the height is zero and opacity is 0. So it should be technically visible, there - but I'll double check.
    



---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] couchdb-fauxton pull request: Notification Center

Posted by robertkowalski <gi...@git.apache.org>.
Github user robertkowalski commented on a diff in the pull request:

    https://github.com/apache/couchdb-fauxton/pull/544#discussion_r41140280
  
    --- Diff: app/addons/fauxton/tests/componentsSpec.react.jsx ---
    @@ -204,5 +207,169 @@ define([
     
       });
     
    -});
     
    +  describe('Clipboard', function () {
    +    var container;
    +    beforeEach(function () {
    +      container = document.createElement('div');
    +    });
    +
    +    afterEach(function () {
    +      React.unmountComponentAtNode(container);
    +    });
    +
    +    it('shows a clipboard icon by default', function () {
    +      var clipboard = React.render(<Views.Clipboard text="copy me" />, container);
    +      assert.equal($(clipboard.getDOMNode()).find('.fonticon-clipboard').length, 1);
    +    });
    +
    +    it('shows text if specified', function () {
    +      var clipboard = React.render(<Views.Clipboard displayType="text" text="copy me" />, container);
    +      assert.equal($(clipboard.getDOMNode()).find('.fonticon-clipboard').length, 0);
    +    });
    +
    +    it('shows custom text if specified ', function () {
    +      var clipboard = React.render(<Views.Clipboard displayType="text" textDisplay='booyah!' text="copy me" />, container);
    +      assert.ok(/booyah!/.test($(clipboard.getDOMNode())[0].outerHTML));
    +    });
    +
    +  });
    +
    +
    +  describe('NotificationRow', function () {
    +    var container;
    +
    +    var notifications = {
    +      success: {
    +        notificationId: 1,
    +        type: 'success',
    +        msg: 'Success!',
    +        time: moment()
    +      },
    +      info: {
    +        notificationId: 2,
    +        type: 'info',
    +        msg: 'Error!',
    +        time: moment()
    +      },
    +      error: {
    +        notificationId: 3,
    +        type: 'error',
    +        msg: 'Error!',
    +        time: moment()
    +      }
    +    };
    +
    +    beforeEach(function () {
    +      container = document.createElement('div');
    +    });
    +
    +    afterEach(function () {
    +      React.unmountComponentAtNode(container);
    +    });
    +
    +    it('shows all notification types when "all" filter applied', function () {
    +      var row1 = React.render(
    +        <Views.NotificationRow filter="all" item={notifications.success} transitionSpeed={0} />,
    +        container
    +      );
    +      assert.equal($(row1.getDOMNode()).data('visible'), true);
    +      React.unmountComponentAtNode(container);
    +
    +      var row2 = React.render(
    +        <Views.NotificationRow filter="all" item={notifications.error} transitionSpeed={0} />,
    +        container
    +      );
    +      assert.equal($(row2.getDOMNode()).data('visible'), true);
    +      React.unmountComponentAtNode(container);
    +
    +      var row3 = React.render(
    +        <Views.NotificationRow filter="all" item={notifications.info} transitionSpeed={0} />,
    +        container
    +      );
    +      assert.equal($(row3.getDOMNode()).data('visible'), true);
    +      React.unmountComponentAtNode(container);
    +    });
    +
    +    it('hides notification when filter doesn\'t match', function () {
    +      var rowEl = React.render(
    +        <Views.NotificationRow filter="success" item={notifications.info} transitionSpeed={0} />,
    +        container
    +      );
    +      assert.equal($(rowEl.getDOMNode()).data('visible'), false);
    +    });
    +
    +    it('shows notification when filter exact match', function () {
    +      var rowEl = React.render(
    +        <Views.NotificationRow filter="info" item={notifications.info} transitionSpeed={0} />,
    +        container
    +      );
    +      assert.equal($(rowEl.getDOMNode()).data('visible'), true);
    +    });
    +
    +  });
    +
    +
    +  describe('NotificationCenterPanel', function () {
    +    var container;
    +
    +    beforeEach(function () {
    +      container = document.createElement('div');
    +      store.reset();
    +    });
    +
    +    afterEach(function () {
    +      React.unmountComponentAtNode(container);
    +    });
    +
    +    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 = React.render(<Views.NotificationCenterPanel />, container);
    +      assert.equal($(panelEl.getDOMNode()).find('.notification-list li[data-visible=true]').length, 6);
    --- End diff --
    
    it feels like an jquery application with some react on top


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] couchdb-fauxton pull request: Notification Center

Posted by robertkowalski <gi...@git.apache.org>.
Github user robertkowalski commented on a diff in the pull request:

    https://github.com/apache/couchdb-fauxton/pull/544#discussion_r41139622
  
    --- Diff: app/addons/fauxton/components.react.jsx ---
    @@ -334,13 +354,246 @@ function (app, FauxtonAPI, React, ZeroClipboard) {
       });
     
     
    +  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({
    +
    +    getInitialState: function () {
    +      return this.getStoreState();
    +    },
    +
    +    getStoreState: function () {
    +      return {
    +        isVisible: notificationStore.isNotificationCenterVisible(),
    +        filter: notificationStore.getNotificationFilter(),
    +        notifications: notificationStore.getNotifications()
    +      };
    +    },
    +
    +    componentDidMount: function () {
    +      notificationStore.on('change', this.onChange, this);
    +    },
    +
    +    componentWillUnmount: function () {
    +      notificationStore.off('change', this.onChange);
    +    },
    +
    +    onChange: function () {
    +      if (this.isMounted()) {
    +        this.setState(this.getStoreState());
    +      }
    +    },
    +
    +    getNotifications: function () {
    +      if (!this.state.notifications.length) {
    +        return (
    +          <li className="no-notifications">No notifications.</li>
    +        );
    +      }
    +
    +      return _.map(this.state.notifications, function (notification, i) {
    +        return (
    +          <NotificationRow
    +            isVisible={this.state.isVisible}
    +            item={notification}
    +            filter={this.state.filter}
    +            key={notification.notificationId}
    +          />
    +        );
    +      }, this);
    +    },
    +
    +    selectFilter: function (e) {
    +      var filter = $(e.target).closest('li').data('filter');
    +      Actions.selectNotificationFilter(filter);
    +    },
    +
    +    render: function () {
    +      var panelClasses = 'notification-center-panel flex-layout flex-col';
    +      if (this.state.isVisible) {
    +        panelClasses += ' visible';
    +      }
    +
    +      var filterClasses = {
    +        all: 'flex-body',
    +        success: 'flex-body',
    +        error: 'flex-body',
    +        info: 'flex-body'
    +      };
    +      filterClasses[this.state.filter] += ' selected';
    +
    +      var maskClasses = 'notification-page-mask' + ((this.state.isVisible) ? ' visible' : '');
    +      return (
    +        <div>
    +          <div className={panelClasses}>
    +
    +            <header className="flex-layout flex-row">
    +              <span className="fonticon fonticon-bell"></span>
    +              <h1 className="flex-body">Notifications</h1>
    +              <button type="button" aria-hidden="true" onClick={Actions.hideNotificationCenter}>×</button>
    +            </header>
    +
    +            <ul className="notification-filter flex-layout flex-row" onClick={this.selectFilter}>
    +              <li className={filterClasses.all} data-filter="all" title="All notifications">All</li>
    +              <li className={filterClasses.success} data-filter="success" title="Success notifications">
    +                <span className="fonticon fonticon-ok-circled"></span>
    +              </li>
    +              <li className={filterClasses.error} data-filter="error" title="Error notifications">
    +                <span className="fonticon fonticon-attention-circled"></span>
    +              </li>
    +              <li className={filterClasses.info} data-filter="info" title="Info notifications">
    +                <span className="fonticon fonticon-info-circled"></span>
    +              </li>
    +            </ul>
    +
    +            <div className="flex-body">
    +              <ul className="notification-list">
    +                {this.getNotifications()}
    +              </ul>
    +            </div>
    +
    +            <footer>
    +              <input type="button" value="Clear All" className="btn btn-small btn-info" onClick={Actions.clearAllNotifications} />
    +            </footer>
    +          </div>
    +
    +          <div className={maskClasses} onClick={Actions.hideNotificationCenter}></div>
    +        </div>
    +      );
    +    }
    +  });
    +
    +  var NotificationRow = React.createClass({
    +    propTypes: {
    +      item: React.PropTypes.object.isRequired,
    +      filter: React.PropTypes.string.isRequired,
    +      transitionSpeed: React.PropTypes.number
    +    },
    +
    +    getDefaultProps: function () {
    +      return {
    +        transitionSpeed: 300
    +      };
    +    },
    +
    +    clearNotification: function () {
    +      var notificationId = this.props.item.notificationId;
    +      this.hide(function () {
    +        Actions.clearSingleNotification(notificationId);
    +      });
    +    },
    +
    +    componentDidMount: function () {
    +      this.setState({
    +        elementHeight: this.getHeight()
    +      });
    +    },
    +
    +    componentDidUpdate: function (prevProps) {
    +      // in order for the nice slide effects to work we need a concrete element height to slide to and from.
    +      // $.outerHeight() only works reliably on visible elements, hence this additional setState here
    +      if (!prevProps.isVisible && this.props.isVisible) {
    +        this.setState({
    +          elementHeight: this.getHeight()
    +        });
    +      }
    +
    +      var show = true;
    +      if (this.props.filter !== 'all') {
    +        show = this.props.item.type === this.props.filter;
    +      }
    +      if (show) {
    +        console.log(this.state.elementHeight);
    +        $(this.getDOMNode()).velocity({ opacity: 1, height: this.state.elementHeight }, this.props.transitionSpeed);
    +      } else {
    +        this.hide();
    --- End diff --
    
    early returns make it easier readable and you don't need to nest that much


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] couchdb-fauxton pull request: Notification Center

Posted by benkeen <gi...@git.apache.org>.
Github user benkeen commented on a diff in the pull request:

    https://github.com/apache/couchdb-fauxton/pull/544#discussion_r41172029
  
    --- Diff: app/addons/fauxton/components.react.jsx ---
    @@ -334,13 +354,246 @@ function (app, FauxtonAPI, React, ZeroClipboard) {
       });
     
     
    +  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({
    +
    +    getInitialState: function () {
    +      return this.getStoreState();
    +    },
    +
    +    getStoreState: function () {
    +      return {
    +        isVisible: notificationStore.isNotificationCenterVisible(),
    +        filter: notificationStore.getNotificationFilter(),
    +        notifications: notificationStore.getNotifications()
    +      };
    +    },
    +
    +    componentDidMount: function () {
    +      notificationStore.on('change', this.onChange, this);
    +    },
    +
    +    componentWillUnmount: function () {
    +      notificationStore.off('change', this.onChange);
    +    },
    +
    +    onChange: function () {
    +      if (this.isMounted()) {
    +        this.setState(this.getStoreState());
    +      }
    +    },
    +
    +    getNotifications: function () {
    +      if (!this.state.notifications.length) {
    +        return (
    +          <li className="no-notifications">No notifications.</li>
    +        );
    +      }
    +
    +      return _.map(this.state.notifications, function (notification, i) {
    +        return (
    +          <NotificationRow
    +            isVisible={this.state.isVisible}
    +            item={notification}
    +            filter={this.state.filter}
    +            key={notification.notificationId}
    +          />
    +        );
    +      }, this);
    +    },
    +
    +    selectFilter: function (e) {
    +      var filter = $(e.target).closest('li').data('filter');
    +      Actions.selectNotificationFilter(filter);
    +    },
    +
    +    render: function () {
    +      var panelClasses = 'notification-center-panel flex-layout flex-col';
    +      if (this.state.isVisible) {
    +        panelClasses += ' visible';
    +      }
    +
    +      var filterClasses = {
    +        all: 'flex-body',
    +        success: 'flex-body',
    +        error: 'flex-body',
    +        info: 'flex-body'
    +      };
    +      filterClasses[this.state.filter] += ' selected';
    +
    +      var maskClasses = 'notification-page-mask' + ((this.state.isVisible) ? ' visible' : '');
    +      return (
    +        <div>
    +          <div className={panelClasses}>
    +
    +            <header className="flex-layout flex-row">
    +              <span className="fonticon fonticon-bell"></span>
    +              <h1 className="flex-body">Notifications</h1>
    +              <button type="button" aria-hidden="true" onClick={Actions.hideNotificationCenter}>×</button>
    +            </header>
    +
    +            <ul className="notification-filter flex-layout flex-row" onClick={this.selectFilter}>
    +              <li className={filterClasses.all} data-filter="all" title="All notifications">All</li>
    +              <li className={filterClasses.success} data-filter="success" title="Success notifications">
    +                <span className="fonticon fonticon-ok-circled"></span>
    +              </li>
    +              <li className={filterClasses.error} data-filter="error" title="Error notifications">
    +                <span className="fonticon fonticon-attention-circled"></span>
    +              </li>
    +              <li className={filterClasses.info} data-filter="info" title="Info notifications">
    +                <span className="fonticon fonticon-info-circled"></span>
    +              </li>
    +            </ul>
    +
    +            <div className="flex-body">
    +              <ul className="notification-list">
    +                {this.getNotifications()}
    +              </ul>
    +            </div>
    +
    +            <footer>
    +              <input type="button" value="Clear All" className="btn btn-small btn-info" onClick={Actions.clearAllNotifications} />
    +            </footer>
    +          </div>
    +
    +          <div className={maskClasses} onClick={Actions.hideNotificationCenter}></div>
    +        </div>
    +      );
    +    }
    +  });
    +
    +  var NotificationRow = React.createClass({
    +    propTypes: {
    +      item: React.PropTypes.object.isRequired,
    +      filter: React.PropTypes.string.isRequired,
    +      transitionSpeed: React.PropTypes.number
    +    },
    +
    +    getDefaultProps: function () {
    +      return {
    +        transitionSpeed: 300
    +      };
    +    },
    +
    +    clearNotification: function () {
    +      var notificationId = this.props.item.notificationId;
    +      this.hide(function () {
    +        Actions.clearSingleNotification(notificationId);
    +      });
    +    },
    +
    +    componentDidMount: function () {
    +      this.setState({
    +        elementHeight: this.getHeight()
    +      });
    +    },
    +
    +    componentDidUpdate: function (prevProps) {
    +      // in order for the nice slide effects to work we need a concrete element height to slide to and from.
    +      // $.outerHeight() only works reliably on visible elements, hence this additional setState here
    +      if (!prevProps.isVisible && this.props.isVisible) {
    +        this.setState({
    +          elementHeight: this.getHeight()
    +        });
    +      }
    +
    +      var show = true;
    +      if (this.props.filter !== 'all') {
    +        show = this.props.item.type === this.props.filter;
    +      }
    +      if (show) {
    +        console.log(this.state.elementHeight);
    +        $(this.getDOMNode()).velocity({ opacity: 1, height: this.state.elementHeight }, this.props.transitionSpeed);
    +      } else {
    +        this.hide();
    +      }
    +    },
    +
    +    getHeight: function () {
    +      return $(this.getDOMNode()).outerHeight(true);
    +    },
    +
    +    hide: function (onHidden) {
    +      $(this.getDOMNode()).velocity({ opacity: 0, height: 0 }, this.props.transitionSpeed, function () {
    +        if (onHidden) {
    +          onHidden();
    +        }
    +      });
    +    },
    +
    +    render: function () {
    +      var iconMap = {
    +        success: 'fonticon-ok-circled',
    +        error: 'fonticon-attention-circled',
    +        info: 'fonticon-info-circled'
    +      };
    +
    +      var timeElapsed = this.props.item.time.fromNow();
    +
    +      // we can safely do this because the store ensures all notifications are of known types
    +      var rowIconClasses = 'fonticon ' + iconMap[this.props.item.type];
    +      var classes = 'flex-layout flex-row';
    +
    +      // for testing purposes
    +      var visible = (this.props.filter === 'all' || this.props.filter === this.props.item.type) ? 'true' : 'false';
    +
    +      // N.B. wrapper <div> needed to ensure smooth hide/show transitions
    +      return (
    +        <li data-visible={visible}>
    +          <div className={classes}>
    +            <span className={rowIconClasses}></span>
    +            <div className="flex-body">
    +              <p dangerouslySetInnerHTML={{__html: this.props.item.msg}}></p>
    +              <div className="notification-actions">
    +                <span className="time-elapsed">{timeElapsed}</span>
    +                <span className="divider">|</span>
    +                <Clipboard text={this.props.item.msg} displayType="text" />
    +              </div>
    +            </div>
    +            <button type="button" aria-hidden="true" onClick={this.clearNotification}>×</button>
    --- End diff --
    
    Good one. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] couchdb-fauxton pull request: Notification Center

Posted by robertkowalski <gi...@git.apache.org>.
Github user robertkowalski commented on a diff in the pull request:

    https://github.com/apache/couchdb-fauxton/pull/544#discussion_r41139920
  
    --- Diff: app/addons/fauxton/stores.js ---
    @@ -0,0 +1,132 @@
    +// 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([
    +  'api',
    +  'addons/fauxton/actiontypes',
    +  'moment'
    +],
    +
    +function (FauxtonAPI, ActionTypes, moment) {
    +  var Stores = {};
    +
    +  // static var used to assign a unique ID to each notification
    +  var counter = 0;
    +  var validNotificationTypes = ['success', 'error', 'info'];
    +
    +
    +  /**
    +   * Notifications are of the form:
    +   * {
    +   *   notificationId: N,
    +   *   message: "string",
    +   *   type: "success"|etc. see above list
    +   *   clear: true|false,
    +   *   escape: true|false
    +   * }
    +   */
    +
    +  Stores.NotificationStore = FauxtonAPI.Store.extend({
    +    initialize: function () {
    +      this.reset();
    +    },
    +
    +    reset: function () {
    +      this._notifications = [];
    +      this._notificationCenterVisible = false;
    +      this._selectedNotificationFilter = 'all';
    +    },
    +
    +    isNotificationCenterVisible: function () {
    +      return this._notificationCenterVisible;
    +    },
    +
    +    addNotification: function (info) {
    +      if (_.isEmpty(info.type) || !_.contains(validNotificationTypes, info.type)) {
    +        console.warn('Invalid message type: ', info);
    +        return;
    --- End diff --
    
    <3


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] couchdb-fauxton pull request: Notification Center

Posted by michellephung <gi...@git.apache.org>.
Github user michellephung commented on the pull request:

    https://github.com/apache/couchdb-fauxton/pull/544#issuecomment-146224516
  
    sounds good


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] couchdb-fauxton pull request: Notification Center

Posted by benkeen <gi...@git.apache.org>.
Github user benkeen commented on a diff in the pull request:

    https://github.com/apache/couchdb-fauxton/pull/544#discussion_r41170394
  
    --- Diff: app/addons/fauxton/components.react.jsx ---
    @@ -334,13 +354,246 @@ function (app, FauxtonAPI, React, ZeroClipboard) {
       });
     
     
    +  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({
    +
    +    getInitialState: function () {
    +      return this.getStoreState();
    +    },
    +
    +    getStoreState: function () {
    +      return {
    +        isVisible: notificationStore.isNotificationCenterVisible(),
    +        filter: notificationStore.getNotificationFilter(),
    +        notifications: notificationStore.getNotifications()
    +      };
    +    },
    +
    +    componentDidMount: function () {
    +      notificationStore.on('change', this.onChange, this);
    +    },
    +
    +    componentWillUnmount: function () {
    +      notificationStore.off('change', this.onChange);
    +    },
    +
    +    onChange: function () {
    +      if (this.isMounted()) {
    +        this.setState(this.getStoreState());
    +      }
    +    },
    +
    +    getNotifications: function () {
    +      if (!this.state.notifications.length) {
    +        return (
    +          <li className="no-notifications">No notifications.</li>
    +        );
    +      }
    +
    +      return _.map(this.state.notifications, function (notification, i) {
    +        return (
    +          <NotificationRow
    +            isVisible={this.state.isVisible}
    +            item={notification}
    +            filter={this.state.filter}
    +            key={notification.notificationId}
    +          />
    +        );
    +      }, this);
    +    },
    +
    +    selectFilter: function (e) {
    +      var filter = $(e.target).closest('li').data('filter');
    --- End diff --
    
    I think it's okay in this case, no? I need to do a little hunting in the DOM to locate the parent list item based on whatever element triggered the event, so jQuery seemed the simplest. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] couchdb-fauxton pull request: Notification Center

Posted by benkeen <gi...@git.apache.org>.
Github user benkeen commented on a diff in the pull request:

    https://github.com/apache/couchdb-fauxton/pull/544#discussion_r41170828
  
    --- Diff: app/addons/fauxton/components.react.jsx ---
    @@ -334,13 +354,246 @@ function (app, FauxtonAPI, React, ZeroClipboard) {
       });
     
     
    +  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({
    +
    +    getInitialState: function () {
    +      return this.getStoreState();
    +    },
    +
    +    getStoreState: function () {
    +      return {
    +        isVisible: notificationStore.isNotificationCenterVisible(),
    +        filter: notificationStore.getNotificationFilter(),
    +        notifications: notificationStore.getNotifications()
    +      };
    +    },
    +
    +    componentDidMount: function () {
    +      notificationStore.on('change', this.onChange, this);
    +    },
    +
    +    componentWillUnmount: function () {
    +      notificationStore.off('change', this.onChange);
    +    },
    +
    +    onChange: function () {
    +      if (this.isMounted()) {
    +        this.setState(this.getStoreState());
    +      }
    +    },
    +
    +    getNotifications: function () {
    +      if (!this.state.notifications.length) {
    +        return (
    +          <li className="no-notifications">No notifications.</li>
    +        );
    +      }
    +
    +      return _.map(this.state.notifications, function (notification, i) {
    +        return (
    +          <NotificationRow
    +            isVisible={this.state.isVisible}
    +            item={notification}
    +            filter={this.state.filter}
    +            key={notification.notificationId}
    +          />
    +        );
    +      }, this);
    +    },
    +
    +    selectFilter: function (e) {
    +      var filter = $(e.target).closest('li').data('filter');
    +      Actions.selectNotificationFilter(filter);
    +    },
    +
    +    render: function () {
    +      var panelClasses = 'notification-center-panel flex-layout flex-col';
    +      if (this.state.isVisible) {
    +        panelClasses += ' visible';
    +      }
    +
    +      var filterClasses = {
    +        all: 'flex-body',
    +        success: 'flex-body',
    +        error: 'flex-body',
    +        info: 'flex-body'
    +      };
    +      filterClasses[this.state.filter] += ' selected';
    +
    +      var maskClasses = 'notification-page-mask' + ((this.state.isVisible) ? ' visible' : '');
    +      return (
    +        <div>
    +          <div className={panelClasses}>
    +
    +            <header className="flex-layout flex-row">
    +              <span className="fonticon fonticon-bell"></span>
    +              <h1 className="flex-body">Notifications</h1>
    +              <button type="button" aria-hidden="true" onClick={Actions.hideNotificationCenter}>×</button>
    +            </header>
    +
    +            <ul className="notification-filter flex-layout flex-row" onClick={this.selectFilter}>
    +              <li className={filterClasses.all} data-filter="all" title="All notifications">All</li>
    +              <li className={filterClasses.success} data-filter="success" title="Success notifications">
    +                <span className="fonticon fonticon-ok-circled"></span>
    +              </li>
    +              <li className={filterClasses.error} data-filter="error" title="Error notifications">
    +                <span className="fonticon fonticon-attention-circled"></span>
    +              </li>
    +              <li className={filterClasses.info} data-filter="info" title="Info notifications">
    +                <span className="fonticon fonticon-info-circled"></span>
    +              </li>
    +            </ul>
    --- End diff --
    
    Yeah, this would work... I think whether it's an improvement is debatable, though, having to add 3X the number of event handlers. (What if there were 10 items & they couldn't be added via a loop?) But fair enough point about generally wanting to avoid accessing DOM directly.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] couchdb-fauxton pull request: Notification Center

Posted by benkeen <gi...@git.apache.org>.
Github user benkeen commented on a diff in the pull request:

    https://github.com/apache/couchdb-fauxton/pull/544#discussion_r41171980
  
    --- Diff: app/addons/fauxton/tests/componentsSpec.react.jsx ---
    @@ -204,5 +207,169 @@ define([
     
       });
     
    -});
     
    +  describe('Clipboard', function () {
    +    var container;
    +    beforeEach(function () {
    +      container = document.createElement('div');
    +    });
    +
    +    afterEach(function () {
    +      React.unmountComponentAtNode(container);
    +    });
    +
    +    it('shows a clipboard icon by default', function () {
    +      var clipboard = React.render(<Views.Clipboard text="copy me" />, container);
    +      assert.equal($(clipboard.getDOMNode()).find('.fonticon-clipboard').length, 1);
    +    });
    +
    +    it('shows text if specified', function () {
    +      var clipboard = React.render(<Views.Clipboard displayType="text" text="copy me" />, container);
    +      assert.equal($(clipboard.getDOMNode()).find('.fonticon-clipboard').length, 0);
    +    });
    +
    +    it('shows custom text if specified ', function () {
    +      var clipboard = React.render(<Views.Clipboard displayType="text" textDisplay='booyah!' text="copy me" />, container);
    +      assert.ok(/booyah!/.test($(clipboard.getDOMNode())[0].outerHTML));
    +    });
    +
    +  });
    +
    +
    +  describe('NotificationRow', function () {
    +    var container;
    +
    +    var notifications = {
    +      success: {
    +        notificationId: 1,
    +        type: 'success',
    +        msg: 'Success!',
    +        time: moment()
    +      },
    +      info: {
    +        notificationId: 2,
    +        type: 'info',
    +        msg: 'Error!',
    +        time: moment()
    +      },
    +      error: {
    +        notificationId: 3,
    +        type: 'error',
    +        msg: 'Error!',
    +        time: moment()
    +      }
    +    };
    +
    +    beforeEach(function () {
    +      container = document.createElement('div');
    +    });
    +
    +    afterEach(function () {
    +      React.unmountComponentAtNode(container);
    +    });
    +
    +    it('shows all notification types when "all" filter applied', function () {
    +      var row1 = React.render(
    +        <Views.NotificationRow filter="all" item={notifications.success} transitionSpeed={0} />,
    +        container
    +      );
    +      assert.equal($(row1.getDOMNode()).data('visible'), true);
    +      React.unmountComponentAtNode(container);
    +
    +      var row2 = React.render(
    +        <Views.NotificationRow filter="all" item={notifications.error} transitionSpeed={0} />,
    +        container
    +      );
    +      assert.equal($(row2.getDOMNode()).data('visible'), true);
    +      React.unmountComponentAtNode(container);
    +
    +      var row3 = React.render(
    +        <Views.NotificationRow filter="all" item={notifications.info} transitionSpeed={0} />,
    +        container
    +      );
    +      assert.equal($(row3.getDOMNode()).data('visible'), true);
    +      React.unmountComponentAtNode(container);
    +    });
    +
    +    it('hides notification when filter doesn\'t match', function () {
    +      var rowEl = React.render(
    +        <Views.NotificationRow filter="success" item={notifications.info} transitionSpeed={0} />,
    +        container
    +      );
    +      assert.equal($(rowEl.getDOMNode()).data('visible'), false);
    +    });
    +
    +    it('shows notification when filter exact match', function () {
    +      var rowEl = React.render(
    +        <Views.NotificationRow filter="info" item={notifications.info} transitionSpeed={0} />,
    +        container
    +      );
    +      assert.equal($(rowEl.getDOMNode()).data('visible'), true);
    +    });
    +
    +  });
    +
    +
    +  describe('NotificationCenterPanel', function () {
    +    var container;
    +
    +    beforeEach(function () {
    +      container = document.createElement('div');
    +      store.reset();
    +    });
    +
    +    afterEach(function () {
    +      React.unmountComponentAtNode(container);
    +    });
    +
    +    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 = React.render(<Views.NotificationCenterPanel />, container);
    +      assert.equal($(panelEl.getDOMNode()).find('.notification-list li[data-visible=true]').length, 6);
    --- End diff --
    
    It's just for testing whether it's visible or not. The problem is just timing: any of the nice transition effects we do (bootstrap, velocity) take time to perform so checking visibility doesn't work immediately. This is a workaround. I *could* use a class, but I kinda prefer a data-role because to me it seems more detached from a class which could have styles. I know data-roles could be styled too, but they are less likely to & I like to draw attention to it. 


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] couchdb-fauxton pull request: Notification Center

Posted by benkeen <gi...@git.apache.org>.
Github user benkeen commented on the pull request:

    https://github.com/apache/couchdb-fauxton/pull/544#issuecomment-146222444
  
    Thanks @michellephung! I'll merge this later today. I'm going to leave the old notifications alone for the moment + refactor them separately. I think the designers had some ideas.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] couchdb-fauxton pull request: Notification Center

Posted by robertkowalski <gi...@git.apache.org>.
Github user robertkowalski commented on the pull request:

    https://github.com/apache/couchdb-fauxton/pull/544#issuecomment-145521343
  
    does the new notification center also mean we can throw away the old notification code?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] couchdb-fauxton pull request: Notification Center

Posted by michellephung <gi...@git.apache.org>.
Github user michellephung commented on the pull request:

    https://github.com/apache/couchdb-fauxton/pull/544#issuecomment-146129578
  
    +1


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] couchdb-fauxton pull request: Notification Center

Posted by robertkowalski <gi...@git.apache.org>.
Github user robertkowalski commented on a diff in the pull request:

    https://github.com/apache/couchdb-fauxton/pull/544#discussion_r41140205
  
    --- Diff: app/addons/fauxton/components.react.jsx ---
    @@ -334,13 +354,246 @@ function (app, FauxtonAPI, React, ZeroClipboard) {
       });
     
     
    +  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({
    +
    +    getInitialState: function () {
    +      return this.getStoreState();
    +    },
    +
    +    getStoreState: function () {
    +      return {
    +        isVisible: notificationStore.isNotificationCenterVisible(),
    +        filter: notificationStore.getNotificationFilter(),
    +        notifications: notificationStore.getNotifications()
    +      };
    +    },
    +
    +    componentDidMount: function () {
    +      notificationStore.on('change', this.onChange, this);
    +    },
    +
    +    componentWillUnmount: function () {
    +      notificationStore.off('change', this.onChange);
    +    },
    +
    +    onChange: function () {
    +      if (this.isMounted()) {
    +        this.setState(this.getStoreState());
    +      }
    +    },
    +
    +    getNotifications: function () {
    +      if (!this.state.notifications.length) {
    +        return (
    +          <li className="no-notifications">No notifications.</li>
    +        );
    +      }
    +
    +      return _.map(this.state.notifications, function (notification, i) {
    +        return (
    +          <NotificationRow
    +            isVisible={this.state.isVisible}
    +            item={notification}
    +            filter={this.state.filter}
    +            key={notification.notificationId}
    +          />
    +        );
    +      }, this);
    +    },
    +
    +    selectFilter: function (e) {
    +      var filter = $(e.target).closest('li').data('filter');
    +      Actions.selectNotificationFilter(filter);
    +    },
    +
    +    render: function () {
    +      var panelClasses = 'notification-center-panel flex-layout flex-col';
    +      if (this.state.isVisible) {
    +        panelClasses += ' visible';
    +      }
    +
    +      var filterClasses = {
    +        all: 'flex-body',
    +        success: 'flex-body',
    +        error: 'flex-body',
    +        info: 'flex-body'
    +      };
    +      filterClasses[this.state.filter] += ' selected';
    +
    +      var maskClasses = 'notification-page-mask' + ((this.state.isVisible) ? ' visible' : '');
    +      return (
    +        <div>
    +          <div className={panelClasses}>
    +
    +            <header className="flex-layout flex-row">
    +              <span className="fonticon fonticon-bell"></span>
    +              <h1 className="flex-body">Notifications</h1>
    +              <button type="button" aria-hidden="true" onClick={Actions.hideNotificationCenter}>×</button>
    +            </header>
    +
    +            <ul className="notification-filter flex-layout flex-row" onClick={this.selectFilter}>
    +              <li className={filterClasses.all} data-filter="all" title="All notifications">All</li>
    +              <li className={filterClasses.success} data-filter="success" title="Success notifications">
    +                <span className="fonticon fonticon-ok-circled"></span>
    +              </li>
    +              <li className={filterClasses.error} data-filter="error" title="Error notifications">
    +                <span className="fonticon fonticon-attention-circled"></span>
    +              </li>
    +              <li className={filterClasses.info} data-filter="info" title="Info notifications">
    +                <span className="fonticon fonticon-info-circled"></span>
    +              </li>
    +            </ul>
    +
    +            <div className="flex-body">
    +              <ul className="notification-list">
    +                {this.getNotifications()}
    +              </ul>
    +            </div>
    +
    +            <footer>
    +              <input type="button" value="Clear All" className="btn btn-small btn-info" onClick={Actions.clearAllNotifications} />
    +            </footer>
    +          </div>
    +
    +          <div className={maskClasses} onClick={Actions.hideNotificationCenter}></div>
    +        </div>
    +      );
    +    }
    +  });
    +
    +  var NotificationRow = React.createClass({
    +    propTypes: {
    +      item: React.PropTypes.object.isRequired,
    +      filter: React.PropTypes.string.isRequired,
    +      transitionSpeed: React.PropTypes.number
    +    },
    +
    +    getDefaultProps: function () {
    +      return {
    +        transitionSpeed: 300
    +      };
    +    },
    +
    +    clearNotification: function () {
    +      var notificationId = this.props.item.notificationId;
    +      this.hide(function () {
    +        Actions.clearSingleNotification(notificationId);
    +      });
    +    },
    +
    +    componentDidMount: function () {
    +      this.setState({
    +        elementHeight: this.getHeight()
    +      });
    +    },
    +
    +    componentDidUpdate: function (prevProps) {
    +      // in order for the nice slide effects to work we need a concrete element height to slide to and from.
    +      // $.outerHeight() only works reliably on visible elements, hence this additional setState here
    +      if (!prevProps.isVisible && this.props.isVisible) {
    +        this.setState({
    +          elementHeight: this.getHeight()
    +        });
    +      }
    +
    +      var show = true;
    +      if (this.props.filter !== 'all') {
    +        show = this.props.item.type === this.props.filter;
    +      }
    +      if (show) {
    +        console.log(this.state.elementHeight);
    +        $(this.getDOMNode()).velocity({ opacity: 1, height: this.state.elementHeight }, this.props.transitionSpeed);
    +      } else {
    +        this.hide();
    +      }
    +    },
    +
    +    getHeight: function () {
    +      return $(this.getDOMNode()).outerHeight(true);
    +    },
    +
    +    hide: function (onHidden) {
    +      $(this.getDOMNode()).velocity({ opacity: 0, height: 0 }, this.props.transitionSpeed, function () {
    +        if (onHidden) {
    +          onHidden();
    +        }
    +      });
    +    },
    +
    +    render: function () {
    +      var iconMap = {
    +        success: 'fonticon-ok-circled',
    +        error: 'fonticon-attention-circled',
    +        info: 'fonticon-info-circled'
    +      };
    +
    +      var timeElapsed = this.props.item.time.fromNow();
    +
    +      // we can safely do this because the store ensures all notifications are of known types
    +      var rowIconClasses = 'fonticon ' + iconMap[this.props.item.type];
    +      var classes = 'flex-layout flex-row';
    +
    +      // for testing purposes
    +      var visible = (this.props.filter === 'all' || this.props.filter === this.props.item.type) ? 'true' : 'false';
    +
    +      // N.B. wrapper <div> needed to ensure smooth hide/show transitions
    +      return (
    +        <li data-visible={visible}>
    --- End diff --
    
    it looks like this was added just for testing?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] couchdb-fauxton pull request: Notification Center

Posted by benkeen <gi...@git.apache.org>.
Github user benkeen commented on the pull request:

    https://github.com/apache/couchdb-fauxton/pull/544#issuecomment-145606077
  
    Thanks @robertkowalski! Terrific feedback. Will apply the various fixes today.
    
    Yes, ultimately we'll be able to drop the old notification code but not yet. I actually started doing that at the beginning of this ticket, but felt I was kinda shaving a yak, so left it alone until later. :)


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] couchdb-fauxton pull request: Notification Center

Posted by robertkowalski <gi...@git.apache.org>.
Github user robertkowalski commented on a diff in the pull request:

    https://github.com/apache/couchdb-fauxton/pull/544#discussion_r41140420
  
    --- Diff: app/addons/fauxton/components.react.jsx ---
    @@ -334,13 +354,246 @@ function (app, FauxtonAPI, React, ZeroClipboard) {
       });
     
     
    +  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({
    +
    +    getInitialState: function () {
    +      return this.getStoreState();
    +    },
    +
    +    getStoreState: function () {
    +      return {
    +        isVisible: notificationStore.isNotificationCenterVisible(),
    +        filter: notificationStore.getNotificationFilter(),
    +        notifications: notificationStore.getNotifications()
    +      };
    +    },
    +
    +    componentDidMount: function () {
    +      notificationStore.on('change', this.onChange, this);
    +    },
    +
    +    componentWillUnmount: function () {
    +      notificationStore.off('change', this.onChange);
    +    },
    +
    +    onChange: function () {
    +      if (this.isMounted()) {
    +        this.setState(this.getStoreState());
    +      }
    +    },
    +
    +    getNotifications: function () {
    +      if (!this.state.notifications.length) {
    +        return (
    +          <li className="no-notifications">No notifications.</li>
    +        );
    +      }
    +
    +      return _.map(this.state.notifications, function (notification, i) {
    +        return (
    +          <NotificationRow
    +            isVisible={this.state.isVisible}
    +            item={notification}
    +            filter={this.state.filter}
    +            key={notification.notificationId}
    +          />
    +        );
    +      }, this);
    +    },
    +
    +    selectFilter: function (e) {
    +      var filter = $(e.target).closest('li').data('filter');
    +      Actions.selectNotificationFilter(filter);
    +    },
    +
    +    render: function () {
    +      var panelClasses = 'notification-center-panel flex-layout flex-col';
    +      if (this.state.isVisible) {
    +        panelClasses += ' visible';
    +      }
    +
    +      var filterClasses = {
    +        all: 'flex-body',
    +        success: 'flex-body',
    +        error: 'flex-body',
    +        info: 'flex-body'
    +      };
    +      filterClasses[this.state.filter] += ' selected';
    +
    +      var maskClasses = 'notification-page-mask' + ((this.state.isVisible) ? ' visible' : '');
    +      return (
    +        <div>
    +          <div className={panelClasses}>
    +
    +            <header className="flex-layout flex-row">
    +              <span className="fonticon fonticon-bell"></span>
    +              <h1 className="flex-body">Notifications</h1>
    +              <button type="button" aria-hidden="true" onClick={Actions.hideNotificationCenter}>×</button>
    +            </header>
    +
    +            <ul className="notification-filter flex-layout flex-row" onClick={this.selectFilter}>
    +              <li className={filterClasses.all} data-filter="all" title="All notifications">All</li>
    +              <li className={filterClasses.success} data-filter="success" title="Success notifications">
    +                <span className="fonticon fonticon-ok-circled"></span>
    +              </li>
    +              <li className={filterClasses.error} data-filter="error" title="Error notifications">
    +                <span className="fonticon fonticon-attention-circled"></span>
    +              </li>
    +              <li className={filterClasses.info} data-filter="info" title="Info notifications">
    +                <span className="fonticon fonticon-info-circled"></span>
    +              </li>
    +            </ul>
    +
    +            <div className="flex-body">
    +              <ul className="notification-list">
    +                {this.getNotifications()}
    +              </ul>
    +            </div>
    +
    +            <footer>
    +              <input type="button" value="Clear All" className="btn btn-small btn-info" onClick={Actions.clearAllNotifications} />
    +            </footer>
    +          </div>
    +
    +          <div className={maskClasses} onClick={Actions.hideNotificationCenter}></div>
    +        </div>
    +      );
    +    }
    +  });
    +
    +  var NotificationRow = React.createClass({
    +    propTypes: {
    +      item: React.PropTypes.object.isRequired,
    +      filter: React.PropTypes.string.isRequired,
    +      transitionSpeed: React.PropTypes.number
    +    },
    +
    +    getDefaultProps: function () {
    +      return {
    +        transitionSpeed: 300
    +      };
    +    },
    +
    +    clearNotification: function () {
    +      var notificationId = this.props.item.notificationId;
    +      this.hide(function () {
    +        Actions.clearSingleNotification(notificationId);
    +      });
    +    },
    +
    +    componentDidMount: function () {
    +      this.setState({
    +        elementHeight: this.getHeight()
    +      });
    +    },
    +
    +    componentDidUpdate: function (prevProps) {
    +      // in order for the nice slide effects to work we need a concrete element height to slide to and from.
    +      // $.outerHeight() only works reliably on visible elements, hence this additional setState here
    +      if (!prevProps.isVisible && this.props.isVisible) {
    +        this.setState({
    +          elementHeight: this.getHeight()
    +        });
    +      }
    +
    +      var show = true;
    +      if (this.props.filter !== 'all') {
    +        show = this.props.item.type === this.props.filter;
    +      }
    +      if (show) {
    +        console.log(this.state.elementHeight);
    +        $(this.getDOMNode()).velocity({ opacity: 1, height: this.state.elementHeight }, this.props.transitionSpeed);
    +      } else {
    +        this.hide();
    +      }
    +    },
    +
    +    getHeight: function () {
    +      return $(this.getDOMNode()).outerHeight(true);
    +    },
    +
    +    hide: function (onHidden) {
    +      $(this.getDOMNode()).velocity({ opacity: 0, height: 0 }, this.props.transitionSpeed, function () {
    +        if (onHidden) {
    +          onHidden();
    +        }
    +      });
    +    },
    +
    +    render: function () {
    +      var iconMap = {
    +        success: 'fonticon-ok-circled',
    +        error: 'fonticon-attention-circled',
    +        info: 'fonticon-info-circled'
    +      };
    +
    +      var timeElapsed = this.props.item.time.fromNow();
    +
    +      // we can safely do this because the store ensures all notifications are of known types
    +      var rowIconClasses = 'fonticon ' + iconMap[this.props.item.type];
    +      var classes = 'flex-layout flex-row';
    +
    +      // for testing purposes
    +      var visible = (this.props.filter === 'all' || this.props.filter === this.props.item.type) ? 'true' : 'false';
    +
    +      // N.B. wrapper <div> needed to ensure smooth hide/show transitions
    +      return (
    +        <li data-visible={visible}>
    +          <div className={classes}>
    +            <span className={rowIconClasses}></span>
    +            <div className="flex-body">
    +              <p dangerouslySetInnerHTML={{__html: this.props.item.msg}}></p>
    +              <div className="notification-actions">
    +                <span className="time-elapsed">{timeElapsed}</span>
    +                <span className="divider">|</span>
    +                <Clipboard text={this.props.item.msg} displayType="text" />
    +              </div>
    +            </div>
    +            <button type="button" aria-hidden="true" onClick={this.clearNotification}>×</button>
    --- End diff --
    
    `aria-hidden="true"` <- is it always hidden?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] couchdb-fauxton pull request: Notification Center

Posted by benkeen <gi...@git.apache.org>.
Github user benkeen commented on a diff in the pull request:

    https://github.com/apache/couchdb-fauxton/pull/544#discussion_r41171150
  
    --- Diff: app/addons/fauxton/components.react.jsx ---
    @@ -334,13 +354,246 @@ function (app, FauxtonAPI, React, ZeroClipboard) {
       });
     
     
    +  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({
    +
    +    getInitialState: function () {
    +      return this.getStoreState();
    +    },
    +
    +    getStoreState: function () {
    +      return {
    +        isVisible: notificationStore.isNotificationCenterVisible(),
    +        filter: notificationStore.getNotificationFilter(),
    +        notifications: notificationStore.getNotifications()
    +      };
    +    },
    +
    +    componentDidMount: function () {
    +      notificationStore.on('change', this.onChange, this);
    +    },
    +
    +    componentWillUnmount: function () {
    +      notificationStore.off('change', this.onChange);
    +    },
    +
    +    onChange: function () {
    +      if (this.isMounted()) {
    +        this.setState(this.getStoreState());
    +      }
    +    },
    +
    +    getNotifications: function () {
    +      if (!this.state.notifications.length) {
    +        return (
    +          <li className="no-notifications">No notifications.</li>
    +        );
    +      }
    +
    +      return _.map(this.state.notifications, function (notification, i) {
    +        return (
    +          <NotificationRow
    +            isVisible={this.state.isVisible}
    +            item={notification}
    +            filter={this.state.filter}
    +            key={notification.notificationId}
    +          />
    +        );
    +      }, this);
    +    },
    +
    +    selectFilter: function (e) {
    +      var filter = $(e.target).closest('li').data('filter');
    +      Actions.selectNotificationFilter(filter);
    +    },
    +
    +    render: function () {
    +      var panelClasses = 'notification-center-panel flex-layout flex-col';
    +      if (this.state.isVisible) {
    +        panelClasses += ' visible';
    +      }
    +
    +      var filterClasses = {
    +        all: 'flex-body',
    +        success: 'flex-body',
    +        error: 'flex-body',
    +        info: 'flex-body'
    +      };
    +      filterClasses[this.state.filter] += ' selected';
    +
    +      var maskClasses = 'notification-page-mask' + ((this.state.isVisible) ? ' visible' : '');
    +      return (
    +        <div>
    +          <div className={panelClasses}>
    +
    +            <header className="flex-layout flex-row">
    +              <span className="fonticon fonticon-bell"></span>
    +              <h1 className="flex-body">Notifications</h1>
    +              <button type="button" aria-hidden="true" onClick={Actions.hideNotificationCenter}>×</button>
    +            </header>
    +
    +            <ul className="notification-filter flex-layout flex-row" onClick={this.selectFilter}>
    +              <li className={filterClasses.all} data-filter="all" title="All notifications">All</li>
    +              <li className={filterClasses.success} data-filter="success" title="Success notifications">
    +                <span className="fonticon fonticon-ok-circled"></span>
    +              </li>
    +              <li className={filterClasses.error} data-filter="error" title="Error notifications">
    +                <span className="fonticon fonticon-attention-circled"></span>
    +              </li>
    +              <li className={filterClasses.info} data-filter="info" title="Info notifications">
    +                <span className="fonticon fonticon-info-circled"></span>
    +              </li>
    +            </ul>
    +
    +            <div className="flex-body">
    +              <ul className="notification-list">
    +                {this.getNotifications()}
    +              </ul>
    +            </div>
    +
    +            <footer>
    +              <input type="button" value="Clear All" className="btn btn-small btn-info" onClick={Actions.clearAllNotifications} />
    +            </footer>
    +          </div>
    +
    +          <div className={maskClasses} onClick={Actions.hideNotificationCenter}></div>
    +        </div>
    +      );
    +    }
    +  });
    +
    +  var NotificationRow = React.createClass({
    +    propTypes: {
    +      item: React.PropTypes.object.isRequired,
    +      filter: React.PropTypes.string.isRequired,
    +      transitionSpeed: React.PropTypes.number
    +    },
    +
    +    getDefaultProps: function () {
    +      return {
    +        transitionSpeed: 300
    +      };
    +    },
    +
    +    clearNotification: function () {
    +      var notificationId = this.props.item.notificationId;
    +      this.hide(function () {
    +        Actions.clearSingleNotification(notificationId);
    +      });
    +    },
    +
    +    componentDidMount: function () {
    +      this.setState({
    +        elementHeight: this.getHeight()
    +      });
    +    },
    +
    +    componentDidUpdate: function (prevProps) {
    +      // in order for the nice slide effects to work we need a concrete element height to slide to and from.
    +      // $.outerHeight() only works reliably on visible elements, hence this additional setState here
    +      if (!prevProps.isVisible && this.props.isVisible) {
    +        this.setState({
    +          elementHeight: this.getHeight()
    +        });
    +      }
    +
    +      var show = true;
    +      if (this.props.filter !== 'all') {
    +        show = this.props.item.type === this.props.filter;
    +      }
    +      if (show) {
    +        console.log(this.state.elementHeight);
    --- End diff --
    
    Oops.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] couchdb-fauxton pull request: Notification Center

Posted by robertkowalski <gi...@git.apache.org>.
Github user robertkowalski commented on a diff in the pull request:

    https://github.com/apache/couchdb-fauxton/pull/544#discussion_r41139833
  
    --- Diff: app/addons/fauxton/components.react.jsx ---
    @@ -334,13 +354,246 @@ function (app, FauxtonAPI, React, ZeroClipboard) {
       });
     
     
    +  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({
    +
    +    getInitialState: function () {
    +      return this.getStoreState();
    +    },
    +
    +    getStoreState: function () {
    +      return {
    +        isVisible: notificationStore.isNotificationCenterVisible(),
    +        filter: notificationStore.getNotificationFilter(),
    +        notifications: notificationStore.getNotifications()
    +      };
    +    },
    +
    +    componentDidMount: function () {
    +      notificationStore.on('change', this.onChange, this);
    +    },
    +
    +    componentWillUnmount: function () {
    +      notificationStore.off('change', this.onChange);
    +    },
    +
    +    onChange: function () {
    +      if (this.isMounted()) {
    +        this.setState(this.getStoreState());
    +      }
    +    },
    +
    +    getNotifications: function () {
    +      if (!this.state.notifications.length) {
    +        return (
    +          <li className="no-notifications">No notifications.</li>
    +        );
    +      }
    +
    +      return _.map(this.state.notifications, function (notification, i) {
    +        return (
    +          <NotificationRow
    +            isVisible={this.state.isVisible}
    +            item={notification}
    +            filter={this.state.filter}
    +            key={notification.notificationId}
    +          />
    +        );
    +      }, this);
    +    },
    +
    +    selectFilter: function (e) {
    +      var filter = $(e.target).closest('li').data('filter');
    +      Actions.selectNotificationFilter(filter);
    +    },
    +
    +    render: function () {
    +      var panelClasses = 'notification-center-panel flex-layout flex-col';
    +      if (this.state.isVisible) {
    +        panelClasses += ' visible';
    +      }
    +
    +      var filterClasses = {
    +        all: 'flex-body',
    +        success: 'flex-body',
    +        error: 'flex-body',
    +        info: 'flex-body'
    +      };
    +      filterClasses[this.state.filter] += ' selected';
    +
    +      var maskClasses = 'notification-page-mask' + ((this.state.isVisible) ? ' visible' : '');
    +      return (
    +        <div>
    +          <div className={panelClasses}>
    +
    +            <header className="flex-layout flex-row">
    +              <span className="fonticon fonticon-bell"></span>
    +              <h1 className="flex-body">Notifications</h1>
    +              <button type="button" aria-hidden="true" onClick={Actions.hideNotificationCenter}>×</button>
    +            </header>
    +
    +            <ul className="notification-filter flex-layout flex-row" onClick={this.selectFilter}>
    +              <li className={filterClasses.all} data-filter="all" title="All notifications">All</li>
    +              <li className={filterClasses.success} data-filter="success" title="Success notifications">
    +                <span className="fonticon fonticon-ok-circled"></span>
    +              </li>
    +              <li className={filterClasses.error} data-filter="error" title="Error notifications">
    +                <span className="fonticon fonticon-attention-circled"></span>
    +              </li>
    +              <li className={filterClasses.info} data-filter="info" title="Info notifications">
    +                <span className="fonticon fonticon-info-circled"></span>
    +              </li>
    +            </ul>
    +
    +            <div className="flex-body">
    +              <ul className="notification-list">
    +                {this.getNotifications()}
    +              </ul>
    +            </div>
    +
    +            <footer>
    +              <input type="button" value="Clear All" className="btn btn-small btn-info" onClick={Actions.clearAllNotifications} />
    +            </footer>
    +          </div>
    +
    +          <div className={maskClasses} onClick={Actions.hideNotificationCenter}></div>
    +        </div>
    +      );
    +    }
    +  });
    +
    +  var NotificationRow = React.createClass({
    +    propTypes: {
    +      item: React.PropTypes.object.isRequired,
    +      filter: React.PropTypes.string.isRequired,
    +      transitionSpeed: React.PropTypes.number
    +    },
    +
    +    getDefaultProps: function () {
    +      return {
    +        transitionSpeed: 300
    +      };
    +    },
    +
    +    clearNotification: function () {
    +      var notificationId = this.props.item.notificationId;
    +      this.hide(function () {
    +        Actions.clearSingleNotification(notificationId);
    +      });
    +    },
    +
    +    componentDidMount: function () {
    +      this.setState({
    +        elementHeight: this.getHeight()
    +      });
    +    },
    +
    +    componentDidUpdate: function (prevProps) {
    +      // in order for the nice slide effects to work we need a concrete element height to slide to and from.
    +      // $.outerHeight() only works reliably on visible elements, hence this additional setState here
    +      if (!prevProps.isVisible && this.props.isVisible) {
    +        this.setState({
    +          elementHeight: this.getHeight()
    +        });
    +      }
    +
    +      var show = true;
    +      if (this.props.filter !== 'all') {
    +        show = this.props.item.type === this.props.filter;
    +      }
    +      if (show) {
    +        console.log(this.state.elementHeight);
    +        $(this.getDOMNode()).velocity({ opacity: 1, height: this.state.elementHeight }, this.props.transitionSpeed);
    +      } else {
    +        this.hide();
    +      }
    +    },
    +
    +    getHeight: function () {
    +      return $(this.getDOMNode()).outerHeight(true);
    +    },
    +
    +    hide: function (onHidden) {
    +      $(this.getDOMNode()).velocity({ opacity: 0, height: 0 }, this.props.transitionSpeed, function () {
    +        if (onHidden) {
    +          onHidden();
    +        }
    +      });
    +    },
    +
    +    render: function () {
    +      var iconMap = {
    +        success: 'fonticon-ok-circled',
    +        error: 'fonticon-attention-circled',
    +        info: 'fonticon-info-circled'
    +      };
    +
    +      var timeElapsed = this.props.item.time.fromNow();
    +
    +      // we can safely do this because the store ensures all notifications are of known types
    +      var rowIconClasses = 'fonticon ' + iconMap[this.props.item.type];
    +      var classes = 'flex-layout flex-row';
    +
    +      // for testing purposes
    +      var visible = (this.props.filter === 'all' || this.props.filter === this.props.item.type) ? 'true' : 'false';
    +
    +      // N.B. wrapper <div> needed to ensure smooth hide/show transitions
    +      return (
    +        <li data-visible={visible}>
    +          <div className={classes}>
    +            <span className={rowIconClasses}></span>
    +            <div className="flex-body">
    +              <p dangerouslySetInnerHTML={{__html: this.props.item.msg}}></p>
    --- End diff --
    
    we have some notifications that include user generated content, so we open a XSS vector here i guess?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] couchdb-fauxton pull request: Notification Center

Posted by robertkowalski <gi...@git.apache.org>.
Github user robertkowalski commented on the pull request:

    https://github.com/apache/couchdb-fauxton/pull/544#issuecomment-145521462
  
    code looks quite well! just spotted some jqueryisms


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] couchdb-fauxton pull request: Notification Center

Posted by benkeen <gi...@git.apache.org>.
Github user benkeen commented on a diff in the pull request:

    https://github.com/apache/couchdb-fauxton/pull/544#discussion_r41171515
  
    --- Diff: app/addons/fauxton/components.react.jsx ---
    @@ -334,13 +354,246 @@ function (app, FauxtonAPI, React, ZeroClipboard) {
       });
     
     
    +  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({
    +
    +    getInitialState: function () {
    +      return this.getStoreState();
    +    },
    +
    +    getStoreState: function () {
    +      return {
    +        isVisible: notificationStore.isNotificationCenterVisible(),
    +        filter: notificationStore.getNotificationFilter(),
    +        notifications: notificationStore.getNotifications()
    +      };
    +    },
    +
    +    componentDidMount: function () {
    +      notificationStore.on('change', this.onChange, this);
    +    },
    +
    +    componentWillUnmount: function () {
    +      notificationStore.off('change', this.onChange);
    +    },
    +
    +    onChange: function () {
    +      if (this.isMounted()) {
    +        this.setState(this.getStoreState());
    +      }
    +    },
    +
    +    getNotifications: function () {
    +      if (!this.state.notifications.length) {
    +        return (
    +          <li className="no-notifications">No notifications.</li>
    +        );
    +      }
    +
    +      return _.map(this.state.notifications, function (notification, i) {
    +        return (
    +          <NotificationRow
    +            isVisible={this.state.isVisible}
    +            item={notification}
    +            filter={this.state.filter}
    +            key={notification.notificationId}
    +          />
    +        );
    +      }, this);
    +    },
    +
    +    selectFilter: function (e) {
    +      var filter = $(e.target).closest('li').data('filter');
    +      Actions.selectNotificationFilter(filter);
    +    },
    +
    +    render: function () {
    +      var panelClasses = 'notification-center-panel flex-layout flex-col';
    +      if (this.state.isVisible) {
    +        panelClasses += ' visible';
    +      }
    +
    +      var filterClasses = {
    +        all: 'flex-body',
    +        success: 'flex-body',
    +        error: 'flex-body',
    +        info: 'flex-body'
    +      };
    +      filterClasses[this.state.filter] += ' selected';
    +
    +      var maskClasses = 'notification-page-mask' + ((this.state.isVisible) ? ' visible' : '');
    +      return (
    +        <div>
    +          <div className={panelClasses}>
    +
    +            <header className="flex-layout flex-row">
    +              <span className="fonticon fonticon-bell"></span>
    +              <h1 className="flex-body">Notifications</h1>
    +              <button type="button" aria-hidden="true" onClick={Actions.hideNotificationCenter}>×</button>
    +            </header>
    +
    +            <ul className="notification-filter flex-layout flex-row" onClick={this.selectFilter}>
    +              <li className={filterClasses.all} data-filter="all" title="All notifications">All</li>
    +              <li className={filterClasses.success} data-filter="success" title="Success notifications">
    +                <span className="fonticon fonticon-ok-circled"></span>
    +              </li>
    +              <li className={filterClasses.error} data-filter="error" title="Error notifications">
    +                <span className="fonticon fonticon-attention-circled"></span>
    +              </li>
    +              <li className={filterClasses.info} data-filter="info" title="Info notifications">
    +                <span className="fonticon fonticon-info-circled"></span>
    +              </li>
    +            </ul>
    +
    +            <div className="flex-body">
    +              <ul className="notification-list">
    +                {this.getNotifications()}
    +              </ul>
    +            </div>
    +
    +            <footer>
    +              <input type="button" value="Clear All" className="btn btn-small btn-info" onClick={Actions.clearAllNotifications} />
    +            </footer>
    +          </div>
    +
    +          <div className={maskClasses} onClick={Actions.hideNotificationCenter}></div>
    +        </div>
    +      );
    +    }
    +  });
    +
    +  var NotificationRow = React.createClass({
    +    propTypes: {
    +      item: React.PropTypes.object.isRequired,
    +      filter: React.PropTypes.string.isRequired,
    +      transitionSpeed: React.PropTypes.number
    +    },
    +
    +    getDefaultProps: function () {
    +      return {
    +        transitionSpeed: 300
    +      };
    +    },
    +
    +    clearNotification: function () {
    +      var notificationId = this.props.item.notificationId;
    +      this.hide(function () {
    +        Actions.clearSingleNotification(notificationId);
    +      });
    +    },
    +
    +    componentDidMount: function () {
    +      this.setState({
    +        elementHeight: this.getHeight()
    +      });
    +    },
    +
    +    componentDidUpdate: function (prevProps) {
    +      // in order for the nice slide effects to work we need a concrete element height to slide to and from.
    +      // $.outerHeight() only works reliably on visible elements, hence this additional setState here
    +      if (!prevProps.isVisible && this.props.isVisible) {
    +        this.setState({
    +          elementHeight: this.getHeight()
    +        });
    +      }
    +
    +      var show = true;
    +      if (this.props.filter !== 'all') {
    +        show = this.props.item.type === this.props.filter;
    +      }
    +      if (show) {
    +        console.log(this.state.elementHeight);
    +        $(this.getDOMNode()).velocity({ opacity: 1, height: this.state.elementHeight }, this.props.transitionSpeed);
    +      } else {
    +        this.hide();
    +      }
    +    },
    +
    +    getHeight: function () {
    +      return $(this.getDOMNode()).outerHeight(true);
    +    },
    +
    +    hide: function (onHidden) {
    +      $(this.getDOMNode()).velocity({ opacity: 0, height: 0 }, this.props.transitionSpeed, function () {
    +        if (onHidden) {
    +          onHidden();
    +        }
    +      });
    +    },
    +
    +    render: function () {
    +      var iconMap = {
    +        success: 'fonticon-ok-circled',
    +        error: 'fonticon-attention-circled',
    +        info: 'fonticon-info-circled'
    +      };
    +
    +      var timeElapsed = this.props.item.time.fromNow();
    +
    +      // we can safely do this because the store ensures all notifications are of known types
    +      var rowIconClasses = 'fonticon ' + iconMap[this.props.item.type];
    +      var classes = 'flex-layout flex-row';
    +
    +      // for testing purposes
    +      var visible = (this.props.filter === 'all' || this.props.filter === this.props.item.type) ? 'true' : 'false';
    +
    +      // N.B. wrapper <div> needed to ensure smooth hide/show transitions
    +      return (
    +        <li data-visible={visible}>
    +          <div className={classes}>
    +            <span className={rowIconClasses}></span>
    +            <div className="flex-body">
    +              <p dangerouslySetInnerHTML={{__html: this.props.item.msg}}></p>
    --- End diff --
    
    Drat, excellent point.
    
    I liked passing in the HTML here because some messages contained links that would continue to work from within the notification centre panel. But on further thought, it could backfire... not just with the XSS - but with links that were relative, or whatever. Potentially there'd be broken links appearing in the notification centre.
    
    I'll change this to strip out HTML + just pass the msg itself.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] couchdb-fauxton pull request: Notification Center

Posted by benkeen <gi...@git.apache.org>.
Github user benkeen commented on a diff in the pull request:

    https://github.com/apache/couchdb-fauxton/pull/544#discussion_r41171681
  
    --- Diff: app/addons/fauxton/components.react.jsx ---
    @@ -334,13 +354,246 @@ function (app, FauxtonAPI, React, ZeroClipboard) {
       });
     
     
    +  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({
    +
    +    getInitialState: function () {
    +      return this.getStoreState();
    +    },
    +
    +    getStoreState: function () {
    +      return {
    +        isVisible: notificationStore.isNotificationCenterVisible(),
    +        filter: notificationStore.getNotificationFilter(),
    +        notifications: notificationStore.getNotifications()
    +      };
    +    },
    +
    +    componentDidMount: function () {
    +      notificationStore.on('change', this.onChange, this);
    +    },
    +
    +    componentWillUnmount: function () {
    +      notificationStore.off('change', this.onChange);
    +    },
    +
    +    onChange: function () {
    +      if (this.isMounted()) {
    +        this.setState(this.getStoreState());
    +      }
    +    },
    +
    +    getNotifications: function () {
    +      if (!this.state.notifications.length) {
    +        return (
    +          <li className="no-notifications">No notifications.</li>
    +        );
    +      }
    +
    +      return _.map(this.state.notifications, function (notification, i) {
    +        return (
    +          <NotificationRow
    +            isVisible={this.state.isVisible}
    +            item={notification}
    +            filter={this.state.filter}
    +            key={notification.notificationId}
    +          />
    +        );
    +      }, this);
    +    },
    +
    +    selectFilter: function (e) {
    +      var filter = $(e.target).closest('li').data('filter');
    +      Actions.selectNotificationFilter(filter);
    +    },
    +
    +    render: function () {
    +      var panelClasses = 'notification-center-panel flex-layout flex-col';
    +      if (this.state.isVisible) {
    +        panelClasses += ' visible';
    +      }
    +
    +      var filterClasses = {
    +        all: 'flex-body',
    +        success: 'flex-body',
    +        error: 'flex-body',
    +        info: 'flex-body'
    +      };
    +      filterClasses[this.state.filter] += ' selected';
    +
    +      var maskClasses = 'notification-page-mask' + ((this.state.isVisible) ? ' visible' : '');
    +      return (
    +        <div>
    +          <div className={panelClasses}>
    +
    +            <header className="flex-layout flex-row">
    +              <span className="fonticon fonticon-bell"></span>
    +              <h1 className="flex-body">Notifications</h1>
    +              <button type="button" aria-hidden="true" onClick={Actions.hideNotificationCenter}>×</button>
    +            </header>
    +
    +            <ul className="notification-filter flex-layout flex-row" onClick={this.selectFilter}>
    +              <li className={filterClasses.all} data-filter="all" title="All notifications">All</li>
    +              <li className={filterClasses.success} data-filter="success" title="Success notifications">
    +                <span className="fonticon fonticon-ok-circled"></span>
    +              </li>
    +              <li className={filterClasses.error} data-filter="error" title="Error notifications">
    +                <span className="fonticon fonticon-attention-circled"></span>
    +              </li>
    +              <li className={filterClasses.info} data-filter="info" title="Info notifications">
    +                <span className="fonticon fonticon-info-circled"></span>
    +              </li>
    +            </ul>
    +
    +            <div className="flex-body">
    +              <ul className="notification-list">
    +                {this.getNotifications()}
    +              </ul>
    +            </div>
    +
    +            <footer>
    +              <input type="button" value="Clear All" className="btn btn-small btn-info" onClick={Actions.clearAllNotifications} />
    +            </footer>
    +          </div>
    +
    +          <div className={maskClasses} onClick={Actions.hideNotificationCenter}></div>
    +        </div>
    +      );
    +    }
    +  });
    +
    +  var NotificationRow = React.createClass({
    +    propTypes: {
    +      item: React.PropTypes.object.isRequired,
    +      filter: React.PropTypes.string.isRequired,
    +      transitionSpeed: React.PropTypes.number
    +    },
    +
    +    getDefaultProps: function () {
    +      return {
    +        transitionSpeed: 300
    +      };
    +    },
    +
    +    clearNotification: function () {
    +      var notificationId = this.props.item.notificationId;
    +      this.hide(function () {
    +        Actions.clearSingleNotification(notificationId);
    +      });
    +    },
    +
    +    componentDidMount: function () {
    +      this.setState({
    +        elementHeight: this.getHeight()
    +      });
    +    },
    +
    +    componentDidUpdate: function (prevProps) {
    +      // in order for the nice slide effects to work we need a concrete element height to slide to and from.
    +      // $.outerHeight() only works reliably on visible elements, hence this additional setState here
    +      if (!prevProps.isVisible && this.props.isVisible) {
    +        this.setState({
    +          elementHeight: this.getHeight()
    +        });
    +      }
    +
    +      var show = true;
    +      if (this.props.filter !== 'all') {
    +        show = this.props.item.type === this.props.filter;
    +      }
    +      if (show) {
    +        console.log(this.state.elementHeight);
    +        $(this.getDOMNode()).velocity({ opacity: 1, height: this.state.elementHeight }, this.props.transitionSpeed);
    +      } else {
    +        this.hide();
    +      }
    +    },
    +
    +    getHeight: function () {
    +      return $(this.getDOMNode()).outerHeight(true);
    +    },
    +
    +    hide: function (onHidden) {
    +      $(this.getDOMNode()).velocity({ opacity: 0, height: 0 }, this.props.transitionSpeed, function () {
    +        if (onHidden) {
    +          onHidden();
    +        }
    +      });
    +    },
    +
    +    render: function () {
    +      var iconMap = {
    +        success: 'fonticon-ok-circled',
    +        error: 'fonticon-attention-circled',
    +        info: 'fonticon-info-circled'
    +      };
    +
    +      var timeElapsed = this.props.item.time.fromNow();
    +
    +      // we can safely do this because the store ensures all notifications are of known types
    +      var rowIconClasses = 'fonticon ' + iconMap[this.props.item.type];
    +      var classes = 'flex-layout flex-row';
    +
    +      // for testing purposes
    +      var visible = (this.props.filter === 'all' || this.props.filter === this.props.item.type) ? 'true' : 'false';
    +
    +      // N.B. wrapper <div> needed to ensure smooth hide/show transitions
    +      return (
    +        <li data-visible={visible}>
    --- End diff --
    
    Yeah, I do hate to do it but it's an acceptable sacrifice, I think. :( We do it all over the place with things like modals to test they're open & not run into timing issues with fade-in/outs.


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] couchdb-fauxton pull request: Notification Center

Posted by robertkowalski <gi...@git.apache.org>.
Github user robertkowalski commented on a diff in the pull request:

    https://github.com/apache/couchdb-fauxton/pull/544#discussion_r41140110
  
    --- Diff: app/addons/fauxton/tests/componentsSpec.react.jsx ---
    @@ -204,5 +207,169 @@ define([
     
       });
     
    -});
     
    +  describe('Clipboard', function () {
    +    var container;
    +    beforeEach(function () {
    +      container = document.createElement('div');
    +    });
    +
    +    afterEach(function () {
    +      React.unmountComponentAtNode(container);
    +    });
    +
    +    it('shows a clipboard icon by default', function () {
    +      var clipboard = React.render(<Views.Clipboard text="copy me" />, container);
    +      assert.equal($(clipboard.getDOMNode()).find('.fonticon-clipboard').length, 1);
    +    });
    +
    +    it('shows text if specified', function () {
    +      var clipboard = React.render(<Views.Clipboard displayType="text" text="copy me" />, container);
    +      assert.equal($(clipboard.getDOMNode()).find('.fonticon-clipboard').length, 0);
    +    });
    +
    +    it('shows custom text if specified ', function () {
    +      var clipboard = React.render(<Views.Clipboard displayType="text" textDisplay='booyah!' text="copy me" />, container);
    +      assert.ok(/booyah!/.test($(clipboard.getDOMNode())[0].outerHTML));
    +    });
    +
    +  });
    +
    +
    +  describe('NotificationRow', function () {
    +    var container;
    +
    +    var notifications = {
    +      success: {
    +        notificationId: 1,
    +        type: 'success',
    +        msg: 'Success!',
    +        time: moment()
    +      },
    +      info: {
    +        notificationId: 2,
    +        type: 'info',
    +        msg: 'Error!',
    +        time: moment()
    +      },
    +      error: {
    +        notificationId: 3,
    +        type: 'error',
    +        msg: 'Error!',
    +        time: moment()
    +      }
    +    };
    +
    +    beforeEach(function () {
    +      container = document.createElement('div');
    +    });
    +
    +    afterEach(function () {
    +      React.unmountComponentAtNode(container);
    +    });
    +
    +    it('shows all notification types when "all" filter applied', function () {
    +      var row1 = React.render(
    --- End diff --
    
    this all should use `TestUtils.renderIntoDocument` i guess?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] couchdb-fauxton pull request: Notification Center

Posted by robertkowalski <gi...@git.apache.org>.
Github user robertkowalski commented on a diff in the pull request:

    https://github.com/apache/couchdb-fauxton/pull/544#discussion_r41373933
  
    --- Diff: app/addons/fauxton/components.react.jsx ---
    @@ -334,13 +354,246 @@ function (app, FauxtonAPI, React, ZeroClipboard) {
       });
     
     
    +  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({
    +
    +    getInitialState: function () {
    +      return this.getStoreState();
    +    },
    +
    +    getStoreState: function () {
    +      return {
    +        isVisible: notificationStore.isNotificationCenterVisible(),
    +        filter: notificationStore.getNotificationFilter(),
    +        notifications: notificationStore.getNotifications()
    +      };
    +    },
    +
    +    componentDidMount: function () {
    +      notificationStore.on('change', this.onChange, this);
    +    },
    +
    +    componentWillUnmount: function () {
    +      notificationStore.off('change', this.onChange);
    +    },
    +
    +    onChange: function () {
    +      if (this.isMounted()) {
    +        this.setState(this.getStoreState());
    +      }
    +    },
    +
    +    getNotifications: function () {
    +      if (!this.state.notifications.length) {
    +        return (
    +          <li className="no-notifications">No notifications.</li>
    +        );
    +      }
    +
    +      return _.map(this.state.notifications, function (notification, i) {
    +        return (
    +          <NotificationRow
    +            isVisible={this.state.isVisible}
    +            item={notification}
    +            filter={this.state.filter}
    +            key={notification.notificationId}
    +          />
    +        );
    +      }, this);
    +    },
    +
    +    selectFilter: function (e) {
    +      var filter = $(e.target).closest('li').data('filter');
    +      Actions.selectNotificationFilter(filter);
    +    },
    +
    +    render: function () {
    +      var panelClasses = 'notification-center-panel flex-layout flex-col';
    +      if (this.state.isVisible) {
    +        panelClasses += ' visible';
    +      }
    +
    +      var filterClasses = {
    +        all: 'flex-body',
    +        success: 'flex-body',
    +        error: 'flex-body',
    +        info: 'flex-body'
    +      };
    +      filterClasses[this.state.filter] += ' selected';
    +
    +      var maskClasses = 'notification-page-mask' + ((this.state.isVisible) ? ' visible' : '');
    +      return (
    +        <div>
    +          <div className={panelClasses}>
    +
    +            <header className="flex-layout flex-row">
    +              <span className="fonticon fonticon-bell"></span>
    +              <h1 className="flex-body">Notifications</h1>
    +              <button type="button" aria-hidden="true" onClick={Actions.hideNotificationCenter}>×</button>
    +            </header>
    +
    +            <ul className="notification-filter flex-layout flex-row" onClick={this.selectFilter}>
    +              <li className={filterClasses.all} data-filter="all" title="All notifications">All</li>
    +              <li className={filterClasses.success} data-filter="success" title="Success notifications">
    +                <span className="fonticon fonticon-ok-circled"></span>
    +              </li>
    +              <li className={filterClasses.error} data-filter="error" title="Error notifications">
    +                <span className="fonticon fonticon-attention-circled"></span>
    +              </li>
    +              <li className={filterClasses.info} data-filter="info" title="Info notifications">
    +                <span className="fonticon fonticon-info-circled"></span>
    +              </li>
    +            </ul>
    +
    +            <div className="flex-body">
    +              <ul className="notification-list">
    +                {this.getNotifications()}
    +              </ul>
    +            </div>
    +
    +            <footer>
    +              <input type="button" value="Clear All" className="btn btn-small btn-info" onClick={Actions.clearAllNotifications} />
    +            </footer>
    +          </div>
    +
    +          <div className={maskClasses} onClick={Actions.hideNotificationCenter}></div>
    +        </div>
    +      );
    +    }
    +  });
    +
    +  var NotificationRow = React.createClass({
    +    propTypes: {
    +      item: React.PropTypes.object.isRequired,
    +      filter: React.PropTypes.string.isRequired,
    +      transitionSpeed: React.PropTypes.number
    +    },
    +
    +    getDefaultProps: function () {
    +      return {
    +        transitionSpeed: 300
    +      };
    +    },
    +
    +    clearNotification: function () {
    +      var notificationId = this.props.item.notificationId;
    +      this.hide(function () {
    +        Actions.clearSingleNotification(notificationId);
    +      });
    +    },
    +
    +    componentDidMount: function () {
    +      this.setState({
    +        elementHeight: this.getHeight()
    +      });
    +    },
    +
    +    componentDidUpdate: function (prevProps) {
    +      // in order for the nice slide effects to work we need a concrete element height to slide to and from.
    +      // $.outerHeight() only works reliably on visible elements, hence this additional setState here
    +      if (!prevProps.isVisible && this.props.isVisible) {
    +        this.setState({
    +          elementHeight: this.getHeight()
    +        });
    +      }
    +
    +      var show = true;
    +      if (this.props.filter !== 'all') {
    +        show = this.props.item.type === this.props.filter;
    +      }
    +      if (show) {
    +        console.log(this.state.elementHeight);
    +        $(this.getDOMNode()).velocity({ opacity: 1, height: this.state.elementHeight }, this.props.transitionSpeed);
    +      } else {
    +        this.hide();
    +      }
    +    },
    +
    +    getHeight: function () {
    +      return $(this.getDOMNode()).outerHeight(true);
    +    },
    +
    +    hide: function (onHidden) {
    +      $(this.getDOMNode()).velocity({ opacity: 0, height: 0 }, this.props.transitionSpeed, function () {
    +        if (onHidden) {
    +          onHidden();
    +        }
    +      });
    +    },
    +
    +    render: function () {
    +      var iconMap = {
    +        success: 'fonticon-ok-circled',
    +        error: 'fonticon-attention-circled',
    +        info: 'fonticon-info-circled'
    +      };
    +
    +      var timeElapsed = this.props.item.time.fromNow();
    +
    +      // we can safely do this because the store ensures all notifications are of known types
    +      var rowIconClasses = 'fonticon ' + iconMap[this.props.item.type];
    +      var classes = 'flex-layout flex-row';
    +
    +      // for testing purposes
    +      var visible = (this.props.filter === 'all' || this.props.filter === this.props.item.type) ? 'true' : 'false';
    +
    +      // N.B. wrapper <div> needed to ensure smooth hide/show transitions
    +      return (
    +        <li data-visible={visible}>
    --- End diff --
    
    i've seen that for the first time someone doing in react.
    
    i am really against storing state in the dom like we did in jquery times, that's one of the reason why we chose react/flux, to enforce a separation of concerns. we run into two issues here:
    
    1. adding code to production code for a test (not to confuse with making code testable)
    2. storing state in the DOM
    
    there are multiple ways to avoid this, one is testing for visibility in your tests:
    
    ```js
    $('.mycomponent').is(':visible')
    ```
    
    another way is to test if the class that gives visibility appears after an action


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] couchdb-fauxton pull request: Notification Center

Posted by robertkowalski <gi...@git.apache.org>.
Github user robertkowalski commented on a diff in the pull request:

    https://github.com/apache/couchdb-fauxton/pull/544#discussion_r41139507
  
    --- Diff: app/addons/fauxton/components.react.jsx ---
    @@ -334,13 +354,246 @@ function (app, FauxtonAPI, React, ZeroClipboard) {
       });
     
     
    +  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({
    +
    +    getInitialState: function () {
    +      return this.getStoreState();
    +    },
    +
    +    getStoreState: function () {
    +      return {
    +        isVisible: notificationStore.isNotificationCenterVisible(),
    +        filter: notificationStore.getNotificationFilter(),
    +        notifications: notificationStore.getNotifications()
    +      };
    +    },
    +
    +    componentDidMount: function () {
    +      notificationStore.on('change', this.onChange, this);
    +    },
    +
    +    componentWillUnmount: function () {
    +      notificationStore.off('change', this.onChange);
    +    },
    +
    +    onChange: function () {
    +      if (this.isMounted()) {
    +        this.setState(this.getStoreState());
    +      }
    +    },
    +
    +    getNotifications: function () {
    +      if (!this.state.notifications.length) {
    +        return (
    +          <li className="no-notifications">No notifications.</li>
    +        );
    +      }
    +
    +      return _.map(this.state.notifications, function (notification, i) {
    +        return (
    +          <NotificationRow
    +            isVisible={this.state.isVisible}
    +            item={notification}
    +            filter={this.state.filter}
    +            key={notification.notificationId}
    +          />
    +        );
    +      }, this);
    +    },
    +
    +    selectFilter: function (e) {
    +      var filter = $(e.target).closest('li').data('filter');
    +      Actions.selectNotificationFilter(filter);
    +    },
    +
    +    render: function () {
    +      var panelClasses = 'notification-center-panel flex-layout flex-col';
    +      if (this.state.isVisible) {
    +        panelClasses += ' visible';
    +      }
    +
    +      var filterClasses = {
    +        all: 'flex-body',
    +        success: 'flex-body',
    +        error: 'flex-body',
    +        info: 'flex-body'
    +      };
    +      filterClasses[this.state.filter] += ' selected';
    +
    +      var maskClasses = 'notification-page-mask' + ((this.state.isVisible) ? ' visible' : '');
    +      return (
    +        <div>
    +          <div className={panelClasses}>
    +
    +            <header className="flex-layout flex-row">
    +              <span className="fonticon fonticon-bell"></span>
    +              <h1 className="flex-body">Notifications</h1>
    +              <button type="button" aria-hidden="true" onClick={Actions.hideNotificationCenter}>×</button>
    +            </header>
    +
    +            <ul className="notification-filter flex-layout flex-row" onClick={this.selectFilter}>
    +              <li className={filterClasses.all} data-filter="all" title="All notifications">All</li>
    +              <li className={filterClasses.success} data-filter="success" title="Success notifications">
    +                <span className="fonticon fonticon-ok-circled"></span>
    +              </li>
    +              <li className={filterClasses.error} data-filter="error" title="Error notifications">
    +                <span className="fonticon fonticon-attention-circled"></span>
    +              </li>
    +              <li className={filterClasses.info} data-filter="info" title="Info notifications">
    +                <span className="fonticon fonticon-info-circled"></span>
    +              </li>
    +            </ul>
    --- End diff --
    
    on possible way to solve this. just add the event handler on all three nodes, react will take care of the memory management / delegation internally:
    
    ```html
                <ul className="notification-filter flex-layout flex-row">
                  <li  onClick={this.selectFilter.bind(this, 'all')} className={filterClasses.all} title="All notifications">All</li>
                
       [...]
    ```
    
    this also removes the need for jquery and accessing the dom directly, what we should really avoid


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---

[GitHub] couchdb-fauxton pull request: Notification Center

Posted by robertkowalski <gi...@git.apache.org>.
Github user robertkowalski commented on a diff in the pull request:

    https://github.com/apache/couchdb-fauxton/pull/544#discussion_r41138994
  
    --- Diff: app/addons/fauxton/components.react.jsx ---
    @@ -334,13 +354,246 @@ function (app, FauxtonAPI, React, ZeroClipboard) {
       });
     
     
    +  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({
    +
    +    getInitialState: function () {
    +      return this.getStoreState();
    +    },
    +
    +    getStoreState: function () {
    +      return {
    +        isVisible: notificationStore.isNotificationCenterVisible(),
    +        filter: notificationStore.getNotificationFilter(),
    +        notifications: notificationStore.getNotifications()
    +      };
    +    },
    +
    +    componentDidMount: function () {
    +      notificationStore.on('change', this.onChange, this);
    +    },
    +
    +    componentWillUnmount: function () {
    +      notificationStore.off('change', this.onChange);
    +    },
    +
    +    onChange: function () {
    +      if (this.isMounted()) {
    +        this.setState(this.getStoreState());
    +      }
    +    },
    +
    +    getNotifications: function () {
    +      if (!this.state.notifications.length) {
    +        return (
    +          <li className="no-notifications">No notifications.</li>
    +        );
    +      }
    +
    +      return _.map(this.state.notifications, function (notification, i) {
    +        return (
    +          <NotificationRow
    +            isVisible={this.state.isVisible}
    +            item={notification}
    +            filter={this.state.filter}
    +            key={notification.notificationId}
    +          />
    +        );
    +      }, this);
    +    },
    +
    +    selectFilter: function (e) {
    +      var filter = $(e.target).closest('li').data('filter');
    --- End diff --
    
    jquery?


---
If your project is set up for it, you can reply to this email and have your
reply appear on GitHub as well. If your project does not have this feature
enabled and wishes so, or if the feature is enabled but not working, please
contact infrastructure at infrastructure@apache.org or file a JIRA ticket
with INFRA.
---