1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-10 23:50:12 +00:00

Merge branch 'feat/delete_confirmation' into 'develop'

Resolve "Add confirmation when the user wants to delete a table, database or group"

Closes 

See merge request 
This commit is contained in:
Bram Wiepjes 2020-10-28 18:35:24 +00:00
commit 731d6022b9
19 changed files with 579 additions and 65 deletions

View file

@ -7,7 +7,9 @@
* Added Ubuntu installation guide documentation.
* Added Email field.
* Added importer abstraction including a CSV and tabular paste importer.
* Added ability to navigate dropdown menus with arrow keys
* Added ability to navigate dropdown menus with arrow keys.
* Added confirmation modals when the user wants to delete a group, application, table,
view or field.
* Fixed bug in the web-frontend URL validation where a '*' was invalidates.
## Released (2020-10-06)

View file

@ -50,6 +50,28 @@ export class ApplicationType extends Registerable {
return null
}
/**
* Should return an array where the first element is the describing name of the
* dependents in singular and the second element in plural. Can be null if there
* aren't any dependants.
*
* Example: ['table', 'tables']
* Result in singular: There is 1 table
* Result in plural: There are 2 tables
*/
getDependentsName() {
return [null, null]
}
/**
* When deleting or listing an application we might want to give a quick overview
* which children / dependents there are. This method should return a list
* containing an object with an id, iconClass and name.
*/
getDependents() {
return []
}
constructor() {
super()
this.type = this.getType()

View file

@ -15,6 +15,11 @@ a {
cursor: pointer;
}
b,
strong {
font-weight: 700;
}
*,
*::before,
*::after {

View file

@ -40,3 +40,4 @@
@import 'grid';
@import 'table_preview';
@import 'file_upload';
@import 'delete_section';

View file

@ -0,0 +1,62 @@
.delete-section {
position: relative;
border: solid 1px $color-neutral-200;
border-radius: 3px;
padding: 31px 16px 16px 16px;
}
.delete-section__label {
display: flex;
align-items: center;
border: 1px solid $color-error-300;
border-radius: 3px;
background-color: $color-error-100;
color: $color-error-900;
line-height: 28px;
padding: 0 10px;
@include absolute(-14px, auto, auto, 12px);
}
.delete-section__label-icon {
background-color: $white;
border: solid 2px $color-error-400;
color: $color-error-900;
border-radius: 100%;
text-align: center;
margin-right: 8px;
height: 18px;
width: 18px;
line-height: 14px;
font-size: 8px;
}
.delete-section__list {
list-style: none;
margin: 0;
padding: 0;
li {
line-height: 18px;
font-size: 14px;
&:not(:last-child) {
margin-bottom: 12px;
}
}
small {
font-size: 12px;
color: $color-neutral-500;
}
}
.delete-section__list-icon {
@extend .fa-fw;
position: relative;
top: -1px;
color: $color-neutral-300;
font-size: 10px;
margin-right: 8px;
}

View file

@ -29,6 +29,7 @@
</a>
</li>
</ul>
<DeleteGroupModal ref="deleteGroupModal" :group="group" />
</Context>
</h2>
<ul class="dashboard__group-items">
@ -79,10 +80,14 @@
import { mapGetters } from 'vuex'
import CreateApplicationContext from '@baserow/modules/core/components/application/CreateApplicationContext'
import DeleteGroupModal from '@baserow/modules/core/components/group/DeleteGroupModal'
import editGroup from '@baserow/modules/core/mixins/editGroup'
export default {
components: { CreateApplicationContext },
components: {
CreateApplicationContext,
DeleteGroupModal,
},
mixins: [editGroup],
props: {
group: {

View file

@ -0,0 +1,106 @@
<template>
<Modal>
<h2 class="box__title">Delete {{ group.name }}</h2>
<Error :error="error"></Error>
<div>
<p>
Are you sure you want to delete the group
<strong>{{ group.name }}</strong
>?
<span v-if="applications.length > 0">
The following
<template v-if="applications.length == 1"
>application including its data is</template
>
<template v-else>applications including their data are</template>
going to be permanently deleted:</span
>
</p>
<div class="delete-section" v-if="applications.length > 0">
<div class="delete-section__label">
<div class="delete-section__label-icon">
<i class="fas fa-exclamation"></i>
</div>
Will also be permanently deleted
</div>
<ul class="delete-section__list">
<li v-for="application in applications" :key="application.id">
<i
class="delete-section__list-icon fas fa-database"
:class="'fa-' + application._.type.iconClass"
></i>
{{ application.name }}
<small>{{ getApplicationDependentsText(application) }}</small>
</li>
</ul>
</div>
<div class="actions">
<div class="align-right">
<button
class="button button--large button--error"
:class="{ 'button--loading': loading }"
:disabled="loading"
@click="deleteGroup()"
>
Delete group
</button>
</div>
</div>
</div>
</Modal>
</template>
<script>
import { mapGetters } from 'vuex'
import modal from '@baserow/modules/core/mixins/modal'
import error from '@baserow/modules/core/mixins/error'
export default {
name: 'DeleteGroupModal',
mixins: [modal, error],
props: {
group: {
type: Object,
required: true,
},
},
data() {
return {
loading: false,
}
},
computed: {
...mapGetters({
getAllOfGroup: 'application/getAllOfGroup',
}),
applications() {
return this.getAllOfGroup(this.group)
},
},
methods: {
async deleteGroup() {
this.hideError()
this.loading = true
try {
await this.$store.dispatch('group/delete', this.group)
this.hide()
} catch (error) {
this.handleError(error, 'application')
}
this.loading = false
},
getApplicationDependentsText(application) {
const dependents = this.$registry
.get('application', application.type)
.getDependents(application)
const names = this.$registry
.get('application', application.type)
.getDependentsName(application)
const name = dependents.length === 1 ? names[0] : names[1]
return `including ${dependents.length} ${name}`
},
},
}
</script>

View file

@ -35,15 +35,18 @@
</a>
</li>
</ul>
<DeleteGroupModal ref="deleteGroupModal" :group="group" />
</Context>
</li>
</template>
<script>
import DeleteGroupModal from '@baserow/modules/core/components/group/DeleteGroupModal'
import editGroup from '@baserow/modules/core/mixins/editGroup'
export default {
name: 'GroupsContextItem',
components: { DeleteGroupModal },
mixins: [editGroup],
props: {
group: {

View file

@ -0,0 +1,98 @@
<template>
<Modal>
<h2 class="box__title">Delete {{ application.name }}</h2>
<Error :error="error"></Error>
<div>
<p>
Are you sure you want to delete the
{{ application._.type.name | lowercase }}
<strong>{{ application.name }}</strong
>?
<span v-if="dependents.length > 0"
>The following {{ dependentsName }}
<template v-if="dependents.length === 1">is</template>
<template v-else>are</template>
also going to be permanently deleted:</span
>
</p>
<div v-if="dependents.length > 0" class="delete-section">
<div class="delete-section__label">
<div class="delete-section__label-icon">
<i class="fas fa-exclamation"></i>
</div>
Will also be permanently deleted
</div>
<ul class="delete-section__list">
<li v-for="dependent in dependents" :key="dependent.id">
<i
class="delete-section__list-icon fas fa-database"
:class="'fa-' + dependent.iconClass"
></i>
{{ dependent.name }}
</li>
</ul>
</div>
<div class="actions">
<div class="align-right">
<button
class="button button--large button--error"
:class="{ 'button--loading': loading }"
:disabled="loading"
@click="deleteApplication()"
>
Delete {{ application._.type.name | lowercase }}
</button>
</div>
</div>
</div>
</Modal>
</template>
<script>
import modal from '@baserow/modules/core/mixins/modal'
import error from '@baserow/modules/core/mixins/error'
export default {
name: 'DeleteApplicationModal',
mixins: [modal, error],
props: {
application: {
type: Object,
required: true,
},
},
data() {
return {
loading: false,
}
},
computed: {
dependentsName() {
const names = this.$registry
.get('application', this.application.type)
.getDependentsName(this.application)
return this.dependents.length === 1 ? names[0] : names[1]
},
dependents() {
return this.$registry
.get('application', this.application.type)
.getDependents(this.application)
},
},
methods: {
async deleteApplication() {
this.hideError()
this.loading = true
try {
await this.$store.dispatch('application/delete', this.application)
this.hide()
} catch (error) {
this.handleError(error, 'application')
}
this.loading = false
},
},
}
</script>

View file

@ -35,12 +35,16 @@
</a>
</li>
<li>
<a @click="deleteApplication(application)">
<a @click="deleteApplication()">
<i class="context__menu-icon fas fa-fw fa-trash"></i>
Delete {{ application._.type.name | lowercase }}
</a>
</li>
</ul>
<DeleteApplicationModal
ref="deleteApplicationModal"
:application="application"
/>
</Context>
</div>
<template
@ -58,9 +62,11 @@
<script>
import { notifyIf } from '@baserow/modules/core/utils/error'
import DeleteApplicationModal from './DeleteApplicationModal'
export default {
name: 'SidebarApplication',
components: { DeleteApplicationModal },
props: {
application: {
type: Object,
@ -126,17 +132,9 @@ export default {
}
)
},
async deleteApplication(application) {
deleteApplication() {
this.$refs.context.hide()
this.setLoading(application, true)
try {
await this.$store.dispatch('application/delete', application)
} catch (error) {
notifyIf(error, 'application')
}
this.setLoading(application, false)
this.$refs.deleteApplicationModal.show()
},
getSelectedApplicationComponent(application) {
const type = this.$registry.get('application', application.type)

View file

@ -41,17 +41,9 @@ export default {
this.setLoading(group, false)
},
async deleteGroup(group) {
deleteGroup(group) {
this.$refs.context.hide()
this.setLoading(group, true)
try {
await this.$store.dispatch('group/delete', group)
} catch (error) {
notifyIf(error, 'group')
}
this.setLoading(group, false)
this.$refs.deleteGroupModal.show()
},
},
}

View file

@ -754,6 +754,26 @@
dignissim mauris dictum imperdiet. Mauris ultrices ac eros at
fringilla. Praesent ut tincidunt dui.
</p>
<div class="delete-section">
<div class="delete-section__label">
<div class="delete-section__label-icon">
<i class="fas fa-exclamation"></i>
</div>
Will also be permanently deleted
</div>
<ul class="delete-section__list">
<li>
<i class="delete-section__list-icon fas fa-database"></i>
Vehicles
<small>including 12 tables</small>
</li>
<li>
<i class="delete-section__list-icon fas fa-database"></i>
Webshop
<small>including 12 tables</small>
</li>
</ul>
</div>
</div>
<div class="modal__box modal__box--with-sidebar">
<a class="modal__close">

View file

@ -19,6 +19,20 @@ export class DatabaseApplicationType extends ApplicationType {
return Sidebar
}
getDependentsName() {
return ['table', 'tables']
}
getDependents(database) {
return database.tables.map((table) => {
return {
id: table.id,
iconClass: 'table',
name: table.name,
}
})
}
populate(application) {
const values = super.populate(application)
values.tables.forEach((object, index, tables) => populateTable(object))

View file

@ -0,0 +1,71 @@
<template>
<Modal>
<h2 class="box__title">Delete {{ field.name }}</h2>
<Error :error="error"></Error>
<div>
<p>
Are you sure you want to delete the field
<strong>{{ field.name }}</strong
>? All the data, filters and sortings related to the field will be
permanently deleted.
</p>
<div class="actions">
<div class="align-right">
<button
class="button button--large button--error"
:class="{ 'button--loading': loading }"
:disabled="loading"
@click="deleteField()"
>
Delete field
</button>
</div>
</div>
</div>
</Modal>
</template>
<script>
import modal from '@baserow/modules/core/mixins/modal'
import error from '@baserow/modules/core/mixins/error'
export default {
name: 'DeleteFieldModal',
mixins: [modal, error],
props: {
field: {
type: Object,
required: true,
},
},
data() {
return {
loading: false,
}
},
methods: {
async deleteField() {
this.hideError()
this.loading = true
const { field } = this
try {
await this.$store.dispatch('field/deleteCall', field)
this.$emit('delete')
this.$store.dispatch('field/forceDelete', field)
this.hide()
} catch (error) {
if (error.response && error.response.status === 404) {
this.$emit('delete')
this.$store.dispatch('field/forceDelete', field)
this.hide()
} else {
this.handleError(error, 'field')
}
}
this.loading = false
},
},
}
</script>

View file

@ -26,23 +26,32 @@
</li>
<slot></slot>
<li v-if="!field.primary">
<a @click="deleteField(field)">
<a @click="deleteField()">
<i class="context__menu-icon fas fa-fw fa-trash"></i>
Delete field
</a>
</li>
</ul>
<DeleteFieldModal
v-if="!field.primary"
ref="deleteFieldModal"
:field="field"
@delete="$emit('delete')"
/>
</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'
import DeleteFieldModal from './DeleteFieldModal'
export default {
name: 'FieldContext',
components: { UpdateFieldContext },
components: {
UpdateFieldContext,
DeleteFieldModal,
},
mixins: [context],
props: {
table: {
@ -58,24 +67,9 @@ export default {
setLoading(field, value) {
this.$store.dispatch('field/setItemLoading', { field, value })
},
async deleteField(field) {
deleteField() {
this.$refs.context.hide()
this.setLoading(field, true)
try {
await this.$store.dispatch('field/deleteCall', field)
this.$emit('delete')
this.$store.dispatch('field/forceDelete', field)
} catch (error) {
if (error.response && error.response.status === 404) {
this.$emit('delete')
this.$store.dispatch('field/forceDelete', field)
} else {
notifyIf(error, 'field')
}
}
this.setLoading(field, false)
this.$refs.deleteFieldModal.show()
},
},
}

View file

@ -0,0 +1,66 @@
<template>
<Modal>
<h2 class="box__title">Delete {{ table.name }}</h2>
<Error :error="error"></Error>
<div>
<p>
Are you sure you want to delete the table
<strong>{{ table.name }}</strong
>? All the the related views and data will be permanently deleted.
</p>
<div class="actions">
<div class="align-right">
<button
class="button button--large button--error"
:class="{ 'button--loading': loading }"
:disabled="loading"
@click="deleteTable()"
>
Delete table
</button>
</div>
</div>
</div>
</Modal>
</template>
<script>
import modal from '@baserow/modules/core/mixins/modal'
import error from '@baserow/modules/core/mixins/error'
export default {
name: 'DeleteTableModal',
mixins: [modal, error],
props: {
database: {
type: Object,
required: true,
},
table: {
type: Object,
required: true,
},
},
data() {
return {
loading: false,
}
},
methods: {
async deleteTable() {
this.hideError()
this.loading = true
const { database, table } = this
try {
await this.$store.dispatch('table/delete', { database, table })
this.hide()
} catch (error) {
this.handleError(error, 'table')
}
this.loading = false
},
},
}
</script>

View file

@ -24,21 +24,28 @@
</a>
</li>
<li>
<a @click="deleteTable(database, table)">
<a @click="deleteTable()">
<i class="context__menu-icon fas fa-fw fa-trash"></i>
Delete
</a>
</li>
</ul>
<DeleteTableModal
ref="deleteTableModal"
:database="database"
:table="table"
/>
</Context>
</li>
</template>
<script>
import { notifyIf } from '@baserow/modules/core/utils/error'
import DeleteTableModal from './DeleteTableModal'
export default {
name: 'SidebarItem',
components: { DeleteTableModal },
props: {
database: {
type: Object,
@ -75,17 +82,9 @@ export default {
}
)
},
async deleteTable(database, table) {
deleteTable() {
this.$refs.context.hide()
this.setLoading(database, true)
try {
await this.$store.dispatch('table/delete', { database, table })
} catch (error) {
notifyIf(error, 'table')
}
this.setLoading(database, false)
this.$refs.deleteTableModal.show()
},
enableRename() {
this.$refs.context.hide()

View file

@ -0,0 +1,61 @@
<template>
<Modal>
<h2 class="box__title">Delete {{ view.name }}</h2>
<Error :error="error"></Error>
<div>
<p>
Are you sure you want to delete the view <strong>{{ view.name }}</strong
>? The table data will be preserved, but the filters, sortings and field
widths related to the view will be deleted.
</p>
<div class="actions">
<div class="align-right">
<button
class="button button--large button--error"
:class="{ 'button--loading': loading }"
:disabled="loading"
@click="deleteView()"
>
Delete view
</button>
</div>
</div>
</div>
</Modal>
</template>
<script>
import modal from '@baserow/modules/core/mixins/modal'
import error from '@baserow/modules/core/mixins/error'
export default {
name: 'DeleteViewModal',
mixins: [modal, error],
props: {
view: {
type: Object,
required: true,
},
},
data() {
return {
loading: false,
}
},
methods: {
async deleteView() {
this.hideError()
this.loading = true
try {
await this.$store.dispatch('view/delete', this.view)
this.hide()
} catch (error) {
this.handleError(error, 'view')
}
this.loading = false
},
},
}
</script>

View file

@ -33,22 +33,25 @@
</a>
</li>
<li>
<a @click="deleteView(view)">
<a @click="deleteView()">
<i class="context__menu-icon fas fa-fw fa-trash"></i>
Delete view
</a>
</li>
</ul>
</Context>
<DeleteViewModal ref="deleteViewModal" :view="view" />
</li>
</template>
<script>
import context from '@baserow/modules/core/mixins/context'
import { notifyIf } from '@baserow/modules/core/utils/error'
import DeleteViewModal from './DeleteViewModal'
export default {
name: 'ViewsContextItem',
components: { DeleteViewModal },
mixins: [context],
props: {
view: {
@ -80,17 +83,9 @@ export default {
this.setLoading(view, false)
},
async deleteView(view) {
deleteView() {
this.$refs.context.hide()
this.setLoading(view, true)
try {
await this.$store.dispatch('view/delete', view)
} catch (error) {
notifyIf(error, 'view')
}
this.setLoading(view, false)
this.$refs.deleteViewModal.show()
},
selectView(view) {
this.$nuxt.$router.push({