<|start_filename|>client/src/components/ProportionConstraintField/ImageSizePresetList.js<|end_filename|>
import React from 'react';
import PropTypes from 'prop-types';
import { Button } from 'reactstrap';
import i18n from 'i18n';

/**
 * Generate an explicit message for screen readers only.
 * @param {string} text
 * @returns {string}
 */
const srText = (text) => (
  i18n.inject(
    i18n._t(
      'AssetAdmin.SET_IMAGE_SIZE_TO',
      'Set image size to "{preset}"'
    ),
    { preset: text }
  )
);

/**
 * Display a link-button that will allow user to select an image size preset.
 */
const PresetButton = ({ onSelect, currentWidth, originalWidth, width, text }) => (
  <Button
    color="link"
    size="sm"
    onClick={() => onSelect(width || originalWidth)}
    disabled={(originalWidth < width) || currentWidth === (width || originalWidth)}
  >
    <span className="sr-only">{srText(text)}</span>
    <span aria-hidden="true">{text}</span>
  </Button>
);

/**
 * Display a list of possible image size presets that the user can click to quicky set the width
 * and height of the image in the placement form.
 * @param {{text: String, width: Number}[]} imageSizePresets
 */
const ImageSizePresetList = ({ imageSizePresets, ...btnProps }) => (
  imageSizePresets ? (
    <ul className="image-size-preset-list">
      { imageSizePresets.map(
        (presetProps) => (
          <li key={presetProps.text} className="image-size-preset-list__list-item">
            <PresetButton {...presetProps} {...btnProps} />
          </li>
        )
      ) }
    </ul>
  ) : null
);

ImageSizePresetList.propTypes = {
  onSelect: PropTypes.func,
  imageSizePresets: PropTypes.arrayOf(PropTypes.shape({
    text: PropTypes.string,
    width: PropTypes.number
  })),
  currentWidth: PropTypes.number,
  originalWidth: PropTypes.number.isRequired,
};

export default ImageSizePresetList;


<|start_filename|>client/src/components/UploadField/UploadField.js<|end_filename|>
import i18n from 'i18n';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators, compose } from 'redux';
import { inject } from 'lib/Injector';
import CONSTANTS from 'constants/index';
import fieldHolder from 'components/FieldHolder/FieldHolder';
import fileShape from 'lib/fileShape';
import getStatusCodeMessage from 'lib/getStatusCodeMessage';
import * as uploadFieldActions from 'state/uploadField/UploadFieldActions';
import * as modalActions from 'state/modal/ModalActions';
import PropTypes from 'prop-types';

/**
 * Check if two arrays of file objects have different id keys
 *
 * @param {Array} left
 * @param {Array} right
 */
function compareValues(left, right) {
  // Check length
  if (left.length !== right.length) {
    return true;
  }
  // Check ids appear in the same order
  for (let i = 0; i < left.length; i++) {
    if (left[i].id !== right[i].id) {
      return true;
    }
  }
  return false;
}

class UploadField extends Component {
  constructor(props) {
    super(props);
    this.getMaxFiles = this.getMaxFiles.bind(this);
    this.getFolderId = this.getFolderId.bind(this);
    this.renderChild = this.renderChild.bind(this);
    this.handleAddShow = this.handleAddShow.bind(this);
    this.handleHide = this.handleHide.bind(this);
    this.handleAddInsert = this.handleAddInsert.bind(this);
    this.handleInsertMany = this.handleInsertMany.bind(this);
    this.handleAddedFile = this.handleAddedFile.bind(this);
    this.handleSending = this.handleSending.bind(this);
    this.handleUploadProgress = this.handleUploadProgress.bind(this);
    this.handleFailedUpload = this.handleFailedUpload.bind(this);
    this.handleSuccessfulUpload = this.handleSuccessfulUpload.bind(this);
    this.handleItemRemove = this.handleItemRemove.bind(this);
    this.handleReplaceShow = this.handleReplaceShow.bind(this);
    this.handleChange = this.handleChange.bind(this);
    this.handleReplace = this.handleReplace.bind(this);
    this.canEdit = this.canEdit.bind(this);
    this.canAttach = this.canAttach.bind(this);
    this.canUpload = this.canUpload.bind(this);

    this.state = {
      selecting: false,
      selectingItem: null,
    };
  }

  componentDidMount() {
    // Copy form schema data into redux and then ignore it
    const { id, data, actions, value, files } = this.props;

    // If the data within the "files" prop already matches the value then we don't need to copy
    // schema data into redux
    if (
      value && value.Files && files && value.Files.length === files.length
      && files.filter(file => !value.Files.includes(file.id)).length === 0
    ) {
      return;
    }

    actions.uploadField.setFiles(id, data.files);
  }

  componentWillReceiveProps(nextProps) {
    // Propegate redux state changes to redux-from value for this field
    const existingFiles = this.props.files || [];
    const newFiles = nextProps.files || [];
    const filesChanged = compareValues(existingFiles, newFiles);

    if (filesChanged) {
      this.handleChange(null, nextProps);
    }
  }

  componentDidUpdate(prevProps) {
    // If the value updates but there's no files entry for the value then we need to perform a "set
    // files" action... This can happen when the value (stored with redux-form) is updated
    const { value: { Files: prevValue } } = prevProps;
    const {
      id,
      data,
      files,
      value: { Files: value },
      actions: { uploadField: { setFiles } }
    } = this.props;

    if (
      // If the lengths match
      value.length === prevValue.length
      // AND there's no difference in the values
      && value.filter(item => !prevValue.includes(item)).length === 0
    ) {
      // Then nothing to do
      return;
    }

    // Now we need to check if the files array that we currently have suits the value
    const fileIds = files.map(file => file.id);

    // This is a similar condition to above, just check the files array rather than the previous
    // value
    if (
      fileIds.length === value.length
      && fileIds.filter(fileId => !value.includes(fileId)).length === 0
    ) {
      return;
    }

    // Run the redux action...
    setFiles(id, data.files);
  }

  /**
   * Returns the max number of files allowed for uploading
   *
   * @return {?Number}
   */
  getMaxFiles() {
    const maxFiles = this.props.data.multi ? this.props.data.maxFiles : 1;
    if (maxFiles === null || typeof maxFiles === 'undefined') {
      return null;
    }

    const filesCount = this.props.files.filter(file =>
      file.id > 0
      && (!file.message || file.message.type !== 'error')
    ).length;

    const allowed = Math.max(maxFiles - filesCount, 0);

    return allowed;
  }

  /**
   * Returns the max allowed filesize (if set)
   *
   * @return {?Number}
   */
  getMaxFilesize() {
    return this.props.data.maxFilesize || null;
  }

  /**
   * Find the ID of the folder to start in.
   * @return {Number}
   */
  getFolderId() {
    const { selectingItem } = this.state;

    if (selectingItem && typeof selectingItem === 'object') {
      // If we are viewing a specific file, return that file's parent folder.
      return selectingItem.parent.id;
    }

    // Otherwise return the default upload folder for the UploadField.
    return this.props.data.parentid || 0;
  }

  handleAddedFile(data) {
    const file = { ...data, uploaded: true };
    this.props.actions.uploadField.addFile(this.props.id, file);
  }

  /**
   * Triggered just before the xhr request is sent.
   *
   * @param {Object} file - File interface. See https://developer.mozilla.org/en-US/docs/Web/API/File
   * @param {Object} xhr
   */
  handleSending(file, xhr) {
    this.props.actions.uploadField.updateQueuedFile(this.props.id, file._queuedId, { xhr });
  }

  /**
   * Update upload progress status.
   *
   * @param {Object} file
   * @param {Number} progress
   */
  handleUploadProgress(file, progress) {
    this.props.actions.uploadField.updateQueuedFile(this.props.id, file._queuedId, { progress });
  }

  /**
   * Handles successful file uploads.
   *
   * @param {Object} file - File interface. See https://developer.mozilla.org/en-US/docs/Web/API/File
   */
  handleSuccessfulUpload(file) {
    const json = JSON.parse(file.xhr.response);

    // SilverStripe send back a success code with an error message sometimes...
    if (typeof json[0].error !== 'undefined') {
      this.handleFailedUpload(file);
      return;
    }

    // @todo - Should successful uploads be moved to another state?
    this.props.actions.uploadField.succeedUpload(this.props.id, file._queuedId, json[0]);
  }

  handleFailedUpload(file, response) {
    const statusCodeMessage = file.xhr && file.xhr.status
      ? getStatusCodeMessage(file.xhr.status)
      : '';
    this.props.actions.uploadField.failUpload(
      this.props.id,
      file._queuedId,
      response,
      statusCodeMessage
    );
  }

  /**
   * Handler for removing an uploaded item
   *
   * @param {Object} event
   * @param {Object} item
   */
  handleItemRemove(event, item) {
    this.props.actions.uploadField.removeFile(this.props.id, item);
  }

  /**
   * Handler for clicking on the uploaded item
   *
   * @param {Object} event
   * @param {Object} selectingItem
   */
  handleReplaceShow(event, selectingItem) {
    this.props.actions.modal.initFormStack('select', 'admin');
    this.setState({
      selecting: true,
      selectingItem,
    });
  }

  /**
   * Event called when selected value is updated
   *
   * @param {Event} event
   * @param {Object} props - new props to get files from
   */
  handleChange(event, props = this.props) {
    if (typeof props.onChange === 'function') {
      // Write back list of files to value
      const fileIds = props.files
        .filter((file) => file.id)
        .map((file) => file.id);
      const newValue = { Files: fileIds };
      props.onChange(event, { id: props.id, value: newValue });
    }
  }

  /**
   * Handler for 'upload' dialog.
   *
   * @param {Object} event - Click event
   */
  handleUploadButton(event) {
    event.preventDefault();
  }

  /**
   * Open new 'add from files' dialog
   *
   * @param {Object} event - Click event
   */
  handleAddShow(event) {
    event.preventDefault();
    this.props.actions.modal.initFormStack('select', 'admin');
    this.setState({
      selecting: true,
      selectingItem: null,
    });
  }

  /**
   * Close 'add from files' dialog
   */
  handleHide() {
    this.props.actions.modal.reset();
    this.setState({
      selecting: false,
      selectingItem: null,
    });
  }

  /**
   * Handle file being added by 'add from files' dialog
   *
   * @param {Event} event
   * @param {Object} data - Submitted insert form data
   * @param {Object} file - file record
   */
  handleAddInsert(event, data, file) {
    this.props.actions.uploadField.addFile(this.props.id, file);
    this.handleHide();

    return Promise.resolve({});
  }

  /**
   * Handle many files being inserted
   *
   * @param {Event} event
   * @param {Array} files
   */
  handleInsertMany(event, files) {
    const { selectingItem } = this.state;
    if (selectingItem) {
      this.handleReplace(event, null, files[0]);
      return;
    }
    files.forEach(file => {
      this.handleAddInsert(event, null, file);
    });
  }

  /**
   * Handle file being replaced from the modal
   *
   * @param {Event} event
   * @param {Object} data
   * @param {Object} file
   */
  handleReplace(event, data, file) {
    const { selectingItem } = this.state;
    const {
      id,
      actions: {
        uploadField: {
          addFile,
          removeFile,
        },
      },
    } = this.props;

    if (!selectingItem) {
      throw new Error('Tried to replace a file when none was selected.');
    }
    removeFile(id, selectingItem);
    addFile(id, file);
    this.handleHide();

    return Promise.resolve({});
  }

  /**
   * Check if this field can be modified
   *
   * @return {Boolean}
   */
  canEdit() {
    return !this.props.disabled
      && !this.props.readOnly
      && (this.props.data.canUpload || this.props.data.canAttach);
  }

  /**
   * Check if this field can upload files
   *
   * @return {Boolean}
   */
  canUpload() {
    return this.canEdit() && this.props.data.canUpload;
  }

  /**
   * Check if this field can select files
   *
   * @return {Boolean}
   */
  canAttach() {
    return this.canEdit() && this.props.data.canAttach;
  }

  /**
   * Render "drop file here" area
   *
   * @returns {object}
   */
  renderDropzone() {
    const { AssetDropzone } = this.props;
    if (!this.props.data.createFileEndpoint) {
      return null;
    }
    const dimensions = {
      height: CONSTANTS.SMALL_THUMBNAIL_HEIGHT,
      width: CONSTANTS.SMALL_THUMBNAIL_WIDTH,
    };
    const maxFiles = this.getMaxFiles();
    const maxFilesize = this.getMaxFilesize();

    const dropzoneOptions = {
      url: this.props.data.createFileEndpoint.url,
      method: this.props.data.createFileEndpoint.method,
      paramName: 'Upload',
      maxFiles,
      maxFilesize,
      thumbnailWidth: CONSTANTS.SMALL_THUMBNAIL_WIDTH,
      thumbnailHeight: CONSTANTS.SMALL_THUMBNAIL_HEIGHT,
    };

    // If single upload and there is a file, don't render dropzone
    const classNames = ['uploadfield__dropzone'];
    if (maxFiles === 0) {
      // needs to be hidden instead of removed from the DOM for upload progress on the last item.
      classNames.push('uploadfield__dropzone--hidden');
    }

    // Handle readonly field
    if (!this.canEdit()) {
      if (this.props.files.length) {
        return null;
      }
      return (
        <p>{i18n._t('AssetAdmin.EMPTY', 'No files')}</p>
      );
    }

    const securityID = this.props.securityId;
    const options = [];
    if (this.canUpload()) {
      options.push(
        <button
          key="uploadbutton"
          type="button"
          onClick={this.handleUploadButton}
          className="uploadfield__upload-button"
        >
          {i18n._t('AssetAdmin.UPLOADFIELD_UPLOAD_NEW', 'Upload new')}
        </button>
      );
    }
    if (this.canAttach()) {
      if (options.length) {
        options.push(
          <span key="uploadjoin" className="uploadfield__join">
            {i18n._t('AssetAdmin.OR', 'or')}
          </span>
        );
      }
      options.push(
        <button
          key="attachbutton"
          type="button"
          onClick={this.handleAddShow}
          className="uploadfield__add-button"
        >
          {i18n._t('AssetAdmin.UPLOADFIELD_CHOOSE_EXISTING', 'Choose existing')}
        </button>
      );
    }

    return (
      <AssetDropzone
        name={this.props.name}
        canUpload={this.canUpload()}
        uploadButton={false}
        uploadSelector=".uploadfield__upload-button, .uploadfield__backdrop"
        folderId={this.props.data.parentid}
        onAddedFile={this.handleAddedFile}
        onError={this.handleFailedUpload}
        onSuccess={this.handleSuccessfulUpload}
        onSending={this.handleSending}
        onUploadProgress={this.handleUploadProgress}
        preview={dimensions}
        options={dropzoneOptions}
        securityID={securityID}
        className={classNames.join(' ')}
      >
        <div className="uploadfield__backdrop" />
        <span className="uploadfield__droptext">{options}</span>
      </AssetDropzone>
    );
  }

  renderModal() {
    const { InsertMediaModal } = this.props;
    const { selecting, selectingItem } = this.state;
    const maxFiles = this.getMaxFiles();
    const folderId = this.getFolderId();

    return (
      <InsertMediaModal
        title={false}
        isOpen={selecting}
        onInsert={selectingItem ? this.handleReplace : this.handleAddInsert}
        onClosed={this.handleHide}
        onInsertMany={this.handleInsertMany}
        maxFiles={selectingItem ? 1 : maxFiles}
        type="select"
        bodyClassName="modal__dialog"
        className="insert-media-react__dialog-wrapper"
        fileAttributes={selectingItem ? { ID: selectingItem.id } : null}
        folderId={folderId}
      />
    );
  }

  /**
   *
   * @param {object} item
   * @param {number} index
   * @returns {object}
   */
  renderChild(item, index) {
    const { UploadFieldItem } = this.props;
    const draftProps = {
      // otherwise only one error file is shown and the rest are hidden due to having the same `key`
      key: item.id ? `file-${item.id}` : `queued-${item.queuedId}`,
      item,
      name: this.props.name,
      onRemove: this.handleItemRemove,
      canEdit: this.canEdit(),
      onView: this.handleReplaceShow,
    };
    const itemProps = this.props.getItemProps(draftProps, index, this.props);

    return <UploadFieldItem {...itemProps} />;
  }

  render() {
    return (
      <div className="uploadfield">
        {this.renderDropzone()}
        {this.props.files.map(this.renderChild)}
        {this.renderModal()}
      </div>
    );
  }
}

UploadField.propTypes = {
  id: PropTypes.string.isRequired,
  name: PropTypes.string.isRequired,
  onChange: PropTypes.func,
  value: PropTypes.shape({ // PHP / posted value
    Files: PropTypes.arrayOf(PropTypes.number),
  }),
  files: PropTypes.arrayOf(fileShape), // Authoritative redux state
  readOnly: PropTypes.bool,
  disabled: PropTypes.bool,
  data: PropTypes.shape({
    files: PropTypes.arrayOf(fileShape),
    createFileEndpoint: PropTypes.shape({
      url: PropTypes.string.isRequired,
      method: PropTypes.string.isRequired,
      payloadFormat: PropTypes.string.isRequired,
    }),
    multi: PropTypes.bool,
    parentid: PropTypes.number,
    canUpload: PropTypes.bool,
    canAttach: PropTypes.bool,
    maxFiles: PropTypes.number,
  }),
  UploadFieldItem: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
  AssetDropzone: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
  InsertMediaModal: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
  getItemProps: PropTypes.func,
};

UploadField.defaultProps = {
  value: { Files: [] },
  className: '',
  getItemProps: props => props,
};

function mapStateToProps(state, ownprops) {
  const id = ownprops.id;
  let files = [];
  if (state.assetAdmin
    && state.assetAdmin.uploadField
    && state.assetAdmin.uploadField.fields
    && state.assetAdmin.uploadField.fields[id]
  ) {
    files = state.assetAdmin.uploadField.fields[id].files || [];
  }
  const securityId = state.config.SecurityID;
  return { files, securityId };
}

function mapDispatchToProps(dispatch) {
  return {
    actions: {
      uploadField: bindActionCreators(uploadFieldActions, dispatch),
      modal: bindActionCreators(modalActions, dispatch)
    },
  };
}

const ConnectedUploadField = connect(mapStateToProps, mapDispatchToProps)(UploadField);

export { UploadField as Component, ConnectedUploadField };

export default compose(
  inject(['UploadFieldItem', 'AssetDropzone', 'InsertMediaModal']),
  fieldHolder,
)(ConnectedUploadField);


<|start_filename|>client/src/constants/index.js<|end_filename|>
/* global alert, confirm */
import i18n from 'i18n';

export default {
  ACTIONS: {
    CREATE_FOLDER: 'create-folder',
    EDIT_FILE: 'edit',
  },
  MOVE_SUCCESS_DURATION: 3000,
  CSS_TRANSITION_TIME: 300,
  SMALL_THUMBNAIL_HEIGHT: 60,
  SMALL_THUMBNAIL_WIDTH: 60,
  THUMBNAIL_HEIGHT: 150,
  THUMBNAIL_WIDTH: 200,
  BULK_ACTIONS: [
    {
      value: 'delete',
      label: i18n._t('AssetAdmin.BULK_ACTIONS_DELETE', 'Delete'),
      className: 'font-icon-trash',
      destructive: true,
      callback: null, // defined in <Gallery> for now
      canApply: (items) => (
        items.every(item => item && item.canDelete)
      ),
    },
    {
      value: 'edit',
      label: i18n._t('AssetAdmin.BULK_ACTIONS_EDIT', 'Edit'),
      className: 'font-icon-edit',
      destructive: false,
      // Only allow editing if a single item (file or folder) is selected
      canApply: (items) => items.length === 1,
      callback: null, // defined in <Gallery> for now
    },
    {
      value: 'move',
      label: i18n._t('AssetAdmin.BULK_ACTIONS_MOVE', 'Move'),
      className: 'font-icon-folder-move',
      canApply: (items) => items.every(item => item && item.canEdit),
      destructive: false,
      callback: null,
    },
    {
      value: 'publish',
      label: i18n._t('AssetAdmin.BULK_ACTIONS_PUBLISH', 'Publish'),
      className: 'font-icon-rocket',
      destructive: false,
      callback: null, // defined in <Gallery> for now
      canApply: (items) => (
        items.some(item => item && item.modified) &&
        items.every(item => item.canEdit && item.type !== 'folder')
      ),
      confirm: null,
    },
    {
      value: 'unpublish',
      label: i18n._t('AssetAdmin.BULK_ACTIONS_UNPUBLISH', 'Unpublish'),
      className: 'font-icon-cancel-circled',
      destructive: false,
      callback: null, // defined in <Gallery> for now
      canApply: (items) => (
        items.some(item => item.published) &&
        items.every(item => item.canEdit && item.type !== 'folder')
      ),
      confirm: null,
    },
    {
      value: 'insert',
      label: i18n._t('AssetAdmin.BULK_ACTIONS_INSERT', 'Insert'),
      className: 'font-icon-plus-circled btn-primary',
      destructive: false,
      callback: null, // defined in <Gallery> for now
      canApply: (items) => items.length,
      confirm: null,
    },
  ],
  BULK_ACTIONS_PLACEHOLDER: i18n._t('AssetAdmin.BULK_ACTIONS_PLACEHOLDER'),
  SPACE_KEY_CODE: 32,
  RETURN_KEY_CODE: 13,
  DEFAULT_PREVIEW: 'framework/client/dist/images/app_icons/generic_92.png',
  MODAL_MOVE: 'MODAL_MOVE',
};


<|start_filename|>client/src/lib/tests/getFormSchema-test.js<|end_filename|>
/* global jest, jasmine, describe, it, expect, beforeEach, FormData */

import getFormSchema from '../getFormSchema';
import CONSTANTS from 'constants/index';

const { CREATE_FOLDER, EDIT_FILE } = CONSTANTS.ACTIONS;

const config = {
  form: {
    folderCreateForm: { schemaUrl: 'folder/create' },
    fileInsertForm: { schemaUrl: 'file/insert' },
    fileEditorLinkForm: { schemaUrl: 'file/link' },
    fileSelectForm: { schemaUrl: 'file/select' },
    fileEditForm: { schemaUrl: 'file/edit' },
  }
};

function expectSchema(actual, schemaUrl, targetId) {
  expect(actual).toHaveProperty('schemaUrl', schemaUrl);
  expect(actual).toHaveProperty('targetId', targetId);
}

describe('getFormSchema', () => {
  describe('folder', () => {
    it('create', () => {
      expectSchema(
        getFormSchema({ config, viewAction: CREATE_FOLDER, folderId: 123 }),
        'folder/create',
        123
      );
    });
  });

  describe('file', () => {
    const types = [
      ['insert-media', 'file/insert'],
      ['insert-link', 'file/link'],
      ['select', 'file/select'],
      ['admin', 'file/edit'],
      ['default', 'file/edit'],
    ];

    types.forEach(
      ([type, expectedSchemaUrl]) => {
        it(type, () => {
          expectSchema(
            getFormSchema({ config, viewAction: EDIT_FILE, fileId: 321, type }),
            expectedSchemaUrl,
            321
          );
        });
      }
    );
  });

  describe('invalid inputs', () => {
    it('bad action', () => {
      expect(getFormSchema({ config, viewAction: 'nonsense', folderId: 123 })).toMatchObject({});
    });

    it('file action without fileId', () => {
      expect(getFormSchema({ config, viewAction: EDIT_FILE, folderId: 123 })).toMatchObject({});
    });
  });
});


<|start_filename|>client/src/containers/BulkDeleteConfirmation/DeletionModal.js<|end_filename|>
import i18n from 'i18n';
import React from 'react';
import PropTypes from 'prop-types';
import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap';

const DeletionModal = ({ isOpen, body, onCancel, actions }) => (
  <Modal isOpen={isOpen} toggle={onCancel}>
    <ModalHeader toggle={onCancel}>
      {i18n._t('AssetAdmin.CONFIRM_FILE_DELETION', 'Confirm deletion')}
    </ModalHeader>
    <ModalBody>{body}</ModalBody>
    <ModalFooter>
      {actions.map(({ label, handler, color }) => (
        <Button key={label} color={color} onClick={handler}>{label}</Button>
        ))}
    </ModalFooter>
  </Modal>
  );

DeletionModal.propTypes = {
  isOpen: PropTypes.bool.isRequired,
  body: PropTypes.node.isRequired,
  onCancel: PropTypes.func.isRequired,
  actions: PropTypes.arrayOf(PropTypes.shape({
    label: PropTypes.string.isRequired,
    handler: PropTypes.func,
    color: PropTypes.string
  }))
};

export default DeletionModal;


<|start_filename|>client/lang/it.js<|end_filename|>
// This file was generated by silverstripe/cow from client/lang/src/it.json.
// See https://github.com/tractorcow/cow for details
if (typeof(ss) === 'undefined' || typeof(ss.i18n) === 'undefined') {
  if (typeof(console) !== 'undefined') { // eslint-disable-line no-console
    console.error('Class ss.i18n not defined');  // eslint-disable-line no-console
  }
} else {
  ss.i18n.addDictionary('it', {
    "AssetAdmin.ADD_FOLDER_BUTTON": "Aggiungi cartella",
    "AssetAdmin.BACK": "Indietro",
    "AssetAdmin.BACK_DESCRIPTION": "Vai al livello superiore",
    "AssetAdmin.BULK_ACTIONS_CONFIRM": "Sei sicuro di voler %s questi file?",
    "AssetAdmin.BULK_ACTIONS_DELETE": "Elimina",
    "AssetAdmin.BULK_ACTIONS_DELETE_CONFIRM": "Sei sicuro di voler eliminare questi file?",
    "AssetAdmin.BULK_ACTIONS_DELETE_FAIL": "%s cartelle/file sono stati archiviati con successo ma non è stato possibile archiviare %s file.",
    "AssetAdmin.BULK_ACTIONS_DELETE_FOLDER": "Queste cartelle contengono file attualmente in uso, devi spostare o eliminare il suo contenuto prima di poter eliminare una cartella.",
    "AssetAdmin.BULK_ACTIONS_DELETE_MULTI_CONFIRM": "Ci sono %s file attualmente in uso, sei sicuro di voler eliminare questi file?",
    "AssetAdmin.BULK_ACTIONS_DELETE_SINGLE_CONFIRM": "Questo file è attualmente usato in %s posti, se sicuro di volerlo eliminare?",
    "AssetAdmin.BULK_ACTIONS_DELETE_SUCCESS": "%s cartelle/file sono state archiviate con successo.",
    "AssetAdmin.BULK_ACTIONS_PLACEHOLDER": "Scegliere un'azione...",
    "AssetAdmin.CANCEL": "Annulla",
    "AssetAdmin.CONFIRMDELETE": "Sei sicuro di voler eliminare questo record?",
    "AssetAdmin.CREATED": "Caricato il",
    "AssetAdmin.DELETE": "Elimina",
    "AssetAdmin.DIM": "Dimensioni",
    "AssetAdmin.DROPZONE_CANCEL_UPLOAD": "Annulla caricamento",
    "AssetAdmin.DROPZONE_CANCEL_UPLOAD_CONFIRMATION": "Sei sicuro di voler annullare questo caricamento?",
    "AssetAdmin.DROPZONE_DEFAULT_MESSAGE": "Rilascia qui i file da caricare",
    "AssetAdmin.DROPZONE_FAILED_UPLOAD": "Caricamento file fallito",
    "AssetAdmin.DROPZONE_FALLBACK_MESSAGE": "Il tuo browser non supporta il caricamento file tramite trascinamento.",
    "AssetAdmin.DROPZONE_FALLBACK_TEXT": "Usare il modulo alternativo sottostante per caricare i fie alla vecchia maniera.",
    "AssetAdmin.DROPZONE_FILE_TOO_BIG": "File troppo grande. Dimensione massima: %sMiB.",
    "AssetAdmin.DROPZONE_INVALID_FILE_TYPE": "Non puoi caricare file di questo tipo.",
    "AssetAdmin.DROPZONE_MAX_FILES_EXCEEDED": "Non puoi più caricare file.",
    "AssetAdmin.DROPZONE_REMOVE_FILE": "Elimina file",
    "AssetAdmin.DROPZONE_RESPONSE_ERROR": "Il server ha risposto con errore.",
    "AssetAdmin.DROPZONE_SUCCESS_UPLOAD": "File caricato",
    "AssetAdmin.DROPZONE_UPLOAD": "Carica",
    "AssetAdmin.EDIT": "Modifica",
    "AssetAdmin.FILENAME": "Nome file",
    "AssetAdmin.FILES": "File",
    "AssetAdmin.FILE_MISSING": "Il file non può essere trovato",
    "AssetAdmin.FILTER_DATE_ASC": "più vecchio",
    "AssetAdmin.FILTER_DATE_DESC": "più nuovo",
    "AssetAdmin.FILTER_TITLE_ASC": "titolo a-z",
    "AssetAdmin.FILTER_TITLE_DESC": "titolo z-a",
    "AssetAdmin.JOIN": ",",
    "AssetAdmin.JOINLAST": "e",
    "AssetAdmin.LASTEDIT": "Ultima modifica",
    "AssetAdmin.LOADMORE": "Carica ancora",
    "AssetAdmin.NOITEMSFOUND": "Nessun elemento trovato",
    "AssetAdmin.PROMPTFOLDERNAME": "Inserire il nome della cartella (o lasciare vuoto per annullare)",
    "AssetAdmin.REPlACE_FILE_SUCCESS": "Caricamento eseguito, il file sarà sostituito con Salva.",
    "AssetAdmin.SAVE": "Salva",
    "AssetAdmin.SEARCHCLEARRESULTS": "Annulla ricerca",
    "AssetAdmin.SEARCHRESULTS": "Risultati ricerca",
    "AssetAdmin.SEARCHRESULTSMESSAGE": "Risultati ricerca {parts}",
    "AssetAdmin.SEARCHRESULTSMESSAGECATEGORY": "catalogato come '{appCategory}'",
    "AssetAdmin.SEARCHRESULTSMESSAGEEDITEDBETWEEN": "ultima modifica tra '{lastEditedFrom}' e '{lastEditedTo}'",
    "AssetAdmin.SEARCHRESULTSMESSAGEEDITEDFROM": "ultima modifica dal '{lastEditedFrom}'",
    "AssetAdmin.SEARCHRESULTSMESSAGEEDITEDTO": "ultima modifica prima del '{lastEditedTo}'",
    "AssetAdmin.SEARCHRESULTSMESSAGEKEYWORDS": "con i termini '{name}'",
    "AssetAdmin.SEARCHRESULTSMESSAGELIMIT": "ristretta alla cartella '{folder}'",
    "AssetAdmin.SELECT": "Scegliere",
    "AssetAdmin.SIZE": "Dimensione",
    "AssetAdmin.TITLE": "Titolo",
    "AssetAdmin.TYPE": "Tipo file",
    "AssetAdmin.URL": "URL",
    "AssetAdmin.ADD_FILES": "Aggiungi da file",
    "AssetAdmin.BROWSE": "Sfoglia",
    "AssetAdmin.EMPTY": "Nessun file",
    "AssetAdmin.OR": "o",
    "AssetAdmin.ERROR_OEMBED_REMOTE": "Embed is only compatible with remote files",
    "AssetAdmin.CreateTitle": "Inserire nuovo materiale dal web",
    "AssetAdmin.EditTitle": "Materiale dal web",
    "AssetAdmin.NEXT": "Prossimo",
    "AssetAdmin.PREVIOUS": "Precedente"
});
}

<|start_filename|>client/src/lib/getStatusCodeMessage.js<|end_filename|>
import i18n from 'i18n';

/**
 * Convert an http status code to a message
 *
 * @param { int } statusCode
 */
export default function getStatusCodeMessage(statusCode) {
  if (statusCode === 413) {
    return i18n._t('AssetAdmin.ERROR_FILE_SIZE', 'File size limit exceeded');
  }
  return i18n._t('AssetAdmin.ERROR_DEFAULT', 'Something went wrong, please try again');
}


<|start_filename|>client/src/state/modal/ModalActionTypes.js<|end_filename|>
export default {
  DEFINE_IMAGE_SIZE_PRESETS: 'DEFINE_IMAGE_SIZE_PRESETS',
  INIT_FORM_SCHEMA_STACK: 'INIT_FORM_SCHEMA_STACK',
  POP_FORM_SCHEMA: 'POP_FORM_SCHEMA',
  PUSH_FORM_SCHEMA: 'PUSH_FORM_SCHEMA',
  RESET: 'RESET',
  RESET_FORM_STACK: 'RESET_FORM_STACK'
};


<|start_filename|>client/src/containers/AssetAdmin/AssetAdminBreadcrumb.js<|end_filename|>
/* global alert, confirm */
import React from 'react';
import PropTypes from 'prop-types';
import i18n from 'i18n';
import { Component as PlainBreadcrumb } from 'components/Breadcrumb/Breadcrumb';
import { hasFilters } from 'components/Search/Search';

/**
 * Display a breadcrumb based on the current folder, bypassing the stateful Breadcrumb component.
 * @param {Object} folder Info about the current folder.
 * @param {Object} query  Info about the current search query.
 *                        Used to figure out if looking at search results.
 * @param {Function} getUrl Method used to compute the URL for a given file or folder.
 * @param {Function} onBrowse Callback for when the user click on one of the breadcrumb element.
 * @param {Function} onFolderIcon Callback for when the user decide to edit the info for a folder.
 */
