diff --git a/packages/builder/src/components/ComponentOptions/components/Content/Loop/Grid/footer.js b/packages/builder/src/components/ComponentOptions/components/Content/Loop/Grid/footer.js
new file mode 100644
index 000000000..3bbf259e7
--- /dev/null
+++ b/packages/builder/src/components/ComponentOptions/components/Content/Loop/Grid/footer.js
@@ -0,0 +1,170 @@
+import React, { useState } from 'react'
+
+import { ButtonDropdown, Button,
+ DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap'
+import { range } from 'lodash'
+
+import Icon from '../../../../../Icon'
+//import FactorialModal from './components/FactorialModal'
+import { useArrayContext } from '../../../../../Form/array'
+
+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 default ({ columns, data }) => {
+ const [dropdownOpen, setDropdownOpen] = useState(false)
+ //const factorialModal = createRef()
+ const { addRow, overwriteAll } = useArrayContext()
+
+ return (
+
+
+ |
+
+ { /*
+
+ */ }
+ setDropdownOpen(!dropdownOpen) }
+ >
+
+
+
+ { /*
+ Generate
+ factorialModal.current.show().then(result => {
+ overwriteAll(result.rows, result.columns)
+ }) }
+ >
+ Factorial design
+
+ */ }
+
+ Repeat
+ {
+ const n = window.prompt('How many times?')
+ if (n) {
+ overwriteAll(
+ data.flatMap(r => range(n).map(() => r)),
+ columns,
+ )
+ }
+ } }
+ >
+ Each row
+
+ {
+ const n = window.prompt('How many times?')
+ if (n) {
+ overwriteAll(
+ range(n).flatMap(() => data),
+ columns,
+ )
+ }
+ } }
+ >
+ 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')
+ ) {
+ overwriteAll(
+ parseResult.data.map(r => Object.values(r)),
+ Object.keys(parseResult.data[0])
+ .map(c => ({ name: c, type: 'string' })),
+ )
+ } 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
+
+
+
+ |
+ |
+
+
+ )
+}
diff --git a/packages/builder/src/components/ComponentOptions/components/Content/Loop/Grid/index.js b/packages/builder/src/components/ComponentOptions/components/Content/Loop/Grid/index.js
index 2ec1c2bcf..5ab79cf25 100644
--- a/packages/builder/src/components/ComponentOptions/components/Content/Loop/Grid/index.js
+++ b/packages/builder/src/components/ComponentOptions/components/Content/Loop/Grid/index.js
@@ -4,6 +4,7 @@ import { FastField, useFormikContext } from 'formik'
import classnames from 'classnames'
import Header from './header'
+import Footer from './footer'
import { Table, DefaultRow } from '../../../../../Form/table'
import './style.css'
@@ -34,6 +35,7 @@ export default () => {
row={ Row }
columns={ columns }
header={ Header }
+ footer={ Footer }
defaultItem={ Array(columns).fill('') }
className={ classnames({
'table': true,