mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-17 18:32:35 +00:00
Multi-Cell Paste
This commit is contained in:
parent
37a27a795a
commit
22ee2f79a5
22 changed files with 456 additions and 185 deletions
changelog.md
premium/web-frontend/modules/baserow_premium/store/view
web-frontend
locales
modules
core
database
test/unit/database
|
@ -45,6 +45,7 @@
|
|||
* Fixed bug where the link row field `link_row_relation_id` could fail when two
|
||||
simultaneous requests are made.
|
||||
* Added password protection for publicly shared grids and forms.
|
||||
* Added multi-cell pasting.
|
||||
* Made views trashable.
|
||||
* Fixed bug where a cell value was not reverted when the request to the backend fails.
|
||||
* **Premium** Added row coloring.
|
||||
|
|
|
@ -776,9 +776,6 @@ export const actions = {
|
|||
const currentFieldValue = row[fieldToCallName]
|
||||
const optimisticFieldValue = fieldType.onRowChange(
|
||||
row,
|
||||
field,
|
||||
value,
|
||||
oldValue,
|
||||
fieldToCall,
|
||||
currentFieldValue
|
||||
)
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
"retry": "Wiederholen",
|
||||
"search": "Suche",
|
||||
"copy": "Kopieren",
|
||||
"paste": "Einfügen",
|
||||
"activate": "Aktivieren",
|
||||
"deactivate": "Deaktivieren"
|
||||
},
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
"retry": "Retry",
|
||||
"search": "Search",
|
||||
"copy": "Copy",
|
||||
"paste": "Paste",
|
||||
"activate": "Activate",
|
||||
"deactivate": "Deactivate"
|
||||
},
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
:state="undoRedoState"
|
||||
></UndoRedoNotification>
|
||||
<CopyingNotification v-if="copying"></CopyingNotification>
|
||||
<PastingNotification v-if="pasting"></PastingNotification>
|
||||
<RestoreNotification
|
||||
v-for="notification in restoreNotifications"
|
||||
:key="notification.id"
|
||||
|
@ -37,6 +38,7 @@ import ConnectingNotification from '@baserow/modules/core/components/notificatio
|
|||
import FailedConnectingNotification from '@baserow/modules/core/components/notifications/FailedConnectingNotification'
|
||||
import RestoreNotification from '@baserow/modules/core/components/notifications/RestoreNotification'
|
||||
import CopyingNotification from '@baserow/modules/core/components/notifications/CopyingNotification'
|
||||
import PastingNotification from '@baserow/modules/core/components/notifications/PastingNotification'
|
||||
import AuthorizationErrorNotification from '@baserow/modules/core/components/notifications/AuthorizationErrorNotification'
|
||||
import UndoRedoNotification from '@baserow/modules/core/components/notifications/UndoRedoNotification'
|
||||
import { UNDO_REDO_STATES } from '@baserow/modules/core/utils/undoRedoConstants'
|
||||
|
@ -49,6 +51,7 @@ export default {
|
|||
ConnectingNotification,
|
||||
FailedConnectingNotification,
|
||||
CopyingNotification,
|
||||
PastingNotification,
|
||||
AuthorizationErrorNotification,
|
||||
UndoRedoNotification,
|
||||
},
|
||||
|
@ -67,6 +70,7 @@ export default {
|
|||
connecting: (state) => state.notification.connecting,
|
||||
failedConnecting: (state) => state.notification.failedConnecting,
|
||||
copying: (state) => state.notification.copying,
|
||||
pasting: (state) => state.notification.pasting,
|
||||
notifications: (state) => state.notification.items,
|
||||
undoRedoState: (state) => state.notification.undoRedoState,
|
||||
}),
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
<template>
|
||||
<div class="alert alert--simple alert--with-shadow alert--has-icon">
|
||||
<div class="alert__icon">
|
||||
<div class="loading alert__icon-loading"></div>
|
||||
</div>
|
||||
<div class="alert__title">{{ $t('pastingNotification.title') }}</div>
|
||||
<p class="alert__content">{{ $t('pastingNotification.content') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PastingNotification',
|
||||
}
|
||||
</script>
|
|
@ -187,6 +187,10 @@
|
|||
"title": "Kopieren...",
|
||||
"content": "Vorbereiten ihre Daten"
|
||||
},
|
||||
"pastingNotification": {
|
||||
"title": "Einfügen...",
|
||||
"content": "Vorbereiten ihre Daten"
|
||||
},
|
||||
"errorLayout": {
|
||||
"notFound": "Die Seite, die Sie suchen, wurde nicht gefunden. Dies könnte daran liegen, dass die URL nicht korrekt ist oder dass Sie keine Berechtigung haben, diese Seite anzuzeigen.",
|
||||
"error": "Beim Laden der Seite ist ein Fehler aufgetreten. Unsere Entwickler wurden über das Problem informiert. Bitte versuchen Sie, die Seite zu aktualisieren oder zum Dashboard zurückzukehren.",
|
||||
|
|
|
@ -192,6 +192,10 @@
|
|||
"title": "Copying...",
|
||||
"content": "Preparing your data"
|
||||
},
|
||||
"pastingNotification": {
|
||||
"title": "Pasting...",
|
||||
"content": "Preparing your data"
|
||||
},
|
||||
"undoRedoNotification": {
|
||||
"undoingTitle": "Undoing...",
|
||||
"undoingText": "Undoing your action",
|
||||
|
|
|
@ -163,6 +163,7 @@ export default function CoreModule(options) {
|
|||
})
|
||||
this.appendPlugin({ src: path.resolve(__dirname, 'plugins/auth.js') })
|
||||
this.appendPlugin({ src: path.resolve(__dirname, 'plugins/featureFlags.js') })
|
||||
this.appendPlugin({ src: path.resolve(__dirname, 'plugins/papa.js') })
|
||||
|
||||
this.extendRoutes((configRoutes) => {
|
||||
// Remove all the routes created by nuxt.
|
||||
|
|
9
web-frontend/modules/core/plugins/papa.js
Normal file
9
web-frontend/modules/core/plugins/papa.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import Papa from 'papaparse'
|
||||
|
||||
export default function () {
|
||||
Papa.parsePromise = function (file, config = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
Papa.parse(file, { complete: resolve, error: reject, ...config })
|
||||
})
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ export const state = () => ({
|
|||
failedConnecting: false,
|
||||
authorizationError: false,
|
||||
copying: false,
|
||||
pasting: false,
|
||||
// See UNDO_REDO_STATES for all possible values.
|
||||
undoRedoState: UNDO_REDO_STATES.HIDDEN,
|
||||
items: [],
|
||||
|
@ -31,6 +32,9 @@ export const mutations = {
|
|||
SET_COPYING(state, value) {
|
||||
state.copying = value
|
||||
},
|
||||
SET_PASTING(state, value) {
|
||||
state.pasting = value
|
||||
},
|
||||
SET_UNDO_REDO_STATE(state, value) {
|
||||
state.undoRedoState = value
|
||||
},
|
||||
|
@ -88,6 +92,9 @@ export const actions = {
|
|||
setCopying({ commit }, value) {
|
||||
commit('SET_COPYING', value)
|
||||
},
|
||||
setPasting({ commit }, value) {
|
||||
commit('SET_PASTING', value)
|
||||
},
|
||||
setUndoRedoState({ commit }, value) {
|
||||
commit('SET_UNDO_REDO_STATE', value)
|
||||
},
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
@cell-mouseup-left="multiSelectStop"
|
||||
@add-row="addRow()"
|
||||
@update="updateValue"
|
||||
@paste="multiplePasteFromCell"
|
||||
@edit="editValue"
|
||||
@selected="selectedCell($event)"
|
||||
@unselected="unselectedCell($event)"
|
||||
|
@ -79,6 +80,7 @@
|
|||
@row-context="showRowContext($event.event, $event.row)"
|
||||
@add-row="addRow()"
|
||||
@update="updateValue"
|
||||
@paste="multiplePasteFromCell"
|
||||
@edit="editValue"
|
||||
@cell-mousedown-left="multiSelectStart"
|
||||
@cell-mouseover="multiSelectHold"
|
||||
|
@ -176,6 +178,7 @@
|
|||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import Papa from 'papaparse'
|
||||
|
||||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||
import GridViewSection from '@baserow/modules/database/components/view/grid/GridViewSection'
|
||||
|
@ -184,8 +187,8 @@ import GridViewRowDragging from '@baserow/modules/database/components/view/grid/
|
|||
import RowEditModal from '@baserow/modules/database/components/row/RowEditModal'
|
||||
import gridViewHelpers from '@baserow/modules/database/mixins/gridViewHelpers'
|
||||
import { maxPossibleOrderValue } from '@baserow/modules/database/viewTypes'
|
||||
import { isElement } from '@baserow/modules/core/utils/dom'
|
||||
import viewHelpers from '@baserow/modules/database/mixins/viewHelpers'
|
||||
import { isElement } from '@baserow/modules/core/utils/dom'
|
||||
|
||||
export default {
|
||||
name: 'GridView',
|
||||
|
@ -326,7 +329,8 @@ export default {
|
|||
window.addEventListener('resize', this.$el.resizeEvent)
|
||||
window.addEventListener('keydown', this.arrowEvent)
|
||||
window.addEventListener('copy', this.exportMultiSelect)
|
||||
window.addEventListener('click', this.cancelMultiSelect)
|
||||
window.addEventListener('paste', this.pasteFromMultipleCellSelection)
|
||||
window.addEventListener('click', this.cancelMultiSelectIfActive)
|
||||
window.addEventListener('mouseup', this.multiSelectStop)
|
||||
this.$refs.left.$el.addEventListener(
|
||||
'scroll',
|
||||
|
@ -341,14 +345,14 @@ export default {
|
|||
window.removeEventListener('resize', this.$el.resizeEvent)
|
||||
window.removeEventListener('keydown', this.arrowEvent)
|
||||
window.removeEventListener('copy', this.exportMultiSelect)
|
||||
window.removeEventListener('click', this.cancelMultiSelect)
|
||||
window.removeEventListener('paste', this.pasteFromMultipleCellSelection)
|
||||
window.removeEventListener('click', this.cancelMultiSelectIfActive)
|
||||
window.removeEventListener('mouseup', this.multiSelectStop)
|
||||
this.$bus.$off('field-deleted', this.fieldDeleted)
|
||||
|
||||
this.$store.dispatch(
|
||||
this.storePrefix + 'view/grid/setMultiSelectActive',
|
||||
false
|
||||
this.storePrefix + 'view/grid/clearAndDisableMultiSelect'
|
||||
)
|
||||
this.$store.dispatch(this.storePrefix + 'view/grid/clearMultiSelect')
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
|
@ -749,10 +753,8 @@ export default {
|
|||
}
|
||||
|
||||
this.$store.dispatch(
|
||||
this.storePrefix + 'view/grid/setMultiSelectActive',
|
||||
false
|
||||
this.storePrefix + 'view/grid/clearAndDisableMultiSelect'
|
||||
)
|
||||
this.$store.dispatch(this.storePrefix + 'view/grid/clearMultiSelect')
|
||||
|
||||
this.$store.dispatch(this.storePrefix + 'view/grid/setSelectedCell', {
|
||||
rowId: nextRowId,
|
||||
|
@ -773,46 +775,44 @@ export default {
|
|||
this.fieldsUpdated()
|
||||
})
|
||||
},
|
||||
/*
|
||||
Called when mouse is clicked and held on a GridViewCell component.
|
||||
Starts multi-select by setting the head and tail index to the currently
|
||||
selected cell.
|
||||
*/
|
||||
/**
|
||||
* Called when mouse is clicked and held on a GridViewCell component.
|
||||
* Starts multi-select by setting the head and tail index to the currently
|
||||
* selected cell.
|
||||
*/
|
||||
multiSelectStart({ event, row, field }) {
|
||||
this.$store.dispatch(this.storePrefix + 'view/grid/multiSelectStart', {
|
||||
rowId: row.id,
|
||||
fieldIndex: this.visibleFields.findIndex((f) => f.id === field.id) + 1,
|
||||
})
|
||||
},
|
||||
/*
|
||||
Called when mouse hovers over a GridViewCell component.
|
||||
Updates the current multi-select grid by updating the tail index
|
||||
with the last cell hovered over.
|
||||
*/
|
||||
/**
|
||||
* Called when mouse hovers over a GridViewCell component.
|
||||
* Updates the current multi-select grid by updating the tail index
|
||||
* with the last cell hovered over.
|
||||
*/
|
||||
multiSelectHold({ event, row, field }) {
|
||||
this.$store.dispatch(this.storePrefix + 'view/grid/multiSelectHold', {
|
||||
rowId: row.id,
|
||||
fieldIndex: this.visibleFields.findIndex((f) => f.id === field.id) + 1,
|
||||
})
|
||||
},
|
||||
/*
|
||||
Called when the mouse is unpressed over a GridViewCell component.
|
||||
Stop multi-select.
|
||||
*/
|
||||
/**
|
||||
* Called when the mouse is unpressed over a GridViewCell component.
|
||||
* Stop multi-select.
|
||||
*/
|
||||
multiSelectStop({ event, row, field }) {
|
||||
this.$store.dispatch(
|
||||
this.storePrefix + 'view/grid/setMultiSelectHolding',
|
||||
false
|
||||
)
|
||||
},
|
||||
/*
|
||||
Cancels multi-select if it's currently active.
|
||||
This function checks if a mouse click event is triggered
|
||||
outside of GridViewRows. This is done by ensuring that the
|
||||
target element's class is either 'grid-view', 'grid-view__row',
|
||||
or 'grid-view__rows'.
|
||||
*/
|
||||
cancelMultiSelect(event) {
|
||||
/**
|
||||
* Cancels multi-select if it's currently active.
|
||||
* This function checks if a mouse click event is triggered
|
||||
* outside of GridViewRows.
|
||||
*/
|
||||
cancelMultiSelectIfActive(event) {
|
||||
if (
|
||||
this.$store.getters[
|
||||
this.storePrefix + 'view/grid/isMultiSelectActive'
|
||||
|
@ -823,10 +823,8 @@ export default {
|
|||
))
|
||||
) {
|
||||
this.$store.dispatch(
|
||||
this.storePrefix + 'view/grid/setMultiSelectActive',
|
||||
false
|
||||
this.storePrefix + 'view/grid/clearAndDisableMultiSelect'
|
||||
)
|
||||
this.$store.dispatch(this.storePrefix + 'view/grid/clearMultiSelect')
|
||||
}
|
||||
},
|
||||
arrowEvent(event) {
|
||||
|
@ -841,14 +839,15 @@ export default {
|
|||
]
|
||||
) {
|
||||
this.$store.dispatch(
|
||||
this.storePrefix + 'view/grid/setMultiSelectActive',
|
||||
false
|
||||
this.storePrefix + 'view/grid/clearAndDisableMultiSelect'
|
||||
)
|
||||
this.$store.dispatch(this.storePrefix + 'view/grid/clearMultiSelect')
|
||||
}
|
||||
}
|
||||
},
|
||||
// Prepare and copy the multi-select cells into the clipboard, formatted as TSV
|
||||
/**
|
||||
* Prepare and copy the multi-select cells into the clipboard,
|
||||
* formatted as TSV
|
||||
*/
|
||||
async exportMultiSelect(event) {
|
||||
try {
|
||||
this.$store.dispatch('notification/setCopying', true)
|
||||
|
@ -856,8 +855,10 @@ export default {
|
|||
this.storePrefix + 'view/grid/exportMultiSelect',
|
||||
this.leftFields.concat(this.visibleFields)
|
||||
)
|
||||
// If the output is undefined, it means that there is no multiple selection.
|
||||
if (output !== undefined) {
|
||||
navigator.clipboard.writeText(output)
|
||||
const tsv = Papa.unparse(output, { delimiter: '\t' })
|
||||
navigator.clipboard.writeText(tsv)
|
||||
}
|
||||
} catch (error) {
|
||||
notifyIf(error, 'view')
|
||||
|
@ -865,6 +866,73 @@ export default {
|
|||
this.$store.dispatch('notification/setCopying', false)
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Called when the @paste event is triggered from the `GridViewSection` component.
|
||||
* This happens when the individual cell doesn't understand the pasted data and
|
||||
* needs to emit it up. This typically happens when multiple cell values are pasted.
|
||||
*/
|
||||
async multiplePasteFromCell({ data, field, row }) {
|
||||
const rowIndex = this.$store.getters[
|
||||
this.storePrefix + 'view/grid/getRowIndexById'
|
||||
](row.id)
|
||||
const fieldIndex =
|
||||
this.visibleFields.findIndex((f) => f.id === field.id) + 1
|
||||
await this.pasteData(data, rowIndex, fieldIndex)
|
||||
},
|
||||
/**
|
||||
* Called when the user pastes data without having an individual cell selected. It
|
||||
* only works when a multiple selection is active because then we know in which
|
||||
* cells we can paste the data.
|
||||
*/
|
||||
async pasteFromMultipleCellSelection(event) {
|
||||
if (!this.isMultiSelectActive) {
|
||||
return
|
||||
}
|
||||
|
||||
const parsed = await Papa.parsePromise(
|
||||
event.clipboardData.getData('text'),
|
||||
{ delimiter: '\t' }
|
||||
)
|
||||
const data = parsed.data
|
||||
await this.pasteData(data)
|
||||
},
|
||||
/**
|
||||
* Called when data must be pasted into the grid view. It basically forwards the
|
||||
* request to a store action which handles the actual updating of rows. It also
|
||||
* shows a loading animation while busy, so the user knows something is while the
|
||||
* update is in progress.
|
||||
*/
|
||||
async pasteData(data, rowIndex, fieldIndex) {
|
||||
// If the data is an empty array, we don't have to do anything because there is
|
||||
// nothing to update. If the view is in read only mode, we can't paste so not
|
||||
// doing anything.
|
||||
if (data.length === 0 || data[0].length === 0 || this.readOnly) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$store.dispatch('notification/setPasting', true)
|
||||
|
||||
try {
|
||||
await this.$store.dispatch(
|
||||
this.storePrefix + 'view/grid/updateDataIntoCells',
|
||||
{
|
||||
table: this.table,
|
||||
view: this.view,
|
||||
primary: this.primary,
|
||||
fields: this.leftFields.concat(this.visibleFields),
|
||||
getScrollTop: () => this.$refs.left.$refs.body.scrollTop,
|
||||
data,
|
||||
rowIndex,
|
||||
fieldIndex,
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
notifyIf(error)
|
||||
}
|
||||
|
||||
this.$store.dispatch('notification/setPasting', false)
|
||||
return true
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -55,6 +55,7 @@
|
|||
:store-prefix="props.storePrefix"
|
||||
:read-only="props.readOnly"
|
||||
@update="(...args) => $options.methods.update(listeners, props, ...args)"
|
||||
@paste="(...args) => $options.methods.paste(listeners, props, ...args)"
|
||||
@edit="(...args) => $options.methods.edit(listeners, props, ...args)"
|
||||
@unselect="$options.methods.unselect(parent, props)"
|
||||
@selected="$options.methods.selected(listeners, props, $event)"
|
||||
|
@ -111,6 +112,15 @@ export default {
|
|||
})
|
||||
}
|
||||
},
|
||||
paste(listeners, props, event) {
|
||||
if (listeners.paste) {
|
||||
listeners.paste({
|
||||
data: event,
|
||||
row: props.row,
|
||||
field: props.field,
|
||||
})
|
||||
}
|
||||
},
|
||||
/**
|
||||
* If the grid field components emits an edit event then the user has changed the
|
||||
* value without saving it yet. This is for example used to check in real time if
|
||||
|
|
|
@ -94,6 +94,7 @@
|
|||
...getSelectedCellStyle(field),
|
||||
}"
|
||||
@update="$emit('update', $event)"
|
||||
@paste="$emit('paste', $event)"
|
||||
@edit="$emit('edit', $event)"
|
||||
@select="$emit('select', $event)"
|
||||
@unselect="$emit('unselect', $event)"
|
||||
|
@ -241,9 +242,9 @@ export default {
|
|||
this.$store.getters[this.storePrefix + 'view/grid/isMultiSelectActive']
|
||||
) {
|
||||
const rowIndex =
|
||||
this.$store.getters[
|
||||
this.storePrefix + 'view/grid/getMultiSelectRowIndexById'
|
||||
](rowId)
|
||||
this.$store.getters[this.storePrefix + 'view/grid/getRowIndexById'](
|
||||
rowId
|
||||
)
|
||||
|
||||
const allFieldIds = this.allFields.map((field) => field.id)
|
||||
let fieldIndex = allFieldIds.findIndex((id) => field.id === id)
|
||||
|
|
|
@ -325,7 +325,7 @@ export class FieldType extends Registerable {
|
|||
* By default the text value if the clipboard data is used.
|
||||
*/
|
||||
prepareValueForPaste(field, clipboardData) {
|
||||
return clipboardData.getData('text')
|
||||
return clipboardData
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -442,14 +442,7 @@ export class FieldType extends Registerable {
|
|||
* is for example used by the last modified field type to update the last modified
|
||||
* value in real time when a row has changed.
|
||||
*/
|
||||
onRowChange(
|
||||
row,
|
||||
updatedField,
|
||||
updatedFieldValue,
|
||||
updatedFieldOldValue,
|
||||
currentField,
|
||||
currentFieldValue
|
||||
) {
|
||||
onRowChange(row, currentField, currentFieldValue) {
|
||||
return currentFieldValue
|
||||
}
|
||||
|
||||
|
@ -714,7 +707,7 @@ export class LinkRowFieldType extends FieldType {
|
|||
let values
|
||||
|
||||
try {
|
||||
values = JSON.parse(clipboardData.getData('text'))
|
||||
values = JSON.parse(clipboardData)
|
||||
} catch (SyntaxError) {
|
||||
return []
|
||||
}
|
||||
|
@ -880,7 +873,7 @@ export class NumberFieldType extends FieldType {
|
|||
* to be formatted.
|
||||
*/
|
||||
prepareValueForPaste(field, clipboardData) {
|
||||
const value = clipboardData.getData('text')
|
||||
const value = clipboardData
|
||||
if (
|
||||
isNaN(parseFloat(value)) ||
|
||||
!isFinite(value) ||
|
||||
|
@ -1012,7 +1005,7 @@ export class RatingFieldType extends FieldType {
|
|||
* to be formatted.
|
||||
*/
|
||||
prepareValueForPaste(field, clipboardData) {
|
||||
const pastedValue = clipboardData.getData('text')
|
||||
const pastedValue = clipboardData
|
||||
const value = parseInt(pastedValue, 10)
|
||||
|
||||
if (isNaN(value) || !isFinite(value)) {
|
||||
|
@ -1101,7 +1094,7 @@ export class BooleanFieldType extends FieldType {
|
|||
* value is true.
|
||||
*/
|
||||
prepareValueForPaste(field, clipboardData) {
|
||||
const value = clipboardData.getData('text').toLowerCase().trim()
|
||||
const value = clipboardData.toLowerCase().trim()
|
||||
return trueString.includes(value)
|
||||
}
|
||||
|
||||
|
@ -1193,7 +1186,7 @@ class BaseDateFieldType extends FieldType {
|
|||
* correct format for the field. If it can't be parsed null is returned.
|
||||
*/
|
||||
prepareValueForPaste(field, clipboardData) {
|
||||
const value = clipboardData.getData('text').toUpperCase()
|
||||
const value = clipboardData.toUpperCase()
|
||||
|
||||
// Formats for ISO dates
|
||||
let formats = [
|
||||
|
@ -1352,14 +1345,7 @@ export class LastModifiedFieldType extends CreatedOnLastModifiedBaseFieldType {
|
|||
return moment().utc().format()
|
||||
}
|
||||
|
||||
onRowChange(
|
||||
row,
|
||||
updatedField,
|
||||
updatedFieldValue,
|
||||
updatedFieldOldValue,
|
||||
currentField,
|
||||
currentFieldValue
|
||||
) {
|
||||
onRowChange(row, currentField, currentFieldValue) {
|
||||
return this._onRowChangeOrMove()
|
||||
}
|
||||
|
||||
|
@ -1421,7 +1407,7 @@ export class URLFieldType extends FieldType {
|
|||
}
|
||||
|
||||
prepareValueForPaste(field, clipboardData) {
|
||||
const value = clipboardData.getData('text')
|
||||
const value = clipboardData
|
||||
return isValidURL(value) ? value : ''
|
||||
}
|
||||
|
||||
|
@ -1498,7 +1484,7 @@ export class EmailFieldType extends FieldType {
|
|||
}
|
||||
|
||||
prepareValueForPaste(field, clipboardData) {
|
||||
const value = clipboardData.getData('text')
|
||||
const value = clipboardData
|
||||
return isValidEmail(value) ? value : ''
|
||||
}
|
||||
|
||||
|
@ -1597,7 +1583,7 @@ export class FileFieldType extends FieldType {
|
|||
let value
|
||||
|
||||
try {
|
||||
value = JSON.parse(clipboardData.getData('text'))
|
||||
value = JSON.parse(clipboardData)
|
||||
} catch (SyntaxError) {
|
||||
return []
|
||||
}
|
||||
|
@ -1736,7 +1722,7 @@ export class SingleSelectFieldType extends FieldType {
|
|||
if (value === undefined || value === null) {
|
||||
return ''
|
||||
}
|
||||
return value.id
|
||||
return value.value
|
||||
}
|
||||
|
||||
_findOptionWithMatchingId(field, rawTextValue) {
|
||||
|
@ -1755,7 +1741,7 @@ export class SingleSelectFieldType extends FieldType {
|
|||
}
|
||||
|
||||
prepareValueForPaste(field, clipboardData) {
|
||||
const rawTextValue = clipboardData.getData('text')
|
||||
const rawTextValue = clipboardData
|
||||
|
||||
return (
|
||||
this._findOptionWithMatchingId(field, rawTextValue) ||
|
||||
|
@ -1895,7 +1881,7 @@ export class MultipleSelectFieldType extends FieldType {
|
|||
prepareValueForPaste(field, clipboardData) {
|
||||
let values
|
||||
try {
|
||||
values = JSON.parse(clipboardData.getData('text'))
|
||||
values = JSON.parse(clipboardData)
|
||||
} catch (SyntaxError) {
|
||||
return []
|
||||
}
|
||||
|
@ -2006,7 +1992,7 @@ export class PhoneNumberFieldType extends FieldType {
|
|||
}
|
||||
|
||||
prepareValueForPaste(field, clipboardData) {
|
||||
const value = clipboardData.getData('text')
|
||||
const value = clipboardData
|
||||
return isSimplePhoneNumber(value) ? value : ''
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { isElement } from '@baserow/modules/core/utils/dom'
|
||||
import { copyToClipboard } from '@baserow/modules/database/utils/clipboard'
|
||||
import baseField from '@baserow/modules/database/mixins/baseField'
|
||||
import Papa from 'papaparse'
|
||||
|
||||
/**
|
||||
* A mixin that can be used by a field grid component. It introduces the props that
|
||||
|
@ -140,7 +141,8 @@ export default {
|
|||
const value = this.$registry
|
||||
.get('field', this.field.type)
|
||||
.prepareValueForCopy(this.field, rawValue)
|
||||
copyToClipboard(value)
|
||||
const tsv = Papa.unparse([[value]], { delimiter: '\t' })
|
||||
copyToClipboard(tsv)
|
||||
}
|
||||
|
||||
// Removes the value if the backspace/delete key is pressed.
|
||||
|
@ -162,23 +164,40 @@ export default {
|
|||
document.body.addEventListener('keydown', this.$el.keyDownEvent)
|
||||
|
||||
// Updates the value of the field when a user pastes something in the field.
|
||||
this.$el.pasteEvent = (event) => {
|
||||
this.$el.pasteEvent = async (event) => {
|
||||
if (!this.canPaste(event)) {
|
||||
return
|
||||
}
|
||||
|
||||
const value = this.$registry
|
||||
.get('field', this.field.type)
|
||||
.prepareValueForPaste(this.field, event.clipboardData)
|
||||
const oldValue = this.value
|
||||
if (
|
||||
value !== undefined &&
|
||||
value !== oldValue &&
|
||||
!this.readOnly &&
|
||||
!this.field._.type.isReadOnly
|
||||
) {
|
||||
this.$emit('update', value, oldValue)
|
||||
}
|
||||
try {
|
||||
// Multiple values in TSV format can be provided, so we need to properly
|
||||
// parse it using Papa.
|
||||
const parsed = await Papa.parsePromise(
|
||||
event.clipboardData.getData('text'),
|
||||
{ delimiter: '\t' }
|
||||
)
|
||||
const data = parsed.data
|
||||
// A grid field cell can only handle one single value. We try to extract
|
||||
// that from the clipboard and update the cell, otherwise we emit the
|
||||
// paste event up.
|
||||
if (data.length === 1 && data[0].length === 1) {
|
||||
const value = this.$registry
|
||||
.get('field', this.field.type)
|
||||
.prepareValueForPaste(this.field, data[0][0])
|
||||
const oldValue = this.value
|
||||
|
||||
if (
|
||||
value !== undefined &&
|
||||
value !== oldValue &&
|
||||
!this.readOnly &&
|
||||
!this.field._.type.isReadOnly
|
||||
) {
|
||||
this.$emit('update', value, oldValue)
|
||||
}
|
||||
} else {
|
||||
this.$emit('paste', data)
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
document.addEventListener('paste', this.$el.pasteEvent)
|
||||
|
||||
|
|
|
@ -72,6 +72,9 @@ export default (client) => {
|
|||
update(tableId, rowId, values) {
|
||||
return client.patch(`/database/rows/table/${tableId}/${rowId}/`, values)
|
||||
},
|
||||
batchUpdate(tableId, items) {
|
||||
return client.patch(`/database/rows/table/${tableId}/batch/`, { items })
|
||||
},
|
||||
/**
|
||||
* Moves the row to the position before the row related to the beforeRowId
|
||||
* parameters. If the before id is not provided then the row will be moved
|
||||
|
|
|
@ -672,9 +672,6 @@ export default ({ service, customPopulateRow }) => {
|
|||
const currentFieldValue = row[fieldToCallName]
|
||||
const optimisticFieldValue = fieldType.onRowChange(
|
||||
row,
|
||||
field,
|
||||
value,
|
||||
oldValue,
|
||||
fieldToCall,
|
||||
currentFieldValue
|
||||
)
|
||||
|
@ -708,6 +705,7 @@ export default ({ service, customPopulateRow }) => {
|
|||
row,
|
||||
values: oldValues,
|
||||
})
|
||||
dispatch('fetchAllFieldAggregationData', { view })
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
|
|
@ -83,13 +83,13 @@ export const state = () => ({
|
|||
multiSelectActive: false,
|
||||
// Indicates if the user is clicking and holding the mouse over a cell
|
||||
multiSelectHolding: false,
|
||||
/*
|
||||
The indexes for head and tail cells in a multi-select grid.
|
||||
Multi-Select works by tracking four different indexes, these are:
|
||||
- The field and row index for the first cell selected, known as the head.
|
||||
- The field and row index for the last cell selected, known as the tail.
|
||||
All the cells between the head and tail cells are later also calculated as selected.
|
||||
*/
|
||||
/**
|
||||
* The indexes for head and tail cells in a multi-select grid.
|
||||
* Multi-Select works by tracking four different indexes, these are:
|
||||
* - The field and row index for the first cell selected, known as the head.
|
||||
* - The field and row index for the last cell selected, known as the tail.
|
||||
* All the cells between the head and tail cells are later also calculated as selected.
|
||||
*/
|
||||
multiSelectHeadRowIndex: -1,
|
||||
multiSelectHeadFieldIndex: -1,
|
||||
multiSelectTailRowIndex: -1,
|
||||
|
@ -1143,13 +1143,14 @@ export const actions = {
|
|||
setMultiSelectActive({ commit }, value) {
|
||||
commit('SET_MULTISELECT_ACTIVE', value)
|
||||
},
|
||||
clearMultiSelect({ commit }) {
|
||||
clearAndDisableMultiSelect({ commit }) {
|
||||
commit('CLEAR_MULTISELECT')
|
||||
commit('SET_MULTISELECT_ACTIVE', false)
|
||||
},
|
||||
multiSelectStart({ getters, commit }, { rowId, fieldIndex }) {
|
||||
commit('CLEAR_MULTISELECT')
|
||||
|
||||
const rowIndex = getters.getMultiSelectRowIndexById(rowId)
|
||||
const rowIndex = getters.getRowIndexById(rowId)
|
||||
// Set the head and tail index to highlight the first cell
|
||||
commit('UPDATE_MULTISELECT', { position: 'head', rowIndex, fieldIndex })
|
||||
commit('UPDATE_MULTISELECT', { position: 'tail', rowIndex, fieldIndex })
|
||||
|
@ -1166,75 +1167,84 @@ export const actions = {
|
|||
|
||||
commit('UPDATE_MULTISELECT', {
|
||||
position: 'tail',
|
||||
rowIndex: getters.getMultiSelectRowIndexById(rowId),
|
||||
rowIndex: getters.getRowIndexById(rowId),
|
||||
fieldIndex,
|
||||
})
|
||||
|
||||
commit('SET_MULTISELECT_ACTIVE', true)
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Prepares a two dimensional array containing prepared values for copy. It only
|
||||
* contains the cell values selected by the multiple select. If one or more rows
|
||||
* are not in the buffer, they are fetched from the backend.
|
||||
*/
|
||||
async exportMultiSelect({ dispatch, getters, commit }, fields) {
|
||||
if (getters.isMultiSelectActive) {
|
||||
const output = []
|
||||
const [minFieldIndex, maxFieldIndex] =
|
||||
getters.getMultiSelectFieldIndexSorted
|
||||
|
||||
let rows = []
|
||||
fields = fields.slice(minFieldIndex, maxFieldIndex + 1)
|
||||
|
||||
if (getters.areMultiSelectRowsWithinBuffer) {
|
||||
rows = getters.getSelectedRows
|
||||
} else {
|
||||
// Fetch rows from backend
|
||||
rows = await dispatch('getMultiSelectedRows', fields)
|
||||
}
|
||||
|
||||
// Loop over selected rows
|
||||
for (const row of rows) {
|
||||
const line = []
|
||||
|
||||
// Loop over selected fields
|
||||
for (const field of fields) {
|
||||
const rawValue = row['field_' + field.id]
|
||||
// Format the value for copying using the field's prepareValueForCopy()
|
||||
const value = this.$registry
|
||||
.get('field', field.type)
|
||||
.toHumanReadableString(field, rawValue)
|
||||
line.push(JSON.stringify(value))
|
||||
}
|
||||
output.push(line.join('\t'))
|
||||
}
|
||||
return output.join('\n')
|
||||
if (!getters.isMultiSelectActive) {
|
||||
return
|
||||
}
|
||||
},
|
||||
/*
|
||||
This function is called if a user attempts to access rows that are
|
||||
no longer in the row buffer and need to be fetched from the backend.
|
||||
A user can select some or all fields in a row, and only those fields
|
||||
will be returned.
|
||||
*/
|
||||
async getMultiSelectedRows({ getters, rootGetters }, fields) {
|
||||
const [minRow, maxRow] = getters.getMultiSelectRowIndexSorted
|
||||
const gridId = getters.getLastGridId
|
||||
const [minFieldIndex, maxFieldIndex] =
|
||||
getters.getMultiSelectFieldIndexSorted
|
||||
|
||||
return await GridService(this.$client)
|
||||
.fetchRows({
|
||||
gridId,
|
||||
offset: minRow,
|
||||
limit: maxRow - minRow + 1,
|
||||
search: getters.getServerSearchTerm,
|
||||
publicUrl: getters.isPublic,
|
||||
publicAuthToken: getters.getPublicAuthToken,
|
||||
orderBy: getOrderBy(getters, rootGetters),
|
||||
filters: getFilters(getters, rootGetters),
|
||||
includeFields: fields.map((field) => `field_${field.id}`),
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error
|
||||
})
|
||||
.then(({ data }) => {
|
||||
return data.results
|
||||
let rows = []
|
||||
fields = fields.slice(minFieldIndex, maxFieldIndex + 1)
|
||||
|
||||
if (getters.areMultiSelectRowsWithinBuffer) {
|
||||
rows = getters.getSelectedRows
|
||||
} else {
|
||||
// Fetch rows from backend
|
||||
const [minRowIndex, maxRowIndex] = getters.getMultiSelectRowIndexSorted
|
||||
const limit = maxRowIndex - minRowIndex + 1
|
||||
rows = await dispatch('fetchRowsByIndex', {
|
||||
startIndex: minRowIndex,
|
||||
limit,
|
||||
fields,
|
||||
})
|
||||
}
|
||||
|
||||
const data = []
|
||||
for (const row of rows) {
|
||||
const line = []
|
||||
|
||||
for (const field of fields) {
|
||||
const rawValue = row['field_' + field.id]
|
||||
const value = this.$registry
|
||||
.get('field', field.type)
|
||||
.prepareValueForCopy(field, rawValue)
|
||||
line.push(value)
|
||||
}
|
||||
data.push(line)
|
||||
}
|
||||
|
||||
return data
|
||||
},
|
||||
/**
|
||||
* This function is called if a user attempts to access rows that are
|
||||
* no longer in the row buffer and need to be fetched from the backend.
|
||||
* A user can select some or all fields in a row, and only those fields
|
||||
* will be returned.
|
||||
*/
|
||||
async fetchRowsByIndex(
|
||||
{ getters, rootGetters },
|
||||
{ startIndex, limit, fields }
|
||||
) {
|
||||
if (fields !== undefined) {
|
||||
fields = fields.map((field) => `field_${field.id}`)
|
||||
}
|
||||
|
||||
const gridId = getters.getLastGridId
|
||||
const { data } = await GridService(this.$client).fetchRows({
|
||||
gridId,
|
||||
offset: startIndex,
|
||||
limit,
|
||||
search: getters.getServerSearchTerm,
|
||||
publicUrl: getters.isPublic,
|
||||
publicAuthToken: getters.getPublicAuthToken,
|
||||
orderBy: getOrderBy(getters, rootGetters),
|
||||
filters: getFilters(getters, rootGetters),
|
||||
includeFields: fields,
|
||||
})
|
||||
return data.results
|
||||
},
|
||||
setRowHover({ commit }, { row, value }) {
|
||||
commit('SET_ROW_HOVER', { row, value })
|
||||
|
@ -1358,10 +1368,6 @@ export const actions = {
|
|||
dispatch('updateMatchFilters', { view, row, fields, primary })
|
||||
dispatch('updateSearchMatchesForRow', { row, fields, primary })
|
||||
|
||||
dispatch('fetchAllFieldAggregationData', {
|
||||
view,
|
||||
})
|
||||
|
||||
// If the row does not match the filters or the search then we don't have to add
|
||||
// it at all.
|
||||
if (!row._.matchFilters || !row._.matchSearch) {
|
||||
|
@ -1479,6 +1485,7 @@ export const actions = {
|
|||
fields,
|
||||
primary,
|
||||
})
|
||||
dispatch('fetchAllFieldAggregationData', { view: grid })
|
||||
} catch (error) {
|
||||
dispatch('updatedExistingRow', {
|
||||
view: grid,
|
||||
|
@ -1524,9 +1531,6 @@ export const actions = {
|
|||
const currentFieldValue = row[fieldID]
|
||||
const optimisticFieldValue = fieldType.onRowChange(
|
||||
row,
|
||||
field,
|
||||
value,
|
||||
oldValue,
|
||||
fieldToCall,
|
||||
currentFieldValue
|
||||
)
|
||||
|
@ -1568,6 +1572,140 @@ export const actions = {
|
|||
throw error
|
||||
}
|
||||
},
|
||||
/**
|
||||
* This action is used by the grid view to change multiple cells when pasting
|
||||
* multiple values. It figures out which cells need to be changed, makes a request
|
||||
* to the backend and updates the affected rows in the store.
|
||||
*/
|
||||
async updateDataIntoCells(
|
||||
{ getters, commit, dispatch },
|
||||
{ table, view, primary, fields, getScrollTop, data, rowIndex, fieldIndex }
|
||||
) {
|
||||
// If the origin origin row and field index are not provided, we need to use the
|
||||
// head indexes of the multiple select.
|
||||
const rowHeadIndex = rowIndex || getters.getMultiSelectHeadRowIndex
|
||||
const fieldHeadIndex = fieldIndex || getters.getMultiSelectHeadFieldIndex
|
||||
|
||||
// Based on the data, we can figure out in which cells we must paste. Here we find
|
||||
// the maximum tail indexes.
|
||||
const rowTailIndex =
|
||||
Math.min(getters.getCount, rowHeadIndex + data.length) - 1
|
||||
const fieldTailIndex =
|
||||
Math.min(fields.length, fieldHeadIndex + data[0].length) - 1
|
||||
|
||||
// Expand the selection of the multiple select to the cells that we're going to
|
||||
// paste in, so the user can see which values have been updated. This is because
|
||||
// it could be that there are more or less values in the clipboard compared to
|
||||
// what was originally selected.
|
||||
commit('UPDATE_MULTISELECT', {
|
||||
position: 'head',
|
||||
rowIndex: rowHeadIndex,
|
||||
fieldIndex: fieldHeadIndex,
|
||||
})
|
||||
commit('UPDATE_MULTISELECT', {
|
||||
position: 'tail',
|
||||
rowIndex: rowTailIndex,
|
||||
fieldIndex: fieldTailIndex,
|
||||
})
|
||||
commit('SET_MULTISELECT_ACTIVE', true)
|
||||
// Unselect a single selected cell because we've just updated the multiple
|
||||
// selected and we don't want that to conflict.
|
||||
commit('SET_SELECTED_CELL', { rowId: -1, fieldId: -1 })
|
||||
|
||||
// Figure out which rows are already in the buffered and temporarily store them
|
||||
// in an array.
|
||||
const fieldsInOrder = fields.slice(fieldHeadIndex, fieldTailIndex + 1)
|
||||
let rowsInOrder = getters.getAllRows.slice(
|
||||
rowHeadIndex - getters.getBufferStartIndex,
|
||||
rowTailIndex + 1 - getters.getBufferStartIndex
|
||||
)
|
||||
|
||||
// Check if there are fields that can be updated. If there aren't any fields,
|
||||
// maybe because the provided index is outside of the available fields or
|
||||
// because there are only read only fields, we don't want to do anything.
|
||||
const writeFields = fieldsInOrder.filter(
|
||||
(field) => !field._.type.isReadOnly
|
||||
)
|
||||
if (writeFields.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate if there are rows outside of the buffer that need to be fetched and
|
||||
// prepended or appended to the `rowsInOrder`
|
||||
const startIndex = rowHeadIndex + rowsInOrder.length
|
||||
const limit = rowTailIndex - rowHeadIndex - rowsInOrder.length + 1
|
||||
if (limit > 0) {
|
||||
const rowsNotInBuffer = await dispatch('fetchRowsByIndex', {
|
||||
startIndex,
|
||||
limit,
|
||||
})
|
||||
// Depends on whether the missing rows are before or after the buffer.
|
||||
rowsInOrder =
|
||||
startIndex < getters.getBufferStartIndex
|
||||
? [...rowsNotInBuffer, ...rowsInOrder]
|
||||
: [...rowsInOrder, ...rowsNotInBuffer]
|
||||
}
|
||||
|
||||
// Create a copy of the existing (old) rows, which are needed to create the
|
||||
// comparison when checking if the rows still matches the filters and position.
|
||||
const oldRowsInOrder = clone(rowsInOrder)
|
||||
// Prepare the values that must be send to the server.
|
||||
const valuesForUpdate = []
|
||||
|
||||
// Prepare the values for update and update the row objects.
|
||||
rowsInOrder.forEach((row, rowIndex) => {
|
||||
valuesForUpdate[rowIndex] = { id: row.id }
|
||||
|
||||
fieldsInOrder.forEach((field, fieldIndex) => {
|
||||
// We can't pre-filter because we need the correct filter index.
|
||||
if (field._.type.isReadOnly) {
|
||||
return
|
||||
}
|
||||
|
||||
const fieldId = `field_${field.id}`
|
||||
const value = data[rowIndex][fieldIndex]
|
||||
const fieldType = this.$registry.get('field', field._.type.type)
|
||||
const preparedValue = fieldType.prepareValueForPaste(field, value)
|
||||
const newValue = fieldType.prepareValueForUpdate(field, preparedValue)
|
||||
valuesForUpdate[rowIndex][fieldId] = newValue
|
||||
})
|
||||
})
|
||||
|
||||
// We don't have to update the rows in the buffer before the request is being made
|
||||
// because we're showing a loading animation to the user indicating that the
|
||||
// rows are being updated.
|
||||
const { data: responseData } = await RowService(this.$client).batchUpdate(
|
||||
table.id,
|
||||
valuesForUpdate
|
||||
)
|
||||
const updatedRows = responseData.items
|
||||
|
||||
// Loop over the old rows, find the matching updated row and update them in the
|
||||
// buffer accordingly.
|
||||
for (const row of oldRowsInOrder) {
|
||||
// The values are the updated row returned by the response.
|
||||
const values = updatedRows.find((updatedRow) => updatedRow.id === row.id)
|
||||
// Calling the updatedExistingRow will automatically remove the row from the
|
||||
// view if it doesn't matter the filters anymore and it will also be moved to
|
||||
// the right position if changed.
|
||||
await dispatch('updatedExistingRow', {
|
||||
view,
|
||||
fields,
|
||||
primary,
|
||||
row,
|
||||
values,
|
||||
})
|
||||
}
|
||||
|
||||
// Must be called because rows could have been removed or moved to a different
|
||||
// position and we might need to fetch missing rows.
|
||||
await dispatch('fetchByScrollTopDelayed', {
|
||||
scrollTop: getScrollTop(),
|
||||
fields,
|
||||
primary,
|
||||
})
|
||||
dispatch('fetchAllFieldAggregationData', { view })
|
||||
},
|
||||
/**
|
||||
* Called after an existing row has been updated, which could be by the user or
|
||||
* via another channel. It will make sure that the row has the correct position or
|
||||
|
@ -1588,10 +1726,6 @@ export const actions = {
|
|||
dispatch('updateMatchFilters', { view, row: newRow, fields, primary })
|
||||
dispatch('updateSearchMatchesForRow', { row: newRow, fields, primary })
|
||||
|
||||
dispatch('fetchAllFieldAggregationData', {
|
||||
view,
|
||||
})
|
||||
|
||||
const oldRowExists = oldRow._.matchFilters && oldRow._.matchSearch
|
||||
const newRowExists = newRow._.matchFilters && newRow._.matchSearch
|
||||
|
||||
|
@ -1730,6 +1864,7 @@ export const actions = {
|
|||
fields,
|
||||
primary,
|
||||
})
|
||||
dispatch('fetchAllFieldAggregationData', { view })
|
||||
} catch (error) {
|
||||
commit('SET_ROW_LOADING', { row, value: false })
|
||||
throw error
|
||||
|
@ -1750,10 +1885,6 @@ export const actions = {
|
|||
dispatch('updateMatchFilters', { view, row, fields, primary })
|
||||
dispatch('updateSearchMatchesForRow', { row, fields, primary })
|
||||
|
||||
dispatch('fetchAllFieldAggregationData', {
|
||||
view,
|
||||
})
|
||||
|
||||
// If the row does not match the filters or the search then did not exist in the
|
||||
// view, so we don't have to do anything.
|
||||
if (!row._.matchFilters || !row._.matchSearch) {
|
||||
|
@ -2061,12 +2192,18 @@ export const getters = {
|
|||
),
|
||||
]
|
||||
},
|
||||
getMultiSelectHeadFieldIndex(state) {
|
||||
return state.multiSelectHeadFieldIndex
|
||||
},
|
||||
getMultiSelectHeadRowIndex(state) {
|
||||
return state.multiSelectHeadRowIndex
|
||||
},
|
||||
// Get the index of a row given it's row id.
|
||||
// This will calculate the row index from the current buffer position and offset.
|
||||
getMultiSelectRowIndexById: (state) => (rowId) => {
|
||||
getRowIndexById: (state, getters) => (rowId) => {
|
||||
const bufferIndex = state.rows.findIndex((r) => r.id === rowId)
|
||||
if (bufferIndex !== -1) {
|
||||
return state.bufferStartIndex + bufferIndex
|
||||
return getters.getBufferStartIndex + bufferIndex
|
||||
}
|
||||
return -1
|
||||
},
|
||||
|
@ -2075,8 +2212,8 @@ export const getters = {
|
|||
const [minRow, maxRow] = getters.getMultiSelectRowIndexSorted
|
||||
|
||||
return (
|
||||
minRow >= state.bufferStartIndex &&
|
||||
maxRow <= state.bufferStartIndex + state.bufferRequestSize
|
||||
minRow >= getters.getBufferStartIndex &&
|
||||
maxRow <= getters.getBufferEndIndex
|
||||
)
|
||||
},
|
||||
// Return all rows within a multi-select grid if they are within the current row buffer
|
||||
|
|
|
@ -500,6 +500,9 @@ export class GridViewType extends ViewType {
|
|||
fields,
|
||||
primary,
|
||||
})
|
||||
store.dispatch(storePrefix + 'view/grid/fetchAllFieldAggregationData', {
|
||||
view: store.getters['view/getSelected'],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -527,6 +530,9 @@ export class GridViewType extends ViewType {
|
|||
fields,
|
||||
primary,
|
||||
})
|
||||
store.dispatch(storePrefix + 'view/grid/fetchAllFieldAggregationData', {
|
||||
view: store.getters['view/getSelected'],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -543,6 +549,9 @@ export class GridViewType extends ViewType {
|
|||
fields,
|
||||
primary,
|
||||
})
|
||||
store.dispatch(storePrefix + 'view/grid/fetchAllFieldAggregationData', {
|
||||
view: store.getters['view/getSelected'],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,8 @@ exports[`Public View Page Tests Can see a publicly shared grid view 1`] = `
|
|||
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -325,15 +325,9 @@ describe('FieldType tests', () => {
|
|||
test.each(datePrepareValueForPaste)(
|
||||
'Verify that prepareValueForPaste for DateFieldType returns the expected output',
|
||||
(value) => {
|
||||
const clipboardData = {
|
||||
getData() {
|
||||
return value.fieldValue
|
||||
},
|
||||
}
|
||||
|
||||
const result = new DateFieldType().prepareValueForPaste(
|
||||
value.field,
|
||||
clipboardData
|
||||
value.fieldValue
|
||||
)
|
||||
expect(result).toBe(value.expectedValue)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue