<commit_before>'use strict';

const BaseManager = require('./BaseManager');
const { Error, TypeError, RangeError } = require('../errors');
const GuildMember = require('../structures/GuildMember');
const Collection = require('../util/Collection');
const { Events, OPCodes } = require('../util/Constants');

/**
 * Manages API methods for GuildMembers and stores their cache.
 * @extends {BaseManager}
 */
class GuildMemberManager extends BaseManager {
  constructor(guild, iterable) {
    super(guild.client, iterable, GuildMember);
    /**
     * The guild this manager belongs to
     * @type {Guild}
     */
    this.guild = guild;
  }

  /**
   * The cache of this Manager
   * @type {Collection<Snowflake, GuildMember>}
   * @name GuildMemberManager#cache
   */

  add(data, cache = true) {
    return super.add(data, cache, { id: data.user.id, extras: [this.guild] });
  }

  /**
   * Data that resolves to give a GuildMember object. This can be:
   * * A GuildMember object
   * * A User resolvable
   * @typedef {GuildMember|UserResolvable} GuildMemberResolvable
   */

  /**
   * Resolves a GuildMemberResolvable to a GuildMember object.
   * @param {GuildMemberResolvable} member The user that is part of the guild
   * @returns {?GuildMember}
   */
  resolve(member) {
    const memberResolvable = super.resolve(member);
    if (memberResolvable) return memberResolvable;
    const userResolvable = this.client.users.resolveID(member);
    if (userResolvable) return super.resolve(userResolvable);
    return null;
  }

  /**
   * Resolves a GuildMemberResolvable to a member ID string.
   * @param {GuildMemberResolvable} member The user that is part of the guild
   * @returns {?Snowflake}
   */
  resolveID(member) {
    const memberResolvable = super.resolveID(member);
    if (memberResolvable) return memberResolvable;
    const userResolvable = this.client.users.resolveID(member);
    return this.cache.has(userResolvable) ? userResolvable : null;
  }

  /**
   * Options used to fetch a single member from a guild.
   * @typedef {Object} FetchMemberOptions
   * @property {UserResolvable} user The user to fetch
   * @property {boolean} [cache=true] Whether or not to cache the fetched member
   */

  /**
   * Options used to fetch multiple members from a guild.
   * @typedef {Object} FetchMembersOptions
   * @property {UserResolvable|UserResolvable[]} user The user(s) to fetch
   * @property {?string} query Limit fetch to members with similar usernames
   * @property {number} [limit=0] Maximum number of members to request
   * @property {boolean} [withPresences=false] Whether or not to include the presences
   * @property {number} [time=120e3] Timeout for receipt of members
   * @property {?string} nonce Nonce for this request (32 characters max - default to base 16 now timestamp)
   */

  /**
   * Fetches member(s) from Discord, even if they're offline.
   * @param {UserResolvable|FetchMemberOptions|FetchMembersOptions} [options] If a UserResolvable, the user to fetch.
   * If undefined, fetches all members.
   * If a query, it limits the results to users with similar usernames.
   * @returns {Promise<GuildMember>|Promise<Collection<Snowflake, GuildMember>>}
   * @example
   * // Fetch all members from a guild
   * guild.members.fetch()
   *   .then(console.log)
   *   .catch(console.error);
   * @example
   * // Fetch a single member
   * guild.members.fetch('66564597481480192')
   *   .then(console.log)
   *   .catch(console.error);
   * @example
   * // Fetch a single member without caching
   * guild.members.fetch({ user, cache: false })
   *   .then(console.log)
   *   .catch(console.error);
   * @example
   * // Fetch by an array of users including their presences
   * guild.members.fetch({ user: ['66564597481480192', '191615925336670208'], withPresences: true })
   *   .then(console.log)
   *   .catch(console.error);
   * @example
   * // Fetch by query
   * guild.members.fetch({ query: 'hydra', limit: 1 })
   *   .then(console.log)
   *   .catch(console.error);
   */
  fetch(options) {
    if (!options) return this._fetchMany();
    const user = this.client.users.resolveID(options);
    if (user) return this._fetchSingle({ user, cache: true });
    if (options.user) {
      if (Array.isArray(options.user)) {
        options.user = options.user.map(u => this.client.users.resolveID(u));
        return this._fetchMany(options);
      } else {
        options.user = this.client.users.resolveID(options.user);
      }
      if (!options.limit && !options.withPresences) return this._fetchSingle(options);
    }
    return this._fetchMany(options);
  }

  /**
   * Prunes members from the guild based on how long they have been inactive.
   * <info>It's recommended to set options.count to `false` for large guilds.</info>
   * @param {Object} [options] Prune options
   * @param {number} [options.days=7] Number of days of inactivity required to kick
   * @param {boolean} [options.dry=false] Get number of users that will be kicked, without actually kicking them
   * @param {boolean} [options.count=true] Whether or not to return the number of users that have been kicked.
   * @param {string} [options.reason] Reason for this prune
   * @returns {Promise<number|null>} The number of members that were/will be kicked
   * @example
   * // See how many members will be pruned
   * guild.members.prune({ dry: true })
   *   .then(pruned => console.log(`This will prune ${pruned} people!`))
   *   .catch(console.error);
   * @example
   * // Actually prune the members
   * guild.members.prune({ days: 1, reason: 'too many people!' })
   *   .then(pruned => console.log(`I just pruned ${pruned} people!`))
   *   .catch(console.error);
   */
  prune({ days = 7, dry = false, count = true, reason } = {}) {
    if (typeof days !== 'number') throw new TypeError('PRUNE_DAYS_TYPE');
    return this.client.api
      .guilds(this.guild.id)
      .prune[dry ? 'get' : 'post']({
        query: {
          days,
          compute_prune_count: count,
        },
        reason,
      })
      .then(data => data.pruned);
  }

  /**
   * Bans a user from the guild.
   * @param {UserResolvable} user The user to ban
   * @param {Object} [options] Options for the ban
   * @param {number} [options.days=0] Number of days of messages to delete
   * @param {string} [options.reason] Reason for banning
   * @returns {Promise<GuildMember|User|Snowflake>} Result object will be resolved as specifically as possible.
   * If the GuildMember cannot be resolved, the User will instead be attempted to be resolved. If that also cannot
   * be resolved, the user ID will be the result.
   * @example
   * // Ban a user by ID (or with a user/guild member object)
   * guild.members.ban('84484653687267328')
   *   .then(user => console.log(`Banned ${user.username || user.id || user} from ${guild.name}`))
   *   .catch(console.error);
   */
  ban(user, options = { days: 0 }) {
    if (options.days) options['delete-message-days'] = options.days;
    const id = this.client.users.resolveID(user);
    if (!id) return Promise.reject(new Error('BAN_RESOLVE_ID', true));
    return this.client.api
      .guilds(this.guild.id)
      .bans[id].put({ query: options })
      .then(() => {
        if (user instanceof GuildMember) return user;
        const _user = this.client.users.resolve(id);
        if (_user) {
          const member = this.resolve(_user);
          return member || _user;
        }
        return id;
      });
  }

  /**
   * Unbans a user from the guild.
   * @param {UserResolvable} user The user to unban
   * @param {string} [reason] Reason for unbanning user
   * @returns {Promise<User>}
   * @example
   * // Unban a user by ID (or with a user/guild member object)
   * guild.members.unban('84484653687267328')
   *   .then(user => console.log(`Unbanned ${user.username} from ${guild.name}`))
   *   .catch(console.error);
   */
  unban(user, reason) {
    const id = this.client.users.resolveID(user);
    if (!id) return Promise.reject(new Error('BAN_RESOLVE_ID'));
    return this.client.api
      .guilds(this.guild.id)
      .bans[id].delete({ reason })
      .then(() => this.client.users.resolve(user));
  }

  _fetchSingle({ user, cache }) {
    const existing = this.cache.get(user);
    if (existing && !existing.partial) return Promise.resolve(existing);
    return this.client.api
      .guilds(this.guild.id)
      .members(user)
      .get()
      .then(data => this.add(data, cache));
  }

  _fetchMany({
    limit = 0,
    withPresences: presences = false,
    user: user_ids,
    query,
    time = 120e3,
    nonce = Date.now().toString(16),
  } = {}) {
    return new Promise((resolve, reject) => {
      if (this.guild.memberCount === this.cache.size && !query && !limit && !presences && !user_ids) {
        resolve(this.cache);
        return;
      }
      if (!query && !user_ids) query = '';
      if (nonce.length > 32) throw new RangeError('MEMBER_FETCH_NONCE_LENGTH');
      this.guild.shard.send({
        op: OPCodes.REQUEST_GUILD_MEMBERS,
        d: {
          guild_id: this.guild.id,
          presences,
          user_ids,
          query,
          nonce,
          limit,
        },
      });
      const fetchedMembers = new Collection();
      const option = query || limit || presences || user_ids;
      let i = 0;
      const handler = (members, _, chunk) => {
        timeout.refresh();
        if (chunk.nonce !== nonce) return;
        i++;
        for (const member of members.values()) {
          if (option) fetchedMembers.set(member.id, member);
        }
        if (
          this.guild.memberCount <= this.cache.size ||
          (option && members.size < 1000) ||
          (limit && fetchedMembers.size >= limit) ||
          i === chunk.count
        ) {
          this.client.clearTimeout(timeout);
          this.client.removeListener(Events.GUILD_MEMBERS_CHUNK, handler);
          this.client.decrementMaxListeners();
          let fetched = option ? fetchedMembers : this.cache;
          if (user_ids && !Array.isArray(user_ids) && fetched.size) fetched = fetched.first();
          resolve(fetched);
        }
      };
      const timeout = this.client.setTimeout(() => {
        this.client.removeListener(Events.GUILD_MEMBERS_CHUNK, handler);
        this.client.decrementMaxListeners();
        reject(new Error('GUILD_MEMBERS_TIMEOUT'));
      }, time);
      this.client.incrementMaxListeners();
      this.client.on(Events.GUILD_MEMBERS_CHUNK, handler);
    });
  }
}

module.exports = GuildMemberManager;
<commit_msg>refactor(GuildMemberManager): use data instead of query (#4370)<commit_after>'use strict';

const BaseManager = require('./BaseManager');
const { Error, TypeError, RangeError } = require('../errors');
const GuildMember = require('../structures/GuildMember');
const Collection = require('../util/Collection');
const { Events, OPCodes } = require('../util/Constants');

/**
 * Manages API methods for GuildMembers and stores their cache.
 * @extends {BaseManager}
 */
class GuildMemberManager extends BaseManager {
  constructor(guild, iterable) {
    super(guild.client, iterable, GuildMember);
    /**
     * The guild this manager belongs to
     * @type {Guild}
     */
    this.guild = guild;
  }

  /**
   * The cache of this Manager
   * @type {Collection<Snowflake, GuildMember>}
   * @name GuildMemberManager#cache
   */

  add(data, cache = true) {
    return super.add(data, cache, { id: data.user.id, extras: [this.guild] });
  }

  /**
   * Data that resolves to give a GuildMember object. This can be:
   * * A GuildMember object
   * * A User resolvable
   * @typedef {GuildMember|UserResolvable} GuildMemberResolvable
   */

  /**
   * Resolves a GuildMemberResolvable to a GuildMember object.
   * @param {GuildMemberResolvable} member The user that is part of the guild
   * @returns {?GuildMember}
   */
  resolve(member) {
    const memberResolvable = super.resolve(member);
    if (memberResolvable) return memberResolvable;
    const userResolvable = this.client.users.resolveID(member);
    if (userResolvable) return super.resolve(userResolvable);
    return null;
  }

  /**
   * Resolves a GuildMemberResolvable to a member ID string.
   * @param {GuildMemberResolvable} member The user that is part of the guild
   * @returns {?Snowflake}
   */
  resolveID(member) {
    const memberResolvable = super.resolveID(member);
    if (memberResolvable) return memberResolvable;
    const userResolvable = this.client.users.resolveID(member);
    return this.cache.has(userResolvable) ? userResolvable : null;
  }

  /**
   * Options used to fetch a single member from a guild.
   * @typedef {Object} FetchMemberOptions
   * @property {UserResolvable} user The user to fetch
   * @property {boolean} [cache=true] Whether or not to cache the fetched member
   */

  /**
   * Options used to fetch multiple members from a guild.
   * @typedef {Object} FetchMembersOptions
   * @property {UserResolvable|UserResolvable[]} user The user(s) to fetch
   * @property {?string} query Limit fetch to members with similar usernames
   * @property {number} [limit=0] Maximum number of members to request
   * @property {boolean} [withPresences=false] Whether or not to include the presences
   * @property {number} [time=120e3] Timeout for receipt of members
   * @property {?string} nonce Nonce for this request (32 characters max - default to base 16 now timestamp)
   */

  /**
   * Fetches member(s) from Discord, even if they're offline.
   * @param {UserResolvable|FetchMemberOptions|FetchMembersOptions} [options] If a UserResolvable, the user to fetch.
   * If undefined, fetches all members.
   * If a query, it limits the results to users with similar usernames.
   * @returns {Promise<GuildMember>|Promise<Collection<Snowflake, GuildMember>>}
   * @example
   * // Fetch all members from a guild
   * guild.members.fetch()
   *   .then(console.log)
   *   .catch(console.error);
   * @example
   * // Fetch a single member
   * guild.members.fetch('66564597481480192')
   *   .then(console.log)
   *   .catch(console.error);
   * @example
   * // Fetch a single member without caching
   * guild.members.fetch({ user, cache: false })
   *   .then(console.log)
   *   .catch(console.error);
   * @example
   * // Fetch by an array of users including their presences
   * guild.members.fetch({ user: ['66564597481480192', '191615925336670208'], withPresences: true })
   *   .then(console.log)
   *   .catch(console.error);
   * @example
   * // Fetch by query
   * guild.members.fetch({ query: 'hydra', limit: 1 })
   *   .then(console.log)
   *   .catch(console.error);
   */
  fetch(options) {
    if (!options) return this._fetchMany();
    const user = this.client.users.resolveID(options);
    if (user) return this._fetchSingle({ user, cache: true });
    if (options.user) {
      if (Array.isArray(options.user)) {
        options.user = options.user.map(u => this.client.users.resolveID(u));
        return this._fetchMany(options);
      } else {
        options.user = this.client.users.resolveID(options.user);
      }
      if (!options.limit && !options.withPresences) return this._fetchSingle(options);
    }
    return this._fetchMany(options);
  }

  /**
   * Prunes members from the guild based on how long they have been inactive.
   * <info>It's recommended to set options.count to `false` for large guilds.</info>
   * @param {Object} [options] Prune options
   * @param {number} [options.days=7] Number of days of inactivity required to kick
   * @param {boolean} [options.dry=false] Get number of users that will be kicked, without actually kicking them
   * @param {boolean} [options.count=true] Whether or not to return the number of users that have been kicked.
   * @param {string} [options.reason] Reason for this prune
   * @returns {Promise<number|null>} The number of members that were/will be kicked
   * @example
   * // See how many members will be pruned
   * guild.members.prune({ dry: true })
   *   .then(pruned => console.log(`This will prune ${pruned} people!`))
   *   .catch(console.error);
   * @example
   * // Actually prune the members
   * guild.members.prune({ days: 1, reason: 'too many people!' })
   *   .then(pruned => console.log(`I just pruned ${pruned} people!`))
   *   .catch(console.error);
   */
  prune({ days = 7, dry = false, count = true, reason } = {}) {
    if (typeof days !== 'number') throw new TypeError('PRUNE_DAYS_TYPE');
    return this.client.api
      .guilds(this.guild.id)
      .prune[dry ? 'get' : 'post']({
        query: {
          days,
          compute_prune_count: count,
        },
        reason,
      })
      .then(data => data.pruned);
  }

  /**
   * Bans a user from the guild.
   * @param {UserResolvable} user The user to ban
   * @param {Object} [options] Options for the ban
   * @param {number} [options.days=0] Number of days of messages to delete
   * @param {string} [options.reason] Reason for banning
   * @returns {Promise<GuildMember|User|Snowflake>} Result object will be resolved as specifically as possible.
   * If the GuildMember cannot be resolved, the User will instead be attempted to be resolved. If that also cannot
   * be resolved, the user ID will be the result.
   * @example
   * // Ban a user by ID (or with a user/guild member object)
   * guild.members.ban('84484653687267328')
   *   .then(user => console.log(`Banned ${user.username || user.id || user} from ${guild.name}`))
   *   .catch(console.error);
   */
  ban(user, options = { days: 0 }) {
    if (options.days) options.delete_message_days = options.days;
    const id = this.client.users.resolveID(user);
    if (!id) return Promise.reject(new Error('BAN_RESOLVE_ID', true));
    return this.client.api
      .guilds(this.guild.id)
      .bans[id].put({ data: options })
      .then(() => {
        if (user instanceof GuildMember) return user;
        const _user = this.client.users.resolve(id);
        if (_user) {
          const member = this.resolve(_user);
          return member || _user;
        }
        return id;
      });
  }

  /**
   * Unbans a user from the guild.
   * @param {UserResolvable} user The user to unban
   * @param {string} [reason] Reason for unbanning user
   * @returns {Promise<User>}
   * @example
   * // Unban a user by ID (or with a user/guild member object)
   * guild.members.unban('84484653687267328')
   *   .then(user => console.log(`Unbanned ${user.username} from ${guild.name}`))
   *   .catch(console.error);
   */
  unban(user, reason) {
    const id = this.client.users.resolveID(user);
    if (!id) return Promise.reject(new Error('BAN_RESOLVE_ID'));
    return this.client.api
      .guilds(this.guild.id)
      .bans[id].delete({ reason })
      .then(() => this.client.users.resolve(user));
  }

  _fetchSingle({ user, cache }) {
    const existing = this.cache.get(user);
    if (existing && !existing.partial) return Promise.resolve(existing);
    return this.client.api
      .guilds(this.guild.id)
      .members(user)
      .get()
      .then(data => this.add(data, cache));
  }

  _fetchMany({
    limit = 0,
    withPresences: presences = false,
    user: user_ids,
    query,
    time = 120e3,
    nonce = Date.now().toString(16),
  } = {}) {
    return new Promise((resolve, reject) => {
      if (this.guild.memberCount === this.cache.size && !query && !limit && !presences && !user_ids) {
        resolve(this.cache);
        return;
      }
      if (!query && !user_ids) query = '';
      if (nonce.length > 32) throw new RangeError('MEMBER_FETCH_NONCE_LENGTH');
      this.guild.shard.send({
        op: OPCodes.REQUEST_GUILD_MEMBERS,
        d: {
          guild_id: this.guild.id,
          presences,
          user_ids,
          query,
          nonce,
          limit,
        },
      });
      const fetchedMembers = new Collection();
      const option = query || limit || presences || user_ids;
      let i = 0;
      const handler = (members, _, chunk) => {
        timeout.refresh();
        if (chunk.nonce !== nonce) return;
        i++;
        for (const member of members.values()) {
          if (option) fetchedMembers.set(member.id, member);
        }
        if (
          this.guild.memberCount <= this.cache.size ||
          (option && members.size < 1000) ||
          (limit && fetchedMembers.size >= limit) ||
          i === chunk.count
        ) {
          this.client.clearTimeout(timeout);
          this.client.removeListener(Events.GUILD_MEMBERS_CHUNK, handler);
          this.client.decrementMaxListeners();
          let fetched = option ? fetchedMembers : this.cache;
          if (user_ids && !Array.isArray(user_ids) && fetched.size) fetched = fetched.first();
          resolve(fetched);
        }
      };
      const timeout = this.client.setTimeout(() => {
        this.client.removeListener(Events.GUILD_MEMBERS_CHUNK, handler);
        this.client.decrementMaxListeners();
        reject(new Error('GUILD_MEMBERS_TIMEOUT'));
      }, time);
      this.client.incrementMaxListeners();
      this.client.on(Events.GUILD_MEMBERS_CHUNK, handler);
    });
  }
}

module.exports = GuildMemberManager;
<|endoftext|>


<commit_before>import React, { PropTypes } from 'react';
import ModalEditor from '../../components/Editor/ModalEditor';
import {Form, Label, Select, Header, Checkbox} from '../../ui-components';
import {buildOptions, getDomainID, getEnterpriseID, getMetaDataAttribute} from './utils';
import { TwoColumnRow } from '../components';

import {
    getNetworkProtocolForText,
    getNetworkTypeOptions,
    getNetworkTypeForValue,
    getSecurityPolicyActionsForValue, NetworkProtocols,
} from './NetworkData';

import {
    fetchAssociatedObjectIfNeeded,
    fetchDestinationNetworkItems,
    fetchSourceNetworkItems,
    getSourceData,
    getDestinationNetworkItems,
    getNetworkItems,
    getSourceNetworkItems,
    NetworkObjectTypes,
    showMessageBoxOnNoFlow
} from './actions';

const getEntityNameForID = (ID, entityCollection) => {
    const entities = entityCollection && entityCollection.options && entityCollection.options.length > 0 ? entityCollection.options.find(item => item.value === ID) : null;
    return entities && entities.length > 0 ? entities[0] :
        entities && Object.getOwnPropertyNames(entities).length > 0 ? entities : null;
}

const buildVFRuleOptions = (options, srcEntity, destEntity) => {
    if (options && options.data && options.data.length > 0) {
        return options.data.map(item => {
            const desc = item.description ? `${item.description}: ` : '';
            const locationType = getNetworkTypeForValue(item.locationType);
            const networkType = getNetworkTypeForValue(item.networkType);
            let action = getSecurityPolicyActionsForValue(item.action);
            action = action && action.length > 0 ? action[0].text : '';
            const srcEntityName = srcEntity && srcEntity.value === item.locationID ? srcEntity.text : null;
            const destEntityName = destEntity && destEntity.value === item.networkID ? destEntity.text : null;
            const from = srcEntityName ? `${locationType[0].text} ${srcEntityName}` : locationType[0].text;
            const to = destEntityName ? `${networkType[0].text} ${destEntityName}` : networkType[0].text;
            const priority = item.priority;
            const templateName = item.ACLTemplateName ? `Policy: ${item.ACLTemplateName}: ` : "";
            const destPort = item.destinationPort ? `D-Port: ${item.destinationPort}` : '';
            const text = `${templateName}${desc}${destPort}: From ${from} To: ${to} Action: ${action} Priority: ${priority}`;
            return ({ text, value: item.ID });
        });
    }
    if (options && options.isFetching) {
        return "Fetching...";
    }
    if (options && options.error) {
        return options.error;
    }
    return "No Data available";
}

class AddToFlowEditor extends React.Component {
    constructor(...props) {
        super(...props);

        this.state = {
            opened: false,
            error: false,
        }
        this.toggleError = this.toggleError.bind(this);
        this.putConfiguration = this.putConfiguration.bind(this);
        this.resetFieldsOnChange = this.resetFieldsOnChange.bind(this);
    }

    componentWillMount() {
        this.setState({opened: true, formName: 'add-flow-editor', error: true});
    }

    handleChangeProtocol = (evt, value) => {
        const { preventDefault, ...values} = evt;
        const protocol = values ? Object.values(values).join('') : null;
        if (protocol === '1') {
            const { changeFieldValue, formName } = this.props;
            if (changeFieldValue) {
                const ICMPCode = getMetaDataAttribute(this.props.data, 'ICMPCode');
                const ICMPType = getMetaDataAttribute(this.props.data, 'ICMPType');
                changeFieldValue(formName, 'ICMPCode', ICMPCode);
                changeFieldValue(formName, 'ICMPType', ICMPType);
            }
        }
    }

    handleSelectRule = (evt) => {
        const { preventDefault, ...values } = evt;
        const ID = values ? Object.values(values).join('') : null;
        if (ID) {
            const vfrules = getNetworkItems(NetworkObjectTypes.VIRTUAL_FIREWALL_RULE, this.props);
            if (vfrules && vfrules.data && vfrules.data.length > 0) {
                const { selectRule, data } = this.props;
                const destData = getSourceData(this.props);
                const selectedRule = vfrules.data.find(item => item.ID === ID);
                if (selectRule && data) {
                    const object = Object.assign({}, selectedRule);
                    var re = new RegExp(data.destinationport, 'gi');
                    object.destinationPort = object.destinationPort === '*' || object.destinationPort.match(re) ? object.destinationPort :
                        `${object.destinationPort},${destData.destinationport}`;
                    selectRule(ID, object);
                }
            }
        }
    }

    buildSourceField = (source) => {
        let srcList = null;
        if (source && !(source.type === 'ANY' || source.type === 'UNDERLAY_INTERNET_POLICYGROUP')) {
            const srcOptions = buildOptions(source);
            srcList = {
                name: 'locationID',
                component: Select,
            };
            if (srcOptions && Array.isArray(srcOptions)) {
                srcList.options = srcOptions;
            }
            else {
                srcList.text = srcOptions;
            }
        }

        return srcList;
    }

    buildDestField = (destination) => {
        let destList = null;
        if (destination && !(destination.type === 'ANY' || destination.type === 'UNDERLAY_INTERNET_POLICYGROUP')) {
            const destOptions = buildOptions(destination);
            destList = {
                name: 'networkID',
                component: Select,
            };
            if (destOptions && Array.isArray(destOptions)) {
                destList.options = destOptions;
            }
            else {
                destList.text = destOptions;
            }
        }
        return destList;
    }

    validate = (values) => {
        const { ID, protocol, ICMPCode, ICMPType } = values;

        if (!ID) {
            return { ID: "Please select a valid virtual firewall rule"};
        }

        if (protocol === '1' && !(ICMPCode || ICMPType)) {
            return { protocol: "ICMP Require valid ICMP code and type for the flow"};
        }
        return {};
    }

    shouldComponentUpdate(nextProps, nextState) {
        return nextProps !== this.props || this.state !== nextState;
    }

    fetchVFRulesIfNeeded = (props) => {
        const {
            data,
            locationTypeValue,
            locationIDValue,
            networkTypeValue,
            networkIDValue,
            operation,
            resourceName,
        } = props;
        if (!data || Object.getOwnPropertyNames(data).length <= 0
            || operation !== 'add') {
            return;
        }
        const vfRules = getNetworkItems(NetworkObjectTypes.VIRTUAL_FIREWALL_RULE, this.props);
        if (vfRules && vfRules.data && Object.getOwnPropertyNames(vfRules.data).length > 0)
            return;
         if (!locationTypeValue || (locationTypeValue !== 'ANY' && locationTypeValue !== 'UNDERLAY_INTERNET_POLICYGROUP' && !locationIDValue)) {
             return;
         }
         if (!networkTypeValue || (networkTypeValue !== 'ANY' && networkTypeValue !== 'UNDERLAY_INTERNET_POLICYGROUP' && !networkIDValue)) {
            return;
        }
        const proto = getNetworkProtocolForText(data.protocol);

        const enterpriseID = getEnterpriseID(props);
        const domainID = getDomainID(resourceName, data);

        fetchAssociatedObjectIfNeeded({
            type: NetworkObjectTypes.VIRTUAL_FIREWALL_RULE,
            args: {
                locationType: locationTypeValue,
                locationID: locationIDValue,
                networkType: networkTypeValue,
                networkID: networkIDValue,
                protocol: proto ? proto : '6',
            }, domainID, enterpriseID, ...props});
    }

    componentWillReceiveProps(nextProps) {
        const {
            data,
            locationTypeValue,
            locationIDValue,
            networkTypeValue,
            networkIDValue,
            resourceName,
        } = nextProps;

        if (!data || Object.getOwnPropertyNames(data).length <= 0) {
            return;
        }

        const enterpriseID = getEnterpriseID(nextProps);
        const domainID = getDomainID(resourceName, data);

        const srcNetworkItems = {
            ...getSourceNetworkItems(nextProps),
            type: locationTypeValue,
            ID: locationIDValue,
        };
        const destNetworkItems = {
            ...getDestinationNetworkItems(nextProps),
            type: networkTypeValue,
            ID: networkIDValue,
        };
        if (!srcNetworkItems.data) {
            fetchSourceNetworkItems(nextProps, domainID, enterpriseID);
        }
        if (!destNetworkItems.data) {
            fetchDestinationNetworkItems(nextProps, domainID, enterpriseID);
        }

        this.fetchVFRulesIfNeeded(nextProps);
        const vfrules = getNetworkItems(NetworkObjectTypes.VIRTUAL_FIREWALL_RULE, nextProps);
        if (vfrules && vfrules.data && vfrules.data.length > 0) {
            this.toggleError(false);
        }
        else {
            this.toggleError(true);
        }
    }

    resetFieldsOnChange = (value, ...fields) => {
        const { changeFieldValue } = this.props;
        const { formName } = this.state;
        if (value) {
            if (fields) {
                fields.forEach(fieldName => changeFieldValue(formName, fieldName, null))
            }
        }
    }

    toggleError = (flag) => {
        if (this.state.error !== flag ) {
            this.setState({ error: flag});
        }
    }

    buildVFRuleField = (srcEntity, destEntity) => {
        const vfrules = getNetworkItems(NetworkObjectTypes.VIRTUAL_FIREWALL_RULE, this.props);
        const vfRuleOptions = buildVFRuleOptions(vfrules, srcEntity, destEntity);
        if (vfRuleOptions && Array.isArray(vfRuleOptions)) {
            return (
                <Form.Field
                    name="ID"
                    component={Select}
                    options={vfRuleOptions}
                    onChange={this.handleSelectRule}
                />
            );
        }
        else {
            return ({vfRuleOptions});
        }

    }

    handleCancel = () => {
        // first dispatch a reset of the selection
        const { resetSelectedFlow, IDValue } = this.props;
        resetSelectedFlow(IDValue);
        this.props.handleClose();
    }

    handleDone = () => {
        // first dispatch a reset of the selection
        const { resetSelectedFlow, query: { id }, IDValue } = this.props;
        resetSelectedFlow(id);
        resetSelectedFlow(IDValue);
        this.props.handleClose();
    }

    putConfiguration = () => {
        const { IDValue } = this.props;
        return {
            service: "VSD",
            query: {
                parentResource: "virtualfirewallrules",
                parentID: IDValue,
            }
        }
    }

    shouldDisplayPort = () => {
        const {
            data,
        } = this.props;

        const protocol = data && data.protocol ? data.protocol : '';
        const proto = getNetworkProtocolForText(protocol);
        return (proto === '6' || proto === '17');

    }

    renderAdd = () => {
        const {
            data,
            locationTypeValue,
            networkTypeValue,
            locationIDValue,
            networkIDValue,
            resourceName,
        } = this.props;

        const title = "Add to Firewall Rule";
        const buttonLabel = "Add";
        const protocol = data && data.protocol ? data.protocol : '';
        const destData = getSourceData(this.props);
        const dPort = destData && destData.destinationport ? destData.destinationport : '';
        const ICMPCode = protocol == '1' ? data && data.ICMPCode : undefined;
        const ICMPType = protocol == '1' ? data && data.ICMPType : undefined;
        const srcNetworkItems = {
            ...getSourceNetworkItems(this.props),
            type: locationTypeValue,
            ID: locationIDValue,
        };
        const destNetworkItems = {
            ...getDestinationNetworkItems(this.props),
            type: networkTypeValue,
            ID: networkIDValue,
        };
        const srcList = this.buildSourceField(srcNetworkItems);

        const destList = this.buildDestField(destNetworkItems);
        const srcEntity = locationIDValue && srcList ? getEntityNameForID(locationIDValue, srcList) : null;
        const destEntity = networkIDValue && destList ? getEntityNameForID(networkIDValue, destList) : null;
        const networkDestinations = getNetworkTypeOptions(resourceName);
        const isDestPortEnabled = this.shouldDisplayPort();
        return (
            <ModalEditor
                title={title}
                submitLabel={buttonLabel}
                open={this.state.opened}
                name={this.state.formName}
                onCancel={this.handleCancel}
                onDone={this.handleDone}
                width='60%'
                onValidate={this.validate}
                configuration={this.putConfiguration}
                errored={this.state.error}
                getInitialValues={() => ({stateful: true})}
            >
                Match Criteria
                <TwoColumnRow
                    secondColumnProps={{
                        name: 'stateful',
                        label: 'Stateful entry',
                        component: Checkbox,
                        hideLabel: true,
                    }}
                />
                <TwoColumnRow firstColumnProps={{
                    name: 'locationType',
                    label: 'Source',
                    component: Select,
                    options: networkDestinations,
                    onChange:(e) => this.resetFieldsOnChange(e, 'locationID')
                }} secondColumnProps={{
                    name: 'networkType',
                    label: 'Destination',
                    component: Select,
                    options: networkDestinations,
                    onChange:(e) => this.resetFieldsOnChange(e, 'networkID')
                }} />
                { (srcList || destList) &&  <TwoColumnRow firstColumnProps={srcList} secondColumnProps={destList} /> }

                Virtual Firewall Rules
                {
                    (locationTypeValue && networkTypeValue && this.buildVFRuleField(srcEntity, destEntity)) || Select a source and destination type
                }
                { isDestPortEnabled &&
                    <TwoColumnRow firstColumnProps={{
                        name: 'protocol',
                        label: 'Protocol',
                        text: protocol,
                        onChange: (value) => this.handleChangeProtocol(value)
                    }} secondColumnProps={{
                        name: 'dPort',
                        label: 'Destination Port',
                        text: dPort,
                    }}/>
                }
                { !isDestPortEnabled &&
                    <TwoColumnRow firstColumnProps={{
                        name: 'protocol',
                        label: 'Protocol',
                        text: protocol,
                    }}/>
                }
                {
                    ICMPType && ICMPCode &&
                        <TwoColumnRow
                            firstColumnProps={{
                                name: 'ICMPCode',
                                label: 'ICMP Code',
                                text: ICMPCode
                            }}
                            secondColumnProps={{
                                name: 'ICMPType',
                                label: 'ICMP Type',
                                text: ICMPType
                            }}
                        />
                }
            </ModalEditor>
        );
    }

    renderError = () => {
        const title = "Add to Firewall Rule";
        const buttonLabel = "Add";
        const errorMsg = !this.shouldDisplayPort() ? "Nothing to add to rules" : 'No Flow Selected';

        return(
            <ModalEditor
                title={title}
                submitLabel={buttonLabel}
                open={this.state.opened}
                name={this.state.formName}
                onCancel={this.props.handleClose}
                configuration={this.putConfiguration}
                width='60%'
                errored={true}
            >
                {errorMsg}
            </ModalEditor>
        );
    }

    render() {
        const isError = !showMessageBoxOnNoFlow({...this.props, toggleError: this.toggleError}) || !this.shouldDisplayPort();
        if (isError) {
            return this.renderError();
        }
        return this.renderAdd();
    }
}

AddToFlowEditor.defaultProps = {
    operation: null,
}

AddToFlowEditor.propTypes = {
    operation: PropTypes.string,
    data: PropTypes.shape({}).isRequired,
    parentQuery: PropTypes.shape({}).isRequired,
    parentPath: PropTypes.string.isRequired,
}

export default AddToFlowEditor;
<commit_msg>VSD-24249: Add stateful flag to virtual firewall rule and ICMP Code and Type if available<commit_after>import React, { PropTypes } from 'react';
import ModalEditor from '../../components/Editor/ModalEditor';
import {Form, Label, Select, Header, Checkbox} from '../../ui-components';
import {buildOptions, getDomainID, getEnterpriseID, getMetaDataAttribute} from './utils';
import { TwoColumnRow } from '../components';

import {
    getNetworkProtocolForText,
    getNetworkTypeOptions,
    getNetworkTypeForValue,
    getSecurityPolicyActionsForValue, NetworkProtocols,
} from './NetworkData';

import {
    fetchAssociatedObjectIfNeeded,
    fetchDestinationNetworkItems,
    fetchSourceNetworkItems,
    getSourceData,
    getDestinationNetworkItems,
    getNetworkItems,
    getSourceNetworkItems,
    NetworkObjectTypes,
    showMessageBoxOnNoFlow
} from './actions';

const getEntityNameForID = (ID, entityCollection) => {
    const entities = entityCollection && entityCollection.options && entityCollection.options.length > 0 ? entityCollection.options.find(item => item.value === ID) : null;
    return entities && entities.length > 0 ? entities[0] :
        entities && Object.getOwnPropertyNames(entities).length > 0 ? entities : null;
}

const buildVFRuleOptions = (options, srcEntity, destEntity) => {
    if (options && options.data && options.data.length > 0) {
        return options.data.map(item => {
            const desc = item.description ? `${item.description}: ` : '';
            const locationType = getNetworkTypeForValue(item.locationType);
            const networkType = getNetworkTypeForValue(item.networkType);
            let action = getSecurityPolicyActionsForValue(item.action);
            action = action && action.length > 0 ? action[0].text : '';
            const srcEntityName = srcEntity && srcEntity.value === item.locationID ? srcEntity.text : null;
            const destEntityName = destEntity && destEntity.value === item.networkID ? destEntity.text : null;
            const from = srcEntityName ? `${locationType[0].text} ${srcEntityName}` : locationType[0].text;
            const to = destEntityName ? `${networkType[0].text} ${destEntityName}` : networkType[0].text;
            const priority = item.priority;
            const templateName = item.ACLTemplateName ? `Policy: ${item.ACLTemplateName}: ` : "";
            const destPort = item.destinationPort ? `D-Port: ${item.destinationPort}` : '';
            const text = `${templateName}${desc}${destPort}: From ${from} To: ${to} Action: ${action} Priority: ${priority}`;
            return ({ text, value: item.ID });
        });
    }
    if (options && options.isFetching) {
        return "Fetching...";
    }
    if (options && options.error) {
        return options.error;
    }
    return "No Data available";
}

class AddToFlowEditor extends React.Component {
    constructor(...props) {
        super(...props);

        this.state = {
            opened: false,
            error: false,
        }
        this.toggleError = this.toggleError.bind(this);
        this.putConfiguration = this.putConfiguration.bind(this);
        this.resetFieldsOnChange = this.resetFieldsOnChange.bind(this);
    }

    componentWillMount() {
        this.setState({opened: true, formName: 'add-flow-editor', error: true});
    }

    handleChangeProtocol = (evt, value) => {
        const { preventDefault, ...values} = evt;
        const protocol = values ? Object.values(values).join('') : null;
        if (protocol === '1') {
            const { changeFieldValue, formName } = this.props;
            if (changeFieldValue) {
                const ICMPCode = getMetaDataAttribute(this.props.data, 'ICMPCode');
                const ICMPType = getMetaDataAttribute(this.props.data, 'ICMPType');
                changeFieldValue(formName, 'ICMPCode', ICMPCode);
                changeFieldValue(formName, 'ICMPType', ICMPType);
            }
        }
    }

    handleSelectRule = (evt) => {
        const { preventDefault, ...values } = evt;
        const ID = values ? Object.values(values).join('') : null;
        if (ID) {
            const vfrules = getNetworkItems(NetworkObjectTypes.VIRTUAL_FIREWALL_RULE, this.props);
            if (vfrules && vfrules.data && vfrules.data.length > 0) {
                const { selectRule, data } = this.props;
                const destData = getSourceData(this.props);
                const selectedRule = vfrules.data.find(item => item.ID === ID);
                if (selectRule && data) {
                    const object = Object.assign({}, selectedRule);
                    var re = new RegExp(data.destinationport, 'gi');
                    object.destinationPort = object.destinationPort === '*' || object.destinationPort.match(re) ? object.destinationPort :
                        `${object.destinationPort},${destData.destinationport}`;
                    selectRule(ID, object);
                }
            }
        }
    }

    buildSourceField = (source) => {
        let srcList = null;
        if (source && !(source.type === 'ANY' || source.type === 'UNDERLAY_INTERNET_POLICYGROUP')) {
            const srcOptions = buildOptions(source);
            srcList = {
                name: 'locationID',
                component: Select,
            };
            if (srcOptions && Array.isArray(srcOptions)) {
                srcList.options = srcOptions;
            }
            else {
                srcList.text = srcOptions;
            }
        }

        return srcList;
    }

    buildDestField = (destination) => {
        let destList = null;
        if (destination && !(destination.type === 'ANY' || destination.type === 'UNDERLAY_INTERNET_POLICYGROUP')) {
            const destOptions = buildOptions(destination);
            destList = {
                name: 'networkID',
                component: Select,
            };
            if (destOptions && Array.isArray(destOptions)) {
                destList.options = destOptions;
            }
            else {
                destList.text = destOptions;
            }
        }
        return destList;
    }

    validate = (values) => {
        const { ID, protocol, ICMPCode, ICMPType } = values;

        if (!ID) {
            return { ID: "Please select a valid virtual firewall rule"};
        }

        if (protocol === '1' && !(ICMPCode || ICMPType)) {
            return { protocol: "ICMP Require valid ICMP code and type for the flow"};
        }
        return {};
    }

    shouldComponentUpdate(nextProps, nextState) {
        return nextProps !== this.props || this.state !== nextState;
    }

    fetchVFRulesIfNeeded = (props) => {
        const {
            data,
            locationTypeValue,
            locationIDValue,
            networkTypeValue,
            networkIDValue,
            operation,
            resourceName,
        } = props;
        if (!data || Object.getOwnPropertyNames(data).length <= 0
            || operation !== 'add') {
            return;
        }
        const vfRules = getNetworkItems(NetworkObjectTypes.VIRTUAL_FIREWALL_RULE, this.props);
        if (vfRules && vfRules.data && Object.getOwnPropertyNames(vfRules.data).length > 0)
            return;
         if (!locationTypeValue || (locationTypeValue !== 'ANY' && locationTypeValue !== 'UNDERLAY_INTERNET_POLICYGROUP' && !locationIDValue)) {
             return;
         }
         if (!networkTypeValue || (networkTypeValue !== 'ANY' && networkTypeValue !== 'UNDERLAY_INTERNET_POLICYGROUP' && !networkIDValue)) {
            return;
        }
        const proto = getNetworkProtocolForText(data.protocol);

        const enterpriseID = getEnterpriseID(props);
        const domainID = getDomainID(resourceName, data);

        fetchAssociatedObjectIfNeeded({
            type: NetworkObjectTypes.VIRTUAL_FIREWALL_RULE,
            args: {
                locationType: locationTypeValue,
                locationID: locationIDValue,
                networkType: networkTypeValue,
                networkID: networkIDValue,
                protocol: proto ? proto : '6',
            }, domainID, enterpriseID, ...props});
    }

    componentWillReceiveProps(nextProps) {
        const {
            data,
            locationTypeValue,
            locationIDValue,
            networkTypeValue,
            networkIDValue,
            resourceName,
        } = nextProps;

        if (!data || Object.getOwnPropertyNames(data).length <= 0) {
            return;
        }

        const enterpriseID = getEnterpriseID(nextProps);
        const domainID = getDomainID(resourceName, data);

        const srcNetworkItems = {
            ...getSourceNetworkItems(nextProps),
            type: locationTypeValue,
            ID: locationIDValue,
        };
        const destNetworkItems = {
            ...getDestinationNetworkItems(nextProps),
            type: networkTypeValue,
            ID: networkIDValue,
        };
        if (!srcNetworkItems.data) {
            fetchSourceNetworkItems(nextProps, domainID, enterpriseID);
        }
        if (!destNetworkItems.data) {
            fetchDestinationNetworkItems(nextProps, domainID, enterpriseID);
        }

        this.fetchVFRulesIfNeeded(nextProps);
        const vfrules = getNetworkItems(NetworkObjectTypes.VIRTUAL_FIREWALL_RULE, nextProps);
        if (vfrules && vfrules.data && vfrules.data.length > 0) {
            this.toggleError(false);
        }
        else {
            this.toggleError(true);
        }
    }

    resetFieldsOnChange = (value, ...fields) => {
        const { changeFieldValue } = this.props;
        const { formName } = this.state;
        if (value) {
            if (fields) {
                fields.forEach(fieldName => changeFieldValue(formName, fieldName, null))
            }
        }
    }

    toggleError = (flag) => {
        if (this.state.error !== flag ) {
            this.setState({ error: flag});
        }
    }

    buildVFRuleField = (srcEntity, destEntity) => {
        const vfrules = getNetworkItems(NetworkObjectTypes.VIRTUAL_FIREWALL_RULE, this.props);
        const vfRuleOptions = buildVFRuleOptions(vfrules, srcEntity, destEntity);
        if (vfRuleOptions && Array.isArray(vfRuleOptions)) {
            return (
                <Form.Field
                    name="ID"
                    component={Select}
                    options={vfRuleOptions}
                    onChange={this.handleSelectRule}
                />
            );
        }
        else {
            return ({vfRuleOptions});
        }

    }

    handleCancel = () => {
        // first dispatch a reset of the selection
        const { resetSelectedFlow, IDValue } = this.props;
        resetSelectedFlow(IDValue);
        this.props.handleClose();
    }

    handleDone = () => {
        // first dispatch a reset of the selection
        const { resetSelectedFlow, query: { id }, IDValue } = this.props;
        resetSelectedFlow(id);
        resetSelectedFlow(IDValue);
        this.props.handleClose();
    }

    putConfiguration = () => {
        const { IDValue } = this.props;
        return {
            service: "VSD",
            query: {
                parentResource: "virtualfirewallrules",
                parentID: IDValue,
            }
        }
    }

    shouldDisplayPort = () => {
        const {
            data,
        } = this.props;

        const protocol = data && data.protocol ? data.protocol : '';
        const proto = getNetworkProtocolForText(protocol);
        return (proto === '6' || proto === '17');

    }

    renderAdd = () => {
        const {
            data,
            locationTypeValue,
            networkTypeValue,
            locationIDValue,
            networkIDValue,
            resourceName,
        } = this.props;

        const title = "Add to Firewall Rule";
        const buttonLabel = "Add";
        const protocol = data && data.protocol ? data.protocol : '';
        const destData = getSourceData(this.props);
        const dPort = destData && destData.destinationport ? destData.destinationport : '';
        const ICMPCode = protocol === '1' ? data && data.ICMPCode : undefined;
        const ICMPType = protocol === '1' ? data && data.ICMPType : undefined;
        const srcNetworkItems = {
            ...getSourceNetworkItems(this.props),
            type: locationTypeValue,
            ID: locationIDValue,
        };
        const destNetworkItems = {
            ...getDestinationNetworkItems(this.props),
            type: networkTypeValue,
            ID: networkIDValue,
        };
        const srcList = this.buildSourceField(srcNetworkItems);

        const destList = this.buildDestField(destNetworkItems);
        const srcEntity = locationIDValue && srcList ? getEntityNameForID(locationIDValue, srcList) : null;
        const destEntity = networkIDValue && destList ? getEntityNameForID(networkIDValue, destList) : null;
        const networkDestinations = getNetworkTypeOptions(resourceName);
        const isDestPortEnabled = this.shouldDisplayPort();
        return (
            <ModalEditor
                title={title}
                submitLabel={buttonLabel}
                open={this.state.opened}
                name={this.state.formName}
                onCancel={this.handleCancel}
                onDone={this.handleDone}
                width='60%'
                onValidate={this.validate}
                configuration={this.putConfiguration}
                errored={this.state.error}
                getInitialValues={() => ({stateful: true})}
            >
                Match Criteria
                <TwoColumnRow
                    secondColumnProps={{
                        name: 'stateful',
                        label: 'Stateful entry',
                        component: Checkbox,
                        hideLabel: true,
                    }}
                />
                <TwoColumnRow firstColumnProps={{
                    name: 'locationType',
                    label: 'Source',
                    component: Select,
                    options: networkDestinations,
                    onChange:(e) => this.resetFieldsOnChange(e, 'locationID')
                }} secondColumnProps={{
                    name: 'networkType',
                    label: 'Destination',
                    component: Select,
                    options: networkDestinations,
                    onChange:(e) => this.resetFieldsOnChange(e, 'networkID')
                }} />
                { (srcList || destList) &&  <TwoColumnRow firstColumnProps={srcList} secondColumnProps={destList} /> }

                Virtual Firewall Rules
                {
                    (locationTypeValue && networkTypeValue && this.buildVFRuleField(srcEntity, destEntity)) || Select a source and destination type
                }
                { isDestPortEnabled &&
                    <TwoColumnRow firstColumnProps={{
                        name: 'protocol',
                        label: 'Protocol',
                        text: protocol,
                        onChange: (value) => this.handleChangeProtocol(value)
                    }} secondColumnProps={{
                        name: 'dPort',
                        label: 'Destination Port',
                        text: dPort,
                    }}/>
                }
                { !isDestPortEnabled &&
                    <TwoColumnRow firstColumnProps={{
                        name: 'protocol',
                        label: 'Protocol',
                        text: protocol,
                    }}/>
                }
                {
                    ICMPType && ICMPCode &&
                        <TwoColumnRow
                            firstColumnProps={{
                                name: 'ICMPCode',
                                label: 'ICMP Code',
                                text: ICMPCode
                            }}
                            secondColumnProps={{
                                name: 'ICMPType',
                                label: 'ICMP Type',
                                text: ICMPType
                            }}
                        />
                }
            </ModalEditor>
        );
    }

    renderError = () => {
        const title = "Add to Firewall Rule";
        const buttonLabel = "Add";
        const errorMsg = !this.shouldDisplayPort() ? "Nothing to add to rules" : 'No Flow Selected';

        return(
            <ModalEditor
                title={title}
                submitLabel={buttonLabel}
                open={this.state.opened}
                name={this.state.formName}
                onCancel={this.props.handleClose}
                configuration={this.putConfiguration}
                width='60%'
                errored={true}
            >
                {errorMsg}
            </ModalEditor>
        );
    }

    render() {
        const isError = !showMessageBoxOnNoFlow({...this.props, toggleError: this.toggleError}) || !this.shouldDisplayPort();
        if (isError) {
            return this.renderError();
        }
        return this.renderAdd();
    }
}

AddToFlowEditor.defaultProps = {
    operation: null,
}

AddToFlowEditor.propTypes = {
    operation: PropTypes.string,
    data: PropTypes.shape({}).isRequired,
    parentQuery: PropTypes.shape({}).isRequired,
    parentPath: PropTypes.string.isRequired,
}

export default AddToFlowEditor;
<|endoftext|>


<commit_before>function getURLParameter(name) {
  return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search) || [null, ''])[1].replace(/\+/g, '%20')) || null;
}

