diff --git a/packages/builder/src/components/ComponentOptions/components/Content/Loop/cells.js b/packages/builder/src/components/ComponentOptions/components/Content/Loop/cells.js
deleted file mode 100644
index 83303f48a..000000000
--- a/packages/builder/src/components/ComponentOptions/components/Content/Loop/cells.js
+++ /dev/null
@@ -1,140 +0,0 @@
-import React, { useState } from 'react'
-import PropTypes from 'prop-types'
-import { Control } from 'react-redux-form'
-
-import './style.css'
-
-import { InputGroupButtonDropdown, DropdownToggle, DropdownMenu, DropdownItem,
- InputGroup } from 'reactstrap'
-import classnames from 'classnames'
-
-import Icon from '../../../../Icon'
-
-export const GridCell = ({ cellData, rowIndex, colIndex, colName }) =>
-
-
-export const CellTypeSelector = ({ type, setType,
- actions, disabled=false }) => {
-
- const [dropdownOpen, setDropdownOpen] = useState(false)
-
- return (
- setDropdownOpen(!dropdownOpen) }
- >
-
-
-
-
- Data type
- setType('string') }
- >
- Text (categorical)
-
- setType('number') }
- >
- Numerical (continuous)
-
- setType('boolean') }
- >
- Boolean (binary)
-
- {
- actions
- ?
-
-
- Actions
-
- {
- Object.entries(actions).map(([k, v], i) =>
-
- { k }
-
- )
- }
-
- :
- }
-
-
- )
-}
-
-export const HeaderCell = ({ columnData, index }, { gridDispatch }) =>
-
-
- gridDispatch('change', {
- model: `local.templateParameters.columns[${ index }]['type']`,
- value
- })
- }
- actions={{
- 'Fill': () => {
- gridDispatch('fillColumn', index)
- },
- 'Clear': () => {
- gridDispatch('clearColumn', index)
- },
- 'Delete': () => {
- if (window.confirm('Are you sure you want to delete this column?')) {
- gridDispatch('deleteColumn', index)
- }
- }
- }}
- />
-
-
-HeaderCell.contextTypes = {
- gridDispatch: PropTypes.func,
-}
diff --git a/packages/builder/src/components/ComponentOptions/components/Content/Loop/components/FactorialModal.js b/packages/builder/src/components/ComponentOptions/components/Content/Loop/components/FactorialModal.js
deleted file mode 100644
index ad532d9a0..000000000
--- a/packages/builder/src/components/ComponentOptions/components/Content/Loop/components/FactorialModal.js
+++ /dev/null
@@ -1,175 +0,0 @@
-import React, { Component } from 'react'
-import { ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap'
-import { Control, LocalForm } from 'react-redux-form'
-
-import Modal from '../../../../../Modal'
-import Grid from '../../../../../RRFGrid'
-import { GridCell as InputCell } from '../cells'
-
-// Copied from the library combinatorics implementation
-const product = function* (...sets) {
- let thresholds = sets
- .map(s => Math.max(s.length, 1)).reverse()
- .reduce(
- (acc, current, i) => acc.concat([
- (acc[i - 1] || 1) * current
- ]), []
- )
- .reverse()
-
- for (let counter = 0; counter < thresholds[0]; counter++) {
- yield sets.map(
- (s, i) => s[Math.floor(counter / (thresholds[i + 1] || 1)) % s.length]
- )
- }
-}
-
-const headers = ['Factor', 'Levels']
-const HeaderCell = ({ index }) =>
-
- { headers[index] }
-
-
-const TextAreaCell = ({ cellData, rowIndex, colIndex, colName }) => {
- const lines = (cellData || '').split('\n').length
-
- return (
-
- )
-}
-
-const GridCell = (props) => {
- if (props.colIndex === 0) {
- return
- } else {
- return
- }
-}
-
-class FactorialModal extends Component {
- constructor(props) {
- super(props)
-
- this.state = {
- open: false,
- formData: {
- factors: {
- rows: [
- ['Factor A', 'Level A1\nLevel A2'],
- ['Factor B', 'Level B1\nLevel B2']
- ],
- },
- },
- }
- this.promiseHandlers = {}
- }
-
- // TODO: Think about collapsing the logic from this modal,
- // and the FileHandler, into one.
- toggle() {
- this.setState((prevState) => {
- // Reject promise
- if (prevState.active && this.promiseHandlers.reject) {
- this.promiseHandlers.reject('FileSelector hidden')
- }
-
- // Switch state
- return { open: !prevState.open }
- })
- }
-
- async show() {
- this.toggle()
- return new Promise(
- (resolve, reject) => {
- this.promiseHandlers = {
- resolve: result => {
- this.setState({ open: false })
- return resolve(result)
- },
- reject
- }
- }
- )
- }
-
- generate() {
- const factors = this.state.formData.factors.rows
- .map(r => r[0])
- const levels = this.state.formData.factors.rows
- .map(r => r[1].split('\n').filter(c => c !== ''))
-
- const output = {
- columns: factors.map(f => ({ name: f, type: 'string' })),
- rows: Array.from(product(...levels)),
- }
-
- if (this.promiseHandlers.resolve) {
- this.promiseHandlers.resolve(output)
- }
-
- return output
- }
-
- render() {
- return (
-
- this.setState({ formData }) }
- getDispatch={ dispatch => this.formDispatch = dispatch }
- >
-
-
this.toggle() }
- >
- Generate factorial design
-
-
- Please specify the factors in your design, as well as their levels. The resulting design is fully crossed, meaning that in the loop, every level of a factor is combined with every level of every other factor.
-
-
-
this.formDispatch(action) }
- className="border-top-0"
- />
-
- this.toggle() }>
- Close
-
- this.generate() }>
- Generate
-
-
-
-
-
- )
- }
-}
-
-export default FactorialModal
diff --git a/packages/builder/src/components/ComponentOptions/components/Content/Loop/components/ShuffleGroups/Group.js b/packages/builder/src/components/ComponentOptions/components/Content/Loop/components/ShuffleGroups/Group.js
deleted file mode 100644
index e087b2e93..000000000
--- a/packages/builder/src/components/ComponentOptions/components/Content/Loop/components/ShuffleGroups/Group.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import React from 'react'
-import { DropTarget } from 'react-dnd'
-
-import classnames from 'classnames'
-
-import Item from './Item'
-
-const groupTarget = {
- drop: (targetProps, monitor) => {
- const { column } = monitor.getItem()
- targetProps.move(
- column, // id
- targetProps.groupId // target
- )
- },
-}
-
-function collect(connect, monitor) {
- return {
- connectDropTarget: connect.dropTarget(),
- isOver: monitor.isOver()
- }
-}
-
-const Group = ({ columns=[], title, connectDropTarget, isOver, children }) =>
- connectDropTarget(
-
-
- { title }
-
-
- {
- children || columns.map((c, i) =>
-
- )
- }
-
-
-
-
- )
-
-export default DropTarget('item', groupTarget, collect)(Group)
diff --git a/packages/builder/src/components/ComponentOptions/components/Content/Loop/components/ShuffleGroups/Item.js b/packages/builder/src/components/ComponentOptions/components/Content/Loop/components/ShuffleGroups/Item.js
deleted file mode 100644
index b01b2bd47..000000000
--- a/packages/builder/src/components/ComponentOptions/components/Content/Loop/components/ShuffleGroups/Item.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import React from 'react'
-import { DragSource } from 'react-dnd'
-
-const itemSource = {
- beginDrag: ({ column }) => {
- return { column }
- },
-}
-
-function collect(connect, monitor) {
- return {
- connectDragSource: connect.dragSource(),
- isDragging: monitor.isDragging(),
- }
-}
-
-const Item = ({ name, connectDragSource, isDragging }) =>
- connectDragSource(
-
-
- { name }
-
-
- )
-
-export default DragSource('item', itemSource, collect)(Item)
diff --git a/packages/builder/src/components/ComponentOptions/components/Content/Loop/components/ShuffleGroups/index.js b/packages/builder/src/components/ComponentOptions/components/Content/Loop/components/ShuffleGroups/index.js
deleted file mode 100644
index 2865c700e..000000000
--- a/packages/builder/src/components/ComponentOptions/components/Content/Loop/components/ShuffleGroups/index.js
+++ /dev/null
@@ -1,74 +0,0 @@
-import React from 'react'
-
-import { Table } from 'reactstrap'
-
-import Group from './Group'
-import Icon from '../../../../../../Icon'
-
-export default ({ groups, moveHandler, globalShuffle }) => {
- const nextGroup = Math.max(
- ...Object.keys(groups)
- .map(Number)
- .filter(x => !isNaN(x)),
- 0
- ) + 1
-
- const entries = Object.entries(groups)
-
- return (
-
-
- {/* Default group */}
- 0
- ? undefined
- :
- (no columns yet)
-
- }
- />
- {/* Actual shuffled groups */}
- {
- entries
- .filter(([group]) => group !== 'undefined')
- .map(([group, columns]) =>
-
- )
- }
- {/* Empty group */}
-
-
-
- Drag columns into rows
-
-
-
- to create independently shuffled groups
-
-
-
-
-
- )
-}
diff --git a/packages/builder/src/components/ComponentOptions/components/Content/Loop/footer.js b/packages/builder/src/components/ComponentOptions/components/Content/Loop/footer.js
deleted file mode 100644
index 73b0e4b0b..000000000
--- a/packages/builder/src/components/ComponentOptions/components/Content/Loop/footer.js
+++ /dev/null
@@ -1,169 +0,0 @@
-import React, { useState, createRef } from 'react'
-import PropTypes from 'prop-types'
-
-import { ButtonDropdown, Button,
- DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap'
-import { range } from 'lodash'
-
-import Icon from '../../../../Icon'
-import FactorialModal from './components/FactorialModal'
-
-import Uploader from '../../../../Uploader'
-import { saveAs } from 'file-saver'
-import { parse, unparse } from 'papaparse'
-
-const exportGrid = (data, columns) => {
- const output = unparse({
- fields: columns.map(c => c.name),
- data
- })
-
- return saveAs(
- new Blob(
- [ output ],
- { type: 'text/csv;charset=utf-8' }
- ),
- 'loop.csv'
- )
-}
-
-export const Footer = ({ columns, data }, { gridDispatch }) => {
- const [dropdownOpen, setDropdownOpen] = useState(false)
- const factorialModal = createRef()
-
- return (
-
-
-
-
-
- setDropdownOpen(!dropdownOpen) }
- >
- gridDispatch('addRow') }
- onMouseUp={
- e => e.target.blur()
- }
- style={{
- paddingLeft: '32px', // 6px standard + 24px toggle width
- }}
- >
-
-
-
-
- Generate
- factorialModal.current.show().then(result => {
- gridDispatch('overwrite', result)
- }) }
- >
- Factorial design
-
-
- Repeat
- {
- const n = window.prompt('How many times?')
- if (n) {
- gridDispatch('overwrite', {
- columns,
- rows: data.flatMap(r => range(n).map(() => r)),
- })
- }
- } }
- >
- Each row
-
- {
- const n = window.prompt('How many times?')
- if (n) {
- gridDispatch('overwrite', {
- columns,
- rows: range(n).flatMap(() => data),
- })
- }
- } }
- >
- Entire grid
-
-
- CSV / TSV
- {
- // TODO: Close the dropdown when the file selector
- // is shown. This needs some more work to make sure
- // that the uploader component isn't removed from the
- // page in between
- setDropdownOpen(false)
-
- const parseResult = parse(
- content.trim(),
- { header: true }
- )
-
- if (
- // No detected issues
- parseResult.errors.length === 0 ||
- // Data is present, but no delimiter found
- (parseResult.data && parseResult.errors.length === 1 &&
- parseResult.errors[0].code === 'UndetectableDelimiter')
- ) {
- gridDispatch('overwrite', {
- columns: Object.keys(parseResult.data[0])
- .map(c => ({ name: c, type: 'string' })),
- rows: parseResult.data.map(r => Object.values(r))
- })
- } else {
- console.log(
- 'CSV import found errors: ',
- parseResult.errors
- )
- alert(
- 'Sorry, I couldn\'t parse that CSV file. ' +
- 'There seems to be an error. ' +
- 'Could you check it, please?'
- )
- }
- }
- }
- >
-
- Import
-
-
- exportGrid(data, columns) }
- >
- Export
-
-
-
-
-
-
-
- )
-}
-
-Footer.contextTypes = {
- gridDispatch: PropTypes.func,
-}
diff --git a/packages/builder/src/components/ComponentOptions/components/Content/Loop/index.js b/packages/builder/src/components/ComponentOptions/components/Content/Loop/index.js
index 25031e154..42a350f99 100644
--- a/packages/builder/src/components/ComponentOptions/components/Content/Loop/index.js
+++ b/packages/builder/src/components/ComponentOptions/components/Content/Loop/index.js
@@ -1,190 +1,13 @@
-import React, { Component } from 'react'
-import { Control, actions } from 'react-redux-form'
-import { Col, CardBody,
- FormGroup, Input, CustomInput, InputGroup, Label } from 'reactstrap'
+import React from 'react'
-import { uniqBy, groupBy } from 'lodash'
-
-import Hint from '../../../../Hint'
-import Form from '../../RRFForm'
import Card from '../../../../Card'
-import Grid from '../../../../RRFGrid'
-import { GridCell, HeaderCell } from './cells'
-import { Footer } from './footer'
-import ShuffleGroups from './components/ShuffleGroups'
-
-// TODO: Move to a general-purpose utility module
-import { integerOrPlaceholder } from '../../Behavior/components/Timeline/util'
-
-const switchLabels = ({
- templateParameters={ rows: [] },
- sample={ n: undefined }
-}, labels) => {
- const samples = parseInt(sample.n)
- const parameters = templateParameters.rows.length
-
- if (samples < parameters) {
- return labels[0]
- } else if (isNaN(samples) || samples === parameters) {
- return labels[1]
- } else {
- return labels[2]
- }
-}
-
-const SampleWidget = ({ data }) =>
-
-
- Sample
-
-
- Control how many, and which of the above parameter sets are used.
-
-
- The sample size defines the number of loop iterations. The mode determines how the iterations are drawn from the entries above.
-
-
- Sequential
- Proceed in order, starting from the top. If more samples are requested than entries are available, start over.
-
- Random order
- Present entries in shuffled order (only available without sampling).
-
- Sample without replacement
- Draw entries at random, starting over when all are exhausted. Shuffles the result for a completely random order.
-
- Sample w/o replacement (in blocks)
- As above, but without shuffling. If the number of samples exceeds the number of entries, this results in blocks of all entries in random order.
-
- Sample with replacement
- Draw entries without regard to which have been selected before.
-
-
-
-
-
-
-
-
- In sequence
-
-
- {
- switchLabels(data, [
- 'Sampled without replacement',
- 'In random order',
- 'Sampled without replacement (then shuffled)'
- ])
- }
-
-
- Sampled without replacement (in blocks)
-
-
- Sampled with replacement
-
-
-
-
-
-
-export default class extends Component {
- constructor(props) {
- super(props)
- this.formDispatch = () => console.log('invalid dispatch')
- }
-
- render() {
- const { id, data } = this.props
- const { columns=[], rows=[] } = data.templateParameters || {}
- return (
- <>
-
-
-
- c.shuffleGroup).length > 1 }
- collapsable={ true }
- wrapContent={ false }
- className="mt-4"
- >
-
- Parameter groups
-
- c.name !== '')
- .map((c, i) => ({ ...c, id: i })),
- 'shuffleGroup'
- )
- }
- moveHandler={
- (key, target) =>
- this.formDispatch(
- actions.change(
- `local.templateParameters.columns[${ key }]['shuffleGroup']`,
- target
- )
- )
- }
- globalShuffle={ data.sample.mode !== 'sequential' }
- />
-
- >
- )
- }
-}
+export default () =>
+ <>
+
+
+
+
+ >
diff --git a/packages/builder/src/components/ComponentOptions/components/Content/Loop/style.css b/packages/builder/src/components/ComponentOptions/components/Content/Loop/style.css
deleted file mode 100644
index e8057e24a..000000000
--- a/packages/builder/src/components/ComponentOptions/components/Content/Loop/style.css
+++ /dev/null
@@ -1,23 +0,0 @@
-/* Reduce padding in a grid dropdown-toggle button */
-table.grid th .input-group button.dropdown-toggle {
- padding: 0.5rem 0.8rem;
-}
-
-/* Reduce size of input group dropdown toggle,
- and hide icon, if columns are very narrow. */
-table.grid.grid-slim th .input-group button.dropdown-toggle {
- padding: 0.5rem;
-}
-
-table.grid.grid-slim th .input-group button.dropdown-toggle i {
- display: none;
-}
-
-table.grid.grid-slim th .input-group button.dropdown-toggle::after {
- margin-left: 0;
-}
-
-/* Highlight active dropdown item */
-.dropdown-menu .dropdown-item.dropdown-item-active {
- background-color: var(--light);
-}