<commit_before>/**
 * # Waiting Room settings
 * Copyright(c) {YEAR} {AUTHOR} <{AUTHOR_EMAIL}>
 * MIT Licensed
 *
 * Waiting Room settings.
 *
 * http://nodegame.org
 * ---
 */
module.exports = {

    /**
     * ## EXECUTION_MODE (string)
     *
     * Sets the execution mode of the waiting room
     *
     * Different modes might have different default values, and need
     * different settintgs.
     *
     * Available modes:
     *
     *   - TIMEOUT, waits until the time is up, then it checks
     *        whether enough players are connected to start the game.
     *   - WAIT_FOR_N_PLAYERS, the game starts right away as soon as
     *        the desired number of connected players is reached.
     */
    EXECUTION_MODE: 'WAIT_FOR_N_PLAYERS',

    /**
     * ## GROUP_SIZE (number)
     *
     * The size of each group dispatched by the waiting room
     */
    GROUP_SIZE: 2,

    /**
     * ## POOL_SIZE (number) Optional
     *
     * If set, waits until POOL_SIZE clients are connected to dispatch a group
     *
     * Must be >= GROUP_SIZE.
     *
     * Default: GROUP_SIZE
     */
    // POOL_SIZE: 2,

    /**
     * ## N_GAMES (number) Optional
     *
     * Number of games to dispatch
     *
     * If set, waiting room will be closed after N_GAMES have been dispatched
     *
     * Default: undefined, no limit.
     */
    // N_GAMES: 1,

    /**
     * ## MAX_WAIT_TIME (number) Optional
     *
     * Maximum waiting time in milliseconds in the waiting room
     *
     * After the max waiting time expired clients are disconnected
     */
    MAX_WAIT_TIME: 90000,

    /**
     * ## START_DATE (string|object) Optional
     *
     * Time and date of the start of the game.
     *
     * Overrides `MAX_WAIT_TIME`.
     *
     * Accepted values: any valid argument to `Date` constructor.
     */
    // START_DATE: 'December 31, {YEAR} 12:00:00',
    // START_DATE: new Date().getTime() + 30000,

    /**
     * ## CHOSEN_TREATMENT (string|function)
     *
     * The treatment assigned to every new group
     *
     * Accepted values:
     *
     *   - "treatment_rotate": rotates the treatments.
     *   - undefined: a random treatment will be selected.
     *   - function: a callback returning the name of the treatment. E.g:
     *
     *       function(treatments, roomCounter) {
     *           return treatments[roomCounter % treatments.length];
     *       }
     *
     * Default: undefined, random treatment
     */
    CHOSEN_TREATMENT: function(treatments, roomCounter) {
        return treatments[roomCounter % treatments.length];
    },

    /**
     * ## PLAYER_SORTING
     *
     * Sorts the order of players before dispatching them
     *
     * Sorting takes place only if:
     *
     *  - the number of connected players > GROUP_SIZE,
     *  - PLAYER_GROUPING is undefined
     *
     * Accepted values:
     *
     *   - 'timesNotSelected': (default) gives priority to players that
     *        have not been selected by a previous call to dispatch
     *   - undefined: rollback to default choice
     *   - null: no sorting (players are anyway randomly shuffled).
     *   - function: a comparator function implementing a criteria
     *       for sorting two objects. E.g:
     *
     *        function timesNotSelected(a, b) {
     *            if ((a.timesNotSelected || 0) < b.timesNotSelected) {
     *                return -1;
     *            }
     *            else if ((a.timesNotSelected || 0) > b.timesNotSelected) {
     *                return 1;
     *            }
     *            return 0;
     *        }
     */
    // PLAYER_SORTING: 'timesNotSelected'

    /**
     * ## PLAYER_GROUPING
     *
     * Creates groups of players to be assigned to treatments
     *
     * This method is alternative to "sorting" and will be invoked only
     * if the number of connected players > GROUP_SIZE
     *
     * @param {PlayerList} pList The list of players to group
     * @param {number} nGroups The number of groups requested by current
     *   dispatch
     *
     * @return {array} An array of nGroups arrays of player objects
     */
    // PLAYER_GROUPING: function(pList, nGroups) {
    //     return [ [ pl1, pl2 ] [ pl3, pl4 ] ];
    // }

    /**
     * ## ON_TIMEOUT (function) Optional
     *
     * A callback function to be executed on the client when wait time expires
     */
    // ON_TIMEOUT: function() {
    //    console.log('I am timed out!');
    // },

    /**
     * ## ON_TIMEOUT_SERVER (function) Optional
     *
     * A callback function to be executed on the server when wait time expires
     *
     * The context of execution is WaitingRoom.
     */
    // ON_TIMEOUT_SERVER: function(code) {
    //    console.log('*** I am timed out! ', code.id);
    // }

    /**
     * ## ON_OPEN (function) Optional
     *
     * Callback to be executed when the waiting room becomes "open"
     *
     * Receives as first parameter the waiting room object itself.
     */
    // this.ON_OPEN = null;

    /**
     * ### WaitingRoom.ON_CLOSE (function) Optional
     *
     * Callback to be executed when the waiting room becomes "close"
     *
     * Receives as first parameter the waiting room object itself.
     */
    // this.ON_CLOSE = null;

    /**
     * ## ON_CONNECT (function) Optional
     *
     * Callback to be executed when a player connects
     *
     * Receives as first parameter the waiting room object itself.
     */
    // this.ON_CONNECT = null;

    /**
     * ## ON_DISCONNECT (function) Optional
     *
     * Callback to be executed when a player disconnects
     *
     * Receives as first parameter the waiting room object itself.
     */
    // this.ON_DISCONNECT = null;

    /**
     * ## ON_INIT (function) Optional
     *
     * Callback to be executed after the settings have been parsed
     *
     * Receives as first parameter the waiting room object itself.
     */
    // this.ON_INIT = null;

    /**
     * ## ON_DISPATCH (function) Optional
     *
     * Callback to be executed just before starting dispatching
     *
     * Receives as first parameter the waiting room object itself,
     * and the options of the dispatch call as second parameter.
     */
    // this.ON_DISPATCH = null;

    /**
     * ## ON_DISPATCHED (function) Optional
     *
     * Callback to be executed at the end of a dispatch call
     *
     * Receives as first parameter the waiting room object itself,
     * and the options of the dispatch call as second parameter.
     */
    // this.ON_DISPATCHED = null;

    /**
     * ## ON_FAILED_DISPATCH (function) Optional
     *
     * Callback to be executed if a dispatch attempt failed
     *
     * Receives as first parameter the waiting room object itself,
     * the options of the dispatch call as second parameter, and
     * optionally the error message as third parameter.
     */
    // this.ON_FAILED_DISPATCH = null;

    /**
     * ## DISPATCH_TO_SAME_ROOM (boolean) Optional
     *
     * If TRUE, every new group will be added to the same game room
     *
     * A new game room will be created for the first dispatch, and
     * reused for all successive groups. Default, FALSE.
     *
     * Notice: the game must support adding players while it is running.
     *
     * Default: FALSE
     *
     * @see WaitingRoom.lastGameRoom
     */
    // DISPATCH_TO_SAME_ROOM: true

    /**
     * ## PING_BEFORE_DISPATCH (boolean) Optional
     *
     * If TRUE, all players are pinged before a dispatch
     *
     * Non-responding clients are disconnected.
     *
     * If only one player is needed and mode is 'WAIT_FOR_N_PLAYERS',
     * pinging is skipped.
     *
     * Default: TRUE
     *
     * @see WaitingRoom.dispatch
     */
    // PING_BEFORE_DISPATCH: true,

    /**
     * ## PING_MAX_REPLY_TIME (number > 0) Optional
     *
     * The number of milliseconds to wait for a reply from a PING
     *
     * Default: 3000
     *
     * @see PING_BEFORE_DISPATCH
     */
    // PING_MAX_REPLY_TIME: 3000,

    /**
     * ## PING_DISPATCH_ANYWAY (boolean) Optional
     *
     * If TRUE, dispatch continues even if disconnections occur during PING
     *
     * Default: FALSE
     *
     * @see PING_BEFORE_DISPATCH
     * @see PING_MAX_REPLY_TIME
     */
    // PING_DISPATCH_ANYWAY: false,

    /**
     * ## logicPath (string) Optional
     *
     * If set, a custom implementation of the wait room will be used
     *
     * @see wait.room.js (nodegame-server)
     */
    // logicPath: 'path/to/a/wait.room.js',

    /**
     * ## DISCONNECT_IF_NOT_SELECTED (boolean) Optional
     *
     * Disconnect a client if not selected for a game when dispatching
     *
     * Default: false
     */
    // DISCONNECT_IF_NOT_SELECTED: true,

    /**
     * ## PAGE_TITLE (object) Optional
     *
     * Sets the page title, optionally adds to page
     *
     * An object containing the title, and a flag if the same text should
     * be added in a H1 element at the top of the page.
     *
     * Default: { title: 'Welcome!', addToBody: true }
     */
    // PAGE_TITLE: { title: 'Welcome!', addToBody: true },

    /** ### TEXTS
     *
     * Collections of texts displayed when given events occurs
     *
     * Each item can be a string, or function returning a string; the function
     * receives two input parameters: the instance of the widget and an object
     * with extra information (depending on the event).
     *
     * @see WaitingRoom.texts
     */
    TEXTS: {

        /**
         * #### blinkTitle
         *
         * Blinks the title of the tab to signal the beginning of the game
         *
         * False will not blink.
         */
        // blinkTitle: 'Custom string: Game Starts!',

        /**
         * #### disconnect
         *
         * Disconnected from waiting room
         */
        // disconnect: 'Custom string: YOU HAVE BEEN DISCONNECTED!',

        /**
         * #### waitedTooLong
         *
         * The MAX_WAIT_TIME countdown expired
         */
        // waitedTooLong: 'Custom string: YOU WAITED TOO LONG!',

        /**
         * #### notEnoughPlayers
         *
         * There are not enough players to start a game
         */
        // notEnoughPlayers: 'Custom string: NOT ENOUGH PLAYERS!',

        /**
         * #### roomClosed
         *
         * A player tries to connect, but the waiting room has been closed
         */
        // roomClosed: 'Custom string: ROOM CLOSED! CANNOT ENTER!',

        /**
         * #### tooManyPlayers
         *
         * Currently there are more players than needed by the game
         */
        // tooManyPlayers: 'Custom string: TOO MANY PLAYERS!',

        /**
         * #### notSelectedClosed
         *
         * Player has not been selected, and cannot participate in other games
         */
        // notSelectedClosed: 'Custom string: NOT SELECTED CLOSED!',

        /**
         * #### notSelectedOpen
         *
         * Player has not been selected, but can still participate in new games
         */
        // notSelectedOpen: 'Custom string: NOT SELECTED OPEN!',

        /**
         * #### exitCode
         *
         * Player disconnected, and an exit code might have been provided
         */
        // exitCode: 'Custom string: EXIT CODE TEXT',
    },

    /** ### SOUNDS
     *
     * Collections of sounds played when given events occurs
     *
     * Each item can be a string, or function returning a string; the function
     * receives two input parameters: the instance of the widget and an object
     * with extra information (depending on the event).
     *
     * @see WaitingRoom.sounds
     */
    SOUNDS: {

        /**
         * ## dispatch
         *
         * Notifies players that a game is about to be dispatched
         *
         * If TRUE, plays default sound, if string plays the file sound
         * located at the specified uri.
         */
        // dispatch: false
    },

    /** ### ALLOW_PLAY_WITH_BOTS
     *
     * Allows a player to request to start the game immediately with bots
     *
     * A button is added to the interface.
     */
    // ALLOW_PLAY_WITH_BOTS: true,

    /** ### ALLOW_SELECT_TREATMENT
     *
     * Allows a player to select the treatment for the game
     *
     * This option requires `ALLOW_PLAY_WITH_BOTS` to be TRUE.
     *
     * A button is added to the interface.
     */
    // ALLOW_SELECT_TREATMENT: true

};
<commit_msg>waitroom-settings<commit_after>/**
 * # Waiting Room settings
 * Copyright(c) {YEAR} {AUTHOR} <{AUTHOR_EMAIL}>
 * MIT Licensed
 *
 * Waiting Room settings.
 *
 * http://nodegame.org
 * ---
 */
module.exports = {

    /**
     * ## EXECUTION_MODE (string)
     *
     * Sets the execution mode of the waiting room
     *
     * Different modes might have different default values, and need
     * different settintgs.
     *
     * Available modes:
     *
     *   - TIMEOUT, waits until the time is up, then it checks
     *        whether enough players are connected to start the game.
     *   - WAIT_FOR_N_PLAYERS, the game starts right away as soon as
     *        the desired number of connected players is reached.
     */
    EXECUTION_MODE: 'WAIT_FOR_N_PLAYERS',

    /**
     * ## GROUP_SIZE (number)
     *
     * The size of each group dispatched by the waiting room
     */
    GROUP_SIZE: 2,

    /**
     * ## POOL_SIZE (number) Optional
     *
     * If set, waits until POOL_SIZE clients are connected to dispatch a group
     *
     * Must be >= GROUP_SIZE.
     *
     * Default: GROUP_SIZE
     */
    // POOL_SIZE: 2,

    /**
     * ## N_GAMES (number) Optional
     *
     * Number of games to dispatch
     *
     * If set, waiting room will be closed after N_GAMES have been dispatched
     *
     * Default: undefined, no limit.
     */
    // N_GAMES: 1,

    /**
     * ## MAX_WAIT_TIME (number) Optional
     *
     * Maximum waiting time in milliseconds in the waiting room
     *
     * After the max waiting time expired clients are disconnected
     */
    MAX_WAIT_TIME: 90000,

    /**
     * ## START_DATE (string|object) Optional
     *
     * Time and date of the start of the game.
     *
     * Overrides `MAX_WAIT_TIME`.
     *
     * Accepted values: any valid argument to `Date` constructor.
     */
    // START_DATE: 'December 31, {YEAR} 12:00:00',
    // START_DATE: new Date().getTime() + 30000,

    /**
     * ## CHOSEN_TREATMENT (string|function)
     *
     * The treatment assigned to every new group
     *
     * Accepted values:
     *
     *   - "treatment_rotate": rotates the treatments.
     *   - undefined: a random treatment will be selected.
     *   - function: a callback returning the name of the treatment. E.g:
     *
     *       function(treatments, roomCounter) {
     *           return treatments[roomCounter % treatments.length];
     *       }
     *
     * Default: undefined, random treatment
     */
    CHOSEN_TREATMENT: function(treatments, roomCounter) {
        return treatments[roomCounter % treatments.length];
    },

    /**
     * ## PLAYER_SORTING
     *
     * Sorts the order of players before dispatching them
     *
     * Sorting takes place only if:
     *
     *  - the number of connected players > GROUP_SIZE,
     *  - PLAYER_GROUPING is undefined
     *
     * Accepted values:
     *
     *   - 'timesNotSelected': (default) gives priority to players that
     *        have not been selected by a previous call to dispatch
     *   - undefined: rollback to default choice
     *   - null: no sorting (players are anyway randomly shuffled).
     *   - function: a comparator function implementing a criteria
     *       for sorting two objects. E.g:
     *
     *        function timesNotSelected(a, b) {
     *            if ((a.timesNotSelected || 0) < b.timesNotSelected) {
     *                return -1;
     *            }
     *            else if ((a.timesNotSelected || 0) > b.timesNotSelected) {
     *                return 1;
     *            }
     *            return 0;
     *        }
     */
    // PLAYER_SORTING: 'timesNotSelected'

    /**
     * ## PLAYER_GROUPING
     *
     * Creates groups of players to be assigned to treatments
     *
     * This method is alternative to "sorting" and will be invoked only
     * if the number of connected players > GROUP_SIZE
     *
     * @param {PlayerList} pList The list of players to group
     * @param {number} nGroups The number of groups requested by current
     *   dispatch
     *
     * @return {array} An array of nGroups arrays of player objects
     */
    // PLAYER_GROUPING: function(pList, nGroups) {
    //     return [ [ pl1, pl2 ] [ pl3, pl4 ] ];
    // }

    /**
     * ## ON_TIMEOUT (function) Optional
     *
     * A callback function to be executed on the client when wait time expires
     */
    // ON_TIMEOUT: function() {
    //    console.log('I am timed out!');
    // },

    /**
     * ## ON_TIMEOUT_SERVER (function) Optional
     *
     * A callback function to be executed on the server when wait time expires
     *
     * The context of execution is WaitingRoom.
     */
    // ON_TIMEOUT_SERVER: function(code) {
    //    console.log('*** I am timed out! ', code.id);
    // }

    /**
     * ## ON_OPEN (function) Optional
     *
     * Callback to be executed when the waiting room becomes "open"
     *
     * Receives as first parameter the waiting room object itself.
     */
    // ON_OPEN = null;

    /**
     * ### WaitingRoom.ON_CLOSE (function) Optional
     *
     * Callback to be executed when the waiting room becomes "close"
     *
     * Receives as first parameter the waiting room object itself.
     */
    // ON_CLOSE = null;

    /**
     * ## ON_CONNECT (function) Optional
     *
     * Callback to be executed when a player connects
     *
     * Receives as first parameter the waiting room object itself.
     */
    // ON_CONNECT = null;

    /**
     * ## ON_DISCONNECT (function) Optional
     *
     * Callback to be executed when a player disconnects
     *
     * Receives as first parameter the waiting room object itself.
     */
    // ON_DISCONNECT = null;

    /**
     * ## ON_INIT (function) Optional
     *
     * Callback to be executed after the settings have been parsed
     *
     * Receives as first parameter the waiting room object itself.
     */
    // ON_INIT = null;

    /**
     * ## ON_DISPATCH (function) Optional
     *
     * Callback to be executed just before starting dispatching
     *
     * Receives as first parameter the waiting room object itself,
     * and the options of the dispatch call as second parameter.
     */
    // ON_DISPATCH = null;

    /**
     * ## ON_DISPATCHED (function) Optional
     *
     * Callback to be executed at the end of a dispatch call
     *
     * Receives as first parameter the waiting room object itself,
     * and the options of the dispatch call as second parameter.
     */
    // ON_DISPATCHED = null;

    /**
     * ## ON_FAILED_DISPATCH (function) Optional
     *
     * Callback to be executed if a dispatch attempt failed
     *
     * Receives as first parameter the waiting room object itself,
     * the options of the dispatch call as second parameter, and
     * optionally the error message as third parameter.
     */
    // ON_FAILED_DISPATCH = null;

    /**
     * ## DISPATCH_TO_SAME_ROOM (boolean) Optional
     *
     * If TRUE, every new group will be added to the same game room
     *
     * A new game room will be created for the first dispatch, and
     * reused for all successive groups. Default, FALSE.
     *
     * Notice: the game must support adding players while it is running.
     *
     * Default: FALSE
     *
     * @see WaitingRoom.lastGameRoom
     */
    // DISPATCH_TO_SAME_ROOM: true

    /**
     * ## PING_BEFORE_DISPATCH (boolean) Optional
     *
     * If TRUE, all players are pinged before a dispatch
     *
     * Non-responding clients are disconnected.
     *
     * If only one player is needed and mode is 'WAIT_FOR_N_PLAYERS',
     * pinging is skipped.
     *
     * Default: TRUE
     *
     * @see WaitingRoom.dispatch
     */
    // PING_BEFORE_DISPATCH: true,

    /**
     * ## PING_MAX_REPLY_TIME (number > 0) Optional
     *
     * The number of milliseconds to wait for a reply from a PING
     *
     * Default: 3000
     *
     * @see PING_BEFORE_DISPATCH
     */
    // PING_MAX_REPLY_TIME: 3000,

    /**
     * ## PING_DISPATCH_ANYWAY (boolean) Optional
     *
     * If TRUE, dispatch continues even if disconnections occur during PING
     *
     * Default: FALSE
     *
     * @see PING_BEFORE_DISPATCH
     * @see PING_MAX_REPLY_TIME
     */
    // PING_DISPATCH_ANYWAY: false,

    /**
     * ## logicPath (string) Optional
     *
     * If set, a custom implementation of the wait room will be used
     *
     * @see wait.room.js (nodegame-server)
     */
    // logicPath: 'path/to/a/wait.room.js',

    /**
     * ## DISCONNECT_IF_NOT_SELECTED (boolean) Optional
     *
     * Disconnect a client if not selected for a game when dispatching
     *
     * Default: false
     */
    // DISCONNECT_IF_NOT_SELECTED: true,

    /**
     * ## PAGE_TITLE (object) Optional
     *
     * Sets the page title, optionally adds to page
     *
     * An object containing the title, and a flag if the same text should
     * be added in a H1 element at the top of the page.
     *
     * Default: { title: 'Welcome!', addToBody: true }
     */
    // PAGE_TITLE: { title: 'Welcome!', addToBody: true },

    /** ### TEXTS
     *
     * Collections of texts displayed when given events occurs
     *
     * Each item can be a string, or function returning a string; the function
     * receives two input parameters: the instance of the widget and an object
     * with extra information (depending on the event).
     *
     * @see WaitingRoom.texts
     */
    TEXTS: {

        /**
         * #### blinkTitle
         *
         * Blinks the title of the tab to signal the beginning of the game
         *
         * False will not blink.
         */
        // blinkTitle: 'Custom string: Game Starts!',

        /**
         * #### disconnect
         *
         * Disconnected from waiting room
         */
        // disconnect: 'Custom string: YOU HAVE BEEN DISCONNECTED!',

        /**
         * #### waitedTooLong
         *
         * The MAX_WAIT_TIME countdown expired
         */
        // waitedTooLong: 'Custom string: YOU WAITED TOO LONG!',

        /**
         * #### notEnoughPlayers
         *
         * There are not enough players to start a game
         */
        // notEnoughPlayers: 'Custom string: NOT ENOUGH PLAYERS!',

        /**
         * #### roomClosed
         *
         * A player tries to connect, but the waiting room has been closed
         */
        // roomClosed: 'Custom string: ROOM CLOSED! CANNOT ENTER!',

        /**
         * #### tooManyPlayers
         *
         * Currently there are more players than needed by the game
         */
        // tooManyPlayers: 'Custom string: TOO MANY PLAYERS!',

        /**
         * #### notSelectedClosed
         *
         * Player has not been selected, and cannot participate in other games
         */
        // notSelectedClosed: 'Custom string: NOT SELECTED CLOSED!',

        /**
         * #### notSelectedOpen
         *
         * Player has not been selected, but can still participate in new games
         */
        // notSelectedOpen: 'Custom string: NOT SELECTED OPEN!',

        /**
         * #### exitCode
         *
         * Player disconnected, and an exit code might have been provided
         */
        // exitCode: 'Custom string: EXIT CODE TEXT',
    },

    /** ### SOUNDS
     *
     * Collections of sounds played when given events occurs
     *
     * Each item can be a string, or function returning a string; the function
     * receives two input parameters: the instance of the widget and an object
     * with extra information (depending on the event).
     *
     * @see WaitingRoom.sounds
     */
    SOUNDS: {

        /**
         * ## dispatch
         *
         * Notifies players that a game is about to be dispatched
         *
         * If TRUE, plays default sound, if string plays the file sound
         * located at the specified uri.
         */
        // dispatch: false
    },

    /** ### ALLOW_PLAY_WITH_BOTS
     *
     * Allows a player to request to start the game immediately with bots
     *
     * A button is added to the interface.
     */
    // ALLOW_PLAY_WITH_BOTS: true,

    /** ### ALLOW_SELECT_TREATMENT
     *
     * Allows a player to select the treatment for the game
     *
     * This option requires `ALLOW_PLAY_WITH_BOTS` to be TRUE.
     *
     * A button is added to the interface.
     */
    // ALLOW_SELECT_TREATMENT: true

};
<|endoftext|>


<commit_before>import React, { Component } from 'react'
import Immutable from 'seamless-immutable'
import Dragula from 'react-dragula'

import Card from 'material-ui/lib/card/card'
import CardText from 'material-ui/lib/card/card-text'
import Tabs from 'material-ui/lib/tabs/tabs'
import Tab from 'material-ui/lib/tabs/tab'
import CheckBox from 'material-ui/lib/checkbox'
import SelectField from 'material-ui/lib/select-field'
import MenuItem from 'material-ui/lib/menus/menu-item'

import TextItemList from './TextItemList'
import { getOptionalProp } from '../utils/object'

import * as bmcCategories from '../data/bmcCategories.json'
import * as bmcDefaults  from'../data/bmcDefaults.json'

import shouldComponentUpdate from '../utils/shouldComponentUpdate'

const styles = {
  container: {
    width: '100%',
    display: 'inline-flex'
  },
  cardText: {
    padding: 0
  },
  inkBar: {
    height: 6,
    marginTop: -6
  },
  tabContents: {
    padding: 16,
    paddingBottom: 50,
    overflow: 'auto'
  }
}

export default class BusinessModelCanvas extends Component {
  constructor (props) {
    super(props)

    // update only when props or state are changed
    this.shouldComponentUpdate = shouldComponentUpdate
  }

  render () {
    const { bmc, onSetProperty } = this.props

    const onChangeType = (event, index, value) => {
      onSetProperty(['bmc', 'description', 'type'], value)
    }
    const onChangeProducts = value => {
      onSetProperty(['bmc', 'description', 'products'], value)
    }
    const onChangeCustomers = value => {
      onSetProperty(['bmc', 'description', 'customers'], value)
    }
    const onChangeUniqueSellingPoint = event => {
      onSetProperty(['bmc', 'description', 'uniqueSellingPoint'], event.target.value)
    }

    return

      <Card className="card">
        <CardText style={styles.cardText}>
          <Tabs inkBarStyle={styles.inkBar} contentContainerStyle={styles.tabContents}>
            <Tab label="Business Model Canvas">

                <colGroup>
                  <col width="10%" />
                  <col width="10%" />
                  <col width="10%" />
                  <col width="10%" />
                  <col width="10%" />
                  <col width="10%" />
                  <col width="10%" />
                  <col width="10%" />
                  <col width="10%" />
                  <col width="10%" />
                </colGroup>

                          We are a <SelectField style={{fontSize: 14}} value={bmc.description && bmc.description.type} onChange={onChangeType}>
                            <MenuItem index={-1} value="" primaryText=""/>
                            {
                              bmcCategories.types.map((c, i) => (
                                  <MenuItem key={c.id} index={i} value={c.id} primaryText={c.text} />
                              ))
                            }
                          </SelectField> company

                            Key partners

                            { renderCategories('partnerships', bmc, onSetProperty) }
                            { renderOther('partnerships', bmc, onSetProperty) }

                            Key activities

                            { renderCategories('activities', bmc, onSetProperty) }
                            { renderOther('activities', bmc, onSetProperty) }

                            Value proposition

                              We make:

                            <TextItemList
                                placeholder="product"
                                items={bmc.description && bmc.description.products}
                                onChange={onChangeProducts} />

                              for:

                            <TextItemList
                                placeholder="customers"
                                items={bmc.description && bmc.description.customers}
                                onChange={onChangeCustomers} />

                              and they like us because of:

                            <input
                                type="text"
                                placeholder="unique selling point"
                                value={bmc.description && bmc.description.uniqueSellingPoint}
                                onChange={onChangeUniqueSellingPoint }
                            />

                            Customer relations

                            { renderCategories('contacts', bmc, onSetProperty) }
                            { renderOther('contacts', bmc, onSetProperty) }

                            Customer segments

                            <TextItemList
                                items={bmc.description && bmc.description.customers}
                                onChange={onChangeCustomers} />

                            Key resources

Resources

                            { renderCategories('resources', bmc, onSetProperty) }
                            { renderOther('resources', bmc, onSetProperty) }

Investments

                            { renderCategories('investments', bmc, onSetProperty) }
                            { renderOther('investments', bmc, onSetProperty) }

                            Channels

                            { renderCategories('channels', bmc, onSetProperty) }
                            { renderOther('channels', bmc, onSetProperty) }

                            Cost structure

                            { this.renderCostStructure(bmc, onSetProperty) }

                            Revenue streams

                            { renderRevenueStreams(bmc, onSetProperty) }

            </Tab>
          </Tabs>

        </CardText>
      </Card>

  }

  renderCostStructure (bmc, onSetProperty) {
    const categories = generateCostCategories(bmc)

    function getGroupId (category) {
      if (bmc.costStructure && bmc.costStructure[category.id]) {
        return bmc.costStructure[category.id].groupId
      }
      else if (category.group) {
        return category.group
      }
      else {
        return 'indirect'
      }
    }

    const isDirect = category => getGroupId(category) === 'direct'
    const isInvestment = category => getGroupId(category) === 'investment'
    const isIndirect = category => {
      const groupId = getGroupId(category)
      return groupId === 'direct' && groupId !== 'investment'
    }

    const direct    = categories.filter(isDirect)
    const indirect  = categories.filter(isIndirect)
    const investment = categories.filter(isInvestment)

    const renderCategory = category => {
      return

        {'\u22ee'} {category.text}

    }

    return

          Direct
          Indirect
          Investment

            { direct.map(renderCategory) }

            { indirect.map(renderCategory) }

            { investment.map(renderCategory) }

  }

  componentDidMount () {
    const containers = [
      this.refs.groupDirect,
      this.refs.groupIndirect,
      this.refs.groupInvestment
    ]

    // we create copies of the dragged elements, which are cleaned up again
    // when dropped so React can create the element itself.
    this.drake = Dragula(containers, {
      copy: true
    })

    this.drake.on('drop', (element) => {
      const parent = element.parentNode
      if (parent) {
        const groupId = parent.getAttribute('data-group-id')
        const categoryId = element.getAttribute('data-category-id')
        const index = toArray(parent.childNodes).indexOf(element)

        // remove the copied element, it will be generated again via React
        parent.removeChild(element)

        this.props.onSetProperty(['bmc', 'costStructure', categoryId], { groupId, index })
      }
    })
  }

  componentWillUnmount () {
    this.drake.destroy()
  }
}

function renderCategories (group, bmc, onSetProperty) {
  return bmcCategories[group].map(category => {

    const props = {
      label: category.text,
      checked: isCategoryChecked(group, bmc, category.id),
      onCheck: (event) => {
        const newValue = {
          value: event.target.checked,
          isDefault: false
        }
        onSetProperty(['bmc', group, 'values', category.id], newValue)
      }
    }

    return

      <CheckBox {...props} />

  })
}

function isCategoryChecked (group, bmc, categoryId) {
  let checked = getOptionalProp(bmc, [group, 'values', categoryId, 'value'])
  if (typeof checked !== 'boolean') {
    checked = getOptionalProp(bmcDefaults, [bmc.description.type, group, 'values', categoryId, 'value'])
    if (typeof checked !== 'boolean') {
      checked = false
    }
  }
  return checked
}

function generateCostCategories (bmc) {
  const groups = Immutable([ 'activities', 'resources', 'investments', 'contacts', 'channels' ])

  return groups
      .flatMap(group => {
        const groups = bmcCategories[group]
            .filter(category => isCategoryChecked (group, bmc, category.id))

        const otherGroups = (bmc[group] && bmc[group].other || [])
            .map(({id, value}) => ({
              id,
              text: value,
              group: (group === 'investments') ? 'investment' : null
            }))

        return groups.concat(otherGroups)
      })
}

function renderRevenueStreams (bmc, onSetProperty) {
  const revenueStreams = generateRevenueCategories(bmc.description.products, bmc.description.customers)

  return revenueStreams.map(category => {
    let checked = getOptionalProp(bmc, ['revenueStreams', 'values', category.id, 'value'])
    if (checked === undefined) {
      checked = true
    }

    const onCheck = (event) => {
      const newValue = {
        value: event.target.checked,
        isDefault: false
      }

      onSetProperty(['bmc', 'revenueStreams', 'values', category.id], newValue)
    }

    return

      <CheckBox checked={checked} label={category.value} onCheck={onCheck} />

  })
}

/**
 * Create a category for every combination of product and customer
 * @param {Array.<{id: string, value: string}>} products
 * @param {Array.<{id: string, value: string}>} customers
 * @return {Array.<{id: string, value: string}>}
 *   Array with entries having a compound key as id, which is a concatenation
 *   of the id's of the product and customer.
 */
function generateRevenueCategories (products = [], customers = []) {
  return Immutable(products).flatMap(product => {
    return customers.map(customer => {
      return {
        id: product.id + ':' + customer.id,
        value: product.value + ' for ' + customer.value
      }
    })
  })
}

function renderOther (group, bmc, onSetProperty) {
  const items = bmc[group] && bmc[group].other || []

  const onChange = items => {
    onSetProperty(['bmc', group, 'other'], items)
  }

  return

Other

    <TextItemList items={items} onChange={onChange} />

}

/**
 * Convert a DOM Node List or Arguments into a regular Array
 * @param nodes
 * @return {Array}
 */
function toArray (nodes) {
  const array = [];

  for (var i = 0; i < nodes.length; i++) {
    array[i] = nodes[i]
  }

  return array
}<commit_msg>Fixed filtering of indrect cost group<commit_after>import React, { Component } from 'react'
import Immutable from 'seamless-immutable'
import Dragula from 'react-dragula'

import Card from 'material-ui/lib/card/card'
import CardText from 'material-ui/lib/card/card-text'
import Tabs from 'material-ui/lib/tabs/tabs'
import Tab from 'material-ui/lib/tabs/tab'
import CheckBox from 'material-ui/lib/checkbox'
import SelectField from 'material-ui/lib/select-field'
import MenuItem from 'material-ui/lib/menus/menu-item'

import TextItemList from './TextItemList'
import { getOptionalProp } from '../utils/object'

import * as bmcCategories from '../data/bmcCategories.json'
import * as bmcDefaults  from'../data/bmcDefaults.json'

import shouldComponentUpdate from '../utils/shouldComponentUpdate'

const styles = {
  container: {
    width: '100%',
    display: 'inline-flex'
  },
  cardText: {
    padding: 0
  },
  inkBar: {
    height: 6,
    marginTop: -6
  },
  tabContents: {
    padding: 16,
    paddingBottom: 50,
    overflow: 'auto'
  }
}

export default class BusinessModelCanvas extends Component {
  constructor (props) {
    super(props)

    // update only when props or state are changed
    this.shouldComponentUpdate = shouldComponentUpdate
  }

  render () {
    const { bmc, onSetProperty } = this.props

    const onChangeType = (event, index, value) => {
      onSetProperty(['bmc', 'description', 'type'], value)
    }
    const onChangeProducts = value => {
      onSetProperty(['bmc', 'description', 'products'], value)
    }
    const onChangeCustomers = value => {
      onSetProperty(['bmc', 'description', 'customers'], value)
    }
    const onChangeUniqueSellingPoint = event => {
      onSetProperty(['bmc', 'description', 'uniqueSellingPoint'], event.target.value)
    }

    return

      <Card className="card">
        <CardText style={styles.cardText}>
          <Tabs inkBarStyle={styles.inkBar} contentContainerStyle={styles.tabContents}>
            <Tab label="Business Model Canvas">

                <colGroup>
                  <col width="10%" />
                  <col width="10%" />
                  <col width="10%" />
                  <col width="10%" />
                  <col width="10%" />
                  <col width="10%" />
                  <col width="10%" />
                  <col width="10%" />
                  <col width="10%" />
                  <col width="10%" />
                </colGroup>

                          We are a <SelectField style={{fontSize: 14}} value={bmc.description && bmc.description.type} onChange={onChangeType}>
                            <MenuItem index={-1} value="" primaryText=""/>
                            {
                              bmcCategories.types.map((c, i) => (
                                  <MenuItem key={c.id} index={i} value={c.id} primaryText={c.text} />
                              ))
                            }
                          </SelectField> company

                            Key partners

                            { renderCategories('partnerships', bmc, onSetProperty) }
                            { renderOther('partnerships', bmc, onSetProperty) }

                            Key activities

                            { renderCategories('activities', bmc, onSetProperty) }
                            { renderOther('activities', bmc, onSetProperty) }

                            Value proposition

                              We make:

                            <TextItemList
                                placeholder="product"
                                items={bmc.description && bmc.description.products}
                                onChange={onChangeProducts} />

                              for:

                            <TextItemList
                                placeholder="customers"
                                items={bmc.description && bmc.description.customers}
                                onChange={onChangeCustomers} />

                              and they like us because of:

                            <input
                                type="text"
                                placeholder="unique selling point"
                                value={bmc.description && bmc.description.uniqueSellingPoint}
                                onChange={onChangeUniqueSellingPoint }
                            />

                            Customer relations

                            { renderCategories('contacts', bmc, onSetProperty) }
                            { renderOther('contacts', bmc, onSetProperty) }

                            Customer segments

                            <TextItemList
                                items={bmc.description && bmc.description.customers}
                                onChange={onChangeCustomers} />

                            Key resources

Resources

                            { renderCategories('resources', bmc, onSetProperty) }
                            { renderOther('resources', bmc, onSetProperty) }

Investments

                            { renderCategories('investments', bmc, onSetProperty) }
                            { renderOther('investments', bmc, onSetProperty) }

                            Channels

                            { renderCategories('channels', bmc, onSetProperty) }
                            { renderOther('channels', bmc, onSetProperty) }

                            Cost structure

                            { this.renderCostStructure(bmc) }

                            Revenue streams

                            { renderRevenueStreams(bmc, onSetProperty) }

            </Tab>
          </Tabs>

        </CardText>
      </Card>

  }

  renderCostStructure (bmc) {
    const categories = generateCostCategories(bmc)

    function getGroupId (category) {
      if (bmc.costStructure && bmc.costStructure[category.id]) {
        return bmc.costStructure[category.id].groupId
      }
      else if (category.group) {
        return category.group
      }
      else {
        return 'indirect'
      }
    }

    const isDirect = category => getGroupId(category) === 'direct'
    const isInvestment = category => getGroupId(category) === 'investment'
    const isIndirect = category => {
      const groupId = getGroupId(category)
      return groupId !== 'direct' && groupId !== 'investment'
    }

    const direct    = categories.filter(isDirect)
    const indirect  = categories.filter(isIndirect)
    const investment = categories.filter(isInvestment)

    const renderCategory = category => {
      return

        {'\u22ee'} {category.text}

    }

    return

          Direct
          Indirect
          Investment

            { direct.map(renderCategory) }

            { indirect.map(renderCategory) }

            { investment.map(renderCategory) }

  }

  componentDidMount () {
    const containers = [
      this.refs.groupDirect,
      this.refs.groupIndirect,
      this.refs.groupInvestment
    ]

    // we create copies of the dragged elements, which are cleaned up again
    // when dropped so React can create the element itself.
    this.drake = Dragula(containers, {
      copy: true
    })

    this.drake.on('drop', (element) => {
      const parent = element.parentNode
      if (parent) {
        const groupId = parent.getAttribute('data-group-id')
        const categoryId = element.getAttribute('data-category-id')
        const index = toArray(parent.childNodes).indexOf(element)

        // remove the copied element, it will be generated again via React
        parent.removeChild(element)

        this.props.onSetProperty(['bmc', 'costStructure', categoryId], { groupId, index })
      }
    })
  }

  componentWillUnmount () {
    this.drake.destroy()
  }
}

function renderCategories (group, bmc, onSetProperty) {
  return bmcCategories[group].map(category => {

    const props = {
      label: category.text,
      checked: isCategoryChecked(group, bmc, category.id),
      onCheck: (event) => {
        const newValue = {
          value: event.target.checked,
          isDefault: false
        }
        onSetProperty(['bmc', group, 'values', category.id], newValue)
      }
    }

    return

      <CheckBox {...props} />

  })
}

function isCategoryChecked (group, bmc, categoryId) {
  let checked = getOptionalProp(bmc, [group, 'values', categoryId, 'value'])
  if (typeof checked !== 'boolean') {
    checked = getOptionalProp(bmcDefaults, [bmc.description.type, group, 'values', categoryId, 'value'])
    if (typeof checked !== 'boolean') {
      checked = false
    }
  }
  return checked
}

function generateCostCategories (bmc) {
  const groups = Immutable([ 'activities', 'resources', 'investments', 'contacts', 'channels' ])

  return groups
      .flatMap(group => {
        const groups = bmcCategories[group]
            .filter(category => isCategoryChecked (group, bmc, category.id))

        const otherGroups = (bmc[group] && bmc[group].other || [])
            .map(({id, value}) => ({
              id,
              text: value,
              group: (group === 'investments') ? 'investment' : null
            }))

        return groups.concat(otherGroups)
      })
}

function renderRevenueStreams (bmc, onSetProperty) {
  const revenueStreams = generateRevenueCategories(bmc.description.products, bmc.description.customers)

  return revenueStreams.map(category => {
    let checked = getOptionalProp(bmc, ['revenueStreams', 'values', category.id, 'value'])
    if (checked === undefined) {
      checked = true
    }

    const onCheck = (event) => {
      const newValue = {
        value: event.target.checked,
        isDefault: false
      }

      onSetProperty(['bmc', 'revenueStreams', 'values', category.id], newValue)
    }

    return

      <CheckBox checked={checked} label={category.value} onCheck={onCheck} />

  })
}

/**
 * Create a category for every combination of product and customer
 * @param {Array.<{id: string, value: string}>} products
 * @param {Array.<{id: string, value: string}>} customers
 * @return {Array.<{id: string, value: string}>}
 *   Array with entries having a compound key as id, which is a concatenation
 *   of the id's of the product and customer.
 */
function generateRevenueCategories (products = [], customers = []) {
  return Immutable(products).flatMap(product => {
    return customers.map(customer => {
      return {
        id: product.id + ':' + customer.id,
        value: product.value + ' for ' + customer.value
      }
    })
  })
}

function renderOther (group, bmc, onSetProperty) {
  const items = bmc[group] && bmc[group].other || []

  const onChange = items => {
    onSetProperty(['bmc', group, 'other'], items)
  }

  return

Other

    <TextItemList items={items} onChange={onChange} />

}

/**
 * Convert a DOM Node List or Arguments into a regular Array
 * @param nodes
 * @return {Array}
 */
function toArray (nodes) {
  const array = [];

  for (var i = 0; i < nodes.length; i++) {
    array[i] = nodes[i]
  }

  return array
}<|endoftext|>


<commit_before>'use strict';
/**
 * @file
 * Currently the main entrypoint served on /work.
 * Provides the work view for the enduser.
 */
import React from 'react';
import Reflux from 'reflux';

import {isEmpty, values} from 'lodash';

import workAction from '../../actions/Work.action.js';
import WorkStore from '../../stores/Work.store.js';
import {CoverImage, OrderButton} from 'dbc-react-components';
import {rewriteCoverImageUrl} from '../../../utils/CoverImage.util.js';
import LikeContainer from '../LikeDislike/LikeContainer.component.js';
import DislikeContainer from '../LikeDislike/DislikeContainer.component.js';
import ProfileStore from '../../stores/Profile.store.js';

const Work = React.createClass({
  displayName: 'Work.component',

  propTypes: {
    id: React.PropTypes.string,
    work: React.PropTypes.object
  },

  mixins: [
    Reflux.connect(WorkStore, 'work'),
    Reflux.connect(ProfileStore, 'profile')
  ],

  getInitialState() {
    if (typeof window === 'undefined') {
      this.getWork();
    }
  },

  componentDidMount() {
    this.getWork();
  },

  getWork() {
    if (this.props.work) {
      workAction.updated([this.props.work]);
    }
    workAction({id: this.props.id});
  },

  getLikeDislikeContainers(id) {
    return (

          <LikeContainer objectId={id} />

          <DislikeContainer objectId={id} />

    );
  },

  render() {
    const profile = this.state.profile;
    const work = this.props.work ? this.props.work : this.state.work;
    const id = this.props.id;

    // No result found!
    if (work.info.hits === '0') {
      return (
Værket blev ikke fundet
);
    }

    // Result is pending, waiting for serviceprovider
    if (isEmpty(work.result) && isEmpty(work.info)) {
      return (

      );
    }

    // Empty result (Should perhaps be an error report)
    if (isEmpty(work.result)) {
      return (
);
    }

    // Data was found, begin rendering
    const likeContainers = this.state.profile.userIsLoggedIn ? this.getLikeDislikeContainers(id) : ' ';
    let editions = [];
    let specifics_object = {};

    work.result.editions.forEach((element, index) => {
      let edition = (

{element.type}

{element.edition + (element.edition !== '' ? ', ' : '')} {element.date}

{element.extent}

            {element.isbns.length > 0 ? 'ISBN: ' : ''}
            {element.isbns.join(', ')}

            {element.link.map((link, idx) => {
              return (Find online);
            })}

      );

      editions.push(edition);

      const elementKey = element.accessType + '_' + element.type;

      if (!specifics_object.hasOwnProperty(elementKey)) {
        specifics_object[elementKey] = {
          accessType: element.accessType,
          creator: element.creator,
          dates: [],
          identifiers: [],
          links: [],
          order: '',
          title: element.title,
          type: element.type
        };
      }

      element.link.forEach((link) => {
        specifics_object[elementKey].links.push(link);
      });
      specifics_object[elementKey].dates.push(element.date);
      specifics_object[elementKey].identifiers.push(element.identifier);
      specifics_object[elementKey].order = '/order?ids=' + specifics_object[elementKey].identifiers.join(',') +
        '&creator=' + specifics_object[elementKey].creator +
        '&title=' + encodeURIComponent(specifics_object[elementKey].title) +
        '&type=' + specifics_object[elementKey].type;
    });

    let specifics = values(specifics_object);

    return (

          <CoverImage pids={[id, work.result.pid]} prefSize='detail_500' rewritwImgUrl={rewriteCoverImageUrl} />

          <OrderButton manifestations={specifics} profile={profile} />

          {likeContainers}

{work.result.title}

                {work.result.creator}

{work.result.abstract}

              {work.result.isbns.length > 0 ? 'ISBN: '+ work.result.isbns[0] : ''}

{work.result.extent}

                {work.result.actors.length > 0 ? 'Medvirkende: ' : ''}
                {work.result.actors.map((actor, index) => {
                  return (

                        {actor}

                    );
                })}

              {work.result.series}

              {work.result.subjects.length > 0 ? 'Emner: ' : ''}
              {work.result.subjects.map((subject, index, array) => {
                const notLast = (array.length - 1) !== index;
                return (

                     {notLast ? subject + ', ' : subject}

                );
              })}

              {work.result.dk5s.length > 0 ? 'Opstilling: ' : ''}
              {work.result.dk5s.map((dk5, index) => {
                return (

                    {dk5.text}

                );
              })}

{work.result.audience.age.join(', ')}

{work.result.audience.pegi}

{work.result.audience.medieraad}

              {work.result.tracks.length > 0 ? 'Trackliste: ' : ''}
              {work.result.tracks.map((track, index) => {
                return (
{track}
);
              })}

              {work.result.languages.length > 0 ? 'Sprog: ' : ''}
              {work.result.languages.join(', ')}

            {specifics.map((specific, index) => {
              return (
                {specific.type} ({specific.dates.join('; ')})
              );
            })}

            {editions.length > 0 ? 'Udgaver:' : ''}
            {editions}

    );
  }
});

export default Work;
<commit_msg>Accumulated commit of the following from branch 'origin/ready/us109':<commit_after>'use strict';
/**
 * @file
 * Currently the main entrypoint served on /work.
 * Provides the work view for the enduser.
 */
import React from 'react';
import Reflux from 'reflux';

import {isEmpty, values} from 'lodash';

import workAction from '../../actions/Work.action.js';
import WorkStore from '../../stores/Work.store.js';
import {CoverImage, OrderButton} from 'dbc-react-components';
import {rewriteCoverImageUrl} from '../../../utils/CoverImage.util.js';
import LikeContainer from '../LikeDislike/LikeContainer.component.js';
import DislikeContainer from '../LikeDislike/DislikeContainer.component.js';
import ProfileStore from '../../stores/Profile.store.js';

const Work = React.createClass({
  displayName: 'Work.component',

  propTypes: {
    id: React.PropTypes.string,
    work: React.PropTypes.object
  },

  mixins: [
    Reflux.connect(WorkStore, 'work'),
    Reflux.connect(ProfileStore, 'profile')
  ],

  getInitialState() {
    if (typeof window === 'undefined') {
      this.getWork();
    }
  },

  componentDidMount() {
    this.getWork();
  },

  getWork() {
    if (this.props.work) {
      workAction.updated([this.props.work]);
    }
    workAction({id: this.props.id});
  },

  getLikeDislikeContainers(id) {
    return (

          <LikeContainer objectId={id} />

          <DislikeContainer objectId={id} />

    );
  },

  render() {
    const profile = this.state.profile;
    const work = this.props.work ? this.props.work : this.state.work;
    const id = this.props.id;

    // No result found!
    if (work.info.hits === '0') {
      return (
Værket blev ikke fundet
);
    }

    // Result is pending, waiting for serviceprovider
    if (isEmpty(work.result) && isEmpty(work.info)) {
      return (

      );
    }

    // Empty result (Should perhaps be an error report)
    if (isEmpty(work.result)) {
      return (
);
    }

    // Data was found, begin rendering
    const likeContainers = this.state.profile.userIsLoggedIn ? this.getLikeDislikeContainers(id) : ' ';
    let editions = [];
    let specifics_object = {};

    work.result.editions.forEach((element, index) => {
      let edition = (

{element.type}

{element.edition + (element.edition !== '' ? ', ' : '')} {element.date}

{element.extent}

            {element.isbns.length > 0 ? 'ISBN: ' : ''}
            {element.isbns.join(', ')}

            {element.link.map((link, idx) => {
              return (Find online);
            })}

      );

      editions.push(edition);

      const elementKey = element.accessType + '_' + element.type;

      if (!specifics_object.hasOwnProperty(elementKey)) {
        specifics_object[elementKey] = {
          accessType: element.accessType,
          creator: element.creator,
          dates: [],
          identifiers: [],
          links: [],
          order: '',
          title: element.title,
          type: element.type
        };
      }

      element.link.forEach((link) => {
        specifics_object[elementKey].links.push(link);
      });
      specifics_object[elementKey].dates.push(element.date);
      specifics_object[elementKey].identifiers.push(element.identifier);
      specifics_object[elementKey].order = '/order?ids=' + specifics_object[elementKey].identifiers.join(',') +
        '&creator=' + specifics_object[elementKey].creator +
        '&title=' + encodeURIComponent(specifics_object[elementKey].title) +
        '&type=' + specifics_object[elementKey].type;
    });

    let specifics = values(specifics_object);

    return (

          <CoverImage pids={[id, work.result.pid]} prefSize='detail_500' rewritwImgUrl={rewriteCoverImageUrl} />

          <OrderButton manifestations={specifics} profile={profile} relations={work.result.relations} />

          {likeContainers}

{work.result.title}

                {work.result.creator}

{work.result.abstract}

              {work.result.isbns.length > 0 ? 'ISBN: '+ work.result.isbns[0] : ''}

{work.result.extent}

                {work.result.actors.length > 0 ? 'Medvirkende: ' : ''}
                {work.result.actors.map((actor, index) => {
                  return (

                        {actor}

                    );
                })}

              {work.result.series}

              {work.result.subjects.length > 0 ? 'Emner: ' : ''}
              {work.result.subjects.map((subject, index, array) => {
                const notLast = (array.length - 1) !== index;
                return (

                     {notLast ? subject + ', ' : subject}

                );
              })}

              {work.result.dk5s.length > 0 ? 'Opstilling: ' : ''}
              {work.result.dk5s.map((dk5, index) => {
                return (

                    {dk5.text}

                );
              })}

{work.result.audience.age.join(', ')}

{work.result.audience.pegi}

{work.result.audience.medieraad}

              {work.result.tracks.length > 0 ? 'Trackliste: ' : ''}
              {work.result.tracks.map((track, index) => {
                return (
{track}
);
              })}

              {work.result.languages.length > 0 ? 'Sprog: ' : ''}
              {work.result.languages.join(', ')}

            {specifics.map((specific, index) => {
              return (
                {specific.type} ({specific.dates.join('; ')})
              );
            })}

            {editions.length > 0 ? 'Udgaver:' : ''}
            {editions}

    );
  }
});

export default Work;
<|endoftext|>


<commit_before>import React       from 'react'
import GoogleLogin from 'react-google-login'
import request     from 'request-promise'
import {
    connect
} from 'react-redux'

import ButtonSub from '../subs/ButtonSub'

import {
    setAccountData
} from '../../actions'

class LoginButton extends React.PureComponent {
    constructor (props) {
        super(props);
        this.state = {
            hover: false
        };
        this.mouseEnter = this.mouseEnter.bind(this);
        this.mouseLeave = this.mouseLeave.bind(this);
        this.onGoogleLoginSuccess = this.onGoogleLoginSuccess.bind(this);
        this.onGoogleLoginFailure = this.onGoogleLoginFailure.bind(this);
        this.onLogOut = this.onLogOut.bind(this);
    }
    componentDidMount () {
        (async () => {
            let res = await request({
                method: 'POST',
                uri: 'http://tarnadas.ddns.net/signin',
                json: true
            });
            if (!res.err) {
                setTimeout(() => { // TODO this is just a workaround to prevent a warning
                    this.props.dispatch(setAccountData(res));
                }, 1000);
            }
        })();
    }
    mouseEnter() {
        this.setState({
            hover: true
        });
    }
    mouseLeave() {
        this.setState({
            hover: false
        });
    }
    async onGoogleLoginSuccess (response) {
        let res = await request({
            method: 'POST',
            uri: 'http://tarnadas.ddns.net/tokensignin',
            body: response,
            json: true
        });
        this.props.dispatch(setAccountData(res));
    }
    onGoogleLoginFailure (response) {
        console.log(response);
    }
    async onLogOut () {
        let res = await request({
            method: 'POST',
            uri: 'http://tarnadas.ddns.net/signout',
            json: true
        });
        if (!res.err) {
            this.props.dispatch(setAccountData());
        } else {
            console.log(res.err);
        }
    }
    render () {
        const accountData = this.props.accountData.toJS();
        const loggedIn = !!accountData.id;
        const styles = {
            smmButton: {
                margin: '0 10px 10px 10px',
                lineHeight: '40px',
                width: '120px',
                height: '40px',
                backgroundColor: this.state.hover ? '#323245' : '#ffe500',
                color: this.state.hover ? '#fff' : '#323245',
                cursor: 'pointer',
                outline: 'none',
                overflow: 'hidden',
                whiteSpace: 'nowrap',
                boxSizing: 'border-box',
                border: '0',
                borderRadius: '5px',
                boxShadow: '1px 4px 13px 0 rgba(0,0,0,0.5)',
                padding: '0',
                textAlign: 'left',
                fontFamily: 'SuperMarioMakerExtended,SuperMarioMaker,Roboto,arial,sans-serif'
            },
            smmIcon: {
                margin: '4px',
                width: '32px',
                height: '32px',
                float: 'left',
                borderRadius: '4px'
            }
        };
        let iconSrc, text;
        if (loggedIn) {
            iconSrc = '/img/logout.png';
            text = 'Sign out';
        } else {
            iconSrc = '//developers.google.com/identity/sign-in/g-normal.png';
            text = 'Sign in';
        }
        return (

                {
                    loggedIn ? (

                            <ButtonSub iconStyle={styles.smmIcon} iconSrc={iconSrc} text={text} />

                    ) : (
                        <GoogleLogin
                            clientId="899493559187-bnvgqj1i8cnph7ilkl4h261836skee25.apps.googleusercontent.com"
                            style={styles.smmButton}
                            onSuccess={this.onGoogleLoginSuccess}
                            onFailure={this.onGoogleLoginFailure}
                        >
                            <ButtonSub iconStyle={styles.smmIcon} iconSrc={iconSrc} text={text} />
                        </GoogleLogin>
                    )
                }

        )
    }
}
export default connect(state => {
    const accountData = state.getIn(['userData', 'accountData']);
    return {
        accountData
    }
})(LoginButton);<commit_msg>login domain fix<commit_after>import React       from 'react'
import GoogleLogin from 'react-google-login'
import request     from 'request-promise'
import {
    connect
} from 'react-redux'

import * as url from 'url'

import ButtonSub from '../subs/ButtonSub'
import {
    setAccountData
} from '../../actions'
import { domain } from '../../../static'

class LoginButton extends React.PureComponent {
    constructor (props) {
        super(props);
        this.state = {
            hover: false
        };
        this.mouseEnter = this.mouseEnter.bind(this);
        this.mouseLeave = this.mouseLeave.bind(this);
        this.onGoogleLoginSuccess = this.onGoogleLoginSuccess.bind(this);
        this.onGoogleLoginFailure = this.onGoogleLoginFailure.bind(this);
        this.onLogOut = this.onLogOut.bind(this);
    }
    componentDidMount () {
        (async () => {
            let res = await request({
                method: 'POST',
                uri: url.resolve(domain, '/signin'),
                json: true
            });
            if (!res.err) {
                setTimeout(() => { // TODO this is just a workaround to prevent a warning
                    this.props.dispatch(setAccountData(res));
                }, 1000);
            }
        })();
    }
    mouseEnter() {
        this.setState({
            hover: true
        });
    }
    mouseLeave() {
        this.setState({
            hover: false
        });
    }
    async onGoogleLoginSuccess (response) {
        let res = await request({
            method: 'POST',
            uri: url.resolve(domain, '/tokensignin'),
            body: response,
            json: true
        });
        this.props.dispatch(setAccountData(res));
    }
    onGoogleLoginFailure (response) {
        console.log(response);
    }
    async onLogOut () {
        let res = await request({
            method: 'POST',
            uri: url.resolve(domain, '/signout'),
            json: true
        });
        if (!res.err) {
            this.props.dispatch(setAccountData());
        } else {
            console.log(res.err);
        }
    }
    render () {
        const accountData = this.props.accountData.toJS();
        const loggedIn = !!accountData.id;
        const styles = {
            smmButton: {
                margin: '0 10px 10px 10px',
                lineHeight: '40px',
                width: '120px',
                height: '40px',
                backgroundColor: this.state.hover ? '#323245' : '#ffe500',
                color: this.state.hover ? '#fff' : '#323245',
                cursor: 'pointer',
                outline: 'none',
                overflow: 'hidden',
                whiteSpace: 'nowrap',
                boxSizing: 'border-box',
                border: '0',
                borderRadius: '5px',
                boxShadow: '1px 4px 13px 0 rgba(0,0,0,0.5)',
                padding: '0',
                textAlign: 'left',
                fontFamily: 'SuperMarioMakerExtended,SuperMarioMaker,Roboto,arial,sans-serif'
            },
            smmIcon: {
                margin: '4px',
                width: '32px',
                height: '32px',
                float: 'left',
                borderRadius: '4px'
            }
        };
        let iconSrc, text;
        if (loggedIn) {
            iconSrc = '/img/logout.png';
            text = 'Sign out';
        } else {
            iconSrc = '//developers.google.com/identity/sign-in/g-normal.png';
            text = 'Sign in';
        }
        return (

                {
                    loggedIn ? (

                            <ButtonSub iconStyle={styles.smmIcon} iconSrc={iconSrc} text={text} />

                    ) : (
                        <GoogleLogin
                            clientId="899493559187-bnvgqj1i8cnph7ilkl4h261836skee25.apps.googleusercontent.com"
                            style={styles.smmButton}
                            onSuccess={this.onGoogleLoginSuccess}
                            onFailure={this.onGoogleLoginFailure}
                        >
                            <ButtonSub iconStyle={styles.smmIcon} iconSrc={iconSrc} text={text} />
                        </GoogleLogin>
                    )
                }

        )
    }
}
export default connect(state => {
    const accountData = state.getIn(['userData', 'accountData']);
    return {
        accountData
    }
})(LoginButton);<|endoftext|>


<commit_before>import React, {PropTypes} from 'react';
import {Field, reduxForm, formValueSelector} from 'redux-form';
import { connect } from 'react-redux';
import StringInput from './StringInput';
import EmailInput from './EmailInput';
import StudentNumberInput from './StudentNumberInput';
import NumberOfCoursesToDisplay from './NumberOfCoursesToDisplay';
import RenderField from './RenderField';

let AcademicConsessionFormView = (props) => {
    const { handleSubmit, numberOfCoursesValue } = props;
    return (
        <form onSubmit={handleSubmit}>

Request for Academic Consession

                    <StringInput name="firstName" label="First Name" placeholder="First Name"/>

                    <StringInput name="lastName" label="Last Name" placeholder="Last Name"/>

                    <StudentNumberInput name="studentNumber" label="Student Number" placeholder="Student Number"/>

                    <EmailInput name="email" label="Email" placeholder="Email"/>

                    Year Level

                        <Field name="yearLevel" component="select" className="input">
                            <option></option>
                            <option value="Year 1">Year 1</option>
                            <option value="Year 2">Year 2</option>
                            <option value="Year 3">Year 3</option>
                            <option value="Year 4">Year 4</option>
                        </Field>

                    Department/Program (if applicable)

                        <Field name="department" component="select" className="input">
                            <option></option>
                            <option value="APBI">APBI</option>
                            <option value="FNH">FNH</option>
                            <option value="GRS">GRS</option>
                        </Field>

                    For how many courses do you need consession?

                        <Field name="numberOfCourses" component="select" className="input">
                            <option></option>
                            <option value="1">1</option>
                            <option value="2">2</option>
                            <option value="3">3</option>
                            <option value="4">4</option>
                            <option value="5">5</option>
                        </Field>

                    <NumberOfCoursesToDisplay courseNumber={numberOfCoursesValue}/>

My request is based on:

                    <Field name="medicalReason"  component={RenderField} type="checkbox" label="Medical Reason" />
                    <Field name="otherReason" component={RenderField} type="input" label="Other Reason" />

In support of my request, I'm submitting one of the following:

        </form>

    );
};

AcademicConsessionFormView.propTypes = {
    handleSubmit: PropTypes.func.isRequired
};

const selector = formValueSelector('AcademicConsessionFormView');
AcademicConsessionFormView = connect(
    state => {
        const numberOfCoursesValue = selector(state, 'numberOfCourses');
        return {
            numberOfCoursesValue
        };
    }
)(AcademicConsessionFormView);

export default reduxForm({
    form: 'AcademicConsessionFormView',
    destroyOnUnmount: false
})(AcademicConsessionFormView);
<commit_msg>ADDED: conditional field based on checkbox<commit_after>import React, {PropTypes} from 'react';
import {Field, reduxForm, formValueSelector} from 'redux-form';
import { connect } from 'react-redux';
import StringInput from './StringInput';
import EmailInput from './EmailInput';
import StudentNumberInput from './StudentNumberInput';
import NumberOfCoursesToDisplay from './NumberOfCoursesToDisplay';
import RenderField from './RenderField';

let AcademicConsessionFormView = (props) => {
    const { handleSubmit, numberOfCoursesValue, hasMedicalReason } = props;
    return (
        <form onSubmit={handleSubmit}>

Request for Academic Consession

                    <StringInput name="firstName" label="First Name" placeholder="First Name"/>

                    <StringInput name="lastName" label="Last Name" placeholder="Last Name"/>

                    <StudentNumberInput name="studentNumber" label="Student Number" placeholder="Student Number"/>

                    <EmailInput name="email" label="Email" placeholder="Email"/>

                    Year Level

                        <Field name="yearLevel" component="select" className="input">
                            <option></option>
                            <option value="Year 1">Year 1</option>
                            <option value="Year 2">Year 2</option>
                            <option value="Year 3">Year 3</option>
                            <option value="Year 4">Year 4</option>
                        </Field>

                    Department/Program (if applicable)

                        <Field name="department" component="select" className="input">
                            <option></option>
                            <option value="APBI">APBI</option>
                            <option value="FNH">FNH</option>
                            <option value="GRS">GRS</option>
                        </Field>

                    For how many courses do you need consession?

                        <Field name="numberOfCourses" component="select" className="input">
                            <option></option>
                            <option value="1">1</option>
                            <option value="2">2</option>
                            <option value="3">3</option>
                            <option value="4">4</option>
                            <option value="5">5</option>
                        </Field>

                    <NumberOfCoursesToDisplay courseNumber={numberOfCoursesValue}/>

My request is based on:

                    <Field name="medicalReason"  component={RenderField} type="checkbox" label="Medical Reason" />
                    {!hasMedicalReason &&

                        <Field name="otherReason" component={RenderField} type="input" label="Other Reason" />

}

In support of my request, I'm submitting one of the following:

        </form>

    );
};

AcademicConsessionFormView.propTypes = {
    handleSubmit: PropTypes.func.isRequired
};

