mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-14 09:08:32 +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.contrib.database.views.view_types import GridViewType
|
||||||
from baserow.core.registries import application_type_registry
|
from baserow.core.registries import application_type_registry
|
||||||
from baserow.core.trash.handler import TrashHandler
|
from baserow.core.trash.handler import TrashHandler
|
||||||
from baserow.core.utils import (
|
from baserow.core.utils import ChildProgressBuilder, Progress, find_unused_name
|
||||||
ChildProgressBuilder,
|
|
||||||
Progress,
|
|
||||||
find_unused_name,
|
|
||||||
split_ending_number,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .constants import TABLE_CREATION
|
from .constants import TABLE_CREATION
|
||||||
from .exceptions import (
|
from .exceptions import (
|
||||||
|
@ -410,8 +405,7 @@ class TableHandler:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
existing_tables_names = list(database.table_set.values_list("name", flat=True))
|
existing_tables_names = list(database.table_set.values_list("name", flat=True))
|
||||||
name, _ = split_ending_number(proposed_name)
|
return find_unused_name([proposed_name], existing_tables_names, max_length=255)
|
||||||
return find_unused_name([name], existing_tables_names, max_length=255)
|
|
||||||
|
|
||||||
def _create_related_link_fields_in_existing_tables_to_import(
|
def _create_related_link_fields_in_existing_tables_to_import(
|
||||||
self, serialized_table: Dict[str, Any], id_mapping: Dict[str, Any]
|
self, serialized_table: Dict[str, Any], id_mapping: Dict[str, Any]
|
||||||
|
@ -476,18 +470,20 @@ class TableHandler:
|
||||||
if not isinstance(table, Table):
|
if not isinstance(table, Table):
|
||||||
raise ValueError("The table is not an instance of 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 = table.database
|
||||||
database.group.has_user(user, raise_error=True)
|
database.group.has_user(user, raise_error=True)
|
||||||
database_type = application_type_registry.get_by_model(database)
|
database_type = application_type_registry.get_by_model(database)
|
||||||
|
|
||||||
serialized_tables = database_type.export_tables_serialized([table])
|
serialized_tables = database_type.export_tables_serialized([table])
|
||||||
progress.increment()
|
|
||||||
|
|
||||||
# Set a unique name for the table to import back as a new one.
|
# Set a unique name for the table to import back as a new one.
|
||||||
exported_table = serialized_tables[0]
|
exported_table = serialized_tables[0]
|
||||||
exported_table["name"] = self.find_unused_table_name(database, table.name)
|
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": {}}
|
id_mapping: Dict[str, Any] = {"database_tables": {}}
|
||||||
|
|
||||||
|
@ -496,13 +492,17 @@ class TableHandler:
|
||||||
exported_table, id_mapping
|
exported_table, id_mapping
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
progress.increment(by=export_progress)
|
||||||
|
|
||||||
imported_tables = database_type.import_tables_serialized(
|
imported_tables = database_type.import_tables_serialized(
|
||||||
database,
|
database,
|
||||||
[exported_table],
|
[exported_table],
|
||||||
id_mapping,
|
id_mapping,
|
||||||
external_table_fields_to_import=link_fields_to_import_to_existing_tables,
|
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]
|
new_table_clone = imported_tables[0]
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,9 @@ class DuplicateTableJobType(JobType):
|
||||||
new_table_clone = action_type_registry.get_by_type(DuplicateTableActionType).do(
|
new_table_clone = action_type_registry.get_by_type(DuplicateTableActionType).do(
|
||||||
job.user,
|
job.user,
|
||||||
job.original_table,
|
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
|
# update the job with the new duplicated table
|
||||||
|
|
|
@ -847,25 +847,32 @@ class CoreHandler:
|
||||||
group = application.group
|
group = application.group
|
||||||
group.has_user(user, raise_error=True)
|
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
|
# export the application
|
||||||
specific_application = application.specific
|
specific_application = application.specific
|
||||||
application_type = application_type_registry.get_by_model(specific_application)
|
application_type = application_type_registry.get_by_model(specific_application)
|
||||||
serialized = application_type.export_serialized(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
|
# Set a new unique name for the new application
|
||||||
serialized["name"] = self.find_unused_application_name(
|
serialized["name"] = self.find_unused_application_name(
|
||||||
group.id, serialized["name"]
|
group.id, serialized["name"]
|
||||||
)
|
)
|
||||||
|
serialized["order"] = application_type.model_class.get_last_order(group)
|
||||||
|
|
||||||
# import it back as a new application
|
# import it back as a new application
|
||||||
id_mapping: Dict[str, Any] = {}
|
id_mapping: Dict[str, Any] = {}
|
||||||
new_application_clone = application_type.import_serialized(
|
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
|
# broadcast the application_created signal
|
||||||
application_created.send(
|
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 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 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)
|
* 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
|
### Bug Fixes
|
||||||
* Resolve circular dependency in `FieldWithFiltersAndSortsSerializer` [#1113](https://gitlab.com/bramw/baserow/-/issues/1113)
|
* 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 {
|
.tree__item {
|
||||||
@extend %first-last-no-margin;
|
@extend %first-last-no-margin;
|
||||||
|
|
||||||
|
@ -40,6 +44,11 @@
|
||||||
padding: 0 6px;
|
padding: 0 6px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
|
||||||
|
&.tree__action--disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
&:not(.tree__action--disabled):hover {
|
&:not(.tree__action--disabled):hover {
|
||||||
background-color: $color-neutral-100;
|
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 {
|
.tree__icon {
|
||||||
@extend %tree__size;
|
@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 {
|
.tree__sub-link {
|
||||||
@extend %tree_sub-size;
|
@extend %tree_sub-size;
|
||||||
@extend %ellipsis;
|
@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 {
|
.tree__sub-add {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0 0 10px 10px;
|
margin: 0 0 10px 10px;
|
||||||
|
|
|
@ -179,6 +179,15 @@
|
||||||
:group="selectedGroup"
|
:group="selectedGroup"
|
||||||
></component>
|
></component>
|
||||||
</ul>
|
</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">
|
<li class="sidebar__new-wrapper">
|
||||||
<a
|
<a
|
||||||
ref="createApplicationContextLink"
|
ref="createApplicationContextLink"
|
||||||
|
@ -329,6 +338,13 @@ export default {
|
||||||
.map((plugin) => plugin.getSidebarMainMenuComponent())
|
.map((plugin) => plugin.getSidebarMainMenuComponent())
|
||||||
.filter((component) => component !== null)
|
.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.
|
* Indicates whether the current user is visiting an admin page.
|
||||||
*/
|
*/
|
||||||
|
@ -358,6 +374,9 @@ export default {
|
||||||
.get('application', application.type)
|
.get('application', application.type)
|
||||||
.getSidebarComponent()
|
.getSidebarComponent()
|
||||||
},
|
},
|
||||||
|
getPendingJobComponent(job) {
|
||||||
|
return this.$registry.get('job', job.type).getSidebarComponent()
|
||||||
|
},
|
||||||
logoff() {
|
logoff() {
|
||||||
this.$store.dispatch('auth/logoff')
|
this.$store.dispatch('auth/logoff')
|
||||||
this.$nuxt.$router.push({ name: 'login' })
|
this.$nuxt.$router.push({ name: 'login' })
|
||||||
|
|
|
@ -38,27 +38,18 @@
|
||||||
<a @click="enableRename()">
|
<a @click="enableRename()">
|
||||||
<i class="context__menu-icon fas fa-fw fa-pen"></i>
|
<i class="context__menu-icon fas fa-fw fa-pen"></i>
|
||||||
{{
|
{{
|
||||||
$t('sidebarApplication.renameApplication', {
|
$t('sidebarApplication.rename', {
|
||||||
type: application._.type.name.toLowerCase(),
|
type: application._.type.name.toLowerCase(),
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<SidebarDuplicateApplicationContextItem
|
||||||
:class="{
|
:application="application"
|
||||||
'context__menu-item--loading': duplicateLoading,
|
:disabled="deleting"
|
||||||
disabled: duplicateLoading || deleteLoading,
|
@click="$refs.context.hide()"
|
||||||
}"
|
></SidebarDuplicateApplicationContextItem>
|
||||||
@click="duplicateApplication()"
|
|
||||||
>
|
|
||||||
<i class="context__menu-icon fas fa-fw fa-copy"></i>
|
|
||||||
{{
|
|
||||||
$t('sidebarApplication.duplicateApplication', {
|
|
||||||
type: application._.type.name.toLowerCase(),
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a @click="openSnapshots">
|
<a @click="openSnapshots">
|
||||||
|
@ -78,12 +69,12 @@
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
:class="{ 'context__menu-item--loading': deleteLoading }"
|
:class="{ 'context__menu-item--loading': deleting }"
|
||||||
@click="deleteApplication()"
|
@click="deleteApplication()"
|
||||||
>
|
>
|
||||||
<i class="context__menu-icon fas fa-fw fa-trash"></i>
|
<i class="context__menu-icon fas fa-fw fa-trash"></i>
|
||||||
{{
|
{{
|
||||||
$t('sidebarApplication.deleteApplication', {
|
$t('sidebarApplication.delete', {
|
||||||
type: application._.type.name.toLowerCase(),
|
type: application._.type.name.toLowerCase(),
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
|
@ -103,17 +94,18 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex'
|
|
||||||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||||
import ApplicationService from '@baserow/modules/core/services/application'
|
import SidebarDuplicateApplicationContextItem from '@baserow/modules/core/components/sidebar/SidebarDuplicateApplicationContextItem.vue'
|
||||||
import jobProgress from '@baserow/modules/core/mixins/jobProgress'
|
|
||||||
import TrashModal from '@baserow/modules/core/components/trash/TrashModal'
|
import TrashModal from '@baserow/modules/core/components/trash/TrashModal'
|
||||||
import SnapshotsModal from '@baserow/modules/core/components/snapshots/SnapshotsModal'
|
import SnapshotsModal from '@baserow/modules/core/components/snapshots/SnapshotsModal'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SidebarApplication',
|
name: 'SidebarApplication',
|
||||||
components: { TrashModal, SnapshotsModal },
|
components: {
|
||||||
mixins: [jobProgress],
|
TrashModal,
|
||||||
|
SidebarDuplicateApplicationContextItem,
|
||||||
|
SnapshotsModal,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
application: {
|
application: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -126,18 +118,9 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
deleteLoading: false,
|
deleting: false,
|
||||||
duplicateLoading: false,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
|
||||||
...mapGetters({
|
|
||||||
selectedTable: 'table/getSelected',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
beforeDestroy() {
|
|
||||||
this.stopPollIfRunning()
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
setLoading(application, value) {
|
setLoading(application, value) {
|
||||||
this.$store.dispatch('application/setItemLoading', {
|
this.$store.dispatch('application/setItemLoading', {
|
||||||
|
@ -166,92 +149,12 @@ export default {
|
||||||
|
|
||||||
this.setLoading(application, false)
|
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() {
|
async deleteApplication() {
|
||||||
if (this.deleteLoading) {
|
if (this.deleting) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.deleteLoading = true
|
this.deleting = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.$store.dispatch('application/delete', this.application)
|
await this.$store.dispatch('application/delete', this.application)
|
||||||
|
@ -263,7 +166,7 @@ export default {
|
||||||
notifyIf(error, 'application')
|
notifyIf(error, 'application')
|
||||||
}
|
}
|
||||||
|
|
||||||
this.deleteLoading = false
|
this.deleting = false
|
||||||
},
|
},
|
||||||
showApplicationTrashModal() {
|
showApplicationTrashModal() {
|
||||||
this.$refs.context.hide()
|
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!"
|
"label": "Copied!"
|
||||||
},
|
},
|
||||||
"sidebarApplication": {
|
"sidebarApplication": {
|
||||||
"renameApplication": "Rename {type}",
|
"rename": "Rename",
|
||||||
"duplicateApplication": "Duplicate {type}",
|
"duplicate": "Duplicate",
|
||||||
"viewTrash": "View trash",
|
"viewTrash": "View trash",
|
||||||
"deleteApplication": "Delete {type}",
|
"delete": "Delete",
|
||||||
"snapshots": "Snapshots"
|
"snapshots": "Snapshots"
|
||||||
},
|
},
|
||||||
|
"duplicateApplicationJobType": {
|
||||||
|
"duplicating": "Duplicating",
|
||||||
|
"duplicatedTitle": "Application duplicated"
|
||||||
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"createGroup": "Create group",
|
"createGroup": "Create group",
|
||||||
"inviteOthers": "Invite others",
|
"inviteOthers": "Invite others",
|
||||||
|
@ -397,4 +401,4 @@
|
||||||
"monthsAgo": "0 months ago | 1 month ago | {n} months ago",
|
"monthsAgo": "0 months ago | 1 month ago | {n} months ago",
|
||||||
"yearsAgo": "0 years ago | 1 year ago | {n} years 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:
|
* To use this mixin you need to create the following methods on your component:
|
||||||
* - `getCustomHumanReadableJobState(state)` returns the human readable message your
|
* - `getCustomHumanReadableJobState(state)` returns the human readable message your
|
||||||
* custom state values.
|
* custom state values.
|
||||||
* - onJobDone() (optional) is called when the job is finished
|
* - onJobUpdated() (optional) is called during the polling for not finished jobs.
|
||||||
* - onJobFailed() (optional) is called if the job failed
|
* - onJobDone() (optional) is called if the job successfully finishes.
|
||||||
* - onJobPollingError(error) (optional) is there is an exception during the job polling
|
* - onJobFailed() (optional) is called if the job fails.
|
||||||
|
* - onJobPollingError(error) (optional) is called if the polling fails.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
job: null,
|
job: null,
|
||||||
pollInterval: null,
|
nextPollTimeout: null,
|
||||||
|
pollTimeoutId: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
jobIsRunning() {
|
|
||||||
return (
|
|
||||||
this.job !== null && !['failed', 'finished'].includes(this.job.state)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
jobIsFinished() {
|
|
||||||
return (
|
|
||||||
this.job !== null && ['failed', 'finished'].includes(this.job.state)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
jobHasSucceeded() {
|
jobHasSucceeded() {
|
||||||
return this.job?.state === 'finished'
|
return this.job?.state === 'finished'
|
||||||
},
|
},
|
||||||
jobHasFailed() {
|
jobHasFailed() {
|
||||||
return this.job?.state === 'failed'
|
return this.job?.state === 'failed'
|
||||||
},
|
},
|
||||||
|
jobIsFinished() {
|
||||||
|
return this.jobHasSucceeded || this.jobHasFailed
|
||||||
|
},
|
||||||
|
jobIsRunning() {
|
||||||
|
return this.job !== null && !this.jobIsFinished
|
||||||
|
},
|
||||||
|
|
||||||
jobHumanReadableState() {
|
jobHumanReadableState() {
|
||||||
if (this.job === null) {
|
if (this.job === null) {
|
||||||
return ''
|
return ''
|
||||||
|
@ -57,35 +56,46 @@ export default {
|
||||||
*/
|
*/
|
||||||
startJobPoller(job) {
|
startJobPoller(job) {
|
||||||
this.job = job
|
this.job = job
|
||||||
this.pollInterval = setTimeout(this.getLatestJobInfo, 1000)
|
this.nextPollTimeout = 200
|
||||||
|
this.pollTimeoutId = setTimeout(
|
||||||
|
this.getLatestJobInfo,
|
||||||
|
this.nextPollTimeout
|
||||||
|
)
|
||||||
},
|
},
|
||||||
async getLatestJobInfo() {
|
async getLatestJobInfo() {
|
||||||
try {
|
try {
|
||||||
const { data } = await JobService(this.$client).get(this.job.id)
|
const { data: job } = await JobService(this.$client).get(this.job.id)
|
||||||
this.job = data
|
this.job = job
|
||||||
if (this.jobHasFailed) {
|
if (job.state === 'failed') {
|
||||||
if (this.onJobFailed) {
|
if (typeof this.onJobFailed === 'function') {
|
||||||
await this.onJobFailed()
|
await this.onJobFailed()
|
||||||
}
|
}
|
||||||
} else if (!this.jobIsRunning) {
|
} else if (job.state === 'finished') {
|
||||||
if (this.onJobDone) {
|
if (typeof this.onJobDone === 'function') {
|
||||||
await this.onJobDone()
|
await this.onJobDone()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// job unfinished, set the next polling timeout
|
// job unfinished, keep polling
|
||||||
this.pollInterval = setTimeout(this.getLatestJobInfo, 1000)
|
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) {
|
} catch (error) {
|
||||||
if (this.onJobPollingError) {
|
if (typeof this.onJobPollingError === 'function') {
|
||||||
this.onJobPollingError(error)
|
this.onJobPollingError(error)
|
||||||
}
|
}
|
||||||
this.job = null
|
this.job = null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
stopPollIfRunning() {
|
stopPollIfRunning() {
|
||||||
if (this.pollInterval) {
|
if (this.pollTimeoutId) {
|
||||||
clearTimeout(this.pollInterval)
|
clearTimeout(this.pollTimeoutId)
|
||||||
this.pollInterval = null
|
this.pollTimeoutId = null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
|
|
||||||
import { Registry } from '@baserow/modules/core/registry'
|
import { Registry } from '@baserow/modules/core/registry'
|
||||||
|
import { DuplicateApplicationJobType } from '@baserow/modules/core/jobTypes'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AccountSettingsType,
|
AccountSettingsType,
|
||||||
|
@ -17,6 +18,7 @@ import settingsStore from '@baserow/modules/core/store/settings'
|
||||||
import applicationStore from '@baserow/modules/core/store/application'
|
import applicationStore from '@baserow/modules/core/store/application'
|
||||||
import authStore from '@baserow/modules/core/store/auth'
|
import authStore from '@baserow/modules/core/store/auth'
|
||||||
import groupStore from '@baserow/modules/core/store/group'
|
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 notificationStore from '@baserow/modules/core/store/notification'
|
||||||
import sidebarStore from '@baserow/modules/core/store/sidebar'
|
import sidebarStore from '@baserow/modules/core/store/sidebar'
|
||||||
import undoRedoStore from '@baserow/modules/core/store/undoRedo'
|
import undoRedoStore from '@baserow/modules/core/store/undoRedo'
|
||||||
|
@ -46,6 +48,7 @@ export default (context, inject) => {
|
||||||
const registry = new Registry()
|
const registry = new Registry()
|
||||||
registry.registerNamespace('plugin')
|
registry.registerNamespace('plugin')
|
||||||
registry.registerNamespace('application')
|
registry.registerNamespace('application')
|
||||||
|
registry.registerNamespace('job')
|
||||||
registry.registerNamespace('view')
|
registry.registerNamespace('view')
|
||||||
registry.registerNamespace('field')
|
registry.registerNamespace('field')
|
||||||
registry.registerNamespace('settings')
|
registry.registerNamespace('settings')
|
||||||
|
@ -64,8 +67,11 @@ export default (context, inject) => {
|
||||||
store.registerModule('settings', settingsStore)
|
store.registerModule('settings', settingsStore)
|
||||||
store.registerModule('application', applicationStore)
|
store.registerModule('application', applicationStore)
|
||||||
store.registerModule('auth', authStore)
|
store.registerModule('auth', authStore)
|
||||||
|
store.registerModule('job', jobStore)
|
||||||
store.registerModule('group', groupStore)
|
store.registerModule('group', groupStore)
|
||||||
store.registerModule('notification', notificationStore)
|
store.registerModule('notification', notificationStore)
|
||||||
store.registerModule('sidebar', sidebarStore)
|
store.registerModule('sidebar', sidebarStore)
|
||||||
store.registerModule('undoRedo', undoRedoStore)
|
store.registerModule('undoRedo', undoRedoStore)
|
||||||
|
|
||||||
|
registry.register('job', new DuplicateApplicationJobType(context))
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,19 +42,23 @@ export const mutations = {
|
||||||
},
|
},
|
||||||
UPDATE_ITEM(state, { id, values }) {
|
UPDATE_ITEM(state, { id, values }) {
|
||||||
const index = state.items.findIndex((item) => item.id === id)
|
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 }) {
|
ORDER_ITEMS(state, { group, order }) {
|
||||||
state.items
|
state.items
|
||||||
.filter((item) => item.group.id === group.id)
|
.filter((item) => item.group.id === group.id)
|
||||||
.forEach((item) => {
|
.forEach((item) => {
|
||||||
const index = order.findIndex((value) => value === item.id)
|
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) {
|
DELETE_ITEM(state, id) {
|
||||||
const index = state.items.findIndex((item) => item.id === 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) {
|
DELETE_ITEMS_FOR_GROUP(state, groupId) {
|
||||||
state.items = state.items.filter((app) => app.group.id !== 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.
|
* Fetches one application for the authenticated user.
|
||||||
*/
|
*/
|
||||||
async fetch({ commit, dispatch }, { applicationId }) {
|
async fetch({ commit, dispatch }, applicationId) {
|
||||||
commit('SET_LOADING', true)
|
commit('SET_LOADING', true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data } = await ApplicationService(this.$client).get(applicationId)
|
const { data } = await ApplicationService(this.$client).get(applicationId)
|
||||||
dispatch('forceCreate', data)
|
dispatch('forceCreate', data)
|
||||||
|
@ -246,8 +249,9 @@ export const actions = {
|
||||||
/**
|
/**
|
||||||
* Forcefully delete an item in the store without making a call to the server.
|
* 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)
|
const type = this.$registry.get('application', application.type)
|
||||||
|
dispatch('job/deleteForApplication', application, { root: true })
|
||||||
type.delete(application, this)
|
type.delete(application, this)
|
||||||
commit('DELETE_ITEM', application.id)
|
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.
|
* example a Table is open that has been deleted because the group has been deleted.
|
||||||
*/
|
*/
|
||||||
forceDelete({ commit, dispatch, rootGetters }, group) {
|
forceDelete({ commit, dispatch, rootGetters }, group) {
|
||||||
|
dispatch('job/deleteForGroup', group, { root: true })
|
||||||
const applications = rootGetters['application/getAllOfGroup'](group)
|
const applications = rootGetters['application/getAllOfGroup'](group)
|
||||||
applications.forEach((application) => {
|
applications.forEach((application) => {
|
||||||
return dispatch('application/forceDelete', application, { root: true })
|
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"
|
:table="table"
|
||||||
></SidebarItem>
|
></SidebarItem>
|
||||||
</ul>
|
</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()">
|
<a class="tree__sub-add" @click="$refs.importFileModal.show()">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
{{ $t('sidebar.createTable') }}
|
{{ $t('sidebar.createTable') }}
|
||||||
|
@ -47,6 +55,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||||
import SidebarItem from '@baserow/modules/database/components/sidebar/SidebarItem'
|
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 ImportFileModal from '@baserow/modules/database/components/table/ImportFileModal'
|
||||||
import SidebarApplication from '@baserow/modules/core/components/sidebar/SidebarApplication'
|
import SidebarApplication from '@baserow/modules/core/components/sidebar/SidebarApplication'
|
||||||
|
|
||||||
|
@ -55,6 +64,7 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
SidebarApplication,
|
SidebarApplication,
|
||||||
SidebarItem,
|
SidebarItem,
|
||||||
|
SidebarItemPendingJob,
|
||||||
ImportFileModal,
|
ImportFileModal,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
@ -73,6 +83,13 @@ export default {
|
||||||
.map((table) => table)
|
.map((table) => table)
|
||||||
.sort((a, b) => a.order - b.order)
|
.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: {
|
methods: {
|
||||||
async selected(application) {
|
async selected(application) {
|
||||||
|
|
|
@ -47,10 +47,7 @@
|
||||||
:database="database"
|
:database="database"
|
||||||
:table="table"
|
:table="table"
|
||||||
:disabled="deleteLoading"
|
:disabled="deleteLoading"
|
||||||
@table-duplicated="
|
@click="$refs.context.hide()"
|
||||||
$refs.context.hide()
|
|
||||||
selectTable(database, $event.table)
|
|
||||||
"
|
|
||||||
></SidebarDuplicateTableContextItem>
|
></SidebarDuplicateTableContextItem>
|
||||||
</li>
|
</li>
|
||||||
<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>
|
<template>
|
||||||
<a
|
<a
|
||||||
:class="{
|
:class="{
|
||||||
'context__menu-item--loading': loading,
|
'context__menu-item--loading': duplicating,
|
||||||
disabled: disabled || loading,
|
disabled: disabled || duplicating,
|
||||||
}"
|
}"
|
||||||
@click="duplicateTable()"
|
@click="duplicateTable()"
|
||||||
>
|
>
|
||||||
|
@ -36,7 +36,7 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: false,
|
duplicating: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -47,44 +47,48 @@ export default {
|
||||||
{ root: true }
|
{ root: true }
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line require-await
|
|
||||||
async onJobFailed() {
|
async onJobFailed() {
|
||||||
this.loading = false
|
await this.$store.dispatch('job/forceUpdate', {
|
||||||
this.showError(
|
job: this.job,
|
||||||
this.$t('clientHandler.notCompletedTitle'),
|
data: this.job,
|
||||||
this.$t('clientHandler.notCompletedDescription')
|
})
|
||||||
)
|
this.duplicating = false
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line require-await
|
|
||||||
async onJobPollingError(error) {
|
async onJobPollingError(error) {
|
||||||
this.loading = false
|
await this.$store.dispatch('job/forceDelete', this.job)
|
||||||
|
this.duplicating = false
|
||||||
notifyIf(error, 'table')
|
notifyIf(error, 'table')
|
||||||
},
|
},
|
||||||
async onJobDone() {
|
async onJobUpdated() {
|
||||||
const database = this.database
|
await this.$store.dispatch('job/forceUpdate', {
|
||||||
const table = this.job.duplicated_table
|
job: this.job,
|
||||||
await this.$store.dispatch('table/forceCreate', {
|
data: this.job,
|
||||||
database,
|
|
||||||
data: table,
|
|
||||||
})
|
})
|
||||||
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() {
|
async duplicateTable() {
|
||||||
if (this.loading || this.disabled) {
|
if (this.duplicating || this.disabled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loading = true
|
this.duplicating = true
|
||||||
try {
|
try {
|
||||||
const { data: job } = await TableService(this.$client).asyncDuplicate(
|
const { data: job } = await TableService(this.$client).asyncDuplicate(
|
||||||
this.table.id
|
this.table.id
|
||||||
)
|
)
|
||||||
this.startJobPoller(job)
|
this.startJobPoller(job)
|
||||||
|
this.$store.dispatch('job/forceCreate', job)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.loading = false
|
this.duplicating = false
|
||||||
notifyIf(error, 'table')
|
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": {
|
"sidebarItem": {
|
||||||
"exportTable": "Export table"
|
"exportTable": "Export table"
|
||||||
},
|
},
|
||||||
|
"duplicateTableJobType": {
|
||||||
|
"duplicating": "Duplicating",
|
||||||
|
"duplicatedTitle": "Table duplicated"
|
||||||
|
},
|
||||||
"apiToken": {
|
"apiToken": {
|
||||||
"create": "create",
|
"create": "create",
|
||||||
"read": "read",
|
"read": "read",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { DatabaseApplicationType } from '@baserow/modules/database/applicationTypes'
|
import { DatabaseApplicationType } from '@baserow/modules/database/applicationTypes'
|
||||||
|
import { DuplicateTableJobType } from '@baserow/modules/database/jobTypes'
|
||||||
import {
|
import {
|
||||||
GridViewType,
|
GridViewType,
|
||||||
GalleryViewType,
|
GalleryViewType,
|
||||||
|
@ -225,6 +226,7 @@ export default (context) => {
|
||||||
|
|
||||||
app.$registry.register('plugin', new DatabasePlugin(context))
|
app.$registry.register('plugin', new DatabasePlugin(context))
|
||||||
app.$registry.register('application', new DatabaseApplicationType(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 GridViewType(context))
|
||||||
app.$registry.register('view', new GalleryViewType(context))
|
app.$registry.register('view', new GalleryViewType(context))
|
||||||
app.$registry.register('view', new FormViewType(context))
|
app.$registry.register('view', new FormViewType(context))
|
||||||
|
|
Loading…
Add table
Reference in a new issue