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:
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
modules
core/assets/scss/components
database
components
row
RowCreateModal.vueRowEditModalField.vueRowEditModalFieldsList.vueSelectRowContent.vueSelectRowModal.vue
view/grid/fields
store/view
utils
test/unit/database/utils
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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', {
|
||||
|
|
22
web-frontend/modules/database/utils/row.js
Normal file
22
web-frontend/modules/database/utils/row.js
Normal 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
|
||||
}, {})
|
||||
}
|
135
web-frontend/test/unit/database/utils/row.spec.js
Normal file
135
web-frontend/test/unit/database/utils/row.spec.js
Normal 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
|
||||
)
|
||||
})
|
||||
})
|
Loading…
Add table
Reference in a new issue