const AssetAdminBreadcrumb = ({ folder, query, getUrl, onBrowse, onFolderIcon }) => {
  // Simple wrapper method around onBrowse that suppress the event
  const handleClick = (...args) => (event) => {
    event.preventDefault();
    onBrowse(...args);
  };

  // Simple wrapper around getURL to make sure a callback has been provided
  const hrefBuilder = (...args) => getUrl && getUrl(...args);

  // Set root breadcrumb
  const breadcrumbs = [{
    text: i18n._t('AssetAdmin.FILES', 'Files'),
    href: hrefBuilder(0, null, query),
    onClick: handleClick(0, null, query),
  }];

  if (folder && folder.id) {
    // Add parent folders
    if (folder.parents) {
      folder.parents.forEach((parent) => {
        breadcrumbs.push({
          text: parent.title,
          href: hrefBuilder(parent.id, null, query),
          onClick: handleClick(parent.id, null, query)
        });
      });
    }

    // Add current folder
    const icons = [
      {
        className: 'icon font-icon-edit-list',
        onClick: (e) => {
          e.preventDefault();
          onFolderIcon();
        },
      }
    ];

    if (folder.hasRestrictedAccess) {
      icons.push(
        {
          nodeName: 'FileStatusIcon',
          hasRestrictedAccess: true,
        }
      );
    }

    breadcrumbs.push({
      text: folder.title,
      href: hrefBuilder(folder.id, null, query),
      onClick: handleClick(folder.id, null, query),
      icons
    });
  }
  // Search leaf if there was a search entered
  if (hasFilters(query.filter)) {
    breadcrumbs.push({
      text: i18n._t('LeftAndMain.SEARCHRESULTS', 'Search results'),
    });
  }

  return <PlainBreadcrumb multiline crumbs={breadcrumbs} />;
};

AssetAdminBreadcrumb.propTypes = {
  onBrowse: PropTypes.func,
  onFolderIcon: PropTypes.func,
  getUrl: PropTypes.func,
  query: PropTypes.shape({
    sort: PropTypes.string,
    limit: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
    page: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
    filter: PropTypes.object,
    view: PropTypes.string,
  }),
  folder: PropTypes.shape({
    id: PropTypes.number,
    title: PropTypes.string,
    parents: PropTypes.array,
    parentId: PropTypes.number,
    canView: PropTypes.bool,
    canEdit: PropTypes.bool,
  }),
};

export default AssetAdminBreadcrumb;


<|start_filename|>client/src/containers/BulkDeleteConfirmation/tests/helpers-test.js<|end_filename|>
/* global jest, describe, it, pit, expect, beforeEach, jasmine */
import { getFolderDescendantFileTotals, getFileTotalItems } from '../helpers';
import { mockfiles as files } from './mockfiles';

describe('BulkDeleteConfirmation Helper methods', () => {
  describe('getFolderInUseCounts', () => {
    const testCases = [
      ['one folder in use', { 1: 1 }, 1, 1],
      ['many folders in use', { 1: 1, 2: 2 }, 2, 3],
      ['nothing in use', { 3: 0, 4: 0, 1: 0, 2: 0 }, 0, 0],
      ['one file in use', { 4: 1 }, 0, 0],
      ['many files in use', { 3: 2, 4: 1 }, 0, 0],
      ['missing file use entries', {}, 0, 0],
      ['unknown file IDs', { 123456: 1 }, 0, 0],
      ['file and folder in use', { 3: 2, 1: 3 }, 1, 3],
    ];

    testCases.forEach((testCaseData) => {
      const [
        description,
        descendantFileCounts,
        expectedTotalItems,
        expectedTotalCount
      ] = testCaseData;

      it(description, () => {
        const { totalItems, totalCount } =
          getFolderDescendantFileTotals(files, descendantFileCounts);
        expect(totalItems).toBe(expectedTotalItems);
        expect(totalCount).toBe(expectedTotalCount);
      });
    });
  });

  describe('getFileTotalItems', () => {
    expect(getFileTotalItems(files)).toBe(2);
  });
});


<|start_filename|>client/src/state/files/readFileUsageQuery.js<|end_filename|>
import { graphqlTemplates } from 'lib/Injector';

const apolloConfig = {
  options({ files }) {
    return {
      variables: {
        ids: files.map((file) => file.id)
      },
    };
  },
  props(
    props
  ) {
    const {
      data: {
        error,
        readFileUsage,
        loading: networkLoading,
      },
    } = props;
    const errors = error && error.graphQLErrors &&
      error.graphQLErrors.map((graphQLError) => graphQLError.message);

    const fileUsage = readFileUsage ?
      readFileUsage.reduce((accumulator, { id, inUseCount }) => (
        { ...accumulator, [id]: inUseCount }
      ), {}) :
      {};

    return {
      loading: networkLoading,
      fileUsage,
      graphQLErrors: errors,
    };
  },
};

const { READ } = graphqlTemplates;
const query = {
  apolloConfig,
  templateName: READ,
  pluralName: 'FileUsage',
  pagination: false,
  params: {
    ids: '[ID]!'
  },
  args: {
    root: {
      ids: 'ids'
    }
  },
  fields: ['id', 'inUseCount'],
};
export default query;


<|start_filename|>client/src/lib/tests/getStatusCodeMessage-test.js<|end_filename|>
/* global jest, jasmine, describe, it, expect, beforeEach, FormData */

import getStatusCodeMessage from '../getStatusCodeMessage';

describe('getStatusCodeMessage', () => {
  const defaultMessage = getStatusCodeMessage(999);
  it('Has a default message', () => {
    expect(defaultMessage.length > 0).toBe(true);
  });
  it('To have a different message for a 413 status code', () => {
    const status413Message = getStatusCodeMessage(413);
    expect(status413Message.length > 0 && status413Message !== defaultMessage).toBe(true);
  });
});


<|start_filename|>client/src/state/modal/helpers.js<|end_filename|>
/**
 * Recursively search the fieldTree to find a field based on its name.
 * @param fieldName
 * @param fieldTree
 * @returns {false|Object}
 */
export function findField(fieldName, fieldTree) {
  let i;
  for (i = 0; i < fieldTree.length; i++) {
    const field = fieldTree[i];
    if (field.name === fieldName) {
      return field;
    }

    if (field.children) {
      const child = findField(fieldName, field.children);
      if (child) {
        return child;
      }
    }
  }

  return false;
}

/**
 * Determine if the provided field should be stash. We stash a field if:
 * * it's non-structual and
 * * visible and
 * * editable
 * @param {string} fieldName
 * @param {array} fieldTree
 */
export default function isStashableField(fieldName, fieldTree) {
  const field = findField(fieldName, fieldTree);

  return field
    && field.type !== 'hidden'
    && field.schemaType !== 'Structural'
    && !field.readOnly
    && !field.disabled;
}


<|start_filename|>client/src/containers/BulkDeleteConfirmation/helpers.js<|end_filename|>
import PropTypes from 'prop-types';

/**
 * Check if the provided file entry is a folder
 * @param {Object} file
 * @return {boolean}
 */
const isFolder = (({ type }) => (type === 'folder'));

/**
 * Check if the provided file entry is a file
 * @param {Object} file
 * @return {boolean}
 */
const isFile = (file => (!isFolder(file)));

/**
 * Given a fileCount map, creates a reducer callback that produces an object containing:
 * - totalItems, number of files
 * - totalCount, total count of nests non-folders across files
 * @param {Object} descendentFileCount
 */
const descendantFileCountsReducer = (descendentFileCount) =>
  (accumulator, { id }) => (
    (descendentFileCount[id] > 0) ?
      {
        totalItems: accumulator.totalItems + 1,
        totalCount: accumulator.totalCount + descendentFileCount[id]
      } : accumulator
  );

/**
 * Initial accumulator to use with `fileCountReducer`.
 * @type {{totalItems: number, totalCount: number}}
 */
const descendantFileCountsInitAccumulator = { totalItems: 0, totalCount: 0 };

/**
 * PropType shape for the file count object
 */
export const descendantFileTotalsShape = PropTypes.shape({
  totalItems: PropTypes.number,
  totalCount: PropTypes.number,
});

/**
 * Count the number of folders that are currently in use and the number of places where they are
 * in use.
 * @param {Object[]} files
 * @param {Object} descendantFileCounts
 * @return {Object}
 * @note Not part of the public API
 */
export const getFolderDescendantFileTotals = (files, descendantFileCounts) => (
  files.filter(isFolder)
    .reduce(descendantFileCountsReducer(descendantFileCounts), descendantFileCountsInitAccumulator)
);
/**
 * Count the number of files
 * @param {Object[]} files
 * @return {number}
 * @note Not part of the public API
 */
export const getFileTotalItems = (files) => files.filter(isFile).length;


<|start_filename|>client/src/state/files/readDescendantFileCountsQuery.js<|end_filename|>
import { graphqlTemplates } from 'lib/Injector';

const apolloConfig = {
  options({ files }) {
    return {
      variables: {
        ids: files.map((file) => file.id)
      },
    };
  },
  props(
    props
  ) {
    const {
      data: {
        error,
        readDescendantFileCounts,
        loading: networkLoading,
      },
    } = props;
    const errors = error && error.graphQLErrors &&
      error.graphQLErrors.map((graphQLError) => graphQLError.message);

    const descendantFileCounts = readDescendantFileCounts ?
      readDescendantFileCounts.reduce((accumulator, { id, count }) => (
        { ...accumulator, [id]: count }
      ), {}) :
      {};

    return {
      loading: networkLoading,
      descendantFileCounts,
      graphQLErrors: errors,
    };
  },
};

const { READ } = graphqlTemplates;
const query = {
  apolloConfig,
  templateName: READ,
  pluralName: 'DescendantFileCounts',
  pagination: false,
  params: {
    ids: '[ID]!'
  },
  args: {
    root: {
      ids: 'ids'
    }
  },
  fields: ['id', 'count'],
};
export default query;


<|start_filename|>client/src/types/ImageSizePreset.js<|end_filename|>
/**
 * @typedef ImageSizePreset
 * @type {object}
 * @property {number|undefined} width Preset size or undefined for original size.
 * @property {string} name Underlying system name. Never displayed to the user.
 * @property {string} text Button text.
 */


<|start_filename|>client/src/containers/BulkDeleteConfirmation/tests/BulkDeleteConfirmation-story.js<|end_filename|>
import React from 'react';
// eslint-disable-next-line import/no-extraneous-dependencies
import { setAddon, storiesOf } from '@storybook/react';
import { Component as BulkDeleteConfirmation } from '../BulkDeleteConfirmation';
import { withKnobs, boolean, select } from '@storybook/addon-knobs/react';
import { JSXAddon } from 'storybook-addon-jsx';
import { mockfiles as files } from './mockfiles';
import LoadingComponent from 'components/Loading/Loading';
import { action } from '@storybook/addon-actions';

setAddon(JSXAddon);

const actions = {
  onCancel: (event) => action('onCancel')(event),
  onConfirm: (event) => action('onConfirm')(event),
  onModalClose: (event) => action('onModalClose')(event)
};

actions.onCancel.toString = () => 'onCancel';
actions.onConfirm.toString = () => 'onConfirm';
actions.onModalClose.toString = () => 'onModalClose';

const propsForState = {
  loading: {
    loading: true,
    LoadingComponent
  },
  'nothing in use': {
    loading: false,
    files: files.filter(({ id }) => id === 3),
    descendantFileCounts: {}
  },
  'single file in use': {
    loading: false,
    files: files.filter(({ id }) => id === 3),
    descendantFileCounts: { 3: 1 }
  },
  'many file in use': {
    loading: false,
    files,
    descendantFileCounts: { 3: 1, 4: 5 }
  },
  'folder in use': {
    loading: false,
    files: files.filter(({ id }) => id === 1),
    descendantFileCounts: { 1: 1 }
  },
};

storiesOf('AssetAdmin/DeleteConfirmation', module)
  .addDecorator(withKnobs)
  .add('Default', () => {
    const key = select('State', Object.keys(propsForState), 'loading');
    const props = propsForState[key];

    return (<BulkDeleteConfirmation
      transition={boolean('canceling', false) && 'canceling'}
      {...props}
      {...actions}
    />);
  });


<|start_filename|>client/src/components/ProportionConstraintField/tests/ImageSizePresetList-test.js<|end_filename|>
/* global jest, jasmine, describe, it, expect, beforeEach */

import React from 'react';
import ImageSizePresetList from '../ImageSizePresetList';
import Enzyme, { mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16/build/index';


Enzyme.configure({ adapter: new Adapter() });

const imageSizePresets = [
  { width: 1234, text: 'default', default: true },
  { width: 4321, text: 'super large' },
  { width: 2, text: 'super small' },
];

/**
 * @param {Object} props
 * @returns {{item: *, calls: }}
 */
const render = (props = {}) => {
  const onSelect = jest.fn();

  const item = mount(
    <ImageSizePresetList
      onSelect={onSelect}
      imageSizePresets={imageSizePresets}
      currentWidth={123}
      originalWidth={321}
      {...props}
    />
  );

  return {
    item,
    calls: onSelect.mock.calls,
    buttons: item ? item.find('Button') : undefined
  };
};

describe('ImageSizePresetList', () => {
  describe('render()', () => {
    it('presets are displayed', () => {
      const { buttons } = render();
      expect(buttons.get(0).props.children[0].props.children).toContain('Set image size to');
      expect(buttons.get(0).props.children[1].props.children).toContain('default');
      expect(buttons.get(1).props.children[0].props.children).toContain('Set image size to');
      expect(buttons.get(1).props.children[1].props.children).toContain('super large');
      expect(buttons.get(2).props.children[0].props.children).toContain('Set image size to');
      expect(buttons.get(2).props.children[1].props.children).toContain('super small');
    });

    it('preset bigger than image are disabled', () => {
      const { buttons } = render();
      expect(buttons.get(0).props.disabled).toBe(true);
      expect(buttons.get(1).props.disabled).toBe(true);
      expect(buttons.get(2).props.disabled).toBe(false);
    });

    it('selected preset is disabled', () => {
      const { buttons } = render({ currentWidth: 2 });
      expect(buttons.get(0).props.disabled).toBe(true);
      expect(buttons.get(1).props.disabled).toBe(true);
      expect(buttons.get(2).props.disabled).toBe(true);
    });
  });

  describe('onSelect()', () => {
    it('clicking a button trigger onSelect()', () => {
      const { buttons, calls } = render();
      buttons.get(2).props.onClick();
      expect(calls.length).toBe(1);
      expect(calls[0][0]).toBe(2);
    });
  });
});


<|start_filename|>client/src/boot/applyTransform.js<|end_filename|>
import Injector from 'lib/Injector';
import ownerAwareUnpublish from 'transforms/FormAction/ownerAwareUnpublish';
import moveTreeDropdownField from 'transforms/TreeDropdownField/moveTreeDropdownField';

const applyTransform = () => {
  Injector.transform(
    'move-form-disabled',
    (updater) => {
      updater.component('TreeDropdownField.AssetAdmin.MoveForm', moveTreeDropdownField);
    }
  );

  Injector.transform(
    'owner-unpublishing',
    (updater) => {
      updater.component('FormAction.AssetAdmin.EditForm.action_unpublish', ownerAwareUnpublish);
    }
  );
};

export default applyTransform;


<|start_filename|>client/src/containers/Editor/EditorHeader.js<|end_filename|>
/* global confirm */
import React from 'react';
import i18n from 'i18n';
import Button from 'components/Button/Button';
import BackButton from 'components/Button/BackButton';
import PropTypes from 'prop-types';
import classnames from 'classnames';

const handle = handler => (e => {
  e.preventDefault();
  if (handler) {
    handler();
  }
});

/** Always display the back button */
const ALWAYS_BACK = 'ALWAYS_BACK';

/** Always display the cancel button */
const ALWAYS_CANCEL = 'ALWAYS_CANCEL';

/** Switch between the back and close button based on media queries */
const SWITCH = 'SWITCH';

/** Display the back button, but only under a certain width */
const ONLY_BACK = 'ONLY_BACK';

/** Display the cancel button, but only above a certain width */
const ONLY_CANCEL = 'ONLY_CANCEL';

/** Never display the back or cancel button */
const NONE = 'NONE';

/**
 * List of acceptable values for `showButton`.
 */
export const buttonStates = {
  ALWAYS_BACK,
  ALWAYS_CANCEL,
  SWITCH,
  ONLY_BACK,
  ONLY_CANCEL,
  NONE
};

/**
 * Wraps the form headers with some buttons to switch between different form.
 */
const EditorHeader = ({ onCancel, onDetails, showButton, children }) => {
    const cancelHandler = handle(onCancel);

    const showBack = [
      ALWAYS_BACK,
      SWITCH,
      ONLY_BACK
    ].indexOf(showButton) >= 0;
    const showCancel = [
      ALWAYS_CANCEL,
      SWITCH,
      ONLY_CANCEL
    ].indexOf(showButton) >= 0;

    const backClassName = classnames(
      'editor-header__back-button',
      'btn--icon-xl',
      {
        'editor-header__back-button--md-below': [SWITCH, ONLY_BACK].indexOf(showButton) >= 0
      }
    );

    const cancelClassName = classnames(
      'editor-header__cancel-button',
      'btn--icon-xl',
      {
        'editor-header__cancel-button--lg-above': [SWITCH, ONLY_CANCEL].indexOf(showButton) >= 0
      }
    );

    return (
      <div className="editor-header">
        {showBack && <BackButton className={backClassName} onClick={cancelHandler} />}
        <div className="editor-header__field">
          {children}
        </div>
        {onDetails &&
        <Button onClick={handle(onDetails)} icon="edit-list" className="editor-header__edit" outline>
          {i18n._t('AssetAdmin.DETAILS', 'Details')}
        </Button>
      }
        {showCancel &&
        <div>
          <Button icon="cancel" className={cancelClassName} noText onClick={cancelHandler} >
            {i18n._t('AssetAdmin.CANCEL')}
          </Button>
        </div>
      }
      </div>
  );
};

EditorHeader.propTypes = {
  onCancel: PropTypes.func,
  onDetails: PropTypes.func,
  showButton: PropTypes.oneOf(Object.keys(buttonStates).map(state => buttonStates[state])),
  children: PropTypes.node
};

export default EditorHeader;


<|start_filename|>client/src/containers/ThumbnailView/ThumbnailView.js<|end_filename|>
import i18n from 'i18n';
import React, { Component } from 'react';
import { inject } from 'lib/Injector';
import { galleryViewPropTypes, galleryViewDefaultProps } from 'containers/Gallery/Gallery';
import Griddle from 'griddle-react';
import PropTypes from 'prop-types';

class ThumbnailView extends Component {
  constructor(props) {
    super(props);

    this.renderItem = this.renderItem.bind(this);
    this.handleSetPage = this.handleSetPage.bind(this);
    this.handlePrevPage = this.handlePrevPage.bind(this);
    this.handleNextPage = this.handleNextPage.bind(this);
    this.handleDrag = this.handleDrag.bind(this);
  }

  handleDrag(dragging) {
    this.props.onEnableDropzone(!dragging);
  }

  /**
   * Handles setting the pagination page number
   *
   * @param {number} page
   */
  handleSetPage(page) {
    // +1 to cater for the 0-indexed page that is returned from Griddle
    this.props.onSetPage(page + 1);
  }

  /**
   * Handler for incrementing the set page
   */
  handleNextPage() {
    // (page -1) to cater for the 0-indexed page that is returned from Griddle
    const currentPage = this.props.page - 1;
    this.handleSetPage(currentPage + 1);
  }

  /**
   * Handler for decrementing the set page
   */
  handlePrevPage() {
    // (page -1) to cater for the 0-indexed page that is returned from Griddle
    const currentPage = this.props.page - 1;
    if (currentPage === 0) {
      this.handleSetPage(currentPage);
      return;
    }
    this.handleSetPage(currentPage - 1);
  }

  /**
   * Filtering by folder type
   *
   * @param {object} file
   * @returns {boolean}
   */
  folderFilter(file) {
    return file.type === 'folder';
  }

  /**
   * Filtering by non-folder types
   *
   * @param {object} file
   * @returns {boolean}
   */
  fileFilter(file) {
    return file.type !== 'folder';
  }

  /**
   * Renders the react component for pagination.
   * Current borrows the pagination from Griddle, to keep styling consistent between the two views
   *
   * @returns {XML|null}
   */
  renderPagination() {
    if (this.props.totalCount <= this.props.limit) {
      return null;
    }
    const props = {
      setPage: this.handleSetPage,
      maxPage: Math.ceil(this.props.totalCount / this.props.limit),
      next: this.handleNextPage,
      nextText: i18n._t('AssetAdmin.NEXT', 'Next'),
      previous: this.handlePrevPage,
      previousText: i18n._t('AssetAdmin.PREVIOUS', 'Previous'),
      currentPage: this.props.page - 1,
      useGriddleStyles: false,
    };
    return (
      <div className="griddle-footer">
        <Griddle.GridPagination {...props} />
      </div>
    );
  }

  /**
   * Renders the item for the this view, assigning relevant props
   *
   * @param {object} item
   * @param {number} index
   * @returns {XML}
   */
  renderItem(item) {
    const {
      File,
      Folder,
      badges,
      sectionConfig,
      selectedFiles,
      selectableItems,
      selectableFolders,
    } = this.props;
    const badge = badges.find((badgeItem) => badgeItem.id === item.id);
    let props = {
      sectionConfig,
      key: item.key,
      selectableKey: item.id,
      item,
      selectedFiles,
      onDrag: this.handleDrag,
      badge,
      canDrag: this.props.canDrag,
    };

    if (item.queuedId && !item.id) {
      const { onCancelUpload, onRemoveErroredUpload } = this.props;
      props = { ...props, onCancelUpload, onRemoveErroredUpload };
    } else {
      const { onOpenFolder, onOpenFile } = this.props;
      props = {
        ...props,
        onActivate: (item.type === 'folder') ? onOpenFolder : onOpenFile,
      };
    }

    if (selectableItems && (selectableFolders || item.type !== 'folder')) {
      const maxSelected = (
        ![null, 1].includes(this.props.maxFilesSelect) &&
        this.props.selectedFiles.length >= this.props.maxFilesSelect
      );
      const onSelect = (this.props.maxFilesSelect === 1) ? props.onActivate : this.props.onSelect;
      props = { ...props, selectable: true, onSelect, maxSelected };
    }

    if (item.type === 'folder') {
      const { onDropFiles } = this.props;
      props = { ...props, onDropFiles };
      return <Folder {...props} />;
    }
    return <File {...props} />;
  }

  render() {
    const className = 'gallery__main-view--tile';
    return (
      <div className={className}>
        <div className="gallery__folders">
          {this.props.files.filter(this.folderFilter).map(this.renderItem)}
        </div>

        <div className="gallery__files">
          {this.props.files.filter(this.fileFilter).map(this.renderItem)}
        </div>

        {this.props.files.length === 0 && !this.props.loading &&
          <p className="gallery__no-item-notice">{i18n._t('AssetAdmin.NOITEMSFOUND')}</p>
        }

        <div className="gallery__load">
          {this.renderPagination()}
        </div>
      </div>
    );
  }
}

ThumbnailView.defaultProps = galleryViewDefaultProps;

ThumbnailView.propTypes = {
  ...galleryViewPropTypes,
  File: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
  Folder: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
};

const injector = inject(
  ['GalleryItemFile', 'GalleryItemFolder'],
  (GalleryItemFile, GalleryItemFolder) => ({
    File: GalleryItemFile,
    Folder: GalleryItemFolder
  }),
  () => 'AssetAdmin.Gallery.ThumbnailView',
);

export { ThumbnailView as Component };

export default injector(ThumbnailView);


<|start_filename|>client/src/containers/MoveModal/MoveModal.js<|end_filename|>
import React from 'react';
import { connect } from 'react-redux';
import { compose } from 'redux';
import i18n from 'i18n';
import CONSTANTS from 'constants/index';
import PropTypes from 'prop-types';
import {
  deactivateModal,
  setFileBadge,
} from 'state/gallery/GalleryActions';
import { display as displayToast } from 'state/toasts/ToastsActions';
import FormBuilderModal from 'components/FormBuilderModal/FormBuilderModal';
import configShape from 'lib/configShape';
import moveFilesMutation from 'state/files/moveFilesMutation';

class MoveModal extends React.Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.timeout = null;
  }

  handleSubmit({ FolderID }) {
    const { moveFiles } = this.props.actions.files;
    const { selectedFiles, onSuccess, onClosed, setNotice, setError, setBadge } = this.props;
    return moveFiles(FolderID || 0, selectedFiles)
      .then(({ data: { moveFiles: { id, filename } } }) => {
        if (typeof onSuccess === 'function') {
          onSuccess(FolderID, selectedFiles);
        }

        setBadge(id, `${selectedFiles.length}`, 'success', CONSTANTS.MOVE_SUCCESS_DURATION);

        setNotice(
          i18n.sprintf(
            i18n._t('AssetAdmin.MOVED_ITEMS_TO', 'Moved %s item(s) to %s'),
            selectedFiles.length,
            filename
          ),
          [{
            label: i18n._t('AssetAdmin.GO_TO_FOLDER', 'Go to folder'),
            onClick: () => this.props.onOpenFolder(id)
          }]
        );

        onClosed();
      })
      .catch(() => {
        setError(i18n._t('AssetAdmin.FAILED_MOVE', 'There was an error moving the selected items.'));
      });
  }

  render() {
    const { isOpen, onClosed, title, folderId, sectionConfig } = this.props;
    const { schemaUrl } = sectionConfig.form.moveForm;
    return (
      <FormBuilderModal
        title={title}
        isOpen={isOpen}
        onClosed={onClosed}
        onSubmit={this.handleSubmit}
        identifier="AssetAdmin.MoveForm"
        schemaUrl={`${schemaUrl}/${folderId}`}
      />
    );
  }
}

MoveModal.propTypes = {
  sectionConfig: configShape,
  folderId: PropTypes.number.isRequired,
  isOpen: PropTypes.bool,
  onClosed: PropTypes.func,
  setNotice: PropTypes.func,
  setBadge: PropTypes.func,
  setError: PropTypes.func,
  title: PropTypes.string,
  onSuccess: PropTypes.func,
  onOpenFolder: PropTypes.func.isRequired,
  selectedFiles: PropTypes.array.isRequired,
  actions: PropTypes.shape({
    files: PropTypes.shape({
      moveFiles: PropTypes.func,
    }),
  }).isRequired,
};

MoveModal.defaultProps = {
  isOpen: false,
};

function mapStateToProps(state) {
  const { modal, selectedFiles } = state.assetAdmin.gallery;
  return {
    isOpen: modal === CONSTANTS.MODAL_MOVE,
    selectedFiles,
    title: i18n.sprintf(
      i18n._t('AssetAdmin.MOVE_ITEMS_TO', 'Move %s item(s) to...'),
      selectedFiles.length
    ),
  };
}

function mapDispatchToProps(dispatch) {
  return {
    onClosed() {
      dispatch(deactivateModal());
    },
    setNotice(msg, actions = []) {
      dispatch(displayToast({ text: msg, type: 'success', actions }));
    },
    setError(msg) {
      dispatch(displayToast({ text: msg, type: 'error' }));
    },
    setBadge(...params) {
      dispatch(setFileBadge(...params));
    },
  };
}

export default compose(
  connect(mapStateToProps, mapDispatchToProps),
  moveFilesMutation
)(MoveModal);


<|start_filename|>client/src/state/confirmDeletion/ConfirmDeletionActionTypes.js<|end_filename|>
export default {
  CONFIRM_DELETION_ASK: 'CONFIRM_DELETION_ASK',
  CONFIRM_DELETION_CONFIRM: 'CONFIRM_DELETION_CONFIRM',
  CONFIRM_DELETION_CANCEL: 'CONFIRM_DELETION_CANCEL',
  CONFIRM_DELETION_RESET: 'CONFIRM_DELETION_RESET',
  CONFIRM_DELETION_MODAL_CLOSE: 'CONFIRM_DELETION_MODAL_CLOSE'
};


<|start_filename|>client/src/state/files/deleteFilesMutation.js<|end_filename|>
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';
import Injector from 'lib/Injector';

const mutation = gql`mutation DeleteFiles($ids:[ID]!) {
  deleteFiles(ids: $ids)
}`;

const config = {
  props: ({ mutate, ownProps }) => {
    const { actions } = ownProps;
    const deleteFiles = (ids, parentId = null) => mutate({
      variables: {
        ids,
      },
      update: (store) => {
        const readFilesQuery = Injector.query.get('ReadFilesQuery');
        const readFilesConfig = readFilesQuery.getApolloConfig();
        const variables = readFilesConfig.options(ownProps).variables;
        if (parentId !== null) {
          variables.rootFilter.id = parentId;
          variables.rootFilter.anyChildId = null;
        }
        const query = readFilesQuery.getGraphqlAST();
        const data = store.readQuery({ query, variables });

        // Query returns a deeply nested object. Explicit reconstruction via spreads is too verbose.
        // This is an alternative, relatively efficient way to deep clone
        const newData = JSON.parse(JSON.stringify(data));

        // GraphQL backward compat hack
        if (newData.readFiles.nodes) {
          let { nodes } = newData.readFiles.nodes[0].children;
          nodes = nodes.filter(node => !ids.includes(node.id));
          newData.readFiles.nodes[0].children.nodes = nodes;
          newData.readFiles.nodes[0].children.pageInfo.totalCount = nodes.length;
        } else {
          let { nodes } = newData.readFiles[0].children;
          nodes = nodes.filter(node => !ids.includes(node.id));
          newData.readFiles[0].children.nodes = nodes;
          newData.readFiles[0].children.pageInfo.totalCount = nodes.length;
        }
        store.writeQuery({ query, data: newData, variables });
      }
    });

    return {
      actions: {
        ...actions,
        files: {
          ...actions.files,
          deleteFiles,
        },
      },
    };
  },
};

export { mutation, config };

export default graphql(mutation, config);


<|start_filename|>client/src/state/modal/tests/helpers-test.js<|end_filename|>
/* global jest, describe, it, pit, expect, beforeEach, jasmine */
import isStashableField, { findField } from '../helpers';

const stashable = {
  name: 'Stashable',
  schemaType: 'Text',
  type: 'text',
  readOnly: false,
  disabled: false
};

const stashableChild = {
  ...stashable,
  name: 'StashableChild'
};

const nonStashableChild = {
  ...stashable,
  name: 'NonStashableChild',
  readOnly: true
};

const hidden = {
  ...stashable,
  name: 'Hidden',
  type: 'hidden'
};

const readOnly = {
  ...stashable,
  name: 'Readonly',
  readOnly: true,
};

const disabled = {
  ...stashable,
  name: 'Disabled',
  disabled: true,
};

const structural = {
  name: 'StructuralField',
  schemaType: 'Structural',
  children: [stashableChild, nonStashableChild]
};


export const fields = [
  stashable,
  structural,
  hidden,
  readOnly,
  disabled
];

describe('Modal Store helpers', () => {
  describe('findField', () => {
    const cases = [
      ['Top level field', 'Stashable', stashable],
      ['Child field', 'StashableChild', stashableChild],
      ['Missing field', 'missing', false],
    ];

    cases.forEach(([caseName, fieldName, expected]) =>
      it(caseName, () => {
        const field = findField(fieldName, fields);
        expect(field).toBe(expected);
      })
    );
  });

  describe('isStashableField', () => {
    const cases = [
      ['Stashable field', 'Stashable', true],
      ['Stashable child field', 'StashableChild', true],
      ['Missing field', 'missing', false],
      ['Non Stashable child field', 'NonStashableChild', false],
      ['Structural Field', 'StructuralField', false],
      ['Hidden Field', 'Hidden', false],
      ['Readonly Field', 'ReadOnly', false],
      ['Disabled Field', 'Disabled', false],
    ];

    cases.forEach(([caseName, fieldName, expected]) =>
      it(caseName, () => {
        const isStashable = isStashableField(fieldName, fields);
        expect(isStashable).toBe(expected);
      })
    );
  });
});



<|start_filename|>client/src/state/modal/ModalReducer.js<|end_filename|>
import ACTION_TYPES from './ModalActionTypes';

/**
 * @typedef {import('types/ImageSizePreset').ImageSizePreset} ImageSizePreset
 */

/**
 * The Modal FormSchemaStack can contains a list of forms. This allows you to push new forms on
 * top of the stack and transition the Editor to a different form. When the form stack is "pop"
 * the user is returned to the previous form they were editing. In practice, this is only used
 * when inserting media to transition to the edit file form. But it could be used for other
 * purposes.
 *
 * @typedef FormSchemaStackEntry
 * @type { Object }
 * @property { string } type The current form type
 * @property { string|undefined } nextType A possible follow up form type
 */

/**
 * @typedef ModalState
 * @type { Object }
 * @property { ImageSizePreset[] } imageSizePresets
 * @property { FormSchemaStackEntry[] } formSchemaStack
 * @property { FormSchemaStackEntry| undefined } formSchema
 */

/**
 * @typedef {ModalState} ModalStateWithFormSchema
 * @property {FormSchemaStackEntry} formSchema - The parent element
 */

/**
 * Initial base state
 * @type { ModalState }
 */
export const initialState = {
  imageSizePresets: [],
  formSchemaStack: [],
  formSchema: undefined
};

/**
 * @param { ModalState } state
 * @param { string } type
 * @param { Object } payload
 * @returns { ModalState }
 */
