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 #311 See merge request bramw/baserow!181
This commit is contained in:
commit
e17fc6f000
41 changed files with 1456 additions and 774 deletions
changelog.mdfieldTypes.js
web-frontend
config
modules
core
database
components
row
view/grid
GridView.vueGridViewCell.vueGridViewField.vueGridViewFieldType.vueGridViewHead.vueGridViewPlaceholder.vueGridViewRow.vueGridViewRowAdd.vueGridViewRows.vueGridViewSection.vue
fields
FunctionalGridViewFieldBoolean.vueFunctionalGridViewFieldDate.vueFunctionalGridViewFieldFile.vueFunctionalGridViewFieldLinkRow.vueFunctionalGridViewFieldLongText.vueFunctionalGridViewFieldNumber.vueFunctionalGridViewFieldSingleSelect.vueFunctionalGridViewFieldText.vueGridViewFieldBoolean.vueGridViewFieldDate.vueGridViewFieldEmail.vueGridViewFieldFile.vueGridViewFieldLinkRow.vueGridViewFieldLongText.vueGridViewFieldNumber.vueGridViewFieldSingleSelect.vueGridViewFieldText.vueGridViewFieldURL.vue
mixins
store/view
|
@ -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.
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,5 @@
|
|||
<template functional>
|
||||
<div class="grid-view__cell">
|
||||
<div class="grid-field-number">{{ props.value }}</div>
|
||||
</div>
|
||||
</template>
|
|
@ -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>
|
|
@ -0,0 +1,5 @@
|
|||
<template functional>
|
||||
<div ref="cell" class="grid-view__cell">
|
||||
<div class="grid-field-text">{{ props.value }}</div>
|
||||
</div>
|
||||
</template>
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
},
|
|
@ -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"
|
|
@ -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>
|
|
@ -1,8 +1,7 @@
|
|||
<template>
|
||||
<div
|
||||
class="grid-view__cell"
|
||||
class="grid-view__cell active"
|
||||
:class="{
|
||||
active: selected,
|
||||
editing: editing,
|
||||
invalid: editing && !isValid(),
|
||||
}"
|
|
@ -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() {
|
|
@ -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"
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
28
web-frontend/modules/database/mixins/gridViewHelpers.js
Normal file
28
web-frontend/modules/database/mixins/gridViewHelpers.js
Normal 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
|
||||
},
|
||||
},
|
||||
}
|
|
@ -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: {
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Add table
Reference in a new issue