<commit_before>hqDefine('hqwebapp/js/multiselect_utils', [
    "jquery",
    "knockout",
    "underscore",
    "hqwebapp/js/assert_properties",
    "multiselect/js/jquery.multi-select",
    "quicksearch/dist/jquery.quicksearch.min",
], function (
    $,
    ko,
    _,
    assertProperties
) {
    var multiselect_utils = {};

    var _renderHeader = function (title, action, search) {
        // Since action and search are created from _renderAction() and _renderSearch()
        // and the variables the functions are esacaped so it would be safe to use them with <%=
        var header = _.template('
<%- headerTitle %><%= actionButton %>
<%= searchInput %>');
        return header({
            headerTitle: title,
            actionButton: action || '',
            searchInput: search || '',
        });
    };

    var _renderAction = function (buttonId, buttonClass, buttonIcon, text, disabled = false) {
        var action = _.template(
            '<button class="btn <%-actionButtonClass %> btn-xs pull-right" id="<%- actionButtonId %>" <% if (actionDisabled) { %> disabled <% } %>>' +
                '<i class="<%- actionButtonIcon %>"> <%- actionButtonText %>' +
            '</button>'
        );
        return action({
            actionButtonId: buttonId,
            actionButtonClass: buttonClass,
            actionButtonIcon: buttonIcon,
            actionButtonText: text,
            actionDisabled: disabled,
        });
    };

    var _renderSearch = function (inputId, placeholder) {
        var input = _.template(
            '
' +
                '' +
                    '' +
                '' +
                '<input type="search" class="form-control search-input" id="<%- searchInputId %>" autocomplete="off" placeholder="<%- searchInputPlaceholder %>" />' +
            '
'
        );
        return input({
            searchInputId: inputId,
            searchInputPlaceholder: placeholder,
        });
    };

    /**
     * Given an element, configures multiselect functionality based on the multiselect.js library
     * @param {object} properties - a key-value object that expects the following optional keys:
     * selectableHeaderTitle -  String title for items yet to be selected. Defaults to "Items".
     * selectedHeaderTitle - String for selected items title. Defaults to "Selected items".
     * searchItemTitle - String for search bar placeholder title. Defaults to "Search items".
     * willSelectAllListener - Function to call before the multiselect processes the Add All action.
     * disableModifyAllActions - Boolean value to enable/disable Add All and Remove All buttons. Defaults to false.
     */
    multiselect_utils.createFullMultiselectWidget = function (elementOrId, properties) {
        assertProperties.assert(properties, [], ['selectableHeaderTitle', 'selectedHeaderTitle', 'searchItemTitle', 'willSelectAllListener', 'disableModifyAllActions']);
        var selectableHeaderTitle = properties.selectableHeaderTitle || gettext("Items");
        var selectedHeaderTitle = properties.selectedHeaderTitle || gettext("Selected items");
        var searchItemTitle = properties.searchItemTitle || gettext("Search items");
        var willSelectAllListener = properties.willSelectAllListener;
        var disableModifyAllActions = properties['disableModifyAllActions'] || false;

        var $element = _.isString(elementOrId) ? $('#' + elementOrId) : $(elementOrId),
            baseId = _.isString(elementOrId) ? elementOrId : "multiselect-" + String(Math.random()).substring(2),
            selectAllId = baseId + '-select-all',
            removeAllId = baseId + '-remove-all',
            searchSelectableId = baseId + '-search-selectable',
            searchSelectedId = baseId + '-search-selected';

        $element.multiSelect({
            selectableHeader: _renderHeader(
                selectableHeaderTitle,
                _renderAction(selectAllId, 'btn-default', 'fa fa-plus', gettext("Add All"), disableModifyAllActions),
                _renderSearch(searchSelectableId, searchItemTitle)
            ),
            selectionHeader: _renderHeader(
                selectedHeaderTitle,
                _renderAction(removeAllId, 'btn-default', 'fa fa-remove', gettext("Remove All"), disableModifyAllActions),
                _renderSearch(searchSelectedId, searchItemTitle)
            ),
            afterInit: function () {
                var that = this,
                    $selectableSearch = $('#' + searchSelectableId),
                    $selectionSearch = $('#' + searchSelectedId),
                    selectableSearchString = '#' + that.$container.attr('id') + ' .ms-elem-selectable:not(.ms-selected)',
                    selectionSearchString = '#' + that.$container.attr('id') + ' .ms-elem-selection.ms-selected';

                that.search_left = $selectableSearch.quicksearch(selectableSearchString)
                    .on('keydown', function (e) {
                        if (e.which === 40) {  // down arrow, was recommended by loudev docs
                            that.$selectableUl.focus();
                            return false;
                        }
                    })
                    .on('keyup change search input', function () {
                    // disable add all functionality so that user is not confused
                        if (that.search_left.val().length > 0) {
                            $('#' + selectAllId).addClass('disabled').prop('disabled', true);
                        } else {
                            if (!disableModifyAllActions) {
                                $('#' + selectAllId).removeClass('disabled').prop('disabled', false);
                            }
                        }
                    });

                that.search_right = $selectionSearch.quicksearch(selectionSearchString)
                    .on('keydown', function (e) {
                        if (e.which === 40) {  // down arrow, was recommended by loudev docs
                            that.$selectionUl.focus();
                            return false;
                        }
                    })
                    .on('keyup change search input', function () {
                    // disable remove all functionality so that user is not confused
                        if (that.search_right.val().length > 0) {
                            $('#' + removeAllId).addClass('disabled').prop('disabled', true);
                        } else if (!disableModifyAllActions) {
                            $('#' + removeAllId).removeClass('disabled').prop('disabled', false);
                        }
                    });
            },
            afterSelect: function () {
                this.search_left.cache();
                // remove search option so that user doesn't get confused
                this.search_right.val('').search('');
                if (!disableModifyAllActions) {
                    $('#' + removeAllId).removeClass('disabled').prop('disabled', false);
                }
                this.search_right.cache();
            },
            afterDeselect: function () {
                // remove search option so that user doesn't get confused
                this.search_left.val('').search('');
                if (!disableModifyAllActions) {
                    $('#' + selectAllId).removeClass('disabled').prop('disabled', false);
                }
                this.search_left.cache();
                this.search_right.cache();
            },
        });

        $('#' + selectAllId).click(function () {
            if (willSelectAllListener) {
                willSelectAllListener();
            }
            $element.multiSelect('select_all');
            return false;
        });
        $('#' + removeAllId).click(function () {
            $element.multiSelect('deselect_all');
            return false;
        });
    };

    multiselect_utils.rebuildMultiselect = function (elementOrId, multiselectProperties) {
        var $element = _.isString(elementOrId) ? $('#' + elementOrId) : $(elementOrId);
        // multiSelect('refresh') breaks existing click handlers, so the alternative is to destroy and rebuild
        $element.multiSelect('destroy');
        multiselect_utils.createFullMultiselectWidget(elementOrId, multiselectProperties);
    };

    /*
     * A custom binding for setting multiselect properties and additional knockout bindings
     * The only dynamic part of this binding are the options
     * For a list of configurable multiselect properties, see http://loudev.com/ under Options
     */
    ko.bindingHandlers.multiselect = {
        init: function (element, valueAccessor) {
            var model = valueAccessor();
            assertProperties.assert(model, [], ['properties', 'options']);
            multiselect_utils.createFullMultiselectWidget(element, model.properties);

            if (model.options) {
                // add the `options` binding to the element, valueAccessor() should return an observable
                // NOTE: apply bindings after the multiselect has been setup
                ko.applyBindingsToNode(element, {options: model.options});
            }
        },
        update: function (element, valueAccessor) {
            var model = valueAccessor();
            if (model.options) {
                // have to access the observable to get the `update` method to fire on changes to options
                ko.unwrap(model.options());
            }

            multiselect_utils.rebuildMultiselect(element, model.properties);
        },
    };

    return multiselect_utils;
});
<commit_msg>add didUpdateListener to custom multiselect binding<commit_after>hqDefine('hqwebapp/js/multiselect_utils', [
    "jquery",
    "knockout",
    "underscore",
    "hqwebapp/js/assert_properties",
    "multiselect/js/jquery.multi-select",
    "quicksearch/dist/jquery.quicksearch.min",
], function (
    $,
    ko,
    _,
    assertProperties
) {
    var multiselect_utils = {};

    var _renderHeader = function (title, action, search) {
        // Since action and search are created from _renderAction() and _renderSearch()
        // and the variables the functions are esacaped so it would be safe to use them with <%=
        var header = _.template('
<%- headerTitle %><%= actionButton %>
<%= searchInput %>');
        return header({
            headerTitle: title,
            actionButton: action || '',
            searchInput: search || '',
        });
    };

    var _renderAction = function (buttonId, buttonClass, buttonIcon, text, disabled = false) {
        var action = _.template(
            '<button class="btn <%-actionButtonClass %> btn-xs pull-right" id="<%- actionButtonId %>" <% if (actionDisabled) { %> disabled <% } %>>' +
                '<i class="<%- actionButtonIcon %>"> <%- actionButtonText %>' +
            '</button>'
        );
        return action({
            actionButtonId: buttonId,
            actionButtonClass: buttonClass,
            actionButtonIcon: buttonIcon,
            actionButtonText: text,
            actionDisabled: disabled,
        });
    };

    var _renderSearch = function (inputId, placeholder) {
        var input = _.template(
            '
' +
                '' +
                    '' +
                '' +
                '<input type="search" class="form-control search-input" id="<%- searchInputId %>" autocomplete="off" placeholder="<%- searchInputPlaceholder %>" />' +
            '
'
        );
        return input({
            searchInputId: inputId,
            searchInputPlaceholder: placeholder,
        });
    };

    /**
     * Given an element, configures multiselect functionality based on the multiselect.js library
     * @param {object} properties - a key-value object that expects the following optional keys:
     * selectableHeaderTitle -  String title for items yet to be selected. Defaults to "Items".
     * selectedHeaderTitle - String for selected items title. Defaults to "Selected items".
     * searchItemTitle - String for search bar placeholder title. Defaults to "Search items".
     * willSelectAllListener - Function to call before the multiselect processes the Add All action.
     * disableModifyAllActions - Boolean value to enable/disable Add All and Remove All buttons. Defaults to false.
     */
    multiselect_utils.createFullMultiselectWidget = function (elementOrId, properties) {
        assertProperties.assert(properties, [], ['selectableHeaderTitle', 'selectedHeaderTitle', 'searchItemTitle', 'willSelectAllListener', 'disableModifyAllActions']);
        var selectableHeaderTitle = properties.selectableHeaderTitle || gettext("Items");
        var selectedHeaderTitle = properties.selectedHeaderTitle || gettext("Selected items");
        var searchItemTitle = properties.searchItemTitle || gettext("Search items");
        var willSelectAllListener = properties.willSelectAllListener;
        var disableModifyAllActions = properties['disableModifyAllActions'] || false;

        var $element = _.isString(elementOrId) ? $('#' + elementOrId) : $(elementOrId),
            baseId = _.isString(elementOrId) ? elementOrId : "multiselect-" + String(Math.random()).substring(2),
            selectAllId = baseId + '-select-all',
            removeAllId = baseId + '-remove-all',
            searchSelectableId = baseId + '-search-selectable',
            searchSelectedId = baseId + '-search-selected';

        $element.multiSelect({
            selectableHeader: _renderHeader(
                selectableHeaderTitle,
                _renderAction(selectAllId, 'btn-default', 'fa fa-plus', gettext("Add All"), disableModifyAllActions),
                _renderSearch(searchSelectableId, searchItemTitle)
            ),
            selectionHeader: _renderHeader(
                selectedHeaderTitle,
                _renderAction(removeAllId, 'btn-default', 'fa fa-remove', gettext("Remove All"), disableModifyAllActions),
                _renderSearch(searchSelectedId, searchItemTitle)
            ),
            afterInit: function () {
                var that = this,
                    $selectableSearch = $('#' + searchSelectableId),
                    $selectionSearch = $('#' + searchSelectedId),
                    selectableSearchString = '#' + that.$container.attr('id') + ' .ms-elem-selectable:not(.ms-selected)',
                    selectionSearchString = '#' + that.$container.attr('id') + ' .ms-elem-selection.ms-selected';

                that.search_left = $selectableSearch.quicksearch(selectableSearchString)
                    .on('keydown', function (e) {
                        if (e.which === 40) {  // down arrow, was recommended by loudev docs
                            that.$selectableUl.focus();
                            return false;
                        }
                    })
                    .on('keyup change search input', function () {
                    // disable add all functionality so that user is not confused
                        if (that.search_left.val().length > 0) {
                            $('#' + selectAllId).addClass('disabled').prop('disabled', true);
                        } else {
                            if (!disableModifyAllActions) {
                                $('#' + selectAllId).removeClass('disabled').prop('disabled', false);
                            }
                        }
                    });

                that.search_right = $selectionSearch.quicksearch(selectionSearchString)
                    .on('keydown', function (e) {
                        if (e.which === 40) {  // down arrow, was recommended by loudev docs
                            that.$selectionUl.focus();
                            return false;
                        }
                    })
                    .on('keyup change search input', function () {
                    // disable remove all functionality so that user is not confused
                        if (that.search_right.val().length > 0) {
                            $('#' + removeAllId).addClass('disabled').prop('disabled', true);
                        } else if (!disableModifyAllActions) {
                            $('#' + removeAllId).removeClass('disabled').prop('disabled', false);
                        }
                    });
            },
            afterSelect: function () {
                this.search_left.cache();
                // remove search option so that user doesn't get confused
                this.search_right.val('').search('');
                if (!disableModifyAllActions) {
                    $('#' + removeAllId).removeClass('disabled').prop('disabled', false);
                }
                this.search_right.cache();
            },
            afterDeselect: function () {
                // remove search option so that user doesn't get confused
                this.search_left.val('').search('');
                if (!disableModifyAllActions) {
                    $('#' + selectAllId).removeClass('disabled').prop('disabled', false);
                }
                this.search_left.cache();
                this.search_right.cache();
            },
        });

        $('#' + selectAllId).click(function () {
            if (willSelectAllListener) {
                willSelectAllListener();
            }
            $element.multiSelect('select_all');
            return false;
        });
        $('#' + removeAllId).click(function () {
            $element.multiSelect('deselect_all');
            return false;
        });
    };

    multiselect_utils.rebuildMultiselect = function (elementOrId, multiselectProperties) {
        var $element = _.isString(elementOrId) ? $('#' + elementOrId) : $(elementOrId);
        // multiSelect('refresh') breaks existing click handlers, so the alternative is to destroy and rebuild
        $element.multiSelect('destroy');
        multiselect_utils.createFullMultiselectWidget(elementOrId, multiselectProperties);
    };

    /*
     * A custom binding for setting multiselect properties and additional knockout bindings
     * The only dynamic part of this binding are the options
     * For a list of configurable multiselect properties, see http://loudev.com/ under Options
     * properties - a dictionary of properties used by the multiselect element (see createFullMultiselectWidget above)
     * options - an observable array of option elements (only supports array of strings)
     * didUpdateListener - method to invoke when the multiselect updates
     */
    ko.bindingHandlers.multiselect = {
        init: function (element, valueAccessor) {
            var model = valueAccessor();
            assertProperties.assert(model, [], ['properties', 'options', 'didUpdateListener']);
            multiselect_utils.createFullMultiselectWidget(element, model.properties);

            if (model.options) {
                // apply bindings after the multiselect has been setup
                ko.applyBindingsToNode(element, {options: model.options});
            }
        },
        update: function (element, valueAccessor) {
            var model = valueAccessor();
            assertProperties.assert(model, [], ['properties', 'options', 'didUpdateListener']);
            if (model.options) {
                // have to access the observable to get the `update` method to fire on changes to options
                ko.unwrap(model.options());
            }

            multiselect_utils.rebuildMultiselect(element, model.properties);
            if (model.didUpdateListener) {
                model.didUpdateListener();
            }
        },
    };

    return multiselect_utils;
});
<|endoftext|>


<commit_before>// @flow

import React from 'react'
import cx from 'classnames'
import {semesterName} from '@gob/school-st-olaf-college'

import './expression--course.scss'

type Props = {
	_result?: bool,
	_taken?: bool,
	department: string,
	international?: bool,
	lab?: bool,
	level?: number,
	number?: number,
	section?: string,
	semester?: number,
	style?: Object,
	type?: string,
	year?: number,
}

export default function CourseExpression(props: Props) {
	const department = props.department

	const international = props.international && (
		I
	)
	const lab =
		props.lab ||
		(props.type === 'Lab' && L)

	const section = props.section &&
		props.section !== '*' && (
			[{props.section}]
		)

	const year = props.year && (
		{props.year}
	)
	const semester = props.semester && (

			{props.semester === '*'
				? 'ANY'
				: semesterName(props.semester).toUpperCase()}

	)

	/////

	const temporalIdentifiers = (semester || year) && (

			{semester}
			{year}

	)

	return (

				{department}

						{props.number || String(props.level)[0] + 'XX'}

					{international}
					{lab ? 'L' : null} {section}

			{temporalIdentifiers}

	)
}
<commit_msg>fix bool/boolean terminology<commit_after>// @flow

import React from 'react'
import cx from 'classnames'
import {semesterName} from '@gob/school-st-olaf-college'

import './expression--course.scss'

type Props = {
	_result?: boolean,
	_taken?: boolean,
	department: string,
	international?: boolean,
	lab?: boolean,
	level?: number,
	number?: number,
	section?: string,
	semester?: number,
	style?: Object,
	type?: string,
	year?: number,
}

export default function CourseExpression(props: Props) {
	const department = props.department

	const international = props.international && (
		I
	)
	const lab =
		props.lab ||
		(props.type === 'Lab' && L)

	const section = props.section &&
		props.section !== '*' && (
			[{props.section}]
		)

	const year = props.year && (
		{props.year}
	)
	const semester = props.semester && (

			{props.semester === '*'
				? 'ANY'
				: semesterName(props.semester).toUpperCase()}

	)

	/////

	const temporalIdentifiers = (semester || year) && (

			{semester}
			{year}

	)

	return (

				{department}

						{props.number || String(props.level)[0] + 'XX'}

					{international}
					{lab ? 'L' : null} {section}

			{temporalIdentifiers}

	)
}
<|endoftext|>


<commit_before>angular.module('n52.core.timeUi')
        .directive('swcSelectDateTime', [
            function () {
                return {
                    restrict: 'E',
                    templateUrl: 'templates/time/date-time-selection.html',
                    scope: {
                        date: '=',
                        title: '='
                    },
                    controller: ['$scope', 'timeService',
                        function ($scope) {
                            debugger;
                            $scope.isToggled = false;
                            $scope.toggle = function() {
                                $scope.isToggled = !$scope.isToggled;
                            };
                        }]
                };
            }]);<commit_msg>remove debugger<commit_after>angular.module('n52.core.timeUi')
        .directive('swcSelectDateTime', [
            function () {
                return {
                    restrict: 'E',
                    templateUrl: 'templates/time/date-time-selection.html',
                    scope: {
                        date: '=',
                        title: '='
                    },
                    controller: ['$scope', 'timeService',
                        function ($scope) {
                            $scope.isToggled = false;
                            $scope.toggle = function() {
                                $scope.isToggled = !$scope.isToggled;
                            };
                        }]
                };
            }]);<|endoftext|>


<commit_before>angular.module('n52.core.timeSelectorButtons', ['n52.core.time', 'n52.core.timeRange'])
        .directive('swcTimeSelectorButtons', function () {
            return {
                restrict: 'E',
                templateUrl: 'templates/time/time-selector-buttons.html',
                controller: ['$scope', 'timeService',
                    function ($scope, timeService) {
                        $scope.time = timeService.time;

                        $scope.back = function () {
                            timeService.stepBack();
                        };

                        $scope.forward = function () {
                            timeService.stepForward();
                        };
                    }]
            };
        });<commit_msg>extract controller<commit_after>angular.module('n52.core.timeSelectorButtons', ['n52.core.time', 'n52.core.timeRange'])
        .directive('swcTimeSelectorButtons', function () {
            return {
                restrict: 'E',
                templateUrl: 'templates/time/time-selector-buttons.html',
                controller: 'TimeSelectorCtrl'
            };
        })
        .controller('TimeSelectorCtrl', ['$scope', 'timeService',
            function ($scope, timeService) {
                $scope.time = timeService.time;

                $scope.back = function () {
                    timeService.stepBack();
                };

                $scope.forward = function () {
                    timeService.stepForward();
                };
            }]);
                <|endoftext|>


<commit_before>import React, { forwardRef, useContext, useRef, useState } from 'react';
import styled, { ThemeContext } from 'styled-components';
import { CircleAlert } from 'grommet-icons/icons/CircleAlert';
import { MessageContext } from '../../contexts/MessageContext';

import { defaultProps } from '../../default-props';

import { disabledStyle, parseMetricToNum, useForwardedRef } from '../../utils';

import { Anchor } from '../Anchor';
import { Box } from '../Box';
import { Button } from '../Button';
import { FormContext } from '../Form/FormContext';
import { Keyboard } from '../Keyboard';
import { Text } from '../Text';

import { StyledFileInput } from './StyledFileInput';
import { FileInputPropTypes } from './propTypes';

const formatBytes = (size) => {
  const units = ['B', 'KB', 'MB', 'GB', 'TB'];
  const factor = 1000;
  let index = 0;
  let num = size;
  while (num >= factor && index < units.length - 1) {
    num /= factor;
    index += 1;
  }
  return `${num.toFixed(1)} ${units[index]}`;
};

// We want the interaction of <input type="file" /> but none of its styling.
// So, we put what we want to show underneath and
// position the <input /> on top with an opacity of zero.
// If there are any files selected, we need to show the buttons to remove them.
// So, we offset the <input /> from the right by the appropriate width.
// We don't use Stack because of how we need to control the positioning.

const ContentsBox = styled(Box)`
  position: relative;
  ${(props) => props.disabled && disabledStyle()}
  ${(props) => props.theme.fileInput && props.theme.fileInput.extend};
  ${(props) =>
    props.hover &&
    props.theme.fileInput &&
    props.theme.fileInput.hover &&
    props.theme.fileInput.hover.extend};
  ${(props) =>
    props.dragOver &&
    props.theme.fileInput &&
    props.theme.fileInput.dragOver &&
    props.theme.fileInput.dragOver.extend};
`;

const Label = styled(Text)`
  ${(props) =>
    props.theme.fileInput &&
    props.theme.fileInput.label &&
    props.theme.fileInput.label.extend};
`;

const Message = styled(Text)`
  ${(props) =>
    props.theme.fileInput &&
    props.theme.fileInput.message &&
    props.theme.fileInput.message.extend};
`;

const defaultPendingRemoval = {
  event: undefined,
  index: undefined,
};

const FileInput = forwardRef(
  (
    {
      a11yTitle,
      background,
      border,
      confirmRemove,
      disabled,
      id,
      plain,
      renderFile,
      maxSize,
      messages,
      margin,
      multiple,
      name,
      onChange,
      pad,
      value: valueProp,
      ...rest
    },
    ref,
  ) => {
    const theme = useContext(ThemeContext);
    const { format } = useContext(MessageContext);
    const formContext = useContext(FormContext);
    const [hover, setHover] = React.useState();
    const [dragOver, setDragOver] = React.useState();
    const [showRemoveConfirmation, setShowRemoveConfirmation] = useState(false);
    const [pendingRemoval, setPendingRemoval] = useState(defaultPendingRemoval);
    const aggregateThreshold = (multiple && multiple.aggregateThreshold) || 10;
    const max = multiple?.max;
    const inputRef = useForwardedRef(ref);
    const controlRef = useRef();
    const removeRef = useRef();
    const ConfirmRemove = confirmRemove;
    const RemoveIcon = theme.fileInput.icons.remove;

    const [files, setFiles] = formContext.useFormInput({
      name,
      value: valueProp,
      initialValue: [],
      validate: [
        maxSize
          ? () => {
              const fileList = [...files];
              let message = '';
              const numOfInvalidFiles = fileList.filter(
                ({ size }) => size > maxSize,
              ).length;
              if (numOfInvalidFiles) {
                let messageId = 'fileInput.maxSizeSingle';
                if (multiple) {
                  messageId = `fileInput.maxSizeMultiple.${
                    numOfInvalidFiles === 1 ? 'singular' : 'plural'
                  }`;
                }
                message = format({
                  id: messageId,
                  messages,
                  values: {
                    maxSize: formatBytes(maxSize),
                    numOfFiles: numOfInvalidFiles,
                  },
                });
              }
              return message;
            }
          : '',
        max
          ? () => {
              const fileList = [...files];
              let message = '';
              if (fileList.length > max) {
                message = format({
                  id: 'fileInput.maxFile',
                  messages,
                  values: { max },
                });
              }
              return message;
            }
          : '',
      ],
    });

    const mergeTheme = (propertyName, defaultKey) => {
      let result = {};
      const themeProp = theme.fileInput[propertyName];
      if (themeProp)
        if (typeof themeProp !== 'object')
          if (defaultKey) result[defaultKey] = themeProp;
          else result = themeProp;
        else result = { ...themeProp };
      const hoverThemeProp = theme.fileInput.hover[propertyName];
      if (hover && hoverThemeProp)
        if (typeof hoverThemeProp !== 'object')
          if (defaultKey) result[defaultKey] = hoverThemeProp;
          else result = hoverThemeProp;
        else result = { ...result, ...hoverThemeProp };
      const dragOverThemeProp = theme.fileInput.dragOver[propertyName];
      if (dragOver && dragOverThemeProp)
        if (typeof dragOverThemeProp !== 'object')
          if (defaultKey) result[defaultKey] = dragOverThemeProp;
          else result = dragOverThemeProp;
        else result = { ...result, ...dragOverThemeProp };
      return typeof result === 'object' && Object.keys(result).length === 0
        ? undefined
        : result;
    };

    let rightPad;
    if (mergeTheme('pad')) {
      const { horizontal, right } = mergeTheme('pad');
      if (right) {
        rightPad = theme.global.edgeSize[right] || right;
      } else if (horizontal) {
        rightPad = theme.global.edgeSize[horizontal] || horizontal;
      }
    }

    // rightPad needs to be included in the rightOffset
    // otherwise input may cover the RemoveButton, making it
    // unreachable by mouse click.
    // If browse anchor or button is greater than remove button then
    // rightoffset will take the larger width
    let rightOffset;
    if (removeRef.current && controlRef.current) {
      const rightOffsetBrowse =
        controlRef.current.getBoundingClientRect().width;
      const rightOffsetRemove = removeRef.current.getBoundingClientRect().width;
      if (rightPad && typeof rightPad === 'string')
        rightOffset = rightOffsetRemove + parseMetricToNum(rightPad);
      if (files.length === 1 || files.length > aggregateThreshold) {
        rightOffset =
          rightOffsetBrowse +
          rightOffsetRemove +
          parseMetricToNum(theme.global.edgeSize.small) * 2;
      } else if (rightOffsetBrowse > rightOffsetRemove) {
        rightOffset =
          rightOffsetBrowse + parseMetricToNum(theme.global.edgeSize.small) * 2;
      } else rightOffset = rightOffsetRemove;
    } else if (!files.length && controlRef.current) {
      rightOffset =
        controlRef.current.getBoundingClientRect().width +
        parseMetricToNum(theme.global.edgeSize.small) * 2;
    }

    // Show the number of files when more than one

    let message;
    if (!files.length) {
      message = format({
        id: multiple ? 'fileInput.dropPromptMultiple' : 'fileInput.dropPrompt',
        messages,
      });
    } else message = `${files.length} items`;

    const removeFile = (event, index) => {
      event.stopPropagation();
      let nextFiles;
      if (index === 'all') {
        nextFiles = [];
      } else {
        nextFiles = [...files];
        nextFiles.splice(index, 1);
      }
      setFiles(nextFiles);
      if (onChange) onChange(event, { files: nextFiles });
      if (nextFiles.length === 0) inputRef.current.value = '';
      inputRef.current.focus();
    };

    return (
      <>
        <ContentsBox
          theme={theme}
          flex={false}
          disabled={disabled}
          background={mergeTheme('background', 'color')}
          border={!plain ? mergeTheme('border', 'side') : undefined}
          margin={mergeTheme('margin')}
          pad={mergeTheme('pad')}
          round={mergeTheme('round', 'size')}
          align={files.length ? 'stretch' : 'center'}
          justify="center"
          hover={hover}
          onMouseOver={disabled ? undefined : () => setHover(true)}
          onMouseOut={disabled ? undefined : () => setHover(false)}
          dragOver={dragOver}
        >
          {(!files.length || files.length > 1) && (
            <Box
              align="center"
              fill="horizontal"
              direction="row"
              justify="between"
            >
              {files.length <= aggregateThreshold && (
                <>
                  <Message {...theme.fileInput.message}>{message}</Message>
                  <Keyboard
                    onSpace={(event) => {
                      if (controlRef.current === event.target)
                        inputRef.current.click();
                    }}
                    onEnter={(event) => {
                      if (controlRef.current === event.target)
                        inputRef.current.click();
                    }}
                  >
                    {theme.fileInput.button ? (
                      <Button
                        ref={controlRef}
                        kind={theme.fileInput.button}
                        label={format({
                          id: 'fileInput.browse',
                          messages,
                        })}
                        onClick={() => {
                          inputRef.current.click();
                          inputRef.current.focus();
                        }}
                      />
                    ) : (
                      <Anchor
                        tabIndex={0}
                        alignSelf="center"
                        ref={controlRef}
                        margin="small"
                        onClick={() => {
                          inputRef.current.click();
                          inputRef.current.focus();
                        }}
                        label={format({
                          id: 'fileInput.browse',
                          messages,
                        })}
                      />
                    )}
                  </Keyboard>
                </>
              )}
            </Box>
          )}
          {files.length > aggregateThreshold && (
            <Box justify="between" direction="row" align="center">
              <Label {...theme.fileInput.label}>
                {files.length}{' '}
                {format({
                  id: 'fileInput.files',
                  messages,
                })}
              </Label>
              <Box flex={false} direction="row" align="center">
                <Button
                  ref={removeRef}
                  a11yTitle={format({
                    id: 'fileInput.removeAll',
                    messages,
                  })}
                  icon={<RemoveIcon />}
                  hoverIndicator
                  onClick={(event) => {
                    if (confirmRemove) {
                      event.persist(); // necessary for when React < v17
                      setPendingRemoval({ event, index: 'all' });
                      setShowRemoveConfirmation(true);
                    } else removeFile(event, 'all');
                  }}
                />
                <Keyboard
                  onSpace={(event) => {
                    if (controlRef.current === event.target)
                      inputRef.current.click();
                  }}
                  onEnter={(event) => {
                    if (controlRef.current === event.target)
                      inputRef.current.click();
                  }}
                >
                  {theme.fileInput.button ? (
                    <Button
                      ref={controlRef}
                      kind={theme.fileInput.button}
                      label={format({
                        id: 'fileInput.browse',
                        messages,
                      })}
                      onClick={() => {
                        inputRef.current.click();
                        inputRef.current.focus();
                      }}
                    />
                  ) : (
                    <Anchor
                      tabIndex={0}
                      alignSelf="center"
                      ref={controlRef}
                      margin="small"
                      onClick={() => {
                        inputRef.current.click();
                        inputRef.current.focus();
                      }}
                      label={format({
                        id: 'fileInput.browse',
                        messages,
                      })}
                    />
                  )}
                </Keyboard>
              </Box>
            </Box>
          )}
          {files.length > 0 &&
            files.length <= aggregateThreshold &&
            files.map((file, index) => (
              <Box
                key={file.name}
                justify="between"
                direction="row"
                align="center"
              >
                {renderFile ? (
                  renderFile(file)
                ) : (
                  <Box
                    {...theme.fileInput.label}
                    gap="xsmall"
                    align="center"
                    direction="row"
                  >
                    {((maxSize && file.size > maxSize) ||
                      (max && index >= max)) && <CircleAlert />}
                    <Label
                      weight={
                        theme.global.input.weight ||
                        theme.global.input.font.weight
                      }
                      truncate
                    >
                      {file.name}
                    </Label>
                  </Box>
                )}
                <Box flex={false} direction="row" align="center">
                  <Button
                    ref={index ? undefined : removeRef}
                    a11yTitle={`${format({
                      id: 'fileInput.remove',
                      messages,
                    })} ${file.name}`}
                    icon={<RemoveIcon />}
                    hoverIndicator
                    onClick={(event) => {
                      if (confirmRemove) {
                        event.persist(); // necessary for when React < v17
                        setPendingRemoval({ event, index });
                        setShowRemoveConfirmation(true);
                      } else removeFile(event, index);
                    }}
                  />
                  {files.length === 1 && (
                    <Keyboard
                      onSpace={(event) => {
                        if (controlRef.current === event.target)
                          inputRef.current.click();
                      }}
                      onEnter={(event) => {
                        if (controlRef.current === event.target)
                          inputRef.current.click();
                      }}
                    >
                      {theme.fileInput.button ? (
                        <Button
                          ref={controlRef}
                          kind={theme.fileInput.button}
                          label={format({
                            id: 'fileInput.browse',
                            messages,
                          })}
                          onClick={() => {
                            inputRef.current.click();
                            inputRef.current.focus();
                          }}
                        />
                      ) : (
                        <Anchor
                          tabIndex={0}
                          ref={controlRef}
                          margin="small"
                          onClick={() => {
                            inputRef.current.click();
                            inputRef.current.focus();
                          }}
                          label={format({
                            id: 'fileInput.browse',
                            messages,
                          })}
                        />
                      )}
                    </Keyboard>
                  )}
                </Box>
              </Box>
            ))}
          <StyledFileInput
            ref={inputRef}
            type="file"
            id={id}
            name={name}
            maxSize={maxSize}
            multiple={multiple}
            disabled={disabled}
            plain
            rightOffset={rightOffset}
            {...rest}
            onDragOver={() => setDragOver(true)}
            onDragLeave={() => setDragOver(false)}
            onChange={(event) => {
              event.persist();
              const fileList = event.target.files;
              const nextFiles = multiple ? [...files] : [];
              for (let i = 0; i < fileList.length; i += 1) {
                // avoid duplicates
                const existing =
                  nextFiles.filter(
                    (file) =>
                      file.name === fileList[i].name &&
                      file.size === fileList[i].size,
                  ).length > 0;
                if (!existing) {
                  nextFiles.push(fileList[i]);
                }
              }
              setFiles(nextFiles);
              setDragOver(false);
              if (onChange) onChange(event, { files: nextFiles });
            }}
          />
        </ContentsBox>
        {showRemoveConfirmation && (
          <ConfirmRemove
            onConfirm={() => {
              removeFile(pendingRemoval.event, pendingRemoval.index);
              setPendingRemoval(defaultPendingRemoval);
              setShowRemoveConfirmation(false);
            }}
            onCancel={() => setShowRemoveConfirmation(false)}
          />
        )}
      </>
    );
  },
);

FileInput.defaultProps = {};

Object.setPrototypeOf(FileInput.defaultProps, defaultProps);

FileInput.displayName = 'FileInput';
FileInput.propTypes = FileInputPropTypes;

export { FileInput };
<commit_msg>Change factor to 1024 (#5775)<commit_after>import React, { forwardRef, useContext, useRef, useState } from 'react';
import styled, { ThemeContext } from 'styled-components';
import { CircleAlert } from 'grommet-icons/icons/CircleAlert';
import { MessageContext } from '../../contexts/MessageContext';

import { defaultProps } from '../../default-props';

import { disabledStyle, parseMetricToNum, useForwardedRef } from '../../utils';

import { Anchor } from '../Anchor';
import { Box } from '../Box';
import { Button } from '../Button';
import { FormContext } from '../Form/FormContext';
import { Keyboard } from '../Keyboard';
import { Text } from '../Text';

import { StyledFileInput } from './StyledFileInput';
import { FileInputPropTypes } from './propTypes';

const formatBytes = (size) => {
  const units = ['B', 'KB', 'MB', 'GB', 'TB'];
  const factor = 1024;
  let index = 0;
  let num = size;
  while (num >= factor && index < units.length - 1) {
    num /= factor;
    index += 1;
  }
  return `${num.toFixed(1)} ${units[index]}`;
};

// We want the interaction of <input type="file" /> but none of its styling.
// So, we put what we want to show underneath and
// position the <input /> on top with an opacity of zero.
// If there are any files selected, we need to show the buttons to remove them.
// So, we offset the <input /> from the right by the appropriate width.
// We don't use Stack because of how we need to control the positioning.

const ContentsBox = styled(Box)`
  position: relative;
  ${(props) => props.disabled && disabledStyle()}
  ${(props) => props.theme.fileInput && props.theme.fileInput.extend};
  ${(props) =>
    props.hover &&
    props.theme.fileInput &&
    props.theme.fileInput.hover &&
    props.theme.fileInput.hover.extend};
  ${(props) =>
    props.dragOver &&
    props.theme.fileInput &&
    props.theme.fileInput.dragOver &&
    props.theme.fileInput.dragOver.extend};
`;

const Label = styled(Text)`
  ${(props) =>
    props.theme.fileInput &&
    props.theme.fileInput.label &&
    props.theme.fileInput.label.extend};
`;

const Message = styled(Text)`
  ${(props) =>
    props.theme.fileInput &&
    props.theme.fileInput.message &&
    props.theme.fileInput.message.extend};
`;

const defaultPendingRemoval = {
  event: undefined,
  index: undefined,
};

const FileInput = forwardRef(
  (
    {
      a11yTitle,
      background,
      border,
      confirmRemove,
      disabled,
      id,
      plain,
      renderFile,
      maxSize,
      messages,
      margin,
      multiple,
      name,
      onChange,
      pad,
      value: valueProp,
      ...rest
    },
    ref,
  ) => {
    const theme = useContext(ThemeContext);
    const { format } = useContext(MessageContext);
    const formContext = useContext(FormContext);
    const [hover, setHover] = React.useState();
    const [dragOver, setDragOver] = React.useState();
    const [showRemoveConfirmation, setShowRemoveConfirmation] = useState(false);
    const [pendingRemoval, setPendingRemoval] = useState(defaultPendingRemoval);
    const aggregateThreshold = (multiple && multiple.aggregateThreshold) || 10;
    const max = multiple?.max;
    const inputRef = useForwardedRef(ref);
    const controlRef = useRef();
    const removeRef = useRef();
    const ConfirmRemove = confirmRemove;
    const RemoveIcon = theme.fileInput.icons.remove;

    const [files, setFiles] = formContext.useFormInput({
      name,
      value: valueProp,
      initialValue: [],
      validate: [
        maxSize
          ? () => {
              const fileList = [...files];
              let message = '';
              const numOfInvalidFiles = fileList.filter(
                ({ size }) => size > maxSize,
              ).length;
              if (numOfInvalidFiles) {
                let messageId = 'fileInput.maxSizeSingle';
                if (multiple) {
                  messageId = `fileInput.maxSizeMultiple.${
                    numOfInvalidFiles === 1 ? 'singular' : 'plural'
                  }`;
                }
                message = format({
                  id: messageId,
                  messages,
                  values: {
                    maxSize: formatBytes(maxSize),
                    numOfFiles: numOfInvalidFiles,
                  },
                });
              }
              return message;
            }
          : '',
        max
          ? () => {
              const fileList = [...files];
              let message = '';
              if (fileList.length > max) {
                message = format({
                  id: 'fileInput.maxFile',
                  messages,
                  values: { max },
                });
              }
              return message;
            }
          : '',
      ],
    });

    const mergeTheme = (propertyName, defaultKey) => {
      let result = {};
      const themeProp = theme.fileInput[propertyName];
      if (themeProp)
        if (typeof themeProp !== 'object')
          if (defaultKey) result[defaultKey] = themeProp;
          else result = themeProp;
        else result = { ...themeProp };
      const hoverThemeProp = theme.fileInput.hover[propertyName];
      if (hover && hoverThemeProp)
        if (typeof hoverThemeProp !== 'object')
          if (defaultKey) result[defaultKey] = hoverThemeProp;
          else result = hoverThemeProp;
        else result = { ...result, ...hoverThemeProp };
      const dragOverThemeProp = theme.fileInput.dragOver[propertyName];
      if (dragOver && dragOverThemeProp)
        if (typeof dragOverThemeProp !== 'object')
          if (defaultKey) result[defaultKey] = dragOverThemeProp;
          else result = dragOverThemeProp;
        else result = { ...result, ...dragOverThemeProp };
      return typeof result === 'object' && Object.keys(result).length === 0
        ? undefined
        : result;
    };

    let rightPad;
    if (mergeTheme('pad')) {
      const { horizontal, right } = mergeTheme('pad');
      if (right) {
        rightPad = theme.global.edgeSize[right] || right;
      } else if (horizontal) {
        rightPad = theme.global.edgeSize[horizontal] || horizontal;
      }
    }

    // rightPad needs to be included in the rightOffset
    // otherwise input may cover the RemoveButton, making it
    // unreachable by mouse click.
    // If browse anchor or button is greater than remove button then
    // rightoffset will take the larger width
    let rightOffset;
    if (removeRef.current && controlRef.current) {
      const rightOffsetBrowse =
        controlRef.current.getBoundingClientRect().width;
      const rightOffsetRemove = removeRef.current.getBoundingClientRect().width;
      if (rightPad && typeof rightPad === 'string')
        rightOffset = rightOffsetRemove + parseMetricToNum(rightPad);
      if (files.length === 1 || files.length > aggregateThreshold) {
        rightOffset =
          rightOffsetBrowse +
          rightOffsetRemove +
          parseMetricToNum(theme.global.edgeSize.small) * 2;
      } else if (rightOffsetBrowse > rightOffsetRemove) {
        rightOffset =
          rightOffsetBrowse + parseMetricToNum(theme.global.edgeSize.small) * 2;
      } else rightOffset = rightOffsetRemove;
    } else if (!files.length && controlRef.current) {
      rightOffset =
        controlRef.current.getBoundingClientRect().width +
        parseMetricToNum(theme.global.edgeSize.small) * 2;
    }

    // Show the number of files when more than one

    let message;
    if (!files.length) {
      message = format({
        id: multiple ? 'fileInput.dropPromptMultiple' : 'fileInput.dropPrompt',
        messages,
      });
    } else message = `${files.length} items`;

    const removeFile = (event, index) => {
      event.stopPropagation();
      let nextFiles;
      if (index === 'all') {
        nextFiles = [];
      } else {
        nextFiles = [...files];
        nextFiles.splice(index, 1);
      }
      setFiles(nextFiles);
      if (onChange) onChange(event, { files: nextFiles });
      if (nextFiles.length === 0) inputRef.current.value = '';
      inputRef.current.focus();
    };

    return (
      <>
        <ContentsBox
          theme={theme}
          flex={false}
          disabled={disabled}
          background={mergeTheme('background', 'color')}
          border={!plain ? mergeTheme('border', 'side') : undefined}
          margin={mergeTheme('margin')}
          pad={mergeTheme('pad')}
          round={mergeTheme('round', 'size')}
          align={files.length ? 'stretch' : 'center'}
          justify="center"
          hover={hover}
          onMouseOver={disabled ? undefined : () => setHover(true)}
          onMouseOut={disabled ? undefined : () => setHover(false)}
          dragOver={dragOver}
        >
          {(!files.length || files.length > 1) && (
            <Box
              align="center"
              fill="horizontal"
              direction="row"
              justify="between"
            >
              {files.length <= aggregateThreshold && (
                <>
                  <Message {...theme.fileInput.message}>{message}</Message>
                  <Keyboard
                    onSpace={(event) => {
                      if (controlRef.current === event.target)
                        inputRef.current.click();
                    }}
                    onEnter={(event) => {
                      if (controlRef.current === event.target)
                        inputRef.current.click();
                    }}
                  >
                    {theme.fileInput.button ? (
                      <Button
                        ref={controlRef}
                        kind={theme.fileInput.button}
                        label={format({
                          id: 'fileInput.browse',
                          messages,
                        })}
                        onClick={() => {
                          inputRef.current.click();
                          inputRef.current.focus();
                        }}
                      />
                    ) : (
                      <Anchor
                        tabIndex={0}
                        alignSelf="center"
                        ref={controlRef}
                        margin="small"
                        onClick={() => {
                          inputRef.current.click();
                          inputRef.current.focus();
                        }}
                        label={format({
                          id: 'fileInput.browse',
                          messages,
                        })}
                      />
                    )}
                  </Keyboard>
                </>
              )}
            </Box>
          )}
          {files.length > aggregateThreshold && (
            <Box justify="between" direction="row" align="center">
              <Label {...theme.fileInput.label}>
                {files.length}{' '}
                {format({
                  id: 'fileInput.files',
                  messages,
                })}
              </Label>
              <Box flex={false} direction="row" align="center">
                <Button
                  ref={removeRef}
                  a11yTitle={format({
                    id: 'fileInput.removeAll',
                    messages,
                  })}
                  icon={<RemoveIcon />}
                  hoverIndicator
                  onClick={(event) => {
                    if (confirmRemove) {
                      event.persist(); // necessary for when React < v17
                      setPendingRemoval({ event, index: 'all' });
                      setShowRemoveConfirmation(true);
                    } else removeFile(event, 'all');
                  }}
                />
                <Keyboard
                  onSpace={(event) => {
                    if (controlRef.current === event.target)
                      inputRef.current.click();
                  }}
                  onEnter={(event) => {
                    if (controlRef.current === event.target)
                      inputRef.current.click();
                  }}
                >
                  {theme.fileInput.button ? (
                    <Button
                      ref={controlRef}
                      kind={theme.fileInput.button}
                      label={format({
                        id: 'fileInput.browse',
                        messages,
                      })}
                      onClick={() => {
                        inputRef.current.click();
                        inputRef.current.focus();
                      }}
                    />
                  ) : (
                    <Anchor
                      tabIndex={0}
                      alignSelf="center"
                      ref={controlRef}
                      margin="small"
                      onClick={() => {
                        inputRef.current.click();
                        inputRef.current.focus();
                      }}
                      label={format({
                        id: 'fileInput.browse',
                        messages,
                      })}
                    />
                  )}
                </Keyboard>
              </Box>
            </Box>
          )}
          {files.length > 0 &&
            files.length <= aggregateThreshold &&
            files.map((file, index) => (
              <Box
                key={file.name}
                justify="between"
                direction="row"
                align="center"
              >
                {renderFile ? (
                  renderFile(file)
                ) : (
                  <Box
                    {...theme.fileInput.label}
                    gap="xsmall"
                    align="center"
                    direction="row"
                  >
                    {((maxSize && file.size > maxSize) ||
                      (max && index >= max)) && <CircleAlert />}
                    <Label
                      weight={
                        theme.global.input.weight ||
                        theme.global.input.font.weight
                      }
                      truncate
                    >
                      {file.name}
                    </Label>
                  </Box>
                )}
                <Box flex={false} direction="row" align="center">
                  <Button
                    ref={index ? undefined : removeRef}
                    a11yTitle={`${format({
                      id: 'fileInput.remove',
                      messages,
                    })} ${file.name}`}
                    icon={<RemoveIcon />}
                    hoverIndicator
                    onClick={(event) => {
                      if (confirmRemove) {
                        event.persist(); // necessary for when React < v17
                        setPendingRemoval({ event, index });
                        setShowRemoveConfirmation(true);
                      } else removeFile(event, index);
                    }}
                  />
                  {files.length === 1 && (
                    <Keyboard
                      onSpace={(event) => {
                        if (controlRef.current === event.target)
                          inputRef.current.click();
                      }}
                      onEnter={(event) => {
                        if (controlRef.current === event.target)
                          inputRef.current.click();
                      }}
                    >
                      {theme.fileInput.button ? (
                        <Button
                          ref={controlRef}
                          kind={theme.fileInput.button}
                          label={format({
                            id: 'fileInput.browse',
                            messages,
                          })}
                          onClick={() => {
                            inputRef.current.click();
                            inputRef.current.focus();
                          }}
                        />
                      ) : (
                        <Anchor
                          tabIndex={0}
                          ref={controlRef}
                          margin="small"
                          onClick={() => {
                            inputRef.current.click();
                            inputRef.current.focus();
                          }}
                          label={format({
                            id: 'fileInput.browse',
                            messages,
                          })}
                        />
                      )}
                    </Keyboard>
                  )}
                </Box>
              </Box>
            ))}
          <StyledFileInput
            ref={inputRef}
            type="file"
            id={id}
            name={name}
            maxSize={maxSize}
            multiple={multiple}
            disabled={disabled}
            plain
            rightOffset={rightOffset}
            {...rest}
            onDragOver={() => setDragOver(true)}
            onDragLeave={() => setDragOver(false)}
            onChange={(event) => {
              event.persist();
              const fileList = event.target.files;
              const nextFiles = multiple ? [...files] : [];
              for (let i = 0; i < fileList.length; i += 1) {
                // avoid duplicates
                const existing =
                  nextFiles.filter(
                    (file) =>
                      file.name === fileList[i].name &&
                      file.size === fileList[i].size,
                  ).length > 0;
                if (!existing) {
                  nextFiles.push(fileList[i]);
                }
              }
              setFiles(nextFiles);
              setDragOver(false);
              if (onChange) onChange(event, { files: nextFiles });
            }}
          />
        </ContentsBox>
        {showRemoveConfirmation && (
          <ConfirmRemove
            onConfirm={() => {
              removeFile(pendingRemoval.event, pendingRemoval.index);
              setPendingRemoval(defaultPendingRemoval);
              setShowRemoveConfirmation(false);
            }}
            onCancel={() => setShowRemoveConfirmation(false)}
          />
        )}
      </>
    );
  },
);

FileInput.defaultProps = {};

Object.setPrototypeOf(FileInput.defaultProps, defaultProps);

FileInput.displayName = 'FileInput';
FileInput.propTypes = FileInputPropTypes;

export { FileInput };
<|endoftext|>


<commit_before>import classNames from "classnames";
import mixin from "reactjs-mixin";
import { Link } from "react-router";
/* eslint-disable no-unused-vars */
import React from "react";
/* eslint-enable no-unused-vars */
import { StoreMixin } from "mesosphere-shared-reactjs";

import BetaOptInUtil from "../../utils/BetaOptInUtil";
import Breadcrumb from "../../components/Breadcrumb";
import BreadcrumbTextContent from "../../components/BreadcrumbTextContent";
import CosmosPackagesStore from "../../stores/CosmosPackagesStore";
import defaultServiceImage
  from "../../../../plugins/services/src/img/icon-service-default-large@2x.png";
import Image from "../../components/Image";
import ImageViewer from "../../components/ImageViewer";
import InstallPackageModal from "../../components/modals/InstallPackageModal";
import Loader from "../../components/Loader";
import MetadataStore from "../../stores/MetadataStore";
import Page from "../../components/Page";
import RequestErrorMsg from "../../components/RequestErrorMsg";
import StringUtil from "../../utils/StringUtil";

const PackageDetailBreadcrumbs = ({ cosmosPackage }) => {
  const name = cosmosPackage.getName();
  const version = cosmosPackage.getCurrentVersion();

  const crumbs = [
    <Breadcrumb key={0} title="Catalog">
      <BreadcrumbTextContent>
        <Link to="/catalog/packages">Catalog</Link>
      </BreadcrumbTextContent>
    </Breadcrumb>,
    <Breadcrumb key={1} title={name}>
      <BreadcrumbTextContent>
        <Link to={`/catalog/packages/${name}`} query={{ version }} key={0}>
          {name}
        </Link>
      </BreadcrumbTextContent>
    </Breadcrumb>
  ];

  return <Page.Header.Breadcrumbs iconID="packages" breadcrumbs={crumbs} />;
};

const METHODS_TO_BIND = [
  "handleInstallModalClose",
  "handleConfigureInstallModalOpen",
  "handleInstallModalOpen"
];

class PackageDetailTab extends mixin(StoreMixin) {
  constructor() {
    super();

    this.state = {
      hasError: 0,
      openInstallModal: false,
      isLoading: true
    };

    this.store_listeners = [
      {
        name: "cosmosPackages",
        events: ["descriptionError", "descriptionSuccess"],
        unmountWhen(store, event) {
          if (event === "descriptionSuccess") {
            return !!CosmosPackagesStore.get("packageDetails");
          }
        },
        listenAlways: false,
        suppressUpdate: true
      }
    ];

    METHODS_TO_BIND.forEach(method => {
      this[method] = this[method].bind(this);
    });
  }

  componentDidMount() {
    super.componentDidMount(...arguments);

    const { packageName } = this.props.params;
    const { version } = this.props.location.query;
    // Fetch package description
    CosmosPackagesStore.fetchPackageDescription(packageName, version);
  }

  onCosmosPackagesStoreDescriptionError() {
    this.setState({ hasError: true });
  }

  onCosmosPackagesStoreDescriptionSuccess() {
    this.setState({ hasError: false, isLoading: false });
  }

  handleInstallModalClose() {
    this.setState({ openInstallModal: false });
  }

  handleInstallModalOpen() {
    this.setState({ openInstallModal: true, advancedConfig: false });
  }

  handleConfigureInstallModalOpen() {
    this.setState({ openInstallModal: true, advancedConfig: true });
  }

  getErrorScreen() {
    return <RequestErrorMsg />;
  }

  getItems(definition, renderItem) {
    const items = [];
    definition.forEach((item, index) => {
      const { label, type, value } = item;

      // When there is no content to render, discard it all together
      if (!value || (Array.isArray(value) && !value.length)) {
        return null;
      }

      // If not specific type assume value is renderable
      let content = value;

      // Render sub items
      if (type === "subItems") {
        content = this.getItems(value, this.getSubItem);
      }

      items.push(renderItem(label, content, index));
    });

    return items;
  }

  getItem(label, value, key) {
    if (!label || !value) {
      return null;
    }

    if (typeof value === "string") {
      value =
{value}
;
    }

    return (

{label}

        {value}

    );
  }

  getLoadingScreen() {
    return <Loader />;
  }

  getSubItem(label, value, key) {
    let content = value;

    if (StringUtil.isEmail(value)) {
      content = <a key={key} href={`mailto:${value}`}>{value};
    }

    if (StringUtil.isUrl(value)) {
      content = {value};
    }

    return
{`${label}: `}{content}
;
  }

  mapLicenses(licenses) {
    return licenses.map(function(license) {
      const item = {
        label: license.name,
        value: license.url
      };

      return item;
    });
  }

  getPackageBadge(cosmosPackage, version) {
    const isCertified = cosmosPackage.isCertified();
    const badgeCopy = isCertified ? "Certified" : "Community";
    const badgeClasses = classNames("badge badge-large badge-rounded", {
      "badge--primary": isCertified
    });

    return (

          {badgeCopy}

        {version}

    );
  }

  getInstallButtons(cosmosPackage) {
    if (cosmosPackage.isCLIOnly()) {
      return (

CLI Only Package

            {"This package can only be installed using the CLI. See the "}

              documentation
            .

      );
    }

    return (

        <button
          className="button button-primary button-link"
          onClick={this.handleConfigureInstallModalOpen}
        >
          Configure
        </button>
        <button
          className="button button-success"
          onClick={this.handleInstallModalOpen}
        >
          Deploy
        </button>

    );
  }

  render() {
    const { props, state } = this;

    if (state.hasError || !props.params.packageName) {
      return this.getErrorScreen();
    }

    const cosmosPackage = CosmosPackagesStore.getPackageDetails();
    if (state.isLoading || !cosmosPackage) {
      return this.getLoadingScreen();
    }

    const name = cosmosPackage.getName();
    const version = cosmosPackage.getCurrentVersion();
    const description = cosmosPackage.getDescription();
    const preInstallNotes = cosmosPackage.getPreInstallNotes();

    const definition = [
      {
        label: "Description",
        value: description &&

      },
      {
        label: "Pre-Install Notes",
        value: preInstallNotes &&

      },
      {
        label: "Information",
        type: "subItems",
        value: [
          { label: "SCM", value: cosmosPackage.getSCM() },
          { label: "Maintainer", value: cosmosPackage.getMaintainer() }
        ]
      },
      {
        label: "Licenses",
        type: "subItems",
        value: this.mapLicenses(cosmosPackage.getLicenses())
      }
    ];

    return (
      <Page>
        <Page.Header
          breadcrumbs={
            <PackageDetailBreadcrumbs cosmosPackage={cosmosPackage} />
          }
        />

                  <Image
                    fallbackSrc={defaultServiceImage}
                    src={cosmosPackage.getIcons()["icon-large"]}
                  />

                  {name}

{this.getPackageBadge(cosmosPackage, version)}

                {this.getInstallButtons(cosmosPackage)}

            {this.getItems(definition, this.getItem)}
            <ImageViewer images={cosmosPackage.getScreenshots()} />

        <InstallPackageModal
          open={state.openInstallModal}
          cosmosPackage={cosmosPackage}
          advancedConfig={state.advancedConfig}
          isBetaPackage={BetaOptInUtil.isBeta(cosmosPackage.getConfig())}
          onClose={this.handleInstallModalClose}
        />
      </Page>
    );
  }
}

module.exports = PackageDetailTab;
<commit_msg>fix(DCOS_OSS-1300): Flex wrap so it's visible on small screen<commit_after>import classNames from "classnames";
import mixin from "reactjs-mixin";
import { Link } from "react-router";
/* eslint-disable no-unused-vars */
import React from "react";
/* eslint-enable no-unused-vars */
import { StoreMixin } from "mesosphere-shared-reactjs";

import BetaOptInUtil from "../../utils/BetaOptInUtil";
import Breadcrumb from "../../components/Breadcrumb";
import BreadcrumbTextContent from "../../components/BreadcrumbTextContent";
import CosmosPackagesStore from "../../stores/CosmosPackagesStore";
import defaultServiceImage
  from "../../../../plugins/services/src/img/icon-service-default-large@2x.png";
import Image from "../../components/Image";
import ImageViewer from "../../components/ImageViewer";
import InstallPackageModal from "../../components/modals/InstallPackageModal";
import Loader from "../../components/Loader";
import MetadataStore from "../../stores/MetadataStore";
import Page from "../../components/Page";
import RequestErrorMsg from "../../components/RequestErrorMsg";
import StringUtil from "../../utils/StringUtil";

const PackageDetailBreadcrumbs = ({ cosmosPackage }) => {
  const name = cosmosPackage.getName();
  const version = cosmosPackage.getCurrentVersion();

  const crumbs = [
    <Breadcrumb key={0} title="Catalog">
      <BreadcrumbTextContent>
        <Link to="/catalog/packages">Catalog</Link>
      </BreadcrumbTextContent>
    </Breadcrumb>,
    <Breadcrumb key={1} title={name}>
      <BreadcrumbTextContent>
        <Link to={`/catalog/packages/${name}`} query={{ version }} key={0}>
          {name}
        </Link>
      </BreadcrumbTextContent>
    </Breadcrumb>
  ];

  return <Page.Header.Breadcrumbs iconID="packages" breadcrumbs={crumbs} />;
};

const METHODS_TO_BIND = [
  "handleInstallModalClose",
  "handleConfigureInstallModalOpen",
  "handleInstallModalOpen"
];

class PackageDetailTab extends mixin(StoreMixin) {
  constructor() {
    super();

    this.state = {
      hasError: 0,
      openInstallModal: false,
      isLoading: true
    };

    this.store_listeners = [
      {
        name: "cosmosPackages",
        events: ["descriptionError", "descriptionSuccess"],
        unmountWhen(store, event) {
          if (event === "descriptionSuccess") {
            return !!CosmosPackagesStore.get("packageDetails");
          }
        },
        listenAlways: false,
        suppressUpdate: true
      }
    ];

    METHODS_TO_BIND.forEach(method => {
      this[method] = this[method].bind(this);
    });
  }

  componentDidMount() {
    super.componentDidMount(...arguments);

    const { packageName } = this.props.params;
    const { version } = this.props.location.query;
    // Fetch package description
    CosmosPackagesStore.fetchPackageDescription(packageName, version);
  }

  onCosmosPackagesStoreDescriptionError() {
    this.setState({ hasError: true });
  }

  onCosmosPackagesStoreDescriptionSuccess() {
    this.setState({ hasError: false, isLoading: false });
  }

  handleInstallModalClose() {
    this.setState({ openInstallModal: false });
  }

  handleInstallModalOpen() {
    this.setState({ openInstallModal: true, advancedConfig: false });
  }

  handleConfigureInstallModalOpen() {
    this.setState({ openInstallModal: true, advancedConfig: true });
  }

  getErrorScreen() {
    return <RequestErrorMsg />;
  }

  getItems(definition, renderItem) {
    const items = [];
    definition.forEach((item, index) => {
      const { label, type, value } = item;

      // When there is no content to render, discard it all together
      if (!value || (Array.isArray(value) && !value.length)) {
        return null;
      }

      // If not specific type assume value is renderable
      let content = value;

      // Render sub items
      if (type === "subItems") {
        content = this.getItems(value, this.getSubItem);
      }

      items.push(renderItem(label, content, index));
    });

    return items;
  }

  getItem(label, value, key) {
    if (!label || !value) {
      return null;
    }

    if (typeof value === "string") {
      value =
{value}
;
    }

    return (

{label}

        {value}

    );
  }

  getLoadingScreen() {
    return <Loader />;
  }

  getSubItem(label, value, key) {
    let content = value;

    if (StringUtil.isEmail(value)) {
      content = <a key={key} href={`mailto:${value}`}>{value};
    }

    if (StringUtil.isUrl(value)) {
      content = {value};
    }

    return
{`${label}: `}{content}
;
  }

  mapLicenses(licenses) {
    return licenses.map(function(license) {
      const item = {
        label: license.name,
        value: license.url
      };

      return item;
    });
  }

  getPackageBadge(cosmosPackage, version) {
    const isCertified = cosmosPackage.isCertified();
    const badgeCopy = isCertified ? "Certified" : "Community";
    const badgeClasses = classNames("badge badge-large badge-rounded", {
      "badge--primary": isCertified
    });

    return (

          {badgeCopy}

        {version}

    );
  }

  getInstallButtons(cosmosPackage) {
    if (cosmosPackage.isCLIOnly()) {
      return (

CLI Only Package

            {"This package can only be installed using the CLI. See the "}

              documentation
            .

      );
    }

    return (

        <button
          className="button button-primary button-link"
          onClick={this.handleConfigureInstallModalOpen}
        >
          Configure
        </button>
        <button
          className="button button-success"
          onClick={this.handleInstallModalOpen}
        >
          Deploy
        </button>

    );
  }

  render() {
    const { props, state } = this;

    if (state.hasError || !props.params.packageName) {
      return this.getErrorScreen();
    }

    const cosmosPackage = CosmosPackagesStore.getPackageDetails();
    if (state.isLoading || !cosmosPackage) {
      return this.getLoadingScreen();
    }

    const name = cosmosPackage.getName();
    const version = cosmosPackage.getCurrentVersion();
    const description = cosmosPackage.getDescription();
    const preInstallNotes = cosmosPackage.getPreInstallNotes();

    const definition = [
      {
        label: "Description",
        value: description &&

      },
      {
        label: "Pre-Install Notes",
        value: preInstallNotes &&

      },
      {
        label: "Information",
        type: "subItems",
        value: [
          { label: "SCM", value: cosmosPackage.getSCM() },
          { label: "Maintainer", value: cosmosPackage.getMaintainer() }
        ]
      },
      {
        label: "Licenses",
        type: "subItems",
        value: this.mapLicenses(cosmosPackage.getLicenses())
      }
    ];

    return (
      <Page>
        <Page.Header
          breadcrumbs={
            <PackageDetailBreadcrumbs cosmosPackage={cosmosPackage} />
          }
        />

                  <Image
                    fallbackSrc={defaultServiceImage}
                    src={cosmosPackage.getIcons()["icon-large"]}
                  />

                  {name}

{this.getPackageBadge(cosmosPackage, version)}

                {this.getInstallButtons(cosmosPackage)}

            {this.getItems(definition, this.getItem)}
            <ImageViewer images={cosmosPackage.getScreenshots()} />

        <InstallPackageModal
          open={state.openInstallModal}
          cosmosPackage={cosmosPackage}
          advancedConfig={state.advancedConfig}
          isBetaPackage={BetaOptInUtil.isBeta(cosmosPackage.getConfig())}
          onClose={this.handleInstallModalClose}
        />
      </Page>
    );
  }
}

module.exports = PackageDetailTab;
<|endoftext|>


<commit_before>import webpack from 'webpack';
import { resolve } from 'path';
import { existsSync } from 'fs';
import merge from 'webpack-merge';
import { filter } from 'minimatch';
import CopyWebpackPlugin from 'copy-webpack-plugin';
import SWPrecacheWebpackPlugin from 'sw-precache-webpack-plugin';
import RenderHTMLPlugin from './render-html-plugin';
import PushManifestPlugin from './push-manifest';
import baseConfig from './webpack-base-config';

function clientConfig(env) {
	const { isProd, source, src /*, port? */ } = env;

	let entry = {
		bundle: resolve(__dirname, './../entry'),
		polyfills: resolve(__dirname, './polyfills')
	};

	if (!isProd) {
		entry.bundle = [
			entry.bundle,
			'webpack-dev-server/client',
			'webpack/hot/dev-server'
		];
	}

	return {
		entry: entry,
		output: {
			path: env.dest,
			publicPath: '/',
			filename: isProd ? '[name].[chunkhash:5].js' : '[name].js',
			chunkFilename: '[name].chunk.[chunkhash:5].js'
		},

		resolveLoader: {
			alias: {
				async: resolve(__dirname, './async-component-loader')
			}
		},

		// automatic async components :)
		module: {
			loaders: [
				{
					test: /\.jsx?$/,
					include: [
						filter(source('routes')+'/{*.js,*/index.js}'),
						filter(source('components')+'/{routes,async}/{*.js,*/index.js}')
					],
					loader: resolve(__dirname, './async-component-loader'),
					options: {
						name(filename) {
							let relative = filename.replace(src, '');
							let isRoute = filename.indexOf('/routes/') >= 0;

							return isRoute ? 'route-' + relative.replace(/(^\/(routes|components\/(routes|async))\/|(\/index)?\.js$)/g, '') : false;
						},
						formatName(filename) {
							let relative = filename.replace(source('.'), '');
							// strip out context dir & any file/ext suffix
							return relative.replace(/(^\/(routes|components\/(routes|async))\/|(\/index)?\.js$)/g, '');
						}
					}
				}
			]
		},

		plugins: [
			...RenderHTMLPlugin(env),
			new PushManifestPlugin(),
			new CopyWebpackPlugin([
				...(
					existsSync(source('manifest.json'))
					? [{ from:'manifest.json' }]
					: [{
						from: resolve(__dirname, '../../resources/manifest.json'),
						to: 'manifest.json'
					}, {
						from: resolve(__dirname, '../../resources/icon.png'),
						to: 'assets/icon.png'
					}]
				),
				// copy any static files
				existsSync(source('assets')) && { from:'assets', to:'assets' }
			].filter(Boolean))
		]
	};
}

function isProd(config) {
	let limit = 200 * 1000; // 200kb

	return {
		performance: {
			hints: 'warning',
			maxAssetSize: limit,
			maxEntrypointSize: limit,
			...config.pkg.performance
		},

		plugins: [
			new webpack.optimize.UglifyJsPlugin({
				output: { comments:false },
				mangle: true,
				sourceMap: true,
				compress: {
					properties: true,
					keep_fargs: false,
					pure_getters: true,
					collapse_vars: true,
					warnings: false,
					screw_ie8: true,
					sequences: true,
					dead_code: true,
					drop_debugger: true,
					comparisons: true,
					conditionals: true,
					evaluate: true,
					booleans: true,
					loops: true,
					unused: true,
					hoist_funs: true,
					if_return: true,
					join_vars: true,
					cascade: true,
					drop_console: false,
					pure_funcs: [
						'classCallCheck',
						'_classCallCheck',
						'_possibleConstructorReturn',
						'Object.freeze',
						'invariant',
						'warning'
					]
				}
			}),
			new SWPrecacheWebpackPlugin({
				filename: 'sw.js',
				navigateFallback: 'index.html',
				navigateFallbackWhitelist: [/^(?!\/__).*/],
				minify: true,
				stripPrefix: config.cwd,
				staticFileGlobsIgnorePatterns: [
					/polyfills(\..*)?\.js$/,
					/\.map$/,
					/push-manifest\.json$/,
					/.DS_Store/
				]
			}),
			new webpack.DefinePlugin({
				'process.env.ADD_SW': config.serviceWorker
			}),
		]
	};
}

function isDev(config) {
	const { cwd, src } = config;

	return {
		plugins: [
			new webpack.NamedModulesPlugin(),
			new webpack.HotModuleReplacementPlugin()
		],

		devServer: {
			inline: true,
			hot:true,
			compress: true,
			publicPath: '/',
			contentBase: src,
			https: config.https,
			port: process.env.PORT || config.port || 8080,
			host: process.env.HOST || config.host || '0.0.0.0',
			// setup(app) {
			// 	app.use(middleware);
			// },
			disableHostCheck: true,
			historyApiFallback: true,
			quiet: true,
			clientLogLevel: 'info',
			overlay: false,
			stats: 'minimal',
			watchOptions: {
				ignored: [
					resolve(cwd, 'build'),
					resolve(cwd, 'node_modules')
				]
			}
		}
	};
}

export default function (env) {
	return merge(
		baseConfig(env),
		clientConfig(env),
		(env.isProd ? isProd : isDev)(env)
	);
}
<commit_msg>Turn logging back off<commit_after>import webpack from 'webpack';
import { resolve } from 'path';
import { existsSync } from 'fs';
import merge from 'webpack-merge';
import { filter } from 'minimatch';
import CopyWebpackPlugin from 'copy-webpack-plugin';
import SWPrecacheWebpackPlugin from 'sw-precache-webpack-plugin';
import RenderHTMLPlugin from './render-html-plugin';
import PushManifestPlugin from './push-manifest';
import baseConfig from './webpack-base-config';

function clientConfig(env) {
	const { isProd, source, src /*, port? */ } = env;

	let entry = {
		bundle: resolve(__dirname, './../entry'),
		polyfills: resolve(__dirname, './polyfills')
	};

	if (!isProd) {
		entry.bundle = [
			entry.bundle,
			'webpack-dev-server/client',
			'webpack/hot/dev-server'
		];
	}

	return {
		entry: entry,
		output: {
			path: env.dest,
			publicPath: '/',
			filename: isProd ? '[name].[chunkhash:5].js' : '[name].js',
			chunkFilename: '[name].chunk.[chunkhash:5].js'
		},

		resolveLoader: {
			alias: {
				async: resolve(__dirname, './async-component-loader')
			}
		},

		// automatic async components :)
		module: {
			loaders: [
				{
					test: /\.jsx?$/,
					include: [
						filter(source('routes')+'/{*.js,*/index.js}'),
						filter(source('components')+'/{routes,async}/{*.js,*/index.js}')
					],
					loader: resolve(__dirname, './async-component-loader'),
					options: {
						name(filename) {
							let relative = filename.replace(src, '');
							let isRoute = filename.indexOf('/routes/') >= 0;

							return isRoute ? 'route-' + relative.replace(/(^\/(routes|components\/(routes|async))\/|(\/index)?\.js$)/g, '') : false;
						},
						formatName(filename) {
							let relative = filename.replace(source('.'), '');
							// strip out context dir & any file/ext suffix
							return relative.replace(/(^\/(routes|components\/(routes|async))\/|(\/index)?\.js$)/g, '');
						}
					}
				}
			]
		},

		plugins: [
			...RenderHTMLPlugin(env),
			new PushManifestPlugin(),
			new CopyWebpackPlugin([
				...(
					existsSync(source('manifest.json'))
					? [{ from:'manifest.json' }]
					: [{
						from: resolve(__dirname, '../../resources/manifest.json'),
						to: 'manifest.json'
					}, {
						from: resolve(__dirname, '../../resources/icon.png'),
						to: 'assets/icon.png'
					}]
				),
				// copy any static files
				existsSync(source('assets')) && { from:'assets', to:'assets' }
			].filter(Boolean))
		]
	};
}

function isProd(config) {
	let limit = 200 * 1000; // 200kb

	return {
		performance: {
			hints: 'warning',
			maxAssetSize: limit,
			maxEntrypointSize: limit,
			...config.pkg.performance
		},

		plugins: [
			new webpack.optimize.UglifyJsPlugin({
				output: { comments:false },
				mangle: true,
				sourceMap: true,
				compress: {
					properties: true,
					keep_fargs: false,
					pure_getters: true,
					collapse_vars: true,
					warnings: false,
					screw_ie8: true,
					sequences: true,
					dead_code: true,
					drop_debugger: true,
					comparisons: true,
					conditionals: true,
					evaluate: true,
					booleans: true,
					loops: true,
					unused: true,
					hoist_funs: true,
					if_return: true,
					join_vars: true,
					cascade: true,
					drop_console: false,
					pure_funcs: [
						'classCallCheck',
						'_classCallCheck',
						'_possibleConstructorReturn',
						'Object.freeze',
						'invariant',
						'warning'
					]
				}
			}),
			new SWPrecacheWebpackPlugin({
				filename: 'sw.js',
				navigateFallback: 'index.html',
				navigateFallbackWhitelist: [/^(?!\/__).*/],
				minify: true,
				stripPrefix: config.cwd,
				staticFileGlobsIgnorePatterns: [
					/polyfills(\..*)?\.js$/,
					/\.map$/,
					/push-manifest\.json$/,
					/.DS_Store/
				]
			}),
			new webpack.DefinePlugin({
				'process.env.ADD_SW': config.serviceWorker
			}),
		]
	};
}

function isDev(config) {
	const { cwd, src } = config;

	return {
		plugins: [
			new webpack.NamedModulesPlugin(),
			new webpack.HotModuleReplacementPlugin()
		],

		devServer: {
			inline: true,
			hot:true,
			compress: true,
			publicPath: '/',
			contentBase: src,
			https: config.https,
			port: process.env.PORT || config.port || 8080,
			host: process.env.HOST || config.host || '0.0.0.0',
			// setup(app) {
			// 	app.use(middleware);
			// },
			disableHostCheck: true,
			historyApiFallback: true,
			quiet: true,
			clientLogLevel: 'none',
			overlay: false,
			stats: 'minimal',
			watchOptions: {
				ignored: [
					resolve(cwd, 'build'),
					resolve(cwd, 'node_modules')
				]
			}
		}
	};
}

export default function (env) {
	return merge(
		baseConfig(env),
		clientConfig(env),
		(env.isProd ? isProd : isDev)(env)
	);
}
<|endoftext|>