function modalReducer(state = initialState, { type, payload }) {
  const { formSchemaStack: stack } = state;

  switch (type) {
    case ACTION_TYPES.DEFINE_IMAGE_SIZE_PRESETS:
      return { ...state, imageSizePresets: payload.imageSizePresets };
    case ACTION_TYPES.PUSH_FORM_SCHEMA:
      return { ...state, formSchemaStack: [...stack, payload.formSchema] };
    case ACTION_TYPES.INIT_FORM_SCHEMA_STACK:
      return { ...state, formSchemaStack: [payload.formSchema] };
    case ACTION_TYPES.POP_FORM_SCHEMA:
      return { ...state, formSchemaStack: stack.slice(0, -1) };
    case ACTION_TYPES.RESET:
      return initialState;
    case ACTION_TYPES.RESET_FORM_STACK:
      return { ...state, formSchemaStack: stack.slice(0, 1) };
    default:
      return state;
  }
}

/**
 * Convenience method to return the top of the stack.
 * @param { ModalState } state
 * @returns {ModalStateWithFormSchema}
 */
const popFormSchemaStackWrapper = (state) => ({
  ...state,
  formSchema: state.formSchemaStack.slice(-1).pop()
});

export default (...args) => popFormSchemaStackWrapper(modalReducer(...args));


<|start_filename|>client/src/containers/BulkDeleteConfirmation/tests/mockfiles.js<|end_filename|>
const FOLDER = 'folder';
const FILE = 'file';

export const mockfiles = [
  { id: 1, title: 'A folder', type: FOLDER },
  { id: 2, title: 'Another folder', type: FOLDER },
  { id: 3, title: 'image.jpg', type: FILE },
  { id: 4, title: 'document.pdf', type: FILE },
];


<|start_filename|>client/src/state/confirmDeletion/tests/ConfirmDeletionReducer-test.js<|end_filename|>
/* global jest, describe, it, pit, expect, beforeEach, jasmine */
import reducer, { initialState } from '../ConfirmDeletionReducer';
import ACTION_TYPES from '../ConfirmDeletionActionTypes';
import * as TRANSITIONS from '../ConfirmDeletionTransitions';

const FOLDER = 'folder';
const FILE = 'file';

const files = [
  { id: 1, title: 'A folder', type: FOLDER },
  { id: 2, title: 'Another folder', type: FOLDER },
  { id: 3, title: 'image.jpg', type: FILE },
  { id: 4, title: 'document.pdf', type: FILE },
];


describe('Confirm Deletion State Reducers', () => {
  describe(ACTION_TYPES.CONFIRM_DELETION_ASK, () => {
    it('Asking to confirm deletion', () => {
      const actualNewState = reducer(
        initialState,
        { type: ACTION_TYPES.CONFIRM_DELETION_ASK, payload: { files } }
      );
      expect(actualNewState.showConfirmation).toBe(true);
      expect(actualNewState.transition).toBe(TRANSITIONS.NO_TRANSITION);
      expect(actualNewState.files).toBe(files);
    });

    it('Asking to confirm deletion with an existing prompt', () => {
      const actualNewState = reducer(
        {
          showConfirmation: true,
          transition: TRANSITIONS.CANCELING,
          files: [{ id: 5, title: 'monkey.jpg', type: FILE }]
        },
        { type: ACTION_TYPES.CONFIRM_DELETION_ASK, payload: { files } }
      );
      expect(actualNewState.showConfirmation).toBe(true);
      expect(actualNewState.transition).toBe(TRANSITIONS.NO_TRANSITION);
      expect(actualNewState.files).toBe(files);
    });
  });

  describe(ACTION_TYPES.CONFIRM_DELETION_CONFIRM, () => {
    it('User as confirm deletion', () => {
      const actualNewState = reducer(
        { showConfirmation: true, transition: TRANSITIONS.NO_TRANSITION, files },
        { type: ACTION_TYPES.CONFIRM_DELETION_CONFIRM, payload: { } }
      );
      expect(actualNewState.showConfirmation).toBe(true);
      expect(actualNewState.transition).toBe(TRANSITIONS.DELETING);
      expect(actualNewState.files).toBe(files);
    });

    it('User confirm deletion, but the modal is already gone', () => {
      const actualNewState = reducer(
        { showConfirmation: false, transition: TRANSITIONS.NO_TRANSITION, files },
        { type: ACTION_TYPES.CONFIRM_DELETION_CONFIRM, payload: { } }
      );
      expect(actualNewState.showConfirmation).toBe(false);
      expect(actualNewState.transition).toBe(TRANSITIONS.NO_TRANSITION);
      expect(actualNewState.files).toBe(files);
    });
  });

  describe(ACTION_TYPES.CONFIRM_DELETION_CANCEL, () => {
    it('User doesn\'t want to delete anymore', () => {
      const actualNewState = reducer(
        { showConfirmation: true, transition: TRANSITIONS.NO_TRANSITION, files },
        { type: ACTION_TYPES.CONFIRM_DELETION_CANCEL, payload: { } }
      );
      expect(actualNewState.showConfirmation).toBe(true);
      expect(actualNewState.transition).toBe(TRANSITIONS.CANCELING);
      expect(actualNewState.files).toBe(files);
    });

    it('Cancel after reset', () => {
      const actualNewState = reducer(
        initialState,
        { type: ACTION_TYPES.CONFIRM_DELETION_CANCEL, payload: { } }
      );
      expect(actualNewState.showConfirmation).toBe(false);
      expect(actualNewState.transition).toBe(TRANSITIONS.NO_TRANSITION);
      expect(actualNewState.files).toEqual([]);
    });
  });

  describe(ACTION_TYPES.CONFIRM_DELETION_CANCEL, () => {
    it('User doesn\'t want to delete anymore', () => {
      const actualNewState = reducer(
        { showConfirmation: true, transition: TRANSITIONS.NO_TRANSITION, files },
        { type: ACTION_TYPES.CONFIRM_DELETION_CANCEL, payload: { } }
      );
      expect(actualNewState.showConfirmation).toBe(true);
      expect(actualNewState.transition).toBe(TRANSITIONS.CANCELING);
      expect(actualNewState.files).toBe(files);
    });

    it('Cancel after reset', () => {
      const actualNewState = reducer(
        initialState,
        { type: ACTION_TYPES.CONFIRM_DELETION_CANCEL, payload: { } }
      );
      expect(actualNewState.showConfirmation).toBe(false);
      expect(actualNewState.transition).toBe(TRANSITIONS.NO_TRANSITION);
      expect(actualNewState.files).toEqual([]);
    });
  });

  describe(ACTION_TYPES.CONFIRM_DELETION_RESET, () => {
    it('Reset bring evertything back to initial state', () => {
      const actualNewState = reducer(
        initialState,
        { type: ACTION_TYPES.CONFIRM_DELETION_RESET, payload: { } }
      );
      expect(actualNewState).toBe(initialState);
    });

    it('Reset udpates unset files as well', () => {
      const actualNewState = reducer(
        { showConfirmation: true, transition: TRANSITIONS.DELETING, files },
        { type: ACTION_TYPES.CONFIRM_DELETION_RESET, payload: { } }
      );
      expect(actualNewState).toBe(initialState);
    });
  });

  describe(ACTION_TYPES.CONFIRM_DELETION_MODAL_CLOSE, () => {
    it('Modal close after delete request is done', () => {
      const actualNewState = reducer(
        initialState,
        { type: ACTION_TYPES.CONFIRM_DELETION_MODAL_CLOSE, payload: { } }
      );
      expect(actualNewState.showConfirmation).toBe(false);
      expect(actualNewState.transition).toBe(TRANSITIONS.NO_TRANSITION);
      expect(actualNewState.files).toEqual([]);
    });

    it('Modal close before delete request is done', () => {
      const actualNewState = reducer(
        { showConfirmation: true, transition: TRANSITIONS.DELETING, files },
        { type: ACTION_TYPES.CONFIRM_DELETION_MODAL_CLOSE, payload: { } }
      );
      expect(actualNewState.showConfirmation).toBe(false);
      expect(actualNewState.transition).toBe(TRANSITIONS.NO_TRANSITION);
      expect(actualNewState.files).toBe(files);
    });
  });
});



<|start_filename|>client/src/containers/BulkDeleteConfirmation/BulkDeleteMessage.js<|end_filename|>
import i18n from 'i18n';
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { descendantFileTotalsShape } from './helpers';

/**
 * Choose which message to delete confirmation message to display.
 * @param {object} folderDescendantFileTotals
 * @param {object} fileTotalItems
 * @returns {string}
 */
const confirmationMessage = (folderCount, folderDescendantFileTotals, fileTotalItems) => {
  const fileCount = folderDescendantFileTotals.totalCount + fileTotalItems;
  if (fileCount > 0) {
    return i18n.sprintf(
      i18n._t(
        'AssetAdmin.BULK_ACTIONS_DELETE_ITEMS_CONFIRM',
        [
          "You're about to delete %s file(s) which may be used in your site's content.",
          'Carefully check the file usage on the files before deleting the folder.'
        ].join(' ')
      ),
      fileCount
    );
  } else if (folderCount === 1) {
    return i18n._t(
      'AssetAdmin.BULK_ACTIONS_DELETE_FOLDER_CONFIRM',
      'Are you sure you want to delete this folder?'
    );
  }
  return i18n._t(
    'AssetAdmin.BULK_ACTIONS_DELETE_FOLDERS_CONFIRM',
    'Are you sure you want to delete these folders?'
  );
};

/**
 * Display a context dependent confirmation message.
 */
const BulkDeleteMessage = ({ folderCount, folderDescendantFileTotals, fileTotalItems }) => (
  <Fragment>
    <p>{confirmationMessage(folderCount, folderDescendantFileTotals, fileTotalItems)}</p>
    {(folderDescendantFileTotals.totalItems > 0 || fileTotalItems > 0) &&
    <p>{i18n._t(
      'AssetAdmin.BULK_ACTIONS_DELETE_WARNING',
      'Ensure files are removed from content areas prior to deleting them, otherwise they will appear as broken links.'
    )}</p>}
  </Fragment>
);

BulkDeleteMessage.propTypes = {
  folderCount: PropTypes.number,
  folderDescendantFileTotals: descendantFileTotalsShape,
  fileTotalItems: PropTypes.number,
};

BulkDeleteMessage.defaultProps = {
  folderCount: 0,
  folderDescendantFileTotals: { totalItems: 0, totalCount: 0 },
  fileTotalItems: 0
};

export default BulkDeleteMessage;


<|start_filename|>client/src/containers/AssetAdmin/tests/AssetAdminBreadcrumb-test.js<|end_filename|>
/* global jest, describe, it, pit, expect, beforeEach, jasmine */
import Breadcrumb from '../AssetAdminBreadcrumb';
import ShallowRenderer from 'react-test-renderer/shallow';
import React from 'react';

const folder = {
  id: 3,
  title: 'Three',
  parents: [
    { id: 1, title: 'One' },
    { id: 2, title: 'Two' },
  ]
};

/**
 * Helper method to render our Asset-Admin specific breadcrumb. Because the asset-admin breadcrumb
 * wraps around the regular breadcrumb, we're simply shallow rendering the componentlooking at
 * what get passed to the underlying component.
 * @param {Object} props
 * @returns {Object}
 */
const render = (props) => {
  const renderer = new ShallowRenderer();
  const baseProps = {
    query: { filter: {} },
    getUrl: (folderId) => (folderId ? folderId.toString() : ''),
    onBrowse: jest.fn(),
    onFolderIcon: jest.fn(),
    ...props
  };
  renderer.render(<Breadcrumb {...baseProps} />);
  return { ...renderer.getRenderOutput(), baseProps };
};

describe('AssetAdmin Breadcrumb', () => {
  it('Root', () => {
    const { props: { multiline, crumbs }, baseProps: { onBrowse } } = render({});

    expect(multiline).toBe(true);
    expect(crumbs).toHaveLength(1);
    expect(crumbs[0]).toHaveProperty('text', 'Files');
    expect(crumbs[0]).toHaveProperty('href', '');
    expect(crumbs[0]).not.toHaveProperty('icon');

    crumbs[0].onClick(new Event('onClick'));
    expect(onBrowse).toHaveBeenCalledWith(0, null, { filter: {} });
  });

  it('With folders', () => {
    const {
      props: {
        multiline,
        crumbs
      },
      baseProps: {
        onBrowse,
        onFolderIcon
      }
    } = render({ folder });

    expect(multiline).toBe(true);
    expect(crumbs).toHaveLength(4);
    expect(crumbs[0]).toHaveProperty('text', 'Files');
    expect(crumbs[0]).toHaveProperty('href', '');

    expect(crumbs[1]).toHaveProperty('text', 'One');
    expect(crumbs[1]).toHaveProperty('href', '1');
    expect(crumbs[2]).toHaveProperty('text', 'Two');
    expect(crumbs[2]).toHaveProperty('href', '2');
    expect(crumbs[3]).toHaveProperty('text', 'Three');
    expect(crumbs[3]).toHaveProperty('href', '3');
    expect(crumbs[3].icons[0]).toHaveProperty('className', 'icon font-icon-edit-list');

    crumbs[3].onClick(new Event('onClick'));
    expect(onBrowse).toHaveBeenCalledWith(3, null, { filter: {} });

    crumbs[3].icons[0].onClick(new Event('onClick'));
    expect(onFolderIcon).toHaveBeenCalledWith();
  });

  it('With top folder', () => {
    const {
      props: { multiline, crumbs },
      baseProps: { onBrowse }
    } = render({ folder: { ...folder, parents: undefined } });

    expect(multiline).toBe(true);
    expect(crumbs).toHaveLength(2);
    expect(crumbs[0]).toHaveProperty('text', 'Files');
    expect(crumbs[0]).toHaveProperty('href', '');

    expect(crumbs[1]).toHaveProperty('text', 'Three');
    expect(crumbs[1]).toHaveProperty('href', '3');

    crumbs[1].onClick(new Event('onClick'));
    expect(onBrowse).toHaveBeenCalledWith(3, null, { filter: {} });
  });

  it('With search', () => {
    const query = { filter: { filters: { title: 'booya' } } };
    const { props: { multiline, crumbs }, baseProps: { onBrowse } } = render({ query });

    expect(multiline).toBe(true);
    expect(crumbs).toHaveLength(2);
    expect(crumbs[0]).toHaveProperty('text', 'Files');
    expect(crumbs[0]).toHaveProperty('href', '');

    expect(crumbs[1]).toHaveProperty('text', 'Search results');
    expect(crumbs[1]).not.toHaveProperty('href');
    expect(crumbs[1]).not.toHaveProperty('icons');
    expect(crumbs[1]).not.toHaveProperty('onClick');

    crumbs[0].onClick(new Event('onClick'));
    expect(onBrowse).toHaveBeenCalledWith(0, null, { filter: { filters: { title: 'booya' } } });
  });
});


<|start_filename|>client/src/containers/BulkDeleteConfirmation/tests/BulkDeleteMessage-test.js<|end_filename|>
/* global jest, jasmine, describe, it, expect, beforeEach, Event */

import React from 'react';
import Component from '../BulkDeleteMessage';
import ShallowRenderer from 'react-test-renderer/shallow';

const unlinkFileWarning = 'Ensure files are removed from content areas prior to deleting them, otherwise they will appear as broken links.';

const noFoldersProps = {
  folderCount: 0,
  folderDescendantFileTotals: { totalItems: 0, totalCount: 0 },
};

const oneEmptyFolder = {
  folderCount: 1,
  folderDescendantFileTotals: { totalItems: 0, totalCount: 0 },
};

const manyEmptyFolders = {
  folderCount: 2,
  folderDescendantFileTotals: { totalItems: 0, totalCount: 0 },
};

const oneFolderProps = {
  folderCount: 1,
  folderDescendantFileTotals: { totalItems: 1, totalCount: 5 },
};

const manyfolderProps = {
  folderCount: 2,
  folderDescendantFileTotals: { totalItems: 2, totalCount: 10 },
};

const noFilesProps = {
  fileTotalItems: 0,
};

const oneFileProps = {
  fileTotalItems: 1,
};

const manyFilesProps = {
  fileTotalItems: 2,
};

const getMessage = (count) => [
  `You're about to delete ${count} file(s) which may be used in your site's content.`,
  'Carefully check the file usage on the files before deleting the folder.'
].join(' ');

const emptyFolderMessage = 'Are you sure you want to delete this folder?';

const emptyFoldersMessage = 'Are you sure you want to delete these folders?';