var tempColorMap = function(temp) {
    if (temp < -10)
        return '#609';
    else if (temp < 20)
        return '#33c';
    else if (temp < 45)
        return '#66f';
    else if (temp < 67)
        return '#39f';
    else if (temp < 77)
        return '#0c6';
    else if (temp < 90)
        return '#fc0';
    else if (temp < 100)
        return '#f60';
    else if (temp < 110)
        return '#f00';
    else
        return '#c00';
}

let tempSpan = function(tempIn) {
    let temp = parseInt(tempIn);
    var kls = "";
    if (temp < -10)
        kls="temp-dangercold";
    else if (temp < 20)
        kls="temp-verycold";
    else if (temp < 45)
        kls="temp-cold";
    else if (temp < 67)
        kls="temp-cool";
    else if (temp < 77)
        kls="temp-comfy";
    else if (temp < 90)
        kls="temp-warm";
    else if (temp < 100)
        kls="temp-hot";
    else if (temp < 110)
        kls="temp-veryhot";
    else
        kls="temp-dangerhot";
    return `<span class="tempSpan ${kls}">${temp}</span>`;
}

var dewpColorMap = function(dewp) {
    if (dewp < 35)
        return '#add';
    else if (dewp < 50)
        return '#4dd';
    else if (dewp < 62)
        return '#0c6';
    else if (dewp < 70)
        return '#fc0';
    else if (dewp < 75)
        return '#f60';
    else
        return '#f00';
}

let dewpSpan = function(dewpIn) {
    let dewp = parseInt(dewpIn);
    if (dewp < 35)
        kls = "dewp-verydry";
    else if (dewp < 50)
        kls = "dewp-dry";
    else if (dewp < 62)
        kls = "dewp-comfy";
    else if (dewp < 70)
        kls = "dewp-humid";
    else if (dewp < 75)
        kls = "dewp-sticky";
    else
        kls = "dewp-tropical";
    return `<span class="dewpSpan ${kls}">${dewp}</span>`;
}

function divit(txt,classes="") {
    if (classes !== "") {
        return `<div class="${classes}">${txt}</div>`;
    } else {
        return `<div>${txt}</div>`;
    }
}

function windColorMap(speed) {
    windSpeeds = [
        [2.0,  '#ffffff'],
        [5.0,  '#c4e1ed'],
        [10.0, '#64b9dc'],
        [15.0, '#64abdc'],
        [25.0, '#5092dc'],
        [38.0, '#3572e3'],
        [50.0, '#41d046'],
        [60.0, '#97e52b'],
        [73.0, '#d0fa26'],
        [96.0, '#ffff11'],
        [111.0,'#ffbb01'],
        [130.0,'#ff7901'],
        [157.0,'#ff3d01']
    ];

    const offScaleColor = '#a00000';

    var retval = offScaleColor;

    for (level of windSpeeds) {
        if (speed<level[0]) {
            retval = level[1];
            break;
        }
    }

    return retval;
}

function popColorMap(pop) {
    pops = [
        [10.0, '#ffffff'],
        [30.0, '#64b9dc'],
        [50.0, '#3572e3'],
        [70.0, '#41d046'],
        [80.0, '#ffff11'],
        [90.0, '#ff3d01']
    ];

    const offScaleColor = '#a00000';

    var retval = offScaleColor;

    for (cpop of pops) {
        if (pop<cpop[0]) {
            retval = cpop[1];
            break;
        }
    }

    return retval;
}

function qpfColorMap(qpf) {
    qpfs = [
        [0.01, '#ffffff'],
        [0.1, '#dddddd'],
        [0.25, '#b2def7'],
        [0.5, '#5bb6fc'],
        [0.75, '#1430e9'],
        [1.0, '#87fd15']
        [1.5, '#4fc519'],
        [2.0, '#357237'],
        [2.5, '#fdfd07'],
        [3.0, '#fcce20'],
        [4.0, '#fc9918'],
        [5.0, '#f9261b']
        [6.0, '#c41c1c'],
        [8.0, '#7c0616'],
        [10.0, '#fe01f9'],
    ];

    const offScaleColor = '#8f38c7';

    var retval = offScaleColor;

    for (row of qpfs) {
        if (qpf<row[0]) {
            retval = row[1];
            break;
        }
    }

    return retval;
}

function barParams(hour) {
    return [
            tempColorMap(parseInt(hour.temp.english)),
            dewpColorMap(parseInt(hour.dewpoint.english)),
            `hsl(${hour.wdir.degrees},100%,50%)`,
            windColorMap(parseInt(hour.wspd.english)),
            popColorMap(parseFloat(hour.pop)),
            qpfColorMap(parseFloat(hour.qpf.english))
        ];
}

function colorbar(day) {
    const pixWidth = 6;
    const pixHeight = 6;
    const numBars = barParams(day.hours[0]).length;

    var bar = document.createElement('canvas');
    bar.width = ((day.hours.length) * pixWidth)+2;
    bar.height = (numBars*pixHeight)+2;

    var ctx = bar.getContext('2d');
    var ctr = 0;
    const w = pixWidth;
    const h = pixHeight;

    ctx.fillStyle = '#999'
    ctx.fillRect(0,0,bar.width,bar.height);

    for (var k=0; k<day.hours.length; k++) {
        var hour = day.hours[k];

        var x = (k*pixWidth) + 1;
        var y = pixHeight;

        var params = barParams(hour);

        for (var i=0; i<params.length; i++) {
            ctx.fillStyle = params[i];
            ctx.fillRect(x, (y*i)+1, w, h);
        }
    }
    return bar;
}

function loadConditions() {
    var container = document.getElementById("container");
    var wx = document.wx;
    var days = {};
    for (per of wx.hourly_forecast) {
        if (!days.hasOwnProperty(per.FCTTIME.yday)) {
            days[per.FCTTIME.yday] = { hours: [per] };
        } else {
            days[per.FCTTIME.yday].hours.push(per);
        }
    }

    for (day of wx.forecast.simpleforecast.forecastday) {
        var td = days[`${day.date.yday}`];
        td.day = day;
        var dewps = new Array;
        for (hour of td.hours) {
            dewps.push(parseInt(hour.dewpoint.english));
        }
        var dewp = Math.round(dewps.reduce((t,s) => t+s) / dewps.length);

        var newdiv = document.createElement('div');
        newdiv.classList.add("fc-row");

        var dobj = day.date;
        var ds = divit(`${dobj.weekday_short} ${dobj.month}/${dobj.day}`,"ds");

        var wi = divit(`<img src="icons/${day.icon}-16.png" />`,"wi");

        var wt = divit(`${day.conditions}`,"wt");

        var hi = tempSpan(day.high.fahrenheit);
        var lo = tempSpan(day.low.fahrenheit);
        var dp = dewpSpan(dewp);

        var cbdiv = document.createElement('div');
        cbdiv.classList.add("colorbar");
        cbdiv.appendChild(colorbar(td));
        var hl = divit(`${hi} ${lo} ${dp}`,"hl");

        newdiv.innerHTML = `${wi}${ds}${hl}`;
        newdiv.appendChild(cbdiv);

        container.appendChild(newdiv);
    }
};

document.addEventListener('DOMContentLoaded', function() {
    var station = getURLParameter('station');
    if (station === null) {
        station = '';
    }
    fetch(`wx${station}.json`)
        .then((resp) => resp.json())
        .then(function(data) {
            document.wx = data;
            loadConditions();
        });
});
<commit_msg>Major refactor/reorg of wx.js<commit_after>const tempColorRange = [
    [-10, '#609'],
    [ 20, '#33c'],
    [ 45, '#66f'],
    [ 67, '#39f'],
    [ 77, '#0c6'],
    [ 90, '#fc0'],
    [100, '#f60'],
    [110, '#f00'],
    [999, '#c00']
];

const tempSpanRange = [
    [-10, 'temp-dangercold'],
    [ 20, 'temp-verycold'],
    [ 45, 'temp-cold'],
    [ 67, 'temp-cool'],
    [ 77, 'temp-comfy'],
    [ 90, 'temp-warm'],
    [100, 'temp-hot'],
    [110, 'temp-veryhot'],
    [999, 'temp-dangerhot']
];

const dewpColorRange = [
    [35, '#add'],
    [50, '#4dd'],
    [62, '#0c6'],
    [70, '#fc0'],
    [75, '#f60'],
    [999,'#f00']
];

const dewpSpanRange = [
    [35, "dewp-verydry"],
    [50, "dewp-dry"],
    [62, "dewp-comfy"],
    [70, "dewp-humid"],
    [75, "dewp-sticky"],
    [999, "dewp-tropical"]
];

const windSpeedRange = [
    [2.0,  '#ffffff'],
    [5.0,  '#c4e1ed'],
    [10.0, '#64b9dc'],
    [15.0, '#64abdc'],
    [25.0, '#5092dc'],
    [38.0, '#3572e3'],
    [50.0, '#41d046'],
    [60.0, '#97e52b'],
    [73.0, '#d0fa26'],
    [96.0, '#ffff11'],
    [111.0,'#ffbb01'],
    [130.0,'#ff7901'],
    [157.0,'#ff3d01'],
    [999.0,'#a00000']
];

const popRange = [
    [10.0, '#ffffff'],
    [30.0, '#64b9dc'],
    [50.0, '#3572e3'],
    [70.0, '#41d046'],
    [80.0, '#ffff11'],
    [90.0, '#ff3d01'],
    [999,  '#a00000'],
];

const qpfRange = [
    [0.01, '#ffffff'],
    [0.1,  '#dddddd'],
    [0.25, '#b2def7'],
    [0.5,  '#5bb6fc'],
    [0.75, '#1430e9'],
    [1.0,  '#87fd15']
    [1.5,  '#4fc519'],
    [2.0,  '#357237'],
    [2.5,  '#fdfd07'],
    [3.0,  '#fcce20'],
    [4.0,  '#fc9918'],
    [5.0,  '#f9261b']
    [6.0,  '#c41c1c'],
    [8.0,  '#7c0616'],
    [10.0, '#fe01f9'],
    [999,  '#8f38c7']
];

function rangeMap(value, rangeArray) {
    /* rangeArray must be an array of two-item arrays */
    var retval;
    for (rangeItem of rangeArray) {
        if (value < rangeItem[0]) {
            retval = rangeItem[1];
            break;
        }
    }

    return retval;
}

function barParams(hour) {
    return [
            rangeMap(parseInt(hour.temp.english), tempColorRange),
            rangeMap(parseInt(hour.dewpoint.english), dewpColorRange),
            `hsl(${hour.wdir.degrees},100%,50%)`, // wind direction
            rangeMap(parseInt(hour.wspd.english), windSpeedRange),
            rangeMap(parseFloat(hour.pop), popRange),
            rangeMap(parseFloat(hour.qpf.english), qpfRange)
        ];
}

function colorbar(day) {
    const pixWidth = 6;
    const pixHeight = 6;
    const numBars = barParams(day.hours[0]).length;

    var bar = document.createElement('canvas');
    bar.width = ((day.hours.length) * pixWidth)+2;
    bar.height = (numBars*pixHeight)+2;

    var ctx = bar.getContext('2d');
    var ctr = 0;
    const w = pixWidth;
    const h = pixHeight;

    ctx.fillStyle = '#999'
    ctx.fillRect(0,0,bar.width,bar.height);

    for (var k=0; k<day.hours.length; k++) {
        var hour = day.hours[k];

        var x = (k*pixWidth) + 1;
        var y = pixHeight;

        var params = barParams(hour);

        for (var i=0; i<params.length; i++) {
            ctx.fillStyle = params[i];
            ctx.fillRect(x, (y*i)+1, w, h);
        }
    }
    return bar;
}

