<commit_before>/**
 * Copyright IBM Corp. 2016, 2018
 *
 * This source code is licensed under the Apache-2.0 license found in the
 * LICENSE file in the root directory of this source tree.
 */

import cx from 'classnames';
import Downshift from 'downshift';
import PropTypes from 'prop-types';
import React from 'react';
import { settings } from 'carbon-components';
import { Checkmark16, WarningFilled16 } from '@carbon/icons-react';
import ListBox, { PropTypes as ListBoxPropTypes } from '../ListBox';
import { match, keys } from '../../internal/keyboard';
import setupGetInstanceId from '../../tools/setupGetInstanceId';

const { prefix } = settings;

const defaultItemToString = (item) => {
  if (typeof item === 'string') {
    return item;
  }

  return item && item.label;
};

const defaultShouldFilterItem = () => true;

const getInputValue = (props, state) => {
  if (props.selectedItem) {
    return props.itemToString(props.selectedItem);
  }
  // TODO: consistent `initialSelectedItem` behavior with other listbox components in v11
  if (props.initialSelectedItem) {
    return props.itemToString(props.initialSelectedItem);
  }

  return state.inputValue || '';
};

const findHighlightedIndex = ({ items, itemToString }, inputValue) => {
  if (!inputValue) {
    return -1;
  }

  const searchValue = inputValue.toLowerCase();

  for (let i = 0; i < items.length; i++) {
    const item = itemToString(items[i]).toLowerCase();
    if (item.indexOf(searchValue) !== -1) {
      return i;
    }
  }

  return -1;
};

const getInstanceId = setupGetInstanceId();

export default class ComboBox extends React.Component {
  static propTypes = {
    /**
     * 'aria-label' of the ListBox component.
     */
    ariaLabel: PropTypes.string,

    /**
     * An optional className to add to the container node
     */
    className: PropTypes.string,

    /**
     * Specify if the control should be disabled, or not
     */
    disabled: PropTypes.bool,

    /**
     * Specify a custom `id` for the input
     */
    id: PropTypes.string.isRequired,

    /**
     * Allow users to pass in an arbitrary item or a string (in case their items are an array of strings)
     * from their collection that are pre-selected
     */
    initialSelectedItem: PropTypes.oneOfType([
      PropTypes.object,
      PropTypes.string,
    ]),

    /**
     * We try to stay as generic as possible here to allow individuals to pass
     * in a collection of whatever kind of data structure they prefer
     */
    items: PropTypes.array.isRequired,

    /**
     * Helper function passed to downshift that allows the library to render a
     * given item to a string label. By default, it extracts the `label` field
     * from a given item to serve as the item label in the list
     */
    itemToString: PropTypes.func,

    /**
     * Optional function to render items as custom components instead of strings.
     * Defaults to null and is overriden by a getter
     */
    itemToElement: PropTypes.func,

    /**
     * `onChange` is a utility for this controlled component to communicate to a
     * consuming component when a specific dropdown item is selected.
     * @param {{ selectedItem }}
     */
    onChange: PropTypes.func.isRequired,

    /**
     * Used to provide a placeholder text node before a user enters any input.
     * This is only present if the control has no items selected
     */
    placeholder: PropTypes.string.isRequired,

    /**
     * Specify your own filtering logic by passing in a `shouldFilterItem`
     * function that takes in the current input and an item and passes back
     * whether or not the item should be filtered.
     */
    shouldFilterItem: PropTypes.func,

    /**
     * Specify if the currently selected value is invalid.
     */
    invalid: PropTypes.bool,

    /**
     * Message which is displayed if the value is invalid.
     */
    invalidText: PropTypes.string,

    /**
     * For full control of the selection
     */
    selectedItem: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),

    /**
     * Specify a custom translation function that takes in a message identifier
     * and returns the localized string for the message
     */
    translateWithId: PropTypes.func,

    /**
     * Currently supports either the default type, or an inline variant
     */
    type: ListBoxPropTypes.ListBoxType,

    /**
     * Specify the size of the ListBox. Currently supports either `sm`, `lg` or `xl` as an option.
     */
    size: ListBoxPropTypes.ListBoxSize,

    /**
     * Callback function to notify consumer when the text input changes.
     * This provides support to change available items based on the text.
     * @param {string} inputText
     */
    onInputChange: PropTypes.func,

    /**
     * should use "light theme" (white background)?
     */
    light: PropTypes.bool,

    /**
     * Additional props passed to Downshift
     */
    downshiftProps: PropTypes.shape(Downshift.propTypes),

