mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-05-03 08:29:54 +00:00
Resolve "Creating multiple rows at once"
This commit is contained in:
parent
f1a469946f
commit
f91b73ab95
13 changed files with 446 additions and 155 deletions
changelog/entries/unreleased/feature
web-frontend
modules
core
database
components/view/grid
locales
services
store/view
test
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "feature",
|
||||
"message": "When right-clicking on the row add button in the grid view, you can now add multiple rows at a time.",
|
||||
"issue_number": 1249,
|
||||
"bullet_points": [],
|
||||
"created_at": "2023-02-14"
|
||||
}
|
|
@ -152,6 +152,50 @@ export default {
|
|||
|
||||
this.$emit('shown')
|
||||
},
|
||||
/**
|
||||
* Toggles context menu next to mouse when click event has happened
|
||||
*/
|
||||
toggleNextToMouse(
|
||||
clickEvent,
|
||||
vertical = 'bottom',
|
||||
horizontal = 'right',
|
||||
verticalOffset = 10,
|
||||
horizontalOffset = 0,
|
||||
value = true
|
||||
) {
|
||||
this.toggle(
|
||||
{
|
||||
top: clickEvent.pageY,
|
||||
left: clickEvent.pageX,
|
||||
},
|
||||
vertical,
|
||||
horizontal,
|
||||
verticalOffset,
|
||||
horizontalOffset,
|
||||
value
|
||||
)
|
||||
},
|
||||
/**
|
||||
* Shows context menu next to mouse when click event has happened
|
||||
*/
|
||||
showNextToMouse(
|
||||
clickEvent,
|
||||
vertical = 'bottom',
|
||||
horizontal = 'right',
|
||||
verticalOffset = 10,
|
||||
horizontalOffset = 0
|
||||
) {
|
||||
this.show(
|
||||
{
|
||||
top: clickEvent.pageY,
|
||||
left: clickEvent.pageX,
|
||||
},
|
||||
vertical,
|
||||
horizontal,
|
||||
verticalOffset,
|
||||
horizontalOffset
|
||||
)
|
||||
},
|
||||
/**
|
||||
* Forces the child elements to render by setting `openedOnce` to `true`. This
|
||||
* could be useful when children of the context must be accessed before the context
|
||||
|
@ -204,30 +248,123 @@ export default {
|
|||
}
|
||||
|
||||
const targetRect = target.getBoundingClientRect()
|
||||
const contextRect = this.$el.getBoundingClientRect()
|
||||
const positions = { top: null, right: null, bottom: null, left: null }
|
||||
|
||||
if (!visible) {
|
||||
target.classList.remove('forced-block')
|
||||
}
|
||||
const positions = { top: null, right: null, bottom: null, left: null }
|
||||
|
||||
// Take into account that the document might be scrollable.
|
||||
verticalOffset += document.documentElement.scrollTop
|
||||
horizontalOffset += document.documentElement.scrollLeft
|
||||
|
||||
// Calculate if top, bottom, left and right positions are possible.
|
||||
const { vertical: verticalAdjusted, horizontal: horizontalAdjusted } =
|
||||
this.checkForEdges(
|
||||
targetRect,
|
||||
vertical,
|
||||
horizontal,
|
||||
verticalOffset,
|
||||
horizontalOffset
|
||||
)
|
||||
|
||||
// Calculate the correct positions for horizontal and vertical values.
|
||||
if (horizontalAdjusted === 'left') {
|
||||
positions.left = targetRect.left + horizontalOffset
|
||||
}
|
||||
|
||||
if (horizontalAdjusted === 'right') {
|
||||
positions.right =
|
||||
window.innerWidth - targetRect.right - horizontalOffset
|
||||
}
|
||||
|
||||
if (verticalAdjusted === 'bottom') {
|
||||
positions.top = targetRect.bottom + verticalOffset
|
||||
}
|
||||
|
||||
if (verticalAdjusted === 'top') {
|
||||
positions.bottom = window.innerHeight - targetRect.top + verticalOffset
|
||||
}
|
||||
|
||||
if (!visible) {
|
||||
target.classList.remove('forced-block')
|
||||
}
|
||||
|
||||
return positions
|
||||
},
|
||||
/**
|
||||
* Calculates the desired position based on the provided coordinates. For now this
|
||||
* is only used by the row context menu, but because of the reserved space of the
|
||||
* grid on the right and bottom there is always room for the context. Therefore we
|
||||
* do not need to check if the context fits.
|
||||
*/
|
||||
calculatePositionFixed(
|
||||
coordinates,
|
||||
vertical,
|
||||
horizontal,
|
||||
verticalOffset,
|
||||
horizontalOffset
|
||||
) {
|
||||
const targetTop = coordinates.top
|
||||
const targetLeft = coordinates.left
|
||||
const targetBottom = window.innerHeight - targetTop
|
||||
const targetRight = window.innerWidth - targetLeft
|
||||
|
||||
const contextRect = this.$el.getBoundingClientRect()
|
||||
const positions = { top: null, right: null, bottom: null, left: null }
|
||||
|
||||
// Take into account that the document might be scrollable.
|
||||
verticalOffset += document.documentElement.scrollTop
|
||||
horizontalOffset += document.documentElement.scrollLeft
|
||||
|
||||
const { vertical: verticalAdjusted, horizontal: horizontalAdjusted } =
|
||||
this.checkForEdges(
|
||||
{
|
||||
top: targetTop,
|
||||
left: targetLeft,
|
||||
bottom: targetBottom,
|
||||
right: targetRight,
|
||||
},
|
||||
vertical,
|
||||
horizontal,
|
||||
verticalOffset,
|
||||
horizontalOffset
|
||||
)
|
||||
|
||||
// Calculate the correct positions for horizontal and vertical values.
|
||||
if (horizontalAdjusted === 'left') {
|
||||
positions.left = targetLeft - contextRect.width + horizontalOffset
|
||||
}
|
||||
|
||||
if (horizontalAdjusted === 'right') {
|
||||
positions.right = targetRight - contextRect.width - horizontalOffset
|
||||
}
|
||||
|
||||
if (verticalAdjusted === 'bottom') {
|
||||
positions.top = targetTop + verticalOffset
|
||||
}
|
||||
|
||||
if (verticalAdjusted === 'top') {
|
||||
positions.bottom = targetBottom + verticalOffset
|
||||
}
|
||||
|
||||
return positions
|
||||
},
|
||||
/**
|
||||
* Checks if we need to adjust the horizontal/vertical value of where the context
|
||||
* menu will be placed. This might happen if the screen size would cause the context
|
||||
* to clip out of the screen if positioned in a certain position.
|
||||
*
|
||||
* @returns {{horizontal: string, vertical: string}}
|
||||
*/
|
||||
checkForEdges(
|
||||
targetRect,
|
||||
vertical,
|
||||
horizontal,
|
||||
verticalOffset,
|
||||
horizontalOffset
|
||||
) {
|
||||
const contextRect = this.$el.getBoundingClientRect()
|
||||
const canTop = targetRect.top - contextRect.height - verticalOffset > 0
|
||||
const canBottom =
|
||||
window.innerHeight -
|
||||
targetRect.bottom -
|
||||
contextRect.height -
|
||||
verticalOffset >
|
||||
0
|
||||
const canOverTop =
|
||||
targetRect.bottom - contextRect.height - verticalOffset > 0
|
||||
const canOverBottom =
|
||||
window.innerHeight -
|
||||
targetRect.bottom -
|
||||
targetRect.top -
|
||||
contextRect.height -
|
||||
verticalOffset >
|
||||
0
|
||||
|
@ -235,7 +372,7 @@ export default {
|
|||
targetRect.right - contextRect.width - horizontalOffset > 0
|
||||
const canLeft =
|
||||
window.innerWidth -
|
||||
targetRect.left -
|
||||
targetRect.right -
|
||||
contextRect.width -
|
||||
horizontalOffset >
|
||||
0
|
||||
|
@ -250,14 +387,6 @@ export default {
|
|||
vertical = 'bottom'
|
||||
}
|
||||
|
||||
if (vertical === 'over-bottom' && !canOverBottom && canOverTop) {
|
||||
vertical = 'over-top'
|
||||
}
|
||||
|
||||
if (vertical === 'over-top' && !canOverTop) {
|
||||
vertical = 'over-bottom'
|
||||
}
|
||||
|
||||
if (horizontal === 'left' && !canLeft && canRight) {
|
||||
horizontal = 'right'
|
||||
}
|
||||
|
@ -266,48 +395,7 @@ export default {
|
|||
horizontal = 'left'
|
||||
}
|
||||
|
||||
// Calculate the correct positions for horizontal and vertical values.
|
||||
if (horizontal === 'left') {
|
||||
positions.left = targetRect.left + horizontalOffset
|
||||
}
|
||||
|
||||
if (horizontal === 'right') {
|
||||
positions.right =
|
||||
window.innerWidth - targetRect.right - horizontalOffset
|
||||
}
|
||||
|
||||
if (vertical === 'bottom') {
|
||||
positions.top = targetRect.bottom + verticalOffset
|
||||
}
|
||||
|
||||
if (vertical === 'top') {
|
||||
positions.bottom = window.innerHeight - targetRect.top + verticalOffset
|
||||
}
|
||||
|
||||
if (vertical === 'over-bottom') {
|
||||
positions.top = targetRect.top + verticalOffset
|
||||
}
|
||||
|
||||
if (vertical === 'over-top') {
|
||||
positions.bottom =
|
||||
window.innerHeight - targetRect.bottom + verticalOffset
|
||||
}
|
||||
|
||||
return positions
|
||||
},
|
||||
/**
|
||||
* Calculates the desired position based on the provided coordinates. For now this
|
||||
* is only used by the row context menu, but because of the reserved space of the
|
||||
* grid on the right and bottom there is always room for the context. Therefore we
|
||||
* do not need to check if the context fits.
|
||||
*/
|
||||
calculatePositionFixed(coordinates) {
|
||||
return {
|
||||
left: coordinates.left,
|
||||
top: coordinates.top,
|
||||
right: null,
|
||||
bottom: null,
|
||||
}
|
||||
return { vertical, horizontal }
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -15,9 +15,15 @@ export default {
|
|||
toggle(...args) {
|
||||
this.getRootContext().toggle(...args)
|
||||
},
|
||||
toggleNextToMouse(...args) {
|
||||
this.getRootContext().toggleNextToMouse(...args)
|
||||
},
|
||||
show(...args) {
|
||||
this.getRootContext().show(...args)
|
||||
},
|
||||
showNextToMouse(...args) {
|
||||
this.getRootContext().showNextToMouse(...args)
|
||||
},
|
||||
hide(...args) {
|
||||
this.getRootContext().hide(...args)
|
||||
},
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
@cell-mouseover="multiSelectHold"
|
||||
@cell-mouseup-left="multiSelectStop"
|
||||
@add-row="addRow()"
|
||||
@add-rows="$refs.rowsAddContext.toggleNextToMouse($event)"
|
||||
@add-row-after="addRowAfter($event)"
|
||||
@update="updateValue"
|
||||
@paste="multiplePasteFromCell"
|
||||
|
@ -54,6 +55,7 @@
|
|||
</div>
|
||||
</template>
|
||||
</GridViewSection>
|
||||
<GridViewRowsAddContext ref="rowsAddContext" @add-rows="addRows" />
|
||||
<div
|
||||
ref="divider"
|
||||
class="grid-view__divider"
|
||||
|
@ -94,6 +96,7 @@
|
|||
@row-hover="setRowHover($event.row, $event.value)"
|
||||
@row-context="showRowContext($event.event, $event.row)"
|
||||
@add-row="addRow()"
|
||||
@add-rows="$refs.rowsAddContext.toggleNextToMouse($event)"
|
||||
@add-row-after="addRowAfter($event)"
|
||||
@update="updateValue"
|
||||
@paste="multiplePasteFromCell"
|
||||
|
@ -269,10 +272,12 @@ import viewDecoration from '@baserow/modules/database/mixins/viewDecoration'
|
|||
import { populateRow } from '@baserow/modules/database/store/view/grid'
|
||||
import { clone } from '@baserow/modules/core/utils/object'
|
||||
import copyPasteHelper from '@baserow/modules/database/mixins/copyPasteHelper'
|
||||
import GridViewRowsAddContext from '@baserow/modules/database/components/view/grid/fields/GridViewRowsAddContext'
|
||||
|
||||
export default {
|
||||
name: 'GridView',
|
||||
components: {
|
||||
GridViewRowsAddContext,
|
||||
GridViewSection,
|
||||
GridViewFieldWidthHandle,
|
||||
GridViewRowDragging,
|
||||
|
@ -621,6 +626,24 @@ export default {
|
|||
notifyIf(error, 'row')
|
||||
}
|
||||
},
|
||||
async addRows(rowsAmount) {
|
||||
this.$refs.rowsAddContext.hide()
|
||||
try {
|
||||
await this.$store.dispatch(
|
||||
this.storePrefix + 'view/grid/createNewRows',
|
||||
{
|
||||
view: this.view,
|
||||
table: this.table,
|
||||
// We need a list of all fields including the primary one here.
|
||||
fields: this.fields,
|
||||
rows: Array.from(Array(rowsAmount)).map(() => ({})),
|
||||
selectPrimaryCell: true,
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
notifyIf(error, 'row')
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Because it is only possible to add a new row before another row, we have to
|
||||
* figure out which row is below the given row and insert before that one. If the
|
||||
|
@ -683,15 +706,7 @@ export default {
|
|||
},
|
||||
showRowContext(event, row) {
|
||||
this.selectedRow = row
|
||||
this.$refs.rowContext.toggle(
|
||||
{
|
||||
top: event.clientY,
|
||||
left: event.clientX,
|
||||
},
|
||||
'bottom',
|
||||
'right',
|
||||
0
|
||||
)
|
||||
this.$refs.rowContext.toggleNextToMouse(event)
|
||||
},
|
||||
/**
|
||||
* Called when the user starts dragging the row. This will initiate the dragging
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
@mouseover="setHover(true)"
|
||||
@mouseleave="setHover(false)"
|
||||
@click="addRow"
|
||||
@click.right.prevent="addRows"
|
||||
>
|
||||
<i v-if="includeRowDetails" class="fas fa-plus"></i>
|
||||
</a>
|
||||
|
@ -61,6 +62,10 @@ export default {
|
|||
event.preventFieldCellUnselect = true
|
||||
this.$emit('add-row')
|
||||
},
|
||||
addRows(event) {
|
||||
event.preventFieldCellUnselect = true
|
||||
this.$emit('add-rows', event)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
<template>
|
||||
<Context>
|
||||
<div class="context__menu-title">
|
||||
{{ $t('gridViewRowsAddContext.title') }}
|
||||
</div>
|
||||
<ul class="context__menu">
|
||||
<li v-for="rowAmountChoice in rowAmountChoices" :key="rowAmountChoice">
|
||||
<a @click="$emit('add-rows', rowAmountChoice)">
|
||||
{{ $t('gridViewRowsAddContext.choice', { rowAmountChoice }) }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</Context>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import context from '@baserow/modules/core/mixins/context'
|
||||
|
||||
export default {
|
||||
name: 'GridViewRowsAddContext',
|
||||
mixins: [context],
|
||||
computed: {
|
||||
rowAmountChoices() {
|
||||
return [5, 10, 20, 50]
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -554,7 +554,9 @@
|
|||
"deleteRow": "Delete row",
|
||||
"deleteRows": "Delete rows",
|
||||
"copyCells": "Copy cells",
|
||||
"rowCount": "No rows | 1 row | {count} rows"
|
||||
"rowCount": "No rows | 1 row | {count} rows",
|
||||
"hiddenRowsInsertedTitle": "Rows added",
|
||||
"hiddenRowsInsertedMessage": "{number} newly added rows have been added, but are not visible because of the active filters."
|
||||
},
|
||||
"gridViewFieldFile": {
|
||||
"dropHere": "Drop here",
|
||||
|
@ -567,6 +569,10 @@
|
|||
"id": "Row identifier",
|
||||
"count": "Count"
|
||||
},
|
||||
"gridViewRowsAddContext": {
|
||||
"title": "Create multiple rows",
|
||||
"choice": "Add {rowAmountChoice} rows"
|
||||
},
|
||||
"formViewMeta": {
|
||||
"includeRowId": "Use {row_id} to include the newly created row id in the URL."
|
||||
},
|
||||
|
@ -747,4 +753,4 @@
|
|||
"errorEmptyFileNameTitle": "Invalid file name",
|
||||
"errorEmptyFileNameMessage": "You can't set an empty name for a file."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,6 +63,19 @@ export default (client) => {
|
|||
|
||||
return client.post(`/database/rows/table/${tableId}/`, values, config)
|
||||
},
|
||||
batchCreate(tableId, rows, beforeId = null) {
|
||||
const config = { params: {} }
|
||||
|
||||
if (beforeId !== null) {
|
||||
config.params.before = beforeId
|
||||
}
|
||||
|
||||
return client.post(
|
||||
`/database/rows/table/${tableId}/batch/`,
|
||||
{ items: rows },
|
||||
config
|
||||
)
|
||||
},
|
||||
update(tableId, rowId, values) {
|
||||
return client.patch(`/database/rows/table/${tableId}/${rowId}/`, values)
|
||||
},
|
||||
|
|
|
@ -18,6 +18,9 @@ import {
|
|||
import { RefreshCancelledError } from '@baserow/modules/core/errors'
|
||||
import { prepareRowForRequest } from '@baserow/modules/database/utils/row'
|
||||
|
||||
const ORDER_STEP = '1'
|
||||
const ORDER_STEP_BEFORE = '0.00000000000000000001'
|
||||
|
||||
export function populateRow(row, metadata = {}) {
|
||||
row._ = {
|
||||
metadata,
|
||||
|
@ -325,37 +328,50 @@ export const mutations = {
|
|||
state.rows.forEach((row) => {
|
||||
const order = new BigNumber(row.order)
|
||||
if (order.isGreaterThan(min) && order.isLessThanOrEqualTo(max)) {
|
||||
row.order = order
|
||||
.minus(new BigNumber('0.00000000000000000001'))
|
||||
.toString()
|
||||
row.order = order.minus(new BigNumber(ORDER_STEP_BEFORE)).toString()
|
||||
}
|
||||
})
|
||||
},
|
||||
INSERT_NEW_ROW_IN_BUFFER_AT_INDEX(state, { row, index }) {
|
||||
state.count++
|
||||
state.bufferLimit++
|
||||
INSERT_NEW_ROWS_IN_BUFFER_AT_INDEX(state, { rows, index }) {
|
||||
if (rows.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// If another row with the same order already exists, then we need to decrease all
|
||||
// the other orders that are within the range by '0.00000000000000000001'.
|
||||
if (
|
||||
state.rows.findIndex((r) => r.id !== row.id && r.order === row.order) > -1
|
||||
) {
|
||||
const min = new BigNumber(row.order).integerValue(BigNumber.ROUND_FLOOR)
|
||||
const max = new BigNumber(row.order)
|
||||
const potentialNewBufferLimit = state.bufferLimit + rows.length
|
||||
const maximumBufferLimit = state.bufferRequestSize * 3
|
||||
|
||||
// Decrease all the orders that have already have been inserted before the same
|
||||
// row.
|
||||
state.count += rows.length
|
||||
state.bufferLimit =
|
||||
potentialNewBufferLimit > maximumBufferLimit
|
||||
? maximumBufferLimit
|
||||
: potentialNewBufferLimit
|
||||
|
||||
const max = new BigNumber(rows[rows.length - 1].order)
|
||||
const min = new BigNumber(rows[rows.length - 1].order).integerValue(
|
||||
BigNumber.ROUND_FLOOR
|
||||
)
|
||||
|
||||
const isBeforeInsertion = index < state.rows.length
|
||||
if (isBeforeInsertion) {
|
||||
// Decrease the order of every row coming before the inserted rows
|
||||
state.rows.forEach((row) => {
|
||||
const order = new BigNumber(row.order)
|
||||
if (order.isGreaterThan(min) && order.isLessThanOrEqualTo(max)) {
|
||||
row.order = order
|
||||
.minus(new BigNumber('0.00000000000000000001'))
|
||||
const orderCurrent = new BigNumber(row.order)
|
||||
if (
|
||||
orderCurrent.isGreaterThan(min) &&
|
||||
orderCurrent.isLessThanOrEqualTo(max)
|
||||
) {
|
||||
row.order = orderCurrent
|
||||
.minus(new BigNumber(ORDER_STEP_BEFORE * rows.length))
|
||||
.toString()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
state.rows.splice(index, 0, row)
|
||||
// Insert the new rows
|
||||
state.rows.splice(index, 0, ...rows)
|
||||
|
||||
// We might have too many rows inserted now
|
||||
state.rows = state.rows.slice(0, state.bufferLimit)
|
||||
},
|
||||
INSERT_EXISTING_ROW_IN_BUFFER_AT_INDEX(state, { row, index }) {
|
||||
state.rows.splice(index, 0, row)
|
||||
|
@ -383,16 +399,28 @@ export const mutations = {
|
|||
const currentValue = row._.metadata[rowMetadataType]
|
||||
Vue.set(row._.metadata, rowMetadataType, updateFunction(currentValue))
|
||||
},
|
||||
FINALIZE_ROW_IN_BUFFER(state, { oldId, id, order, values }) {
|
||||
const index = state.rows.findIndex((item) => item.id === oldId)
|
||||
if (index !== -1) {
|
||||
state.rows[index].id = id
|
||||
state.rows[index].order = order
|
||||
state.rows[index]._.loading = false
|
||||
Object.keys(values).forEach((key) => {
|
||||
state.rows[index][key] = values[key]
|
||||
FINALIZE_ROWS_IN_BUFFER(state, { oldRows, newRows }) {
|
||||
const stateRowsCopy = { ...state.rows }
|
||||
|
||||
for (let i = 0; i < oldRows.length; i++) {
|
||||
const oldRow = oldRows[i]
|
||||
const newRow = newRows[i]
|
||||
|
||||
const index = state.rows.findIndex((row) => row.id === oldRow.id)
|
||||
|
||||
if (index === -1) {
|
||||
continue
|
||||
}
|
||||
|
||||
stateRowsCopy[index].id = newRow.id
|
||||
stateRowsCopy[index].order = new BigNumber(newRow.order)
|
||||
stateRowsCopy[index]._.loading = false
|
||||
Object.keys(newRow).forEach((key) => {
|
||||
stateRowsCopy[index][key] = newRow[key]
|
||||
})
|
||||
}
|
||||
|
||||
this.state.rows = stateRowsCopy
|
||||
},
|
||||
/**
|
||||
* Deletes a row of which we are sure that it is in the buffer right now.
|
||||
|
@ -1306,7 +1334,7 @@ export const actions = {
|
|||
* object can be provided which will forcefully add the row before that row. If no
|
||||
* `before` is provided, the row will be added last.
|
||||
*/
|
||||
async createNewRow(
|
||||
createNewRow(
|
||||
{ commit, getters, dispatch },
|
||||
{
|
||||
view,
|
||||
|
@ -1317,75 +1345,157 @@ export const actions = {
|
|||
selectPrimaryCell = false,
|
||||
}
|
||||
) {
|
||||
// Fill values with empty values of field if they are not provided
|
||||
fields.forEach((field) => {
|
||||
dispatch('createNewRows', {
|
||||
view,
|
||||
table,
|
||||
fields,
|
||||
rows: [values],
|
||||
before,
|
||||
selectPrimaryCell,
|
||||
})
|
||||
},
|
||||
async createNewRows(
|
||||
{ commit, getters, dispatch },
|
||||
{ view, table, fields, rows = {}, before = null, selectPrimaryCell = false }
|
||||
) {
|
||||
// Create an object of default field values that can be used to fill the row with
|
||||
// missing default values
|
||||
const fieldNewRowValueMap = fields.reduce((map, field) => {
|
||||
const name = `field_${field.id}`
|
||||
const fieldType = this.$registry.get('field', field._.type.type)
|
||||
map[name] = fieldType.getNewRowValue(field)
|
||||
return map
|
||||
}, {})
|
||||
|
||||
if (!(name in values)) {
|
||||
values[name] = fieldType.getNewRowValue(field)
|
||||
}
|
||||
})
|
||||
|
||||
// Fill the not provided values with the empty value of the field type so we can
|
||||
// immediately commit the created row to the state.
|
||||
const preparedRow = prepareRowForRequest(values, fields, this.$registry)
|
||||
const step = before ? ORDER_STEP_BEFORE : ORDER_STEP
|
||||
|
||||
// If before is not provided, then the row is added last. Because we don't know
|
||||
// the total amount of rows in the table, we are going to add find the highest
|
||||
// existing order in the buffer and increase that by one.
|
||||
let order = getters.getHighestOrder
|
||||
.integerValue(BigNumber.ROUND_CEIL)
|
||||
.plus('1')
|
||||
.plus(step)
|
||||
.toString()
|
||||
let index = getters.getBufferEndIndex
|
||||
if (before !== null) {
|
||||
// If the row has been placed before another row we can specifically insert to
|
||||
// the row at a calculated index.
|
||||
const change = new BigNumber('0.00000000000000000001')
|
||||
order = new BigNumber(before.order).minus(change).toString()
|
||||
index = getters.getAllRows.findIndex((r) => r.id === before.id)
|
||||
order = new BigNumber(before.order)
|
||||
.minus(new BigNumber(step * rows.length))
|
||||
.toString()
|
||||
}
|
||||
|
||||
// Populate the row and set the loading state to indicate that the row has not
|
||||
// yet been added.
|
||||
const row = Object.assign({}, values)
|
||||
populateRow(row)
|
||||
row.id = uuid()
|
||||
row.order = order
|
||||
row._.loading = true
|
||||
const index =
|
||||
before === null
|
||||
? getters.getBufferEndIndex
|
||||
: getters.getAllRows.findIndex((r) => r.id === before.id)
|
||||
|
||||
const rowsPrepared = rows.map((row) => {
|
||||
row = { ...clone(fieldNewRowValueMap), ...row }
|
||||
row = prepareRowForRequest(row, fields, this.$registry)
|
||||
return row
|
||||
})
|
||||
|
||||
const rowsPopulated = rowsPrepared.map((row) => {
|
||||
row = { ...clone(fieldNewRowValueMap), ...row }
|
||||
row = populateRow(row)
|
||||
row.id = uuid()
|
||||
row.order = order
|
||||
row._.loading = true
|
||||
|
||||
order = new BigNumber(order).plus(new BigNumber(step)).toString()
|
||||
|
||||
return row
|
||||
})
|
||||
|
||||
const isSingleRowInsertion = rowsPopulated.length === 1
|
||||
const oldCount = getters.getCount
|
||||
|
||||
if (isSingleRowInsertion) {
|
||||
// When a single row is inserted we don't want to deal with filters, sorts and
|
||||
// search just yet. Therefore it is okay to just insert the row into the buffer.
|
||||
commit('INSERT_NEW_ROWS_IN_BUFFER_AT_INDEX', {
|
||||
rows: rowsPopulated,
|
||||
index,
|
||||
})
|
||||
} else {
|
||||
// When inserting multiple rows we will need to deal with filters, sorts or search
|
||||
// not matching. `createdNewRow` deals with exactly that for us.
|
||||
for (let i = 0; i < rowsPopulated.length; i += 1) {
|
||||
await dispatch('createdNewRow', {
|
||||
view,
|
||||
fields,
|
||||
values: rowsPopulated[i],
|
||||
metadata: {},
|
||||
populate: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
commit('INSERT_NEW_ROW_IN_BUFFER_AT_INDEX', { row, index })
|
||||
dispatch('visibleByScrollTop')
|
||||
|
||||
// Check if not all rows are visible.
|
||||
const diff = oldCount - getters.getCount + rowsPopulated.length
|
||||
if (!isSingleRowInsertion && diff > 0) {
|
||||
dispatch(
|
||||
'notification/success',
|
||||
{
|
||||
title: this.$i18n.t('gridView.hiddenRowsInsertedTitle'),
|
||||
message: this.$i18n.t('gridView.hiddenRowsInsertedMessage', {
|
||||
number: diff,
|
||||
}),
|
||||
},
|
||||
{ root: true }
|
||||
)
|
||||
}
|
||||
|
||||
const primaryField = fields.find((f) => f.primary)
|
||||
if (selectPrimaryCell && primaryField) {
|
||||
if (selectPrimaryCell && primaryField && isSingleRowInsertion) {
|
||||
await dispatch('setSelectedCell', {
|
||||
rowId: row.id,
|
||||
rowId: rowsPopulated[0].id,
|
||||
fieldId: primaryField.id,
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await RowService(this.$client).create(
|
||||
const { data } = await RowService(this.$client).batchCreate(
|
||||
table.id,
|
||||
preparedRow,
|
||||
rowsPrepared,
|
||||
before !== null ? before.id : null
|
||||
)
|
||||
commit('FINALIZE_ROW_IN_BUFFER', {
|
||||
oldId: row.id,
|
||||
id: data.id,
|
||||
order: data.order,
|
||||
values: data,
|
||||
|
||||
commit('FINALIZE_ROWS_IN_BUFFER', {
|
||||
oldRows: rowsPopulated,
|
||||
newRows: data.items,
|
||||
})
|
||||
await dispatch('onRowChange', { view, row, fields })
|
||||
|
||||
for (let i = 0; i < data.items.length; i += 1) {
|
||||
const oldRow = rowsPopulated[i]
|
||||
dispatch('onRowChange', { view, row: oldRow, fields })
|
||||
}
|
||||
|
||||
await dispatch('fetchAllFieldAggregationData', {
|
||||
view,
|
||||
})
|
||||
} catch (error) {
|
||||
commit('DELETE_ROW_IN_BUFFER', row)
|
||||
if (isSingleRowInsertion) {
|
||||
commit('DELETE_ROW_IN_BUFFER', rowsPopulated[0])
|
||||
} else {
|
||||
// When we have multiple rows we will need to re-evaluate where the rest of the
|
||||
// rows are now positioned. Therefore, we need to call `deletedExistingRow` to
|
||||
// deal with all the potential edge cases
|
||||
for (let i = 0; i < rowsPopulated.length; i += 1) {
|
||||
await dispatch('deletedExistingRow', {
|
||||
view,
|
||||
fields,
|
||||
row: rowsPopulated[i],
|
||||
})
|
||||
}
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
dispatch('fetchByScrollTopDelayed', {
|
||||
scrollTop: getters.getScrollTop,
|
||||
fields,
|
||||
})
|
||||
},
|
||||
/**
|
||||
* Called after a new row has been created, which could be by the user or via
|
||||
|
@ -1394,10 +1504,13 @@ export const actions = {
|
|||
*/
|
||||
createdNewRow(
|
||||
{ commit, getters, dispatch },
|
||||
{ view, fields, values, metadata }
|
||||
{ view, fields, values, metadata, populate = true }
|
||||
) {
|
||||
const row = clone(values)
|
||||
populateRow(row, metadata)
|
||||
|
||||
if (populate) {
|
||||
populateRow(row, metadata)
|
||||
}
|
||||
|
||||
// Check if the row belongs into the current view by checking if it matches the
|
||||
// filters and search.
|
||||
|
@ -1432,7 +1545,7 @@ export const actions = {
|
|||
(isLast && getters.getBufferEndIndex === getters.getCount) ||
|
||||
(index > 0 && index < allRowsCopy.length - 1)
|
||||
) {
|
||||
commit('INSERT_NEW_ROW_IN_BUFFER_AT_INDEX', { row, index })
|
||||
commit('INSERT_NEW_ROWS_IN_BUFFER_AT_INDEX', { rows: [row], index })
|
||||
} else {
|
||||
if (isFirst) {
|
||||
// Because the row has been added before the our buffer, we need know that the
|
||||
|
@ -1465,7 +1578,7 @@ export const actions = {
|
|||
if (before !== null) {
|
||||
// If the row has been placed before another row we can specifically insert to
|
||||
// the row at a calculated index.
|
||||
const change = new BigNumber('0.00000000000000000001')
|
||||
const change = new BigNumber(ORDER_STEP_BEFORE)
|
||||
order = new BigNumber(before.order).minus(change).toString()
|
||||
}
|
||||
|
||||
|
|
6
web-frontend/test/fixtures/mockServer.js
vendored
6
web-frontend/test/fixtures/mockServer.js
vendored
|
@ -128,8 +128,10 @@ export class MockServer {
|
|||
})
|
||||
}
|
||||
|
||||
creatingRowInTableReturns(table, result) {
|
||||
this.mock.onPost(`/database/rows/table/${table.id}/`).reply(200, result)
|
||||
creatingRowsInTableReturns(table, result) {
|
||||
this.mock
|
||||
.onPost(`/database/rows/table/${table.id}/batch/`)
|
||||
.reply(200, result)
|
||||
}
|
||||
|
||||
updateViewFilter(filterId, newValue) {
|
||||
|
|
|
@ -308,6 +308,7 @@ exports[`Public View Page Tests Can see a publicly shared grid view 1`] = `
|
|||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div
|
||||
class="grid-view__divider"
|
||||
style="left: 70px;"
|
||||
|
|
|
@ -152,6 +152,7 @@ exports[`GridView component with decoration Default component with first_cell de
|
|||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div
|
||||
class="grid-view__divider"
|
||||
style="left: 70px;"
|
||||
|
@ -579,6 +580,7 @@ exports[`GridView component with decoration Default component with row wrapper d
|
|||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div
|
||||
class="grid-view__divider"
|
||||
style="left: 70px;"
|
||||
|
@ -1009,6 +1011,7 @@ exports[`GridView component with decoration Default component with unavailable d
|
|||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div
|
||||
class="grid-view__divider"
|
||||
style="left: 70px;"
|
||||
|
|
|
@ -31,13 +31,17 @@ describe('Table Component Tests', () => {
|
|||
|
||||
expect(tableComponent.html()).toContain('gridView.rowCount - 1')
|
||||
|
||||
mockServer.creatingRowInTableReturns(table, {
|
||||
id: 2,
|
||||
order: '2.00000000000000000000',
|
||||
field_1: '',
|
||||
field_2: '',
|
||||
field_3: '',
|
||||
field_4: false,
|
||||
mockServer.creatingRowsInTableReturns(table, {
|
||||
items: [
|
||||
{
|
||||
id: 2,
|
||||
order: '2.00000000000000000000',
|
||||
field_1: '',
|
||||
field_2: '',
|
||||
field_3: '',
|
||||
field_4: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const button = tableComponent.find('.grid-view__add-row')
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue