mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-07 14:25:37 +00:00
Resolve "Show database and tables duplication job progress in the left sidebar"
This commit is contained in:
parent
5a60bb0d9a
commit
335a40255d
23 changed files with 688 additions and 194 deletions
backend/src/baserow
changelog.mdweb-frontend/modules
core
assets/scss/components
components/sidebar
Sidebar.vueSidebarApplication.vueSidebarApplicationPendingJob.vueSidebarDuplicateApplicationContextItem.vue
jobTypes.jslocales
mixins
plugin.jsstore
database
|
@ -26,12 +26,7 @@ from baserow.contrib.database.views.handler import ViewHandler
|
|||
from baserow.contrib.database.views.view_types import GridViewType
|
||||
from baserow.core.registries import application_type_registry
|
||||
from baserow.core.trash.handler import TrashHandler
|
||||
from baserow.core.utils import (
|
||||
ChildProgressBuilder,
|
||||
Progress,
|
||||
find_unused_name,
|
||||
split_ending_number,
|
||||
)
|
||||
from baserow.core.utils import ChildProgressBuilder, Progress, find_unused_name
|
||||
|
||||
from .constants import TABLE_CREATION
|
||||
from .exceptions import (
|
||||
|
@ -410,8 +405,7 @@ class TableHandler:
|
|||
"""
|
||||
|
||||
existing_tables_names = list(database.table_set.values_list("name", flat=True))
|
||||
name, _ = split_ending_number(proposed_name)
|
||||
return find_unused_name([name], existing_tables_names, max_length=255)
|
||||
return find_unused_name([proposed_name], existing_tables_names, max_length=255)
|
||||
|
||||
def _create_related_link_fields_in_existing_tables_to_import(
|
||||
self, serialized_table: Dict[str, Any], id_mapping: Dict[str, Any]
|
||||
|
@ -476,18 +470,20 @@ class TableHandler:
|
|||
if not isinstance(table, Table):
|
||||
raise ValueError("The table is not an instance of Table")
|
||||
|
||||
progress = ChildProgressBuilder.build(progress_builder, child_total=2)
|
||||
start_progress, export_progress, import_progress = 10, 30, 60
|
||||
progress = ChildProgressBuilder.build(progress_builder, child_total=100)
|
||||
progress.increment(by=start_progress)
|
||||
|
||||
database = table.database
|
||||
database.group.has_user(user, raise_error=True)
|
||||
database_type = application_type_registry.get_by_model(database)
|
||||
|
||||
serialized_tables = database_type.export_tables_serialized([table])
|
||||
progress.increment()
|
||||
|
||||
# Set a unique name for the table to import back as a new one.
|
||||
exported_table = serialized_tables[0]
|
||||
exported_table["name"] = self.find_unused_table_name(database, table.name)
|
||||
exported_table["order"] = Table.get_last_order(database)
|
||||
|
||||
id_mapping: Dict[str, Any] = {"database_tables": {}}
|
||||
|
||||
|
@ -496,13 +492,17 @@ class TableHandler:
|
|||
exported_table, id_mapping
|
||||
)
|
||||
)
|
||||
progress.increment(by=export_progress)
|
||||
|
||||
imported_tables = database_type.import_tables_serialized(
|
||||
database,
|
||||
[exported_table],
|
||||
id_mapping,
|
||||
external_table_fields_to_import=link_fields_to_import_to_existing_tables,
|
||||
progress_builder=progress.create_child_builder(
|
||||
represents_progress=import_progress
|
||||
),
|
||||
)
|
||||
progress.increment()
|
||||
|
||||
new_table_clone = imported_tables[0]
|
||||
|
||||
|
|
|
@ -53,7 +53,9 @@ class DuplicateTableJobType(JobType):
|
|||
new_table_clone = action_type_registry.get_by_type(DuplicateTableActionType).do(
|
||||
job.user,
|
||||
job.original_table,
|
||||
progress.create_child_builder(represents_progress=progress.total),
|
||||
progress_builder=progress.create_child_builder(
|
||||
represents_progress=progress.total
|
||||
),
|
||||
)
|
||||
|
||||
# update the job with the new duplicated table
|
||||
|
|
|
@ -847,25 +847,32 @@ class CoreHandler:
|
|||
group = application.group
|
||||
group.has_user(user, raise_error=True)
|
||||
|
||||
progress = ChildProgressBuilder.build(progress_builder, child_total=2)
|
||||
start_progress, export_progress, import_progress = 10, 30, 60
|
||||
progress = ChildProgressBuilder.build(progress_builder, child_total=100)
|
||||
progress.increment(by=start_progress)
|
||||
|
||||
# export the application
|
||||
specific_application = application.specific
|
||||
application_type = application_type_registry.get_by_model(specific_application)
|
||||
serialized = application_type.export_serialized(specific_application)
|
||||
progress.increment()
|
||||
progress.increment(by=export_progress)
|
||||
|
||||
# Set a new unique name for the new application
|
||||
serialized["name"] = self.find_unused_application_name(
|
||||
group.id, serialized["name"]
|
||||
)
|
||||
serialized["order"] = application_type.model_class.get_last_order(group)
|
||||
|
||||
# import it back as a new application
|
||||
id_mapping: Dict[str, Any] = {}
|
||||
new_application_clone = application_type.import_serialized(
|
||||
group, serialized, id_mapping
|
||||
group,
|
||||
serialized,
|
||||
id_mapping,
|
||||
progress_builder=progress.create_child_builder(
|
||||
represents_progress=import_progress
|
||||
),
|
||||
)
|
||||
progress.increment()
|
||||
|
||||
# broadcast the application_created signal
|
||||
application_created.send(
|
||||
|
|
|
@ -45,6 +45,7 @@ For example:
|
|||
* Add API token authentication support to multipart and via-URL file uploads. [#255](https://gitlab.com/bramw/baserow/-/issues/255)
|
||||
* Add a rich preview while importing data to an existing table. [#1120](https://gitlab.com/bramw/baserow/-/issues/1120)
|
||||
* Add env vars for controlling which URLs and IPs webhooks are allowed to use. [#931](https://gitlab.com/bramw/baserow/-/issues/931)
|
||||
* Show database and table duplication progress in the left sidebar. [#1059](https://gitlab.com/bramw/baserow/-/issues/1059)
|
||||
|
||||
### Bug Fixes
|
||||
* Resolve circular dependency in `FieldWithFiltersAndSortsSerializer` [#1113](https://gitlab.com/bramw/baserow/-/issues/1113)
|
||||
|
|
|
@ -10,6 +10,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.tree:not(:last-child) {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.tree__item {
|
||||
@extend %first-last-no-margin;
|
||||
|
||||
|
@ -40,6 +44,11 @@
|
|||
padding: 0 6px;
|
||||
border-radius: 3px;
|
||||
|
||||
&.tree__action--disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:not(.tree__action--disabled):hover {
|
||||
background-color: $color-neutral-100;
|
||||
}
|
||||
|
@ -79,6 +88,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
.tree__progress_percentage {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
right: 35px;
|
||||
color: $color-primary-500;
|
||||
}
|
||||
|
||||
.tree__icon {
|
||||
@extend %tree__size;
|
||||
|
||||
|
@ -135,6 +151,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
.tree__subs:not(:last-of-type) {
|
||||
padding: 0;
|
||||
|
||||
& .tree__sub:last-child::before {
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.tree__sub-link {
|
||||
@extend %tree_sub-size;
|
||||
@extend %ellipsis;
|
||||
|
@ -154,6 +178,23 @@
|
|||
}
|
||||
}
|
||||
|
||||
.tree__sub-link--loading {
|
||||
@include loading(14px);
|
||||
@include absolute(9px, 9px, auto, auto);
|
||||
}
|
||||
|
||||
.tree__sub-link--disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
color: $color-primary-900;
|
||||
opacity: 0.6;
|
||||
text-decoration: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.tree__sub-add {
|
||||
display: inline-block;
|
||||
margin: 0 0 10px 10px;
|
||||
|
|
|
@ -179,6 +179,15 @@
|
|||
:group="selectedGroup"
|
||||
></component>
|
||||
</ul>
|
||||
<ul v-if="pendingJobs.length" class="tree">
|
||||
<component
|
||||
:is="getPendingJobComponent(job)"
|
||||
v-for="job in pendingJobs"
|
||||
:key="job.id"
|
||||
:job="job"
|
||||
>
|
||||
</component>
|
||||
</ul>
|
||||
<li class="sidebar__new-wrapper">
|
||||
<a
|
||||
ref="createApplicationContextLink"
|
||||
|
@ -329,6 +338,13 @@ export default {
|
|||
.map((plugin) => plugin.getSidebarMainMenuComponent())
|
||||
.filter((component) => component !== null)
|
||||
},
|
||||
pendingJobs() {
|
||||
return this.$store.getters['job/getAll'].filter((job) =>
|
||||
this.$registry
|
||||
.get('job', job.type)
|
||||
.isJobPartOfGroup(job, this.selectedGroup)
|
||||
)
|
||||
},
|
||||
/**
|
||||
* Indicates whether the current user is visiting an admin page.
|
||||
*/
|
||||
|
@ -358,6 +374,9 @@ export default {
|
|||
.get('application', application.type)
|
||||
.getSidebarComponent()
|
||||
},
|
||||
getPendingJobComponent(job) {
|
||||
return this.$registry.get('job', job.type).getSidebarComponent()
|
||||
},
|
||||
logoff() {
|
||||
this.$store.dispatch('auth/logoff')
|
||||
this.$nuxt.$router.push({ name: 'login' })
|
||||
|
|
|
@ -38,27 +38,18 @@
|
|||
<a @click="enableRename()">
|
||||
<i class="context__menu-icon fas fa-fw fa-pen"></i>
|
||||
{{
|
||||
$t('sidebarApplication.renameApplication', {
|
||||
$t('sidebarApplication.rename', {
|
||||
type: application._.type.name.toLowerCase(),
|
||||
})
|
||||
}}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
:class="{
|
||||
'context__menu-item--loading': duplicateLoading,
|
||||
disabled: duplicateLoading || deleteLoading,
|
||||
}"
|
||||
@click="duplicateApplication()"
|
||||
>
|
||||
<i class="context__menu-icon fas fa-fw fa-copy"></i>
|
||||
{{
|
||||
$t('sidebarApplication.duplicateApplication', {
|
||||
type: application._.type.name.toLowerCase(),
|
||||
})
|
||||
}}
|
||||
</a>
|
||||
<SidebarDuplicateApplicationContextItem
|
||||
:application="application"
|
||||
:disabled="deleting"
|
||||
@click="$refs.context.hide()"
|
||||
></SidebarDuplicateApplicationContextItem>
|
||||
</li>
|
||||
<li>
|
||||
<a @click="openSnapshots">
|
||||
|
@ -78,12 +69,12 @@
|
|||
</li>
|
||||
<li>
|
||||
<a
|
||||
:class="{ 'context__menu-item--loading': deleteLoading }"
|
||||
:class="{ 'context__menu-item--loading': deleting }"
|
||||
@click="deleteApplication()"
|
||||
>
|
||||
<i class="context__menu-icon fas fa-fw fa-trash"></i>
|
||||
{{
|
||||
$t('sidebarApplication.deleteApplication', {
|
||||
$t('sidebarApplication.delete', {
|
||||
type: application._.type.name.toLowerCase(),
|
||||
})
|
||||
}}
|
||||
|
@ -103,17 +94,18 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||
import ApplicationService from '@baserow/modules/core/services/application'
|
||||
import jobProgress from '@baserow/modules/core/mixins/jobProgress'
|
||||
import SidebarDuplicateApplicationContextItem from '@baserow/modules/core/components/sidebar/SidebarDuplicateApplicationContextItem.vue'
|
||||
import TrashModal from '@baserow/modules/core/components/trash/TrashModal'
|
||||
import SnapshotsModal from '@baserow/modules/core/components/snapshots/SnapshotsModal'
|
||||
|
||||
export default {
|
||||
name: 'SidebarApplication',
|
||||
components: { TrashModal, SnapshotsModal },
|
||||
mixins: [jobProgress],
|
||||
components: {
|
||||
TrashModal,
|
||||
SidebarDuplicateApplicationContextItem,
|
||||
SnapshotsModal,
|
||||
},
|
||||
props: {
|
||||
application: {
|
||||
type: Object,
|
||||
|
@ -126,18 +118,9 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
deleteLoading: false,
|
||||
duplicateLoading: false,
|
||||
deleting: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
selectedTable: 'table/getSelected',
|
||||
}),
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.stopPollIfRunning()
|
||||
},
|
||||
methods: {
|
||||
setLoading(application, value) {
|
||||
this.$store.dispatch('application/setItemLoading', {
|
||||
|
@ -166,92 +149,12 @@ export default {
|
|||
|
||||
this.setLoading(application, false)
|
||||
},
|
||||
showError(title, message) {
|
||||
this.$store.dispatch(
|
||||
'notification/error',
|
||||
{ title, message },
|
||||
{ root: true }
|
||||
)
|
||||
},
|
||||
// eslint-disable-next-line require-await
|
||||
async onJobFailed() {
|
||||
this.duplicateLoading = false
|
||||
this.$refs.context.hide()
|
||||
this.showError(
|
||||
this.$t('clientHandler.notCompletedTitle'),
|
||||
this.$t('clientHandler.notCompletedDescription')
|
||||
)
|
||||
},
|
||||
// eslint-disable-next-line require-await
|
||||
async onJobPollingError(error) {
|
||||
this.duplicateLoading = false
|
||||
this.$refs.context.hide()
|
||||
notifyIf(error, 'application')
|
||||
},
|
||||
async onJobDone() {
|
||||
const newApplicationId = this.job.duplicated_application.id
|
||||
let newApplication
|
||||
try {
|
||||
newApplication = await this.$store.dispatch('application/fetch', {
|
||||
applicationId: newApplicationId,
|
||||
})
|
||||
} catch (error) {
|
||||
notifyIf(error, 'application')
|
||||
} finally {
|
||||
this.duplicateLoading = false
|
||||
this.$refs.context.hide()
|
||||
}
|
||||
|
||||
// find the matching table in the duplicated application if any
|
||||
// otherwise just select the first table and show it
|
||||
if (newApplication) {
|
||||
if (newApplication.tables.length) {
|
||||
let selectTable = newApplication.tables[0]
|
||||
const originalSelectedTable = this.selectedTable
|
||||
if (originalSelectedTable) {
|
||||
for (const table of newApplication.tables) {
|
||||
if (table.name === originalSelectedTable.name) {
|
||||
selectTable = table
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
this.$nuxt.$router.push({
|
||||
name: 'database-table',
|
||||
params: {
|
||||
databaseId: newApplication.id,
|
||||
tableId: selectTable.id,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
this.$emit('selected', newApplication)
|
||||
}
|
||||
}
|
||||
},
|
||||
async duplicateApplication() {
|
||||
if (this.duplicateLoading || this.deleteLoading) {
|
||||
return
|
||||
}
|
||||
|
||||
const application = this.application
|
||||
this.duplicateLoading = true
|
||||
|
||||
try {
|
||||
const { data: job } = await ApplicationService(
|
||||
this.$client
|
||||
).asyncDuplicate(application.id)
|
||||
this.startJobPoller(job)
|
||||
} catch (error) {
|
||||
this.duplicateLoading = false
|
||||
notifyIf(error, 'application')
|
||||
}
|
||||
},
|
||||
async deleteApplication() {
|
||||
if (this.deleteLoading) {
|
||||
if (this.deleting) {
|
||||
return
|
||||
}
|
||||
|
||||
this.deleteLoading = true
|
||||
this.deleting = true
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('application/delete', this.application)
|
||||
|
@ -263,7 +166,7 @@ export default {
|
|||
notifyIf(error, 'application')
|
||||
}
|
||||
|
||||
this.deleteLoading = false
|
||||
this.deleting = false
|
||||
},
|
||||
showApplicationTrashModal() {
|
||||
this.$refs.context.hide()
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
<template>
|
||||
<li class="tree__item tree__item--loading">
|
||||
<div class="tree__action tree__action--disabled">
|
||||
<a class="tree__link">
|
||||
<i
|
||||
class="tree__icon tree__icon--type fas"
|
||||
:class="'fa-' + jobIconClass"
|
||||
></i>
|
||||
{{ jobSidebarText }}
|
||||
<div class="tree__progress_percentage">
|
||||
{{ job.progress_percentage }} %
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SidebarApplicationPendingJob',
|
||||
props: {
|
||||
job: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
jobSidebarText() {
|
||||
return this.$registry.get('job', this.job.type).getSidebarText(this.job)
|
||||
},
|
||||
jobIconClass() {
|
||||
return this.$registry.get('job', this.job.type).getIconClass()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,98 @@
|
|||
<template>
|
||||
<a
|
||||
:class="{
|
||||
'context__menu-item--loading': duplicating,
|
||||
disabled: disabled || duplicating,
|
||||
}"
|
||||
@click="duplicateApplication()"
|
||||
>
|
||||
<i class="context__menu-icon fas fa-fw fa-copy"></i>
|
||||
{{
|
||||
$t('sidebarApplication.duplicate', {
|
||||
type: application._.type.name.toLowerCase(),
|
||||
})
|
||||
}}
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||
import ApplicationService from '@baserow/modules/core/services/application'
|
||||
import jobProgress from '@baserow/modules/core/mixins/jobProgress'
|
||||
|
||||
export default {
|
||||
name: 'SidebarDuplicateApplicationContextItem',
|
||||
mixins: [jobProgress],
|
||||
props: {
|
||||
application: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
duplicating: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showError(title, message) {
|
||||
this.$store.dispatch(
|
||||
'notification/error',
|
||||
{ title, message },
|
||||
{ root: true }
|
||||
)
|
||||
},
|
||||
// eslint-disable-next-line require-await
|
||||
async onJobFailed() {
|
||||
await this.$store.dispatch('job/forceUpdate', {
|
||||
job: this.job,
|
||||
data: this.job,
|
||||
})
|
||||
|
||||
this.duplicating = false
|
||||
},
|
||||
async onJobPollingError(error) {
|
||||
await this.$store.dispatch('job/forceDelete', this.job)
|
||||
this.duplicating = false
|
||||
notifyIf(error, 'application')
|
||||
},
|
||||
async onJobUpdated() {
|
||||
await this.$store.dispatch('job/forceUpdate', {
|
||||
job: this.job,
|
||||
data: this.job,
|
||||
})
|
||||
},
|
||||
// eslint-disable-next-line require-await
|
||||
async onJobDone() {
|
||||
await this.$store.dispatch('job/forceUpdate', {
|
||||
job: this.job,
|
||||
data: this.job,
|
||||
})
|
||||
this.duplicating = false
|
||||
},
|
||||
async duplicateApplication() {
|
||||
if (this.duplicating || this.disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
this.duplicating = true
|
||||
try {
|
||||
const { data: job } = await ApplicationService(
|
||||
this.$client
|
||||
).asyncDuplicate(this.application.id)
|
||||
this.startJobPoller(job)
|
||||
this.$store.dispatch('job/forceCreate', job)
|
||||
} catch (error) {
|
||||
this.duplicating = false
|
||||
notifyIf(error, 'application')
|
||||
}
|
||||
this.$emit('click')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
159
web-frontend/modules/core/jobTypes.js
Normal file
159
web-frontend/modules/core/jobTypes.js
Normal file
|
@ -0,0 +1,159 @@
|
|||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||
import { Registerable } from '@baserow/modules/core/registry'
|
||||
import SidebarApplicationPendingJob from '@baserow/modules/core/components/sidebar/SidebarApplicationPendingJob'
|
||||
|
||||
/**
|
||||
* The job type base class that can be extended when creating a plugin
|
||||
* for the frontend.
|
||||
*/
|
||||
export class JobType extends Registerable {
|
||||
/**
|
||||
* The font awesome 5 icon name that is used as convenience for the user to
|
||||
* recognize certain job types. If you for example want the database
|
||||
* icon, you must return 'database' here. This will result in the classname
|
||||
* 'fas fa-database'.
|
||||
*/
|
||||
getIconClass() {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* A human readable name of the job type.
|
||||
*/
|
||||
getName() {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the text to show in the sidebar when the job is running.
|
||||
*/
|
||||
getSidebarText(job) {
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* The sidebar component will be rendered in the sidebar if the application is
|
||||
* in the selected group. All the applications of a group are listed in the
|
||||
* sidebar and this component should give the user the possibility to select
|
||||
* that application.
|
||||
*/
|
||||
getSidebarComponent() {
|
||||
return null
|
||||
}
|
||||
|
||||
constructor(...args) {
|
||||
super(...args)
|
||||
this.type = this.getType()
|
||||
|
||||
if (this.type === null) {
|
||||
throw new Error('The type name of an application type must be set.')
|
||||
}
|
||||
if (this.name === null) {
|
||||
throw new Error('The name of an application type must be set.')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Every time a fresh job object is fetched from the backend, it will
|
||||
* be populated, this is the moment to update some values. Because each
|
||||
* jobType can have unique properties, they might need to be populated.
|
||||
* This method can be overwritten in order the populate the correct values.
|
||||
*/
|
||||
populate(job) {
|
||||
return job
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the specific job is part of the group
|
||||
*/
|
||||
isJobPartOfGroup(job, group) {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the specific job is part of the application
|
||||
*/
|
||||
isJobPartOfApplication(job, application) {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* When an application is deleted it could be that an action should be taken,
|
||||
* like redirect the user to another page. This method is called when application
|
||||
* of this type is deleted.
|
||||
*/
|
||||
beforeDelete(job) {}
|
||||
|
||||
/**
|
||||
* Before the application values are updated, they can be modified here. This
|
||||
* might be needed because providing certain values could break the update.
|
||||
*/
|
||||
async beforeUpdate(job, data) {}
|
||||
|
||||
async afterUpdate(job, data) {
|
||||
if (job.state === 'finished') {
|
||||
await this.onJobDone(job, data)
|
||||
} else if (job.state === 'failed') {
|
||||
await this.onJobFailed(job, data)
|
||||
}
|
||||
}
|
||||
|
||||
async onJobDone(job) {}
|
||||
async onJobFailed(job) {}
|
||||
}
|
||||
|
||||
export class DuplicateApplicationJobType extends JobType {
|
||||
static getType() {
|
||||
return 'duplicate_application'
|
||||
}
|
||||
|
||||
getIconClass() {
|
||||
// TODO: This should be moved to a registry and in the database module.
|
||||
return 'database'
|
||||
}
|
||||
|
||||
getName() {
|
||||
return 'duplicateApplication'
|
||||
}
|
||||
|
||||
getSidebarText(job) {
|
||||
const { i18n } = this.app
|
||||
return i18n.t('duplicateApplicationJobType.duplicating') + '... '
|
||||
}
|
||||
|
||||
getSidebarComponent() {
|
||||
return SidebarApplicationPendingJob
|
||||
}
|
||||
|
||||
isJobPartOfGroup(job, group) {
|
||||
return job.original_application.group.id === group.id
|
||||
}
|
||||
|
||||
async onJobDone(job) {
|
||||
const { i18n, store } = this.app
|
||||
const application = job.duplicated_application
|
||||
try {
|
||||
await store.dispatch('application/fetch', application.id)
|
||||
store.dispatch('notification/info', {
|
||||
title: i18n.t('duplicateApplicationJobType.duplicatedTitle'),
|
||||
message: application.name,
|
||||
})
|
||||
await store.dispatch('job/forceDelete', job)
|
||||
} catch (error) {
|
||||
notifyIf(error, 'application')
|
||||
}
|
||||
}
|
||||
|
||||
async onJobFailed(job) {
|
||||
const { i18n, store } = this.app
|
||||
await store.dispatch(
|
||||
'notification/error',
|
||||
{
|
||||
title: i18n.t('clientHandler.notCompletedTitle'),
|
||||
message: i18n.t('clientHandler.notCompletedDescription'),
|
||||
},
|
||||
{ root: true }
|
||||
)
|
||||
await this.app.store.dispatch('job/forceDelete', job)
|
||||
}
|
||||
}
|
|
@ -7,12 +7,16 @@
|
|||
"label": "Copied!"
|
||||
},
|
||||
"sidebarApplication": {
|
||||
"renameApplication": "Rename {type}",
|
||||
"duplicateApplication": "Duplicate {type}",
|
||||
"rename": "Rename",
|
||||
"duplicate": "Duplicate",
|
||||
"viewTrash": "View trash",
|
||||
"deleteApplication": "Delete {type}",
|
||||
"delete": "Delete",
|
||||
"snapshots": "Snapshots"
|
||||
},
|
||||
"duplicateApplicationJobType": {
|
||||
"duplicating": "Duplicating",
|
||||
"duplicatedTitle": "Application duplicated"
|
||||
},
|
||||
"sidebar": {
|
||||
"createGroup": "Create group",
|
||||
"inviteOthers": "Invite others",
|
||||
|
@ -397,4 +401,4 @@
|
|||
"monthsAgo": "0 months ago | 1 month ago | {n} months ago",
|
||||
"yearsAgo": "0 years ago | 1 year ago | {n} years ago"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,35 +4,34 @@ import JobService from '@baserow/modules/core/services/job'
|
|||
* To use this mixin you need to create the following methods on your component:
|
||||
* - `getCustomHumanReadableJobState(state)` returns the human readable message your
|
||||
* custom state values.
|
||||
* - onJobDone() (optional) is called when the job is finished
|
||||
* - onJobFailed() (optional) is called if the job failed
|
||||
* - onJobPollingError(error) (optional) is there is an exception during the job polling
|
||||
* - onJobUpdated() (optional) is called during the polling for not finished jobs.
|
||||
* - onJobDone() (optional) is called if the job successfully finishes.
|
||||
* - onJobFailed() (optional) is called if the job fails.
|
||||
* - onJobPollingError(error) (optional) is called if the polling fails.
|
||||
*
|
||||
*/
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
job: null,
|
||||
pollInterval: null,
|
||||
nextPollTimeout: null,
|
||||
pollTimeoutId: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
jobIsRunning() {
|
||||
return (
|
||||
this.job !== null && !['failed', 'finished'].includes(this.job.state)
|
||||
)
|
||||
},
|
||||
jobIsFinished() {
|
||||
return (
|
||||
this.job !== null && ['failed', 'finished'].includes(this.job.state)
|
||||
)
|
||||
},
|
||||
jobHasSucceeded() {
|
||||
return this.job?.state === 'finished'
|
||||
},
|
||||
jobHasFailed() {
|
||||
return this.job?.state === 'failed'
|
||||
},
|
||||
jobIsFinished() {
|
||||
return this.jobHasSucceeded || this.jobHasFailed
|
||||
},
|
||||
jobIsRunning() {
|
||||
return this.job !== null && !this.jobIsFinished
|
||||
},
|
||||
|
||||
jobHumanReadableState() {
|
||||
if (this.job === null) {
|
||||
return ''
|
||||
|
@ -57,35 +56,46 @@ export default {
|
|||
*/
|
||||
startJobPoller(job) {
|
||||
this.job = job
|
||||
this.pollInterval = setTimeout(this.getLatestJobInfo, 1000)
|
||||
this.nextPollTimeout = 200
|
||||
this.pollTimeoutId = setTimeout(
|
||||
this.getLatestJobInfo,
|
||||
this.nextPollTimeout
|
||||
)
|
||||
},
|
||||
async getLatestJobInfo() {
|
||||
try {
|
||||
const { data } = await JobService(this.$client).get(this.job.id)
|
||||
this.job = data
|
||||
if (this.jobHasFailed) {
|
||||
if (this.onJobFailed) {
|
||||
const { data: job } = await JobService(this.$client).get(this.job.id)
|
||||
this.job = job
|
||||
if (job.state === 'failed') {
|
||||
if (typeof this.onJobFailed === 'function') {
|
||||
await this.onJobFailed()
|
||||
}
|
||||
} else if (!this.jobIsRunning) {
|
||||
if (this.onJobDone) {
|
||||
} else if (job.state === 'finished') {
|
||||
if (typeof this.onJobDone === 'function') {
|
||||
await this.onJobDone()
|
||||
}
|
||||
} else {
|
||||
// job unfinished, set the next polling timeout
|
||||
this.pollInterval = setTimeout(this.getLatestJobInfo, 1000)
|
||||
// job unfinished, keep polling
|
||||
if (typeof this.onJobUpdated === 'function') {
|
||||
await this.onJobUpdated()
|
||||
}
|
||||
this.nextPollTimeout = Math.min(this.nextPollTimeout * 1.5, 2500)
|
||||
this.pollTimeoutId = setTimeout(
|
||||
this.getLatestJobInfo,
|
||||
this.nextPollTimeout
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.onJobPollingError) {
|
||||
if (typeof this.onJobPollingError === 'function') {
|
||||
this.onJobPollingError(error)
|
||||
}
|
||||
this.job = null
|
||||
}
|
||||
},
|
||||
stopPollIfRunning() {
|
||||
if (this.pollInterval) {
|
||||
clearTimeout(this.pollInterval)
|
||||
this.pollInterval = null
|
||||
if (this.pollTimeoutId) {
|
||||
clearTimeout(this.pollTimeoutId)
|
||||
this.pollTimeoutId = null
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Vue from 'vue'
|
||||
|
||||
import { Registry } from '@baserow/modules/core/registry'
|
||||
import { DuplicateApplicationJobType } from '@baserow/modules/core/jobTypes'
|
||||
|
||||
import {
|
||||
AccountSettingsType,
|
||||
|
@ -17,6 +18,7 @@ import settingsStore from '@baserow/modules/core/store/settings'
|
|||
import applicationStore from '@baserow/modules/core/store/application'
|
||||
import authStore from '@baserow/modules/core/store/auth'
|
||||
import groupStore from '@baserow/modules/core/store/group'
|
||||
import jobStore from '@baserow/modules/core/store/job'
|
||||
import notificationStore from '@baserow/modules/core/store/notification'
|
||||
import sidebarStore from '@baserow/modules/core/store/sidebar'
|
||||
import undoRedoStore from '@baserow/modules/core/store/undoRedo'
|
||||
|
@ -46,6 +48,7 @@ export default (context, inject) => {
|
|||
const registry = new Registry()
|
||||
registry.registerNamespace('plugin')
|
||||
registry.registerNamespace('application')
|
||||
registry.registerNamespace('job')
|
||||
registry.registerNamespace('view')
|
||||
registry.registerNamespace('field')
|
||||
registry.registerNamespace('settings')
|
||||
|
@ -64,8 +67,11 @@ export default (context, inject) => {
|
|||
store.registerModule('settings', settingsStore)
|
||||
store.registerModule('application', applicationStore)
|
||||
store.registerModule('auth', authStore)
|
||||
store.registerModule('job', jobStore)
|
||||
store.registerModule('group', groupStore)
|
||||
store.registerModule('notification', notificationStore)
|
||||
store.registerModule('sidebar', sidebarStore)
|
||||
store.registerModule('undoRedo', undoRedoStore)
|
||||
|
||||
registry.register('job', new DuplicateApplicationJobType(context))
|
||||
}
|
||||
|
|
|
@ -42,19 +42,23 @@ export const mutations = {
|
|||
},
|
||||
UPDATE_ITEM(state, { id, values }) {
|
||||
const index = state.items.findIndex((item) => item.id === id)
|
||||
Object.assign(state.items[index], state.items[index], values)
|
||||
if (index !== -1) {
|
||||
Object.assign(state.items[index], state.items[index], values)
|
||||
}
|
||||
},
|
||||
ORDER_ITEMS(state, { group, order }) {
|
||||
state.items
|
||||
.filter((item) => item.group.id === group.id)
|
||||
.forEach((item) => {
|
||||
const index = order.findIndex((value) => value === item.id)
|
||||
item.order = index === -1 ? 0 : index + 1
|
||||
item.order = index === -1 ? undefined : index + 1
|
||||
})
|
||||
},
|
||||
DELETE_ITEM(state, id) {
|
||||
const index = state.items.findIndex((item) => item.id === id)
|
||||
state.items.splice(index, 1)
|
||||
if (index !== -1) {
|
||||
state.items.splice(index, 1)
|
||||
}
|
||||
},
|
||||
DELETE_ITEMS_FOR_GROUP(state, groupId) {
|
||||
state.items = state.items.filter((app) => app.group.id !== groupId)
|
||||
|
@ -111,9 +115,8 @@ export const actions = {
|
|||
/**
|
||||
* Fetches one application for the authenticated user.
|
||||
*/
|
||||
async fetch({ commit, dispatch }, { applicationId }) {
|
||||
async fetch({ commit, dispatch }, applicationId) {
|
||||
commit('SET_LOADING', true)
|
||||
|
||||
try {
|
||||
const { data } = await ApplicationService(this.$client).get(applicationId)
|
||||
dispatch('forceCreate', data)
|
||||
|
@ -246,8 +249,9 @@ export const actions = {
|
|||
/**
|
||||
* Forcefully delete an item in the store without making a call to the server.
|
||||
*/
|
||||
forceDelete({ commit }, application) {
|
||||
forceDelete({ commit, dispatch }, application) {
|
||||
const type = this.$registry.get('application', application.type)
|
||||
dispatch('job/deleteForApplication', application, { root: true })
|
||||
type.delete(application, this)
|
||||
commit('DELETE_ITEM', application.id)
|
||||
},
|
||||
|
|
|
@ -226,6 +226,7 @@ export const actions = {
|
|||
* example a Table is open that has been deleted because the group has been deleted.
|
||||
*/
|
||||
forceDelete({ commit, dispatch, rootGetters }, group) {
|
||||
dispatch('job/deleteForGroup', group, { root: true })
|
||||
const applications = rootGetters['application/getAllOfGroup'](group)
|
||||
applications.forEach((application) => {
|
||||
return dispatch('application/forceDelete', application, { root: true })
|
||||
|
|
90
web-frontend/modules/core/store/job.js
Normal file
90
web-frontend/modules/core/store/job.js
Normal file
|
@ -0,0 +1,90 @@
|
|||
export function populateJob(job, registry) {
|
||||
const type = registry.get('job', job.type)
|
||||
return type.populate(job)
|
||||
}
|
||||
|
||||
export const state = () => ({
|
||||
items: [],
|
||||
})
|
||||
|
||||
export const mutations = {
|
||||
ADD_ITEM(state, item) {
|
||||
state.items.push(item)
|
||||
},
|
||||
UPDATE_ITEM(state, { id, values }) {
|
||||
const index = state.items.findIndex((item) => item.id === id)
|
||||
if (index !== -1) {
|
||||
Object.assign(state.items[index], state.items[index], values)
|
||||
}
|
||||
},
|
||||
DELETE_ITEM(state, id) {
|
||||
const index = state.items.findIndex((item) => item.id === id)
|
||||
if (index !== -1) {
|
||||
state.items.splice(index, 1)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
/**
|
||||
* Forcefully create an item in the store without making a call to the server.
|
||||
*/
|
||||
forceCreate({ commit }, job) {
|
||||
populateJob(job, this.$registry)
|
||||
commit('ADD_ITEM', job)
|
||||
},
|
||||
/**
|
||||
* Forcefully update an item in the store without making a call to the server.
|
||||
*/
|
||||
async forceUpdate({ commit }, { job, data }) {
|
||||
const type = this.$registry.get('job', job.type)
|
||||
await type.beforeUpdate(job, data)
|
||||
commit('UPDATE_ITEM', { id: job.id, values: data })
|
||||
await type.afterUpdate(job, data)
|
||||
},
|
||||
/**
|
||||
* Forcefully delete an item in the store without making a call to the server.
|
||||
*/
|
||||
forceDelete({ commit }, job) {
|
||||
const type = this.$registry.get('job', job.type)
|
||||
type.beforeDelete(job, this)
|
||||
commit('DELETE_ITEM', job.id)
|
||||
},
|
||||
/**
|
||||
* Deletes all the jobs belonging to the given group.
|
||||
*/
|
||||
deleteForGroup({ commit, state }, group) {
|
||||
const jobs = state.items.filter((job) =>
|
||||
this.$registry.get('job', job.type).isJobPartOfGroup(job, group)
|
||||
)
|
||||
jobs.forEach((job) => commit('DELETE_ITEM', job.id))
|
||||
},
|
||||
/**
|
||||
* Deletes all the jobs belonging to the given application.
|
||||
*/
|
||||
deleteForApplication({ commit, state }, application) {
|
||||
const jobs = state.items.filter((job) =>
|
||||
this.$registry
|
||||
.get('job', job.type)
|
||||
.isJobPartOfApplication(job, application)
|
||||
)
|
||||
jobs.forEach((job) => commit('DELETE_ITEM', job.id))
|
||||
},
|
||||
}
|
||||
|
||||
export const getters = {
|
||||
get: (state) => (id) => {
|
||||
return state.items.find((item) => item.id === id)
|
||||
},
|
||||
getAll: (state) => {
|
||||
return state.items
|
||||
},
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations,
|
||||
}
|
|
@ -35,6 +35,14 @@
|
|||
:table="table"
|
||||
></SidebarItem>
|
||||
</ul>
|
||||
<ul v-if="pendingJobs.length" class="tree__subs">
|
||||
<SidebarItemPendingJob
|
||||
v-for="job in pendingJobs"
|
||||
:key="job.id"
|
||||
:job="job"
|
||||
>
|
||||
</SidebarItemPendingJob>
|
||||
</ul>
|
||||
<a class="tree__sub-add" @click="$refs.importFileModal.show()">
|
||||
<i class="fas fa-plus"></i>
|
||||
{{ $t('sidebar.createTable') }}
|
||||
|
@ -47,6 +55,7 @@
|
|||
<script>
|
||||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||
import SidebarItem from '@baserow/modules/database/components/sidebar/SidebarItem'
|
||||
import SidebarItemPendingJob from '@baserow/modules/database/components/sidebar/SidebarItemPendingJob'
|
||||
import ImportFileModal from '@baserow/modules/database/components/table/ImportFileModal'
|
||||
import SidebarApplication from '@baserow/modules/core/components/sidebar/SidebarApplication'
|
||||
|
||||
|
@ -55,6 +64,7 @@ export default {
|
|||
components: {
|
||||
SidebarApplication,
|
||||
SidebarItem,
|
||||
SidebarItemPendingJob,
|
||||
ImportFileModal,
|
||||
},
|
||||
props: {
|
||||
|
@ -73,6 +83,13 @@ export default {
|
|||
.map((table) => table)
|
||||
.sort((a, b) => a.order - b.order)
|
||||
},
|
||||
pendingJobs() {
|
||||
return this.$store.getters['job/getAll'].filter((job) =>
|
||||
this.$registry
|
||||
.get('job', job.type)
|
||||
.isJobPartOfApplication(job, this.application)
|
||||
)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async selected(application) {
|
||||
|
|
|
@ -47,10 +47,7 @@
|
|||
:database="database"
|
||||
:table="table"
|
||||
:disabled="deleteLoading"
|
||||
@table-duplicated="
|
||||
$refs.context.hide()
|
||||
selectTable(database, $event.table)
|
||||
"
|
||||
@click="$refs.context.hide()"
|
||||
></SidebarDuplicateTableContextItem>
|
||||
</li>
|
||||
<li>
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
<template>
|
||||
<li class="tree__sub">
|
||||
<a class="tree__sub-link tree__sub-link--disabled">
|
||||
{{ jobSidebarText }}
|
||||
<div class="tree__progress_percentage">
|
||||
{{ job.progress_percentage }} %
|
||||
</div>
|
||||
</a>
|
||||
<div class="tree__sub-link--loading"></div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SidebarItemPendingJob',
|
||||
props: {
|
||||
job: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
jobSidebarText() {
|
||||
return this.$registry.get('job', this.job.type).getSidebarText(this.job)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<a
|
||||
:class="{
|
||||
'context__menu-item--loading': loading,
|
||||
disabled: disabled || loading,
|
||||
'context__menu-item--loading': duplicating,
|
||||
disabled: disabled || duplicating,
|
||||
}"
|
||||
@click="duplicateTable()"
|
||||
>
|
||||
|
@ -36,7 +36,7 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
duplicating: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -47,44 +47,48 @@ export default {
|
|||
{ root: true }
|
||||
)
|
||||
},
|
||||
// eslint-disable-next-line require-await
|
||||
async onJobFailed() {
|
||||
this.loading = false
|
||||
this.showError(
|
||||
this.$t('clientHandler.notCompletedTitle'),
|
||||
this.$t('clientHandler.notCompletedDescription')
|
||||
)
|
||||
await this.$store.dispatch('job/forceUpdate', {
|
||||
job: this.job,
|
||||
data: this.job,
|
||||
})
|
||||
this.duplicating = false
|
||||
},
|
||||
// eslint-disable-next-line require-await
|
||||
async onJobPollingError(error) {
|
||||
this.loading = false
|
||||
await this.$store.dispatch('job/forceDelete', this.job)
|
||||
this.duplicating = false
|
||||
notifyIf(error, 'table')
|
||||
},
|
||||
async onJobDone() {
|
||||
const database = this.database
|
||||
const table = this.job.duplicated_table
|
||||
await this.$store.dispatch('table/forceCreate', {
|
||||
database,
|
||||
data: table,
|
||||
async onJobUpdated() {
|
||||
await this.$store.dispatch('job/forceUpdate', {
|
||||
job: this.job,
|
||||
data: this.job,
|
||||
})
|
||||
this.loading = false
|
||||
this.$emit('table-duplicated', { table })
|
||||
},
|
||||
async onJobDone() {
|
||||
await this.$store.dispatch('job/forceUpdate', {
|
||||
job: this.job,
|
||||
data: this.job,
|
||||
})
|
||||
this.duplicating = false
|
||||
},
|
||||
async duplicateTable() {
|
||||
if (this.loading || this.disabled) {
|
||||
if (this.duplicating || this.disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
this.duplicating = true
|
||||
try {
|
||||
const { data: job } = await TableService(this.$client).asyncDuplicate(
|
||||
this.table.id
|
||||
)
|
||||
this.startJobPoller(job)
|
||||
this.$store.dispatch('job/forceCreate', job)
|
||||
} catch (error) {
|
||||
this.loading = false
|
||||
this.duplicating = false
|
||||
notifyIf(error, 'table')
|
||||
}
|
||||
this.$emit('click')
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
61
web-frontend/modules/database/jobTypes.js
Normal file
61
web-frontend/modules/database/jobTypes.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
import { JobType } from '@baserow/modules/core/jobTypes'
|
||||
|
||||
import SidebarItemPendingJob from '@baserow/modules/database/components/sidebar/SidebarItemPendingJob.vue'
|
||||
|
||||
export class DuplicateTableJobType extends JobType {
|
||||
static getType() {
|
||||
return 'duplicate_table'
|
||||
}
|
||||
|
||||
getName() {
|
||||
return 'duplicateTable'
|
||||
}
|
||||
|
||||
getSidebarText(job) {
|
||||
const { i18n } = this.app
|
||||
return i18n.t('duplicateTableJobType.duplicating') + '...'
|
||||
}
|
||||
|
||||
getSidebarComponent() {
|
||||
return SidebarItemPendingJob
|
||||
}
|
||||
|
||||
isJobPartOfApplication(job, application) {
|
||||
return job.original_table.database_id === application.id
|
||||
}
|
||||
|
||||
async onJobFailed(job) {
|
||||
const { i18n, store } = this.app
|
||||
|
||||
store.dispatch(
|
||||
'notification/error',
|
||||
{
|
||||
title: i18n.t('clientHandler.notCompletedTitle'),
|
||||
message: i18n.t('clientHandler.notCompletedDescription'),
|
||||
},
|
||||
{ root: true }
|
||||
)
|
||||
await store.dispatch('job/forceDelete', job)
|
||||
}
|
||||
|
||||
async onJobDone(job) {
|
||||
const { i18n, store } = this.app
|
||||
|
||||
const duplicatedTable = job.duplicated_table
|
||||
const database = store.getters['application/get'](
|
||||
duplicatedTable.database_id
|
||||
)
|
||||
|
||||
await store.dispatch('table/forceCreate', {
|
||||
database,
|
||||
data: duplicatedTable,
|
||||
})
|
||||
|
||||
store.dispatch('notification/info', {
|
||||
title: i18n.t('duplicateTableJobType.duplicatedTitle'),
|
||||
message: duplicatedTable.name,
|
||||
})
|
||||
|
||||
store.dispatch('job/forceDelete', job)
|
||||
}
|
||||
}
|
|
@ -71,6 +71,10 @@
|
|||
"sidebarItem": {
|
||||
"exportTable": "Export table"
|
||||
},
|
||||
"duplicateTableJobType": {
|
||||
"duplicating": "Duplicating",
|
||||
"duplicatedTitle": "Table duplicated"
|
||||
},
|
||||
"apiToken": {
|
||||
"create": "create",
|
||||
"read": "read",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { DatabaseApplicationType } from '@baserow/modules/database/applicationTypes'
|
||||
import { DuplicateTableJobType } from '@baserow/modules/database/jobTypes'
|
||||
import {
|
||||
GridViewType,
|
||||
GalleryViewType,
|
||||
|
@ -225,6 +226,7 @@ export default (context) => {
|
|||
|
||||
app.$registry.register('plugin', new DatabasePlugin(context))
|
||||
app.$registry.register('application', new DatabaseApplicationType(context))
|
||||
app.$registry.register('job', new DuplicateTableJobType(context))
|
||||
app.$registry.register('view', new GridViewType(context))
|
||||
app.$registry.register('view', new GalleryViewType(context))
|
||||
app.$registry.register('view', new FormViewType(context))
|
||||
|
|
Loading…
Add table
Reference in a new issue