describe('BulkDeleteMessage', () => {
  const renderer = new ShallowRenderer();

  describe('Deleting a file and a folder', () => {
    const testCases = [
      [
        'file in use',
        { ...noFoldersProps, ...oneFileProps },
        getMessage('1'),
      ],
      [
        'folder in use',
        { ...noFilesProps, ...oneFolderProps },
        getMessage('5'),
      ],
      [
        'file and folder in use',
        { ...oneFileProps, ...oneFolderProps },
        getMessage('6'),
      ],
    ];

    testCases.forEach(([desc, props, expectedMessage]) => {
      it(desc, () => {
        renderer.render(<Component {...props} />);
        const result = renderer.getRenderOutput();
        expect(result.props.children.length).toEqual(2);
        expect(result.props.children[0]).toEqual(
          <p>{expectedMessage}</p>
        );
      });
    });
  });

  describe('Deleting folders', () => {
    const testCases = [
      [
        'one folder in use',
        { ...noFilesProps, ...oneFolderProps },
        getMessage('5'),
      ],
      [
        'multiple folders in use',
        { ...noFilesProps, ...manyfolderProps },
        getMessage('10'),
      ],
    ];

    testCases.forEach(([desc, props, expectedMessage]) => {
      it(desc, () => {
        renderer.render(<Component {...props} />);
        const result = renderer.getRenderOutput();
        expect(result.props.children.length).toEqual(2);
        expect(result.props.children[0]).toEqual(
          <p>{expectedMessage}</p>
        );
      });
    });
  });

  describe('Deleting empty folders', () => {
    const testCases = [
      [
        'one empty folder',
        { ...noFilesProps, ...oneEmptyFolder },
        emptyFolderMessage,
      ],
      [
        'multiple empty folders',
        { ...noFilesProps, ...manyEmptyFolders },
        emptyFoldersMessage,
      ],
    ];

    testCases.forEach(([desc, props, expectedMessage]) => {
      it(desc, () => {
        renderer.render(<Component {...props} />);
        const result = renderer.getRenderOutput();
        expect(result.props.children.length).toEqual(2);
        expect(result.props.children[0]).toEqual(
          <p>{expectedMessage}</p>
        );
      });
    });
  });

  describe('Deleting files', () => {
    const testCases = [
      [
        'one file in use',
        { ...noFoldersProps, ...oneFileProps },
        getMessage('1'),
      ],
      [
        'many files in use',
        { ...noFoldersProps, ...manyFilesProps },
        getMessage('2'),
      ]
    ];

    testCases.forEach(([desc, props, expectedMessage]) => {
      it(desc, () => {
        renderer.render(<Component {...props} />);
        const result = renderer.getRenderOutput();
        expect(result.props.children).toEqual([
          <p>{expectedMessage}</p>,
          <p>{unlinkFileWarning}</p>
        ]);
      });
    });
  });
});


<|start_filename|>client/src/state/files/buildPublicationMutation.js<|end_filename|>
import gql from 'graphql-tag';
import { fileInterface, file } from 'lib/fileFragments';

const buildPublicationMutation = (mutationName) => {
  const operationName = mutationName.charAt(0).toUpperCase() + mutationName.slice(1);
  const mutation = gql`
  mutation ${operationName}($ids:[ID]!, $force:Boolean, $quiet:Boolean) {
    ${mutationName}(ids: $ids, force: $force, quiet: $quiet) {
      ...on File {
        __typename
        ...FileInterfaceFields
        ...FileFields
      }
      ...on PublicationNotice {
        __typename
        noticeType
        message
        ids
      }
    }
  }
  ${fileInterface}
  ${file}
`;

  const isProd = process.env.NODE_ENV === 'production';
  const config = {
    props: ({ mutate, ownProps: { actions } }) => {
      const mutationAction = (ids, force = false, quiet = isProd) => mutate({
        variables: {
          ids,
          quiet,
          force
        },
      });

      return {
        actions: {
          ...actions,
          files: {
            ...actions.files,
            [mutationName]: mutationAction,
          },
        },
      };
    },
  };

  return { mutation, config };
};

export default buildPublicationMutation;


<|start_filename|>client/lang/src/it.json<|end_filename|>
{
    "AssetAdmin.ADD_FOLDER_BUTTON": "Aggiungi cartella",
    "AssetAdmin.BACK": "Indietro",
    "AssetAdmin.BACK_DESCRIPTION": "Vai al livello superiore",
    "AssetAdmin.BULK_ACTIONS_CONFIRM": "Sei sicuro di voler %s questi file?",
    "AssetAdmin.BULK_ACTIONS_DELETE": "Elimina",
    "AssetAdmin.BULK_ACTIONS_DELETE_CONFIRM": "Sei sicuro di voler eliminare questi file?",
    "AssetAdmin.BULK_ACTIONS_DELETE_FAIL": "%s cartelle/file sono stati archiviati con successo ma non è stato possibile archiviare %s file.",
    "AssetAdmin.BULK_ACTIONS_DELETE_FOLDER": "Queste cartelle contengono file attualmente in uso, devi spostare o eliminare il suo contenuto prima di poter eliminare una cartella.",
    "AssetAdmin.BULK_ACTIONS_DELETE_MULTI_CONFIRM": "Ci sono %s file attualmente in uso, sei sicuro di voler eliminare questi file?",
    "AssetAdmin.BULK_ACTIONS_DELETE_SINGLE_CONFIRM": "Questo file è attualmente usato in %s posti, se sicuro di volerlo eliminare?",
    "AssetAdmin.BULK_ACTIONS_DELETE_SUCCESS": "%s cartelle/file sono state archiviate con successo.",
    "AssetAdmin.BULK_ACTIONS_PLACEHOLDER": "Scegliere un'azione...",
    "AssetAdmin.CANCEL": "Annulla",
    "AssetAdmin.CONFIRMDELETE": "Sei sicuro di voler eliminare questo record?",
    "AssetAdmin.CREATED": "Caricato il",
    "AssetAdmin.DELETE": "Elimina",
    "AssetAdmin.DIM": "Dimensioni",
    "AssetAdmin.DROPZONE_CANCEL_UPLOAD": "Annulla caricamento",
    "AssetAdmin.DROPZONE_CANCEL_UPLOAD_CONFIRMATION": "Sei sicuro di voler annullare questo caricamento?",
    "AssetAdmin.DROPZONE_DEFAULT_MESSAGE": "Rilascia qui i file da caricare",
    "AssetAdmin.DROPZONE_FAILED_UPLOAD": "Caricamento file fallito",
    "AssetAdmin.DROPZONE_FALLBACK_MESSAGE": "Il tuo browser non supporta il caricamento file tramite trascinamento.",
    "AssetAdmin.DROPZONE_FALLBACK_TEXT": "Usare il modulo alternativo sottostante per caricare i fie alla vecchia maniera.",
    "AssetAdmin.DROPZONE_FILE_TOO_BIG": "File troppo grande. Dimensione massima: %sMiB.",
    "AssetAdmin.DROPZONE_INVALID_FILE_TYPE": "Non puoi caricare file di questo tipo.",
    "AssetAdmin.DROPZONE_MAX_FILES_EXCEEDED": "Non puoi più caricare file.",
    "AssetAdmin.DROPZONE_REMOVE_FILE": "Elimina file",
    "AssetAdmin.DROPZONE_RESPONSE_ERROR": "Il server ha risposto con errore.",
    "AssetAdmin.DROPZONE_SUCCESS_UPLOAD": "File caricato",
    "AssetAdmin.DROPZONE_UPLOAD": "Carica",
    "AssetAdmin.EDIT": "Modifica",
    "AssetAdmin.FILENAME": "Nome file",
    "AssetAdmin.FILES": "File",
    "AssetAdmin.FILE_MISSING": "Il file non può essere trovato",
    "AssetAdmin.FILTER_DATE_ASC": "più vecchio",
    "AssetAdmin.FILTER_DATE_DESC": "più nuovo",
    "AssetAdmin.FILTER_TITLE_ASC": "titolo a-z",
    "AssetAdmin.FILTER_TITLE_DESC": "titolo z-a",
    "AssetAdmin.JOIN": ",",
    "AssetAdmin.JOINLAST": "e",
    "AssetAdmin.LASTEDIT": "Ultima modifica",
    "AssetAdmin.LOADMORE": "Carica ancora",
    "AssetAdmin.NOITEMSFOUND": "Nessun elemento trovato",
    "AssetAdmin.PROMPTFOLDERNAME": "Inserire il nome della cartella (o lasciare vuoto per annullare)",
    "AssetAdmin.REPlACE_FILE_SUCCESS": "Caricamento eseguito, il file sarà sostituito con Salva.",
    "AssetAdmin.SAVE": "Salva",
    "AssetAdmin.SEARCHCLEARRESULTS": "Annulla ricerca",
    "AssetAdmin.SEARCHRESULTS": "Risultati ricerca",
    "AssetAdmin.SEARCHRESULTSMESSAGE": "Risultati ricerca {parts}",
    "AssetAdmin.SEARCHRESULTSMESSAGECATEGORY": "catalogato come '{appCategory}'",
    "AssetAdmin.SEARCHRESULTSMESSAGEEDITEDBETWEEN": "ultima modifica tra '{lastEditedFrom}' e '{lastEditedTo}'",
    "AssetAdmin.SEARCHRESULTSMESSAGEEDITEDFROM": "ultima modifica dal '{lastEditedFrom}'",
    "AssetAdmin.SEARCHRESULTSMESSAGEEDITEDTO": "ultima modifica prima del '{lastEditedTo}'",
    "AssetAdmin.SEARCHRESULTSMESSAGEKEYWORDS": "con i termini '{name}'",
    "AssetAdmin.SEARCHRESULTSMESSAGELIMIT": "ristretta alla cartella '{folder}'",
    "AssetAdmin.SELECT": "Scegliere",
    "AssetAdmin.SIZE": "Dimensione",
    "AssetAdmin.TITLE": "Titolo",
    "AssetAdmin.TYPE": "Tipo file",
    "AssetAdmin.URL": "URL",
    "AssetAdmin.ADD_FILES": "Aggiungi da file",
    "AssetAdmin.BROWSE": "Sfoglia",
    "AssetAdmin.EMPTY": "Nessun file",
    "AssetAdmin.OR": "o",
    "AssetAdmin.ERROR_OEMBED_REMOTE": "Embed is only compatible with remote files",
    "AssetAdmin.CreateTitle": "Inserire nuovo materiale dal web",
    "AssetAdmin.EditTitle": "Materiale dal web",
    "AssetAdmin.NEXT": "Prossimo",
    "AssetAdmin.PREVIOUS": "Precedente"
}

<|start_filename|>client/src/state/modal/tests/ModalReducer-test.js<|end_filename|>
/* global jest, describe, it, pit, expect, beforeEach, jasmine */
import reducer, { initialState } from '../ModalReducer';
import {
  defineImageSizePresets,
  pushFormStackEntry,
  popFormStackEntry,
  reset,
  stashFormValues,
  resetFormStack,
  initFormStack
} from '../ModalActions';
import setIn from 'redux-form/lib/structure/plain/setIn';
import SCHEMA_ACTION_TYPES from 'state/schema/SchemaActionTypes';

const presetList = [
  { width: 123, text: 'test preset' }
];

describe('Modal State Reducers', () => {
  describe('Image Size Preset', () => {
    it('Defining new preset', () => {
      const actualNewState = reducer(
        initialState,
        defineImageSizePresets(presetList)
      );
      expect(actualNewState.imageSizePresets).toBe(presetList);
    });
  });

  describe('Initialising Form stack', () => {
    it('Initilising empty stack', () => {
      const actualNewState = reducer(
        initialState,
        initFormStack('admin')
      );

      expect(actualNewState.formSchema.type).toBe('admin');
      expect(actualNewState.formSchema.nextType).toBeFalsy();
      expect(actualNewState.formSchemaStack).toHaveLength(1);
      expect(actualNewState.formSchema).toBe(actualNewState.formSchemaStack[0]);
    });

    it('Initialising empty stack with follow up type', () => {
      const actualNewState = reducer(
        initialState,
        initFormStack('select-link', 'admin')
      );

      expect(actualNewState.formSchema.type).toBe('select-link');
      expect(actualNewState.formSchema.nextType).toBe('admin');
      expect(actualNewState.formSchemaStack).toHaveLength(1);
      expect(actualNewState.formSchema).toBe(actualNewState.formSchemaStack[0]);
    });

    it('Re-initialising stack', () => {
      let actualNewState = reducer(
        initialState,
        pushFormStackEntry('admin')
      );

      actualNewState = reducer(
        actualNewState,
        initFormStack('select-link', 'admin')
      );

      expect(actualNewState.formSchema.type).toBe('select-link');
      expect(actualNewState.formSchema.nextType).toBe('admin');
      expect(actualNewState.formSchemaStack).toHaveLength(1);
      expect(actualNewState.formSchema).toBe(actualNewState.formSchemaStack[0]);
    });
  });

  describe('Pushing Form stack', () => {
    it('Pushing a new form on the stack', () => {
      const actualNewState = reducer(
        initialState,
        pushFormStackEntry('admin')
      );

      expect(actualNewState.formSchema.type).toBe('admin');
      expect(actualNewState.formSchema.nextType).toBeFalsy();
      expect(actualNewState.formSchemaStack).toHaveLength(1);
      expect(actualNewState.formSchema).toBe(actualNewState.formSchemaStack[0]);
    });

    it('Pushing a new form with a follow up type on the stack', () => {
      const actualNewState = reducer(
        initialState,
        pushFormStackEntry('select-link', 'admin')
      );

      expect(actualNewState.formSchema.type).toBe('select-link');
      expect(actualNewState.formSchema.nextType).toBe('admin');
      expect(actualNewState.formSchemaStack).toHaveLength(1);
      expect(actualNewState.formSchema).toBe(actualNewState.formSchemaStack[0]);
    });

    it('Pushing 2 new forms on the stack', () => {
      let actualNewState = reducer(
        initialState,
        pushFormStackEntry('admin')
      );

      actualNewState = reducer(
        actualNewState,
        pushFormStackEntry('select-link', 'admin')
      );

      expect(actualNewState.formSchema.type).toBe('select-link');
      expect(actualNewState.formSchema.nextType).toBe('admin');
      expect(actualNewState.formSchemaStack).toHaveLength(2);
      expect(actualNewState.formSchema).toBe(actualNewState.formSchemaStack[1]);
    });
  });

  describe('Poping Form stack', () => {
    it('Poping empty form stack', () => {
      const actualNewState = reducer(
        initialState,
        popFormStackEntry()
      );

      expect(actualNewState.formSchema).toBeFalsy();
      expect(actualNewState.formSchemaStack).toHaveLength(0);
    });

    it('Poping form stack with 1 entry', () => {
      let actualNewState = reducer(
        initialState,
        pushFormStackEntry('admin')
      );
      actualNewState = reducer(
        actualNewState,
        popFormStackEntry()
      );

      expect(actualNewState.formSchema).toBeFalsy();
      expect(actualNewState.formSchemaStack).toHaveLength(0);
    });

    it('Poping form stack with 2 entries', () => {
      let actualNewState = reducer(
        initialState,
        pushFormStackEntry('admin')
      );
      actualNewState = reducer(
        actualNewState,
        pushFormStackEntry('select-link', 'admin')
      );
      actualNewState = reducer(
        actualNewState,
        popFormStackEntry()
      );

      expect(actualNewState.formSchema.type).toBe('admin');
      expect(actualNewState.formSchema.nextType).toBeFalsy();
      expect(actualNewState.formSchemaStack).toHaveLength(1);
      expect(actualNewState.formSchema).toBe(actualNewState.formSchemaStack[0]);
    });
  });

  describe('Resetting to initial state', () => {
    it('Reseting form stack with 2 entries', () => {
      let actualNewState = reducer(
        initialState,
        pushFormStackEntry('admin')
      );
      actualNewState = reducer(
        actualNewState,
        pushFormStackEntry('select-link', 'admin')
      );
      actualNewState = reducer(
        actualNewState,
        reset()
      );

      expect(actualNewState.formSchema).toBeFalsy();
      expect(actualNewState.imageSizePresets).toHaveLength(0);
      expect(actualNewState.formSchemaStack).toHaveLength(0);
    });

    it('Selecting a different', () => {
      let actualNewState = reducer(
        initialState,
        pushFormStackEntry('select-link', 'admin')
      );
      actualNewState = reducer(
        actualNewState,
        pushFormStackEntry('admin')
      );
      actualNewState = reducer(
        actualNewState,
        resetFormStack()
      );

      expect(actualNewState.formSchema.type).toBe('select-link');
      expect(actualNewState.formSchema.nextType).toBe('admin');
      expect(actualNewState.formSchemaStack).toHaveLength(1);
      expect(actualNewState.formSchema).toBe(actualNewState.formSchemaStack[0]);
    });
  });

  describe('Stashing form data', () => {
    const formIdentifier = 'AssetAdmin.EditorForm.fileInsertForm';
    const schemaUrl = 'admin/assets/schema/fileInsertForm';

    const stashFn = stashFormValues(formIdentifier, schemaUrl);

    it('Values are stash if they exist on the redux-form store', () => {
      const dispatch = jest.fn();
      const values = {
        foo: 'bar'
      };
      const fields = [{
        name: 'foo',
        id: 'foo',
        type: 'text',
        schemaType: 'Text'
      }];

      let state = setIn({}, `form.formState.${formIdentifier}.values`, values);
      state = setIn(state, `form.formSchemas.${schemaUrl}.schema.fields`, fields);

      stashFn(dispatch, () => state);

      expect(dispatch).toHaveBeenCalledWith({
        type: SCHEMA_ACTION_TYPES.SET_SCHEMA_STATE_OVERRIDES,
        payload: {
          id: schemaUrl,
          stateOverride: {
            fields: [{
              name: 'foo',
              value: 'bar'
            }]
          }
        },
      });
    });

    it('Nothing happens if values don\'t exist on redux-form store', () => {
      const dispatch = jest.fn();
      const state = setIn({}, 'form.formState', {});
      stashFn(dispatch, () => (state));

      expect(dispatch).not.toHaveBeenCalled();
    });
  });
});



