1
0
Fork 0
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:
Alexander Haller 2023-02-23 08:43:10 +00:00
parent f1a469946f
commit f91b73ab95
13 changed files with 446 additions and 155 deletions
changelog/entries/unreleased/feature
web-frontend
modules
core
components
mixins
database
components/view/grid
locales
services
store/view
test
fixtures
unit/database
__snapshots__
components/view/grid/__snapshots__
table.spec.js

View file

@ -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"
}

View file

@ -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 }
},
},
}

View file

@ -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)
},

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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."
}
}
}

View 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)
},

View file

@ -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()
}

View file

@ -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) {

View file

@ -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;"

View file

@ -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;"

View file

@ -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')