const selector = formValueSelector('AcademicConsessionFormView');
AcademicConsessionFormView = connect(
    state => {
        const numberOfCoursesValue = selector(state, 'numberOfCourses');
        const hasMedicalReason = selector(state, 'medicalReason');
        return {
            numberOfCoursesValue,
            hasMedicalReason
        };
    }
)(AcademicConsessionFormView);

export default reduxForm({
    form: 'AcademicConsessionFormView',
    destroyOnUnmount: false
})(AcademicConsessionFormView);
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import Phone, { formatPhoneNumber, parsePhoneNumber, isValidPhoneNumber } from 'react-phone-number-input';
import rrui from 'react-phone-number-input/rrui.css';
import rpni from 'react-phone-number-input/style.css';
import { parse, format, asYouType } from 'libphonenumber-js';
import Modal from "./modal";
import serverApi from "../../../utils/quid-server-api";
import web3Api from "../../../utils/web3-common-api";
import sha3 from 'solidity-sha3';
import ksHelper from '../../../utils/keystoreHelper';
import verifiedProxyContractApi from "../../..//utils/verified-proxy-contract-api";
import History from "./History";

export default class Form extends Component {

	constructor(props) {
		console.log("constructor");
		super(props);
		this.state = {
			phone: '1',
			phoneCode: '',
			amount: '',
			code: '',
			confirmPressed: false,
			showModal: false,
			sendingTx: false,
			error: false,
			errorMsg: "",
			phoneIsValid: false,
			historyUpdateCounter: 0 // counter is updated in order to <History /> to fetch new transfer
		};
	}

	greenTick() {
		return (

✓

		)
	}

	generateCode() {
		const random = Math.random().toString(32).slice(5).toUpperCase();
		return random;
	}

	generateWallet(secretCode) {
		const { address, keystoreData } = ksHelper.create(secretCode);
		return { address, ksData: keystoreData };
	}

	closeModal() {
		this.setState({ showModal: false });
	}
	showModal() {
		this.setState({ showModal: true });
	}

	handleSubmit() {
		this.setState({ errorMsg: "", error: false });
		const component = this;
		let transferId;
		if (this.state.phoneIsValid === false) {
			this.setState({ errorMsg: "Invalid phone number", error: true });
			return null;
		}
		if (this.state.amount.length === 0) {
			this.setState({ errorMsg: "Wrong amount", error: true });
			return null;
		}
		const secretCode = this.generateCode();
		const { address, ksData } = this.generateWallet(secretCode);
		component.setState({
			confirmPressed: true,
			code: secretCode,
			showModal: true,
			sendingTx: true,
			errorMsg: ""
		});

		transferId = sha3(component.state.phoneCode + component.state.phone + secretCode);
		console.log({ transferId, phone: component.state.phone, phoneCode: component.state.phoneCode });
		serverApi.sendTransferKeystore(transferId, component.state.phone, component.state.phoneCode, ksData)
			.then(function (result) {
				let errorMsg = "";
				if (!result || !result.success) {
					errorMsg = result.errorMsg || "Server error!";
					throw new Error(errorMsg);
				}

				return verifiedProxyContractApi.deposit(address, component.state.amount, transferId);
			}).then((result) => {
				console.log({ result });
				component.setState({
					sendingTx: false,
					historyUpdateCounter: 1
				});
			}).catch((err) => {
				console.log({ err });
				component.setState({
					sendingTx: false,
					error: true,
					errorMsg: (err.msg || err)
				});
			});
	}

	render() {
		const component = this;

		const ReactTelInput = require('react-telephone-input');
		const form = (

						Phone number

							<Phone
								value={component.state.phone} onChange={phone => {
									const phoneIsValid = isValidPhoneNumber(phone);
									const formatter = new asYouType();
									formatter.input(phone);
									this.setState({ phoneCode: formatter.country_phone_code, phone, phoneIsValid });
								}} />

{this.state.phoneIsValid ? this.greenTick() : ""}

						Amount

							<input className="form-control" type="text" value={component.state.amount} onChange={(event) => component.setState({ amount: event.target.value })} />

{this.state.amount > 0 ? this.greenTick() : ""}

				 component.handleSubmit()}>Send
				 {component.state.errorMsg}

		);
		const txDetails = (

						Phone number

 {component.state.phone}

						Amount

 {component.state.amount}

					 component.showModal()}>Show code

		);

		return (

				{!this.state.confirmPressed ? form : txDetails}

				<History updateCounter={this.state.historyUpdateCounter} />
				<Modal sendingTx={component.state.sendingTx}
					code={component.state.code}
					showModal={component.state.showModal}
					error={component.state.error}
					errorMsg={component.state.errorMsg}
					closeModal={() => component.closeModal()} />

		);

	}
}

<commit_msg>fee calculation<commit_after>import React, { Component } from 'react';
import Phone, { formatPhoneNumber, parsePhoneNumber, isValidPhoneNumber } from 'react-phone-number-input';
import rrui from 'react-phone-number-input/rrui.css';
import rpni from 'react-phone-number-input/style.css';
import { parse, format, asYouType } from 'libphonenumber-js';
import Modal from "./modal";
import serverApi from "../../../utils/quid-server-api";
import web3Api from "../../../utils/web3-common-api";
import sha3 from 'solidity-sha3';
import ksHelper from '../../../utils/keystoreHelper';
import verifiedProxyContractApi from "../../..//utils/verified-proxy-contract-api";
import History from "./History";

export default class Form extends Component {

	constructor(props) {
		console.log("constructor");
		super(props);
		this.state = {
			phone: '1',
			phoneCode: '',
			amount: '',
			code: '',
			confirmPressed: false,
			showModal: false,
			sendingTx: false,
			error: false,
			errorMsg: "",
			phoneIsValid: false,
			historyUpdateCounter: 0 // counter is updated in order to <History /> to fetch new transfer
		};
	}

	greenTick() {
		return (

✓

		)
	}

	generateCode() {
		const random = Math.random().toString(32).slice(5).toUpperCase();
		return random;
	}

	generateWallet(secretCode) {
		const { address, keystoreData } = ksHelper.create(secretCode);
		return { address, ksData: keystoreData };
	}

	closeModal() {
		this.setState({ showModal: false });
	}
	showModal() {
		this.setState({ showModal: true });
	}

	handleSubmit() {
		this.setState({ errorMsg: "", error: false });
		const component = this;
		let transferId;
		if (this.state.phoneIsValid === false) {
			this.setState({ errorMsg: "Invalid phone number", error: true });
			return null;
		}
		if (this.state.amount.length === 0) {
			this.setState({ errorMsg: "Wrong amount", error: true });
			return null;
		}
		const secretCode = this.generateCode();
		const { address, ksData } = this.generateWallet(secretCode);
		component.setState({
			confirmPressed: true,
			code: secretCode,
			showModal: true,
			sendingTx: true,
			errorMsg: ""
		});

		transferId = sha3(component.state.phoneCode + component.state.phone + secretCode);
		console.log({ transferId, phone: component.state.phone, phoneCode: component.state.phoneCode });
		serverApi.sendTransferKeystore(transferId, component.state.phone, component.state.phoneCode, ksData)
			.then(function (result) {
				let errorMsg = "";
				if (!result || !result.success) {
					errorMsg = result.errorMsg || "Server error!";
					throw new Error(errorMsg);
				}

				return verifiedProxyContractApi.deposit(address, component.state.amount, transferId);
			}).then((result) => {
				console.log({ result });
				component.setState({
					sendingTx: false,
					historyUpdateCounter: 1
				});
			}).catch((err) => {
				console.log({ err });
				component.setState({
					sendingTx: false,
					error: true,
					errorMsg: (err.msg || err)
				});
			});
	}

	render() {
		const component = this;

		const ReactTelInput = require('react-telephone-input');
		const form = (

						Phone number

							<Phone
								value={component.state.phone} onChange={phone => {
									const phoneIsValid = isValidPhoneNumber(phone);
									const formatter = new asYouType();
									formatter.input(phone);
									this.setState({ phoneCode: formatter.country_phone_code, phone, phoneIsValid });
								}} />

{this.state.phoneIsValid ? this.greenTick() : ""}

							Amount to send, eth.

									<input className="form-control" type="text" value={component.state.amount} onChange={(event) => component.setState({ amount: event.target.value })} />

{this.state.amount > 0 ? this.greenTick() : ""}

							Amount to pay*, eth.

									<input className="form-control" style={{background: "white"}} value={this.state.amount > 0 ? parseFloat(this.state.amount) + 0.01 : ""} disabled type="text" onChange={(event) => component.setState({ amount: event.target.value })} />

*The amount to be withdrawn including fixed commission of 0.01 ether. We only charge it to cover the SMS provider expense and server maintenance, plus a two cheap meals a day for our small team to keep going.

				 component.handleSubmit()}>Send
				 {component.state.errorMsg}

		);
		const txDetails = (

						Phone number

 {component.state.phone}

						Amount

 {component.state.amount}

					 component.showModal()}>Show code

		);

		return (

				{!this.state.confirmPressed ? form : txDetails}

				<History updateCounter={this.state.historyUpdateCounter} />
				<Modal sendingTx={component.state.sendingTx}
					code={component.state.code}
					showModal={component.state.showModal}
					error={component.state.error}
					errorMsg={component.state.errorMsg}
					closeModal={() => component.closeModal()} />

		);

	}
}

<|endoftext|>