<|start_filename|>client/src/containers/BulkDeleteConfirmation/tests/BulkDeleteConfirmation-test.js<|end_filename|>
/* global jest, jasmine, describe, it, expect, beforeEach, Event */

import React from 'react';
import { Component } from '../BulkDeleteConfirmation';
import ShallowRenderer from 'react-test-renderer/shallow';

describe('BulkDeleteMessage', () => {
  const renderer = new ShallowRenderer();

  const FOLDER = 'folder';
  const FILE = 'file';

  const files = [
    { id: 1, title: 'A folder', type: FOLDER },
    { id: 2, title: 'Another folder', type: FOLDER },
    { id: 3, title: 'image.jpg', type: FILE },
    { id: 4, title: 'document.pdf', type: FILE },
  ];

  let props;

  beforeEach(() => {
    props = {
      loading: false,
      LoadingComponent: () => <p>Loading...</p>,
      transition: false,
      descendantFileCounts: {},
      onCancel: jest.fn(),
      onModalClose: jest.fn(),
      onConfirm: jest.fn(),
    };
  });

  it('Nothing in use', () => {
    renderer.render(<Component {...props} files={files.slice(0, 2)} />);
    const { props: { isOpen, actions } } = renderer.getRenderOutput();

    expect(isOpen).toBe(true);
    expect(actions.length).toBe(2);
    expect(actions[0].label).toBe('Delete');
    expect(actions[1].label).toBe('Cancel');

    actions[0].handler();
    expect(props.onConfirm.mock.calls.length).toBe(1);
    expect(props.onCancel.mock.calls.length).toBe(0);
  });

  it('Folder in use', () => {
    renderer.render(<Component {...props} files={files} fileUsage={{ 1: 5 }} />);
    const { props: { isOpen, actions } } = renderer.getRenderOutput();

    expect(isOpen).toBe(true);
    expect(actions.length).toBe(2);
    expect(actions[0].label).toBe('Cancel');

    actions[0].handler();
    expect(props.onConfirm.mock.calls.length).toBe(0);
    expect(props.onCancel.mock.calls.length).toBe(1);
  });

  it('Files in use', () => {
    renderer.render(<Component {...props} files={files} fileUsage={{ 3: 5 }} />);
    const { props: { isOpen, actions } } = renderer.getRenderOutput();

    expect(isOpen).toBe(true);
    expect(actions.length).toBe(2);
    expect(actions[0].label).toBe('Cancel');
    expect(actions[1].label).toBe('Delete');

    actions[0].handler();
    expect(props.onConfirm.mock.calls.length).toBe(0);
    expect(props.onCancel.mock.calls.length).toBe(1);
  });
});


<|start_filename|>client/lang/src/de.json<|end_filename|>
{
    "AssetAdmin.ADD_FOLDER_BUTTON": "Verzeichnis hinzufügen",
    "AssetAdmin.BACK": "Zurück",
    "AssetAdmin.BACK_DESCRIPTION": "Eine Ebene nach oben",
    "AssetAdmin.BULK_ACTIONS_CONFIRM": "Sind Sie sicher, dass Sie diese Dateien %s möchten?",
    "AssetAdmin.BULK_ACTIONS_DELETE": "Löschen",
    "AssetAdmin.BULK_ACTIONS_DELETE_CONFIRM": "Sind Sie sicher, dass Sie diese Datei löschen möchten?",
    "AssetAdmin.BULK_ACTIONS_DELETE_FAIL": "%s Ordner/Dateien wurden erfolgreich archiviert, %s Dateien konnten jedoch nicht archiviert werden.",
    "AssetAdmin.BULK_ACTIONS_DELETE_FOLDER": "Diese Ordner enthalten Dateien, die gerade verwendet werden. Sie müssen ihren Inhalt verschieben oder löschen, bevor Sie den Ordner löschen können.",
    "AssetAdmin.BULK_ACTIONS_DELETE_MULTI_CONFIRM": "Derzeit werden %s Dateien verwendet. Möchten Sie diese Dateien wirklich löschen?",
    "AssetAdmin.BULK_ACTIONS_DELETE_SINGLE_CONFIRM": "Diese Datei wird derzeit an %s Stellen verwendet. Möchten Sie sie wirklich löschen?",
    "AssetAdmin.BULK_ACTIONS_DELETE_SUCCESS": "%s Ordner/Dateien wurden erfolgreich archiviert.",
    "AssetAdmin.BULK_ACTIONS_PLACEHOLDER": "Wähle Sie eine Aktion...",
    "AssetAdmin.CANCEL": "Abbrechen",
    "AssetAdmin.CONFIRMDELETE": "Sind Sie sicher, dass Sie diesen Datensatz löschen möchten?",
    "AssetAdmin.CREATED": "Zuerst hochgeladen",
    "AssetAdmin.DELETE": "Löschen",
    "AssetAdmin.DIM": "Abmessungen",
    "AssetAdmin.DROPZONE_CANCEL_UPLOAD": "Upload abbrechen",
    "AssetAdmin.DROPZONE_CANCEL_UPLOAD_CONFIRMATION": "Sind Sie sicher, dass Sie den Upload abbrechen möchten?",
    "AssetAdmin.DROPZONE_DEFAULT_MESSAGE": "Dateien für den Upload hier ablegen",
    "AssetAdmin.DROPZONE_FAILED_UPLOAD": "Fehler beim Dateiupload",
    "AssetAdmin.DROPZONE_FALLBACK_MESSAGE": "Ihr Browser unterstützt das Hochladen von Dateien über Drag'n'Drop nicht.",
    "AssetAdmin.DROPZONE_FALLBACK_TEXT": "Bitte benutzen Sie das unten stehende Fallback-Formular, um Ihre Dateien wie in alten Zeiten hochzuladen.",
    "AssetAdmin.DROPZONE_FILE_TOO_BIG": "Die Datei ist zu groß. Maximale Dateigröße ist %sMiB.",
    "AssetAdmin.DROPZONE_INVALID_FILE_TYPE": "Sie können keine Dateien dieses Typs hochladen.",
    "AssetAdmin.DROPZONE_MAX_FILES_EXCEEDED": "Sie können keine weiteren Dateien hochladen",
    "AssetAdmin.DROPZONE_REMOVE_FILE": "Datei entfernen",
    "AssetAdmin.DROPZONE_RESPONSE_ERROR": "Der Server hat mit einem Fehler geantwortet.",
    "AssetAdmin.DROPZONE_SUCCESS_UPLOAD": "Datei hochgeladen",
    "AssetAdmin.DROPZONE_UPLOAD": "Hochladen",
    "AssetAdmin.EDIT": "Ändern",
    "AssetAdmin.FILENAME": "Dateiname",
    "AssetAdmin.FILES": "Dateien",
    "AssetAdmin.FILE_MISSING": "Datei kann nicht gefunden werden",
    "AssetAdmin.FILTER_DATE_ASC": "älteste",
    "AssetAdmin.FILTER_DATE_DESC": "neueste",
    "AssetAdmin.FILTER_TITLE_ASC": "Titel a-z",
    "AssetAdmin.FILTER_TITLE_DESC": "Titel z-a",
    "AssetAdmin.JOIN": ",",
    "AssetAdmin.JOINLAST": "und",
    "AssetAdmin.LASTEDIT": "Letzte Änderung",
    "AssetAdmin.LOADMORE": "Lade mehr",
    "AssetAdmin.NOITEMSFOUND": "Keine Items gefunden",
    "AssetAdmin.PROMPTFOLDERNAME": "Bitte geben Sie einen Ordnernamen ein (oder leer zum Abbrechen)",
    "AssetAdmin.REPlACE_FILE_SUCCESS": "Hochladen erfolgreich, die Datei wird beim Speichern ersetzt.",
    "AssetAdmin.SAVE": "Speichern",
    "AssetAdmin.SEARCHCLEARRESULTS": "Suche löschen",
    "AssetAdmin.SEARCHRESULTS": "Suchergebnisse",
    "AssetAdmin.SEARCHRESULTSMESSAGE": "Suchergebnisse {parts}",
    "AssetAdmin.SEARCHRESULTSMESSAGECATEGORY": "kategorisiert als '{appCategory}'",
    "AssetAdmin.SEARCHRESULTSMESSAGEEDITEDBETWEEN": "zuletzt zwischen '{lastEditedFrom}' und '{lastEditedTo}' bearbeitet",
    "AssetAdmin.SEARCHRESULTSMESSAGEEDITEDFROM": "zuletzt von '{lastEditedFrom}' bearbeitet",
    "AssetAdmin.SEARCHRESULTSMESSAGEEDITEDTO": "zuletzt vor '{lastEditedTo}' bearbeitet",
    "AssetAdmin.SEARCHRESULTSMESSAGEKEYWORDS": "mit Schlüsselwörtern '{name}'",
    "AssetAdmin.SEARCHRESULTSMESSAGELIMIT": "beschränkt auf den Ordner '{folder}'",
    "AssetAdmin.SELECT": "Auswählen",
    "AssetAdmin.SIZE": "Größe",
    "AssetAdmin.TITLE": "Titel",
    "AssetAdmin.TYPE": "Dateityp",
    "AssetAdmin.URL": "URL",
    "AssetAdmin.ADD_FILES": "Aus Dateien hinzufügen",
    "AssetAdmin.BROWSE": "Durchsuchen",
    "AssetAdmin.EMPTY": "Keine Dateien",
    "AssetAdmin.OR": "oder",
    "AssetAdmin.ERROR_OEMBED_REMOTE": "Einbetten ist nur mit Remote-Dateien möglich.",
    "AssetAdmin.CreateTitle": "Fügen Sie Medien aus dem Web ein",
    "AssetAdmin.EditTitle": "Medien aus dem Web",
    "AssetAdmin.NEXT": "Nächste",
    "AssetAdmin.PREVIOUS": "Vorherige"
}

<|start_filename|>client/src/entwine/TinyMCE_ssmedia_sizepressets.js<|end_filename|>
/**
 * @typedef {import('types/ImageSizePreset').ImageSizePreset} ImageSizePreset
 */

/**
 * Set up one button
 * @note There's a bit of split logic to handle the original size button based on the width.
 * @param {Object} editor
 * @param {ImageSizePreset} preset
 * @return {string} Button name
 */
function presetButton(editor, preset) {
  const { name, width, text } = preset;

  // We add a prefix to the button name to avoid potential clash.
  const formatName = `ssmedia${name}`;

  // Register a new format on the fly
  editor.on('init', () => {
    editor.formatter.register(formatName, {
      selector: 'img',
      attributes: {
        width: width ? width.toString() : ''
      }
    });
  });

  /**
   * Retrieve the current selection if it's an image.
   * @returns {HTMLElement|undefined}
   */
  const image = () => {
    const img = editor.selection.getNode();
    return img && img.tagName === 'IMG' ? img : undefined;
  };


  /**
   * Enabled/Disabled the provided TinyMCE button
   * @param {Object} button TinyMCE button object
   */
  const disableCheck = (button) => {
    const img = image();
    // Original size button is always enable ... other buttons are only enable
    // if they are smaller than the natural width of the image
    button.disabled(img && width ? img.naturalWidth < width : false);
  };

  /**
   * Check if the current selections matches the format for this button
   * @returns {boolean}
   */
  const formatMatches = () => {
    if (editor.formatter.match(formatName)) {
      // This will work fine for fixed size button
      return true;
    }

    // Original size button will also match if the element
    // width is undefined or set the natural width of the image
    const img = image();
    if (!width && img) {
      const imgWidth = img.getAttribute('width');
      return !imgWidth || imgWidth.toString() === img.naturalWidth.toString();
    }

    return false;
  };

  /**
   * Setups the button. Fired once, the first time the button is rendered.
   * @param event
   */
  const onpostrender = (event) => {
    // Get a reference to the button
    const button = event.target;

    const onFormatChanged = () => { button.active(formatMatches()); };

    // Set the disabled status of the button ... first check needs to be triggered manually.
    editor.on('NodeChange', () => {
      disableCheck(button);
      onFormatChanged();
    });
    disableCheck(button);

    if (editor.formatter) {
      editor.formatter.formatChanged(formatName, onFormatChanged);
      if (formatMatches()) {
        editor.formatter.apply(formatName);
        const img = image();
        img.setAttribute('width', width || img.naturalWidth);
      }
    }
  };

  /**
   * Action that gets run when a user clicks on the button
   */
  const onclick = () => {
    const img = image();
    if (!img) {
      return;
    }

    // Clearn any preset width/height
    img.removeAttribute('height');
    img.removeAttribute('width');

    // Apply format
    editor.formatter.apply(formatName);

    if (width) {
      // Explicitely set the height for fixed size image
      img.setAttribute('height', img.clientHeight);
    } else {
      // For original size image set the width and height
      img.setAttribute('width', img.naturalWidth);
      img.setAttribute('height', img.naturalHeight);
    }
  };

  // Tell TinyMCE about our new button
  editor.addButton(formatName, {
    icon: false,
    text,
    onclick,
    onpostrender
  });

  return formatName;
}

/**
 * Set up all the buttons for tiny mce and return a list of their names
 * @param {Object} editor TinyMCE Editor object
 * @param {SizePreset[]} sizePresets
 * @return {string[]}
 */
export function imageSizePresetButtons(editor, sizePresets) {
  return sizePresets.map(preset => presetButton(editor, preset));
}


<|start_filename|>client/src/state/confirmDeletion/ConfirmDeletionActions.js<|end_filename|>
import ACTION_TYPES from './ConfirmDeletionActionTypes';

/**
 * Ask for permission to delete
 *
 * @param {Object[]} List of files we want to delete
 */
export function confirm(files) {
  return {
    type: ACTION_TYPES.CONFIRM_DELETION_ASK,
    payload: { files },
  };
}

/**
 * The GraphQL request for deleting files has started.
 *
 */
export function deleting() {
  return {
    type: ACTION_TYPES.CONFIRM_DELETION_CONFIRM,
    payload: { },
  };
}

/**
 * User has changed their mind and doesn't want to delete files any more.
 *
 */
export function cancel() {
  return {
    type: ACTION_TYPES.CONFIRM_DELETION_CANCEL,
    payload: { },
  };
}

/**
 * Deletion workflow was completed and we're returning to our regular state.
 *
 */
export function reset() {
  return {
    type: ACTION_TYPES.CONFIRM_DELETION_RESET,
    payload: { },
  };
}

/**
 * Modal has closed, we can stop rendering it.
 *
 */
export function modalClose() {
  return {
    type: ACTION_TYPES.CONFIRM_DELETION_MODAL_CLOSE,
    payload: { },
  };
}


<|start_filename|>client/src/containers/Editor/tests/EditorHeader-test.js<|end_filename|>
/* global jest, jasmine, describe, it, expect, beforeEach, FormData */
jest.mock('containers/FormBuilderLoader/FormBuilderLoader');
jest.mock('components/FormBuilderModal/FormBuilderModal');