    /**
     * Specify the direction of the combobox dropdown. Can be either top or bottom.
     */
    direction: PropTypes.oneOf(['top', 'bottom']),
  };

  static defaultProps = {
    disabled: false,
    itemToString: defaultItemToString,
    itemToElement: null,
    shouldFilterItem: defaultShouldFilterItem,
    type: 'default',
    ariaLabel: 'Choose an item',
    light: false,
    direction: 'bottom',
  };

  static getDerivedStateFromProps(nextProps, state) {
    const { prevSelectedItem, doneInitialSelectedItem } = state;
    const { selectedItem } = nextProps;
    if (!doneInitialSelectedItem || prevSelectedItem !== selectedItem) {
      return {
        doneInitialSelectedItem: true,
        prevSelectedItem: selectedItem,
        inputValue: getInputValue(nextProps, state),
      };
    }
    return null;
  }

  constructor(props) {
    super(props);

    this.textInput = React.createRef();

    this.comboBoxInstanceId = getInstanceId();

    this.state = {
      inputValue: getInputValue(props, {}),
    };
  }

  filterItems = (items, itemToString, inputValue) =>
    items.filter((item) =>
      this.props.shouldFilterItem({
        item,
        itemToString,
        inputValue,
      })
    );

  handleOnChange = (selectedItem) => {
    if (this.props.onChange) {
      this.props.onChange({ selectedItem });
    }
  };

  handleOnInputValueChange = (inputValue) => {
    const { onInputChange } = this.props;

    this.setState(
      () => ({
        // Default to empty string if we have a false-y `inputValue`
        inputValue: inputValue || '',
      }),
      () => {
        if (onInputChange) {
          onInputChange(inputValue);
        }
      }
    );
  };

  handleSelectionClear = () => {
    if (this.textInput?.current) {
      this.textInput.current.focus();
    }
  };

  handleOnStateChange = (newState, { setHighlightedIndex }) => {
    if (Object.prototype.hasOwnProperty.call(newState, 'inputValue')) {
      const { inputValue } = newState;
      setHighlightedIndex(findHighlightedIndex(this.props, inputValue));
    }
  };

  onToggleClick = (isOpen) => (event) => {
    if (event.target === this.textInput.current && isOpen) {
      event.preventDownshiftDefault = true;
      event.persist();
    }
  };

  render() {
    const {
      className: containerClassName,
      disabled,
      id,
      items,
      itemToString,
      itemToElement,
      titleText,
      helperText,
      placeholder,
      initialSelectedItem,
      selectedItem,
      ariaLabel,
      translateWithId,
      invalid,
      invalidText,
      light,
      type, // eslint-disable-line no-unused-vars
      size,
      shouldFilterItem, // eslint-disable-line no-unused-vars
      onChange, // eslint-disable-line no-unused-vars
      onInputChange, // eslint-disable-line no-unused-vars
      downshiftProps,
      direction,
      ...rest
    } = this.props;
    const className = cx(`${prefix}--combo-box`, containerClassName, {
      [`${prefix}--list-box--up`]: direction === 'top',
    });
    const titleClasses = cx(`${prefix}--label`, {
      [`${prefix}--label--disabled`]: disabled,
    });
    const comboBoxHelperId = !helperText
      ? undefined
      : `combobox-helper-text-${this.comboBoxInstanceId}`;
    const helperClasses = cx(`${prefix}--form__helper-text`, {
      [`${prefix}--form__helper-text--disabled`]: disabled,
    });
    const wrapperClasses = cx(`${prefix}--list-box__wrapper`);
    const inputClasses = cx(`${prefix}--text-input`, {
      [`${prefix}--text-input--empty`]: !this.state.inputValue,
    });

    // needs to be Capitalized for react to render it correctly
    const ItemToElement = itemToElement;
    return (
      <Downshift
        {...downshiftProps}
        onChange={this.handleOnChange}
        onInputValueChange={this.handleOnInputValueChange}
        onStateChange={this.handleOnStateChange}
        inputValue={this.state.inputValue || ''}
        itemToString={itemToString}
        defaultSelectedItem={initialSelectedItem}
        selectedItem={selectedItem}>
        {({
          getToggleButtonProps,
          getInputProps,
          getItemProps,
          getLabelProps,
          isOpen,
          inputValue,
          selectedItem,
          highlightedIndex,
          clearSelection,
          toggleMenu,
          getMenuProps,
        }) => (

            {titleText && (

                {titleText}

            )}
            <ListBox
              className={className}
              disabled={disabled}
              invalid={invalid}
              aria-label={ariaLabel}
              invalidText={invalidText}
              isOpen={isOpen}
              light={light}
              size={size}>
              <ListBox.Field
                id={id}
                {...getToggleButtonProps({
                  disabled,
                  onClick: this.onToggleClick(isOpen),
                })}>
                <input
                  disabled={disabled}
                  className={inputClasses}
                  type="text"
                  tabIndex="0"
                  aria-autocomplete="list"
                  ref={this.textInput}
                  {...rest}
                  {...getInputProps({
                    disabled,
                    placeholder,
                    onKeyDown: (event) => {
                      if (match(event, keys.Space)) {
                        event.stopPropagation();
                      }

                      if (match(event, keys.Enter)) {
                        toggleMenu();
                      }
                    },
                  })}
                />
                {invalid && (
                  <WarningFilled16
                    className={`${prefix}--list-box__invalid-icon`}
                  />
                )}
                {inputValue && (
                  <ListBox.Selection
                    clearSelection={clearSelection}
                    translateWithId={translateWithId}
                    disabled={disabled}
                    onClearSelection={this.handleSelectionClear}
                  />
                )}
                <ListBox.MenuIcon
                  isOpen={isOpen}
                  translateWithId={translateWithId}
                />
              </ListBox.Field>
              {isOpen && (
                <ListBox.Menu
                  id={id}
                  {...getMenuProps({ 'aria-label': ariaLabel })}>
                  {this.filterItems(items, itemToString, inputValue).map(
                    (item, index) => {
                      const itemProps = getItemProps({ item, index });
                      return (
                        <ListBox.MenuItem
                          key={itemProps.id}
                          isActive={selectedItem === item}
                          tabIndex="-1"
                          isHighlighted={
                            highlightedIndex === index ||
                            (selectedItem && selectedItem.id === item.id) ||
                            false
                          }
                          title={itemToElement ? item.text : itemToString(item)}
                          {...itemProps}>
                          {itemToElement ? (
                            <ItemToElement key={itemProps.id} {...item} />
                          ) : (
                            itemToString(item)
                          )}
                          {selectedItem === item && (
                            <Checkmark16
                              className={`${prefix}--list-box__menu-item__selected-icon`}
                            />
                          )}
                        </ListBox.MenuItem>
                      );
                    }
                  )}
                </ListBox.Menu>
              )}
            </ListBox>
            {helperText && !invalid && (

                {helperText}

            )}

        )}
      </Downshift>
    );
  }
}
<commit_msg>fix(ComboBox): find highlighted index in correct items array (#6244)<commit_after>/**
 * Copyright IBM Corp. 2016, 2018
 *
 * This source code is licensed under the Apache-2.0 license found in the
 * LICENSE file in the root directory of this source tree.
 */

import cx from 'classnames';
import Downshift from 'downshift';
import PropTypes from 'prop-types';
import React from 'react';
import { settings } from 'carbon-components';
import { Checkmark16, WarningFilled16 } from '@carbon/icons-react';
import ListBox, { PropTypes as ListBoxPropTypes } from '../ListBox';
import { match, keys } from '../../internal/keyboard';
import setupGetInstanceId from '../../tools/setupGetInstanceId';

const { prefix } = settings;

const defaultItemToString = (item) => {
  if (typeof item === 'string') {
    return item;
  }

  return item && item.label;
};

const defaultShouldFilterItem = () => true;

const getInputValue = (props, state) => {
  if (props.selectedItem) {
    return props.itemToString(props.selectedItem);
  }
  // TODO: consistent `initialSelectedItem` behavior with other listbox components in v11
  if (props.initialSelectedItem) {
    return props.itemToString(props.initialSelectedItem);
  }

  return state.inputValue || '';
};

const findHighlightedIndex = ({ items, itemToString }, inputValue) => {
  if (!inputValue) {
    return -1;
  }

  const searchValue = inputValue.toLowerCase();

  for (let i = 0; i < items.length; i++) {
    const item = itemToString(items[i]).toLowerCase();
    if (item.indexOf(searchValue) !== -1) {
      return i;
    }
  }

  return -1;
};

const getInstanceId = setupGetInstanceId();

export default class ComboBox extends React.Component {
  static propTypes = {
    /**
     * 'aria-label' of the ListBox component.
     */
    ariaLabel: PropTypes.string,

    /**
     * An optional className to add to the container node
     */
    className: PropTypes.string,

    /**
     * Specify if the control should be disabled, or not
     */
    disabled: PropTypes.bool,

    /**
     * Specify a custom `id` for the input
     */
    id: PropTypes.string.isRequired,

    /**
     * Allow users to pass in an arbitrary item or a string (in case their items are an array of strings)
     * from their collection that are pre-selected
     */
    initialSelectedItem: PropTypes.oneOfType([
      PropTypes.object,
      PropTypes.string,
    ]),

    /**
     * We try to stay as generic as possible here to allow individuals to pass
     * in a collection of whatever kind of data structure they prefer
     */
    items: PropTypes.array.isRequired,

    /**
     * Helper function passed to downshift that allows the library to render a
     * given item to a string label. By default, it extracts the `label` field
     * from a given item to serve as the item label in the list
     */
    itemToString: PropTypes.func,

    /**
     * Optional function to render items as custom components instead of strings.
     * Defaults to null and is overriden by a getter
     */
    itemToElement: PropTypes.func,

    /**
     * `onChange` is a utility for this controlled component to communicate to a
     * consuming component when a specific dropdown item is selected.
     * @param {{ selectedItem }}
     */
    onChange: PropTypes.func.isRequired,

    /**
     * Used to provide a placeholder text node before a user enters any input.
     * This is only present if the control has no items selected
     */
    placeholder: PropTypes.string.isRequired,

    /**
     * Specify your own filtering logic by passing in a `shouldFilterItem`
     * function that takes in the current input and an item and passes back
     * whether or not the item should be filtered.
     */
    shouldFilterItem: PropTypes.func,

    /**
     * Specify if the currently selected value is invalid.
     */
    invalid: PropTypes.bool,

    /**
     * Message which is displayed if the value is invalid.
     */
    invalidText: PropTypes.string,

    /**
     * For full control of the selection
     */
    selectedItem: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),

    /**
     * Specify a custom translation function that takes in a message identifier
     * and returns the localized string for the message
     */
    translateWithId: PropTypes.func,

    /**
     * Currently supports either the default type, or an inline variant
     */
    type: ListBoxPropTypes.ListBoxType,

    /**
     * Specify the size of the ListBox. Currently supports either `sm`, `lg` or `xl` as an option.
     */
    size: ListBoxPropTypes.ListBoxSize,

    /**
     * Callback function to notify consumer when the text input changes.
     * This provides support to change available items based on the text.
     * @param {string} inputText
     */
    onInputChange: PropTypes.func,

    /**
     * should use "light theme" (white background)?
     */
    light: PropTypes.bool,

    /**
     * Additional props passed to Downshift
     */
    downshiftProps: PropTypes.shape(Downshift.propTypes),

    /**
     * Specify the direction of the combobox dropdown. Can be either top or bottom.
     */
    direction: PropTypes.oneOf(['top', 'bottom']),
  };

  static defaultProps = {
    disabled: false,
    itemToString: defaultItemToString,
    itemToElement: null,
    shouldFilterItem: defaultShouldFilterItem,
    type: 'default',
    ariaLabel: 'Choose an item',
    light: false,
    direction: 'bottom',
  };

  static getDerivedStateFromProps(nextProps, state) {
    const { prevSelectedItem, doneInitialSelectedItem } = state;
    const { selectedItem } = nextProps;
    if (!doneInitialSelectedItem || prevSelectedItem !== selectedItem) {
      return {
        doneInitialSelectedItem: true,
        prevSelectedItem: selectedItem,
        inputValue: getInputValue(nextProps, state),
      };
    }
    return null;
  }

  constructor(props) {
    super(props);

    this.textInput = React.createRef();

    this.comboBoxInstanceId = getInstanceId();

    this.state = {
      inputValue: getInputValue(props, {}),
    };
  }

  filterItems = (items, itemToString, inputValue) =>
    items.filter((item) =>
      this.props.shouldFilterItem({
        item,
        itemToString,
        inputValue,
      })
    );

  handleOnChange = (selectedItem) => {
    if (this.props.onChange) {
      this.props.onChange({ selectedItem });
    }
  };

  handleOnInputValueChange = (inputValue) => {
    const { onInputChange } = this.props;

    this.setState(
      () => ({
        // Default to empty string if we have a false-y `inputValue`
        inputValue: inputValue || '',
      }),
      () => {
        if (onInputChange) {
          onInputChange(inputValue);
        }
      }
    );
  };

  handleSelectionClear = () => {
    if (this.textInput?.current) {
      this.textInput.current.focus();
    }
  };

  handleOnStateChange = (newState, { setHighlightedIndex }) => {
    if (Object.prototype.hasOwnProperty.call(newState, 'inputValue')) {
      const { inputValue } = newState;
      const items = this.filterItems(
        this.props.items,
        this.props.itemToString,
        inputValue
      );
      setHighlightedIndex(
        findHighlightedIndex(
          {
            ...this.props,
            items,
          },
          inputValue
        )
      );
    }
  };

  onToggleClick = (isOpen) => (event) => {
    if (event.target === this.textInput.current && isOpen) {
      event.preventDownshiftDefault = true;
      event.persist();
    }
  };

  render() {
    const {
      className: containerClassName,
      disabled,
      id,
      items,
      itemToString,
      itemToElement,
      titleText,
      helperText,
      placeholder,
      initialSelectedItem,
      selectedItem,
      ariaLabel,
      translateWithId,
      invalid,
      invalidText,
      light,
      type, // eslint-disable-line no-unused-vars
      size,
      shouldFilterItem, // eslint-disable-line no-unused-vars
      onChange, // eslint-disable-line no-unused-vars
      onInputChange, // eslint-disable-line no-unused-vars
      downshiftProps,
      direction,
      ...rest
    } = this.props;
    const className = cx(`${prefix}--combo-box`, containerClassName, {
      [`${prefix}--list-box--up`]: direction === 'top',
    });
    const titleClasses = cx(`${prefix}--label`, {
      [`${prefix}--label--disabled`]: disabled,
    });
    const comboBoxHelperId = !helperText
      ? undefined
      : `combobox-helper-text-${this.comboBoxInstanceId}`;
    const helperClasses = cx(`${prefix}--form__helper-text`, {
      [`${prefix}--form__helper-text--disabled`]: disabled,
    });
    const wrapperClasses = cx(`${prefix}--list-box__wrapper`);
    const inputClasses = cx(`${prefix}--text-input`, {
      [`${prefix}--text-input--empty`]: !this.state.inputValue,
    });

    // needs to be Capitalized for react to render it correctly
    const ItemToElement = itemToElement;
    return (
      <Downshift
        {...downshiftProps}
        onChange={this.handleOnChange}
        onInputValueChange={this.handleOnInputValueChange}
        onStateChange={this.handleOnStateChange}
        inputValue={this.state.inputValue || ''}
        itemToString={itemToString}
        defaultSelectedItem={initialSelectedItem}
        selectedItem={selectedItem}>
        {({
          getToggleButtonProps,
          getInputProps,
          getItemProps,
          getLabelProps,
          isOpen,
          inputValue,
          selectedItem,
          highlightedIndex,
          clearSelection,
          toggleMenu,
          getMenuProps,
        }) => (

            {titleText && (

                {titleText}

            )}
            <ListBox
              className={className}
              disabled={disabled}
              invalid={invalid}
              aria-label={ariaLabel}
              invalidText={invalidText}
              isOpen={isOpen}
              light={light}
              size={size}>
              <ListBox.Field
                id={id}
                {...getToggleButtonProps({
                  disabled,
                  onClick: this.onToggleClick(isOpen),
                })}>
                <input
                  disabled={disabled}
                  className={inputClasses}
                  type="text"
                  tabIndex="0"
                  aria-autocomplete="list"
                  ref={this.textInput}
                  {...rest}
                  {...getInputProps({
                    disabled,
                    placeholder,
                    onKeyDown: (event) => {
                      if (match(event, keys.Space)) {
                        event.stopPropagation();
                      }

                      if (match(event, keys.Enter)) {
                        toggleMenu();
                      }
                    },
                  })}
                />
                {invalid && (
                  <WarningFilled16
                    className={`${prefix}--list-box__invalid-icon`}
                  />
                )}
                {inputValue && (
                  <ListBox.Selection
                    clearSelection={clearSelection}
                    translateWithId={translateWithId}
                    disabled={disabled}
                    onClearSelection={this.handleSelectionClear}
                  />
                )}
                <ListBox.MenuIcon
                  isOpen={isOpen}
                  translateWithId={translateWithId}
                />
              </ListBox.Field>
              {isOpen && (
                <ListBox.Menu
                  id={id}
                  {...getMenuProps({ 'aria-label': ariaLabel })}>
                  {this.filterItems(items, itemToString, inputValue).map(
                    (item, index) => {
                      const itemProps = getItemProps({ item, index });
                      return (
                        <ListBox.MenuItem
                          key={itemProps.id}
                          isActive={selectedItem === item}
                          tabIndex="-1"
                          isHighlighted={
                            highlightedIndex === index ||
                            (selectedItem && selectedItem.id === item.id) ||
                            false
                          }
                          title={itemToElement ? item.text : itemToString(item)}
                          {...itemProps}>
                          {itemToElement ? (
                            <ItemToElement key={itemProps.id} {...item} />
                          ) : (
                            itemToString(item)
                          )}
                          {selectedItem === item && (
                            <Checkmark16
                              className={`${prefix}--list-box__menu-item__selected-icon`}
                            />
                          )}
                        </ListBox.MenuItem>
                      );
                    }
                  )}
                </ListBox.Menu>
              )}
            </ListBox>
            {helperText && !invalid && (

                {helperText}

            )}

        )}
      </Downshift>
    );
  }
}
<|endoftext|>


<commit_before>Package.describe({
  name: 'space:accounts',
  version: '0.1.0',
  summary: 'Accounts module for Space applications',
  git: 'https://github.com/meteor-space/accounts.git',
  documentation: 'README.md'
});

Package.onUse(function(api) {

  api.versionsFrom('1.0');

  api.use([
    'coffeescript',
    'mongo',
    'accounts-password',
    'check',
    'space:event-sourcing@1.3.0',
  ]);

  api.addFiles([
    'source/server/module.coffee',
    'source/server/commands.coffee',
    'source/server/events.coffee',
    // USERS
    'source/server/users/user.coffee',
    'source/server/users/users-router.coffee'
  ], 'server');

});

Package.onTest(function(api) {

  api.use([
    'coffeescript',
    'mongo',
    'accounts-password',
    'space:accounts',
    'practicalmeteor:munit@2.1.4',
    'space:testing@1.4.3'
  ]);

  api.addFiles([
    'tests/test-app.coffee',
    'tests/meteor-users-dao.unit.coffee'
  ], 'server');

});
<commit_msg>updates to latest space:event-sourcing version<commit_after>Package.describe({
  name: 'space:accounts',
  version: '0.1.0',
  summary: 'Accounts module for Space applications',
  git: 'https://github.com/meteor-space/accounts.git',
  documentation: 'README.md'
});

Package.onUse(function(api) {

  api.versionsFrom('1.0');

  api.use([
    'coffeescript',
    'mongo',
    'accounts-password',
    'check',
    'space:event-sourcing@1.3.1',
  ]);

  api.addFiles([
    'source/server/module.coffee',
    'source/server/commands.coffee',
    'source/server/events.coffee',
    // USERS
    'source/server/users/user.coffee',
    'source/server/users/users-router.coffee'
  ], 'server');

});

Package.onTest(function(api) {

  api.use([
    'coffeescript',
    'mongo',
    'accounts-password',
    'space:accounts',
    'practicalmeteor:munit@2.1.4',
    'space:testing@1.4.3'
  ]);

  api.addFiles([
    'tests/test-app.coffee',
    'tests/meteor-users-dao.unit.coffee'
  ], 'server');

});
<|endoftext|>


<commit_before>/**
 * Copyright (c) 2013-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule RelayFileWriter
 * @flow
 */

'use strict';

const ASTConvert = require('ASTConvert');
const CodegenDirectory = require('CodegenDirectory');
const RelayCompiler = require('RelayCompiler');
const RelayCompilerContext = require('RelayCompilerContext');
const RelayValidator = require('RelayValidator');

const invariant = require('invariant');
const path = require('path');
const printFlowTypes = require('printFlowTypes');
const writeLegacyFlowFile = require('./writeLegacyFlowFile');
const writeRelayGeneratedFile = require('./writeRelayGeneratedFile');

const {isOperationDefinitionAST} = require('RelaySchemaUtils');
const {Map: ImmutableMap} = require('immutable');

import type {CompilerTransforms} from 'RelayCompiler';
import type {SchemaTransform} from 'RelayIRTransforms';
import type {
  DocumentNode,
  GraphQLSchema,
} from 'graphql';

type GenerateExtraFiles = (
  getOutputDirectory: (path?: string) => CodegenDirectory,
  compilerContext: RelayCompilerContext,
) => void;

export type WriterConfig = {
  baseDir: string,
  buildCommand: string,
  compilerTransforms: CompilerTransforms,
  generateExtraFiles?: GenerateExtraFiles,
  outputDir?: string,
  persistQuery?: (text: string) => Promise<string>,
  platform?: string,
  fragmentsWithLegacyFlowTypes?: Set<string>,
  schemaTransforms: Array<SchemaTransform>,
  relayRuntimeModule?: string,
};

/* eslint-disable no-console-disallow */

class RelayFileWriter {
  _onlyValidate: boolean;
  _config: WriterConfig;
  _baseSchema: GraphQLSchema;
  _baseDocuments: ImmutableMap<string, DocumentNode>;
  _documents: ImmutableMap<string, DocumentNode>;

  constructor(options: {
    config: WriterConfig,
    onlyValidate: boolean,
    baseDocuments: ImmutableMap<string, DocumentNode>,
    documents: ImmutableMap<string, DocumentNode>,
    schema: GraphQLSchema,
  }) {
    const {config, onlyValidate, baseDocuments, documents, schema} = options;
    this._baseDocuments = baseDocuments || ImmutableMap();
    this._baseSchema = schema;
    this._config = config;
    this._documents = documents;
    this._onlyValidate = onlyValidate;
  }

  async writeAll(): Promise<Map<string, CodegenDirectory>> {
    const tStart = Date.now();

    // Can't convert to IR unless the schema already has Relay-local extensions
    const transformedSchema = ASTConvert.transformASTSchema(
      this._baseSchema,
      this._config.schemaTransforms,
    );
    const extendedSchema = ASTConvert.extendASTSchema(
      transformedSchema,
      this._baseDocuments.merge(this._documents).valueSeq().toArray(),
    );

    // Build a context from all the documents
    const baseDefinitionNames = new Set();
    this._baseDocuments.forEach(doc => {
      doc.definitions.forEach(def => {
        if (isOperationDefinitionAST(def) && def.name) {
          baseDefinitionNames.add(def.name.value);
        }
      });
    });
    const definitionDirectories = new Map();
    const allOutputDirectories: Map<string, CodegenDirectory> = new Map();
    const addCodegenDir = dirPath => {
      const codegenDir = new CodegenDirectory(
        dirPath,
        {onlyValidate: this._onlyValidate},
      );
      allOutputDirectories.set(dirPath, codegenDir);
      return codegenDir;
    };

    let configOutputDirectory;
    if (this._config.outputDir) {
      configOutputDirectory = addCodegenDir(this._config.outputDir);
    } else {
      this._documents.forEach((doc, filePath) => {
        doc.definitions.forEach(def => {
          if (isOperationDefinitionAST(def) && def.name) {
            definitionDirectories.set(
              def.name.value,
              path.join(this._config.baseDir, path.dirname(filePath)),
            );
          }
        });
      });
    }

    const definitions = ASTConvert.convertASTDocumentsWithBase(
      extendedSchema,
      this._baseDocuments.valueSeq().toArray(),
      this._documents.valueSeq().toArray(),
      RelayValidator.LOCAL_RULES,
    );

    const compilerContext = new RelayCompilerContext(extendedSchema);
    const compiler = new RelayCompiler(
      this._baseSchema,
      compilerContext,
      this._config.compilerTransforms,
    );

    const getGeneratedDirectory = definitionName => {
      if (configOutputDirectory) {
        return configOutputDirectory;
      }
      const definitionDir = definitionDirectories.get(definitionName);
      invariant(
        definitionDir,
        'RelayFileWriter: Could not determine source directory for definition: %s',
        definitionName,
      );
      const generatedPath = path.join(
        definitionDir,
        '__generated__',
      );
      let cachedDir = allOutputDirectories.get(generatedPath);
      if (!cachedDir) {
        cachedDir = addCodegenDir(generatedPath);
      }
      return cachedDir;
    };

    const nodes = compiler.addDefinitions(definitions);

    const transformedQueryContext = compiler.transformedQueryContext();
    const compiledDocumentMap = compiler.compile();

    const tCompiled = Date.now();

    let tGenerated;
    try {
      await Promise.all(nodes.map(async (node) => {
        if (baseDefinitionNames.has(node.name)) {
          // don't add definitions that were part of base context
          return;
        }
        if (
          this._config.fragmentsWithLegacyFlowTypes &&
          this._config.fragmentsWithLegacyFlowTypes.has(node.name)
        ) {
          const legacyFlowTypes = printFlowTypes(node);
          if (legacyFlowTypes) {
            writeLegacyFlowFile(
              getGeneratedDirectory(node.name),
              node.name,
              legacyFlowTypes,
              this._config.buildCommand,
              this._config.platform,
            );
          }
        }

        const flowTypes = printFlowTypes(node);
        const compiledNode = compiledDocumentMap.get(node.name);
        invariant(
          compiledNode,
          'RelayCompiler: did not compile definition: %s',
          node.name,
        );
        await writeRelayGeneratedFile(
          getGeneratedDirectory(compiledNode.name),
          compiledNode,
          this._config.buildCommand,
          flowTypes,
          this.skipPersist ? null : this._config.persistQuery,
          this._config.platform,
          this._config.relayRuntimeModule || 'relay-runtime'
        );
      }));
      tGenerated = Date.now();

      if (this._config.generateExtraFiles) {
        const configDirectory = this._config.outputDir;
        invariant(
          configDirectory,
          'RelayFileWriter: cannot generate extra files without specifying ' +
          ' an outputDir in the config.'
        );

        this._config.generateExtraFiles(
          dir => {
            const outputDirectory = dir || configDirectory;
            let outputDir = allOutputDirectories.get(outputDirectory);
            if (!outputDir) {
              outputDir = addCodegenDir(outputDirectory);
            }
            return outputDir;
          },
          transformedQueryContext,
        );
      }

      // clean output directories
      allOutputDirectories.forEach(dir => {
        dir.deleteExtraFiles();
      });
    } catch (error) {
      tGenerated = Date.now();
      let details;
      try {
        details = JSON.parse(error.message);
      } catch (_) {
      }
      if (details && details.name === 'GraphQL2Exception' && details.message) {
        console.log('ERROR writing modules:\n' + details.message);
      } else {
        console.log('Error writing modules:\n' + error.toString());
      }
      return allOutputDirectories;
    }

    const tExtra = Date.now();
    console.log(
      'Writer time: %s [%s compiling, %s generating, %s extra]',
      toSeconds(tStart, tExtra),
      toSeconds(tStart, tCompiled),
      toSeconds(tCompiled, tGenerated),
      toSeconds(tGenerated, tExtra),
    );
    return allOutputDirectories;
  }
}

function toSeconds(t0, t1) {
  return ((t1 - t0) / 1000).toFixed(2) + 's';
}

module.exports = RelayFileWriter;
<commit_msg>switch fragments to new flow types<commit_after>/**
 * Copyright (c) 2013-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule RelayFileWriter
 * @flow
 */

'use strict';

const ASTConvert = require('ASTConvert');
const CodegenDirectory = require('CodegenDirectory');
const RelayCompiler = require('RelayCompiler');
const RelayCompilerContext = require('RelayCompilerContext');
const RelayFlowGenerator = require('RelayFlowGenerator');
const RelayValidator = require('RelayValidator');

const invariant = require('invariant');
const path = require('path');
const printFlowTypes = require('printFlowTypes');
const writeLegacyFlowFile = require('./writeLegacyFlowFile');
const writeRelayGeneratedFile = require('./writeRelayGeneratedFile');

const {isOperationDefinitionAST} = require('RelaySchemaUtils');
const {Map: ImmutableMap} = require('immutable');

import type {CompilerTransforms} from 'RelayCompiler';
import type {SchemaTransform} from 'RelayIRTransforms';
import type {
  DocumentNode,
  GraphQLSchema,
} from 'graphql';

type GenerateExtraFiles = (
  getOutputDirectory: (path?: string) => CodegenDirectory,
  compilerContext: RelayCompilerContext,
) => void;

export type WriterConfig = {
  baseDir: string,
  buildCommand: string,
  compilerTransforms: CompilerTransforms,
  generateExtraFiles?: GenerateExtraFiles,
  outputDir?: string,
  persistQuery?: (text: string) => Promise<string>,
  platform?: string,
  fragmentsWithLegacyFlowTypes?: Set<string>,
  schemaTransforms: Array<SchemaTransform>,
  relayRuntimeModule?: string,
};

/* eslint-disable no-console-disallow */

class RelayFileWriter {
  _onlyValidate: boolean;
  _config: WriterConfig;
  _baseSchema: GraphQLSchema;
  _baseDocuments: ImmutableMap<string, DocumentNode>;
  _documents: ImmutableMap<string, DocumentNode>;

  constructor(options: {
    config: WriterConfig,
    onlyValidate: boolean,
    baseDocuments: ImmutableMap<string, DocumentNode>,
    documents: ImmutableMap<string, DocumentNode>,
    schema: GraphQLSchema,
  }) {
    const {config, onlyValidate, baseDocuments, documents, schema} = options;
    this._baseDocuments = baseDocuments || ImmutableMap();
    this._baseSchema = schema;
    this._config = config;
    this._documents = documents;
    this._onlyValidate = onlyValidate;
  }

  async writeAll(): Promise<Map<string, CodegenDirectory>> {
    const tStart = Date.now();

    // Can't convert to IR unless the schema already has Relay-local extensions
    const transformedSchema = ASTConvert.transformASTSchema(
      this._baseSchema,
      this._config.schemaTransforms,
    );
    const extendedSchema = ASTConvert.extendASTSchema(
      transformedSchema,
      this._baseDocuments.merge(this._documents).valueSeq().toArray(),
    );

    // Build a context from all the documents
    const baseDefinitionNames = new Set();
    this._baseDocuments.forEach(doc => {
      doc.definitions.forEach(def => {
        if (isOperationDefinitionAST(def) && def.name) {
          baseDefinitionNames.add(def.name.value);
        }
      });
    });
    const definitionDirectories = new Map();
    const allOutputDirectories: Map<string, CodegenDirectory> = new Map();
    const addCodegenDir = dirPath => {
      const codegenDir = new CodegenDirectory(
        dirPath,
        {onlyValidate: this._onlyValidate},
      );
      allOutputDirectories.set(dirPath, codegenDir);
      return codegenDir;
    };

    let configOutputDirectory;
    if (this._config.outputDir) {
      configOutputDirectory = addCodegenDir(this._config.outputDir);
    } else {
      this._documents.forEach((doc, filePath) => {
        doc.definitions.forEach(def => {
          if (isOperationDefinitionAST(def) && def.name) {
            definitionDirectories.set(
              def.name.value,
              path.join(this._config.baseDir, path.dirname(filePath)),
            );
          }
        });
      });
    }

    const definitions = ASTConvert.convertASTDocumentsWithBase(
      extendedSchema,
      this._baseDocuments.valueSeq().toArray(),
      this._documents.valueSeq().toArray(),
      RelayValidator.LOCAL_RULES,
    );

    const compilerContext = new RelayCompilerContext(extendedSchema);
    const compiler = new RelayCompiler(
      this._baseSchema,
      compilerContext,
      this._config.compilerTransforms,
    );

    const getGeneratedDirectory = definitionName => {
      if (configOutputDirectory) {
        return configOutputDirectory;
      }
      const definitionDir = definitionDirectories.get(definitionName);
      invariant(
        definitionDir,
        'RelayFileWriter: Could not determine source directory for definition: %s',
        definitionName,
      );
      const generatedPath = path.join(
        definitionDir,
        '__generated__',
      );
      let cachedDir = allOutputDirectories.get(generatedPath);
      if (!cachedDir) {
        cachedDir = addCodegenDir(generatedPath);
      }
      return cachedDir;
    };

    const nodes = compiler.addDefinitions(definitions);

    const transformedQueryContext = compiler.transformedQueryContext();
    const compiledDocumentMap = compiler.compile();

    const tCompiled = Date.now();

    let tGenerated;
    try {
      await Promise.all(nodes.map(async (node) => {
        if (baseDefinitionNames.has(node.name)) {
          // don't add definitions that were part of base context
          return;
        }
        if (
          this._config.fragmentsWithLegacyFlowTypes &&
          this._config.fragmentsWithLegacyFlowTypes.has(node.name)
        ) {
          const legacyFlowTypes = printFlowTypes(node);
          if (legacyFlowTypes) {
            writeLegacyFlowFile(
              getGeneratedDirectory(node.name),
              node.name,
              legacyFlowTypes,
              this._config.buildCommand,
              this._config.platform,
            );
          }
        }

        const flowTypes = node.kind === 'Fragment' ?
          RelayFlowGenerator.generate(node) :
          printFlowTypes(node);
        const compiledNode = compiledDocumentMap.get(node.name);
        invariant(
          compiledNode,
          'RelayCompiler: did not compile definition: %s',
          node.name,
        );
        await writeRelayGeneratedFile(
          getGeneratedDirectory(compiledNode.name),
          compiledNode,
          this._config.buildCommand,
          flowTypes,
          this.skipPersist ? null : this._config.persistQuery,
          this._config.platform,
          this._config.relayRuntimeModule || 'relay-runtime'
        );
      }));
      tGenerated = Date.now();

      if (this._config.generateExtraFiles) {
        const configDirectory = this._config.outputDir;
        invariant(
          configDirectory,
          'RelayFileWriter: cannot generate extra files without specifying ' +
          ' an outputDir in the config.'
        );

        this._config.generateExtraFiles(
          dir => {
            const outputDirectory = dir || configDirectory;
            let outputDir = allOutputDirectories.get(outputDirectory);
            if (!outputDir) {
              outputDir = addCodegenDir(outputDirectory);
            }
            return outputDir;
          },
          transformedQueryContext,
        );
      }

      // clean output directories
      allOutputDirectories.forEach(dir => {
        dir.deleteExtraFiles();
      });
    } catch (error) {
      tGenerated = Date.now();
      let details;
      try {
        details = JSON.parse(error.message);
      } catch (_) {
      }
      if (details && details.name === 'GraphQL2Exception' && details.message) {
        console.log('ERROR writing modules:\n' + details.message);
      } else {
        console.log('Error writing modules:\n' + error.toString());
      }
      return allOutputDirectories;
    }

    const tExtra = Date.now();
    console.log(
      'Writer time: %s [%s compiling, %s generating, %s extra]',
      toSeconds(tStart, tExtra),
      toSeconds(tStart, tCompiled),
      toSeconds(tCompiled, tGenerated),
      toSeconds(tGenerated, tExtra),
    );
    return allOutputDirectories;
  }
}

function toSeconds(t0, t1) {
  return ((t1 - t0) / 1000).toFixed(2) + 's';
}

module.exports = RelayFileWriter;
<|endoftext|>


<commit_before>_r(function (app) {
  if ( ! window.app.views.hasOwnProperty('user')) {
    app.views.user = {};
  }

  var isNotLoggedIn = function () {
    if ( this.action === 'login' && ! this.$el.hasClass('main_content')) {
      return true; // this is a popup and probably results from a 401 ajax response
    }
    return ! isLoggedIn.call(this);
  }

  var isLoggedIn = function () {
    return app.user_self.get('name');
  }

  /**
   * #/user/index
   */
  app.views.user.index = app.base.listView.extend({

    collection: app.collections.User,
    auto_render: true

  });

  /**
   * #/user/register
   */
  app.views.user.register = app.base.formView.extend({

    auto_render: true,
    model: app.models.User,
    max_age: 0,
    checkAllowed: isNotLoggedIn,
    wait_for_user_loaded: false,
    reload_on_login: true,

    render: function () {
      if (app.user_self.get('name')) {
        app.back();
      } else {
        app.base.formView.prototype.render.apply(this, _.toArray(arguments));
      }
    },

    saved: function () {
      this.reload_on_login = false; // we only want to reload if the login is not from here.
      app.once('user_loaded', function () {
        app.go('user/profile/');
        app.trigger('login');
      });
      app.user_self.load();
    }

  });

  /**
   * #/user/profile
   */
  app.views.user.profile = app.base.pageView.extend({

    auto_render: true,
    model: app.models.User,

    load: function (callback) {
      var self = this;
      var id = this.params[0] || app.user_self.id;
      this.model.set({'id': id});
      this.model.fetch()
        .always(function (res) {
          callback(res.status >= 400, self.model);
        });
    }

  });

  /**
   * #/user/edit_profile(/:id)
   */
  app.views.user.edit_profile = app.base.formView.extend({

    auto_render: true,
    model: app.models.User,
    max_age: 0,

    checkAllowed: isLoggedIn,
    edit_is_self: false,

    events: {
      'click form.acl label a': 'markAclInputs',
      'change .acl :checkbox': 'changeAcl',
      'change .admin :checkbox': 'changeAdmin'
    },

    init: function () {
      var default_acl = ['view', 'list', 'create', 'edit', 'delete'];
      this.addLocals({
        acl: {
          'User': ['self'].concat(default_acl.concat(['grant'])),
          'Show': default_acl,
          'Episode': default_acl
        }
      });
    },

    load: function (callback) {
      if (this.params[0] && app.user_self.may('edit', 'User')) {
        this.model.id = this.params[0];
      } else {
        this.model.id = app.user_self.id;
        this.edit_is_self = true;
      }
      this.model.fetch(function (user, response) {
        callback(null, user);
      });
    },

    saved: function () {
      if (this.edit_is_self) {
        app.user_self.set(this.model.toJSON());
      }
      app.go('user/profile/'+this.model.id);
    },

    markAclInputs: function (e) {
      var $target = $(e.target);
      var boxes = $target.closest('.control-group').find(':checkbox');
      if ($target.hasClass('setRead')) {
        boxes
          .filter('[data-action="view"], [data-action="list"]')
          .click();
      }
      if ($target.hasClass('setWrite')) {
        boxes
          .filter('[data-action="create"], [data-action="edit"], [data-action="delete"]')
          .click();
      }
    },

    getChangeCallback: function ($target, checked) {
      return function (err) {
        var class_name = 'success_blink';
        if (err) {
          class_name = 'error_blink';
          $target.prop('checked', !checked);
        }
        var $parent = $target.parent().addClass(class_name);
        setTimeout(function () {
          $target.attr('disabled', false);
          $parent.removeClass(class_name);
        }, 500);
      };
    },

    changeAcl: function (e) {
      var $target = $(e.target);
      var checked = $target.prop('checked');
      var allow_or_deny = checked ? 'allow' : 'deny';
      $target.attr('disabled', true);
      this.model.changeAcl(allow_or_deny, $target.data('action'), $target.data('subject'), this.getChangeCallback($target, checked));
    },

    changeAdmin: function (e) {
      var self = this;
      var $target = $(e.target);
      var checked = $target.prop('checked');
      var name = this.model.get('name');
      var done = self.getChangeCallback($target, checked);
      var cancel = function () {
        $target.prop('checked', !checked);
        $target.attr('disabled', false);
      };

      $target.attr('disabled', true);

      if ( ! checked) {
        if (this.model.id === app.user_self.id) {
          app.overlay({
            view: 'confirm_take_self_admin',
            locals: {
              text: this._t('admin.self_warning')
            },
            confirm: function () {
              self.model.changeAdmin(false, done);
            },
            cancel: cancel
          });
        } else {
          self.model.changeAdmin(false, done);
        }
      } else {
        app.overlay({
          view: 'confirm_grant_admin',
          locals: {
            text: this._t(['admin.grant_warning', name])
          },
          confirm: function () {
            self.model.changeAdmin(true, done);
          },
          cancel: cancel
        });
      }
    }

  });

  /**
   * #/user/login or manual call
   */
  app.views.user.login = app.base.formView.extend({

    model: app.models.Self,

    auto_render: true,
    max_age: 0,
    wait_for_user_loaded: false,
    requires_login: false,
    reload_on_login: false,

    /**
     * Login successful
     */
    saved: function () {
      $.jGrowl('Login successful');
      app.user_self.set(this.model.toJSON());
      app.trigger('login');

      if (app.current.view instanceof app.views.user.login) {
        app.go('#');
      }
    }

  });

  /**
   * #/user/logout
   */
  app.views.user.logout = app.base.pageView.extend({
    init: function () {
      if (app.user_self) {
        app.user_self.logout();
      }
    }
  });

  /**
   * Userbox
   */
  app.views.userbox = app.base.pageView.extend({

    requires_login: false,

    model: app.user_self,

    module: 'user',
    action: 'userbox',

    $el: $('#userbox'),
    auto_render: true,

    init: function () {
      this.model.bind('change:name', this.render);
    },

    load: function (callback) {
      callback(null, app.user_self);
    }
  });

});<commit_msg>Fix userView to use new login checks<commit_after>_r(function (app) {
  if ( ! window.app.views.hasOwnProperty('user')) {
    app.views.user = {};
  }

  /**
   * #/user/index
   */
  app.views.user.index = app.base.listView.extend({

    collection: app.collections.User,
    auto_render: true

  });

  /**
   * #/user/register
   */
  app.views.user.register = app.base.formView.extend({

    auto_render: true,
    model: app.models.User,
    max_age: 0,
    wait_for_user_loaded: false,
    reload_on_login: true,
    requires_login: false,

    render: function () {
      if (app.user_self.get('name')) {
        app.back();
      } else {
        app.base.formView.prototype.render.apply(this, _.toArray(arguments));
      }
    },

    saved: function () {
      this.reload_on_login = false; // we only want to reload if the login is not from here.
      app.once('user_loaded', function () {
        app.go('user/profile/');
        app.trigger('login');
      });
      app.user_self.load();
    }

  });

  /**
   * #/user/profile
   */
  app.views.user.profile = app.base.pageView.extend({

    auto_render: true,
    model: app.models.User,

    load: function (callback) {
      var self = this;
      var id = this.params[0] || app.user_self.id;
      this.model.set({'id': id});
      this.model.fetch()
        .always(function (res) {
          callback(res.status >= 400, self.model);
        });
    }

  });

  /**
   * #/user/edit_profile(/:id)
   */
  app.views.user.edit_profile = app.base.formView.extend({

    auto_render: true,
    model: app.models.User,
    max_age: 0,

    edit_is_self: false,

    events: {
      'click form.acl label a': 'markAclInputs',
      'change .acl :checkbox': 'changeAcl',
      'change .admin :checkbox': 'changeAdmin'
    },

    init: function () {
      var default_acl = ['view', 'list', 'create', 'edit', 'delete'];
      this.addLocals({
        acl: {
          'User': ['self'].concat(default_acl.concat(['grant'])),
          'Show': default_acl,
          'Episode': default_acl
        }
      });
    },

    load: function (callback) {
      if (this.params[0] && app.user_self.may('edit', 'User')) {
        this.model.id = this.params[0];
      } else {
        this.model.id = app.user_self.id;
        this.edit_is_self = true;
      }
      this.model.fetch(function (user, response) {
        callback(null, user);
      });
    },

    saved: function () {
      if (this.edit_is_self) {
        app.user_self.set(this.model.toJSON());
      }
      app.go('user/profile/'+this.model.id);
    },

    markAclInputs: function (e) {
      var $target = $(e.target);
      var boxes = $target.closest('.control-group').find(':checkbox');
      if ($target.hasClass('setRead')) {
        boxes
          .filter('[data-action="view"], [data-action="list"]')
          .click();
      }
      if ($target.hasClass('setWrite')) {
        boxes
          .filter('[data-action="create"], [data-action="edit"], [data-action="delete"]')
          .click();
      }
    },

    getChangeCallback: function ($target, checked) {
      return function (err) {
        var class_name = 'success_blink';
        if (err) {
          class_name = 'error_blink';
          $target.prop('checked', !checked);
        }
        var $parent = $target.parent().addClass(class_name);
        setTimeout(function () {
          $target.attr('disabled', false);
          $parent.removeClass(class_name);
        }, 500);
      };
    },

    changeAcl: function (e) {
      var $target = $(e.target);
      var checked = $target.prop('checked');
      var allow_or_deny = checked ? 'allow' : 'deny';
      $target.attr('disabled', true);
      this.model.changeAcl(allow_or_deny, $target.data('action'), $target.data('subject'), this.getChangeCallback($target, checked));
    },

    changeAdmin: function (e) {
      var self = this;
      var $target = $(e.target);
      var checked = $target.prop('checked');
      var name = this.model.get('name');
      var done = self.getChangeCallback($target, checked);
      var cancel = function () {
        $target.prop('checked', !checked);
        $target.attr('disabled', false);
      };

      $target.attr('disabled', true);

      if ( ! checked) {
        if (this.model.id === app.user_self.id) {
          app.overlay({
            view: 'confirm_take_self_admin',
            locals: {
              text: this._t('admin.self_warning')
            },
            confirm: function () {
              self.model.changeAdmin(false, done);
            },
            cancel: cancel
          });
        } else {
          self.model.changeAdmin(false, done);
        }
      } else {
        app.overlay({
          view: 'confirm_grant_admin',
          locals: {
            text: this._t(['admin.grant_warning', name])
          },
          confirm: function () {
            self.model.changeAdmin(true, done);
          },
          cancel: cancel
        });
      }
    }

  });

  /**
   * #/user/login or manual call
   */
  app.views.user.login = app.base.formView.extend({

    model: app.models.Self,

    auto_render: true,
    max_age: 0,
    wait_for_user_loaded: false,
    requires_login: false,
    reload_on_login: false,

    /**
     * Login successful
     */
    saved: function () {
      $.jGrowl('Login successful');
      app.user_self.set(this.model.toJSON());
      app.trigger('login');

      if (app.current.view instanceof app.views.user.login) {
        app.go('#');
      }
    }

  });

  /**
   * #/user/logout
   */
  app.views.user.logout = app.base.pageView.extend({
    init: function () {
      if (app.user_self) {
        app.user_self.logout();
      }
    }
  });

  /**
   * Userbox
   */
  app.views.userbox = app.base.pageView.extend({

    requires_login: false,

    model: app.user_self,

    module: 'user',
    action: 'userbox',

    $el: $('#userbox'),
    auto_render: true,

    init: function () {
      this.model.bind('change:name', this.render);
    },

    load: function (callback) {
      callback(null, app.user_self);
    }
  });

});<|endoftext|>


<commit_before>module.exports = {
  title: 'Information collection notice',
  body: 'The information in this application is collected by the Ministry of Health under section 26(a) of the Freedom of Information and Protection of Privacy Act and will be used to determine eligibility for provincial health care benefits in BC and administer premium assistance. Should you have any questions about the collection of this personal information please contact Health Insurance of BC .',
  agreeLabel: 'I have read and understand the information above and would like to continue to apply for MSP',
  continueButton: 'Continue'

}<commit_msg>Capitalize Premium Assistance<commit_after>module.exports = {
  title: 'Information collection notice',
  body: 'The information in this application is collected by the Ministry of Health under section 26(a) of the Freedom of Information and Protection of Privacy Act and will be used to determine eligibility for provincial health care benefits in BC and administer Premium Assistance. Should you have any questions about the collection of this personal information please contact Health Insurance of BC .',
  agreeLabel: 'I have read and understand the information above and would like to continue to apply for MSP',
  continueButton: 'Continue'

}
<|endoftext|>


<commit_before>// JavaScript Document
<commit_msg>Remove unnecessary file<commit_after><|endoftext|>


<commit_before>
'use strict';

/* globals jQuery, ajaxify, define, config, socket, app, utils, $, window */

define('composer/tags', function () {
	var tags = {};

	var minTags;
	var maxTags;

	tags.init = function (postContainer, postData) {
		var tagEl = postContainer.find('.tags');
		if (!tagEl.length) {
			return;
		}

		minTags = ajaxify.data.hasOwnProperty('minTags') ? ajaxify.data.minTags : config.minimumTagsPerTopic;
		maxTags = ajaxify.data.hasOwnProperty('maxTags') ? ajaxify.data.maxTags : config.maximumTagsPerTopic;

		tagEl.tagsinput({
			confirmKeys: [13, 44],
			trimValue: true,
		});
		var input = postContainer.find('.bootstrap-tagsinput input');

		toggleTagInput(postContainer, postData, ajaxify.data);

		app.loadJQueryUI(function () {
			input.autocomplete({
				delay: 100,
				position: { my: 'left bottom', at: 'left top', collision: 'flip' },
				appendTo: postContainer.find('.bootstrap-tagsinput'),
				open: function () {
					$(this).autocomplete('widget').css('z-index', 20000);
				},
				source: function (request, response) {
					socket.emit('topics.autocompleteTags', {
						query: request.term,
						cid: postData.cid,
					}, function (err, tags) {
						if (err) {
							return app.alertError(err.message);
						}
						if (tags) {
							response(tags);
						}
						$('.ui-autocomplete a').attr('data-ajaxify', 'false');
					});
				},
				select: function (/* event, ui */) {
					// when autocomplete is selected from the dropdown simulate a enter key down to turn it into a tag
					triggerEnter(input);
				},
			});

			addTags(postData.tags, tagEl);

			tagEl.on('beforeItemAdd', function (event) {
				var reachedMaxTags = maxTags && maxTags <= tags.getTags(postContainer.attr('data-uuid')).length;
				var cleanTag = utils.cleanUpTag(event.item, config.maximumTagLength);
				var different = cleanTag !== event.item;
				event.cancel = different ||
					event.item.length < config.minimumTagLength ||
					event.item.length > config.maximumTagLength ||
					reachedMaxTags;

				if (event.item.length < config.minimumTagLength) {
					return app.alertError('[[error:tag-too-short, ' + config.minimumTagLength + ']]');
				} else if (event.item.length > config.maximumTagLength) {
					return app.alertError('[[error:tag-too-long, ' + config.maximumTagLength + ']]');
				} else if (reachedMaxTags) {
					return app.alertError('[[error:too-many-tags, ' + maxTags + ']]');
				}
				if (different) {
					tagEl.tagsinput('add', cleanTag);
				}
			});

			var readdSystemTag = false;
			tagEl.on('itemRemoved', function (event) {
				socket.emit('topics.canRemoveTag', { tag: event.item }, function (err, allowed) {
					if (err) {
						return app.alertError(err.message);
					}
					if (!allowed) {
						app.alertError('[[error:cant-remove-system-tag]]');
						readdSystemTag = true;
						tagEl.tagsinput('add', event.item);
					}
				});
			});

			tagEl.on('itemAdded', function (event) {
				if (readdSystemTag) {
					readdSystemTag = false;
					return;
				}
				var cid = postData.hasOwnProperty('cid') ? postData.cid : ajaxify.data.cid;
				socket.emit('topics.isTagAllowed', { tag: event.item, cid: cid || 0 }, function (err, allowed) {
					if (err) {
						return app.alertError(err.message);
					}
					if (!allowed) {
						return tagEl.tagsinput('remove', event.item);
					}
					$(window).trigger('action:tag.added', { cid: cid, tagEl: tagEl, tag: event.item });
					if (input.length) {
						input.autocomplete('close');
					}
				});
			});
		});

		input.attr('tabIndex', tagEl.attr('tabIndex'));
		input.on('blur', function () {
			triggerEnter(input);
		});

		$('[component="composer/tag/dropdown"]').on('click', 'li', function () {
			var tag = $(this).attr('data-tag');
			if (tag) {
				addTags([tag], tagEl);
			}
			return false;
		});
	};

	tags.isEnoughTags = function (post_uuid) {
		return tags.getTags(post_uuid).length >= minTags;
	};

	tags.minTagCount = function () {
		return minTags;
	};

	tags.onChangeCategory = function (postContainer, postData, cid, categoryData) {
		var tagDropdown = postContainer.find('[component="composer/tag/dropdown"]');
		if (!tagDropdown.length) {
			return;
		}

		toggleTagInput(postContainer, postData, categoryData);
		tagDropdown.toggleClass('hidden', !categoryData.tagWhitelist || !categoryData.tagWhitelist.length);
		if (categoryData.tagWhitelist) {
			app.parseAndTranslate('composer', 'tagWhitelist', { tagWhitelist: categoryData.tagWhitelist }, function (html) {
				tagDropdown.find('.dropdown-menu').html(html);
			});
		}
	};

	function toggleTagInput(postContainer, postData, data) {
		var tagEl = postContainer.find('.tags');
		var input = postContainer.find('.bootstrap-tagsinput input');
		if (!input.length) {
			return;
		}

		if (data.hasOwnProperty('minTags')) {
			minTags = data.minTags;
		}
		if (data.hasOwnProperty('maxTags')) {
			maxTags = data.maxTags;
		}

		if (data.tagWhitelist && data.tagWhitelist.length) {
			input.attr('readonly', '');
			input.attr('placeholder', '');

			tagEl.tagsinput('items').slice().forEach(function (tag) {
				if (data.tagWhitelist.indexOf(tag) === -1) {
					tagEl.tagsinput('remove', tag);
				}
			});
		} else {
			input.removeAttr('readonly');
			input.attr('placeholder', postContainer.find('input.tags').attr('placeholder'));
		}

		postContainer.find('.tags-container').toggleClass('hidden', (
			data.privileges && data.privileges.hasOwnProperty('topics:tag') && !data.privileges['topics:tag']) ||
			(maxTags === 0 && !postData.tags.length)
		);

		if (data.privileges && data.privileges.hasOwnProperty('topics:tag') && !data.privileges['topics:tag']) {
			tagEl.tagsinput('removeAll');
		}

		$(window).trigger('action:tag.toggleInput', {
			postContainer: postContainer,
			tagWhitelist: data.tagWhitelist,
			tagsInput: input,
		});
	}

	function triggerEnter(input) {
		// http://stackoverflow.com/a/3276819/583363
		var e = jQuery.Event('keypress');
		e.which = 13;
		e.keyCode = 13;
		setTimeout(function () {
			input.trigger(e);
		}, 100);
	}

	function addTags(tags, tagEl) {
		if (tags && tags.length) {
			for (var i = 0; i < tags.length; ++i) {
				tagEl.tagsinput('add', tags[i]);
			}
		}
	}

	tags.getTags = function (post_uuid) {
		return $('.composer[data-uuid="' + post_uuid + '"] .tags').tagsinput('items');
	};

	return tags;
});
<commit_msg>fix: adding system tag, better names<commit_after>
'use strict';

/* globals jQuery, ajaxify, define, config, socket, app, utils, $, window */

define('composer/tags', function () {
	var tags = {};

	var minTags;
	var maxTags;

	tags.init = function (postContainer, postData) {
		var tagEl = postContainer.find('.tags');
		if (!tagEl.length) {
			return;
		}

		minTags = ajaxify.data.hasOwnProperty('minTags') ? ajaxify.data.minTags : config.minimumTagsPerTopic;
		maxTags = ajaxify.data.hasOwnProperty('maxTags') ? ajaxify.data.maxTags : config.maximumTagsPerTopic;

		tagEl.tagsinput({
			confirmKeys: [13, 44],
			trimValue: true,
		});
		var input = postContainer.find('.bootstrap-tagsinput input');

		toggleTagInput(postContainer, postData, ajaxify.data);

		app.loadJQueryUI(function () {
			input.autocomplete({
				delay: 100,
				position: { my: 'left bottom', at: 'left top', collision: 'flip' },
				appendTo: postContainer.find('.bootstrap-tagsinput'),
				open: function () {
					$(this).autocomplete('widget').css('z-index', 20000);
				},
				source: function (request, response) {
					socket.emit('topics.autocompleteTags', {
						query: request.term,
						cid: postData.cid,
					}, function (err, tags) {
						if (err) {
							return app.alertError(err.message);
						}
						if (tags) {
							response(tags);
						}
						$('.ui-autocomplete a').attr('data-ajaxify', 'false');
					});
				},
				select: function (/* event, ui */) {
					// when autocomplete is selected from the dropdown simulate a enter key down to turn it into a tag
					triggerEnter(input);
				},
			});

			addTags(postData.tags, tagEl);

			tagEl.on('beforeItemAdd', function (event) {
				var reachedMaxTags = maxTags && maxTags <= tags.getTags(postContainer.attr('data-uuid')).length;
				var cleanTag = utils.cleanUpTag(event.item, config.maximumTagLength);
				var different = cleanTag !== event.item;
				event.cancel = different ||
					event.item.length < config.minimumTagLength ||
					event.item.length > config.maximumTagLength ||
					reachedMaxTags;

				if (event.item.length < config.minimumTagLength) {
					return app.alertError('[[error:tag-too-short, ' + config.minimumTagLength + ']]');
				} else if (event.item.length > config.maximumTagLength) {
					return app.alertError('[[error:tag-too-long, ' + config.maximumTagLength + ']]');
				} else if (reachedMaxTags) {
					return app.alertError('[[error:too-many-tags, ' + maxTags + ']]');
				}
				if (different) {
					tagEl.tagsinput('add', cleanTag);
				}
			});

			var skipAddCheck = false;
			var skipRemoveCheck = false;
			tagEl.on('itemRemoved', function (event) {
				if (skipRemoveCheck) {
					skipRemoveCheck = false;
					return;
				}
				socket.emit('topics.canRemoveTag', { tag: event.item }, function (err, allowed) {
					if (err) {
						return app.alertError(err.message);
					}
					if (!allowed) {
						app.alertError('[[error:cant-remove-system-tag]]');
						skipAddCheck = true;
						tagEl.tagsinput('add', event.item);
					}
				});
			});

			tagEl.on('itemAdded', function (event) {
				if (skipAddCheck) {
					skipAddCheck = false;
					return;
				}
				var cid = postData.hasOwnProperty('cid') ? postData.cid : ajaxify.data.cid;
				socket.emit('topics.isTagAllowed', { tag: event.item, cid: cid || 0 }, function (err, allowed) {
					if (err) {
						return app.alertError(err.message);
					}
					if (!allowed) {
						skipRemoveCheck = true;
						return tagEl.tagsinput('remove', event.item);
					}
					$(window).trigger('action:tag.added', { cid: cid, tagEl: tagEl, tag: event.item });
					if (input.length) {
						input.autocomplete('close');
					}
				});
			});
		});

		input.attr('tabIndex', tagEl.attr('tabIndex'));
		input.on('blur', function () {
			triggerEnter(input);
		});

		$('[component="composer/tag/dropdown"]').on('click', 'li', function () {
			var tag = $(this).attr('data-tag');
			if (tag) {
				addTags([tag], tagEl);
			}
			return false;
		});
	};

	tags.isEnoughTags = function (post_uuid) {
		return tags.getTags(post_uuid).length >= minTags;
	};

	tags.minTagCount = function () {
		return minTags;
	};

	tags.onChangeCategory = function (postContainer, postData, cid, categoryData) {
		var tagDropdown = postContainer.find('[component="composer/tag/dropdown"]');
		if (!tagDropdown.length) {
			return;
		}

		toggleTagInput(postContainer, postData, categoryData);
		tagDropdown.toggleClass('hidden', !categoryData.tagWhitelist || !categoryData.tagWhitelist.length);
		if (categoryData.tagWhitelist) {
			app.parseAndTranslate('composer', 'tagWhitelist', { tagWhitelist: categoryData.tagWhitelist }, function (html) {
				tagDropdown.find('.dropdown-menu').html(html);
			});
		}
	};

	function toggleTagInput(postContainer, postData, data) {
		var tagEl = postContainer.find('.tags');
		var input = postContainer.find('.bootstrap-tagsinput input');
		if (!input.length) {
			return;
		}

		if (data.hasOwnProperty('minTags')) {
			minTags = data.minTags;
		}
		if (data.hasOwnProperty('maxTags')) {
			maxTags = data.maxTags;
		}

		if (data.tagWhitelist && data.tagWhitelist.length) {
			input.attr('readonly', '');
			input.attr('placeholder', '');

			tagEl.tagsinput('items').slice().forEach(function (tag) {
				if (data.tagWhitelist.indexOf(tag) === -1) {
					tagEl.tagsinput('remove', tag);
				}
			});
		} else {
			input.removeAttr('readonly');
			input.attr('placeholder', postContainer.find('input.tags').attr('placeholder'));
		}

		postContainer.find('.tags-container').toggleClass('hidden', (
			data.privileges && data.privileges.hasOwnProperty('topics:tag') && !data.privileges['topics:tag']) ||
			(maxTags === 0 && !postData.tags.length)
		);

		if (data.privileges && data.privileges.hasOwnProperty('topics:tag') && !data.privileges['topics:tag']) {
			tagEl.tagsinput('removeAll');
		}

		$(window).trigger('action:tag.toggleInput', {
			postContainer: postContainer,
			tagWhitelist: data.tagWhitelist,
			tagsInput: input,
		});
	}

	function triggerEnter(input) {
		// http://stackoverflow.com/a/3276819/583363
		var e = jQuery.Event('keypress');
		e.which = 13;
		e.keyCode = 13;
		setTimeout(function () {
			input.trigger(e);
		}, 100);
	}

	function addTags(tags, tagEl) {
		if (tags && tags.length) {
			for (var i = 0; i < tags.length; ++i) {
				tagEl.tagsinput('add', tags[i]);
			}
		}
	}

	tags.getTags = function (post_uuid) {
		return $('.composer[data-uuid="' + post_uuid + '"] .tags').tagsinput('items');
	};

	return tags;
});
<|endoftext|>


<commit_before>var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
//app 是一个监听函数
var app = express();
//使用此中间件后会在req.cookies=cookieObj
app.use(cookieParser());
//如何写cookie  res.cookie(name,value); res.cookie('count',1);
app.get('/visit',function(req,res){

});

app.listen(8080);<commit_msg>cookie的原理<commit_after>var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
//app 是一个监听函数
var app = express();
//使用此中间件后会在req.cookies=cookieObj
app.use(cookieParser());
//如何写cookie  res.cookie(name,value); res.cookie('count',1);
// res.setHeader('Set-Cookie','count=1');
/**
 * 1. 获取请求对象中的cookie  req.cookies
 * 2. 如何向客户端写入cookie res.cookie()
 */
app.get('/visit',function(req,res){
    //得到cookie对象
   var cookie = req.cookies;
    //判断此对象如果存在并且count有值的话
    if(cookie && cookie.count){
        cookie.count++;
        res.cookie('count',cookie.count);
        res.send('欢迎你老顾客的第'+cookie.count+'次光临')
    }else {
        res.cookie('count',1);
        res.send('欢迎你新顾客的第1次光临')
    }
});

app.listen(8080);<|endoftext|>


<commit_before>/* eslint-disable no-undef */
const showdown = require('showdown');
const fs = require('fs');
const data = fs.readFileSync('README.md');
const converter = new showdown.Converter();
const html = converter.makeHtml(data.toString());
const htmlBody = `
<!DOCTYPE html>

  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="./demo/assets/vendor.css">
  <link rel="stylesheet" href="./assets/app.css">
  <title>Ember Semantic Ui</title>

          ember-semantic-ui

                Github

                Wecatch

Docs

Demo

  ${html}

`;

fs.writeFileSync('index.html', htmlBody);
<commit_msg>fix readme<commit_after>/* eslint-disable no-undef */
const showdown = require('showdown');
const fs = require('fs');
const data = fs.readFileSync('README.md');
const converter = new showdown.Converter();
const html = converter.makeHtml(data.toString());
const htmlBody = `
<!DOCTYPE html>

  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="./demo/assets/vendor.css">
  <link rel="stylesheet" href="./demo/assets/semantic.min.css">
  <link rel="stylesheet" href="./assets/app.css">
  <title>Ember Semantic Ui</title>

          ember-semantic-ui

                Github

                Wecatch

Docs

Demo

  ${html}

`;

fs.writeFileSync('index.html', htmlBody);
<|endoftext|>


<commit_before>#!/usr/bin/env node

(function(require, console) {
  var fs = require('fs'),
      xcode = require('xcode'),
      path = require('path'),
      projectRootDir = path.resolve(__dirname, '..', '..', '..'),
      projectConfigFilePath = path.join(
        projectRootDir, 'custom', 'brand', 'project_config.json'),
      projectName = require(projectConfigFilePath).projectName,
      iosProjectObject,
      elementsFilePath = path.join(projectRootDir, 'custom', 'elements.json'),
      elements = require(elementsFilePath),
      elementsNum = 0,
      platformDestinations = {
        android: path.join(projectRootDir, 'platforms', 'android'),
        ios: path.join(projectRootDir, 'platforms', 'ios', projectName)
      },
      platformDispositions = {},
      targetActions = {},
      platform;

  console.log("[customize hook] Applying customizations from %s",
              path.relative(projectRootDir, elementsFilePath));

  /** Fill in platformDispositions.
   *
   * For each platform in platformDispositions, each disposition is one of:
   *
   * - 'ready'
   * - 'absent' - platform directory not present
   * - 'done' - [NOT YET IMPLEMENTED] prevent repeating on multiple passes
   *
   * Also, log a message for platforms we'll be skipping, due to disposition.
   */
  function determineDispositions() {
    var platform, location;
    for (platform in platformDestinations) {
      if (platformDestinations.hasOwnProperty(platform)) {
        location = platformDestinations[platform];
        if (! fs.existsSync(location)) {
          platformDispositions[platform] = 'absent';
          console.log("[customize hook] skipping not-yet existent " +
                      " platform " + platform);
        } else {
          platformDispositions[platform] = 'ready';
        }
      }
    };
  };
  determineDispositions();

  if (platformDispositions['ios'] === 'ready') {
    iosProjectObject = xcode.project(platformDestinations['ios'] +
                                     ".xcodeproj/project.pbxproj");
    iosProjectObject.parseSync();
  };

  /** Copy optional customization elements to the indicated platform dir.
   *
   * We fail if the source item is present but the target directory is not.
   *
   * @return target path if item was present and copied, false otherwise.
   * @param {string} sourceFileName The file being copied from
   * @param {string} targetFileName The file being copied to
   * @param {string} targetPath The target path relative to platfom dir root
   * @param {string} platform The name of target platform (case insensitive)
   */
  function doCopyIfPresent(sourceDir, sourceFileName, targetFileName,
                           targetPath, platform) {
    var platformLC = platform.toLowerCase();
    if (! platformDestinations.hasOwnProperty(platformLC)) {
      throw new Error("[customize hook] Unrecognized customization platform" +
                      " '" + platform + "'");
    }
    var fromPath = path.join(sourceDir, sourceFileName);
    var destPath = path.join(platformDestinations[platformLC],
                             path.join.apply({}, targetPath.split("/")),
                             targetFileName);
    // Copy if optional item is present.
    if (fs.existsSync(fromPath)) {
      if (! fs.existsSync(path.dirname(destPath))) {
        throw new Error("[customize hook] Copy failed due to missing " +
                        platform + " destination directory for item " +
                        sourceFileName + ": " + path.dirname(destPath));
      }
      var readStream = fs.createReadStream(fromPath);
      var writeStream = fs.createWriteStream(destPath);
      // Occupy event queue until write end so process doesn't exit 'til done:
      writeStream.on('end', function (event) {
        //console.log("[customize hook] %s written.", destPath);
      });
      console.log("[customize hook] (%s) %s => %s",
                  platform, sourceFileName,
                  path.relative(projectRootDir, destPath));
      readStream.pipe(writeStream);
      return destPath;
    } else {
      console.log("[customize hook] (%s) %s is absent (%s).",
                  platform, sourceFileName, fromPath);
    }
    return false;
  };

  function doAction(action, sourceDir, sourceName, targetName, platform) {
    if (! targetActions.hasOwnProperty(action)) {
      throw new Error("[customize hook] Unknown targetAction" +
                      " '" + action + "'");
    } else {
      return targetActions[action](action,
                                   sourceDir,
                                   sourceName,
                                   targetName,
                                   platform);
    }
  };
  targetActions.iOSaddCertificate = iOSaddCertAction;
  function iOSaddCertAction(action, sourceDir, sourceName, targetName,
                            platform) {
    var resourcePath = path.join(sourceDir, sourceName),
        destPath, projFile;
    destPath = doCopyIfPresent(sourceDir, sourceName, sourceName, "",
                                   'ios');
    projFile = iosProjectObject.addResourceFile(destPath);
    console.log("[customize hook] Did %s: %s",
                action, path.relative(projectRootDir, resourcePath));
    fs.writeFileSync(iosProjectObject.filepath, iosProjectObject.writeSync());
    return projFile;
  };

  /** Return an array of files in dir matching target.
   *
   * If the target name starts with "^", it's treated as a regexp.
   *
   * @param{string} dir within which to seek matches
   * @param{string} target name to match a filename in dir
   */
  function matchesInDir(target, dir) {
    var entries = fs.readdirSync(dir) || [],
        asRegexp = target.charAt(0) === "^",
        got = entries.filter(function (name) {
          return asRegexp ? name.match(target) : (name === target);
        });
    return got;
  };

  /* Iterate the custom element entries, copying elements found in the
   * CustomElements folder to locations indicated, relative to the
   * projectRootDir. */
  for (var i in elements.Items) {
    var item = elements.Items[i],
        sourceName = item.SourceFileName,
        targetName = item.TargetFileName,
        getCustomElementsFrom =
          path.join.apply({},
                          // Split+.join for platform independence:
                          (item.GetCustomElementsFrom ||
                           elements.GetCustomElementsFrom).split('/')),
        customElementsDir =
          path.join(projectRootDir, getCustomElementsFrom);
    item.Platforms.forEach(function (platform) {
      if (platformDispositions[platform.toLowerCase()] !== 'ready') {
        return;
      };
      if (! item.TargetFolder && ! item.TargetAction) {
        throw new Error("[customize hook] Entry '%s' must have either a" +
                        " TargetFolder or TargetAction",
                        JSON.stringify(item));
      } else {
        var got = sourceName;
        matchesInDir(sourceName, customElementsDir).forEach(
          function (fromName) {
            var toName = targetName || fromName;
            if (item.TargetFolder) {
              doCopyIfPresent(customElementsDir,
                              fromName,
                              toName,
                              item.TargetFolder,
                              platform);
            } else if (item.TargetAction) {
              doAction(item.TargetAction,
                       customElementsDir,
                       fromName,
                       toName,
                       platform);
            };
          });
      };
    });
  }
})(require, console);
<commit_msg>Reload and parse the iOS project each time we're going to change it.<commit_after>#!/usr/bin/env node

(function(require, console) {
  var fs = require('fs'),
      xcode = require('xcode'),
      path = require('path'),
      projectRootDir = path.resolve(__dirname, '..', '..', '..'),
      projectConfigFilePath = path.join(
        projectRootDir, 'custom', 'brand', 'project_config.json'),
      projectName = require(projectConfigFilePath).projectName,
      iosProjectObject,
      elementsFilePath = path.join(projectRootDir, 'custom', 'elements.json'),
      elements = require(elementsFilePath),
      elementsNum = 0,
      platformDestinations = {
        android: path.join(projectRootDir, 'platforms', 'android'),
        ios: path.join(projectRootDir, 'platforms', 'ios', projectName)
      },
      platformDispositions = {},
      targetActions = {},
      platform;

  console.log("[customize hook] Applying customizations from %s",
              path.relative(projectRootDir, elementsFilePath));

  /** Fill in platformDispositions.
   *
   * For each platform in platformDispositions, each disposition is one of:
   *
   * - 'ready'
   * - 'absent' - platform directory not present
   * - 'done' - [NOT YET IMPLEMENTED] prevent repeating on multiple passes
   *
   * Also, log a message for platforms we'll be skipping, due to disposition.
   */
  function determineDispositions() {
    var platform, location;
    for (platform in platformDestinations) {
      if (platformDestinations.hasOwnProperty(platform)) {
        location = platformDestinations[platform];
        if (! fs.existsSync(location)) {
          platformDispositions[platform] = 'absent';
          console.log("[customize hook] skipping not-yet existent " +
                      " platform " + platform);
        } else {
          platformDispositions[platform] = 'ready';
        }
      }
    };
  };
  determineDispositions();

  /** Copy optional customization elements to the indicated platform dir.
   *
   * We fail if the source item is present but the target directory is not.
   *
   * @return target path if item was present and copied, false otherwise.
   * @param {string} sourceFileName The file being copied from
   * @param {string} targetFileName The file being copied to
   * @param {string} targetPath The target path relative to platfom dir root
   * @param {string} platform The name of target platform (case insensitive)
   */
  function doCopyIfPresent(sourceDir, sourceFileName, targetFileName,
                           targetPath, platform) {
    var platformLC = platform.toLowerCase();
    if (! platformDestinations.hasOwnProperty(platformLC)) {
      throw new Error("[customize hook] Unrecognized customization platform" +
                      " '" + platform + "'");
    }
    var fromPath = path.join(sourceDir, sourceFileName);
    var destPath = path.join(platformDestinations[platformLC],
                             path.join.apply({}, targetPath.split("/")),
                             targetFileName);
    // Copy if optional item is present.
    if (fs.existsSync(fromPath)) {
      if (! fs.existsSync(path.dirname(destPath))) {
        throw new Error("[customize hook] Copy failed due to missing " +
                        platform + " destination directory for item " +
                        sourceFileName + ": " + path.dirname(destPath));
      }
      var readStream = fs.createReadStream(fromPath);
      var writeStream = fs.createWriteStream(destPath);
      // Occupy event queue until write end so process doesn't exit 'til done:
      writeStream.on('end', function (event) {
        //console.log("[customize hook] %s written.", destPath);
      });
      console.log("[customize hook] (%s) %s => %s",
                  platform, sourceFileName,
                  path.relative(projectRootDir, destPath));
      readStream.pipe(writeStream);
      return destPath;
    } else {
      console.log("[customize hook] (%s) %s is absent (%s).",
                  platform, sourceFileName, fromPath);
    }
    return false;
  };

  function doAction(action, sourceDir, sourceName, targetName, platform) {
    if (! targetActions.hasOwnProperty(action)) {
      throw new Error("[customize hook] Unknown targetAction" +
                      " '" + action + "'");
    } else {
      return targetActions[action](action,
                                   sourceDir,
                                   sourceName,
                                   targetName,
                                   platform);
    }
  };
  targetActions.iOSaddCertificate = iOSaddCertAction;
  function iOSaddCertAction(action, sourceDir, sourceName, targetName,
                            platform) {
    var resourcePath = path.join(sourceDir, sourceName),
        iosProjectObject = xcode.project(platformDestinations['ios'] +
                                         ".xcodeproj/project.pbxproj"),
        destPath, projFile;
    iosProjectObject.parseSync();
    destPath = doCopyIfPresent(sourceDir, sourceName, sourceName, "",
                                   'ios');
    projFile = iosProjectObject.addResourceFile(destPath);
    console.log("[customize hook] Did %s: %s",
                action, path.relative(projectRootDir, resourcePath));
    fs.writeFileSync(iosProjectObject.filepath, iosProjectObject.writeSync());
    return projFile;
  };

  /** Return an array of files in dir matching target.
   *
   * If the target name starts with "^", it's treated as a regexp.
   *
   * @param{string} dir within which to seek matches
   * @param{string} target name to match a filename in dir
   */
  function matchesInDir(target, dir) {
    var entries = fs.readdirSync(dir) || [],
        asRegexp = target.charAt(0) === "^",
        got = entries.filter(function (name) {
          return asRegexp ? name.match(target) : (name === target);
        });
    return got;
  };

  /* Iterate the custom element entries, copying elements found in the
   * CustomElements folder to locations indicated, relative to the
   * projectRootDir. */
  for (var i in elements.Items) {
    var item = elements.Items[i],
        sourceName = item.SourceFileName,
        targetName = item.TargetFileName,
        getCustomElementsFrom =
          path.join.apply({},
                          // Split+.join for platform independence:
                          (item.GetCustomElementsFrom ||
                           elements.GetCustomElementsFrom).split('/')),
        customElementsDir =
          path.join(projectRootDir, getCustomElementsFrom);
    item.Platforms.forEach(function (platform) {
      if (platformDispositions[platform.toLowerCase()] !== 'ready') {
        return;
      };
      if (! item.TargetFolder && ! item.TargetAction) {
        throw new Error("[customize hook] Entry '%s' must have either a" +
                        " TargetFolder or TargetAction",
                        JSON.stringify(item));
      } else {
        var got = sourceName;
        matchesInDir(sourceName, customElementsDir).forEach(
          function (fromName) {
            var toName = targetName || fromName;
            if (item.TargetFolder) {
              doCopyIfPresent(customElementsDir,
                              fromName,
                              toName,
                              item.TargetFolder,
                              platform);
            } else if (item.TargetAction) {
              doAction(item.TargetAction,
                       customElementsDir,
                       fromName,
                       toName,
                       platform);
            };
          });
      };
    });
  }
})(require, console);
<|endoftext|>