<commit_before>import React, { Component } from 'react';
import { cloneDeep } from 'lodash';
import debugFactory from 'debug/browser';

import Avatar from 'material-ui/lib/avatar';
import AppBar from 'material-ui/lib/app-bar';
import Dialog from 'material-ui/lib/dialog';
import Snackbar from 'material-ui/lib/snackbar';
import FlatButton from 'material-ui/lib/flat-button';
import IconButton from 'material-ui/lib/icon-button';
import LeftNav from 'material-ui/lib/left-nav';
import NavigationMenuIcon from 'material-ui/lib/svg-icons/navigation/menu';
import List from 'material-ui/lib/lists/list';
import ListItem from 'material-ui/lib/lists/list-item';
import ContentEdit from 'material-ui/lib/svg-icons/image/edit';
import ContentOpen from 'material-ui/lib/svg-icons/file/folder-open';
import ContentSave from 'material-ui/lib/svg-icons/content/save';
import ContentCreate from 'material-ui/lib/svg-icons/content/add';
import ContentClear from 'material-ui/lib/svg-icons/content/clear';

import Scenario from './Scenario';
import InputForm from './InputForm';
import ProfitAndLoss from './ProfitAndLoss';
import { request } from './request';

const debug = debugFactory('vbi:app');

const MAX_DOCS = 10;  // maximum number of visible docs in the left navigation menu

const APP_BAR_STYLE = {
  position: 'fixed',
  top: 0,
  left: 0,
  zIndex: 999,
  background: '#f3742c'
};

// expose the debug library to window, so we can enable and disable it
if (typeof window !== 'undefined') {
  console.info('To enable debugging, enter debug.enable(\'vbi:*\') in this console, then refresh the page');
  console.info('To disable debugging, enter debug.disable() in this console, then refresh the page');
  window.debug = debugFactory;
}

export default class App extends Component {
  constructor (props) {
    super(props);

    this.scenario = new Scenario();
    this.scenario.on('change', (doc) => {
      this.setState({doc, changed: false});
    });
    this.scenario.on('notification', (notification) => {
      debug('notification', notification);
      this.setState({notification});
    });
    this.scenario.on('error', (err) => this.handleError(err));

    // update the redirectTo url when the url changes
    window.addEventListener('hashchange', () => this.setState({
      redirectTo: encodeURIComponent(location.href)
    }));

    this.state = {
      user: {},

      changed: false,
      doc: this.scenario.get(),   // The current doc
      docs: [],                   // list with all docs of the user
      docsLimit: true,            // limit the number of visible docs

      showLeftNav: false,
      showTitleDialog: false,
      newTitle: null,

      showSignInDialog: false,
      showAskToSignIn: false,
      deleteDocDialog: null, // null or { title: string, id: string, rev: string }

      notification: null, // null or { message: string, closeable: boolean, duration: number | null}

      redirectTo: encodeURIComponent(location.href)
    };
  }

  render() {
    return (

        { this.renderAppBar() }
        { this.renderLefNav() }
        { this.renderTitleDialog() }
        { this.renderSignInDialog() }
        { this.renderAskToSignInDialog() }
        { this.renderDeleteDocDialog() }
        { this.renderNotification() }

            <InputForm data={this.state.doc.data} onChange={data => this.handleChange(data)} />

            <ProfitAndLoss data={this.state.doc.data} />

          Copyright © 2016 VanPaz

    );
  }

  renderAppBar () {
    let docTitle = this.state.doc
        ?  {
            event.stopPropagation();
            event.preventDefault();

            this.setState({showLeftNav: false});
            this.handleRename()
          }}>[{this.state.doc.title}]
        : '';
    let handleSave = (event) => {
      event.preventDefault();
      this.handleSave();
    };
    let changed = this.state.doc && this.state.doc._id && this.state.changed
        ?
            changed (
            save now
            )

        : null;
    let title =
VanPaz Business Intelligence {docTitle} {changed}
;

    return <AppBar
        style={APP_BAR_STYLE}
        title={title}
        iconElementLeft={
              <IconButton onTouchTap={(event) => this.handleToggleLeftNav() }>
                <NavigationMenuIcon />
              </IconButton> }
        iconElementRight={ this.renderUser() } />
  }

  renderLefNav () {
    const docsList = this.state.docs.map(doc => {
      const rightIcon = <ContentClear onTouchTap={ (event) => {
        event.stopPropagation();
        this.setState({
          deleteDocDialog: {
            id: doc.id,
            rev: doc.rev,
            title: doc.title
          }
        });
      }} />;

      return <ListItem
          key={doc.id}
          primaryText={doc.title || doc.id}
          rightIcon={rightIcon}
          onTouchTap={(event) => {
            this.setState({showLeftNav: false});
            this.handleOpen(doc.id)
          }}
      />;
    });

    // limit the number of displayed documents
    if (this.state.docsLimit) {
      if (docsList.length > MAX_DOCS) {
        let more = <ListItem
            key={'more'}
            primaryText={'more...'}
            onTouchTap={(event) => {
              this.setState({docsLimit: false});
            }}
        />;

        docsList.splice(MAX_DOCS);
        docsList.push(more);
      }
    }

    return <LeftNav docked={false}
             open={this.state.showLeftNav}
             onRequestChange={(show) => this.setState({showLeftNav: show}) } >
      <AppBar title="Menu"
              style={{background: APP_BAR_STYLE.background}}
              iconElementLeft={
                    <IconButton onTouchTap={(event) => this.handleToggleLeftNav() }>
                      <NavigationMenuIcon />
                    </IconButton>
                  } />

      <List subheader="Manage scenarios">
        <ListItem
            primaryText="New"
            leftIcon={<ContentCreate />}
            onTouchTap={(event) => {
              this.setState({showLeftNav: false});
              this.handleNew()
            }} />
        <ListItem
            primaryText="Rename"
            leftIcon={<ContentEdit />}
            onTouchTap={(event) => {
              this.setState({showLeftNav: false});
              this.handleRename()
            }} />
        <ListItem
            primaryText="Open"
            leftIcon={<ContentOpen />}
            initiallyOpen={true}
            primaryTogglesNestedList={true}
            nestedItems={docsList} />
        <ListItem
            primaryText="Save"
            leftIcon={<ContentSave />}
            onTouchTap={(event) => {
              this.setState({showLeftNav: false});
              this.handleSave()
            }} />
        <ListItem
            primaryText="Save as..."
            leftIcon={<ContentSave />}
            onTouchTap={(event) => alert('Sorry, not yet implemented...') } />
      </List>

    </LeftNav>
  }

  // render "sign in" or "signed in as"
  renderUser () {
    if (this.isSignedIn()) {
      let source = this.state.user.email || this.state.user.provider;
      let title = `Logged in as ${this.state.user.displayName} (${source})`;
      let buttonContents =

        Sign out
        <Avatar src={this.state.user.photo} style={{verticalAlign: 'bottom'}} />

;

      return
        <FlatButton children={ buttonContents } onTouchTap={(event) => this.handleSignOut()} />
      ;
    }
    else {
      return <FlatButton label="Sign in" onTouchTap={(event) => this.setState({showSignInDialog: true})} />
    }
  }

  isSignedIn () {
    return this.state.user && this.state.user.id;
  }

  renderSignInDialog () {
    const signInActions = [
      <FlatButton
          label="Cancel"
          onTouchTap={ (event) => this.setState({ showSignInDialog: false}) }
      />
    ];

    return <Dialog
        title="Sign in"
        actions={signInActions}
        modal={false}
        open={this.state.showSignInDialog}
        onRequestClose={ (event) => this.setState({showSignInDialog: false}) }
        contentStyle={{maxWidth: 500}} >

        Sign in with your Google or Facebook account:

        <a href={`/api/v1/auth/google/signin?redirectTo=${this.state.redirectTo || ''}`} className="sign-in" >

        <a href={`/api/v1/auth/facebook/signin?redirectTo=${this.state.redirectTo || ''}`} className="sign-in" >

    </Dialog>
  }

  renderAskToSignInDialog () {
    let cancel = (event) => this.setState({
      showAskToSignIn: false
    });
    let ok = (event) => this.setState({
      showAskToSignIn: false,
      showSignInDialog: true
    });

    const actions = [
      <FlatButton
          label="No"
          secondary={true}
          onTouchTap={cancel}
      />,
      <FlatButton
          label="Yes"
          primary={true}
          keyboardFocused={true}
          onTouchTap={ok}
      />
    ];

    return <Dialog
        title="Not signed in"
        actions={actions}
        modal={false}
        open={this.state.showAskToSignIn}
        onRequestClose={cancel} >

        To open, save, or delete scenarios you have to sign in first.

        Do you want to sign in now?

    </Dialog>
  }

  renderDeleteDocDialog () {
    const cancel = (event) => {
      this.setState({ deleteDocDialog: null });
    };
    const ok = (event) => {
      this.setState({ deleteDocDialog: null });

      this.handleDelete(this.state.deleteDocDialog);
    };

    const actions = [
      <FlatButton
          label="Cancel"
          secondary={true}
          onTouchTap={cancel}
      />,
      <FlatButton
          label="Delete"
          primary={true}
          keyboardFocused={true}
          onTouchTap={ok}
      />
    ];

    const title = this.state.deleteDocDialog &&
        (this.state.deleteDocDialog.title || this.state.deleteDocDialog.id);

    return <Dialog
        title="Delete scenario"
        actions={actions}
        modal={false}
        open={this.state.deleteDocDialog ? true : false}
        onRequestClose={cancel} >

        Are you sure you want to delete {title}?

    </Dialog>
  }

  renderTitleDialog () {
    const cancel = (event) => {
      this.setState({ showTitleDialog: false });
    };
    const ok = (event) => {
      event.preventDefault();
      event.stopPropagation();

      this.setState({ showTitleDialog: false });

      let doc = cloneDeep(this.state.doc);
      doc.title = this.state.newTitle;
      this.changeDoc(doc);
    };

    const actions = [
      <FlatButton
          label="Cancel"
          secondary={true}
          onTouchTap={cancel}
      />,
      <FlatButton
          label="Ok"
          primary={true}
          keyboardFocused={true}
          onTouchTap={ok}
      />
    ];

    return <Dialog
        title="Rename"
        actions={actions}
        modal={false}
        open={this.state.showTitleDialog}
        onRequestClose={cancel} >

        Enter a title for the scenario:

      <form onSubmit={ok}>
        <input
            className="title"
            ref="title"
            value={this.state.newTitle}
            onChange={(event) => this.setState({newTitle: event.target.value}) } />
      </form>
    </Dialog>
  }

  renderNotification () {
    let notification = this.state.notification || {};
    let isError = notification.type === 'error';
    let closeable = this.state.closeable !== undefined
      ? notification.closeable
      : !isError;
    let close = () => this.setState({notification: null});
    let ignore = () => null;  // just ignore request to close
    let onRequestClose = closeable ? close : ignore;

    return

      <Snackbar
          className={`snackbar${isError ? ' error' : ''}`}
          open={notification.message ? true : false}
          message={notification.message || ''}
          autoHideDuration={notification.duration}
          onRequestClose={onRequestClose}
          action={isError ? 'Close' : null}
          onActionTouchTap={close}
      />

;
  }

  componentDidMount () {
    this.fetchUser()
        .then(user => this.setState({ user }))
        .catch(err => this.handleError(err));

    this.fetchDocs();
  }

  handleNew () {
    this.scenario.createNew();
    this.setState({
      doc: this.scenario.get()
    });
  }

  handleRename () {
    this.setState({
      showTitleDialog: true,
      newTitle: this.state.doc.title
    });

    // select the contents of the input field on opening the dialog
    setTimeout( () => this.refs.title.select(), 0);
  }

  handleOpen (id) {
    debug('handleOpen', id);

    if (this.isSignedIn()) {
      this.scenario.open(id)
          .then(doc => this.setState({ doc }))
          .catch((err) => this.handleError(err));
    }
    else {
      this.setState({showAskToSignIn: true});
    }
  }

  handleSave () {
    debug('handleSave');

    if (this.isSignedIn()) {
      this.scenario.save()
          .then((doc) => {
            this.setState({doc, changed: false});
            this.fetchDocs();
          })
          .catch((err) => this.handleError(err));
    } else {
      this.setState({showAskToSignIn: true});
    }
  }

  /**
   * Delete a document
   * @param {{id: string, rev: string, title: string}} what
   */
  handleDelete (what) {
    debug('handleDelete');

    if (this.isSignedIn()) {
      this.scenario.del(what.id, what.rev, what.title)
          .then(() => this.fetchDocs())
          .catch(err => this.handleError(err));
    } else {
      this.setState({showAskToSignIn: true});
    }
  }

  handleError (err) {
    this.setState({
      notification: {
        type: 'error',
        message: err.toString()
      }
    });
  }

  handleToggleLeftNav () {
    this.setState({
      showLeftNav: !this.state.showLeftNav
    })
  }

  handleChange (data) {
    debug('handleChange', data);

    let updatedDoc = cloneDeep(this.state.doc);
    updatedDoc.data = data;

    this.changeDoc(updatedDoc);
  }

  handleSignOut () {
    window.open(`/api/v1/auth/signout?redirectTo=${this.state.redirectTo || ''}`, '_self');
  }

  changeDoc (doc) {
    this.scenario.set(doc);

    this.setState({
      doc,
      changed: true
    });

  }

  fetchUser () {
    debug('fetching user...');
    return request('GET', '/user');
  }

  fetchDocs () {
    debug('fetching docs...');
    this.scenario.list()
        .then(docs => {
          // sort by updated field, from newest to oldest document
          docs.sort( this.compareUpdated  );
          debug('docs', docs);

          this.setState({ docs })
        })
        .catch(err => this.handleError(err));
  }

  /**
   * Compare two documents by their updated value and order them with
   * newest document first.
   * @param {{updated: string}} a
   * @param {{updated: string}} b
   * @return {number} returns -1 when a is newer then b, 1 when a is older than b,
   *                  and 0 when both have the same age.
   */
  compareUpdated (a, b) {
    return a.updated > b.updated ? -1 : a.updated < b.updated ? 1 : 0;
  }
}

