1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-10 23:50:12 +00:00

Merge branch '311-user-reports-that-a-table-is-slow-having-21-columns' into 'develop'

Resolve "User reports that a table is slow having 21 columns"

Closes 

See merge request 
This commit is contained in:
Bram Wiepjes 2021-03-09 20:53:40 +00:00
commit e17fc6f000
41 changed files with 1456 additions and 774 deletions

View file

@ -3,6 +3,7 @@
## Unreleased
* Prevent websocket reconnect loop when the authentication fails.
* Refactored the GridView component and improved interface speed.
* Prevent websocket reconnect when the connection closes without error.
* Added gunicorn worker test to the CI pipeline.

View file

@ -4,6 +4,15 @@ import StyleLintPlugin from 'stylelint-webpack-plugin'
import base from './nuxt.config.base.js'
export default _.assign(base(), {
vue: {
config: {
productionTip: false,
devtools: true,
performance: true,
silent: false,
},
},
dev: true,
build: {
extend(config, ctx) {
if (ctx.isDev && ctx.isClient) {

View file

@ -91,6 +91,7 @@
.grid-view__head {
@include absolute(0, 0, auto, 0);
display: flex;
height: 33px;
background-color: $color-neutral-50;
border-bottom: 1px solid $color-neutral-200;
@ -177,6 +178,7 @@
@extend .clearfix;
position: relative;
display: flex;
height: 32px + 1px;
&.grid-view__row--warning::before {
@ -202,7 +204,6 @@
// Because the width of a column can be adjusted it is specified in the html file.
.grid-view__column {
position: relative;
float: left;
height: 32px + 1px;
border-right: 1px solid $color-neutral-200;
border-bottom: 1px solid $color-neutral-200;
@ -453,7 +454,10 @@
color: $color-neutral-900;
background-color: $white;
&:hover,
&:hover {
text-decoration: none;
}
&.hover {
background-color: $color-primary-100;
}

View file

@ -73,7 +73,13 @@ export default {
* Calculate the position, show the context menu and register a click event on the
* body to check if the user has clicked outside the context.
*/
show(target, vertical, horizontal, verticalOffset, horizontalOffset) {
show(
target,
vertical,
horizontal,
verticalOffset = 10,
horizontalOffset = 0
) {
const isElementOrigin = isDomElement(target)
const updatePosition = () => {
const css = isElementOrigin

View file

@ -105,7 +105,7 @@ export default {
* parent.
*/
updateVertical() {
const element = this.$parent.$refs[this.vertical]
const element = this.$parent[this.vertical]()
const show = element.scrollHeight > element.clientHeight
// @TODO if the client height is very high we have a minimum of 2%, but this needs
// to be subtracted from the top position so that it fits. Same goes for the
@ -126,7 +126,7 @@ export default {
* parent.
*/
updateHorizontal() {
const element = this.$parent.$refs[this.horizontal]
const element = this.$parent[this.horizontal]()
const show = element.scrollWidth > element.clientWidth
const width = Math.max(
floor((element.clientWidth / element.scrollWidth) * 100, 2),
@ -164,7 +164,7 @@ export default {
mouseMove(event) {
if (this.dragging === 'vertical') {
event.preventDefault()
const element = this.$parent.$refs[this.vertical]
const element = this.$parent[this.vertical]()
const delta = event.clientY - this.mouseStart
const pixel = element.scrollHeight / element.clientHeight
const top = Math.ceil((this.elementStart + delta) * pixel)
@ -175,7 +175,7 @@ export default {
if (this.dragging === 'horizontal') {
event.preventDefault()
const element = this.$parent.$refs[this.horizontal]
const element = this.$parent[this.horizontal]()
const delta = event.clientX - this.mouseStart
const pixel = element.scrollWidth / element.clientWidth
const left = Math.ceil((this.elementStart + delta) * pixel)

View file

@ -4,7 +4,7 @@
<div v-if="loaded" :class="{ 'select-row-modal__loading': loading }">
<Scrollbars
ref="scrollbars"
horizontal="right"
horizontal="getHorizontalScrollbarElement"
:style="{ left: '240px' }"
@horizontal="horizontalScroll"
></Scrollbars>
@ -189,6 +189,12 @@ export default {
this.loaded = true
},
methods: {
/**
* Returns the scrollable element for the scrollbar.
*/
getHorizontalScrollbarElement() {
return this.$refs.right
},
/**
* Event that is called when the users does any form of scrolling on the whole grid
* surface.

View file

@ -1,10 +1,11 @@
<template>
<component
:is="getFieldComponent(field.type)"
ref="field"
:field="field"
:value="row['field_' + field.id]"
:selected="false"
:state="{}"
:read-only="true"
/>
</template>
@ -23,7 +24,9 @@ export default {
},
methods: {
getFieldComponent(type) {
return this.$registry.get('field', type).getGridViewFieldComponent()
return this.$registry
.get('field', type)
.getFunctionalGridViewFieldComponent()
},
},
}

View file

@ -2,289 +2,69 @@
<div v-scroll="scroll" class="grid-view">
<Scrollbars
ref="scrollbars"
horizontal="right"
vertical="rightBody"
:style="{ left: widths.left + 'px' }"
horizontal="getHorizontalScrollbarElement"
vertical="getVerticalScrollbarElement"
:style="{ left: leftWidth + 'px' }"
@vertical="verticalScroll"
@horizontal="horizontalScroll"
></Scrollbars>
<div class="grid-view__left" :style="{ width: widths.left + 'px' }">
<div class="grid-view__inner" :style="{ width: widths.left + 'px' }">
<div class="grid-view__head">
<div
class="grid-view__column"
:style="{ width: widths.leftReserved + 'px' }"
></div>
<GridViewFieldType
v-if="primary !== null"
:table="table"
:view="view"
:field="primary"
:filters="view.filters"
:style="{ width: widths.fields[primary.id] + 'px' }"
@refresh="$emit('refresh', $event)"
></GridViewFieldType>
<GridViewSection
ref="left"
class="grid-view__left"
:fields="leftFields"
:table="table"
:view="view"
:include-field-width-handles="false"
:include-row-details="true"
:style="{ width: leftWidth + 'px' }"
@refresh="$emit('refresh', $event)"
@row-hover="setRowHover($event.row, $event.value)"
@row-context="showRowContext($event.event, $event.row)"
@add-row="addRow()"
@update="updateValue"
@edit="editValue"
@selected="selectedCell($event)"
@unselected="unselectedCell($event)"
@select-next="selectNextCell($event)"
@edit-modal="$refs.rowEditModal.show($event.id)"
>
<template #foot>
<div class="grid-view__column" :style="{ width: leftWidth + 'px' }">
<div class="grid-view__foot-info">{{ count }} rows</div>
</div>
<div ref="leftBody" class="grid-view__body">
<div class="grid-view__body-inner">
<div
class="grid-view__placeholder"
:style="{
height: placeholderHeight + 'px',
width: widths.left + 'px',
}"
>
<div
class="grid-view__placeholder-column"
:style="{
width: widths.left + 'px',
}"
></div>
</div>
<div
class="grid-view__rows"
:style="{ transform: `translateY(${rowsTop}px)` }"
>
<div
v-for="row in rows"
:key="'left-row-' + view.id + '-' + row.id"
class="grid-view__row"
:class="{
'grid-view__row--selected': row._.selectedBy.length > 0,
'grid-view__row--loading': row._.loading,
'grid-view__row--hover': row._.hover,
'grid-view__row--warning':
!row._.matchFilters || !row._.matchSortings,
}"
@mouseover="setRowHover(row, true)"
@mouseleave="setRowHover(row, false)"
@contextmenu.prevent="showRowContext($event, row)"
>
<div
v-if="!row._.matchFilters || !row._.matchSortings"
class="grid-view__row-warning"
>
<template v-if="!row._.matchFilters">
Row does not match filters
</template>
<template v-else-if="!row._.matchSortings">
Row has moved
</template>
</div>
<div
class="grid-view__column"
:style="{ width: widths.leftReserved + 'px' }"
>
<div class="grid-view__row-info">
<div class="grid-view__row-count" :title="row.id">
{{ row.id }}
</div>
<a
class="grid-view__row-more"
@click="$refs.rowEditModal.show(row.id)"
>
<i class="fas fa-expand"></i>
</a>
</div>
</div>
<GridViewField
v-if="primary !== null"
:ref="'row-' + row.id + '-field-' + primary.id"
:field="primary"
:row="row"
:style="{ width: widths.fields[primary.id] + 'px' }"
@selected="selectedField(primary, $event)"
@unselected="unselectedField(primary, $event)"
@selectNext="selectNextField(row, primary, fields, primary)"
@selectAbove="
selectNextField(row, primary, fields, primary, 'above')
"
@selectBelow="
selectNextField(row, primary, fields, primary, 'below')
"
@update="updateValue"
@edit="editValue"
></GridViewField>
</div>
</div>
<div class="grid-view__row">
<div
class="grid-view__column"
:style="{ width: widths.left + 'px' }"
>
<a
class="grid-view__add-row"
:class="{ hover: addHover }"
@mouseover="addHover = true"
@mouseleave="addHover = false"
@click="addRow()"
>
<i class="fas fa-plus"></i>
</a>
</div>
</div>
</div>
</div>
<div class="grid-view__foot">
<div class="grid-view__column" :style="{ width: widths.left + 'px' }">
<div class="grid-view__foot-info">{{ count }} rows</div>
</div>
</div>
</div>
</div>
</template>
</GridViewSection>
<div
ref="divider"
class="grid-view__divider"
:style="{ left: widths.left + 'px' }"
:style="{ left: leftWidth + 'px' }"
></div>
<GridViewFieldWidthHandle
class="grid-view__divider-width"
:style="{ left: widths.left + 'px' }"
:style="{ left: leftWidth + 'px' }"
:grid="view"
:field="primary"
:width="widths.fields[primary.id]"
:width="leftFieldsWidth"
></GridViewFieldWidthHandle>
<div
<GridViewSection
ref="right"
class="grid-view__right"
:style="{ left: widths.left + 'px' }"
>
<div
class="grid-view__inner"
:style="{ 'min-width': widths.right + 'px' }"
>
<div class="grid-view__head">
<GridViewFieldType
v-for="field in visibleFields"
:key="'right-head-field-' + view.id + '-' + field.id"
:table="table"
:view="view"
:field="field"
:filters="view.filters"
:style="{ width: widths.fields[field.id] + 'px' }"
@refresh="$emit('refresh', $event)"
>
<GridViewFieldWidthHandle
class="grid-view__description-width"
:grid="view"
:field="field"
:width="widths.fields[field.id]"
></GridViewFieldWidthHandle>
</GridViewFieldType>
<div
class="grid-view__column"
:style="{ width: widths.rightAdd + 'px' }"
>
<a
ref="createFieldContextLink"
class="grid-view__add-column"
@click="
$refs.createFieldContext.toggle($refs.createFieldContextLink)
"
>
<i class="fas fa-plus"></i>
</a>
<CreateFieldContext
ref="createFieldContext"
:table="table"
></CreateFieldContext>
</div>
</div>
<div ref="rightBody" class="grid-view__body">
<div class="grid-view__body-inner">
<div
class="grid-view__placeholder"
:style="{
height: placeholderHeight + 'px',
width: widths.rightFieldsOnly + 'px',
}"
>
<div
v-for="(value, id) in widths.placeholderPositions"
:key="'right-placeholder-column-' + view.id + '-' + id"
class="grid-view__placeholder-column"
:style="{ left: value - 1 + 'px' }"
></div>
</div>
<div
class="grid-view__rows"
:style="{ transform: `translateY(${rowsTop}px)` }"
>
<!-- @TODO figure out a faster way to render the rows on scroll. -->
<div
v-for="row in rows"
:key="'right-row-' + view.id + '-' + row.id"
class="grid-view__row"
:class="{
'grid-view__row--selected': row._.selectedBy.length > 0,
'grid-view__row--loading': row._.loading,
'grid-view__row--hover': row._.hover,
'grid-view__row--warning':
!row._.matchFilters || !row._.matchSortings,
}"
@mouseover="setRowHover(row, true)"
@mouseleave="setRowHover(row, false)"
@contextmenu.prevent="showRowContext($event, row)"
>
<GridViewField
v-for="field in visibleFields"
:ref="'row-' + row.id + '-field-' + field.id"
:key="
'right-row-field-' + view.id + '-' + row.id + '-' + field.id
"
:field="field"
:row="row"
:style="{ width: widths.fields[field.id] + 'px' }"
@selected="selectedField(field, $event)"
@unselected="unselectedField(field, $event)"
@selectPrevious="
selectNextField(row, field, fields, primary, 'previous')
"
@selectNext="selectNextField(row, field, fields, primary)"
@selectAbove="
selectNextField(row, field, fields, primary, 'above')
"
@selectBelow="
selectNextField(row, field, fields, primary, 'below')
"
@update="updateValue"
@edit="editValue"
></GridViewField>
</div>
</div>
<div
class="grid-view__row"
:style="{ width: widths.rightFieldsOnly + 'px' }"
>
<div
class="grid-view__column"
:style="{ width: widths.rightFieldsOnly + 'px' }"
>
<a
class="grid-view__add-row"
:class="{ hover: addHover }"
@mouseover="addHover = true"
@mouseleave="addHover = false"
@click="addRow()"
></a>
</div>
</div>
</div>
</div>
<div class="grid-view__foot"></div>
</div>
</div>
<div
v-if="view.filters.length > 0 && count === 0"
class="grid-view__filtered-no-results"
>
<div class="grid-view__filtered-no-results-icon">
<i class="fas fa-filter"></i>
</div>
<div class="grid-view__filtered-no-results-content">
Rows are filtered
</div>
</div>
:fields="fields"
:table="table"
:view="view"
:include-add-field="true"
:style="{ left: leftWidth + 'px' }"
@refresh="$emit('refresh', $event)"
@row-hover="setRowHover($event.row, $event.value)"
@row-context="showRowContext($event.event, $event.row)"
@add-row="addRow()"
@update="updateValue"
@edit="editValue"
@selected="selectedCell($event)"
@unselected="unselectedCell($event)"
@select-next="selectNextCell($event)"
@edit-modal="$refs.rowEditModal.show($event.id)"
></GridViewSection>
<Context ref="rowContext">
<ul class="context__menu">
<li>
@ -337,23 +117,20 @@
<script>
import { mapGetters } from 'vuex'
import CreateFieldContext from '@baserow/modules/database/components/field/CreateFieldContext'
import GridViewFieldType from '@baserow/modules/database/components/view/grid/GridViewFieldType'
import GridViewField from '@baserow/modules/database/components/view/grid/GridViewField'
import { notifyIf } from '@baserow/modules/core/utils/error'
import GridViewSection from '@baserow/modules/database/components/view/grid/GridViewSection'
import GridViewFieldWidthHandle from '@baserow/modules/database/components/view/grid/GridViewFieldWidthHandle'
import RowEditModal from '@baserow/modules/database/components/row/RowEditModal'
import { notifyIf } from '@baserow/modules/core/utils/error'
import _ from 'lodash'
import gridViewHelpers from '@baserow/modules/database/mixins/gridViewHelpers'
export default {
name: 'GridView',
components: {
CreateFieldContext,
GridViewFieldType,
GridViewField,
GridViewSection,
GridViewFieldWidthHandle,
RowEditModal,
},
mixins: [gridViewHelpers],
props: {
primary: {
type: Object,
@ -378,52 +155,45 @@ export default {
},
data() {
return {
addHover: false,
selectedRow: null,
lastHoveredRow: null,
widths: {
fields: {},
},
selectedRow: null,
}
},
computed: {
visibleFields() {
return this.fields.filter((field) => {
const exists = Object.prototype.hasOwnProperty.call(
this.fieldOptions,
field.id
)
return !exists || (exists && !this.fieldOptions[field.id].hidden)
})
leftFields() {
return [this.primary]
},
leftFieldsWidth() {
return this.leftFields.reduce(
(value, field) => this.getFieldWidth(field.id) + value,
0
)
},
leftWidth() {
return this.leftFieldsWidth + this.gridViewRowDetailsWidth
},
...mapGetters({
allRows: 'view/grid/getAllRows',
rows: 'view/grid/getRows',
count: 'view/grid/getCount',
rowHeight: 'view/grid/getRowHeight',
rowsTop: 'view/grid/getRowsTop',
placeholderHeight: 'view/grid/getPlaceholderHeight',
fieldOptions: 'view/grid/getAllFieldOptions',
}),
},
watch: {
// The field options contain the widths of the field. Every time one of the values
// changes we need to recalculate all the widths.
fieldOptions: {
deep: true,
handler(value) {
this.calculateWidths(this.primary, this.fields, value)
handler() {
// When the field options have changed, it could be that the width of the
// fields have changed and in that case we want to update the scrollbars.
this.fieldsUpdated()
},
},
// If a field is added or removed we need to recalculate all the widths.
fields(value) {
this.calculateWidths(this.primary, this.fields, this.fieldOptions)
fields() {
// When a field is added or removed, we want to update the scrollbars.
this.fieldsUpdated()
},
},
created() {
// We have to calculate the widths when the component is created so that we can
// render the page properly on the server side.
this.calculateWidths(this.primary, this.fields, this.fieldOptions)
// When the grid view is created we want to update the scrollbars.
this.fieldsUpdated()
},
beforeMount() {
this.$bus.$on('field-deleted', this.fieldDeleted)
@ -449,18 +219,18 @@ export default {
}
},
/**
* This method is called from the parent component when the data in the view has
* been reset. This can for example happen when a user filters.
* Is called when anything related to a field has changed and in that case we want
* to update the scrollbars.
*/
async refresh() {
await this.$store.dispatch('view/grid/visibleByScrollTop', {
scrollTop: this.$refs.rightBody.scrollTop,
windowHeight: this.$refs.rightBody.clientHeight,
})
this.$nextTick(() => {
fieldsUpdated() {
if (this.$refs.scrollbars) {
this.$refs.scrollbars.update()
})
}
},
/**
* Called when a cell value has been updated. This can for example happen via the
* row edit modal or when editing a cell directly in the grid.
*/
async updateValue({ field, row, value, oldValue }) {
try {
await this.$store.dispatch('view/grid/updateValue', {
@ -497,9 +267,26 @@ export default {
overrides,
})
},
/**
* This method is called by the Scrollbars component and should return the element
* that handles the the horizontal scrolling.
*/
getHorizontalScrollbarElement() {
return this.$refs.right.$el
},
/**
* This method is called by the Scrollbars component and should return the element
* that handles the the vertical scrolling.
*/
getVerticalScrollbarElement() {
return this.$refs.right.$refs.body
},
/**
* Called when a user scrolls without using the scrollbar.
*/
scroll(pixelY, pixelX) {
const $rightBody = this.$refs.rightBody
const $right = this.$refs.right
const $rightBody = this.$refs.right.$refs.body
const $right = this.$refs.right.$el
const top = $rightBody.scrollTop + pixelY
const left = $right.scrollLeft + pixelX
@ -509,101 +296,33 @@ export default {
this.$refs.scrollbars.update()
},
/**
* Called when the user scrolls vertically. The scroll offset of both the left and
* right section must be updated and we want might need to fetch new rows which
* is handled by the grid view store.
*/
verticalScroll(top) {
this.$refs.leftBody.scrollTop = top
this.$refs.rightBody.scrollTop = top
this.$refs.left.$refs.body.scrollTop = top
this.$refs.right.$refs.body.scrollTop = top
this.$store.dispatch('view/grid/fetchByScrollTopDelayed', {
gridId: this.view.id,
scrollTop: this.$refs.rightBody.scrollTop,
windowHeight: this.$refs.rightBody.clientHeight,
scrollTop: this.$refs.left.$refs.body.scrollTop,
windowHeight: this.$refs.left.$refs.body.clientHeight,
})
},
/**
* Called when the user scrolls horizontally. If the user scrolls we might want to
* show a shadow next to the left section because that one has a fixed position.
*/
horizontalScroll(left) {
const $right = this.$refs.right
const $right = this.$refs.right.$el
const $divider = this.$refs.divider
const canScroll = $right.scrollWidth > $right.clientWidth
$divider.classList.toggle('shadow', canScroll && left > 0)
$right.scrollLeft = left
},
/**
* Calculates the widths of all fields, left side, right side and place holder
* positions and returns the values in an object.
*/
getCalculatedWidths(primary, fields, fieldOptions) {
const getFieldWidth = (fieldId) => {
const hasFieldOptions = Object.prototype.hasOwnProperty.call(
fieldOptions,
fieldId
)
if (hasFieldOptions && fieldOptions[fieldId].hidden) {
return 0
}
return hasFieldOptions ? fieldOptions[fieldId].width : 200
}
// Calculate the widths left side of the grid view. This is the sticky side that
// contains the primary field and ids.
const leftReserved = 60
const leftFieldsOnly = getFieldWidth(primary.id)
const left = leftFieldsOnly + leftReserved
// Calculate the widths of the right side that contains all the other fields.
const rightAdd = 100
const rightReserved = 100
const rightFieldsOnly = fields.reduce(
(value, field) => getFieldWidth(field.id) + value,
0
)
const right = rightFieldsOnly + rightAdd + rightReserved
// Calculate the left positions of the placeholder columns. These are the gray
// vertical lines that are always visible, even when the data hasn't loaded yet.
let last = 0
const placeholderPositions = {}
fields.forEach((field) => {
last += getFieldWidth(field.id)
placeholderPositions[field.id] = last
})
const fieldWidths = {}
fieldWidths[primary.id] = getFieldWidth(primary.id)
fields.forEach((field) => {
fieldWidths[field.id] = getFieldWidth(field.id)
})
return {
left,
leftReserved,
leftFieldsOnly,
right,
rightReserved,
rightAdd,
rightFieldsOnly,
placeholderPositions,
fields: fieldWidths,
}
},
/**
* This method is called when the fieldOptions or fields changes. The reason why we
* don't have smaller methods that are called from the template to calculate the
* widths is that because that would quickly result in thousands of function calls
* when the smallest things change in the data. This is a speed improving
* workaround.
*/
calculateWidths(primary, fields, fieldOptions) {
_.assign(
this.widths,
this.getCalculatedWidths(primary, fields, fieldOptions)
)
if (this.$refs.scrollbars) {
this.$refs.scrollbars.update()
}
},
async addRow(before = null) {
try {
await this.$store.dispatch('view/grid/create', {
@ -634,6 +353,37 @@ export default {
this.addRow(nextRow)
},
async deleteRow(row) {
try {
this.$refs.rowContext.hide()
// We need a small helper function that calculates the current scrollTop because
// the delete action will recalculate the visible scroll range and buffer.
const getScrollTop = () => this.$refs.left.$refs.body.scrollTop
await this.$store.dispatch('view/grid/delete', {
table: this.table,
grid: this.view,
row,
getScrollTop,
})
} catch (error) {
notifyIf(error, 'row')
}
},
setRowHover(row, value) {
// Sometimes the mouseleave is not triggered, but because you can hover only one
// row at a time we can remember which was hovered last and set the hover state to
// false if it differs.
if (this.lastHoveredRow !== null && this.lastHoveredRow.id !== row.id) {
this.$store.dispatch('view/grid/setRowHover', {
row: this.lastHoveredRow,
value: false,
})
this.lastHoveredRow = null
}
this.$store.dispatch('view/grid/setRowHover', { row, value })
this.lastHoveredRow = row
},
showRowContext(event, row) {
this.selectedRow = row
this.$refs.rowContext.toggle(
@ -646,30 +396,36 @@ export default {
0
)
},
async deleteRow(row) {
try {
this.$refs.rowContext.hide()
// We need a small helper function that calculates the current scrollTop because
// the delete action will recalculate the visible scroll range and buffer.
const getScrollTop = () => this.$refs.leftBody.scrollTop
await this.$store.dispatch('view/grid/delete', {
table: this.table,
grid: this.view,
row,
getScrollTop,
})
} catch (error) {
notifyIf(error, 'row')
/**
* When the modal hides and the related row does not match the filters anymore it
* must be deleted.
*/
rowEditModalHidden({ row }) {
// It could be that the row is not in the buffer anymore and in that case we also
// don't need to refresh the row.
if (
row === undefined ||
!Object.prototype.hasOwnProperty.call(row, 'id')
) {
return
}
this.$store.dispatch('view/grid/refreshRow', {
grid: this.view,
fields: this.fields,
primary: this.primary,
row,
getScrollTop: () => this.$refs.left.$refs.body.scrollTop,
})
},
/**
* When a field is selected we want to make sure it is visible in the viewport, so
* When a cell is selected we want to make sure it is visible in the viewport, so
* we might need to scroll a little bit.
*/
selectedField(field, { component, row }) {
selectedCell({ component, row, field }) {
const element = component.$el
const verticalContainer = this.$refs.rightBody
const horizontalContainer = this.$refs.right
const verticalContainer = this.$refs.right.$refs.body
const horizontalContainer = this.$refs.right.$el
const verticalContainerRect = verticalContainer.getBoundingClientRect()
const verticalContainerHeight = verticalContainer.clientHeight
@ -719,22 +475,32 @@ export default {
this.$store.dispatch('view/grid/addRowSelectedBy', { row, field })
},
unselectedField(field, { row }) {
this.$store.dispatch('view/grid/removeRowSelectedBy', {
grid: this.view,
fields: this.fields,
primary: this.primary,
row,
field,
getScrollTop: () => this.$refs.leftBody.scrollTop,
/**
* When a cell is unselected need to change the selected state of the row.
*/
unselectedCell({ row, field }) {
// We want to change selected state of the row on the next tick because if another
// cell within a row is selected, we want to wait for that selected state tot
// change. This will make sure that the row is stays selected.
this.$nextTick(() => {
this.$store.dispatch('view/grid/removeRowSelectedBy', {
grid: this.view,
fields: this.fields,
primary: this.primary,
row,
field,
getScrollTop: () => this.$refs.left.$refs.body.scrollTop,
})
})
},
/**
* This method is called when the next field must be selected. This can for example
* happen when the tab key is pressed. It tries to find the next field and will
* select that one.
* This method is called when the next cell must be selected. This can for example
* happen when the tab key is pressed. It tries to find the next field based on the
* direction and will select that one.
*/
selectNextField(row, field, fields, primary, direction = 'next') {
selectNextCell({ row, field, direction = 'next' }) {
const fields = this.fields
const primary = this.primary
let nextFieldId = -1
let nextRowId = -1
@ -784,51 +550,23 @@ export default {
return
}
const current = this.$refs['row-' + row.id + '-field-' + field.id]
const next = this.$refs['row-' + nextRowId + '-field-' + nextFieldId]
if (next === undefined || current === undefined) {
return
}
current[0].unselect()
next[0].select()
},
setRowHover(row, value) {
// Sometimes the mouseleave is not triggered, but because you can hover only one
// row at a time we can remember which was hovered last and set the hover state to
// false if it differs.
if (this.lastHoveredRow !== null && this.lastHoveredRow.id !== row.id) {
this.$store.dispatch('view/grid/setRowHover', {
row: this.lastHoveredRow,
value: false,
})
this.lastHoveredRow = true
}
this.$store.dispatch('view/grid/setRowHover', { row, value })
this.lastHoveredRow = row
this.$store.dispatch('view/grid/setSelectedCell', {
rowId: nextRowId,
fieldId: nextFieldId,
})
},
/**
* When the modal hides and the related row does not match the filters anymore it
* must be deleted.
* This method is called from the parent component when the data in the view has
* been reset. This can for example happen when a user creates or updates a filter
* or wants to sort on a field.
*/
rowEditModalHidden({ row }) {
// It could be that the row is not in the buffer anymore and in that case we also
// don't need to refresh the row.
if (
row === undefined ||
!Object.prototype.hasOwnProperty.call(row, 'id')
) {
return
}
this.$store.dispatch('view/grid/refreshRow', {
grid: this.view,
fields: this.fields,
primary: this.primary,
row,
getScrollTop: () => this.$refs.leftBody.scrollTop,
async refresh() {
await this.$store.dispatch('view/grid/visibleByScrollTop', {
scrollTop: this.$refs.right.$refs.body.scrollTop,
windowHeight: this.$refs.right.$refs.body.clientHeight,
})
this.$nextTick(() => {
this.$refs.scrollbars.update()
})
},
},

View file

@ -0,0 +1,153 @@
<template functional>
<div
ref="wrapper"
class="grid-view__column"
:style="data.style"
@click="$options.methods.select($event, parent, props.field.id)"
>
<component
:is="$options.methods.getFunctionalComponent(parent, props)"
v-if="
!parent.isCellSelected(props.field.id) &&
// It could happen that the selected component needs to be alive in order to
// finish a task. This is for example the case when still uploading files. The
// alive list contains the field ids that must be kept alive. Once finished it
// is removed from that list.
!parent.alive.includes(props.field.id)
"
ref="unselectedField"
:field="props.field"
:value="props.row['field_' + props.field.id]"
:state="props.state"
/>
<component
:is="$options.methods.getComponent(parent, props)"
v-else
ref="selectedField"
:field="props.field"
:value="props.row['field_' + props.field.id]"
:selected="parent.isCellSelected(props.field.id)"
@update="(...args) => $options.methods.update(listeners, props, ...args)"
@edit="(...args) => $options.methods.edit(listeners, props, ...args)"
@unselect="$options.methods.unselect(parent, props)"
@selected="$options.methods.selected(listeners, props, $event)"
@unselected="$options.methods.unselected(listeners, props, $event)"
@selectPrevious="
$options.methods.selectNext(listeners, props, 'previous')
"
@selectNext="$options.methods.selectNext(listeners, props, 'next')"
@selectAbove="$options.methods.selectNext(listeners, props, 'above')"
@selectBelow="$options.methods.selectNext(listeners, props, 'below')"
@add-keep-alive="parent.addKeepAlive(props.field.id)"
@remove-keep-alive="parent.removeKeepAlive(props.field.id)"
/>
</div>
</template>
<script>
export default {
methods: {
/**
* Returns the functional component related to the field type. Functional
* components are much faster then regular components because they don't have a
* state. Unselected cells renders this functional component to improve speed
* because that will give the user a better experience. Once the user clicks on the
* cell, it is replaced with a the real field component which has the ability to
* change the data.
*/
getFunctionalComponent(parent, props) {
return parent.$registry
.get('field', props.field.type)
.getFunctionalGridViewFieldComponent()
},
/**
* Returns the component related to the field type. This component will only be
* rendered when the user has selected the cell. It will be used to edit the value.
*/
getComponent(parent, props) {
return parent.$registry
.get('field', props.field.type)
.getGridViewFieldComponent()
},
/**
* If the grid field component emits an update event then this method will be
* called which will add forward the event to the parent components which will
* eventually update the value.
*/
update(listeners, props, value, oldValue) {
if (listeners.update) {
listeners.update({
row: props.row,
field: props.field,
value,
oldValue,
})
}
},
/**
* 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
* the value still matches the filters.
*/
edit(listeners, props, value, oldValue) {
if (listeners.edit) {
listeners.edit({
row: props.row,
field: props.field,
value,
oldValue,
})
}
},
/**
* When the user clicks on the cell it must be selected. We can only change that
* state by calling the parent `selectCell` method.
*/
select(event, parent, fieldId) {
event.preventFieldCellUnselect = true
parent.selectCell(fieldId)
},
/**
* Called when the cell field type component needs to cell to be unselected.
*/
unselect(parent, props) {
if (parent.isCellSelected(props.field.id)) {
parent.selectCell(-1, -1)
}
},
/**
* Called after the field type component is selected.
*/
selected(listeners, props, event) {
if (listeners.selected) {
event.row = props.row
event.field = props.field
listeners.selected(event)
}
},
/**
* Called after the field type component is unselected.
*/
unselected(listeners, props, event) {
if (listeners.unselected) {
event.row = props.row
event.field = props.field
listeners.unselected(event)
}
},
/**
* Called when the field type component want to select to next cell. This for
* example happens when the user presses an arrow key.
*/
selectNext(listeners, props, direction) {
if (listeners['select-next']) {
listeners['select-next']({
row: props.row,
field: props.field,
direction,
})
}
},
},
}
</script>

View file

@ -1,234 +0,0 @@
<template>
<div ref="wrapper" class="grid-view__column" @click="select($event)">
<component
:is="getFieldComponent(field.type)"
ref="field"
:field="field"
:value="row['field_' + field.id]"
:selected="selected"
@update="update"
@edit="edit"
@select="$refs.wrapper.click()"
/>
</div>
</template>
<script>
import { isElement } from '@baserow/modules/core/utils/dom'
import { copyToClipboard } from '@baserow/modules/database/utils/clipboard'
export default {
name: 'GridViewField',
props: {
field: {
type: Object,
required: true,
},
row: {
type: Object,
required: true,
},
},
data() {
return {
/**
* Indicates whether the field is selected.
*/
selected: false,
/**
* Timestamp of the last the time the user clicked on the field. We need this to
* check if it was double clicked.
*/
clickTimestamp: null,
}
},
/**
* Because the component can be destroyed if it moves out of the viewport we might
* need to take some action if the the component is in a selected state.
*/
beforeDestroy() {
if (this.selected) {
this.unselect()
}
},
methods: {
getFieldComponent(type) {
return this.$registry.get('field', type).getGridViewFieldComponent()
},
/**
* If the grid field component emits an update event this method will be called
* which will actually update the value via the store.
*/
update(value, oldValue) {
this.$emit('update', {
row: this.row,
field: this.field,
value,
oldValue,
})
},
/**
* 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
* the value still matches the filters.
*/
edit(value, oldValue) {
this.$emit('edit', {
row: this.row,
field: this.field,
value,
oldValue,
})
},
/**
* Method that is called when a user clicks on the grid field. It wil
* @TODO improve speed somehow, maybe with the fastclick library.
*/
select(event) {
const timestamp = new Date().getTime()
if (this.selected) {
// If the field is already selected we will check if the click is a doubleclick
// if it was within 200 ms. The double click event can be useful for components
// because they might want to change the editing state.
if (
this.clickTimestamp !== null &&
timestamp - this.clickTimestamp < 200
) {
this.$refs.field.doubleClick(event)
}
} else {
// If the field is not yet selected we can change the state to selected.
this.selected = true
this.$nextTick(() => {
// Call the select method on the next tick because we want to wait for all
// changes to have rendered.
this.$refs.field.select(event)
})
// Register a body click event listener so that we can detect if a user has
// clicked outside the field. If that happens we want to unselect the field and
// possibly save the value.
this.$el.clickOutsideEvent = (event) => {
if (
// Check if the column is still selected.
this.selected &&
// Check if the event has the 'preventFieldCellUnselect' attribute which
// if true should prevent the field from being unselected.
!(
'preventFieldCellUnselect' in event &&
event.preventFieldCellUnselect
) &&
// If the click was outside the column element.
!isElement(this.$el, event.target) &&
// If the child field allows to unselect when clicked outside.
this.$refs.field.canUnselectByClickingOutside(event)
) {
this.unselect()
}
}
document.body.addEventListener('click', this.$el.clickOutsideEvent)
// Event that is called when a key is pressed while the field is selected.
this.$el.keyDownEvent = (event) => {
// When for example a related modal is open all the key combinations must be
// ignored because the focus is not in the cell.
if (!this.$refs.field.canKeyDown(event)) {
return
}
// If the tab or arrow keys are pressed we want to select the next field. This
// is however out of the scope of this component so we emit the selectNext
// event that the GridView can handle.
const { keyCode, ctrlKey, metaKey } = event
const arrowKeysMapping = {
37: 'selectPrevious',
38: 'selectAbove',
39: 'selectNext',
40: 'selectBelow',
}
if (
Object.keys(arrowKeysMapping).includes(keyCode.toString()) &&
this.$refs.field.canSelectNext(event)
) {
event.preventDefault()
this.$emit(arrowKeysMapping[keyCode])
}
if (keyCode === 9 && this.$refs.field.canSelectNext(event)) {
event.preventDefault()
this.$emit(event.shiftKey ? 'selectPrevious' : 'selectNext')
}
// Copie the value to the clipboard if ctrl/cmd + c is pressed.
if (
(ctrlKey || metaKey) &&
keyCode === 67 &&
this.$refs.field.canCopy(event)
) {
const rawValue = this.row['field_' + this.field.id]
const value = this.$registry
.get('field', this.field.type)
.prepareValueForCopy(this.field, rawValue)
copyToClipboard(value)
}
// Removes the value if the backspace/delete key is pressed.
if (
(keyCode === 46 || keyCode === 8) &&
this.$refs.field.canEmpty(event)
) {
event.preventDefault()
const value = this.$registry
.get('field', this.field.type)
.getEmptyValue(this.field)
const oldValue = this.row['field_' + this.field.id]
if (value !== oldValue) {
this.update(value, oldValue)
}
}
}
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) => {
if (!this.$refs.field.canPaste(event)) {
return
}
const value = this.$registry
.get('field', this.field.type)
.prepareValueForPaste(this.field, event.clipboardData)
const oldValue = this.row['field_' + this.field.id]
if (value !== oldValue) {
this.update(value, oldValue)
}
}
document.addEventListener('paste', this.$el.pasteEvent)
// Emit the selected event so that the parent component can take an action like
// making sure that the element fits in the viewport.
this.$emit('selected', {
component: this,
row: this.row,
field: this.field,
})
}
this.clickTimestamp = timestamp
},
unselect() {
this.$refs.field.beforeUnSelect()
this.$nextTick(() => {
this.selected = false
this.$emit('unselected', {
row: this.row,
field: this.field,
})
})
document.body.removeEventListener('click', this.$el.clickOutsideEvent)
document.body.removeEventListener('keydown', this.$el.keyDownEvent)
document.removeEventListener('paste', this.$el.pasteEvent)
},
},
}
</script>

View file

@ -8,6 +8,7 @@
'grid-view__column--sorted':
view.sortings.findIndex((sort) => sort.field === field.id) !== -1,
}"
:style="{ width: width + 'px' }"
>
<div
class="grid-view__description"
@ -90,18 +91,29 @@
</a>
</li>
</FieldContext>
<slot></slot>
<GridViewFieldWidthHandle
v-if="includeFieldWidthHandles"
class="grid-view__description-width"
:grid="view"
:field="field"
:width="width"
></GridViewFieldWidthHandle>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import { notifyIf } from '@baserow/modules/core/utils/error'
import FieldContext from '@baserow/modules/database/components/field/FieldContext'
import GridViewFieldWidthHandle from '@baserow/modules/database/components/view/grid/GridViewFieldWidthHandle'
import gridViewHelpers from '@baserow/modules/database/mixins/gridViewHelpers'
export default {
name: 'GridViewFieldType',
components: { FieldContext },
components: { FieldContext, GridViewFieldWidthHandle },
mixins: [gridViewHelpers],
props: {
table: {
type: Object,
@ -115,8 +127,15 @@ export default {
type: Object,
required: true,
},
includeFieldWidthHandles: {
type: Boolean,
required: false,
},
},
computed: {
width() {
return this.getFieldWidth(this.field.id)
},
canFilter() {
const filters = Object.values(this.$registry.getAll('viewFilter'))
for (const type in filters) {
@ -126,6 +145,9 @@ export default {
}
return false
},
...mapGetters({
fieldOptions: 'view/grid/getAllFieldOptions',
}),
},
methods: {
async createFilter(event, view, field) {

View file

@ -0,0 +1,80 @@
<template>
<div class="grid-view__head">
<div
v-if="includeRowDetails"
class="grid-view__column"
:style="{ width: gridViewRowDetailsWidth + 'px' }"
></div>
<GridViewFieldType
v-for="field in fields"
:key="'field-type-' + field.id"
:table="table"
:view="view"
:field="field"
:filters="view.filters"
:include-field-width-handles="includeFieldWidthHandles"
@refresh="$emit('refresh', $event)"
></GridViewFieldType>
<div
v-if="includeAddField"
class="grid-view__column"
:style="{ width: 100 + 'px' }"
>
<a
ref="createFieldContextLink"
class="grid-view__add-column"
@click="$refs.createFieldContext.toggle($refs.createFieldContextLink)"
>
<i class="fas fa-plus"></i>
</a>
<CreateFieldContext
ref="createFieldContext"
:table="table"
></CreateFieldContext>
</div>
</div>
</template>
<script>
import CreateFieldContext from '@baserow/modules/database/components/field/CreateFieldContext'
import GridViewFieldType from '@baserow/modules/database/components/view/grid/GridViewFieldType'
import gridViewHelpers from '@baserow/modules/database/mixins/gridViewHelpers'
export default {
name: 'GridViewHead',
components: {
GridViewFieldType,
CreateFieldContext,
},
mixins: [gridViewHelpers],
props: {
fields: {
type: Array,
required: true,
},
table: {
type: Object,
required: true,
},
view: {
type: Object,
required: true,
},
includeFieldWidthHandles: {
type: Boolean,
required: false,
default: () => false,
},
includeRowDetails: {
type: Boolean,
required: false,
default: () => false,
},
includeAddField: {
type: Boolean,
required: false,
default: () => false,
},
},
}
</script>

View file

@ -0,0 +1,65 @@
<template>
<div
class="grid-view__placeholder"
:style="{
height: placeholderHeight + 'px',
width: placeholderWidth + 'px',
}"
>
<div
v-for="(value, index) in placeholderPositions"
:key="'placeholder-column-' + index"
class="grid-view__placeholder-column"
:style="{ left: value - 1 + 'px' }"
></div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import gridViewHelpers from '@baserow/modules/database/mixins/gridViewHelpers'
export default {
name: 'GridViewPlaceholder',
mixins: [gridViewHelpers],
props: {
fields: {
type: Array,
required: true,
},
includeRowDetails: {
type: Boolean,
required: true,
},
},
computed: {
/**
* Calculate the left positions of the placeholder columns. These are the gray
* vertical lines that are always visible, even when the data hasn't loaded yet.
*/
placeholderPositions() {
let last = 0
const placeholderPositions = {}
this.fields.forEach((field) => {
last += this.getFieldWidth(field.id)
placeholderPositions[field.id] = last
})
return placeholderPositions
},
placeholderWidth() {
let width = this.fields.reduce(
(value, field) => this.getFieldWidth(field.id) + value,
0
)
if (this.includeRowDetails) {
width += this.gridViewRowDetailsWidth
}
return width
},
...mapGetters({
placeholderHeight: 'view/grid/getPlaceholderHeight',
}),
},
}
</script>

View file

@ -0,0 +1,127 @@
<template>
<div
class="grid-view__row"
:class="{
'grid-view__row--selected': row._.selectedBy.length > 0,
'grid-view__row--loading': row._.loading,
'grid-view__row--hover': row._.hover,
'grid-view__row--warning': !row._.matchFilters || !row._.matchSortings,
}"
@mouseover="$emit('row-hover', { row, value: true })"
@mouseleave="$emit('row-hover', { row, value: false })"
@contextmenu.prevent="$emit('row-context', { row, event: $event })"
>
<template v-if="includeRowDetails">
<div
v-if="!row._.matchFilters || !row._.matchSortings"
class="grid-view__row-warning"
>
<template v-if="!row._.matchFilters">
Row does not match filters
</template>
<template v-else-if="!row._.matchSortings">Row has moved</template>
</div>
<div
class="grid-view__column"
:style="{ width: gridViewRowDetailsWidth + 'px' }"
>
<div class="grid-view__row-info">
<div class="grid-view__row-count" :title="row.id">
{{ row.id }}
</div>
<a class="grid-view__row-more" @click="$emit('edit-modal', row)">
<i class="fas fa-expand"></i>
</a>
</div>
</div>
</template>
<!--
Somehow re-declaring all the events instead of using v-on="$listeners" speeds
everything up because the rows don't need to be updated everytime a new one is
rendered, which happens a lot when scrolling.
-->
<GridViewCell
v-for="field in fields"
:key="'row-field-' + row.id + '-' + field.id"
:field="field"
:row="row"
:state="state"
:style="{ width: fieldWidths[field.id] + 'px' }"
@update="$emit('update', $event)"
@edit="$emit('edit', $event)"
@select="$emit('select', $event)"
@unselect="$emit('unselect', $event)"
@selected="$emit('selected', $event)"
@unselected="$emit('unselected', $event)"
@select-next="$emit('select-next', $event)"
></GridViewCell>
</div>
</template>
<script>
import GridViewCell from '@baserow/modules/database/components/view/grid/GridViewCell'
import gridViewHelpers from '@baserow/modules/database/mixins/gridViewHelpers'
export default {
name: 'GridViewRow',
components: { GridViewCell },
mixins: [gridViewHelpers],
props: {
row: {
type: Object,
required: true,
},
fields: {
type: Array,
required: true,
},
fieldWidths: {
type: Object,
required: true,
},
includeRowDetails: {
type: Boolean,
required: false,
default: () => false,
},
},
data() {
return {
// The state can be used by functional components to make changes to the dom.
// This is for example used by the functional file field component to enable the
// drop effect without having the cell selected.
state: {},
// A list containing field id's of field cells that must not be converted to the
// functional component even though the user has selected another cell. This is
// for example used by the file field to finish the uploading task if the user
// has selected another cell while uploading.
alive: [],
}
},
methods: {
isCellSelected(fieldId) {
return this.row._.selected && this.row._.selectedFieldId === fieldId
},
selectCell(fieldId, rowId = this.row.id) {
this.$store.dispatch('view/grid/setSelectedCell', {
rowId,
fieldId,
})
},
setState(value) {
this.state = value
},
addKeepAlive(fieldId) {
if (!this.alive.includes(fieldId)) {
this.alive.push(fieldId)
}
},
removeKeepAlive(fieldId) {
const index = this.alive.findIndex((id) => id === fieldId)
if (index > -1) {
this.alive.splice(index, 1)
}
},
},
}
</script>

View file

@ -0,0 +1,56 @@
<template>
<div class="grid-view__row" :style="{ width: width + 'px' }">
<div class="grid-view__column" :style="{ width: width + 'px' }">
<a
class="grid-view__add-row"
:class="{ hover: addHover }"
@mouseover="setHover(true)"
@mouseleave="setHover(false)"
@click="$emit('add-row')"
>
<i v-if="includeRowDetails" class="fas fa-plus"></i>
</a>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import gridViewHelpers from '@baserow/modules/database/mixins/gridViewHelpers'
export default {
name: 'GridViewRowAdd',
mixins: [gridViewHelpers],
props: {
fields: {
type: Array,
required: true,
},
includeRowDetails: {
type: Boolean,
required: true,
},
},
computed: {
width() {
let width = this.fields.reduce(
(value, field) => this.getFieldWidth(field.id) + value,
0
)
if (this.includeRowDetails) {
width += this.gridViewRowDetailsWidth
}
return width
},
...mapGetters({
addHover: 'view/grid/getAddRowHover',
}),
},
methods: {
setHover(value) {
this.$store.dispatch('view/grid/setAddRowHover', value)
},
},
}
</script>

View file

@ -0,0 +1,61 @@
<template>
<div
class="grid-view__rows"
:style="{ transform: `translateY(${rowsTop}px)` }"
>
<GridViewRow
v-for="row in rows"
:key="'row-' + '-' + row.id"
:row="row"
:fields="fields"
:field-widths="fieldWidths"
:include-row-details="includeRowDetails"
v-on="$listeners"
></GridViewRow>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import GridViewRow from '@baserow/modules/database/components/view/grid/GridViewRow'
import gridViewHelpers from '@baserow/modules/database/mixins/gridViewHelpers'
export default {
name: 'GridViewRows',
components: { GridViewRow },
mixins: [gridViewHelpers],
props: {
fields: {
type: Array,
required: true,
},
table: {
type: Object,
required: true,
},
view: {
type: Object,
required: true,
},
includeRowDetails: {
type: Boolean,
required: false,
default: () => false,
},
},
computed: {
fieldWidths() {
const fieldWidths = {}
this.fields.forEach((field) => {
fieldWidths[field.id] = this.getFieldWidth(field.id)
})
return fieldWidths
},
...mapGetters({
rows: 'view/grid/getRows',
rowsTop: 'view/grid/getRowsTop',
}),
},
}
</script>

View file

@ -0,0 +1,118 @@
<template>
<div>
<div class="grid-view__inner" :style="{ 'min-width': width + 'px' }">
<GridViewHead
:table="table"
:view="view"
:fields="visibleFields"
:include-field-width-handles="includeFieldWidthHandles"
:include-row-details="includeRowDetails"
:include-add-field="includeAddField"
@refresh="$emit('refresh', $event)"
></GridViewHead>
<div ref="body" class="grid-view__body">
<div class="grid-view__body-inner">
<GridViewPlaceholder
:fields="visibleFields"
:include-row-details="includeRowDetails"
></GridViewPlaceholder>
<GridViewRows
:table="table"
:view="view"
:fields="visibleFields"
:include-row-details="includeRowDetails"
v-on="$listeners"
></GridViewRows>
<GridViewRowAdd
:fields="visibleFields"
:include-row-details="includeRowDetails"
v-on="$listeners"
></GridViewRowAdd>
</div>
</div>
<div class="grid-view__foot">
<slot name="foot"></slot>
</div>
</div>
</div>
</template>
<script>
import GridViewHead from '@baserow/modules/database/components/view/grid/GridViewHead'
import GridViewPlaceholder from '@baserow/modules/database/components/view/grid/GridViewPlaceholder'
import GridViewRows from '@baserow/modules/database/components/view/grid/GridViewRows'
import GridViewRowAdd from '@baserow/modules/database/components/view/grid/GridViewRowAdd'
import gridViewHelpers from '@baserow/modules/database/mixins/gridViewHelpers'
export default {
name: 'GridViewSection',
components: {
GridViewHead,
GridViewPlaceholder,
GridViewRows,
GridViewRowAdd,
},
mixins: [gridViewHelpers],
props: {
fields: {
type: Array,
required: true,
},
table: {
type: Object,
required: true,
},
view: {
type: Object,
required: true,
},
includeFieldWidthHandles: {
type: Boolean,
required: false,
default: () => true,
},
includeRowDetails: {
type: Boolean,
required: false,
default: () => false,
},
includeAddField: {
type: Boolean,
required: false,
default: () => false,
},
},
computed: {
visibleFields() {
return this.fields.filter((field) => {
const exists = Object.prototype.hasOwnProperty.call(
this.fieldOptions,
field.id
)
return !exists || (exists && !this.fieldOptions[field.id].hidden)
})
},
/**
* Calculates the total width of the whole section based on the fields and the
* given options.
*/
width() {
let width = Object.values(this.visibleFields).reduce(
(value, field) => this.getFieldWidth(field.id) + value,
0
)
if (this.includeRowDetails) {
width += this.gridViewRowDetailsWidth
}
// The add button has a width of 100 and we reserve 100 at the right side.
if (this.includeAddField) {
width += 100 + 100
}
return width
},
},
}
</script>

View file

@ -0,0 +1,12 @@
<template functional>
<div class="grid-view__cell">
<div class="grid-field-boolean">
<div
class="grid-field-boolean__checkbox"
:class="{ active: props.value }"
>
<i class="fas fa-check grid-field-boolean__checkbox-icon"></i>
</div>
</div>
</div>
</template>

View file

@ -0,0 +1,51 @@
<template functional>
<div ref="cell" class="grid-view__cell">
<div
class="grid-field-date"
:class="{ 'grid-field-date--has-time': props.field.date_include_time }"
>
<div ref="dateDisplay" class="grid-field-date__date">
{{ $options.methods.getDate(props.field, props.value) }}
</div>
<div
v-if="props.field.date_include_time"
ref="timeDisplay"
class="grid-field-date__time"
>
{{ $options.methods.getTime(props.field, props.value) }}
</div>
</div>
</div>
</template>
<script>
import moment from 'moment'
import {
getDateMomentFormat,
getTimeMomentFormat,
} from '@baserow/modules/database/utils/date'
export default {
name: 'FunctionalGridViewFieldDate',
methods: {
getDate(field, value) {
if (value === null) {
return ''
}
const existing = moment.utc(value || undefined)
const dateFormat = getDateMomentFormat(field.date_format)
return existing.format(dateFormat)
},
getTime(field, value) {
if (value === null) {
return ''
}
const existing = moment.utc(value || undefined)
const timeFormat = getTimeMomentFormat(field.date_time_format)
return existing.format(timeFormat)
},
},
}
</script>

View file

@ -0,0 +1,86 @@
<template functional>
<div
class="grid-view__cell grid-field-file__cell"
@drop.prevent="$options.methods.drop(parent, props, $event)"
@dragover.prevent
@dragenter.prevent="$options.methods.dragEnter(parent, props, $event)"
@dragleave="$options.methods.dragLeave(parent, props, $event)"
>
<div
v-show="Object.prototype.hasOwnProperty.call(props.state, props.field.id)"
class="grid-field-file__dragging"
>
<div>
<i class="grid-field-file__drop-icon fas fa-cloud-upload-alt"></i>
Drop here
</div>
</div>
<ul v-if="Array.isArray(props.value)" class="grid-field-file__list">
<li
v-for="(file, index) in props.value"
:key="file.name + index"
class="grid-field-file__item"
>
<a class="grid-field-file__link">
<img
v-if="file.is_image"
class="grid-field-file__image"
:src="file.thumbnails.tiny.url"
/>
<i
v-else
class="fas grid-field-file__icon"
:class="'fa-' + $options.methods.getIconClass(file.mime_type)"
></i>
</a>
</li>
</ul>
</div>
</template>
<script>
import { mimetype2fa } from '@baserow/modules/core/utils/fontawesome'
export default {
name: 'FunctionalGridViewFieldFile',
methods: {
getIconClass(mimeType) {
return mimetype2fa(mimeType)
},
drop(parent, props, event) {
if (props.readOnly) {
return
}
parent.selectCell(props.field.id)
parent.setState({})
parent.$nextTick(() => {
parent.$refs.selectedField.uploadFiles(event)
})
},
dragEnter(parent, props, event) {
if (props.readOnly) {
return
}
parent.setState({
[props.field.id]: event.target,
})
},
dragLeave(parent, props, event) {
if (props.readOnly) {
return
}
if (
Object.prototype.hasOwnProperty.call(props.state, props.field.id) &&
props.state[props.field.id] === event.target
) {
event.stopPropagation()
event.preventDefault()
parent.setState({})
}
},
},
}
</script>

View file

@ -0,0 +1,21 @@
<template functional>
<div class="grid-view__cell grid-field-link-row__cell">
<div class="grid-field-link-row__list">
<div
v-for="item in props.value"
:key="item.id"
class="grid-field-link-row__item"
>
<span
class="grid-field-link-row__name"
:class="{
'grid-field-link-row__name--unnamed':
item.value === null || item.value === '',
}"
>
{{ item.value || 'unnamed row ' + item.id }}
</span>
</div>
</div>
</div>
</template>

View file

@ -0,0 +1,5 @@
<template functional>
<div class="grid-view__cell grid-field-long-text__cell">
<div class="grid-field-long-text">{{ props.value }}</div>
</div>
</template>

View file

@ -0,0 +1,5 @@
<template functional>
<div class="grid-view__cell">
<div class="grid-field-number">{{ props.value }}</div>
</div>
</template>

View file

@ -0,0 +1,13 @@
<template functional>
<div ref="cell" class="grid-view__cell">
<div class="grid-field-single-select">
<div
v-if="props.value"
class="grid-field-single-select__option"
:class="'background-color--' + props.value.color"
>
{{ props.value.value }}
</div>
</div>
</div>
</template>

View file

@ -0,0 +1,5 @@
<template functional>
<div ref="cell" class="grid-view__cell">
<div class="grid-field-text">{{ props.value }}</div>
</div>
</template>

View file

@ -1,5 +1,5 @@
<template>
<div class="grid-view__cell" :class="{ active: selected }">
<div class="grid-view__cell active">
<div class="grid-field-boolean">
<div
class="grid-field-boolean__checkbox"
@ -32,9 +32,6 @@ export default {
document.body.removeEventListener('keydown', this.$el.keydownEvent)
},
toggle(value) {
if (!this.selected) {
return
}
const oldValue = !!value
const newValue = !value
this.$emit('update', newValue, oldValue)

View file

@ -1,8 +1,8 @@
<template>
<div
ref="cell"
class="grid-view__cell"
:class="{ active: selected, editing: editing }"
class="grid-view__cell active"
:class="{ editing: editing }"
@contextmenu="stopContextIfEditing($event)"
>
<div

View file

@ -1,18 +1,14 @@
<template>
<div
class="grid-view__cell"
class="grid-view__cell active"
:class="{
active: selected,
editing: editing,
invalid: editing && !isValid(),
}"
@contextmenu="stopContextIfEditing($event)"
>
<div v-show="!editing" class="grid-field-text">
<template v-if="!selected">{{ value }}</template>
<a v-if="selected" :href="'mailto:' + value" target="_blank">
{{ value }}
</a>
<a :href="'mailto:' + value" target="_blank">{{ value }}</a>
</div>
<template v-if="editing">
<input

View file

@ -21,7 +21,7 @@
class="grid-field-file__item"
>
<a
v-tooltip="selected ? file.visible_name : null"
v-tooltip="file.visible_name"
class="grid-field-file__link"
@click.prevent="showFileModal(index)"
>
@ -44,7 +44,7 @@
>
<div class="grid-field-file__loading"></div>
</li>
<li v-if="selected" class="grid-field-file__item">
<li v-show="selected" class="grid-field-file__item">
<a class="grid-field-file__item-add" @click.prevent="showUploadModal()">
<i class="fas fa-plus"></i>
</a>
@ -102,6 +102,10 @@ export default {
async uploadFiles(event) {
this.dragging = false
// Indicates that this component must not be destroyed even though the user might
// select another cell.
this.$emit('add-keep-alive')
const files = Array.from(event.dataTransfer.files).map((file) => {
return {
id: uuid(),
@ -137,6 +141,9 @@ export default {
const index = this.loadings.findIndex((l) => l.id === id)
this.loadings.splice(index, 1)
// Indicates that this component can be destroyed if it is not selected.
this.$emit('remove-keep-alive')
}
},
select() {
@ -178,10 +185,6 @@ export default {
this.$refs.uploadModal.show(UploadFileUserFileUploadType.getType())
},
showFileModal(index) {
if (!this.selected) {
return
}
this.modalOpen = true
this.$refs.fileModal.show(index)
},

View file

@ -1,8 +1,5 @@
<template>
<div
class="grid-view__cell grid-field-link-row__cell"
:class="{ active: selected }"
>
<div class="grid-view__cell grid-field-link-row__cell active">
<div class="grid-field-link-row__list">
<div
v-for="item in value"

View file

@ -1,8 +1,8 @@
<template>
<div
ref="cell"
class="grid-view__cell grid-field-long-text__cell"
:class="{ active: selected, editing: editing }"
class="grid-view__cell grid-field-long-text__cell active"
:class="{ editing: editing }"
@contextmenu="stopContextIfEditing($event)"
>
<div v-show="!editing" class="grid-field-long-text">{{ value }}</div>

View file

@ -1,8 +1,7 @@
<template>
<div
class="grid-view__cell"
class="grid-view__cell active"
:class="{
active: selected,
editing: editing,
invalid: editing && !isValid(),
}"

View file

@ -1,9 +1,8 @@
<template>
<div ref="cell" class="grid-view__cell" :class="{ active: selected }">
<div ref="cell" class="grid-view__cell active">
<div
ref="dropdownLink"
class="grid-field-single-select"
:class="{ 'grid-field-single-select--selected': selected }"
class="grid-field-single-select grid-field-single-select--selected"
@click="toggleDropdown()"
>
<div
@ -13,13 +12,9 @@
>
{{ value.value }}
</div>
<i
v-if="selected"
class="fa fa-caret-down grid-field-single-select__icon"
></i>
<i class="fa fa-caret-down grid-field-single-select__icon"></i>
</div>
<FieldSingleSelectDropdown
v-if="selected"
ref="dropdown"
:value="valueId"
:options="field.select_options"
@ -48,10 +43,6 @@ export default {
},
methods: {
toggleDropdown(value, query) {
if (!this.selected) {
return
}
this.$refs.dropdown.toggle(this.$refs.dropdownLink, value, query)
},
hideDropdown() {
@ -85,7 +76,6 @@ export default {
document.body.addEventListener('keydown', this.$el.keydownEvent)
},
beforeUnSelect() {
this.hideDropdown()
document.body.removeEventListener('keydown', this.$el.keydownEvent)
},
canSelectNext() {

View file

@ -1,13 +1,13 @@
<template>
<div
ref="cell"
class="grid-view__cell"
:class="{ active: selected, editing: editing }"
class="grid-view__cell active"
:class="{ editing: editing }"
@contextmenu="stopContextIfEditing($event)"
>
<div v-show="!editing" class="grid-field-text">{{ value }}</div>
<div v-if="!editing" class="grid-field-text">{{ value }}</div>
<input
v-if="editing"
v-else
ref="input"
v-model="copy"
type="text"

View file

@ -1,16 +1,14 @@
<template>
<div
class="grid-view__cell"
class="grid-view__cell active"
:class="{
active: selected,
editing: editing,
invalid: editing && !isValid(),
}"
@contextmenu="stopContextIfEditing($event)"
>
<div v-show="!editing" class="grid-field-text">
<template v-if="!selected">{{ value }}</template>
<a v-if="selected" :href="value" target="_blank">{{ value }}</a>
<a :href="value" target="_blank">{{ value }}</a>
</div>
<template v-if="editing">
<input

View file

@ -10,16 +10,25 @@ import FieldDateSubForm from '@baserow/modules/database/components/field/FieldDa
import FieldLinkRowSubForm from '@baserow/modules/database/components/field/FieldLinkRowSubForm'
import FieldSingleSelectSubForm from '@baserow/modules/database/components/field/FieldSingleSelectSubForm'
import GridViewFieldText from '@baserow/modules/database/components/view/grid/GridViewFieldText'
import GridViewFieldLongText from '@baserow/modules/database/components/view/grid/GridViewFieldLongText'
import GridViewFieldURL from '@baserow/modules/database/components/view/grid/GridViewFieldURL'
import GridViewFieldEmail from '@baserow/modules/database/components/view/grid/GridViewFieldEmail'
import GridViewFieldLinkRow from '@baserow/modules/database/components/view/grid/GridViewFieldLinkRow'
import GridViewFieldNumber from '@baserow/modules/database/components/view/grid/GridViewFieldNumber'
import GridViewFieldBoolean from '@baserow/modules/database/components/view/grid/GridViewFieldBoolean'
import GridViewFieldDate from '@baserow/modules/database/components/view/grid/GridViewFieldDate'
import GridViewFieldFile from '@baserow/modules/database/components/view/grid/GridViewFieldFile'
import GridViewFieldSingleSelect from '@baserow/modules/database/components/view/grid/GridViewFieldSingleSelect'
import GridViewFieldText from '@baserow/modules/database/components/view/grid/fields/GridViewFieldText'
import GridViewFieldLongText from '@baserow/modules/database/components/view/grid/fields/GridViewFieldLongText'
import GridViewFieldURL from '@baserow/modules/database/components/view/grid/fields/GridViewFieldURL'
import GridViewFieldEmail from '@baserow/modules/database/components/view/grid/fields/GridViewFieldEmail'
import GridViewFieldLinkRow from '@baserow/modules/database/components/view/grid/fields/GridViewFieldLinkRow'
import GridViewFieldNumber from '@baserow/modules/database/components/view/grid/fields/GridViewFieldNumber'
import GridViewFieldBoolean from '@baserow/modules/database/components/view/grid/fields/GridViewFieldBoolean'
import GridViewFieldDate from '@baserow/modules/database/components/view/grid/fields/GridViewFieldDate'
import GridViewFieldFile from '@baserow/modules/database/components/view/grid/fields/GridViewFieldFile'
import GridViewFieldSingleSelect from '@baserow/modules/database/components/view/grid/fields/GridViewFieldSingleSelect'
import FunctionalGridViewFieldText from '@baserow/modules/database/components/view/grid/fields/FunctionalGridViewFieldText'
import FunctionalGridViewFieldLongText from '@baserow/modules/database/components/view/grid/fields/FunctionalGridViewFieldLongText'
import FunctionalGridViewFieldLinkRow from '@baserow/modules/database/components/view/grid/fields/FunctionalGridViewFieldLinkRow'
import FunctionalGridViewFieldNumber from '@baserow/modules/database/components/view/grid/fields/FunctionalGridViewFieldNumber'
import FunctionalGridViewFieldBoolean from '@baserow/modules/database/components/view/grid/fields/FunctionalGridViewFieldBoolean'
import FunctionalGridViewFieldDate from '@baserow/modules/database/components/view/grid/fields/FunctionalGridViewFieldDate'
import FunctionalGridViewFieldFile from '@baserow/modules/database/components/view/grid/fields/FunctionalGridViewFieldFile'
import FunctionalGridViewFieldSingleSelect from '@baserow/modules/database/components/view/grid/fields/FunctionalGridViewFieldSingleSelect'
import RowEditFieldText from '@baserow/modules/database/components/row/RowEditFieldText'
import RowEditFieldLongText from '@baserow/modules/database/components/row/RowEditFieldLongText'
@ -78,6 +87,19 @@ export class FieldType extends Registerable {
)
}
/**
* This functional component should represent an unselect field cell related to the
* value of this type. It will only be used in the grid view and is only for fast
* displaying purposes, not for editing the value. This is because functional
* components are much faster. When a user clicks on the cell it will be replaced
* with the real component.
*/
getFunctionalGridViewFieldComponent() {
throw new Error(
'Not implement error. This method should return a component.'
)
}
/**
* The row edit field should represent a the related row value of this type. It
* will be used in the row edit modal, but can also be used in other forms. It is
@ -274,6 +296,10 @@ export class TextFieldType extends FieldType {
return GridViewFieldText
}
getFunctionalGridViewFieldComponent() {
return FunctionalGridViewFieldText
}
getRowEditFieldComponent() {
return RowEditFieldText
}
@ -323,6 +349,10 @@ export class LongTextFieldType extends FieldType {
return GridViewFieldLongText
}
getFunctionalGridViewFieldComponent() {
return FunctionalGridViewFieldLongText
}
getRowEditFieldComponent() {
return RowEditFieldLongText
}
@ -376,6 +406,10 @@ export class LinkRowFieldType extends FieldType {
return GridViewFieldLinkRow
}
getFunctionalGridViewFieldComponent() {
return FunctionalGridViewFieldLinkRow
}
getRowEditFieldComponent() {
return RowEditFieldLinkRow
}
@ -486,6 +520,10 @@ export class NumberFieldType extends FieldType {
return GridViewFieldNumber
}
getFunctionalGridViewFieldComponent() {
return FunctionalGridViewFieldNumber
}
getRowEditFieldComponent() {
return RowEditFieldNumber
}
@ -610,6 +648,10 @@ export class BooleanFieldType extends FieldType {
return GridViewFieldBoolean
}
getFunctionalGridViewFieldComponent() {
return FunctionalGridViewFieldBoolean
}
getRowEditFieldComponent() {
return RowEditFieldBoolean
}
@ -673,6 +715,10 @@ export class DateFieldType extends FieldType {
return GridViewFieldDate
}
getFunctionalGridViewFieldComponent() {
return FunctionalGridViewFieldDate
}
getRowEditFieldComponent() {
return RowEditFieldDate
}
@ -773,6 +819,10 @@ export class URLFieldType extends FieldType {
return GridViewFieldURL
}
getFunctionalGridViewFieldComponent() {
return FunctionalGridViewFieldText
}
getRowEditFieldComponent() {
return RowEditFieldURL
}
@ -823,6 +873,10 @@ export class EmailFieldType extends FieldType {
return GridViewFieldEmail
}
getFunctionalGridViewFieldComponent() {
return FunctionalGridViewFieldText
}
getRowEditFieldComponent() {
return RowEditFieldEmail
}
@ -873,6 +927,10 @@ export class FileFieldType extends FieldType {
return GridViewFieldFile
}
getFunctionalGridViewFieldComponent() {
return FunctionalGridViewFieldFile
}
getRowEditFieldComponent() {
return RowEditFieldFile
}
@ -984,6 +1042,10 @@ export class SingleSelectFieldType extends FieldType {
return GridViewFieldSingleSelect
}
getFunctionalGridViewFieldComponent() {
return FunctionalGridViewFieldSingleSelect
}
getRowEditFieldComponent() {
return RowEditFieldSingleSelect
}

View file

@ -22,7 +22,9 @@ export default {
delete file.original_name
return file
})
this.$refs.uploadModal.hide()
if (this.$refs.uploadModal) {
this.$refs.uploadModal.hide()
}
const newValue = JSON.parse(JSON.stringify(value))
newValue.push(...files)
this.$emit('update', newValue, value)

View file

@ -1,3 +1,6 @@
import { isElement } from '@baserow/modules/core/utils/dom'
import { copyToClipboard } from '@baserow/modules/database/utils/clipboard'
/**
* A mixin that can be used by a field grid component. It introduces the props that
* will be passed by the GridViewField component and it created methods that are
@ -30,7 +33,171 @@ export default {
required: true,
},
},
data() {
return {
/**
* Timestamp of the last the time the user clicked on the field. We need this to
* check if it was double clicked.
*/
clickTimestamp: null,
}
},
watch: {
/**
* It could happen that the cell is not select, but still being kept alive to
* finish a task. This for example happens when the user selects another cell
* while still uploading a file. When the selected state changes, we do want to
* add and remove the event listeners to prevent conflicts.
*/
selected(value) {
if (value) {
this._select()
} else {
this._beforeUnSelect()
}
},
},
mounted() {
if (this.selected) {
this._select()
}
},
beforeDestroy() {
// It could be that the cell has already been unselected, in that case we don't
// have to before unselect twice.
if (this.selected) {
this._beforeUnSelect()
}
},
methods: {
/**
* Adds all the event listeners related to all the field types, for example when a
* user presses the one of the arrow keys, tab, backspace, double clicks etc. This
* method is not meant to be overwritten.
*/
_select() {
this.$el.clickEvent = (event) => {
const timestamp = new Date().getTime()
if (
this.clickTimestamp !== null &&
timestamp - this.clickTimestamp < 200
) {
this.doubleClick(event)
}
this.clickTimestamp = timestamp
}
this.$el.addEventListener('click', this.$el.clickEvent)
// Register a body click event listener so that we can detect if a user has
// clicked outside the field. If that happens we want to unselect the field and
// possibly save the value.
this.$el.clickOutsideEvent = (event) => {
if (
// Check if the event has the 'preventFieldCellUnselect' attribute which
// if true should prevent the field from being unselected.
!(
'preventFieldCellUnselect' in event &&
event.preventFieldCellUnselect
) &&
// If the click was outside the column element.
!isElement(this.$el, event.target) &&
// If the child field allows to unselect when clicked outside.
this.canUnselectByClickingOutside(event)
) {
this.$emit('unselect')
}
}
document.body.addEventListener('click', this.$el.clickOutsideEvent)
// Event that is called when a key is pressed while the field is selected.
this.$el.keyDownEvent = (event) => {
// When for example a related modal is open all the key combinations must be
// ignored because the focus is not in the cell.
if (!this.canKeyDown(event)) {
return
}
// If the tab or arrow keys are pressed we want to select the next field. This
// is however out of the scope of this component so we emit the selectNext
// event that the GridView can handle.
const { keyCode, ctrlKey, metaKey } = event
const arrowKeysMapping = {
37: 'selectPrevious',
38: 'selectAbove',
39: 'selectNext',
40: 'selectBelow',
}
if (
Object.keys(arrowKeysMapping).includes(keyCode.toString()) &&
this.canSelectNext(event)
) {
event.preventDefault()
this.$emit(arrowKeysMapping[keyCode])
}
if (keyCode === 9 && this.canSelectNext(event)) {
event.preventDefault()
this.$emit(event.shiftKey ? 'selectPrevious' : 'selectNext')
}
// Copy the value to the clipboard if ctrl/cmd + c is pressed.
if ((ctrlKey || metaKey) && keyCode === 67 && this.canCopy(event)) {
const rawValue = this.value
const value = this.$registry
.get('field', this.field.type)
.prepareValueForCopy(this.field, rawValue)
copyToClipboard(value)
}
// Removes the value if the backspace/delete key is pressed.
if ((keyCode === 46 || keyCode === 8) && this.canEmpty(event)) {
event.preventDefault()
const value = this.$registry
.get('field', this.field.type)
.getEmptyValue(this.field)
const oldValue = this.value
if (value !== oldValue) {
this.$emit('update', value, oldValue)
}
}
}
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) => {
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 !== oldValue) {
this.$emit('update', value, oldValue)
}
}
document.addEventListener('paste', this.$el.pasteEvent)
this.clickTimestamp = new Date().getTime()
this.select()
// Emit the selected event so that the parent component can take an action like
// making sure that the element fits in the viewport.
this.$emit('selected', { component: this })
},
/**
* Removes all the listeners related to all field types.
*/
_beforeUnSelect() {
this.$el.removeEventListener('click', this.$el.clickEvent)
document.body.removeEventListener('click', this.$el.clickOutsideEvent)
document.body.removeEventListener('keydown', this.$el.keyDownEvent)
document.removeEventListener('paste', this.$el.pasteEvent)
this.beforeUnSelect()
this.$emit('unselected', {})
},
/**
* Method that is called when the column is selected. For example when clicked
* on the field. This is the moment to register event listeners if they are needed.

View file

@ -0,0 +1,28 @@
import { mapGetters } from 'vuex'
export default {
data() {
return {
gridViewRowDetailsWidth: 60,
}
},
computed: {
...mapGetters({
fieldOptions: 'view/grid/getAllFieldOptions',
}),
},
methods: {
getFieldWidth(fieldId) {
const hasFieldOptions = Object.prototype.hasOwnProperty.call(
this.fieldOptions,
fieldId
)
if (hasFieldOptions && this.fieldOptions[fieldId].hidden) {
return 0
}
return hasFieldOptions ? this.fieldOptions[fieldId].width : 200
},
},
}

View file

@ -7,7 +7,7 @@ export default {
components: { FieldSingleSelectDropdown },
computed: {
valueId() {
return this.value !== null ? this.value.id : null
return this.value && this.value !== null ? this.value.id : null
},
},
methods: {

View file

@ -18,6 +18,10 @@ export function populateRow(row) {
selectedBy: [],
matchFilters: true,
matchSortings: true,
// Keeping the selected state with the row has the best performance when navigating
// between cells.
selected: false,
selectedFieldId: -1,
}
return row
}
@ -56,6 +60,8 @@ export const state = () => ({
scrollTop: 0,
// The last windowHeight when the visibleByScrollTop was called.
windowHeight: 0,
// Indicates if the user is hovering over the add row button.
addRowHover: false,
})
export const mutations = {
@ -249,6 +255,21 @@ export const mutations = {
row._.selectedBy.splice(index, 1)
}
},
SET_ADD_ROW_HOVER(state, value) {
state.addRowHover = value
},
SET_SELECTED_CELL(state, { rowId, fieldId }) {
state.rows.forEach((row) => {
if (row._.selected) {
row._.selected = false
row._.selectedFieldId = -1
}
if (row.id === rowId) {
row._.selected = true
row._.selectedFieldId = fieldId
}
})
},
}
// Contains the timeout needed for the delayed delayed scroll top action.
@ -936,6 +957,12 @@ export const actions = {
}
}
},
setAddRowHover({ commit }, value) {
commit('SET_ADD_ROW_HOVER', value)
},
setSelectedCell({ commit }, { rowId, fieldId }) {
commit('SET_SELECTED_CELL', { rowId, fieldId })
},
}
export const getters = {
@ -1010,6 +1037,9 @@ export const getters = {
const index = state.rows.findIndex((row) => row.id === id)
return index === state.rows.length - 1
},
getAddRowHover(state) {
return state.addRowHover
},
}
export default {