1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-11 16:01:20 +00:00

Resolve "Create new row via the row select modal"

This commit is contained in:
Alexander Haller 2022-08-27 12:41:40 +00:00 committed by Bram Wiepjes
parent 8adf694c11
commit 289139083c
13 changed files with 346 additions and 67 deletions
changelog.md
premium/web-frontend/modules/baserow_premium/store/view
web-frontend

View file

@ -12,6 +12,7 @@ For example:
### New Features
* Added missing success printouts to `count_rows` and `calculate_storage_usage` commands.
* Add `isort` settings to sort python imports.
* Allow creating new rows when selecting a related row [#1064](https://gitlab.com/bramw/baserow/-/issues/1064).
* Add row url parameter to `gallery` and `kanban` view.
* Only allow relative urls in the in the original query parameter.
* Infer user language when viewing a public view. [#834](https://gitlab.com/bramw/baserow/-/issues/834)

View file

@ -10,6 +10,7 @@ import {
import RowService from '@baserow/modules/database/services/row'
import FieldService from '@baserow/modules/database/services/field'
import { SingleSelectFieldType } from '@baserow/modules/database/fieldTypes'
import { prepareRowForRequest } from '@baserow/modules/database/utils/row'
export function populateRow(row) {
row._ = {
@ -333,27 +334,11 @@ export const actions = {
{ dispatch, commit, getters },
{ view, table, fields, values }
) {
// First prepare an object that we can send to the
const preparedValues = {}
fields.forEach((field) => {
const name = `field_${field.id}`
const fieldType = this.$registry.get('field', field._.type.type)
if (fieldType.isReadOnly) {
return
}
preparedValues[name] = Object.prototype.hasOwnProperty.call(values, name)
? (preparedValues[name] = fieldType.prepareValueForUpdate(
field,
values[name]
))
: fieldType.getEmptyValue(field)
})
const preparedRow = prepareRowForRequest(values, fields, this.$registry)
const { data } = await RowService(this.$client).create(
table.id,
preparedValues
preparedRow
)
return await dispatch('createdNewRow', {
view,

View file

@ -70,7 +70,7 @@
.select-row-modal__rows {
position: relative;
height: 33px + (33px * 10) + 44px;
height: 33px + (33px * 11) + 44px;
background-color: $color-neutral-100;
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
@ -154,6 +154,18 @@
}
}
.select-row-modal__cell--single {
flex-basis: 100%;
}
.select-row-modal__add-row {
display: block;
line-height: 33px;
color: $color-neutral-900;
background-color: $white;
padding-left: 8px;
}
.select-row-modal__row--hover {
cursor: pointer;

View file

@ -8,11 +8,12 @@
<RowEditModalFieldsList
:primary-is-sortable="primaryIsSortable"
:fields="visibleFields"
:sortable="true"
:sortable="sortable"
:hidden="false"
:read-only="false"
:row="row"
:table="table"
:can-modify-fields="canModifyFields"
@field-updated="$emit('field-updated', $event)"
@field-deleted="$emit('field-deleted')"
@order-fields="$emit('order-fields', $event)"
@ -34,6 +35,7 @@
:read-only="false"
:row="row"
:table="table"
:can-modify-fields="canModifyFields"
@field-updated="$emit('field-updated', $event)"
@field-deleted="$emit('field-deleted')"
@toggle-field-visibility="$emit('toggle-field-visibility', $event)"
@ -81,6 +83,16 @@ export default {
required: false,
default: false,
},
sortable: {
type: Boolean,
required: false,
default: true,
},
canModifyFields: {
type: Boolean,
required: false,
default: true,
},
visibleFields: {
type: Array,
required: true,

View file

@ -11,7 +11,7 @@
></i>
{{ field.name }}
<a
v-if="!readOnly"
v-if="!readOnly && canModifyFields"
ref="contextLink"
class="control__context"
@click="$refs.context.toggle($refs.contextLink, 'bottom', 'left', 0)"
@ -66,6 +66,11 @@ export default {
type: Boolean,
default: false,
},
canModifyFields: {
type: Boolean,
required: false,
default: true,
},
canBeHidden: {
type: Boolean,
required: false,

View file

@ -23,6 +23,7 @@
:read-only="readOnly"
:row="row"
:table="table"
:can-modify-fields="canModifyFields"
@field-updated="$emit('field-updated', $event)"
@field-deleted="$emit('field-deleted')"
@update="$emit('update', $event)"
@ -54,6 +55,11 @@ export default {
type: Boolean,
required: true,
},
canModifyFields: {
type: Boolean,
required: false,
default: true,
},
hidden: {
type: Boolean,
required: true,

View file

@ -54,6 +54,22 @@
<SelectRowField :field="primary" :row="row"></SelectRowField>
</div>
</div>
<div
class="select-row-modal__row"
:class="{ 'select-row-modal__row--hover': addRowHover }"
@mouseover="addRowHover = true"
@mouseleave="addRowHover = false"
@click="$refs.rowCreateModal.show()"
>
<a
class="
select-row-modal__cell
select-row-modal__cell--single
select-row-modal__add-row
"
><i class="fas fa-plus"></i
></a>
</div>
</div>
<div class="select-row-modal__foot">
<Paginator
@ -98,6 +114,18 @@
<SelectRowField :field="field" :row="row"></SelectRowField>
</div>
</div>
<div
class="select-row-modal__row"
:style="{ width: addRowWidth }"
:class="{ 'select-row-modal__row--hover': addRowHover }"
@mouseover="addRowHover = true"
@mouseleave="addRowHover = false"
@click="$refs.rowCreateModal.show()"
>
<div
class="select-row-modal__cell select-row-modal__cell--single"
></div>
</div>
</div>
<div
class="select-row-modal__foot"
@ -108,10 +136,21 @@
</div>
</div>
</div>
<RowCreateModal
v-if="table"
ref="rowCreateModal"
:table="table"
:sortable="false"
:visible-fields="allFields"
:can-modify-fields="false"
@created="createRow"
></RowCreateModal>
</div>
</template>
<script>
import debounce from 'lodash/debounce'
import { notifyIf } from '@baserow/modules/core/utils/error'
import FieldService from '@baserow/modules/database/services/field'
import { populateField } from '@baserow/modules/database/store/field'
@ -119,12 +158,14 @@ import RowService from '@baserow/modules/database/services/row'
import { populateRow } from '@baserow/modules/database/store/view/grid'
import Paginator from '@baserow/modules/core/components/Paginator'
import SelectRowField from './SelectRowField'
import debounce from 'lodash/debounce'
import SelectRowField from '@baserow/modules/database/components/row/SelectRowField'
import RowCreateModal from '@baserow/modules/database/components/row/RowCreateModal'
import { prepareRowForRequest } from '@baserow/modules/database/utils/row'
import { DatabaseApplicationType } from '@baserow/modules/database/applicationTypes'
export default {
name: 'SelectRowContent',
components: { Paginator, SelectRowField },
components: { Paginator, SelectRowField, RowCreateModal },
props: {
tableId: {
type: Number,
@ -148,16 +189,47 @@ export default {
page: 1,
totalPages: null,
lastHoveredRow: null,
addRowHover: false,
searchDebounce: null,
}
},
computed: {
addRowWidth() {
return this.fields.length * 200 + 'px'
},
allFields() {
return [].concat(this.primary || [], this.fields || [])
},
table() {
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 foundTable
}
}
return null
},
},
async mounted() {
// The first time we have to fetch the fields because they are unknown for this
// table.
await this.fetchFields(this.tableId)
if (!(await this.fetchFields(this.tableId))) {
return false
}
// We want to start with some initial data when the modal opens for the first time.
await this.fetch(1)
if (!(await this.fetch(1))) {
return false
}
// Because most of the template depends on having some initial data we mark the
// state as loaded after that. Only a loading animation is shown if there isn't any
@ -226,9 +298,12 @@ export default {
this.primary =
primaryIndex !== -1 ? data.splice(primaryIndex, 1)[0] : null
this.fields = data
return true
} catch (error) {
this.loading = false
notifyIf(error, 'row')
this.$emit('hide')
this.loading = false
return false
}
},
/**
@ -270,11 +345,14 @@ export default {
this.page = page
this.totalPages = Math.ceil(data.count / 10)
this.rows = data.results
this.loading = false
return true
} catch (error) {
notifyIf(error, 'row')
this.$emit('hide')
this.loading = false
return false
}
this.loading = false
},
/**
* Called when the user selects a row.
@ -292,6 +370,42 @@ export default {
focusSearch() {
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
)
// 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']
const viewType = this.$registry.get('view', view.type)
viewType.rowCreated(
{ store: this.$store },
this.table.id,
this.allFields,
rowCreated,
{},
'page/'
)
this.select(populateRow(rowCreated), this.primary, this.fields)
callback()
} catch (error) {
callback(error)
}
},
},
}
</script>

View file

@ -1,5 +1,5 @@
<template>
<Modal class="select-row-modal" @hidden="$emit('hidden')">
<Modal ref="modal" class="select-row-modal" @hidden="$emit('hidden')">
<!--
Because of how the moveToBody mixin works it takes a small moment before the $refs
become available. In order for the Scrollbars components to work the refs need to
@ -10,6 +10,7 @@
:table-id="tableId"
:value="value"
@selected="selected"
@hide="hide"
></SelectRowContent>
</Modal>
</template>

View file

@ -106,10 +106,17 @@ export default {
* inside one of these contexts.
*/
canUnselectByClickingOutside(event) {
return (
!isElement(this.$refs.selectModal.$el, event.target) &&
!isElement(this.$refs.rowEditModal.$refs.modal.$el, event.target)
)
const openModals = [
...this.$refs.selectModal.$refs.modal.moveToBody.children.map(
(child) => child.$el
),
this.$refs.selectModal.$el,
this.$refs.rowEditModal.$refs.modal.$el,
]
return !openModals.some((modal) => {
return isElement(modal, event.target)
})
},
/**
* Prevent unselecting the field cell by changing the event. Because the deleted

View file

@ -10,6 +10,7 @@ import {
getOrderBy,
} from '@baserow/modules/database/utils/view'
import RowService from '@baserow/modules/database/services/row'
import { prepareRowForRequest } from '@baserow/modules/database/utils/row'
/**
* This view store mixin can be used to efficiently keep and maintain the rows of a
@ -573,30 +574,11 @@ export default ({ service, customPopulateRow }) => {
{ dispatch, commit, getters },
{ view, table, fields, values }
) {
// First prepare an object that we can send to the backend.
const preparedValues = {}
fields.forEach((field) => {
const name = `field_${field.id}`
const fieldType = this.$registry.get('field', field._.type.type)
if (fieldType.isReadOnly) {
return
}
preparedValues[name] = Object.prototype.hasOwnProperty.call(
values,
name
)
? (preparedValues[name] = fieldType.prepareValueForUpdate(
field,
values[name]
))
: fieldType.getEmptyValue(field)
})
const preparedRow = prepareRowForRequest(values, fields, this.$registry)
const { data } = await RowService(this.$client).create(
table.id,
preparedValues
preparedRow
)
return await dispatch('afterNewRowCreated', {
view,

View file

@ -16,6 +16,7 @@ import {
getOrderBy,
} from '@baserow/modules/database/utils/view'
import { RefreshCancelledError } from '@baserow/modules/core/errors'
import { prepareRowForRequest } from '@baserow/modules/database/utils/row'
export function populateRow(row, metadata = {}) {
row._ = {
@ -1314,24 +1315,20 @@ export const actions = {
{ commit, getters, dispatch },
{ view, table, fields, values = {}, before = null }
) {
// Fill the not provided values with the empty value of the field type so we can
// immediately commit the created row to the state.
const valuesForApiRequest = {}
// Fill values with empty values of field if they are not provided
fields.forEach((field) => {
const name = `field_${field.id}`
const fieldType = this.$registry.get('field', field._.type.type)
if (!(name in values)) {
const empty = fieldType.getNewRowValue(field)
values[name] = empty
}
// In case the fieldType is a read only field, we need to create a second
// values dictionary, which gets sent to the API without the fieldType.
if (!fieldType.isReadOnly) {
const newValue = fieldType.prepareValueForUpdate(field, values[name])
valuesForApiRequest[name] = newValue
values[name] = fieldType.getNewRowValue(field)
}
})
// Fill the not provided values with the empty value of the field type so we can
// immediately commit the created row to the state.
const preparedRow = prepareRowForRequest(values, fields, this.$registry)
// If before is not provided, then the row is added last. Because we don't know
// the total amount of rows in the table, we are going to add find the highest
// existing order in the buffer and increase that by one.
@ -1362,7 +1359,7 @@ export const actions = {
try {
const { data } = await RowService(this.$client).create(
table.id,
valuesForApiRequest,
preparedRow,
before !== null ? before.id : null
)
commit('FINALIZE_ROW_IN_BUFFER', {

View file

@ -0,0 +1,22 @@
/**
* Serializes a row to make sure that the values are according to what the API expects.
*
* If a field doesn't have a value it will be assigned the empty value of the field
* type.
*/
export function prepareRowForRequest(row, fields, registry) {
return fields.reduce((preparedRow, field) => {
const name = `field_${field.id}`
const fieldType = registry.get('field', field._.type.type)
if (fieldType.isReadOnly) {
return preparedRow
}
preparedRow[name] = Object.prototype.hasOwnProperty.call(row, name)
? (preparedRow[name] = fieldType.prepareValueForUpdate(field, row[name]))
: fieldType.getEmptyValue(field)
return preparedRow
}, {})
}

View file

@ -0,0 +1,135 @@
import { prepareRowForRequest } from '@baserow/modules/database/utils/row'
import { TestApp } from '@baserow/test/helpers/testApp'
describe('Row untilities', () => {
describe('prepareRowForRequest', () => {
let testApp = null
let store = null
beforeAll(() => {
testApp = new TestApp()
store = testApp.store
})
afterEach((done) => {
testApp.afterEach().then(done)
})
const rowsToTest = [
// Empty case
{
input: {
row: {},
fields: [],
},
output: {},
},
// Basic case of just a simple text field
{
input: {
row: {
field_1: 'value',
},
fields: [
{
name: 'field_1',
id: 1,
_: {
type: {
type: 'text',
},
},
},
],
},
output: {
field_1: 'value',
},
},
// Empty value is being used if no value is provided
{
input: {
row: {
field_1: 'value',
},
fields: [
{
name: 'field_1',
id: 1,
_: {
type: {
type: 'text',
},
},
},
{
name: 'field_2',
id: 2,
text_default: 'some default',
_: {
type: {
type: 'text',
},
},
},
],
},
output: {
field_1: 'value',
field_2: 'some default',
},
},
// Read only field
{
input: {
row: {
field_1: 'value',
},
fields: [
{
name: 'field_1',
id: 1,
_: {
type: {
type: 'formula',
},
},
},
],
},
output: {},
},
// Missing value
{
input: {
row: {},
fields: [
{
name: 'field_1',
id: 1,
text_default: 'some default',
_: {
type: {
type: 'text',
},
},
},
],
},
output: {
field_1: 'some default',
},
},
]
test.each(rowsToTest)(
'Test that %o is correctly prepared for request',
({ input, output }) => {
expect(
prepareRowForRequest(input.row, input.fields, store.$registry)
).toEqual(output)
},
200
)
})
})