<commit_msg>Implemented auto save<commit_after>import React, { Component } from 'react';
import { cloneDeep, debounce } from 'lodash';
import debugFactory from 'debug/browser';

import Avatar from 'material-ui/lib/avatar';
import AppBar from 'material-ui/lib/app-bar';
import Dialog from 'material-ui/lib/dialog';
import Snackbar from 'material-ui/lib/snackbar';
import FlatButton from 'material-ui/lib/flat-button';
import IconButton from 'material-ui/lib/icon-button';
import LeftNav from 'material-ui/lib/left-nav';
import NavigationMenuIcon from 'material-ui/lib/svg-icons/navigation/menu';
import List from 'material-ui/lib/lists/list';
import ListItem from 'material-ui/lib/lists/list-item';
import ContentEdit from 'material-ui/lib/svg-icons/image/edit';
import ContentOpen from 'material-ui/lib/svg-icons/file/folder-open';
import ContentSave from 'material-ui/lib/svg-icons/content/save';
import ContentCreate from 'material-ui/lib/svg-icons/content/add';
import ContentClear from 'material-ui/lib/svg-icons/content/clear';

import Scenario from './Scenario';
import InputForm from './InputForm';
import ProfitAndLoss from './ProfitAndLoss';
import { request } from './request';

const debug = debugFactory('vbi:app');

const MAX_DOCS = 10;  // maximum number of visible docs in the left navigation menu

const AUTO_SAVE_DELAY = 5000; // milliseconds

const APP_BAR_STYLE = {
  position: 'fixed',
  top: 0,
  left: 0,
  zIndex: 999,
  background: '#f3742c'
};

// expose the debug library to window, so we can enable and disable it
if (typeof window !== 'undefined') {
  console.info('To enable debugging, enter debug.enable(\'vbi:*\') in this console, then refresh the page');
  console.info('To disable debugging, enter debug.disable() in this console, then refresh the page');
  window.debug = debugFactory;
}

export default class App extends Component {
  constructor (props) {
    super(props);

    this.scenario = new Scenario();
    this.scenario.on('change', (doc) => {
      this.setState({doc, changed: false});
      this.handleAutoSave.cancel();
    });
    this.scenario.on('notification', (notification) => {
      debug('notification', notification);
      this.setState({notification});
    });
    this.scenario.on('error', (err) => this.handleError(err));

    this.handleSave = this.handleSave.bind(this);
    this.handleAutoSave = debounce(this.handleSave, AUTO_SAVE_DELAY);

    // update the redirectTo url when the url changes
    window.addEventListener('hashchange', () => this.setState({
      redirectTo: encodeURIComponent(location.href)
    }));

    this.state = {
      user: {},

      changed: false,
      doc: this.scenario.get(),   // The current doc
      docs: [],                   // list with all docs of the user
      docsLimit: true,            // limit the number of visible docs

      showLeftNav: false,
      showTitleDialog: false,
      newTitle: null,

      showSignInDialog: false,
      showAskToSignIn: false,
      deleteDocDialog: null, // null or { title: string, id: string, rev: string }

      notification: null, // null or { message: string, closeable: boolean, duration: number | null}

      redirectTo: encodeURIComponent(location.href)
    };
  }

  render() {
    return (

        { this.renderAppBar() }
        { this.renderLefNav() }
        { this.renderTitleDialog() }
        { this.renderSignInDialog() }
        { this.renderAskToSignInDialog() }
        { this.renderDeleteDocDialog() }
        { this.renderNotification() }

            <InputForm data={this.state.doc.data} onChange={data => this.handleChange(data)} />

            <ProfitAndLoss data={this.state.doc.data} />

          Copyright © 2016 VanPaz

    );
  }

  renderAppBar () {
    let docTitle = this.state.doc
        ?  {
            event.stopPropagation();
            event.preventDefault();

            this.setState({showLeftNav: false});
            this.handleRename()
          }}>[{this.state.doc.title}]
        : '';
    let handleSave = (event) => {
      event.preventDefault();
      this.handleSave();
    };
    let changed = this.state.doc && this.state.doc._id && this.state.changed
        ?
            changed (
            save now
            )

        : null;
    let title =
VanPaz Business Intelligence {docTitle} {changed}
;

    return <AppBar
        style={APP_BAR_STYLE}
        title={title}
        iconElementLeft={
              <IconButton onTouchTap={(event) => this.handleToggleLeftNav() }>
                <NavigationMenuIcon />
              </IconButton> }
        iconElementRight={ this.renderUser() } />
  }

  renderLefNav () {
    const docsList = this.state.docs.map(doc => {
      const rightIcon = <ContentClear onTouchTap={ (event) => {
        event.stopPropagation();
        this.setState({
          deleteDocDialog: {
            id: doc.id,
            rev: doc.rev,
            title: doc.title
          }
        });
      }} />;

      return <ListItem
          key={doc.id}
          primaryText={doc.title || doc.id}
          rightIcon={rightIcon}
          onTouchTap={(event) => {
            this.setState({showLeftNav: false});
            this.handleOpen(doc.id)
          }}
      />;
    });

    // limit the number of displayed documents
    if (this.state.docsLimit) {
      if (docsList.length > MAX_DOCS) {
        let more = <ListItem
            key={'more'}
            primaryText={'more...'}
            onTouchTap={(event) => {
              this.setState({docsLimit: false});
            }}
        />;

        docsList.splice(MAX_DOCS);
        docsList.push(more);
      }
    }

    return <LeftNav docked={false}
             open={this.state.showLeftNav}
             onRequestChange={(show) => this.setState({showLeftNav: show}) } >
      <AppBar title="Menu"
              style={{background: APP_BAR_STYLE.background}}
              iconElementLeft={
                    <IconButton onTouchTap={(event) => this.handleToggleLeftNav() }>
                      <NavigationMenuIcon />
                    </IconButton>
                  } />

      <List subheader="Manage scenarios">
        <ListItem
            primaryText="New"
            leftIcon={<ContentCreate />}
            onTouchTap={(event) => {
              this.setState({showLeftNav: false});
              this.handleNew()
            }} />
        <ListItem
            primaryText="Rename"
            leftIcon={<ContentEdit />}
            onTouchTap={(event) => {
              this.setState({showLeftNav: false});
              this.handleRename()
            }} />
        <ListItem
            primaryText="Open"
            leftIcon={<ContentOpen />}
            initiallyOpen={true}
            primaryTogglesNestedList={true}
            nestedItems={docsList} />
        <ListItem
            primaryText="Save"
            leftIcon={<ContentSave />}
            onTouchTap={(event) => {
              this.setState({showLeftNav: false});
              this.handleSave()
            }} />
        <ListItem
            primaryText="Save as..."
            leftIcon={<ContentSave />}
            onTouchTap={(event) => alert('Sorry, not yet implemented...') } />
      </List>

    </LeftNav>
  }

  // render "sign in" or "signed in as"
  renderUser () {
    if (this.isSignedIn()) {
      let source = this.state.user.email || this.state.user.provider;
      let title = `Logged in as ${this.state.user.displayName} (${source})`;
      let buttonContents =

        Sign out
        <Avatar src={this.state.user.photo} style={{verticalAlign: 'bottom'}} />

;

      return
        <FlatButton children={ buttonContents } onTouchTap={(event) => this.handleSignOut()} />
      ;
    }
    else {
      return <FlatButton label="Sign in" onTouchTap={(event) => this.setState({showSignInDialog: true})} />
    }
  }

  isSignedIn () {
    return this.state.user && this.state.user.id;
  }

  renderSignInDialog () {
    const signInActions = [
      <FlatButton
          label="Cancel"
          onTouchTap={ (event) => this.setState({ showSignInDialog: false}) }
      />
    ];

    return <Dialog
        title="Sign in"
        actions={signInActions}
        modal={false}
        open={this.state.showSignInDialog}
        onRequestClose={ (event) => this.setState({showSignInDialog: false}) }
        contentStyle={{maxWidth: 500}} >

        Sign in with your Google or Facebook account:

        <a href={`/api/v1/auth/google/signin?redirectTo=${this.state.redirectTo || ''}`} className="sign-in" >

        <a href={`/api/v1/auth/facebook/signin?redirectTo=${this.state.redirectTo || ''}`} className="sign-in" >

    </Dialog>
  }

  renderAskToSignInDialog () {
    let cancel = (event) => this.setState({
      showAskToSignIn: false
    });
    let ok = (event) => this.setState({
      showAskToSignIn: false,
      showSignInDialog: true
    });

    const actions = [
      <FlatButton
          label="No"
          secondary={true}
          onTouchTap={cancel}
      />,
      <FlatButton
          label="Yes"
          primary={true}
          keyboardFocused={true}
          onTouchTap={ok}
      />
    ];

    return <Dialog
        title="Not signed in"
        actions={actions}
        modal={false}
        open={this.state.showAskToSignIn}
        onRequestClose={cancel} >

        To open, save, or delete scenarios you have to sign in first.

        Do you want to sign in now?

    </Dialog>
  }

  renderDeleteDocDialog () {
    const cancel = (event) => {
      this.setState({ deleteDocDialog: null });
    };
    const ok = (event) => {
      this.setState({ deleteDocDialog: null });

      this.handleDelete(this.state.deleteDocDialog);
    };

    const actions = [
      <FlatButton
          label="Cancel"
          secondary={true}
          onTouchTap={cancel}
      />,
      <FlatButton
          label="Delete"
          primary={true}
          keyboardFocused={true}
          onTouchTap={ok}
      />
    ];

    const title = this.state.deleteDocDialog &&
        (this.state.deleteDocDialog.title || this.state.deleteDocDialog.id);

    return <Dialog
        title="Delete scenario"
        actions={actions}
        modal={false}
        open={this.state.deleteDocDialog ? true : false}
        onRequestClose={cancel} >

        Are you sure you want to delete {title}?

    </Dialog>
  }

  renderTitleDialog () {
    const cancel = (event) => {
      this.setState({ showTitleDialog: false });
    };
    const ok = (event) => {
      event.preventDefault();
      event.stopPropagation();

      this.setState({ showTitleDialog: false });

      let doc = cloneDeep(this.state.doc);
      doc.title = this.state.newTitle;
      this.changeDoc(doc);
    };

    const actions = [
      <FlatButton
          label="Cancel"
          secondary={true}
          onTouchTap={cancel}
      />,
      <FlatButton
          label="Ok"
          primary={true}
          keyboardFocused={true}
          onTouchTap={ok}
      />
    ];

    return <Dialog
        title="Rename"
        actions={actions}
        modal={false}
        open={this.state.showTitleDialog}
        onRequestClose={cancel} >

        Enter a title for the scenario:

      <form onSubmit={ok}>
        <input
            className="title"
            ref="title"
            value={this.state.newTitle}
            onChange={(event) => this.setState({newTitle: event.target.value}) } />
      </form>
    </Dialog>
  }

  renderNotification () {
    let notification = this.state.notification || {};
    let isError = notification.type === 'error';
    let closeable = this.state.closeable !== undefined
      ? notification.closeable
      : !isError;
    let close = () => this.setState({notification: null});
    let ignore = () => null;  // just ignore request to close
    let onRequestClose = closeable ? close : ignore;

    return

      <Snackbar
          className={`snackbar${isError ? ' error' : ''}`}
          open={notification.message ? true : false}
          message={notification.message || ''}
          autoHideDuration={notification.duration}
          onRequestClose={onRequestClose}
          action={isError ? 'Close' : null}
          onActionTouchTap={close}
      />

;
  }

  componentDidMount () {
    this.fetchUser()
        .then(user => this.setState({ user }))
        .catch(err => this.handleError(err));

    this.fetchDocs();
  }

  handleNew () {
    this.scenario.createNew();
    this.setState({
      doc: this.scenario.get()
    });
  }

  handleRename () {
    this.setState({
      showTitleDialog: true,
      newTitle: this.state.doc.title
    });

    // select the contents of the input field on opening the dialog
    setTimeout( () => this.refs.title.select(), 0);
  }

  handleOpen (id) {
    debug('handleOpen', id);

    if (this.isSignedIn()) {
      this.scenario.open(id)
          .then(doc => this.setState({ doc }))
          .catch((err) => this.handleError(err));
    }
    else {
      this.setState({showAskToSignIn: true});
    }
  }

  handleSave () {
    debug('handleSave');

    if (this.isSignedIn()) {
      this.handleAutoSave.cancel();

      this.scenario.save()
          .then((doc) => {
            this.setState({doc, changed: false});
            this.fetchDocs();
          })
          .catch((err) => this.handleError(err));
    } else {
      this.setState({showAskToSignIn: true});
    }
  }

  /**
   * Delete a document
   * @param {{id: string, rev: string, title: string}} what
   */
  handleDelete (what) {
    debug('handleDelete');

    if (this.isSignedIn()) {
      this.scenario.del(what.id, what.rev, what.title)
          .then(() => this.fetchDocs())
          .catch(err => this.handleError(err));
    } else {
      this.setState({showAskToSignIn: true});
    }
  }

  handleError (err) {
    this.setState({
      notification: {
        type: 'error',
        message: err.toString()
      }
    });
  }

  handleToggleLeftNav () {
    this.setState({
      showLeftNav: !this.state.showLeftNav
    })
  }

  handleChange (data) {
    debug('handleChange', data);

    let updatedDoc = cloneDeep(this.state.doc);
    updatedDoc.data = data;

    this.changeDoc(updatedDoc);
  }

  handleSignOut () {
    window.open(`/api/v1/auth/signout?redirectTo=${this.state.redirectTo || ''}`, '_self');
  }

  changeDoc (doc) {
    this.scenario.set(doc);

    this.setState({
      doc,
      changed: true
    });

    // auto save after a delay
    this.handleAutoSave();
  }

  fetchUser () {
    debug('fetching user...');
    return request('GET', '/user');
  }

  fetchDocs () {
    debug('fetching docs...');
    this.scenario.list()
        .then(docs => {
          // sort by updated field, from newest to oldest document
          docs.sort( this.compareUpdated  );
          debug('docs', docs);

          this.setState({ docs })
        })
        .catch(err => this.handleError(err));
  }

  /**
   * Compare two documents by their updated value and order them with
   * newest document first.
   * @param {{updated: string}} a
   * @param {{updated: string}} b
   * @return {number} returns -1 when a is newer then b, 1 when a is older than b,
   *                  and 0 when both have the same age.
   */
  compareUpdated (a, b) {
    return a.updated > b.updated ? -1 : a.updated < b.updated ? 1 : 0;
  }
}

<|endoftext|>


<commit_before>import * as core from '../../core';
import { WRAP_MODES } from '../../core/const';
import { readFileSync } from 'fs';
import { join } from 'path';

const tempMat = new core.Matrix();
const tempArray = new Float32Array(4);

/**
 * WebGL renderer plugin for tiling sprites
 *
 * @class
 * @memberof PIXI
 * @extends PIXI.ObjectRenderer
 */
export default class TilingSpriteRenderer extends core.ObjectRenderer
{

    /**
     * constructor for renderer
     *
     * @param {WebGLRenderer} renderer The renderer this tiling awesomeness works for.
     */
    constructor(renderer)
    {
        super(renderer);

        this.shader = null;
        this.simpleShader = null;
        this.quad = null;
    }

    /**
     * Sets up the renderer context and necessary buffers.
     *
     * @private
     */
    onContextChange()
    {
        const gl = this.renderer.gl;

        this.shader = new core.Shader(gl,
            readFileSync(join(__dirname, './tilingSprite.vert'), 'utf8'),
            readFileSync(join(__dirname, './tilingSprite.frag'), 'utf8'));
        this.simpleShader = new core.Shader(gl,
            readFileSync(join(__dirname, './tilingSprite.vert'), 'utf8'),
            readFileSync(join(__dirname, './tilingSprite_simple.frag'), 'utf8'));

        this.renderer.bindVao(null);
        this.quad = new core.Quad(gl, this.renderer.state.attribState);
        this.quad.initVao(this.shader);
    }

    /**
     *
     * @param {PIXI.extras.TilingSprite} ts tilingSprite to be rendered
     */
    render(ts)
    {
        const renderer = this.renderer;
        const quad = this.quad;

        renderer.bindVao(quad.vao);

        let vertices = quad.vertices;

        vertices[0] = vertices[6] = (ts._width) * -ts.anchor.x;
        vertices[1] = vertices[3] = ts._height * -ts.anchor.y;

        vertices[2] = vertices[4] = (ts._width) * (1.0 - ts.anchor.x);
        vertices[5] = vertices[7] = ts._height * (1.0 - ts.anchor.y);

        if (ts.uvRespectAnchor)
        {
            vertices = quad.uvs;

            vertices[0] = vertices[6] = -ts.anchor.x;
            vertices[1] = vertices[3] = -ts.anchor.y;

            vertices[2] = vertices[4] = 1.0 - ts.anchor.x;
            vertices[5] = vertices[7] = 1.0 - ts.anchor.y;
        }

        quad.upload();

        const tex = ts._texture;
        const baseTex = tex.baseTexture;
        const lt = ts.tileTransform.localTransform;
        const uv = ts.uvTransform;
        let isSimple = baseTex.isPowerOfTwo
            && tex.frame.width === baseTex.width && tex.frame.height === baseTex.height;

        // auto, force repeat wrapMode for big tiling textures
        if (isSimple)
        {
            if (!baseTex._glTextures[renderer.CONTEXT_UID])
            {
                if (baseTex.wrapMode === WRAP_MODES.CLAMP)
                {
                    baseTex.wrapMode = WRAP_MODES.REPEAT;
                }
            }
            else
            {
                isSimple = baseTex.wrapMode !== WRAP_MODES.CLAMP;
            }
        }

        const shader = isSimple ? this.simpleShader : this.shader;

        renderer.bindShader(shader);

        const w = tex.width;
        const h = tex.height;
        const W = ts._width;
        const H = ts._height;

        tempMat.set(lt.a * w / W,
            lt.b * w / H,
            lt.c * h / W,
            lt.d * h / H,
            lt.tx / W,
            lt.ty / H);

        // that part is the same as above:
        // tempMat.identity();
        // tempMat.scale(tex.width, tex.height);
        // tempMat.prepend(lt);
        // tempMat.scale(1.0 / ts._width, 1.0 / ts._height);

        tempMat.invert();
        if (isSimple)
        {
            tempMat.prepend(uv.mapCoord);
        }
        else
        {
            shader.uniforms.uMapCoord = uv.mapCoord.toArray(true);
            shader.uniforms.uClampFrame = uv.uClampFrame;
            shader.uniforms.uClampOffset = uv.uClampOffset;
        }

        shader.uniforms.uTransform = tempMat.toArray(true);

        const color = tempArray;

        core.utils.hex2rgb(ts.tint, color);
        color[3] = ts.worldAlpha;
        shader.uniforms.uColor = color;
        shader.uniforms.translationMatrix = ts.transform.worldTransform.toArray(true);

        shader.uniforms.uSampler = renderer.bindTexture(tex);

        renderer.setBlendMode(ts.blendMode);

        quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0);
    }
}

core.WebGLRenderer.registerPlugin('tilingSprite', TilingSpriteRenderer);
<commit_msg>cosmetic<commit_after>import * as core from '../../core';
import { WRAP_MODES } from '../../core/const';
import { readFileSync } from 'fs';
import { join } from 'path';

const tempMat = new core.Matrix();
const tempArray = new Float32Array(4);

/**
 * WebGL renderer plugin for tiling sprites
 *
 * @class
 * @memberof PIXI.extras
 * @extends PIXI.ObjectRenderer
 */
export default class TilingSpriteRenderer extends core.ObjectRenderer
{

    /**
     * constructor for renderer
     *
     * @param {WebGLRenderer} renderer The renderer this tiling awesomeness works for.
     */
    constructor(renderer)
    {
        super(renderer);

        this.shader = null;
        this.simpleShader = null;
        this.quad = null;
    }