function divit(txt,classes="") {
    if (classes !== "") {
        return `<div class="${classes}">${txt}</div>`;
    } else {
        return `<div>${txt}</div>`;
    }
}

function loadConditions() {
    var container = document.getElementById("container");
    var wx = document.wx;

    var days = {};
    for (per of wx.hourly_forecast) {
        if (!days.hasOwnProperty(per.FCTTIME.yday)) {
            days[per.FCTTIME.yday] = { hours: [per] };
        } else {
            days[per.FCTTIME.yday].hours.push(per);
        }
    }

    for (day of wx.forecast.simpleforecast.forecastday) {
        var td = days[`${day.date.yday}`];
        td.day = day;

        var dewps = new Array;
        for (hour of td.hours) {
            dewps.push(parseInt(hour.dewpoint.english));
        }
        var dewp = Math.round(dewps.reduce((t,s) => t+s) / dewps.length);

        var newdiv = document.createElement('div');
        newdiv.classList.add("fc-row");

        var dobj = day.date;
        var ds = divit(`${dobj.weekday_short} ${dobj.month}/${dobj.day}`,"ds");

        var wi = divit(`<img src="icons/${day.icon}-16.png" />`,"wi");

        var wt = divit(`${day.conditions}`,"wt");

        var tempClassText = rangeMap(day.high.fahrenheit, tempSpanRange);
        var hi = `<span class="tempSpan ${tempClassText}">${day.high.fahrenheit}</span>`;

        tempClassText = rangeMap(day.low.fahrenheit, tempSpanRange);
        var lo = `<span class="tempSpan ${tempClassText}">${day.low.fahrenheit}</span>`

        var dewpClassText = rangeMap(dewp, dewpSpanRange);
        var dp = `<span class="dewpSpan ${dewpClassText}">${dewp}</span>`;

        var cbdiv = document.createElement('div');
        cbdiv.classList.add("colorbar");
        cbdiv.appendChild(colorbar(td));
        var hl = divit(`${hi} ${lo} ${dp}`,"hl");

        newdiv.innerHTML = `${wi}${ds}${hl}`;
        newdiv.appendChild(cbdiv);

        container.appendChild(newdiv);
    }
};

function getURLParameter(name) {
  return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search) || [null, ''])[1].replace(/\+/g, '%20')) || null;
}

document.addEventListener('DOMContentLoaded', function() {
    var station = getURLParameter('station');
    if (station === null) {
        station = '';
    }
    fetch(`wx${station}.json`)
        .then((resp) => resp.json())
        .then(function(data) {
            document.wx = data;
            loadConditions();
        });
});
<|endoftext|>


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

