diff --git a/changelog.md b/changelog.md index b6fa17ca4..6404ab537 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,8 @@ ## Unreleased +* Added row modal editing feature to the grid view. +* Made it possible to resize the field width per view. * Added validation and formatting for the number field. * Cancel the editing state of a fields when the escape key is pressed. * The next field is now selected when the tab character is pressed when a field is diff --git a/web-frontend/modules/core/assets/scss/components/_form.scss b/web-frontend/modules/core/assets/scss/components/_form.scss index ab67cefcc..c6a07d4c6 100644 --- a/web-frontend/modules/core/assets/scss/components/_form.scss +++ b/web-frontend/modules/core/assets/scss/components/_form.scss @@ -15,6 +15,10 @@ } } +.control-label-icon { + margin-right: 6px; +} + .control-context { color: $color-primary-900; margin-left: 6px; diff --git a/web-frontend/modules/database/components/field/FieldContext.vue b/web-frontend/modules/database/components/field/FieldContext.vue new file mode 100644 index 000000000..156bbc7e9 --- /dev/null +++ b/web-frontend/modules/database/components/field/FieldContext.vue @@ -0,0 +1,69 @@ +<template> + <Context ref="context"> + <ul class="context-menu"> + <li> + <a + ref="updateFieldContextLink" + class="grid-view-description-options" + @click=" + $refs.updateFieldContext.toggle( + $refs.updateFieldContextLink, + 'bottom', + 'left', + 0 + ) + " + > + <i class="context-menu-icon fas fa-fw fa-pen"></i> + Edit field + </a> + <UpdateFieldContext + ref="updateFieldContext" + :field="field" + @update="$refs.context.hide()" + ></UpdateFieldContext> + </li> + <li v-if="!field.primary"> + <a @click="deleteField(field)"> + <i class="context-menu-icon fas fa-fw fa-trash"></i> + Delete field + </a> + </li> + </ul> + </Context> +</template> + +<script> +import { notifyIf } from '@baserow/modules/core/utils/error' +import context from '@baserow/modules/core/mixins/context' +import UpdateFieldContext from '@baserow/modules/database/components/field/UpdateFieldContext' + +export default { + name: 'FieldContext', + components: { UpdateFieldContext }, + mixins: [context], + props: { + field: { + type: Object, + required: true, + }, + }, + methods: { + setLoading(field, value) { + this.$store.dispatch('field/setItemLoading', { field, value }) + }, + async deleteField(field) { + this.$refs.context.hide() + this.setLoading(field, true) + + try { + await this.$store.dispatch('field/delete', field) + } catch (error) { + notifyIf(error, 'field') + } + + this.setLoading(field, false) + }, + }, +} +</script> diff --git a/web-frontend/modules/database/components/row/RowEditFieldBoolean.vue b/web-frontend/modules/database/components/row/RowEditFieldBoolean.vue new file mode 100644 index 000000000..4363efbb2 --- /dev/null +++ b/web-frontend/modules/database/components/row/RowEditFieldBoolean.vue @@ -0,0 +1,26 @@ +<template> + <div class="control-elements"> + <div + class="field-boolean-checkbox" + :class="{ active: value }" + @click="toggle(value)" + > + <i class="fas fa-check check"></i> + </div> + </div> +</template> + +<script> +import rowEditField from '@baserow/modules/database/mixins/rowEditField' + +export default { + mixins: [rowEditField], + methods: { + toggle(value) { + const oldValue = !!value + const newValue = !value + this.$emit('update', newValue, oldValue) + }, + }, +} +</script> diff --git a/web-frontend/modules/database/components/row/RowEditFieldNumber.vue b/web-frontend/modules/database/components/row/RowEditFieldNumber.vue new file mode 100644 index 000000000..909fc2225 --- /dev/null +++ b/web-frontend/modules/database/components/row/RowEditFieldNumber.vue @@ -0,0 +1,27 @@ +<template> + <div class="control-elements"> + <input + ref="input" + v-model="copy" + type="text" + class="input input-large field-number" + :class="{ 'input-error': !isValid() }" + @keyup.enter="$refs.input.blur()" + @focus="select()" + @blur="unselect()" + /> + <div v-show="!isValid()" class="error"> + {{ getError() }} + </div> + </div> +</template> + +<script> +import rowEditField from '@baserow/modules/database/mixins/rowEditField' +import rowEditFieldInput from '@baserow/modules/database/mixins/rowEditFieldInput' +import numberField from '@baserow/modules/database/mixins/numberField' + +export default { + mixins: [rowEditField, rowEditFieldInput, numberField], +} +</script> diff --git a/web-frontend/modules/database/components/row/RowEditFieldText.vue b/web-frontend/modules/database/components/row/RowEditFieldText.vue new file mode 100644 index 000000000..41e6516ae --- /dev/null +++ b/web-frontend/modules/database/components/row/RowEditFieldText.vue @@ -0,0 +1,22 @@ +<template> + <div class="control-elements"> + <input + ref="input" + v-model="copy" + type="text" + class="input input-large" + @keyup.enter="$refs.input.blur()" + @focus="select()" + @blur="unselect()" + /> + </div> +</template> + +<script> +import rowEditField from '@baserow/modules/database/mixins/rowEditField' +import rowEditFieldInput from '@baserow/modules/database/mixins/rowEditFieldInput' + +export default { + mixins: [rowEditField, rowEditFieldInput], +} +</script> diff --git a/web-frontend/modules/database/components/row/RowEditModal.vue b/web-frontend/modules/database/components/row/RowEditModal.vue new file mode 100644 index 000000000..39f8952ba --- /dev/null +++ b/web-frontend/modules/database/components/row/RowEditModal.vue @@ -0,0 +1,92 @@ +<template> + <Modal> + <h2 v-if="primary !== undefined" class="box-title"> + {{ getHeading(primary, row) }} + </h2> + <form> + <RowEditModalField + v-for="field in getFields(fields, primary)" + :ref="'field-' + field.id" + :key="'row-edit-field-' + field.id" + :field="field" + :row="row" + @update="update" + ></RowEditModalField> + <div class="actions"> + <a + ref="createFieldContextLink" + @click="$refs.createFieldContext.toggle($refs.createFieldContextLink)" + > + <i class="fas fa-plus"></i> + add field + </a> + <CreateFieldContext + ref="createFieldContext" + :table="table" + ></CreateFieldContext> + </div> + </form> + </Modal> +</template> + +<script> +import modal from '@baserow/modules/core/mixins/modal' +import RowEditModalField from '@baserow/modules/database/components/row/RowEditModalField' +import CreateFieldContext from '@baserow/modules/database/components/field/CreateFieldContext' + +export default { + name: 'RowEditModal', + components: { + RowEditModalField, + CreateFieldContext, + }, + mixins: [modal], + props: { + table: { + type: Object, + required: true, + }, + primary: { + type: Object, + required: false, + default: undefined, + }, + fields: { + type: Array, + required: true, + }, + }, + data() { + return { + row: {}, + } + }, + methods: { + show(row, ...args) { + this.row = row + this.getRootModal().show(...args) + }, + /** + * Because the modal can't update values by himself, an event will be called to + * notify the parent component to actually update the value. + */ + update(context) { + context.table = this.table + this.$emit('update', context) + }, + getFields(fields, primary) { + return primary !== undefined ? [primary].concat(fields) : fields + }, + getHeading(primary, row) { + const name = `field_${primary.id}` + if (Object.prototype.hasOwnProperty.call(row, name)) { + return this.$registry + .get('field', primary.type) + .toHumanReadableString(primary, row[name]) + } else { + return null + } + }, + }, +} +</script> diff --git a/web-frontend/modules/database/components/row/RowEditModalField.vue b/web-frontend/modules/database/components/row/RowEditModalField.vue new file mode 100644 index 000000000..a234ac984 --- /dev/null +++ b/web-frontend/modules/database/components/row/RowEditModalField.vue @@ -0,0 +1,58 @@ +<template> + <div class="control"> + <label class="control-label"> + <i + class="fas control-label-icon" + :class="'fa-' + field._.type.iconClass" + ></i> + {{ field.name }} + <a + ref="contextLink" + class="control-context" + @click="$refs.context.toggle($refs.contextLink, 'bottom', 'left', 0)" + > + <i class="fas fa-caret-down"></i> + </a> + </label> + <FieldContext ref="context" :field="field"></FieldContext> + <component + :is="getFieldComponent(field.type)" + ref="field" + :field="field" + :value="row['field_' + field.id]" + @update="update" + /> + </div> +</template> + +<script> +import FieldContext from '@baserow/modules/database/components/field/FieldContext' + +export default { + name: 'RowEditModalField', + components: { FieldContext }, + props: { + field: { + type: Object, + required: true, + }, + row: { + type: Object, + required: true, + }, + }, + methods: { + getFieldComponent(type) { + return this.$registry.get('field', type).getRowEditFieldComponent() + }, + update(value, oldValue) { + this.$emit('update', { + row: this.row, + field: this.field, + value, + oldValue, + }) + }, + }, +} +</script> diff --git a/web-frontend/modules/database/components/view/grid/GridView.vue b/web-frontend/modules/database/components/view/grid/GridView.vue index 7c7a612bb..3f5a33afa 100644 --- a/web-frontend/modules/database/components/view/grid/GridView.vue +++ b/web-frontend/modules/database/components/view/grid/GridView.vue @@ -45,7 +45,12 @@ v-for="row in rows" :key="'left-row-' + view.id + '-' + row.id" class="grid-view-row" - :class="{ 'grid-view-row-loading': row._.loading }" + :class="{ + 'grid-view-row-loading': row._.loading, + 'grid-view-row-hover': row._.hover, + }" + @mouseover="setRowHover(row, true)" + @mouseleave="setRowHover(row, false)" @contextmenu.prevent="showRowContext($event, row)" > <div @@ -54,7 +59,10 @@ > <div class="grid-view-row-info"> <div class="grid-view-row-count">{{ row.id }}</div> - <a href="#" class="grid-view-row-more"> + <a + class="grid-view-row-more" + @click="$refs.rowEditModal.show(row)" + > <i class="fas fa-expand"></i> </a> </div> @@ -68,6 +76,7 @@ :style="{ width: widths.fields[primary.id] + 'px' }" @selected="selectedField(primary, $event.component)" @selectNext="selectNextField(row, primary, fields, primary)" + @update="updateValue" ></GridViewField> </div> </div> @@ -175,7 +184,12 @@ v-for="row in rows" :key="'right-row-' + view.id + '-' + row.id" class="grid-view-row" - :class="{ 'grid-view-row-loading': row._.loading }" + :class="{ + 'grid-view-row-loading': row._.loading, + 'grid-view-row-hover': row._.hover, + }" + @mouseover="setRowHover(row, true)" + @mouseleave="setRowHover(row, false)" @contextmenu.prevent="showRowContext($event, row)" > <GridViewField @@ -193,6 +207,7 @@ selectNextField(row, field, fields, primary, true) " @selectNext="selectNextField(row, field, fields, primary)" + @update="updateValue" ></GridViewField> </div> </div> @@ -225,6 +240,13 @@ </li> </ul> </Context> + <RowEditModal + ref="rowEditModal" + :table="table" + :primary="primary" + :fields="fields" + @update="updateValue" + ></RowEditModal> </div> </template> @@ -235,6 +257,7 @@ import CreateFieldContext from '@baserow/modules/database/components/field/Creat import GridViewFieldType from '@baserow/modules/database/components/view/grid/GridViewFieldType' import GridViewField from '@baserow/modules/database/components/view/grid/GridViewField' 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' @@ -245,6 +268,7 @@ export default { GridViewFieldType, GridViewField, GridViewFieldWidthHandle, + RowEditModal, }, props: { primary: { @@ -273,6 +297,7 @@ export default { addHover: false, loading: true, selectedRow: null, + lastHoveredRow: null, widths: { fields: {}, }, @@ -308,6 +333,19 @@ export default { this.calculateWidths(this.primary, this.fields, this.fieldOptions) }, methods: { + async updateValue({ field, row, value, oldValue }) { + try { + await this.$store.dispatch('view/grid/updateValue', { + table: this.table, + row, + field, + value, + oldValue, + }) + } catch (error) { + notifyIf(error, 'field') + } + }, scroll(pixelY, pixelX) { const $rightBody = this.$refs.rightBody const $right = this.$refs.right @@ -412,7 +450,8 @@ export default { try { await this.$store.dispatch('view/grid/create', { table: this.table, - fields: this.fields, + // We need a list of all fields including the primary one here. + fields: [this.primary].concat(...this.fields), values: {}, }) } catch (error) { @@ -523,6 +562,21 @@ export default { 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 + }, }, } </script> diff --git a/web-frontend/modules/database/components/view/grid/GridViewField.vue b/web-frontend/modules/database/components/view/grid/GridViewField.vue index 46a042441..6466214db 100644 --- a/web-frontend/modules/database/components/view/grid/GridViewField.vue +++ b/web-frontend/modules/database/components/view/grid/GridViewField.vue @@ -2,7 +2,7 @@ <div class="grid-view-column" @click="select()"> <component :is="getFieldComponent(field.type)" - ref="column" + ref="field" :field="field" :value="row['field_' + field.id]" :selected="selected" @@ -13,15 +13,10 @@ <script> import { isElement } from '@baserow/modules/core/utils/dom' -import { notifyIf } from '@baserow/modules/core/utils/error' export default { name: 'GridViewField', props: { - table: { - type: Object, - required: true, - }, field: { type: Object, required: true, @@ -62,25 +57,12 @@ export default { * which will actually update the value via the store. */ update(value, oldValue) { - this.$store - .dispatch('view/grid/updateValue', { - table: this.table, - row: this.row, - field: this.field, - value, - oldValue, - }) - .catch((error) => { - notifyIf(error, 'column') - }) - .then(() => { - this.$forceUpdate() - }) - - // This is needed because in some cases we do have a value yet, so a watcher of - // the value is not guaranteed. This will make sure the component shows the - // latest value. - this.$forceUpdate() + this.$emit('update', { + row: this.row, + field: this.field, + value, + oldValue, + }) }, /** * Method that is called when a user clicks on the grid field. It wil @@ -97,7 +79,7 @@ export default { this.clickTimestamp !== null && timestamp - this.clickTimestamp < 200 ) { - this.$refs.column.doubleClick() + this.$refs.field.doubleClick() } } else { // If the field is not yet selected we can change the state to selected. @@ -105,7 +87,7 @@ export default { this.$nextTick(() => { // Call the select method on the next tick because we want to wait for all // changes to have rendered. - this.$refs.column.select() + this.$refs.field.select() }) // Register a body click event listener so that we can detect if a user has @@ -146,7 +128,7 @@ export default { this.clickTimestamp = timestamp }, unselect() { - this.$refs.column.beforeUnSelect() + this.$refs.field.beforeUnSelect() this.$nextTick(() => { this.selected = false }) diff --git a/web-frontend/modules/database/components/view/grid/GridViewFieldBoolean.vue b/web-frontend/modules/database/components/view/grid/GridViewFieldBoolean.vue index ebf0807de..acb9f5886 100644 --- a/web-frontend/modules/database/components/view/grid/GridViewFieldBoolean.vue +++ b/web-frontend/modules/database/components/view/grid/GridViewFieldBoolean.vue @@ -18,6 +18,19 @@ import gridField from '@baserow/modules/database/mixins/gridField' export default { mixins: [gridField], methods: { + select() { + // While the field is selected we want to toggle the value by pressing the enter + // key. + this.$el.keydownEvent = (event) => { + if (event.keyCode === 13) { + this.toggle(this.value) + } + } + document.body.addEventListener('keydown', this.$el.keydownEvent) + }, + beforeUnSelect() { + document.body.removeEventListener('keydown', this.$el.keydownEvent) + }, toggle(value) { if (!this.selected) { return diff --git a/web-frontend/modules/database/components/view/grid/GridViewFieldNumber.vue b/web-frontend/modules/database/components/view/grid/GridViewFieldNumber.vue index be822f12b..936fcf7f6 100644 --- a/web-frontend/modules/database/components/view/grid/GridViewFieldNumber.vue +++ b/web-frontend/modules/database/components/view/grid/GridViewFieldNumber.vue @@ -25,42 +25,17 @@ <script> import gridField from '@baserow/modules/database/mixins/gridField' import gridFieldInput from '@baserow/modules/database/mixins/gridFieldInput' +import numberField from '@baserow/modules/database/mixins/numberField' export default { - mixins: [gridField, gridFieldInput], + mixins: [gridField, gridFieldInput, numberField], methods: { - getError() { - if (this.copy === null || this.copy === '') { - return null - } - if (isNaN(parseFloat(this.copy)) || !isFinite(this.copy)) { - return 'Invalid number' - } - return null - }, - isValid() { - return this.getError() === null - }, afterEdit() { this.$nextTick(() => { this.$refs.input.focus() this.$refs.input.selectionStart = this.$refs.input.selectionEnd = 100000 }) }, - beforeSave(value) { - if (value === '' || isNaN(value) || value === undefined) { - return null - } - const decimalPlaces = - this.field.number_type === 'DECIMAL' - ? this.field.number_decimal_places - : 0 - let number = parseFloat(value) - if (!this.field.number_negative && number < 0) { - number = 0 - } - return number.toFixed(decimalPlaces) - }, }, } </script> diff --git a/web-frontend/modules/database/components/view/grid/GridViewFieldType.vue b/web-frontend/modules/database/components/view/grid/GridViewFieldType.vue index 04c9172ed..d75ed4c88 100644 --- a/web-frontend/modules/database/components/view/grid/GridViewFieldType.vue +++ b/web-frontend/modules/database/components/view/grid/GridViewFieldType.vue @@ -15,72 +15,23 @@ > <i class="fas fa-caret-down"></i> </a> - <Context ref="context"> - <ul class="context-menu"> - <li> - <a - ref="updateFieldContextLink" - class="grid-view-description-options" - @click=" - $refs.updateFieldContext.toggle( - $refs.updateFieldContextLink, - 'bottom', - 'right', - 0 - ) - " - > - <i class="context-menu-icon fas fa-fw fa-pen"></i> - Edit field - </a> - <UpdateFieldContext - ref="updateFieldContext" - :field="field" - @update="$refs.context.hide()" - ></UpdateFieldContext> - </li> - <li v-if="!field.primary"> - <a @click="deleteField(field)"> - <i class="context-menu-icon fas fa-fw fa-trash"></i> - Delete field - </a> - </li> - </ul> - </Context> + <FieldContext ref="context" :field="field"></FieldContext> <slot></slot> </div> </div> </template> <script> -import { notifyIf } from '@baserow/modules/core/utils/error' -import UpdateFieldContext from '@baserow/modules/database/components/field/UpdateFieldContext' +import FieldContext from '@baserow/modules/database/components/field/FieldContext' export default { name: 'GridViewFieldType', - components: { UpdateFieldContext }, + components: { FieldContext }, props: { field: { type: Object, required: true, }, }, - methods: { - setLoading(field, value) { - this.$store.dispatch('field/setItemLoading', { field, value }) - }, - async deleteField(field) { - this.$refs.context.hide() - this.setLoading(field, true) - - try { - await this.$store.dispatch('field/delete', field) - } catch (error) { - notifyIf(error, 'field') - } - - this.setLoading(field, false) - }, - }, } </script> diff --git a/web-frontend/modules/database/fieldTypes.js b/web-frontend/modules/database/fieldTypes.js index 76534f361..dc46e67fc 100644 --- a/web-frontend/modules/database/fieldTypes.js +++ b/web-frontend/modules/database/fieldTypes.js @@ -7,6 +7,10 @@ import GridViewFieldText from '@baserow/modules/database/components/view/grid/Gr import GridViewFieldNumber from '@baserow/modules/database/components/view/grid/GridViewFieldNumber' import GridViewFieldBoolean from '@baserow/modules/database/components/view/grid/GridViewFieldBoolean' +import RowEditFieldText from '@baserow/modules/database/components/row/RowEditFieldText' +import RowEditFieldNumber from '@baserow/modules/database/components/row/RowEditFieldNumber' +import RowEditFieldBoolean from '@baserow/modules/database/components/row/RowEditFieldBoolean' + export class FieldType extends Registerable { /** * The font awesome 5 icon name that is used as convenience for the user to @@ -37,7 +41,9 @@ export class FieldType extends Registerable { } /** - * @TODO make this depending on the view type. + * This grid view field component should represent the related row value of this + * type. It will only be used in the grid view and it also responsible for editing + * the value. */ getGridViewFieldComponent() { throw new Error( @@ -45,6 +51,17 @@ export class FieldType extends Registerable { ) } + /** + * 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 + * responsible for editing the value. + */ + getRowEditFieldComponent() { + throw new Error( + 'Not implement error. This method should return a component.' + ) + } + /** * Because we want to show a new row immediately after creating we need to have an * empty value to show right away. @@ -90,6 +107,13 @@ export class FieldType extends Registerable { name: this.name, } } + + /** + * Should return a for humans readable representation of the value. + */ + toHumanReadableString(field, value) { + return value + } } export class TextFieldType extends FieldType { @@ -113,6 +137,10 @@ export class TextFieldType extends FieldType { return GridViewFieldText } + getRowEditFieldComponent() { + return RowEditFieldText + } + getEmptyValue(field) { return field.text_default } @@ -138,6 +166,10 @@ export class NumberFieldType extends FieldType { getGridViewFieldComponent() { return GridViewFieldNumber } + + getRowEditFieldComponent() { + return RowEditFieldNumber + } } export class BooleanFieldType extends FieldType { @@ -157,6 +189,10 @@ export class BooleanFieldType extends FieldType { return GridViewFieldBoolean } + getRowEditFieldComponent() { + return RowEditFieldBoolean + } + getEmptyValue(field) { return false } diff --git a/web-frontend/modules/database/mixins/gridFieldInput.js b/web-frontend/modules/database/mixins/gridFieldInput.js index 91566d549..8ba7cabb2 100644 --- a/web-frontend/modules/database/mixins/gridFieldInput.js +++ b/web-frontend/modules/database/mixins/gridFieldInput.js @@ -8,11 +8,11 @@ export default { data() { return { /** - * Indicates whether of the user is editing the value. + * Indicates whether the user is editing the value. */ editing: false, /** - * A temporary copy of the value when editing. + * A temporary copy of the value when editing. */ copy: null, } diff --git a/web-frontend/modules/database/mixins/numberField.js b/web-frontend/modules/database/mixins/numberField.js new file mode 100644 index 000000000..49c860abf --- /dev/null +++ b/web-frontend/modules/database/mixins/numberField.js @@ -0,0 +1,43 @@ +/** + * This mixin contains some method overrides for validating and formatting the + * number field. This mixin is used in both the GridViewFieldNumber and + * RowEditFieldNumber components. + */ +export default { + methods: { + /** + * Generates a human readable error for the user if something is wrong. + */ + getError() { + if (this.copy === null || this.copy === '') { + return null + } + if (isNaN(parseFloat(this.copy)) || !isFinite(this.copy)) { + return 'Invalid number' + } + return null + }, + isValid() { + return this.getError() === null + }, + /** + * Formats the value based on the field's settings. The number will be rounded + * if to much decimal places are provided and if negative numbers aren't allowed + * they will be set to 0. + */ + beforeSave(value) { + if (value === '' || isNaN(value) || value === undefined) { + return null + } + const decimalPlaces = + this.field.number_type === 'DECIMAL' + ? this.field.number_decimal_places + : 0 + let number = parseFloat(value) + if (!this.field.number_negative && number < 0) { + number = 0 + } + return number.toFixed(decimalPlaces) + }, + }, +} diff --git a/web-frontend/modules/database/mixins/rowEditField.js b/web-frontend/modules/database/mixins/rowEditField.js new file mode 100644 index 000000000..0c7dc1f43 --- /dev/null +++ b/web-frontend/modules/database/mixins/rowEditField.js @@ -0,0 +1,25 @@ +/** + * A mixin that can be used by a row edit modal component. It introduces the props that + * will be passed by the RowEditModalField component. + */ +export default { + props: { + /** + * Contains the field type object. Because each field type can have different + * settings you need this in order to render the correct component or implement + * correct validation. + */ + field: { + type: Object, + required: true, + }, + /** + * The value of the grid field, this could for example for a number field 10, + * text field 'Random string' etc. + */ + value: { + type: [String, Number, Object, Boolean], + required: false, + }, + }, +} diff --git a/web-frontend/modules/database/mixins/rowEditFieldInput.js b/web-frontend/modules/database/mixins/rowEditFieldInput.js new file mode 100644 index 000000000..7fdd58e76 --- /dev/null +++ b/web-frontend/modules/database/mixins/rowEditFieldInput.js @@ -0,0 +1,84 @@ +/** + * This mixin can be used with a row edit field if the field only needs an input. For + * example for the text and number fields. It depends on the rowEditField mixin. + */ +export default { + data() { + return { + /** + * Indicates whether the user is editing the value. + */ + editing: false, + /** + * A temporary copy of the value when editing. + */ + copy: null, + } + }, + watch: { + value(value) { + if (!this.editing) { + this.copy = value + } + }, + }, + mounted() { + this.copy = this.value + }, + methods: { + /** + * Event that is called when the user starts editing the value. In this case we + * will only enable the editing state. + */ + select() { + this.editing = true + }, + /** + * Event that is called when the user finishes editing. If the value is not + * valid we aren't going to do anything because it can't be changed anyway and + * we want to give the user a change to fix the value. + */ + unselect() { + if (!this.isValid() || !this.editing) { + return + } + + this.editing = false + this.save() + }, + /** + * Saves the value if it has changed. Should only be called by the unselect + * method and not directly. + */ + save() { + const newValue = this.beforeSave(this.copy) + + // If the value hasn't changed we don't want to do anything. + if (newValue === this.value) { + this.copy = this.value + } else { + this.$emit('update', newValue, this.value) + this.afterSave() + } + }, + /** + * This method is called before saving the value. Optionally the value can be + * changed or formatted here if necessary. + */ + beforeSave(value) { + return value + }, + /** + * Method that is called after saving the value. This can be overridden in the + * component. + */ + afterSave() {}, + /** + * Should return a boolean if the copy that is going to be saved is valid. If it + * returns false unselecting is not possible. + */ + isValid() { + return true + }, + }, +} diff --git a/web-frontend/modules/database/store/view/grid.js b/web-frontend/modules/database/store/view/grid.js index 02804f02b..4070fc382 100644 --- a/web-frontend/modules/database/store/view/grid.js +++ b/web-frontend/modules/database/store/view/grid.js @@ -1,3 +1,4 @@ +import Vue from 'vue' import axios from 'axios' import _ from 'lodash' @@ -5,7 +6,10 @@ import GridService from '@baserow/modules/database/services/view/grid' import RowService from '@baserow/modules/database/services/row' export function populateRow(row) { - row._ = { loading: false } + row._ = { + loading: false, + hover: false, + } return row } @@ -118,7 +122,10 @@ export const mutations = { ADD_FIELD(state, { field, value }) { const name = `field_${field.id}` state.rows.forEach((row) => { - row[name] = value + // We have to use the Vue.set function here to make it reactive immediately. + // If we don't do this the value in the field components of the grid and modal + // don't have the correct value and will act strange. + Vue.set(row, name, value) }) }, SET_ROW_LOADING(state, { row, value }) { @@ -136,6 +143,9 @@ export const mutations = { }) } }, + SET_ROW_HOVER(state, { row, value }) { + row._.hover = value + }, } // Contains the timeout needed for the delayed delayed scroll top action. @@ -549,6 +559,9 @@ export const actions = { values, }) }, + setRowHover({ commit }, { row, value }) { + commit('SET_ROW_HOVER', { row, value }) + }, } export const getters = {