    /**
     * Sets up the renderer context and necessary buffers.
     *
     * @private
     */
    onContextChange()
    {
        const gl = this.renderer.gl;

        this.shader = new core.Shader(gl,
            readFileSync(join(__dirname, './tilingSprite.vert'), 'utf8'),
            readFileSync(join(__dirname, './tilingSprite.frag'), 'utf8'));
        this.simpleShader = new core.Shader(gl,
            readFileSync(join(__dirname, './tilingSprite.vert'), 'utf8'),
            readFileSync(join(__dirname, './tilingSprite_simple.frag'), 'utf8'));

        this.renderer.bindVao(null);
        this.quad = new core.Quad(gl, this.renderer.state.attribState);
        this.quad.initVao(this.shader);
    }

    /**
     *
     * @param {PIXI.extras.TilingSprite} ts tilingSprite to be rendered
     */
    render(ts)
    {
        const renderer = this.renderer;
        const quad = this.quad;

        renderer.bindVao(quad.vao);

        let vertices = quad.vertices;

        vertices[0] = vertices[6] = (ts._width) * -ts.anchor.x;
        vertices[1] = vertices[3] = ts._height * -ts.anchor.y;

        vertices[2] = vertices[4] = (ts._width) * (1.0 - ts.anchor.x);
        vertices[5] = vertices[7] = ts._height * (1.0 - ts.anchor.y);

        if (ts.uvRespectAnchor)
        {
            vertices = quad.uvs;

            vertices[0] = vertices[6] = -ts.anchor.x;
            vertices[1] = vertices[3] = -ts.anchor.y;

            vertices[2] = vertices[4] = 1.0 - ts.anchor.x;
            vertices[5] = vertices[7] = 1.0 - ts.anchor.y;
        }

        quad.upload();

        const tex = ts._texture;
        const baseTex = tex.baseTexture;
        const lt = ts.tileTransform.localTransform;
        const uv = ts.uvTransform;
        let isSimple = baseTex.isPowerOfTwo
            && tex.frame.width === baseTex.width && tex.frame.height === baseTex.height;

        // auto, force repeat wrapMode for big tiling textures
        if (isSimple)
        {
            if (!baseTex._glTextures[renderer.CONTEXT_UID])
            {
                if (baseTex.wrapMode === WRAP_MODES.CLAMP)
                {
                    baseTex.wrapMode = WRAP_MODES.REPEAT;
                }
            }
            else
            {
                isSimple = baseTex.wrapMode !== WRAP_MODES.CLAMP;
            }
        }

        const shader = isSimple ? this.simpleShader : this.shader;

        renderer.bindShader(shader);

        const w = tex.width;
        const h = tex.height;
        const W = ts._width;
        const H = ts._height;

        tempMat.set(lt.a * w / W,
            lt.b * w / H,
            lt.c * h / W,
            lt.d * h / H,
            lt.tx / W,
            lt.ty / H);

        // that part is the same as above:
        // tempMat.identity();
        // tempMat.scale(tex.width, tex.height);
        // tempMat.prepend(lt);
        // tempMat.scale(1.0 / ts._width, 1.0 / ts._height);

        tempMat.invert();
        if (isSimple)
        {
            tempMat.prepend(uv.mapCoord);
        }
        else
        {
            shader.uniforms.uMapCoord = uv.mapCoord.toArray(true);
            shader.uniforms.uClampFrame = uv.uClampFrame;
            shader.uniforms.uClampOffset = uv.uClampOffset;
        }

        shader.uniforms.uTransform = tempMat.toArray(true);

        const color = tempArray;

        core.utils.hex2rgb(ts.tint, color);
        color[3] = ts.worldAlpha;
        shader.uniforms.uColor = color;
        shader.uniforms.translationMatrix = ts.transform.worldTransform.toArray(true);

        shader.uniforms.uSampler = renderer.bindTexture(tex);

        renderer.setBlendMode(ts.blendMode);

        quad.vao.draw(this.renderer.gl.TRIANGLES, 6, 0);
    }
}

core.WebGLRenderer.registerPlugin('tilingSprite', TilingSpriteRenderer);
<|endoftext|>


<commit_before>// 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([
  "app",
  "api",
  "backbone",
  "d3"
],

function (app, FauxtonAPI, Backbone, d3) {

  var Log = FauxtonAPI.addon();

  Log.Model = Backbone.Model.extend({

    date: function () {
      var date = new Date(this.get('date')),
          formatter = d3.time.format("%b %e %H:%M%:%S");

      return formatter(date);
    },

    logLevel: function () {
      return this.get('log_level').replace(/ /g,'');
    },

    pid: function () {
      return _.escape(this.get('pid'));
    },

    args: function () {
      return _.escape(this.get('args'));
    }

  });

  Log.Collection = Backbone.Collection.extend({
    model: Log.Model,

    initialize: function (options) {
      this.params = {bytes: 5000};
    },

    documentation: "log",

    url: function () {
      query = "?" + $.param(this.params);
      return app.host + '/_log' + query;
    },

    // override fetch because backbone expects json and couchdb sends text/html for logs,
    // I think its more elegant to set the dataType here than where ever fetch is called
    fetch: function (options) {
      options = options ? options : {};

      return Backbone.Collection.prototype.fetch.call(this, _.extend(options, {dataType: "html"}));
    },

    parse: function (resp) {
      var lines =  resp.split(/\n/);
      return _.foldr(lines, function (acc, logLine) {
        var match = logLine.match(/^\[(.*?)\]\s\[(.*?)\]\s\[(.*?)\]\s(.*)/);

        if (!match) { return acc;}

        acc.push({
                  date: match[1],
                  log_level: match[2],
                  pid: match[3],
                  args: match[4]
                 });

        return acc;
      }, []);
    }
  });

  Log.events = {};
  _.extend(Log.events, Backbone.Events);

  Log.Views.View = FauxtonAPI.View.extend({
    template: "addons/logs/templates/dashboard",

    initialize: function (options) {
      this.refreshTime = options.refreshTime || 5000;

      Log.events.on("log:filter", this.filterLogs, this);
      Log.events.on("log:remove", this.removeFilterLogs, this);

      this.filters = [];

      this.collection.on("add", function () {
        this.render();
      }, this);
    },

    establish: function () {
      return [this.collection.fetch()];
    },

    serialize: function () {
      return { logs: new Log.Collection(this.createFilteredCollection())};
    },

    afterRender: function () {
      this.startRefreshInterval();
    },

    cleanup: function () {
      this.stopRefreshInterval();
    },

    filterLogs: function (filter) {
      this.filters.push(filter);
      this.render();
    },

    createFilteredCollection: function () {
      var that = this;

      return _.reduce(this.filters, function (logs, filter) {

        return _.filter(logs, function (log) {
          var match = false;

          _.each(log, function (value) {
            if (value.toString().match(new RegExp(filter))) {
              match = true;
            }
          });
          return match;
        });

      }, this.collection.toJSON(), this);

    },

    removeFilterLogs: function (filter) {
      this.filters.splice(this.filters.indexOf(filter), 1);
      this.render();
    },

    startRefreshInterval: function () {
      var collection = this.collection;

      // Interval already set
      if (this.intervalId) { return ; }

      this.intervalId = setInterval(function () {
        collection.fetch();
      }, this.refreshTime);

    },

    stopRefreshInterval: function () {
      clearInterval(this.intervalId);
    }
  });

  Log.Views.FilterView = FauxtonAPI.View.extend({
    template: "addons/logs/templates/sidebar",

    events: {
      "submit #log-filter-form": "filterLogs"
    },

    filterLogs: function (event) {
      event.preventDefault();
      var $filter = this.$('input[name="filter"]'),
          filter = $filter.val();

      Log.events.trigger("log:filter", filter);

      this.insertView("#filter-list", new Log.Views.FilterItemView({
        filter: filter
      })).render();

      $filter.val('');
    }

  });

  Log.Views.FilterItemView = FauxtonAPI.View.extend({
    template: "addons/logs/templates/filterItem",
    tagName: "li",

    initialize: function (options) {
      this.filter = options.filter;
    },

    events: {
      "click .remove-filter": "removeFilter"
    },

    serialize: function () {
      return {
        filter: this.filter
      };
    },

    removeFilter: function (event) {
      event.preventDefault();

      Log.events.trigger("log:remove", this.filter);
      this.remove();
    }

  });

  return Log;

});
<commit_msg>Fauxton: Fix global var in logs/resources<commit_after>// 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([
  "app",
  "api",
  "backbone",
  "d3"
],

function (app, FauxtonAPI, Backbone, d3) {

  var Log = FauxtonAPI.addon();

  Log.Model = Backbone.Model.extend({

    date: function () {
      var date = new Date(this.get('date')),
          formatter = d3.time.format("%b %e %H:%M%:%S");

      return formatter(date);
    },

    logLevel: function () {
      return this.get('log_level').replace(/ /g,'');
    },

    pid: function () {
      return _.escape(this.get('pid'));
    },

    args: function () {
      return _.escape(this.get('args'));
    }

  });

  Log.Collection = Backbone.Collection.extend({
    model: Log.Model,

    initialize: function (options) {
      this.params = {bytes: 5000};
    },

    documentation: "log",

    url: function () {
      var query = "?" + $.param(this.params);
      return app.host + '/_log' + query;
    },

    // override fetch because backbone expects json and couchdb sends text/html for logs,
    // I think its more elegant to set the dataType here than where ever fetch is called
    fetch: function (options) {
      options = options ? options : {};

      return Backbone.Collection.prototype.fetch.call(this, _.extend(options, {dataType: "html"}));
    },

    parse: function (resp) {
      var lines =  resp.split(/\n/);
      return _.foldr(lines, function (acc, logLine) {
        var match = logLine.match(/^\[(.*?)\]\s\[(.*?)\]\s\[(.*?)\]\s(.*)/);

        if (!match) { return acc;}

        acc.push({
                  date: match[1],
                  log_level: match[2],
                  pid: match[3],
                  args: match[4]
                 });

        return acc;
      }, []);
    }
  });

  Log.events = {};
  _.extend(Log.events, Backbone.Events);

  Log.Views.View = FauxtonAPI.View.extend({
    template: "addons/logs/templates/dashboard",

    initialize: function (options) {
      this.refreshTime = options.refreshTime || 5000;

      Log.events.on("log:filter", this.filterLogs, this);
      Log.events.on("log:remove", this.removeFilterLogs, this);

      this.filters = [];

      this.collection.on("add", function () {
        this.render();
      }, this);
    },

    establish: function () {
      return [this.collection.fetch()];
    },

    serialize: function () {
      return { logs: new Log.Collection(this.createFilteredCollection())};
    },

    afterRender: function () {
      this.startRefreshInterval();
    },

    cleanup: function () {
      this.stopRefreshInterval();
    },

    filterLogs: function (filter) {
      this.filters.push(filter);
      this.render();
    },

    createFilteredCollection: function () {
      var that = this;

      return _.reduce(this.filters, function (logs, filter) {

        return _.filter(logs, function (log) {
          var match = false;

          _.each(log, function (value) {
            if (value.toString().match(new RegExp(filter))) {
              match = true;
            }
          });
          return match;
        });

      }, this.collection.toJSON(), this);

    },

    removeFilterLogs: function (filter) {
      this.filters.splice(this.filters.indexOf(filter), 1);
      this.render();
    },

    startRefreshInterval: function () {
      var collection = this.collection;

      // Interval already set
      if (this.intervalId) { return ; }

      this.intervalId = setInterval(function () {
        collection.fetch();
      }, this.refreshTime);

    },

    stopRefreshInterval: function () {
      clearInterval(this.intervalId);
    }
  });

  Log.Views.FilterView = FauxtonAPI.View.extend({
    template: "addons/logs/templates/sidebar",

    events: {
      "submit #log-filter-form": "filterLogs"
    },

    filterLogs: function (event) {
      event.preventDefault();
      var $filter = this.$('input[name="filter"]'),
          filter = $filter.val();

      Log.events.trigger("log:filter", filter);

      this.insertView("#filter-list", new Log.Views.FilterItemView({
        filter: filter
      })).render();

      $filter.val('');
    }

  });

  Log.Views.FilterItemView = FauxtonAPI.View.extend({
    template: "addons/logs/templates/filterItem",
    tagName: "li",

    initialize: function (options) {
      this.filter = options.filter;
    },

    events: {
      "click .remove-filter": "removeFilter"
    },

    serialize: function () {
      return {
        filter: this.filter
      };
    },

    removeFilter: function (event) {
      event.preventDefault();

      Log.events.trigger("log:remove", this.filter);
      this.remove();
    }

  });

  return Log;

});
<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2019 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

