1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-02-18 19:12:30 +00:00
bramw_baserow/web-frontend/modules/database/components/row/SelectRowContent.vue

509 lines
15 KiB
Vue

<template>
<div>
<div class="select-row-modal__head">
<div class="select-row-modal__search">
<i class="iconoir-search select-row-modal__search-icon"></i>
<input
ref="search"
v-model="visibleSearch"
type="text"
:placeholder="$t('selectRowContent.search')"
class="select-row-modal__search-input"
@input="doSearch(visibleSearch, false)"
@keydown.enter="doSearch(visibleSearch, true)"
@keydown.up.down="$refs.search.blur()"
/>
</div>
<div class="select-row-modal__fields">
<Button
ref="fieldsButton"
size="tiny"
type="secondary"
icon="iconoir-eye-off"
@click="toggleFieldsContext"
>
{{ $t('selectRowContent.hideFields') }}
</Button>
<ViewFieldsContext
ref="fieldsContext"
:database="{}"
:view="{}"
:fields="fields || []"
:field-options="fieldOptionsIncludingOverride"
@update-all-field-options="updateAllFieldOptions"
@update-field-options-of-field="updateFieldOptionsOfField"
@update-order="orderFieldOptions"
></ViewFieldsContext>
</div>
</div>
<div
class="select-row-modal__rows"
:class="{
'select-row-modal__rows--loading': loading || !metaDataLoaded,
}"
>
<SimpleGrid
v-if="metaDataLoaded && firstPageLoaded"
:fixed-fields="[primary]"
:fields="fields"
:field-options="fieldOptionsIncludingOverride"
:rows="rows"
:full-height="true"
:can-add-row="true"
:with-footer="true"
:show-hovered-row="true"
:selected-rows="selectedRows"
:multiple="multiple"
:show-row-id="true"
@add-row="$refs.rowCreateModal.show()"
@row-click="select($event)"
@update-field-width="updateFieldWidth"
>
<template #footLeft>
<Paginator
:total-pages="totalPages"
:page="page"
@change-page="fetch($event, true)"
></Paginator>
</template>
</SimpleGrid>
</div>
<RowCreateModal
v-if="table"
ref="rowCreateModal"
:database="database"
:table="table"
:sortable="false"
:all-fields-in-table="allFields"
:visible-fields="allFields"
:can-modify-fields="false"
:presets="newRowPresets"
@created="createRow"
></RowCreateModal>
</div>
</template>
<script>
import debounce from 'lodash/debounce'
import merge from 'lodash/extend'
import { notifyIf } from '@baserow/modules/core/utils/error'
import FieldService from '@baserow/modules/database/services/field'
import { populateField } from '@baserow/modules/database/store/field'
import RowService from '@baserow/modules/database/services/row'
import { populateRow } from '@baserow/modules/database/store/view/grid'
import ViewService from '@baserow/modules/database/services/view'
import Paginator from '@baserow/modules/core/components/Paginator'
import RowCreateModal from '@baserow/modules/database/components/row/RowCreateModal'
import { prepareRowForRequest } from '@baserow/modules/database/utils/row'
import { DatabaseApplicationType } from '@baserow/modules/database/applicationTypes'
import { GridViewType } from '@baserow/modules/database/viewTypes'
import SimpleGrid from '@baserow/modules/database/components/view/grid/SimpleGrid.vue'
import { getDefaultSearchModeFromEnv } from '@baserow/modules/database/utils/search'
import ViewFieldsContext from '@baserow/modules/database/components/view/ViewFieldsContext'
import { clone } from '@baserow/modules/core/utils/object'
import { getData, setData } from '@baserow/modules/core/utils/indexedDB'
const databaseName = 'SelectRowContent'
const storeName = 'FieldOptions'
export default {
name: 'SelectRowContent',
components: { ViewFieldsContext, Paginator, RowCreateModal, SimpleGrid },
props: {
tableId: {
type: Number,
required: true,
},
viewId: {
type: [Number, null],
required: false,
default: null,
},
value: {
type: Array,
required: false,
default: () => [],
},
initialSearch: {
type: String,
required: false,
default: '',
},
multiple: {
type: Boolean,
required: false,
default: false,
},
newRowPresets: {
type: Object,
required: false,
default: () => ({}),
},
persistentFieldOptionsKey: {
type: String,
required: false,
default: '',
},
},
data() {
return {
// Indicates if we're loading new rows.
loading: false,
// Indicates if the metadata (fields, etc) has been loaded.
metaDataLoaded: false,
// Indicates if the page has loaded for the first time. We keep track of this
// state to show a non flickering loading state for the user.
firstPageLoaded: false,
primary: null,
fields: null,
fieldOptions: {},
rows: [],
search: '',
visibleSearch: '',
page: 1,
totalPages: 0,
lastHoveredRow: null,
addRowHover: false,
searchDebounce: null,
fieldOptionsOverride: {},
}
},
computed: {
allFields() {
return [].concat(this.primary || [], this.fields || [])
},
databaseAndTable() {
const databaseType = DatabaseApplicationType.getType()
for (const application of this.$store.getters['application/getAll']) {
if (application.type !== databaseType) {
continue
}
const foundTable = application.tables.find(
({ id }) => id === this.tableId
)
if (foundTable) {
return [application, foundTable]
}
}
return [null, null]
},
database() {
return this.databaseAndTable[0]
},
table() {
return this.databaseAndTable[1]
},
selectedRows() {
return this.value.map(({ id }) => id)
},
/**
* Merges the fieldOptions and fieldOptionsOverride deep, so that we're visually
* rendering what the user has configured.
*/
fieldOptionsIncludingOverride() {
const fieldOptions = clone(this.fieldOptions)
Object.keys(this.fieldOptionsOverride).forEach((key) => {
if (!Object.prototype.hasOwnProperty.call(fieldOptions, key)) {
fieldOptions[key] = {}
}
fieldOptions[key] = merge(
{},
fieldOptions[key],
this.fieldOptionsOverride[key]
)
})
return fieldOptions
},
},
watch: {
/**
* Stores the overrides into the local storage, so order, visibilty, etc only
* persists for the user that configured it.
*/
async fieldOptionsOverride(value) {
// There is no need to store the values in the local storage if the persistent
// key is not set because we can't compute a unique key.
if (!this.persistentFieldOptionsKey) {
return
}
// Remove the not existing keys because the related fields might have been
// deleted in the meantime, and so we're keeping the local storage clean.
value = Object.fromEntries(
Object.entries(value).filter((key) => {
return Object.prototype.hasOwnProperty.call(this.fieldOptions, key[0])
})
)
try {
await setData(
databaseName,
storeName,
this.persistentFieldOptionsKey,
value
)
} catch (error) {}
},
},
async mounted() {
// Focus the search field so the user may begin typing immediately.
this.$nextTick(() => {
this.focusSearch({})
})
// The first time we have to fetch the fields because they are unknown for this
// table.
if (!(await this.fetchFields(this.tableId))) {
return false
}
await this.orderFieldsByFirstGridViewFieldOptions(this.tableId)
// Because the page data depends on having some initial metadata we mark the state
// as loaded after that. Only a loading animation is shown if there isn't any
// data.
this.metaDataLoaded = true
this.doSearch(this.visibleSearch, false)
this.$priorityBus.$on(
'start-search',
this.$priorityBus.level.HIGHEST,
this.focusSearch
)
},
beforeDestroy() {
this.$priorityBus.$off('start-search', this.focusSearch)
},
methods: {
/**
* Fetches all the fields of the given table id. We need the fields so that we can
* show the data in the correct format.
*/
async fetchFields(tableId) {
try {
const { data } = await FieldService(this.$client).fetchAll(tableId)
data.forEach((part, index, d) => {
populateField(data[index], this.$registry)
})
const primaryIndex = data.findIndex((item) => item.primary === true)
this.primary =
primaryIndex !== -1 ? data.splice(primaryIndex, 1)[0] : null
this.fields = data
return true
} catch (error) {
notifyIf(error, 'row')
this.$emit('hide')
this.loading = false
return false
}
},
/**
* This method fetches the first grid and the related field options. The ordering
* of that grid view will be applied to the already fetched fields. If anything
* goes wrong or if there isn't a grid view, the original order will be used.
*/
async orderFieldsByFirstGridViewFieldOptions(tableId) {
try {
const { data: views } = await ViewService(this.$client).fetchAll(
tableId,
false,
false,
false,
false,
// We can safely limit to `1` because the backend provides the views ordered.
1,
// We want to fetch the first grid view because for that type we're sure it's
// compatible with `filterVisibleFieldsFunction` and
// `sortFieldsByOrderAndIdFunction`. Others might also work, but this
// component is styled like a grid view and it makes to most sense to reflect
// that here.
GridViewType.getType()
)
if (views.length === 0) {
return
}
const {
data: { field_options: fieldOptions },
} = await ViewService(this.$client).fetchFieldOptions(views[0].id)
this.fieldOptions = fieldOptions
if (this.persistentFieldOptionsKey) {
const override = await getData(
databaseName,
storeName,
this.persistentFieldOptionsKey
)
this.fieldOptionsOverride = override || {}
}
} catch (error) {
notifyIf(error, 'view')
}
},
/**
* Does a row search in the table related to the state. It will also reset the
* pagination.
*/
doSearch(query, immediate) {
const search = () => {
this.search = query
this.totalPages = 0
return this.fetch(1, false)
}
if (this.searchDebounce) {
this.searchDebounce.cancel()
}
this.loading = true
if (immediate) {
search()
} else {
this.searchDebounce = debounce(search, 400)
this.searchDebounce()
}
},
/**
* Fetches the rows of a given page and adds them to the state. If a search query
* has been stored in the state then that will be remembered.
*/
async fetch(page, startLoading = true) {
if (startLoading) {
this.loading = true
}
try {
const { data } = await RowService(this.$client).fetchAll({
tableId: this.tableId,
page,
size: 10,
search: this.search,
searchMode: getDefaultSearchModeFromEnv(this.$config),
viewId: this.viewId,
})
data.results.forEach((part, index) => populateRow(data.results[index]))
this.page = page
this.totalPages = Math.ceil(data.count / 10)
this.rows = data.results
this.loading = false
this.firstPageLoaded = true
return true
} catch (error) {
notifyIf(error, 'row')
this.loading = false
this.$emit('hide')
return false
}
},
/**
* Called when the user selects a row.
*/
select(row) {
const exists = this.selectedRows.includes(row.id)
// In multiple mode it's also possible to unselect.
if (!this.multiple && exists) {
return
}
this.$emit(exists ? 'unselected' : 'selected', {
row,
primary: this.primary,
fields: this.fields,
})
},
/**
* Focuses the search field when the component mounts.
*/
focusSearch({ event }) {
event?.preventDefault()
this.$refs.search?.focus()
},
async createRow({ row, callback }) {
try {
const preparedRow = prepareRowForRequest(
row,
this.allFields,
this.$registry
)
const { data: rowCreated } = await RowService(this.$client).create(
this.table.id,
preparedRow
)
await this.fetch(this.page)
// When you create a new row from a linked row that links to its own table,the
// realtime update will be sent from you, and you won't receive it.Since you
// don't receive the realtime update we have to manually add the new row to the
// state. We can do that by using the same function that is used by the
// realtime update. (`viewType.rowCreated`)
const view = this.$store.getters['view/getSelected']
// The `view.type` check ensures that the Builder doesn't crash when
// creating a new row in the Data Source modal.
//
// In AB's Data Source modal, it is possible to create a new row for
// fields of the type "Link to table". Since there is no selected view,
// there is no view type.
if (view.type) {
const viewType = this.$registry.get('view', view.type)
viewType.rowCreated(
{ store: this.$store },
this.table.id,
this.allFields,
rowCreated,
{},
'page/'
)
this.select(populateRow(rowCreated))
}
callback()
} catch (error) {
callback(error)
}
},
toggleFieldsContext() {
this.$refs.fieldsContext.toggle(this.$refs.fieldsButton.$el)
},
updateAllFieldOptions({ newFieldOptions, oldFieldOptions }) {
const override = clone(this.fieldOptionsOverride)
Object.keys(newFieldOptions).forEach((key) => {
if (!Object.prototype.hasOwnProperty.call(override, key)) {
override[key] = {}
}
override[key] = merge({}, override[key], newFieldOptions[key])
})
this.fieldOptionsOverride = override
},
updateFieldOptionsOfField({ field, values }) {
const override = clone(this.fieldOptionsOverride)
const key = field.id.toString()
override[field.id.toString()] = merge({}, override[key] || {}, values)
this.fieldOptionsOverride = override
},
orderFieldOptions({ order }) {
const override = clone(this.fieldOptionsOverride)
order.forEach((fieldId, index) => {
const id = fieldId.toString()
if (!Object.prototype.hasOwnProperty.call(override, id)) {
override[id] = {}
}
override[id].order = index
})
this.fieldOptionsOverride = override
},
updateFieldWidth({ field, width }) {
this.updateFieldOptionsOfField({ field, values: { width } })
},
},
}
</script>