foam.CLASS({
  package: 'foam.u2.view',
  name: 'MultiChoiceView',
  extends: 'foam.u2.View',

  implements: [ 'foam.mlang.Expressions' ],

  documentation: `
    Wraps a tag that represents multiple choices.

    The choices are in [value, label, isSelected, choiceMode ] quartets.

    However the client can simply pass in [value, label] and it will adapt to a [value, label, isSelected, choiceMode ] format

    Or the client can instead pass in a DAO to the MultiChoiceView.dao and the choices list will
    be automatically generated

    Calling the following methods:

    1. MultiChoiceView.outputSelectedChoicesInValueLabelFormat() - will return an array containing the
    selected choices in [ value, label ] format only if the minSelected, maxSelected criteria is respected
    2. MultiChoiceView.outputSelectedChoicesInValueLabelFormat() - will return a predicated dao containing thee
    selected choices in only if the minSelected, maxSelected criteria is respected

    MultiChoiceView.data will be automatically set to a predicated dao based on the choices selected only if
    the minSelected, maxSelected criteria is respected, it will be foam.dao.NullDAO othrewise

    this.booleanView is a ViewSpec for each choice. It defaults to foam.u2.CheckBox
  `,

  properties: [
    {
      name: 'choices',
      documentation: `
        An array of choices which are single choice is denoted as [value, label, isSelected, choiceMode], however the user can
        just pass in [value, label] and the adapt function will turn it into the [value, label, isSelected, choiceMode] format
        for processing purposes
      `,
      factory: function() {
        return [];
      },
      adapt: function(_, n) {
        if ( ! Array.isArray(n) ) throw new Error("Please submit an array to choices in the MultiChoiceView");

        if ( n.length > 0 ){
          var valueLabelChoices = n.filter(choice => choice.length === 2)
          var fullChoices = n.filter(choice => choice.length === 4)

          if ( valueLabelChoices.length === n.length ) return n.map(choice => [ choice[0], choice[1], false, this.mode ])
          if ( fullChoices.length === n.length  ) return n;

          throw new Error("Items in choices array do not have consistent lengths")
        }

        return n;
      },
      postSet: function(_,n){
        var selectedChoices = n.filter(choice => choice[2] );

        if ( selectedChoices.length >= this.minSelected && selectedChoices.length <= this.maxSelected ){
          this.isValidNumberOfChoices = true;
        } else {
          this.isValidNumberOfChoices = false;
        }

        if ( selectedChoices.length < this.maxSelected ) {
          n.forEach((choice) => {
            if ( ! choice[2] ){
              choice[3] = this.mode
            }
          })
        } else {
          n.forEach((choice) => {
            if ( ! choice[2] ){
              choice[3] = foam.u2.DisplayMode.DISABLED
            }
          })
        }

        if (this.isValidNumberOfChoices ){
          this.data = this.outputSelectedChoicesInDAO()
        }
      }
    },
    {
      class: 'foam.dao.DAOProperty',
      name: 'dao',
      documentation: `
        If the user just wants to pass in a dao, it will be processed to populate the
        choices array instead of manually inputing the choices
      `
    },
    {
      class: 'Boolean',
      name: 'isValidNumberOfChoices',
      value: this.minSelected === 0 ? true : false
    },
    {
      class: 'Boolean',
      name: 'showMinMaxHelper',
      value: true
    },
    {
      class: 'Boolean',
      name: 'showValidNumberOfChoicesHelper',
      value: true
    },
    {
      class: 'Int',
      name: 'minSelected',
      factory: function() {
        return this.choices.length > 0 ? 1 : 0
      }
    },
    {
      class: 'Int',
      name: 'maxSelected',
      factory: function () {
        return 2;
      }
    },
    {
      class: 'foam.u2.ViewSpecWithJava',
      name: 'booleanView',
      value: { class: 'foam.u2.view.CardSelectView' }
    },
    {
      class: 'Boolean',
      name: 'isVertical',
      value: false
    },
    {
      class: 'Boolean',
      name: 'isDaoFetched',
      value: false
    },
    {
      class: 'Int',
      name: 'numberOfColumns',
      value: 3
    },
    {
      name: 'objToChoice',
      value: function(obj) {
        return [ obj.id, obj.toSummary() ];
      }
    }
  ],

  methods: [
    function initE() {
      var self = this;

      this.onDAOUpdate();

      this
        .start()
          .add(self.slot(function(showMinMaxHelper, minSelected, maxSelected) {
            return self.E().callIf(showMinMaxHelper, function() {
              this
              .start(foam.u2.layout.Rows)
                .start()
                  .add(`Min number of choices: ${minSelected}`)
                .end()
                .start()
                  .add(`Max number of choices: ${maxSelected}`)
                .end()
              .end()
            })
          }))
        .end()
        .start(self.isVertical ? foam.u2.layout.Rows : foam.u2.layout.Cols).style({'flex-wrap': 'wrap;'})
          .add(
            self.isDaoFetched$.map(isDaoFetched => {

              var arraySlotForChoices = foam.core.ArraySlot.create({
                slots: []
              });

              var toRender = self.choices.map(function (choice) {
                var simpSlot0 = foam.core.SimpleSlot.create({ value: choice[0] });
                var simpSlot1 = foam.core.SimpleSlot.create({ value: choice[1] });
                var simpSlot2 = foam.core.SimpleSlot.create({ value: choice[2] });
                var simpSlot3 = foam.core.SimpleSlot.create({ value: choice[3] });

                var arraySlotForChoice = foam.core.ArraySlot.create({
                  slots: [ simpSlot0, simpSlot1, simpSlot2, simpSlot3 ]
                })

                arraySlotForChoices.slots.push(arraySlotForChoice);

                return self.E().style({ 'height': '100%;', 'width':`${100 / self.numberOfColumns}%` })
                  .tag(self.booleanView, {
                      data$: simpSlot2,
                      label$: simpSlot1,
                      mode$: simpSlot3
                    })
              })

              this.choices$ = arraySlotForChoices;
              return toRender;
            })
          )
        .end()
        .start()
          .add(
            self.slot(function(showValidNumberOfChoicesHelper ,isValidNumberOfChoices, choices) {
              return self.E()
              .callIf(! isValidNumberOfChoices && showValidNumberOfChoicesHelper, function() {
                this
                  .add("Please select a valid number of choices")
              })
              .callIf(isValidNumberOfChoices && showValidNumberOfChoicesHelper, function() {
                this
                  .start()
                    .add("Here are your valid selected choices using the outputSelectedChoicesInValueLabelFormat() function:")
                  .end()
                  .start()
                    .add(
                      self.outputSelectedChoicesInValueLabelFormat().map(function(choice){
                        return self.E().add(`[${choice[0]},${choice[1]}]`)
                      })
                    )
                  .end()
              })
            })
          )
        .end()

    },

    function outputSelectedChoicesInValueLabelFormat() {
      if ( ! this.isValidNumberOfChoices ) {
        console.warn("Please select a valid number of choices");
        return [];
      }

      var filteredChoices = this.choices.filter(choice => choice[2]);

      return filteredChoices.map(choice => [choice[0], choice[1]]);
    },

    function outputSelectedChoicesInDAO() {
      if ( ! this.isValidNumberOfChoices ) {
        console.warn("Please select a valid number of choices");
        return foam.dao.NullDAO;
      }

      var of = this.dao.of

      var filteredChoices = this.choices.filter(choice => choice[2]);

      return this.dao.where(this.IN(of.ID, filteredChoices.map(choice => choice[0])));
    }
  ],

  listeners: [
    {
      name: 'onDAOUpdate',
      isFramed: true,
      code: function() {
        if ( ! this.dao || ! foam.dao.DAO.isInstance(this.dao) ) return;

        var of = this.dao.of
        if ( of._CHOICE_TEXT_ ) {
          this.dao.select(this.PROJECTION(of.ID, of._CHOICE_TEXT_)).then((s) => {
            this.choices = s.array;
            this.isDaoFetched = true
          });
          return;
        } else {
          console.warn('Inefficient ChoiceView. Consider creating transient _choiceText_ property on ' + of.id + ' DAO, prop: ' + this.prop_);

          /* Ex.:
          {
            class: 'String',
            name: '_choiceText_',
            transient: true,
            javaGetter: 'return getName();',
            getter: function() { return this.name; }
          }
          */
        }
        var p = this.mode === foam.u2.DisplayMode.RW ?
          this.dao.select().then(s => s.array) :
          this.dao.find(this.data).then(o => o ? [o] : []);

        p.then(a => {
          var choices = a.map(this.objToChoice);
          var choiceLabels = a.map(o => { return this.objToChoice(o)[1]});
          Promise.all(choiceLabels).then(resolvedChoiceLabels => {
            for ( let i = 0; i < choices.length; i++ ) {
              choices[i][1] = resolvedChoiceLabels[i];
            }
            this.choices = choices;
            this.isDaoFetched = true
          });
        });
      }
    }
  ]
});
<commit_msg>Allow slots of MultiChoice choices (#3813)<commit_after>/**
 * @license
 * Copyright 2020 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

foam.CLASS({
  package: 'foam.u2.view',
  name: 'MultiChoiceView',
  extends: 'foam.u2.View',

  implements: [ 'foam.mlang.Expressions' ],

  documentation: `
    Wraps a tag that represents multiple choices.

    The choices are in [value, label, isSelected, choiceMode ] quartets.

    However the client can simply pass in [value, label] and it will adapt to a [value, label, isSelected, choiceMode ] format

    Or the client can instead pass in a DAO to the MultiChoiceView.dao and the choices list will
    be automatically generated

    Calling the following methods:

    1. MultiChoiceView.outputSelectedChoicesInValueLabelFormat() - will return an array containing the
    selected choices in [ value, label ] format only if the minSelected, maxSelected criteria is respected
    2. MultiChoiceView.outputSelectedChoicesInValueLabelFormat() - will return a predicated dao containing thee
    selected choices in only if the minSelected, maxSelected criteria is respected

    MultiChoiceView.data will be automatically set to a predicated dao based on the choices selected only if
    the minSelected, maxSelected criteria is respected, it will be foam.dao.NullDAO othrewise

    this.booleanView is a ViewSpec for each choice. It defaults to foam.u2.CheckBox
  `,

  properties: [
    {
      name: 'choices',
      documentation: `
        An array of choices which are single choice is denoted as [value, label, isSelected, choiceMode], however the user can
        just pass in [value, label] and the adapt function will turn it into the [value, label, isSelected, choiceMode] format
        for processing purposes
      `,
      factory: function() {
        return [];
      },
      adapt: function(_, n) {
        if ( ! Array.isArray(n) ) throw new Error("Please submit an array to choices in the MultiChoiceView");

        if ( n.length > 0 ){
          var valueLabelChoices = n.filter(choice => choice.length === 2)
          var fullChoices = n.filter(choice => choice.length === 4)

          if ( valueLabelChoices.length === n.length ) return n.map(choice => [ choice[0], choice[1], false, this.mode ])
          if ( fullChoices.length === n.length  ) return n;

          throw new Error("Items in choices array do not have consistent lengths")
        }

        return n;
      },
      postSet: function(_,n){
        var selectedChoices = n.filter(choice => choice[2] );

        if ( selectedChoices.length >= this.minSelected && selectedChoices.length <= this.maxSelected ){
          this.isValidNumberOfChoices = true;
        } else {
          this.isValidNumberOfChoices = false;
        }

        if ( selectedChoices.length < this.maxSelected ) {
          n.forEach((choice) => {
            if ( ! choice[2] ){
              choice[3] = this.mode
            }
          })
        } else {
          n.forEach((choice) => {
            if ( ! choice[2] ){
              choice[3] = foam.u2.DisplayMode.DISABLED
            }
          })
        }

        if (this.isValidNumberOfChoices ){
          this.data = this.outputSelectedChoicesInDAO()
        }
      }
    },
    {
      class: 'foam.dao.DAOProperty',
      name: 'dao',
      documentation: `
        If the user just wants to pass in a dao, it will be processed to populate the
        choices array instead of manually inputing the choices
      `
    },
    {
      class: 'Boolean',
      name: 'isValidNumberOfChoices',
      value: this.minSelected === 0 ? true : false
    },
    {
      class: 'Boolean',
      name: 'showMinMaxHelper',
      value: true
    },
    {
      class: 'Boolean',
      name: 'showValidNumberOfChoicesHelper',
      value: true
    },
    {
      class: 'Int',
      name: 'minSelected',
      factory: function() {
        return this.choices.length > 0 ? 1 : 0
      }
    },
    {
      class: 'Int',
      name: 'maxSelected',
      factory: function () {
        return 2;
      }
    },
    {
      class: 'foam.u2.ViewSpecWithJava',
      name: 'booleanView',
      value: { class: 'foam.u2.view.CardSelectView' }
    },
    {
      class: 'Boolean',
      name: 'isVertical',
      value: false
    },
    {
      class: 'Boolean',
      name: 'isDaoFetched',
      value: false
    },
    {
      class: 'Int',
      name: 'numberOfColumns',
      value: 3
    },
    {
      name: 'objToChoice',
      value: function(obj) {
        return [ obj.id, obj.toSummary() ];
      }
    }
  ],

  methods: [
    function initE() {
      var self = this;

      this.onDAOUpdate();

      this
        .start()
          .add(self.slot(function(showMinMaxHelper, minSelected, maxSelected) {
            return self.E().callIf(showMinMaxHelper, function() {
              this
              .start(foam.u2.layout.Rows)
                .start()
                  .add(`Min number of choices: ${minSelected}`)
                .end()
                .start()
                  .add(`Max number of choices: ${maxSelected}`)
                .end()
              .end()
            })
          }))
        .end()
        .start(self.isVertical ? foam.u2.layout.Rows : foam.u2.layout.Cols).style({'flex-wrap': 'wrap;'})
          .add(
            self.isDaoFetched$.map(isDaoFetched => {

              var arraySlotForChoices = foam.core.ArraySlot.create({
                slots: []
              });

              var toRender = self.choices.map(function (choice) {
                var simpSlot0 = foam.core.SimpleSlot.create({ value: choice[0] });
                var simpSlot1 = foam.core.SimpleSlot.create({ value: choice[1] });
                var simpSlot2 = foam.core.Slot.isInstance(choice[2])
                  ? choice[2]
                  : foam.core.SimpleSlot.create({ value: choice[2] });
                var simpSlot3 = foam.core.SimpleSlot.create({ value: choice[3] });

                var arraySlotForChoice = foam.core.ArraySlot.create({
                  slots: [ simpSlot0, simpSlot1, simpSlot2, simpSlot3 ]
                })

                arraySlotForChoices.slots.push(arraySlotForChoice);

                return self.E().style({ 'height': '100%;', 'width':`${100 / self.numberOfColumns}%` })
                  .tag(self.booleanView, {
                      data$: simpSlot2,
                      label$: simpSlot1,
                      mode$: simpSlot3
                    })
              })

              this.choices$ = arraySlotForChoices;
              return toRender;
            })
          )
        .end()
        .start()
          .add(
            self.slot(function(showValidNumberOfChoicesHelper ,isValidNumberOfChoices, choices) {
              return self.E()
              .callIf(! isValidNumberOfChoices && showValidNumberOfChoicesHelper, function() {
                this
                  .add("Please select a valid number of choices")
              })
              .callIf(isValidNumberOfChoices && showValidNumberOfChoicesHelper, function() {
                this
                  .start()
                    .add("Here are your valid selected choices using the outputSelectedChoicesInValueLabelFormat() function:")
                  .end()
                  .start()
                    .add(
                      self.outputSelectedChoicesInValueLabelFormat().map(function(choice){
                        return self.E().add(`[${choice[0]},${choice[1]}]`)
                      })
                    )
                  .end()
              })
            })
          )
        .end()

    },

    function outputSelectedChoicesInValueLabelFormat() {
      if ( ! this.isValidNumberOfChoices ) {
        console.warn("Please select a valid number of choices");
        return [];
      }

      var filteredChoices = this.choices.filter(choice => choice[2]);

      return filteredChoices.map(choice => [choice[0], choice[1]]);
    },

    function outputSelectedChoicesInDAO() {
      if ( ! this.isValidNumberOfChoices ) {
        console.warn("Please select a valid number of choices");
        return foam.dao.NullDAO;
      }

      var of = this.dao.of

      var filteredChoices = this.choices.filter(choice => choice[2]);

      return this.dao.where(this.IN(of.ID, filteredChoices.map(choice => choice[0])));
    }
  ],

  listeners: [
    {
      name: 'onDAOUpdate',
      isFramed: true,
      code: function() {
        if ( ! this.dao || ! foam.dao.DAO.isInstance(this.dao) ) return;

        var of = this.dao.of
        if ( of._CHOICE_TEXT_ ) {
          this.dao.select(this.PROJECTION(of.ID, of._CHOICE_TEXT_)).then((s) => {
            this.choices = s.array;
            this.isDaoFetched = true
          });
          return;
        } else {
          console.warn('Inefficient ChoiceView. Consider creating transient _choiceText_ property on ' + of.id + ' DAO, prop: ' + this.prop_);

          /* Ex.:
          {
            class: 'String',
            name: '_choiceText_',
            transient: true,
            javaGetter: 'return getName();',
            getter: function() { return this.name; }
          }
          */
        }
        var p = this.mode === foam.u2.DisplayMode.RW ?
          this.dao.select().then(s => s.array) :
          this.dao.find(this.data).then(o => o ? [o] : []);

        p.then(a => {
          var choices = a.map(this.objToChoice);
          var choiceLabels = a.map(o => { return this.objToChoice(o)[1]});
          Promise.all(choiceLabels).then(resolvedChoiceLabels => {
            for ( let i = 0; i < choices.length; i++ ) {
              choices[i][1] = resolvedChoiceLabels[i];
            }
            this.choices = choices;
            this.isDaoFetched = true
          });
        });
      }
    }
  ]
});
<|endoftext|>