foam.CLASS({
  package: 'foam.nanos.auth',
  name: 'LifecycleAwareDAO',
  extends: 'foam.dao.ProxyDAO',

  javaImports: [
    'foam.core.FObject',
    'foam.core.X',
    'foam.dao.*',
    'foam.mlang.MLang',
    'foam.mlang.order.Comparator',
    'foam.mlang.predicate.Predicate',
    'foam.util.SafetyUtil',
  ],

  documentation: `
    DAO decorator that handles different lifecycle states
    (PENDING, REJECTED, APPROVED & DELETED) for objects

    In the case of remove_, LifecycleAwareDAO marks object as DELETED instead of actually removing
    the object from DAO then returns thus it should be placed at the end
    of the decorator stack before MDAO so that it wont't cut-off other
    decorators that also override "remove_" and "removeAll_".

    For filtering, objects with a lifecycle state of DELETED or REJECTED
    will be filtered out unless the user group has {name}.read.lifecyclestate.deleted
    and/or {name}.read.lifecyclestate.rejected respectively permission where {name} is
    either the lowercase name of the model of the objects by default or the {name} provided by the user.
  `,

  properties: [
    {
      class: 'String',
      name: 'name',
      javaFactory: 'return getOf().getClass().getSimpleName().toLowerCase();'
    },
    {
      class: 'String',
      name: 'deletePermission_',
      javaFactory: 'return getName() + ".read.lifecyclestate.deleted";'
    },
    {
      class: 'String',
      name: 'rejectPermission_',
      javaFactory: 'return getName() + ".read.lifecyclestate.rejected";'
    }
  ],

  methods: [
    {
      name: 'find_',
      javaCode: `
        FObject obj = getDelegate().find_(x, id);

        LifecycleAware lifecycleAwareObj = (LifecycleAware) obj;

        // ! we are also handling the deprecated DeletedAware until we fully remove it from the system
        if ( obj instanceof DeletedAware ){
          DeletedAware deletedAwareObj = (DeletedAware) obj;

          if ( obj == null ||
            ( ( lifecycleAwareObj.getLifecycleState() == LifecycleState.DELETED || deletedAwareObj.getDeleted() == true ) && ! canReadDeleted(x) ) ||
            ( lifecycleAwareObj.getLifecycleState() == LifecycleState.REJECTED && ! canReadRejected(x)) ) {
            return null;
          }
          return obj;
        }

        if ( obj == null ||
          ( lifecycleAwareObj.getLifecycleState() == LifecycleState.DELETED && ! canReadDeleted(x) ) ||
          ( lifecycleAwareObj.getLifecycleState() == LifecycleState.REJECTED && ! canReadRejected(x)) ) {
          return null;
        }

        return obj;
      `
    },
    {
      name: 'select_',
      javaCode: `
        if ( canReadDeleted(x) && canReadRejected(x) ) {
          return getDelegate().select_(x, sink, skip, limit, order, predicate);
        }

        if ( canReadDeleted(x) && ! canReadRejected(x) ) {
          return getDelegate()
            .where(
              MLang.NOT(
                MLang.EQ(getOf().getAxiomByName("lifecycleState"), LifecycleState.REJECTED)
              )
            )
            .select_(x, sink, skip, limit, order, predicate);
        }

        // ! we are also handling the deprecated DeletedAware until we fully remove it from the system
        if ( getOf().getAxiomByName("deleted") != null ){
          if ( ! canReadDeleted(x) && canReadRejected(x) ) {
            return getDelegate()
              .where(
                MLang.NOT(
                  MLang.OR(
                    MLang.EQ(getOf().getAxiomByName("lifecycleState"), LifecycleState.DELETED),
                    MLang.EQ(getOf().getAxiomByName("deleted"), true)
                  )
                )
              )
              .select_(x, sink, skip, limit, order, predicate);
          }

          return getDelegate()
            .where(
              MLang.NOT(
                MLang.OR(
                  MLang.EQ(getOf().getAxiomByName("lifecycleState"), LifecycleState.DELETED),
                  MLang.EQ(getOf().getAxiomByName("lifecycleState"), LifecycleState.REJECTED),
                  MLang.EQ(getOf().getAxiomByName("deleted"), true)
                )
              )
            )
            .select_(x, sink, skip, limit, order, predicate);
        }

        if ( ! canReadDeleted(x) && canReadRejected(x) ) {
          return getDelegate()
            .where(
              MLang.NOT(
                MLang.EQ(getOf().getAxiomByName("lifecycleState"), LifecycleState.DELETED)
              )
            )
            .select_(x, sink, skip, limit, order, predicate);
        }

        return getDelegate()
          .where(
            MLang.NOT(
              MLang.OR(
                MLang.EQ(getOf().getAxiomByName("lifecycleState"), LifecycleState.DELETED),
                MLang.EQ(getOf().getAxiomByName("lifecycleState"), LifecycleState.REJECTED)
              )
            )
          )
          .select_(x, sink, skip, limit, order, predicate);
      `
    },
    {
      name: 'remove_',
      javaCode: `
        if ( obj instanceof LifecycleAware ) {
          LifecycleAware clone = (LifecycleAware) obj.fclone();
          clone.setLifecycleState(LifecycleState.DELETED);
          obj = getDelegate().put_(x, (FObject) clone);
        } else {
          obj = getDelegate().remove_(x, obj); // can remove a non lifecycle Aware Obj
        }

        return obj;
      `
    },
    {
      name: 'removeAll_',
      javaCode: `
        getDelegate().select_(x, new RemoveSink(x, this), skip, limit, order, predicate);
      `
    },
    {
      name: 'canReadDeleted',
      type: 'Boolean',
      args: [
        { type: 'Context', name: 'x' }
      ],
      javaCode: `
        AuthService authService = (AuthService) getX().get("auth");
        return authService.check(x, getDeletePermission_());
      `
    },
    {
      name: 'canReadRejected',
      type: 'Boolean',
      args: [
        { type: 'Context', name: 'x' }
      ],
      javaCode: `
        AuthService authService = (AuthService) getX().get("auth");
        return authService.check(x, getRejectPermission_());
      `
    }
  ],
});
<commit_msg>Removed trailing comma<commit_after>/**
 * @license
 * Copyright 2019 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

foam.CLASS({
  package: 'foam.nanos.auth',
  name: 'LifecycleAwareDAO',
  extends: 'foam.dao.ProxyDAO',

  javaImports: [
    'foam.core.FObject',
    'foam.core.X',
    'foam.dao.*',
    'foam.mlang.MLang',
    'foam.mlang.order.Comparator',
    'foam.mlang.predicate.Predicate',
    'foam.util.SafetyUtil'
  ],

  documentation: `
    DAO decorator that handles different lifecycle states
    (PENDING, REJECTED, APPROVED & DELETED) for objects

    In the case of remove_, LifecycleAwareDAO marks object as DELETED instead of actually removing
    the object from DAO then returns thus it should be placed at the end
    of the decorator stack before MDAO so that it wont't cut-off other
    decorators that also override "remove_" and "removeAll_".

    For filtering, objects with a lifecycle state of DELETED or REJECTED
    will be filtered out unless the user group has {name}.read.lifecyclestate.deleted
    and/or {name}.read.lifecyclestate.rejected respectively permission where {name} is
    either the lowercase name of the model of the objects by default or the {name} provided by the user.
  `,

  properties: [
    {
      class: 'String',
      name: 'name',
      javaFactory: 'return getOf().getClass().getSimpleName().toLowerCase();'
    },
    {
      class: 'String',
      name: 'deletePermission_',
      javaFactory: 'return getName() + ".read.lifecyclestate.deleted";'
    },
    {
      class: 'String',
      name: 'rejectPermission_',
      javaFactory: 'return getName() + ".read.lifecyclestate.rejected";'
    }
  ],

  methods: [
    {
      name: 'find_',
      javaCode: `
        FObject obj = getDelegate().find_(x, id);

        LifecycleAware lifecycleAwareObj = (LifecycleAware) obj;

        // ! we are also handling the deprecated DeletedAware until we fully remove it from the system
        if ( obj instanceof DeletedAware ){
          DeletedAware deletedAwareObj = (DeletedAware) obj;

          if ( obj == null ||
            ( ( lifecycleAwareObj.getLifecycleState() == LifecycleState.DELETED || deletedAwareObj.getDeleted() == true ) && ! canReadDeleted(x) ) ||
            ( lifecycleAwareObj.getLifecycleState() == LifecycleState.REJECTED && ! canReadRejected(x)) ) {
            return null;
          }
          return obj;
        }

        if ( obj == null ||
          ( lifecycleAwareObj.getLifecycleState() == LifecycleState.DELETED && ! canReadDeleted(x) ) ||
          ( lifecycleAwareObj.getLifecycleState() == LifecycleState.REJECTED && ! canReadRejected(x)) ) {
          return null;
        }

        return obj;
      `
    },
    {
      name: 'select_',
      javaCode: `
        if ( canReadDeleted(x) && canReadRejected(x) ) {
          return getDelegate().select_(x, sink, skip, limit, order, predicate);
        }

        if ( canReadDeleted(x) && ! canReadRejected(x) ) {
          return getDelegate()
            .where(
              MLang.NOT(
                MLang.EQ(getOf().getAxiomByName("lifecycleState"), LifecycleState.REJECTED)
              )
            )
            .select_(x, sink, skip, limit, order, predicate);
        }

        // ! we are also handling the deprecated DeletedAware until we fully remove it from the system
        if ( getOf().getAxiomByName("deleted") != null ){
          if ( ! canReadDeleted(x) && canReadRejected(x) ) {
            return getDelegate()
              .where(
                MLang.NOT(
                  MLang.OR(
                    MLang.EQ(getOf().getAxiomByName("lifecycleState"), LifecycleState.DELETED),
                    MLang.EQ(getOf().getAxiomByName("deleted"), true)
                  )
                )
              )
              .select_(x, sink, skip, limit, order, predicate);
          }

          return getDelegate()
            .where(
              MLang.NOT(
                MLang.OR(
                  MLang.EQ(getOf().getAxiomByName("lifecycleState"), LifecycleState.DELETED),
                  MLang.EQ(getOf().getAxiomByName("lifecycleState"), LifecycleState.REJECTED),
                  MLang.EQ(getOf().getAxiomByName("deleted"), true)
                )
              )
            )
            .select_(x, sink, skip, limit, order, predicate);
        }

        if ( ! canReadDeleted(x) && canReadRejected(x) ) {
          return getDelegate()
            .where(
              MLang.NOT(
                MLang.EQ(getOf().getAxiomByName("lifecycleState"), LifecycleState.DELETED)
              )
            )
            .select_(x, sink, skip, limit, order, predicate);
        }

        return getDelegate()
          .where(
            MLang.NOT(
              MLang.OR(
                MLang.EQ(getOf().getAxiomByName("lifecycleState"), LifecycleState.DELETED),
                MLang.EQ(getOf().getAxiomByName("lifecycleState"), LifecycleState.REJECTED)
              )
            )
          )
          .select_(x, sink, skip, limit, order, predicate);
      `
    },
    {
      name: 'remove_',
      javaCode: `
        if ( obj instanceof LifecycleAware ) {
          LifecycleAware clone = (LifecycleAware) obj.fclone();
          clone.setLifecycleState(LifecycleState.DELETED);
          obj = getDelegate().put_(x, (FObject) clone);
        } else {
          obj = getDelegate().remove_(x, obj); // can remove a non lifecycle Aware Obj
        }

        return obj;
      `
    },
    {
      name: 'removeAll_',
      javaCode: `
        getDelegate().select_(x, new RemoveSink(x, this), skip, limit, order, predicate);
      `
    },
    {
      name: 'canReadDeleted',
      type: 'Boolean',
      args: [
        { type: 'Context', name: 'x' }
      ],
      javaCode: `
        AuthService authService = (AuthService) getX().get("auth");
        return authService.check(x, getDeletePermission_());
      `
    },
    {
      name: 'canReadRejected',
      type: 'Boolean',
      args: [
        { type: 'Context', name: 'x' }
      ],
      javaCode: `
        AuthService authService = (AuthService) getX().get("auth");
        return authService.check(x, getRejectPermission_());
      `
    }
  ],
});
<|endoftext|>


<commit_before>/*
 * winston.js: Top-level include defining Winston.
 *
 * (C) 2010 Charlie Robbins
 * MIT LICENCE
 *
 */

require.paths.unshift(require('path').join(__dirname));

var winston = exports;

// Expose version
winston.version = [0, 2, 9];

//
// Include transports defined by default by winston
//
winston.transports = require('winston/transports');

//
// function findTransport (transport)
//   Helper method to find existing transport
//
winston.findTransport = function (transport) {
  var name, existing = Object.keys(winston.transports).filter(function (k) {
    return winston.transports[k] === transport;
  });

  return existing.length > 0 ? existing[0].toLowerCase() : null;
};

var utils              = require('winston/utils');
winston.hash           = utils.hash;
winston.clone          = utils.clone;
winston.longestElement = utils.longestElement;
winston.config         = require('winston/config');
winston.addColors      = winston.config.addColors;
winston.Logger         = require('winston/logger').Logger;

//
// We create and expose a "defaultLogger" so that the programmer may do the
// following without the need to create an instance of winston.Logger directly:
//   var winston = require('winston');
//   winston.log('info', 'some message');
//   winston.error('some error');
//
var defaultLogger = new (winston.Logger)({ transports: [new (winston.transports.Console)()] });
utils.setLevels(winston, null, defaultLogger.levels);

['log', 'add', 'remove', 'profile', 'extend', 'cli'].forEach(function (method) {
  winston[method] = function () {
    return defaultLogger[method].apply(defaultLogger, arguments);
  };
});

winston.cli = function (foo, bar) {
  winston.padLevels = true;
  utils.setLevels(winston, defaultLogger.levels, winston.config.cli.levels);
  winston.config.addColors(winston.config.cli.colors);

  if (defaultLogger.transports.console) {
    defaultLogger.transports.console.colorize = true;
    defaultLogger.transports.console.timestamp = false;
  }
};

winston.setLevels = function (levels) {
  utils.setLevels(winston, defaultLogger.levels, levels);
  defaultLogger.setLevels(levels);
};

//
// Define getters / setters for appropriate properties of the
// default logger which need to be exposed by winston.
//
['emitErrs', 'padLevels', 'levelLength'].forEach(function (prop) {
  Object.defineProperty(winston, prop, {
    get: function () {
      return defaultLogger[prop];
    },
    set: function (val) {
      defaultLogger[prop] = val;
    }
  });
});

//
// function defaultTransports ()
//   returns the transports set on the default winston logger
//
winston.defaultTransports = function () {
  return defaultLogger.transports;
};<commit_msg>[fix] Set levels on the defaultLogger when calling `winston.cli()`<commit_after>/*
 * winston.js: Top-level include defining Winston.
 *
 * (C) 2010 Charlie Robbins
 * MIT LICENCE
 *
 */

require.paths.unshift(require('path').join(__dirname));

var winston = exports;

// Expose version
winston.version = [0, 2, 9];

//
// Include transports defined by default by winston
//
winston.transports = require('winston/transports');

//
// function findTransport (transport)
//   Helper method to find existing transport
//
winston.findTransport = function (transport) {
  var name, existing = Object.keys(winston.transports).filter(function (k) {
    return winston.transports[k] === transport;
  });

  return existing.length > 0 ? existing[0].toLowerCase() : null;
};

var utils              = require('winston/utils');
winston.hash           = utils.hash;
winston.clone          = utils.clone;
winston.longestElement = utils.longestElement;
winston.config         = require('winston/config');
winston.addColors      = winston.config.addColors;
winston.Logger         = require('winston/logger').Logger;

//
// We create and expose a "defaultLogger" so that the programmer may do the
// following without the need to create an instance of winston.Logger directly:
//   var winston = require('winston');
//   winston.log('info', 'some message');
//   winston.error('some error');
//
var defaultLogger = new (winston.Logger)({ transports: [new (winston.transports.Console)()] });
utils.setLevels(winston, null, defaultLogger.levels);

['log', 'add', 'remove', 'profile', 'extend', 'cli'].forEach(function (method) {
  winston[method] = function () {
    return defaultLogger[method].apply(defaultLogger, arguments);
  };
});

winston.cli = function (foo, bar) {
  winston.padLevels = true;
  utils.setLevels(winston, defaultLogger.levels, winston.config.cli.levels);
  defaultLogger.setLevels(winston.config.cli.levels);
  winston.config.addColors(winston.config.cli.colors);

  if (defaultLogger.transports.console) {
    defaultLogger.transports.console.colorize = true;
    defaultLogger.transports.console.timestamp = false;
  }
};

winston.setLevels = function (levels) {
  utils.setLevels(winston, defaultLogger.levels, levels);
  defaultLogger.setLevels(levels);
};

//
// Define getters / setters for appropriate properties of the
// default logger which need to be exposed by winston.
//
['emitErrs', 'padLevels', 'levelLength'].forEach(function (prop) {
  Object.defineProperty(winston, prop, {
    get: function () {
      return defaultLogger[prop];
    },
    set: function (val) {
      defaultLogger[prop] = val;
    }
  });
});

//
// function defaultTransports ()
//   returns the transports set on the default winston logger
//
winston.defaultTransports = function () {
  return defaultLogger.transports;
};<|endoftext|>


<commit_before>(function() {
  // Check if we can detect Redmine, if not stop execution.
  if (!$('meta[content="Redmine"]').length) {
    return;
  }

  console.log('Redmine detected, initializing Redmine Plus...');

  // Store things passed in from our custom TqRedminePlusStore API.
  internalStorage = {};

  var originalTimerWrapperSelector = '.time-logger-menu';

  // Listen for TqRedminePlusStore fired from the page with key/value pair data.
  $(document).on('TqRedminePlusStore', function(event) {
    var dataFromPage = event.detail;
    internalStorage[dataFromPage.key] = dataFromPage.value;

    // Trigger additional event for the given key.
    $(document).trigger(dataFromPage.key, [dataFromPage.value]);
  });

  // Inject the api store script directlty in the site instead of the extensions
  // sandboxed environment. We do this to be able to send JS data of the orignial
  // page to this extension script.
  var s = document.createElement('script');
  s.src = chrome.extension.getURL('tq-redmine-plus-injected-api.js');
  (document.head||document.documentElement).appendChild(s);
  s.onload = function() {
    s.parentNode.removeChild(s);
  };

  var $taskboard = $("#taskboard"),
    $backlogsContainer = $("#backlogs_container");

  // Define some global variables if there is a taskboard.
  if ($taskboard.length) {
    var $tasks = $('.task'),
      $stories = $('.story'),
      taskboarChangeTimer,
      $toolbarLinksTimer,
      userId = 0;
  }

  // Enable chosen select for project switcher.
  $("#project_quick_jump_box, #quick-search select").chosen().on('change', function() {
    // Get onchange attribute code and execute it.
    var onchangeCode = $(this).attr('onchange');
    if (onchangeCode.length) {
      eval(onchangeCode);
    }
  });

  var processBacklogs = function($statusElements) {
    // Add status classes to story list items.
    $statusElements.each(function(index, item) {
      var $itemStatus = $(item),
        $model = $itemStatus.closest('.model');

      if (!$model.hasClass('editing')) {
        var status = createCssClass($itemStatus.text());
        $model.attr('class', $itemStatus.data('original-classes')).addClass(status);
      }
    });
  };

  // Enhance the backlog page after a delay.
  var processBacklogsDelayed = function($backlogs) {
    $backlogs.each(function () {
      var $backlog = $(this),
        $header = $backlog.find('.header'),
        $menuItems = $header.find('.menu .items'),
        $addStory = $menuItems.find('.add_new_story');

      if ($backlog.hasClass('tq-processed')) {
        return;
      }

      $backlog.addClass('tq-processed');

      // Add first two menu items as visible links.
      var $firstTwo = $menuItems.find('li').slice(0, 2).detach(),
        $buttonLinks = $firstTwo.find('a');

      if (!$buttonLinks.eq(1).hasClass('add_new_sprint')) {
        $buttonLinks.eq(1).attr('target', '_blank');
      }

      $buttonLinks.insertAfter($header);

      $addStory.on('mouseup', function() {
        $backlog.find('select.tracker_id').val(12);
      });

    });
  };

  var processTaskboardTasks = function() {
    // Get tasks again (on taskboard).
    $tasks = $('.task');

    $tasks.each(function() {
      var $task = $(this),
        $timerButtons = $task.find('.tq-timer-button');

      // Add timer control to tasks.
      if ($timerButtons.length == 0) {
        // Get tasks issue id.
        var issueID = $task.find('.id .v').text();

        // Create timer start/pause toggle.
        var $timerToggle = $('').on('click.tq-timer-toggle', function() {
          // Refresh timer status after a click.
          var $timer = $toolbarLinksTimer.contents().find(originalTimerWrapperSelector),
            $timerIcon = $timer.find('a.icon'),
            timerIsRunning = $timerIcon.hasClass('icon-clock'),
            timerIsPaused = $timerIcon.hasClass('icon-pause');

          if (timerIsPaused) {
            var $startTimer = $timer.find('.icon-start-action');
            $startTimer[0].click();
          }
          else if (timerIsRunning) {
            var $pauseTimer = $timer.find('.icon-pause-action');
            $pauseTimer[0].click();
          }
          else {
            // Change timer to use this issue (point iFrame to the issue page).
            $toolbarLinksTimer.attr('src', '/issues/' + issueID);
            $task.addClass('tq-start-timer');
            // Starting the actual timer will be done on load of the iframe.
          }
        });

        // Create timer stop button on the task in the table.
        // Add a click handler which finds the stop button from the Iframe in the top toolbar and click on it.
        var $timerStop = $('').on('click.tq-timer-stop', function() {
          var $timer = $toolbarLinksTimer.contents().find(originalTimerWrapperSelector),
            $stopTimer = $timer.find('.icon-stop-action');

          $stopTimer[0].click();
          $timerButtons.remove();
        });

        $task.find('.id .t').prepend($timerToggle, $timerStop);
      }

      // Attach our own open task click event, make sure we don't attach multiple handlers.
      $task.off('click.tq-task').on('click.tq-task', function() {
        var storyId = $task.closest('tr').find('.story > .story_tooltip > a').text(),
          $taskEditorDialog = $('.task_editor_dialog');

        // Attach click hander to the task submit buttons, to be able to remove
        // our prefilled comment if nou hours are given.
        // This prevents 0.0 hours time log entries.
        $taskEditorDialog.find('button').off('click.tq-task mousedown.tq-task keydown.tq-task').on('click.tq-task mousedown.tq-task keydown.tq-task', function() {
          if ($('input.time_entry_hours').val().length == 0) {
            $('textarea.time_entry_comments').val('');
          }
        });

        // Pre-fil content
        $taskEditorDialog.find('textarea.time_entry_comments').val('#'+storyId+' ');
        // Set log time user.
        getStorage({
            redmineUserId: '',
            redmineTaskboardTimer: true
          },
          function(items) {
            if (items.redmineTaskboardTimer && userId) {
              $('.time_entry_user_id').val(userId);
            }
            else if (items.redmineUserId.length > 0) {
              $('.time_entry_user_id').val(items.redmineUserId);
            }
          });
      });
    });
  };

  var processTaskboardStories = function() {
    $stories.each(function(){
      var $story = $(this),
        $tooltip = $story.find('.story_tooltip'),
        storyId = $tooltip.find('a').text();

      // Add unique id to every story table row.
      $story.closest('tr').attr('id', 'story_' + storyId);
    });

    // React on when we get info about the tooltips (asynchronous).
    $(document).on('taskboardTooltipData', function(event, taskboardTooltipData) {
      $.each(taskboardTooltipData, function(storyId, tooltipData) {
        var regEx = /Status<\/b>:\s(.*?)</g;
        var match = regEx.exec(tooltipData);

        if (typeof match == 'object' && typeof match[1] !== 'undefined') {
          // Process tooltipData to get the status.
          var statusClass = createCssClass(match[1]);

          // Add class to the matching story row.
          $('#story_' + storyId).attr('class', 'story-swimlane').addClass(statusClass);
        }
      });
    });
  };

  $(document).ready(function() {
    if ($backlogsContainer.length) {
      var $stories = $backlogsContainer.find('.story'),
        $statusElements = $stories.find('.status_id > .t'),
        originalStoryClasses = $stories.eq(0).attr('class');

      $statusElements.data('original-classes', originalStoryClasses);

      processBacklogs($statusElements);

      $stories.on('DOMSubtreeModified.tq-stories', function (e) {
        // Make sure we don't run this multiple times in a row.
        clearTimeout(taskboarChangeTimer);
        taskboarChangeTimer = setTimeout(function() {
          processBacklogs($(e.currentTarget).find('.status_id > .t'));
        }, 200);
      });

      // Processing backlogs needs to wait at least a second to wait,
      // otherwise some DOM elements aren't build.
      // This is an experimental value.
      setTimeout(function(){
        var $backlogs = $backlogsContainer.find('.backlog');
        processBacklogsDelayed($backlogs)
      }, 800);
    }

    // Only enable taskboard enhancement if there is a taskboard.
    if ($taskboard.length) {
      var $storyRows = $taskboard.find('.story-swimlane').add('#impediments tr');

      // Use sticky header.
      getStorage({redmineTaskboardStickyToolbar: true}, function(items) {
        if (items.redmineTaskboardStickyToolbar) {
          $('html').addClass('tq-sticky-toolbar');
        }
      });

      // Initialize timer if configured (or by default).
      getStorage({redmineTaskboardTimer: true}, function(items) {
        if (!items.redmineTaskboardTimer) {
          return;
        }
        // Place timer placeholder.
        $toolbarLinksTimer = $('').prependTo($('#toolbar').find('.links'));

        // Load a page with the timer in an iframe and enhance on load.
        $toolbarLinksTimer.attr('src', '/').on('load', function() {
          // Get iframe content.
          var $issuePage = $(this).contents(),
            $originalTimer = $issuePage.find(originalTimerWrapperSelector),
            $originalStartTimer = $originalTimer.find('.icon-start'),
            modifiedTimer;

          // Store current users id in the global scope.
          userId = $issuePage.find('#loggedas a').attr('href').substring(7);

          // Change / enhance timer links when there are changes.
          // Every minute this is triggered at least once due to timer updates.
          $originalTimer.on("DOMSubtreeModified", function() {
            // Make sure we don't run this multiple times in a row.
            clearTimeout(modifiedTimer);
            modifiedTimer = setTimeout(function() {
              // Get tasks again and remove active classes.
              $tasks = $('.task').removeClass('tq-active-timer tq-paused-timer tq-start-timer hide-timer-toggle');
              // Get timer icon, we can use its Class to detect the timer status.
              var $timerIcon = $originalTimer.find('a.icon'),
                timerIsRunning = $timerIcon.hasClass('icon-clock'),
                timerIsPaused = $timerIcon.hasClass('icon-pause'),
                // Get active issue.
                issueId = $timerIcon.text().substring(1);

              // If active issue is found.
              if (issueId > 0) {
                // Add special class to task block.
                var $timerTask = $('#issue_' + issueId);
                var $timerToggle = $timerTask.find('.tq-timer-toggle');

                // If timer is running.
                if (timerIsRunning) {
                  $timerToggle.addClass('tq-timer-pause').removeClass('tq-timer-start');
                  $timerTask.addClass('tq-active-timer');
                }
                else if (timerIsPaused) {
                  $timerToggle.removeClass('tq-timer-pause').addClass('tq-timer-start');
                  $timerTask.addClass('tq-paused-timer');
                }
                else {
                  $timerToggle.removeClass('tq-timer-pause').addClass('tq-timer-start');
                }

                // Hide other timer toggles.
                $tasks.not('#issue_' + issueId).addClass('hide-timer-toggle');
              }
              else {
                $tasks.find('.tq-timer-toggle').removeClass('tq-timer-pause').addClass('tq-timer-start')
              }

              // Make sure all timer links open in a new tab.
              $originalTimer.find('a').attr('target', '_blank');

              // After clicking start/pause timer in the bar: update tasks.
              $originalTimer.find('.icon-start-action, .icon-pause-action').off('click.tq').on('click.tq', function() {
                $originalTimer.trigger('DOMSubtreeModified');
              });

              // After clicking stop timer in the bar: reset this iframe and
              // open the stop page in new tab.
              $originalTimer.find('.icon-stop-action').off('click.tq').on('click.tq', function() {
                $toolbarLinksTimer.attr('src', '/');
              });
            }, 200);
          }).trigger('DOMSubtreeModified');

          // Move the timer to the top of the page in the iFrame.
          $originalTimer.detach().prependTo($issuePage.find('body'));

          // Change the loaded page styling and hide not needed elements.
          // .css() doesn't work for some reason.
          $issuePage.find('html').attr('style', 'overflow:hidden;');
          $issuePage.find('body').attr('style', 'background:transparent;');
          $issuePage.find('#wrapper').hide();

          // Auto start timer on load.
          if ($originalStartTimer.length) {
            $originalStartTimer[0].click();
          }
        });
      });

      // Allow creating new tasks by clicking in a cell in the 'New' column.
      $storyRows.each(function() {
        var $storyRow = $(this);

        $storyRow.find('> td').eq(1).click(function(e) {
          // Do not trigger 'add new task' when clicking on an existing task.
          if ($(e.target).closest('.task').length === 0) {
            $storyRow.find('.add_new').click();
          }
        });
      });

      // Reinitialize the taskboard tasks when something changes.
      $taskboard.on("DOMSubtreeModified.tq-taskboard", function() {
        // Make sure we don't run this multiple times in a row.
        clearTimeout(taskboarChangeTimer);
        taskboarChangeTimer = setTimeout(function() {
          processTaskboardTasks();
        }, 200);
      });

      // Trigger once on load.
      processTaskboardTasks();
      processTaskboardStories();
    }
  });

  // Get story ID (parent issue on full issue view).
  var storyId = $('#issue_parent_issue_id').val();

  // Show more 'Change properties'.
  getStorage({
      redmineShowAllProperties: true
    },
    function(items) {
      if (items.redmineShowAllProperties) {
        $('#issue-form').find('#issue_descr_fields').show();
      }
    }
  );

  // Prefill time entry (on full issue view)
  if (deeptest('storyId') && storyId.length > 0) {
    $('#time_entry_comments').val('#'+storyId+' ');
  }

  // Attach click hander to the issue update submit buttons, to be able to remove
  // our prefilled comment if nou hours are given.
  $('#issue-form').on('submit', function() {
    if ($('#time_entry_hours').val().length == 0) {
      $('#time_entry_comments').val('');
    }
  });
})();
<commit_msg>Fix initialization of plugin on the backlog<commit_after>(function() {
  // Check if we can detect Redmine, if not stop execution.
  if (!$('meta[content="Redmine"]').length && !$('meta[content="Redmine Backlogs"]').length) {
    return;
  }

  console.log('Redmine detected, initializing Redmine Plus...');

  // Store things passed in from our custom TqRedminePlusStore API.
  internalStorage = {};

  var originalTimerWrapperSelector = '.time-logger-menu';

  // Listen for TqRedminePlusStore fired from the page with key/value pair data.
  $(document).on('TqRedminePlusStore', function(event) {
    var dataFromPage = event.detail;
    internalStorage[dataFromPage.key] = dataFromPage.value;

    // Trigger additional event for the given key.
    $(document).trigger(dataFromPage.key, [dataFromPage.value]);
  });

  // Inject the api store script directlty in the site instead of the extensions
  // sandboxed environment. We do this to be able to send JS data of the orignial
  // page to this extension script.
  var s = document.createElement('script');
  s.src = chrome.extension.getURL('tq-redmine-plus-injected-api.js');
  (document.head||document.documentElement).appendChild(s);
  s.onload = function() {
    s.parentNode.removeChild(s);
  };

  var $taskboard = $("#taskboard"),
    $backlogsContainer = $("#backlogs_container");

  // Define some global variables if there is a taskboard.
  if ($taskboard.length) {
    var $tasks = $('.task'),
      $stories = $('.story'),
      taskboarChangeTimer,
      $toolbarLinksTimer,
      userId = 0;
  }

  // Enable chosen select for project switcher.
  $("#project_quick_jump_box, #quick-search select").chosen().on('change', function() {
    // Get onchange attribute code and execute it.
    var onchangeCode = $(this).attr('onchange');
    if (onchangeCode.length) {
      eval(onchangeCode);
    }
  });

  var processBacklogs = function($statusElements) {
    // Add status classes to story list items.
    $statusElements.each(function(index, item) {
      var $itemStatus = $(item),
        $model = $itemStatus.closest('.model');

      if (!$model.hasClass('editing')) {
        var status = createCssClass($itemStatus.text());
        $model.attr('class', $itemStatus.data('original-classes')).addClass(status);
      }
    });
  };

  // Enhance the backlog page after a delay.
  var processBacklogsDelayed = function($backlogs) {
    $backlogs.each(function () {
      var $backlog = $(this),
        $header = $backlog.find('.header'),
        $menuItems = $header.find('.menu .items'),
        $addStory = $menuItems.find('.add_new_story');

      if ($backlog.hasClass('tq-processed')) {
        return;
      }

      $backlog.addClass('tq-processed');

      // Add first two menu items as visible links.
      var $firstTwo = $menuItems.find('li').slice(0, 2).detach(),
        $buttonLinks = $firstTwo.find('a');

      if (!$buttonLinks.eq(1).hasClass('add_new_sprint')) {
        $buttonLinks.eq(1).attr('target', '_blank');
      }

      $buttonLinks.insertAfter($header);

      $addStory.on('mouseup', function() {
        $backlog.find('select.tracker_id').val(12);
      });

    });
  };

  var processTaskboardTasks = function() {
    // Get tasks again (on taskboard).
    $tasks = $('.task');

    $tasks.each(function() {
      var $task = $(this),
        $timerButtons = $task.find('.tq-timer-button');

      // Add timer control to tasks.
      if ($timerButtons.length == 0) {
        // Get tasks issue id.
        var issueID = $task.find('.id .v').text();

        // Create timer start/pause toggle.
        var $timerToggle = $('').on('click.tq-timer-toggle', function() {
          // Refresh timer status after a click.
          var $timer = $toolbarLinksTimer.contents().find(originalTimerWrapperSelector),
            $timerIcon = $timer.find('a.icon'),
            timerIsRunning = $timerIcon.hasClass('icon-clock'),
            timerIsPaused = $timerIcon.hasClass('icon-pause');

          if (timerIsPaused) {
            var $startTimer = $timer.find('.icon-start-action');
            $startTimer[0].click();
          }
          else if (timerIsRunning) {
            var $pauseTimer = $timer.find('.icon-pause-action');
            $pauseTimer[0].click();
          }
          else {
            // Change timer to use this issue (point iFrame to the issue page).
            $toolbarLinksTimer.attr('src', '/issues/' + issueID);
            $task.addClass('tq-start-timer');
            // Starting the actual timer will be done on load of the iframe.
          }
        });

        // Create timer stop button on the task in the table.
        // Add a click handler which finds the stop button from the Iframe in the top toolbar and click on it.
        var $timerStop = $('').on('click.tq-timer-stop', function() {
          var $timer = $toolbarLinksTimer.contents().find(originalTimerWrapperSelector),
            $stopTimer = $timer.find('.icon-stop-action');

          $stopTimer[0].click();
          $timerButtons.remove();
        });

        $task.find('.id .t').prepend($timerToggle, $timerStop);
      }

      // Attach our own open task click event, make sure we don't attach multiple handlers.
      $task.off('click.tq-task').on('click.tq-task', function() {
        var storyId = $task.closest('tr').find('.story > .story_tooltip > a').text(),
          $taskEditorDialog = $('.task_editor_dialog');

        // Attach click hander to the task submit buttons, to be able to remove
        // our prefilled comment if nou hours are given.
        // This prevents 0.0 hours time log entries.
        $taskEditorDialog.find('button').off('click.tq-task mousedown.tq-task keydown.tq-task').on('click.tq-task mousedown.tq-task keydown.tq-task', function() {
          if ($('input.time_entry_hours').val().length == 0) {
            $('textarea.time_entry_comments').val('');
          }
        });

        // Pre-fil content
        $taskEditorDialog.find('textarea.time_entry_comments').val('#'+storyId+' ');
        // Set log time user.
        getStorage({
            redmineUserId: '',
            redmineTaskboardTimer: true
          },
          function(items) {
            if (items.redmineTaskboardTimer && userId) {
              $('.time_entry_user_id').val(userId);
            }
            else if (items.redmineUserId.length > 0) {
              $('.time_entry_user_id').val(items.redmineUserId);
            }
          });
      });
    });
  };

  var processTaskboardStories = function() {
    $stories.each(function(){
      var $story = $(this),
        $tooltip = $story.find('.story_tooltip'),
        storyId = $tooltip.find('a').text();

      // Add unique id to every story table row.
      $story.closest('tr').attr('id', 'story_' + storyId);
    });

    // React on when we get info about the tooltips (asynchronous).
    $(document).on('taskboardTooltipData', function(event, taskboardTooltipData) {
      $.each(taskboardTooltipData, function(storyId, tooltipData) {
        var regEx = /Status<\/b>:\s(.*?)</g;
        var match = regEx.exec(tooltipData);

        if (typeof match == 'object' && typeof match[1] !== 'undefined') {
          // Process tooltipData to get the status.
          var statusClass = createCssClass(match[1]);

          // Add class to the matching story row.
          $('#story_' + storyId).attr('class', 'story-swimlane').addClass(statusClass);
        }
      });
    });
  };

  $(document).ready(function() {
    if ($backlogsContainer.length) {
      var $stories = $backlogsContainer.find('.story'),
        $statusElements = $stories.find('.status_id > .t'),
        originalStoryClasses = $stories.eq(0).attr('class');

      $statusElements.data('original-classes', originalStoryClasses);

      processBacklogs($statusElements);

      $stories.on('DOMSubtreeModified.tq-stories', function (e) {
        // Make sure we don't run this multiple times in a row.
        clearTimeout(taskboarChangeTimer);
        taskboarChangeTimer = setTimeout(function() {
          processBacklogs($(e.currentTarget).find('.status_id > .t'));
        }, 200);
      });

      // Processing backlogs needs to wait at least a second to wait,
      // otherwise some DOM elements aren't build.
      // This is an experimental value.
      setTimeout(function(){
        var $backlogs = $backlogsContainer.find('.backlog');
        processBacklogsDelayed($backlogs)
      }, 800);
    }

    // Only enable taskboard enhancement if there is a taskboard.
    if ($taskboard.length) {
      var $storyRows = $taskboard.find('.story-swimlane').add('#impediments tr');

      // Use sticky header.
      getStorage({redmineTaskboardStickyToolbar: true}, function(items) {
        if (items.redmineTaskboardStickyToolbar) {
          $('html').addClass('tq-sticky-toolbar');
        }
      });

      // Initialize timer if configured (or by default).
      getStorage({redmineTaskboardTimer: true}, function(items) {
        if (!items.redmineTaskboardTimer) {
          return;
        }
        // Place timer placeholder.
        $toolbarLinksTimer = $('').prependTo($('#toolbar').find('.links'));

        // Load a page with the timer in an iframe and enhance on load.
        $toolbarLinksTimer.attr('src', '/').on('load', function() {
          // Get iframe content.
          var $issuePage = $(this).contents(),
            $originalTimer = $issuePage.find(originalTimerWrapperSelector),
            $originalStartTimer = $originalTimer.find('.icon-start'),
            modifiedTimer;

          // Store current users id in the global scope.
          userId = $issuePage.find('#loggedas a').attr('href').substring(7);

          // Change / enhance timer links when there are changes.
          // Every minute this is triggered at least once due to timer updates.
          $originalTimer.on("DOMSubtreeModified", function() {
            // Make sure we don't run this multiple times in a row.
            clearTimeout(modifiedTimer);
            modifiedTimer = setTimeout(function() {
              // Get tasks again and remove active classes.
              $tasks = $('.task').removeClass('tq-active-timer tq-paused-timer tq-start-timer hide-timer-toggle');
              // Get timer icon, we can use its Class to detect the timer status.
              var $timerIcon = $originalTimer.find('a.icon'),
                timerIsRunning = $timerIcon.hasClass('icon-clock'),
                timerIsPaused = $timerIcon.hasClass('icon-pause'),
                // Get active issue.
                issueId = $timerIcon.text().substring(1);

              // If active issue is found.
              if (issueId > 0) {
                // Add special class to task block.
                var $timerTask = $('#issue_' + issueId);
                var $timerToggle = $timerTask.find('.tq-timer-toggle');

                // If timer is running.
                if (timerIsRunning) {
                  $timerToggle.addClass('tq-timer-pause').removeClass('tq-timer-start');
                  $timerTask.addClass('tq-active-timer');
                }
                else if (timerIsPaused) {
                  $timerToggle.removeClass('tq-timer-pause').addClass('tq-timer-start');
                  $timerTask.addClass('tq-paused-timer');
                }
                else {
                  $timerToggle.removeClass('tq-timer-pause').addClass('tq-timer-start');
                }

                // Hide other timer toggles.
                $tasks.not('#issue_' + issueId).addClass('hide-timer-toggle');
              }
              else {
                $tasks.find('.tq-timer-toggle').removeClass('tq-timer-pause').addClass('tq-timer-start')
              }

              // Make sure all timer links open in a new tab.
              $originalTimer.find('a').attr('target', '_blank');

              // After clicking start/pause timer in the bar: update tasks.
              $originalTimer.find('.icon-start-action, .icon-pause-action').off('click.tq').on('click.tq', function() {
                $originalTimer.trigger('DOMSubtreeModified');
              });

              // After clicking stop timer in the bar: reset this iframe and
              // open the stop page in new tab.
              $originalTimer.find('.icon-stop-action').off('click.tq').on('click.tq', function() {
                $toolbarLinksTimer.attr('src', '/');
              });
            }, 200);
          }).trigger('DOMSubtreeModified');

          // Move the timer to the top of the page in the iFrame.
          $originalTimer.detach().prependTo($issuePage.find('body'));

          // Change the loaded page styling and hide not needed elements.
          // .css() doesn't work for some reason.
          $issuePage.find('html').attr('style', 'overflow:hidden;');
          $issuePage.find('body').attr('style', 'background:transparent;');
          $issuePage.find('#wrapper').hide();

          // Auto start timer on load.
          if ($originalStartTimer.length) {
            $originalStartTimer[0].click();
          }
        });
      });

      // Allow creating new tasks by clicking in a cell in the 'New' column.
      $storyRows.each(function() {
        var $storyRow = $(this);

        $storyRow.find('> td').eq(1).click(function(e) {
          // Do not trigger 'add new task' when clicking on an existing task.
          if ($(e.target).closest('.task').length === 0) {
            $storyRow.find('.add_new').click();
          }
        });
      });

      // Reinitialize the taskboard tasks when something changes.
      $taskboard.on("DOMSubtreeModified.tq-taskboard", function() {
        // Make sure we don't run this multiple times in a row.
        clearTimeout(taskboarChangeTimer);
        taskboarChangeTimer = setTimeout(function() {
          processTaskboardTasks();
        }, 200);
      });

      // Trigger once on load.
      processTaskboardTasks();
      processTaskboardStories();
    }
  });

  // Get story ID (parent issue on full issue view).
  var storyId = $('#issue_parent_issue_id').val();

  // Show more 'Change properties'.
  getStorage({
      redmineShowAllProperties: true
    },
    function(items) {
      if (items.redmineShowAllProperties) {
        $('#issue-form').find('#issue_descr_fields').show();
      }
    }
  );

  // Prefill time entry (on full issue view)
  if (deeptest('storyId') && storyId.length > 0) {
    $('#time_entry_comments').val('#'+storyId+' ');
  }

  // Attach click hander to the issue update submit buttons, to be able to remove
  // our prefilled comment if nou hours are given.
  $('#issue-form').on('submit', function() {
    if ($('#time_entry_hours').val().length == 0) {
      $('#time_entry_comments').val('');
    }
  });
})();
<|endoftext|>