mirror of
https://gitlab.com/bramw/baserow.git
synced 2024-11-25 00:46:46 +00:00
423 lines
14 KiB
Vue
423 lines
14 KiB
Vue
<template>
|
|
<RecursiveWrapper
|
|
:components="
|
|
wrapperDecorations.map((comp) => ({
|
|
...comp,
|
|
props: comp.propsFn(row),
|
|
}))
|
|
"
|
|
first-component-class="grid-view__row-background-wrapper"
|
|
>
|
|
<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 || !row._.matchSearch,
|
|
}"
|
|
@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 || !row._.matchSearch
|
|
"
|
|
class="grid-view__row-warning"
|
|
>
|
|
<template v-if="!row._.matchFilters">
|
|
{{ $t('gridViewRow.rowNotMatchingFilters') }}
|
|
</template>
|
|
<template v-else-if="!row._.matchSearch">
|
|
{{ $t('gridViewRow.rowNotMatchingSearch') }}
|
|
</template>
|
|
<template v-else-if="!row._.matchSortings">{{
|
|
$t('gridViewRow.rowHasMoved')
|
|
}}</template>
|
|
</div>
|
|
<div
|
|
class="grid-view__column grid-view__column--no-border-right"
|
|
:class="{ 'grid-view__column--group-end': groupEnd }"
|
|
:style="{ width: gridViewRowDetailsWidth + 'px' }"
|
|
>
|
|
<div
|
|
class="grid-view__row-info"
|
|
:class="{
|
|
'grid-view__row-info--matches-search':
|
|
row._.matchSearch &&
|
|
row._.fieldSearchMatches.includes('row_id'),
|
|
}"
|
|
>
|
|
<div
|
|
class="grid-view__row-count"
|
|
:class="{ 'grid-view__row-count--small': rowIdentifier > 9999 }"
|
|
:title="rowIdentifier"
|
|
>
|
|
{{ rowIdentifier }}
|
|
</div>
|
|
<div
|
|
v-if="!readOnly && canDrag"
|
|
class="grid-view__row-drag"
|
|
@mousedown="startDragging($event, row)"
|
|
></div>
|
|
<component
|
|
:is="rowExpandButton"
|
|
v-if="!row._.loading"
|
|
:row="row"
|
|
:workspace-id="workspaceId"
|
|
:table="view.table"
|
|
@edit-modal="$emit('edit-modal', row)"
|
|
></component>
|
|
<component
|
|
:is="dec.component"
|
|
v-for="dec in firstCellDecorations"
|
|
:key="dec.decoration.id"
|
|
v-bind="dec.propsFn(row)"
|
|
/>
|
|
</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 fieldsToRender"
|
|
:key="'row-field-' + row._.persistentId + '-' + field.id.toString()"
|
|
:workspace-id="workspaceId"
|
|
:field="field"
|
|
:row="row"
|
|
:all-fields-in-table="allFieldsInTable"
|
|
:state="state"
|
|
:multi-select-position="getMultiSelectPosition(row.id, field)"
|
|
:read-only="readOnly || field.read_only"
|
|
:store-prefix="storePrefix"
|
|
:group-end="groupEnd"
|
|
:style="{
|
|
width: fieldWidths[field.id] + 'px',
|
|
...getSelectedCellStyle(field),
|
|
}"
|
|
@update="$emit('update', $event)"
|
|
@paste="$emit('paste', $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)"
|
|
@refresh-row="$emit('refresh-row', $event)"
|
|
@cell-mousedown-left="$emit('cell-mousedown-left', { row, field })"
|
|
@cell-mouseover="$emit('cell-mouseover', { row, field })"
|
|
@cell-mouseup-left="$emit('cell-mouseup-left', { row, field })"
|
|
@cell-shift-click="$emit('cell-shift-click', { row, field })"
|
|
@add-row-after="$emit('add-row-after', $event)"
|
|
@edit-modal="$emit('edit-modal', row)"
|
|
></GridViewCell>
|
|
</div>
|
|
</RecursiveWrapper>
|
|
</template>
|
|
|
|
<script>
|
|
import GridViewCell from '@baserow/modules/database/components/view/grid/GridViewCell'
|
|
import gridViewHelpers from '@baserow/modules/database/mixins/gridViewHelpers'
|
|
import GridViewRowExpandButton from '@baserow/modules/database/components/view/grid/GridViewRowExpandButton'
|
|
import RecursiveWrapper from '@baserow/modules/database/components/RecursiveWrapper'
|
|
|
|
export default {
|
|
name: 'GridViewRow',
|
|
components: {
|
|
GridViewRowExpandButton,
|
|
GridViewCell,
|
|
RecursiveWrapper,
|
|
},
|
|
mixins: [gridViewHelpers],
|
|
provide() {
|
|
return {
|
|
$hasPermission: this.$hasPermission,
|
|
}
|
|
},
|
|
props: {
|
|
view: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
workspaceId: {
|
|
type: Number,
|
|
required: true,
|
|
},
|
|
row: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
groupEnd: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: () => false,
|
|
},
|
|
renderedFields: {
|
|
type: Array,
|
|
required: true,
|
|
},
|
|
decorationsByPlace: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
visibleFields: {
|
|
type: Array,
|
|
required: true,
|
|
},
|
|
allFieldsInTable: {
|
|
type: Array,
|
|
required: true,
|
|
},
|
|
fieldWidths: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
includeRowDetails: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: () => false,
|
|
},
|
|
includeGroupBy: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: () => false,
|
|
},
|
|
readOnly: {
|
|
type: Boolean,
|
|
required: true,
|
|
},
|
|
canDrag: {
|
|
type: Boolean,
|
|
required: true,
|
|
},
|
|
rowIdentifierType: {
|
|
type: String,
|
|
required: true,
|
|
default: 'count',
|
|
},
|
|
count: {
|
|
type: Number,
|
|
required: true,
|
|
},
|
|
primaryFieldIsSticky: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: () => true,
|
|
},
|
|
},
|
|
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: [],
|
|
rowExpandButton: this.$registry
|
|
.get('application', 'database')
|
|
.getRowExpandButtonComponent(),
|
|
}
|
|
},
|
|
computed: {
|
|
/**
|
|
* This component already accepts a `renderedFields` property containing the fields
|
|
* that must be rendered based on the viewport width and horizontal scroll offset,
|
|
* meaning it only renders the fields that are in the viewport. Because a selected
|
|
* field must always be rendered, this computed property checks if there is a
|
|
* selected field and if so, it's added to the array. This doesn't influence the
|
|
* position of the other cells because the position will be absolute. The selected
|
|
* field must always be rendered, otherwise the arrow keys and other functionality
|
|
* won't work.
|
|
*/
|
|
fieldsToRender() {
|
|
// If the row doesn't have a selected field, we can safely return the fields
|
|
// because we just want to render the fields inside of the view port.
|
|
if (!this.row._.selected) {
|
|
return this.renderedFields
|
|
}
|
|
|
|
// Check if the selected field exists in the all fields array, so not just the to
|
|
// be rendered ones.
|
|
const selectedField = this.visibleFields.find(
|
|
(field) => field.id === this.row._.selectedFieldId
|
|
)
|
|
|
|
// If it doesn't exist or if it's already in the fields array, we don't have to
|
|
// add it because it's already rendered.
|
|
if (
|
|
selectedField === undefined ||
|
|
this.renderedFields.find((field) => field.id === selectedField.id) !==
|
|
undefined
|
|
) {
|
|
return this.renderedFields
|
|
}
|
|
|
|
// If the selected field exists in all fields, but not in fields it must be added
|
|
// to the fields array because we want to render it. It won't influence the other
|
|
// cells because it's positioned absolute.
|
|
const fields = this.renderedFields.slice()
|
|
fields.unshift(selectedField)
|
|
return fields
|
|
},
|
|
firstCellDecorations() {
|
|
return this.decorationsByPlace?.first_cell || []
|
|
},
|
|
wrapperDecorations() {
|
|
return this.decorationsByPlace?.wrapper || []
|
|
},
|
|
rowIdentifier() {
|
|
switch (this.rowIdentifierType) {
|
|
case 'count':
|
|
return this.count
|
|
default:
|
|
return this.row.id
|
|
}
|
|
},
|
|
},
|
|
methods: {
|
|
isCellSelected(fieldId) {
|
|
return this.row._.selected && this.row._.selectedFieldId === fieldId
|
|
},
|
|
selectCell(fieldId, rowId = this.row.id) {
|
|
this.$emit('cell-selected', { fieldId, rowId })
|
|
},
|
|
// Return an object that represents if a cell is selected,
|
|
// and it's current position in the selection grid
|
|
getMultiSelectPosition(rowId, field) {
|
|
const position = {
|
|
selected: false,
|
|
top: false,
|
|
right: false,
|
|
bottom: false,
|
|
left: false,
|
|
}
|
|
if (
|
|
this.$store.getters[this.storePrefix + 'view/grid/isMultiSelectActive']
|
|
) {
|
|
const rowIndex =
|
|
this.$store.getters[this.storePrefix + 'view/grid/getRowIndexById'](
|
|
rowId
|
|
)
|
|
|
|
const allFieldIds = this.visibleFields.map((field) => field.id)
|
|
let fieldIndex = allFieldIds.findIndex((id) => field.id === id)
|
|
fieldIndex += !field.primary && this.primaryFieldIsSticky ? 1 : 0
|
|
|
|
const [minRow, maxRow] =
|
|
this.$store.getters[
|
|
this.storePrefix + 'view/grid/getMultiSelectRowIndexSorted'
|
|
]
|
|
const [minField, maxField] =
|
|
this.$store.getters[
|
|
this.storePrefix + 'view/grid/getMultiSelectFieldIndexSorted'
|
|
]
|
|
|
|
if (rowIndex >= minRow && rowIndex <= maxRow) {
|
|
if (fieldIndex >= minField && fieldIndex <= maxField) {
|
|
position.selected = true
|
|
if (rowIndex === minRow) {
|
|
position.top = true
|
|
}
|
|
if (rowIndex === maxRow) {
|
|
position.bottom = true
|
|
}
|
|
if (fieldIndex === minField) {
|
|
position.left = true
|
|
}
|
|
if (fieldIndex === maxField) {
|
|
position.right = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return position
|
|
},
|
|
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)
|
|
}
|
|
},
|
|
startDragging(event, row) {
|
|
if (this.readOnly) {
|
|
return
|
|
}
|
|
|
|
event.preventDefault()
|
|
this.$emit('row-dragging', { row, event })
|
|
},
|
|
/**
|
|
* Returns an object with additional styling if the field is selected and outside
|
|
* of the viewport. This is because selected fields must always be rendered because
|
|
* otherwise certain functionality won't work.
|
|
*/
|
|
getSelectedCellStyle(field) {
|
|
const exists =
|
|
this.renderedFields.find((f) => f.id === field.id) !== undefined
|
|
|
|
// If the field already exists in the field list it means that it's already
|
|
// rendered. In that case we don't have to provide any other styling because it's
|
|
// already in the position it's supposed to be in.
|
|
if (exists) {
|
|
return {}
|
|
}
|
|
|
|
// If the field doesn't exist in the fields array, it's being rendered because
|
|
// it's selected. In that case, the element must be positioned without influencing
|
|
// the other cells.
|
|
const styling = { position: 'absolute' }
|
|
|
|
const selectedFieldIndex = this.visibleFields.findIndex(
|
|
(field) => field.id === this.row._.selectedFieldId
|
|
)
|
|
const firstVisibleFieldIndex = this.visibleFields.findIndex(
|
|
(field) => field.id === this.renderedFields[0].id
|
|
)
|
|
const lastVisibleFieldIndex = this.visibleFields.findIndex(
|
|
(field) =>
|
|
field.id === this.renderedFields[this.renderedFields.length - 1].id
|
|
)
|
|
|
|
// Positions the selected field cell on the right position without influencing the
|
|
// position of the rendered cells. This is needed because other components depend
|
|
// on the cell to be in the right position, for example when using the arrow key
|
|
// navigation.
|
|
if (selectedFieldIndex < firstVisibleFieldIndex) {
|
|
// If the selected field must be positioned before the other fields
|
|
let spaceBetween = 0
|
|
for (let i = selectedFieldIndex; i < firstVisibleFieldIndex; i++) {
|
|
spaceBetween += this.fieldWidths[this.visibleFields[i].id]
|
|
}
|
|
styling.left = -spaceBetween + 'px'
|
|
} else if (selectedFieldIndex > lastVisibleFieldIndex) {
|
|
// If the selected field must be positioned after the other fields.
|
|
let spaceBetween = 0
|
|
for (let i = lastVisibleFieldIndex; i < selectedFieldIndex; i++) {
|
|
spaceBetween += this.fieldWidths[this.visibleFields[i].id]
|
|
}
|
|
styling.right = -spaceBetween + 'px'
|
|
}
|
|
|
|
return styling
|
|
},
|
|
},
|
|
}
|
|
</script>
|