import React from 'react';
import Enzyme, { shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16/build/index';
import EditorHeader, { buttonStates } from '../EditorHeader';

Enzyme.configure({ adapter: new Adapter() });

/**
 *
 * @param props
 * @returns {{baseProps: {showButton: string}, wrapper: ReactWrapper}}
 */
const render = (props) => {
  const baseProps = {
    showButton: buttonStates.NONE,
    ...props
  };
  const wrapper = shallow(<EditorHeader {...baseProps}>Title field</EditorHeader>);
  return { wrapper, baseProps };
};

const BACK_SELECTOR = '.editor-header__back-button';
const BACK_MEDIA_QUERY_SELECTOR = `${BACK_SELECTOR}--md-below`;
const CANCEL_SELECTOR = '.editor-header__cancel-button';
const CANCEL_MEDIA_QUERY_SELECTOR = `${CANCEL_SELECTOR}--lg-above`;
const EDIT_SELECTOR = '.editor-header__edit';

describe('EditorHeader', () => {
  describe('render', () => {
    it('No button at all', () => {
      const { wrapper } = render({});
      expect(wrapper.text().includes('Title field')).toBeTruthy();
      expect(wrapper.find(BACK_SELECTOR).exists()).toBeFalsy();
      expect(wrapper.find(CANCEL_SELECTOR).exists()).toBeFalsy();
      expect(wrapper.find(EDIT_SELECTOR).exists()).toBeFalsy();
    });

    it('Details button', () => {
      const onDetails = jest.fn();
      const { wrapper } = render({ onDetails });
      expect(wrapper.text().includes('Title field')).toBeTruthy();
      expect(wrapper.find(BACK_SELECTOR).exists()).toBeFalsy();
      expect(wrapper.find(CANCEL_SELECTOR).exists()).toBeFalsy();

      const btnDetails = wrapper.find(EDIT_SELECTOR);
      expect(btnDetails.exists()).toBeTruthy();

      btnDetails.simulate('click', new MouseEvent('click'));
      expect(onDetails).toBeCalled();
    });

    it('Back button always displayed', () => {
      const onCancel = jest.fn();
      const { wrapper } = render({ onCancel, showButton: buttonStates.ALWAYS_BACK });
      expect(wrapper.text().includes('Title field')).toBeTruthy();
      expect(wrapper.find(CANCEL_SELECTOR).exists()).toBeFalsy();
      expect(wrapper.find(EDIT_SELECTOR).exists()).toBeFalsy();

      const btnBack = wrapper.find(BACK_SELECTOR);
      expect(btnBack.exists()).toBeTruthy();
      expect(btnBack.is(BACK_MEDIA_QUERY_SELECTOR)).toBeFalsy();

      btnBack.simulate('click', new MouseEvent('click'));
      expect(onCancel).toBeCalled();
    });

    it('Back button sometimes displayed', () => {
      const onCancel = jest.fn();
      const { wrapper } = render({ onCancel, showButton: buttonStates.ONLY_BACK });
      expect(wrapper.text().includes('Title field')).toBeTruthy();
      expect(wrapper.find(CANCEL_SELECTOR).exists()).toBeFalsy();
      expect(wrapper.find(EDIT_SELECTOR).exists()).toBeFalsy();

      const btnBack = wrapper.find(BACK_SELECTOR);
      expect(btnBack.exists()).toBeTruthy();
      expect(btnBack.is(BACK_MEDIA_QUERY_SELECTOR)).toBeTruthy();

      btnBack.simulate('click', new MouseEvent('click'));
      expect(onCancel).toBeCalled();
    });

    it('Cancel button always displayed', () => {
      const onCancel = jest.fn();
      const { wrapper } = render({ onCancel, showButton: buttonStates.ALWAYS_CANCEL });

      expect(wrapper.text().includes('Title field')).toBeTruthy();
      expect(wrapper.find(BACK_SELECTOR).exists()).toBeFalsy();
      expect(wrapper.find(EDIT_SELECTOR).exists()).toBeFalsy();

      const btnCancel = wrapper.find(CANCEL_SELECTOR);
      expect(btnCancel.exists()).toBeTruthy();
      expect(btnCancel.is(CANCEL_MEDIA_QUERY_SELECTOR)).toBeFalsy();

      btnCancel.simulate('click', new MouseEvent('click'));
      expect(onCancel).toBeCalled();
    });


    it('Cancel button sometimes displayed', () => {
      const onCancel = jest.fn();
      const { wrapper } = render({ onCancel, showButton: buttonStates.ONLY_CANCEL });

      expect(wrapper.text().includes('Title field')).toBeTruthy();
      expect(wrapper.find(BACK_SELECTOR).exists()).toBeFalsy();
      expect(wrapper.find(EDIT_SELECTOR).exists()).toBeFalsy();

      const btnCancel = wrapper.find(CANCEL_SELECTOR);
      expect(btnCancel.exists()).toBeTruthy();
      expect(btnCancel.is(CANCEL_MEDIA_QUERY_SELECTOR)).toBeTruthy();

      btnCancel.simulate('click', new MouseEvent('click'));
      expect(onCancel).toBeCalled();
    });

    it('Switch between cancel and back', () => {
      const { wrapper } = render({ showButton: buttonStates.SWITCH });

      const btnCancel = wrapper.find(CANCEL_SELECTOR);
      expect(btnCancel.exists()).toBeTruthy();
      expect(btnCancel.is(CANCEL_MEDIA_QUERY_SELECTOR)).toBeTruthy();

      const btnBack = wrapper.find(BACK_SELECTOR);
      expect(btnBack.exists()).toBeTruthy();
      expect(btnBack.is(BACK_MEDIA_QUERY_SELECTOR)).toBeTruthy();
    });
  });
});


<|start_filename|>client/src/lib/getFormSchema.js<|end_filename|>
import CONSTANTS from 'constants/index';

const { CREATE_FOLDER, EDIT_FILE } = CONSTANTS.ACTIONS;

/**
 * This method is used to figure out what form schema to display in the Editor component. The
 * Editor is used for to display form to
 * * edit file metadata
 * * insert link to files in WYSIWYG
 * * insert image in WYSIWYG
 * * select files in the UploadField
 *
 * @param { Object } config Asset admin configuration with the schemaUrls to use
 * @param { string } viewAction Current action being performed
 * @param { number } folderId ID of the current folder
 * @param { number } fileId ID of the current file
 * @param { string } type Type of action to perform
 * @returns {{targetId: number, schemaUrl: string}}
 */
export default function getFormSchema({ config: { form }, viewAction, folderId, fileId, type }) {
  let schemaUrl = null;
  let targetId = null;

  if (viewAction === CREATE_FOLDER) {
    schemaUrl = form.folderCreateForm.schemaUrl;
    targetId = folderId;

    return { schemaUrl, targetId };
  }

  if (viewAction === EDIT_FILE && fileId) {
    switch (type) {
      case 'insert-media':
        schemaUrl = form.fileInsertForm.schemaUrl;
        break;
      case 'insert-link':
        schemaUrl = form.fileEditorLinkForm.schemaUrl;
        break;
      case 'select':
        schemaUrl = form.fileSelectForm.schemaUrl;
        break;
      case 'admin':
      default:
        schemaUrl = form.fileEditForm.schemaUrl;
        break;
    }

    targetId = fileId;
    return { schemaUrl, targetId };
  }

  return {};
}


<|start_filename|>client/src/state/confirmDeletion/ConfirmDeletionTransitions.js<|end_filename|>
/**
 * State value for ConfirmDeletion.transition when no transition is ongoing.
 */
export const NO_TRANSITION = false;

/**
 * State value for ConfirmDeletion.transition when the modal is being hidden
 * because the user has decided not to delete the files.
 */
export const CANCELING = 'canceling';

/**
 * State value for ConfirmDeletion.transition when the files are in the
 * process of being deleted and we're waiting for a response from the server.
 */
export const DELETING = 'deleting';


<|start_filename|>client/src/state/queuedFiles/QueuedFilesActions.js<|end_filename|>
import ACTION_TYPES from './QueuedFilesActionTypes';

/**
 * Adds a file which has not been persisted to the server yet.
 *
 * @param object file - File interface. See https://developer.mozilla.org/en-US/docs/Web/API/File
 */
export function addQueuedFile(file) {
  return (dispatch) =>
    dispatch({
      type: ACTION_TYPES.ADD_QUEUED_FILE,
      payload: { file },
    });
}

/**
 * Updates a queued file if it fails to upload.
 *
 * @param {String} queuedId - Temporary id assigned when this file was queued
 * @param {Object|String} response
 * @param {String} statusCodeMessage - message to use based on http status code
 */
export function failUpload(queuedId, response, statusCodeMessage) {
  return (dispatch) => {
    let message = response.message;
    if (response.errors && response.errors.length) {
      message = response.errors[0];
    }

    if (typeof response === 'string') {
      message = {
        value: statusCodeMessage || response,
        type: 'error',
      };
    }
    return dispatch({
      type: ACTION_TYPES.FAIL_UPLOAD,
      payload: {
        queuedId,
        message,
      },
    });
  };
}

/**
 * Purges the upload queue.
 *   - Failed uploads are removed.
 *   - Successful uploads are removed.
 *   - Pending uploads are ignored.
 */
export function purgeUploadQueue() {
  return (dispatch) =>
    dispatch({
      type: ACTION_TYPES.PURGE_UPLOAD_QUEUE,
      payload: null,
    });
}

/**
 * Removes a file from the queue.
 *
 * @param {String} queuedId - Temporary id assigned when this file was queued
 */
export function removeQueuedFile(queuedId) {
  return (dispatch) =>
    dispatch({
      type: ACTION_TYPES.REMOVE_QUEUED_FILE,
      payload: { queuedId },
    });
}

/**
 * Updates a queued file when it successfully uploads.
 *
 * @param {String} queuedId - Temporary id assigned when this file was queued
 */
export function succeedUpload(queuedId, json) {
  return (dispatch) =>
    dispatch({
      type: ACTION_TYPES.SUCCEED_UPLOAD,
      payload: { queuedId, json },
    });
}

/**
 * Override the values of a currently queued file.
 *
 * @param {String} queuedId - Temporary id assigned when this file was queued
 * @param object updates - The values to update.
 */
export function updateQueuedFile(queuedId, updates) {
  return (dispatch) =>
    dispatch({
      type: ACTION_TYPES.UPDATE_QUEUED_FILE,
      payload: { queuedId, updates },
    });
}


<|start_filename|>client/src/state/modal/ModalActions.js<|end_filename|>
import ACTION_TYPES from './ModalActionTypes';
import getIn from 'redux-form/lib/structure/plain/getIn';
import { setSchemaStateOverrides } from 'state/schema/SchemaActions';
import isStashableField from './helpers';

/**
 * @typedef {import('types/ImageSizePreset').ImageSizePreset} ImageSizePreset
 */

/**
 * Define the image size presets for the modal
 *
 * @param {ImageSizePreset[]} List of image size presets
 */
export function defineImageSizePresets(imageSizePresets) {
  return {
    type: ACTION_TYPES.DEFINE_IMAGE_SIZE_PRESETS,
    payload: { imageSizePresets },
  };
}

/**
 * Stash the form data as a form schema override so the user doesn't loose their changes.
 * @param {string} formIdentifier Redux-Form form name
 * @param {string} schemaUrl Full schema URL where to store the schema overrides
 * @returns {function(...[*]=)}
 */
export function stashFormValues(formIdentifier, schemaUrl) {
  // This need to be run as a thunk because we need getState to get the full state
  return (dispatch, getState) => {
    const state = getState();
    const values = getIn(state.form.formState, `${formIdentifier}.values`);
    const fieldSchema = getIn(state.form.formSchemas, `${schemaUrl}.schema.fields`);

    if (values) {
      const fields = Object
        .keys(values)
        .filter(name => values[name] !== null && isStashableField(name, fieldSchema))
        .map(name => ({ name, value: values[name] }));

      dispatch(setSchemaStateOverrides(schemaUrl, { fields }));
    }
  };
}

/**
 * Display a new form in the Editor.
 * @param {string} type Type of the next form
 * @param {string|undefined} nextType Possible type of the next next form
 */
export function pushFormStackEntry(type, nextType = undefined) {
  return ({
    type: ACTION_TYPES.PUSH_FORM_SCHEMA,
    payload: { formSchema: { type, nextType } },
  });
}

/**
 * Initialise the form schema stack.
 * @param {string} type Type of the next form
 * @param {string|undefined} nextType Possible type of the next next form
 */
export function initFormStack(type, nextType = undefined) {
  return ({
    type: ACTION_TYPES.INIT_FORM_SCHEMA_STACK,
    payload: { formSchema: { type, nextType } },
  });
}

/**
 * Define the image size presets for the modal
 *
 * @param {ImageSizePreset[]} List of image size presets
 */
export function popFormStackEntry() {
  return { type: ACTION_TYPES.POP_FORM_SCHEMA };
}

/**
 * Reset Modal state to the initial state.
 */
export function reset() {
  return { type: ACTION_TYPES.RESET };
}

/**
 * Define the image size presets for the modal.
 */
export function resetFormStack() {
  return { type: ACTION_TYPES.RESET_FORM_STACK };
}


<|start_filename|>client/src/state/files/readFilesQuery.js<|end_filename|>
import { hasFilters } from 'components/Search/Search';
import { graphqlTemplates } from 'lib/Injector';

const apolloConfig = {
  options({ sectionConfig, folderId, fileId, query: params }) {
    const filter = Object.assign({}, params.filter);
    const childrenFilter = Object.assign(
      {},
      filter,
      {
        // Unset key, taken from rootFilter
        parentId: undefined,
        // Currently all searches are recursive, and only filtered by a ParentID
        recursive: hasFilters(filter),
        // Unset this key since it's not a valid GraphQL argument
        currentFolderOnly: undefined,
      }
    );

    // only populate anyChildId param if no search is applied
    const anyChildId = (hasFilters(filter)) ? null : (fileId || null);
    const id = (anyChildId) ? null : (folderId || 0);

    const rootFilter = {
      // can be 0 (root)
      id,
      // treat 0 as null
      anyChildId,
    };

    // Covers a few variations:
    // - Display the root folder with its direct children
    // - Display the root folder with its recursive children and filters (a full "search")
    // - Display a folder with its direct children, without any filters
    // - Display a folder with its direct children and filters (a "search" in the current folder)
    const [sortField, sortDir] = params.sort ? params.sort.split(',') : ['', ''];
    const limit = params.limit || sectionConfig.limit;
    return {
      variables: {
        rootFilter,
        childrenFilter,
        limit,
        offset: ((params.page || 1) - 1) * limit,
        sortBy: (sortField && sortDir)
          ? { [sortField]: sortDir.toUpperCase() }
          : undefined,
      },
    };
  },
  props(
    {
      data: {
        error,
        refetch,
        readFiles,
        loading: networkLoading,
      },
      ownProps: { actions },
    }
  ) {
    // Uses same query as search and file list to return a single result (the containing folder)
    const folder = readFiles ? readFiles[0] : null;
    const files = (folder && folder.children)
      // Filter nodes because the DELETE resultBehaviour doesn't delete the edge, only the node
      ? folder.children.nodes.filter((file) => file)
      : [];
    const filesTotalCount = (folder && folder.children)
      ? folder.children.pageInfo.totalCount
      : 0;

    const filesLoading = (folder && !folder.children);
    const errors = error && error.graphQLErrors &&
      error.graphQLErrors.map((graphQLError) => graphQLError.message);
    return {
      loading: networkLoading || filesLoading,
      folder,
      files,
      filesTotalCount,
      graphQLErrors: errors,
      actions: Object.assign({}, actions, {
        files: Object.assign({}, actions.files, {
          readFiles: refetch,
        }),
      }),
    };
  },
};

const { READ } = graphqlTemplates;
const query = {
  apolloConfig,
  templateName: READ,
  pluralName: 'Files',
  pagination: false,
  params: {
    limit: 'Int!',
    offset: 'Int!',
    rootFilter: 'FileFilterInput',
    childrenFilter: 'FileFilterInput',
    sortBy: 'FolderChildrenSortFields',
  },
  args: {
    root: {
      filter: 'rootFilter'
    },
    'root/...on Folder/children': {
      limit: 'limit',
      offset: 'offset',
      filter: 'childrenFilter',
      sort: 'sortBy',
    },
  },
  fragments: [
    'FileInterfaceFields',
    'FileFields',
  ],
  fields: [
    '...FileInterfaceFields',
    '...FileFields',
    '...on Folder', [
      'children', [
        'pageInfo', [
          'totalCount',
        ],
        'nodes', [
          '...FileInterfaceFields',
          '...FileFields',
        ]
      ],
      'parents', [
        'id',
        'title',
      ],
    ],
  ],
};


export default query;


<|start_filename|>client/src/state/confirmDeletion/ConfirmDeletionReducer.js<|end_filename|>
import ACTION_TYPES from './ConfirmDeletionActionTypes';
import * as TRANSITIONS from './ConfirmDeletionTransitions';

/**
 * Initial base state
 * @type { Object }
 */
export const initialState = {
  showConfirmation: false,
  files: [],
  transition: TRANSITIONS.NO_TRANSITION,
};

function confirmDeletionReducer(state = initialState, action) {
  switch (action.type) {
    case ACTION_TYPES.CONFIRM_DELETION_ASK:
      return { ...initialState, showConfirmation: true, files: action.payload.files };

    case ACTION_TYPES.CONFIRM_DELETION_CANCEL:
      if (state.showConfirmation) {
        return { ...state, transition: TRANSITIONS.CANCELING };
      }
      break;

    case ACTION_TYPES.CONFIRM_DELETION_CONFIRM:
      if (state.showConfirmation) {
        return { ...state, transition: TRANSITIONS.DELETING };
      }
      break;

    case ACTION_TYPES.CONFIRM_DELETION_MODAL_CLOSE:
      return { ...state, showConfirmation: false, transition: TRANSITIONS.NO_TRANSITION };

    case ACTION_TYPES.CONFIRM_DELETION_RESET:
      return initialState;

    default:
  }

  return state;
}

export default confirmDeletionReducer;
