1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-17 18:32:35 +00:00

Integrations are now loaded with the application

This commit is contained in:
Jérémie Pardou 2024-03-15 15:39:50 +00:00
parent a65dadfe0a
commit 3c17df7fe0
12 changed files with 202 additions and 188 deletions
backend/src/baserow
api/integrations
contrib/builder
changelog/entries/unreleased/bug
enterprise/web-frontend/modules/baserow_enterprise/integrations
web-frontend
locales
modules
builder/components
core
integrations/localBaserow/components/services

View file

@ -4,6 +4,7 @@ from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from baserow.api.polymorphic import PolymorphicSerializer
from baserow.core.integrations.models import Integration
from baserow.core.integrations.registries import integration_type_registry
@ -31,6 +32,16 @@ class IntegrationSerializer(serializers.ModelSerializer):
}
class PolymorphicIntegrationSerializer(PolymorphicSerializer):
"""
Polymorphic serializer for the integrations.
"""
base_class = IntegrationSerializer
registry = integration_type_registry
request = False
class CreateIntegrationSerializer(serializers.ModelSerializer):
"""
This serializer allow to set the type of an integration and the integration id

View file

@ -3,6 +3,7 @@ from typing import List
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from baserow.api.integrations.serializers import PolymorphicIntegrationSerializer
from baserow.api.user_sources.serializers import PolymorphicUserSourceSerializer
from baserow.contrib.builder.api.pages.serializers import PageSerializer
from baserow.contrib.builder.api.theme.serializers import (
@ -12,6 +13,9 @@ from baserow.contrib.builder.api.theme.serializers import (
from baserow.contrib.builder.models import Builder
from baserow.contrib.builder.operations import ListPagesBuilderOperationType
from baserow.core.handler import CoreHandler
from baserow.core.integrations.operations import (
ListIntegrationsApplicationOperationType,
)
from baserow.core.user_sources.operations import ListUserSourcesApplicationOperationType
@ -28,6 +32,9 @@ class BuilderSerializer(serializers.ModelSerializer):
help_text="This field is specific to the `builder` application and contains "
"an array of pages that are in the builder."
)
integrations = serializers.SerializerMethodField(
help_text="The integrations related with this builder."
)
user_sources = serializers.SerializerMethodField(
help_text="The user sources related with this builder."
)
@ -39,7 +46,7 @@ class BuilderSerializer(serializers.ModelSerializer):
class Meta:
model = Builder
ref_name = "BuilderApplication"
fields = ("id", "name", "pages", "theme", "user_sources")
fields = ("id", "name", "pages", "theme", "integrations", "user_sources")
@extend_schema_field(PageSerializer(many=True))
def get_pages(self, instance: Builder) -> List:
@ -70,10 +77,38 @@ class BuilderSerializer(serializers.ModelSerializer):
return PageSerializer(pages, many=True).data
@extend_schema_field(PolymorphicIntegrationSerializer(many=True))
def get_integrations(self, instance: Builder) -> List:
"""
Returns the integrations related to this builder.
:param instance: The builder application instance.
:return: A list of serialized integrations that belong to this instance.
"""
integrations = instance.integrations.all()
user = self.context.get("user")
request = self.context.get("request")
if user is None and hasattr(request, "user"):
user = request.user
if user:
integrations = CoreHandler().filter_queryset(
user,
ListIntegrationsApplicationOperationType.type,
integrations,
workspace=instance.workspace,
allow_if_template=True,
)
return PolymorphicIntegrationSerializer(integrations, many=True).data
@extend_schema_field(PolymorphicUserSourceSerializer(many=True))
def get_user_sources(self, instance: Builder) -> List:
"""
Returns the user sources related to this public builder.
Returns the user sources related to this builder.
:param instance: The builder application instance.
:return: A list of serialized user sources that belong to this instance.

View file

@ -327,5 +327,6 @@ class BuilderApplicationType(ApplicationType):
def enhance_queryset(self, queryset):
queryset = queryset.prefetch_related("page_set")
queryset = queryset.prefetch_related("user_sources")
queryset = queryset.prefetch_related("integrations")
queryset = theme_config_block_registry.enhance_list_builder_queryset(queryset)
return queryset

View file

@ -0,0 +1,7 @@
{
"type": "bug",
"message": "Application builder integrations are now loaded with the application",
"issue_number": 2156,
"bullet_points": [],
"created_at": "2024-03-06"
}

View file

@ -58,10 +58,12 @@ export class LocalBaserowUserSourceType extends UserSourceType {
}
getSummary(userSource) {
const integrations = this.app.store.getters['integration/getIntegrations']
const integration = integrations.find(
({ id }) => id === userSource.integration_id
const application = this.app.store.getters['application/get'](
userSource.application_id
)
const integration = this.app.store.getters[
'integration/getIntegrationById'
](application, userSource.integration_id)
if (!integration) {
return this.app.i18n.t('localBaserowUserSourceType.notConfigured')

View file

@ -6,8 +6,6 @@
"none": "None",
"free": "Free",
"comingSoon": "Coming soon...",
"table": "Table",
"link": "Link",
"billable": "Billable"
},
"action": {

View file

@ -4,7 +4,6 @@
:class="{ 'context--loading-overlay': state === 'loading' }"
:overflow-scroll="true"
:max-height-if-outside-viewport="true"
@shown="shown"
>
<template v-if="state === 'loaded'">
<div v-if="dataSources.length > 0">
@ -51,7 +50,7 @@
<script>
import context from '@baserow/modules/core/mixins/context'
import DataSourceForm from '@baserow/modules/builder/components/dataSource/DataSourceForm'
import { mapActions, mapGetters } from 'vuex'
import { mapActions } from 'vuex'
import _ from 'lodash'
import { clone } from '@baserow/modules/core/utils/object'
import { notifyIf } from '@baserow/modules/core/utils/error'
@ -69,41 +68,27 @@ export default {
},
data() {
return {
state: null,
state: 'loaded',
creationInProgress: false,
onGoingUpdate: {},
dataSourcesLoading: [],
}
},
computed: {
...mapGetters({
integrations: 'integration/getIntegrations',
}),
integrations() {
return this.$store.getters['integration/getIntegrations'](this.builder)
},
dataSources() {
return this.$store.getters['dataSource/getPageDataSources'](this.page)
},
},
methods: {
...mapActions({
actionFetchIntegrations: 'integration/fetch',
actionCreateDataSource: 'dataSource/create',
actionUpdateDataSource: 'dataSource/debouncedUpdate',
actionDeleteDataSource: 'dataSource/delete',
actionFetchDataSources: 'dataSource/fetch',
}),
async shown() {
this.state = 'loading'
try {
await Promise.all([
this.actionFetchIntegrations({
applicationId: this.builder.id,
}),
])
} catch (error) {
notifyIf(error)
}
this.state = 'loaded'
},
async createDataSource() {
this.creationInProgress = true
try {

View file

@ -51,7 +51,7 @@
<script>
import IntegrationCreateEditModal from '@baserow/modules/core/components/integrations/IntegrationCreateEditModal'
import { mapActions, mapGetters } from 'vuex'
import { mapActions } from 'vuex'
import { notifyIf } from '@baserow/modules/core/utils/error'
export default {
@ -64,27 +64,19 @@ export default {
},
},
data() {
return { state: null }
return { state: 'loaded' }
},
computed: {
integrationTypes() {
return this.$registry.getOrderedList('integration')
},
integrations() {
return this.$store.getters['integration/getIntegrations'](this.builder)
},
},
...mapGetters({ integrations: 'integration/getIntegrations' }),
},
async mounted() {
this.state = 'pending'
try {
await this.actionFetchIntegrations({ applicationId: this.builder.id })
} catch (error) {
notifyIf(error)
}
this.state = 'loaded'
},
methods: {
...mapActions({
actionFetchIntegrations: 'integration/fetch',
actionDeleteIntegration: 'integration/delete',
}),
getIntegrationType(integration) {
@ -92,7 +84,10 @@ export default {
},
async deleteIntegration(integration) {
try {
await this.actionDeleteIntegration({ integrationId: integration.id })
await this.actionDeleteIntegration({
application: this.builder,
integrationId: integration.id,
})
} catch (error) {
notifyIf(error)
}

View file

@ -12,42 +12,33 @@
</Button>
</div>
<div
v-if="$fetchState.pending && !error.visible"
class="user-source-settings__loader"
/>
<template v-else>
<div
v-for="userSource in userSources"
:key="userSource.id"
class="user-source-settings__user-source"
@delete="deleteUserSource(userSource)"
>
<Presentation
:image="getUserSourceType(userSource).image"
:title="userSource.name"
:subtitle="getUserSourceType(userSource).getSummary(userSource)"
:rounded-icon="false"
avatar-color="transparent"
style="flex: 1"
/>
<div class="user-source-settings__user-source-actions">
<Button
icon="iconoir-edit"
type="light"
@click="showForm(userSource)"
/>
<Button
icon="iconoir-trash"
type="light"
@click="deleteUserSource(userSource)"
/>
</div>
</div>
</template>
<p
v-if="!error.visible && !$fetchState.pending && userSources.length === 0"
class="margin-top-3"
v-for="userSource in userSources"
:key="userSource.id"
class="user-source-settings__user-source"
@delete="deleteUserSource(userSource)"
>
<Presentation
:image="getUserSourceType(userSource).image"
:title="userSource.name"
:subtitle="getUserSourceType(userSource).getSummary(userSource)"
:rounded-icon="false"
avatar-color="transparent"
style="flex: 1"
/>
<div class="user-source-settings__user-source-actions">
<Button
icon="iconoir-edit"
type="light"
@click="showForm(userSource)"
/>
<Button
icon="iconoir-trash"
type="light"
@click="deleteUserSource(userSource)"
/>
</div>
</div>
<p v-if="!error.visible && userSources.length === 0" class="margin-top-3">
{{ $t('userSourceSettings.noUserSourceMessage') }}
</p>
</div>
@ -128,7 +119,7 @@
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { mapActions } from 'vuex'
import error from '@baserow/modules/core/mixins/error'
import { clone } from '@baserow/modules/core/utils/object'
import { notifyIf } from '@baserow/modules/core/utils/error'
@ -153,18 +144,10 @@ export default {
invalidForm: false,
}
},
async fetch() {
try {
await this.actionFetchIntegrations({ applicationId: this.builder.id })
this.hideError()
} catch (error) {
this.handleError(error)
}
},
computed: {
...mapGetters({
integrations: 'integration/getIntegrations',
}),
integrations() {
return this.$store.getters['integration/getIntegrations'](this.builder)
},
userSources() {
return this.$store.getters['userSource/getUserSources'](this.builder)
},
@ -174,7 +157,6 @@ export default {
},
methods: {
...mapActions({
actionFetchIntegrations: 'integration/fetch',
actionCreateUserSource: 'userSource/create',
actionUpdateUserSource: 'userSource/update',
actionDeleteUserSource: 'userSource/delete',

View file

@ -38,7 +38,7 @@
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
import { mapActions } from 'vuex'
import error from '@baserow/modules/core/mixins/error'
import modal from '@baserow/modules/core/mixins/modal'
import IntegrationEditForm from '@baserow/modules/core/components/integrations/IntegrationEditForm'
@ -78,13 +78,18 @@ export default {
}
return this.$registry.get('integration', this.integration.type)
},
...mapGetters({ integrations: 'integration/getIntegrations' }),
integrations() {
return this.$store.getters['integration/getIntegrations'](
this.application
)
},
},
methods: {
...mapActions({
actionUpdateIntegration: 'integration/update',
actionCreateIntegration: 'integration/create',
}),
shown() {
this.hideError()
},
@ -102,13 +107,14 @@ export default {
try {
if (this.create) {
const newIntegration = await this.actionCreateIntegration({
applicationId: this.application.id,
application: this.application,
integrationType: this.actualIntegrationType.type,
values,
})
this.$emit('created', newIntegration)
} else {
await this.actionUpdateIntegration({
application: this.application,
integrationId: this.integration.id,
values,
})

View file

@ -1,9 +1,6 @@
import IntegrationService from '@baserow/modules/core/services/integration'
const state = {
// The loaded integrations
integrations: [],
}
const state = {}
const updateContext = {
updateTimeout: null,
@ -12,57 +9,60 @@ const updateContext = {
}
const mutations = {
ADD_ITEM(state, { integration, beforeId = null }) {
ADD_ITEM(state, { application, integration, beforeId = null }) {
if (beforeId === null) {
state.integrations.push(integration)
application.integrations.push(integration)
} else {
const insertionIndex = state.integrations.findIndex(
const insertionIndex = application.integrations.findIndex(
(e) => e.id === beforeId
)
state.integrations.splice(insertionIndex, 0, integration)
application.integrations.splice(insertionIndex, 0, integration)
}
},
UPDATE_ITEM(state, { integration: integrationToUpdate, values }) {
state.integrations.forEach((integration) => {
UPDATE_ITEM(
state,
{ application, integration: integrationToUpdate, values }
) {
application.integrations.forEach((integration) => {
if (integration.id === integrationToUpdate.id) {
Object.assign(integration, values)
}
})
},
DELETE_ITEM(state, { integrationId }) {
const index = state.integrations.findIndex(
DELETE_ITEM(state, { application, integrationId }) {
const index = application.integrations.findIndex(
(integration) => integration.id === integrationId
)
if (index > -1) {
state.integrations.splice(index, 1)
application.integrations.splice(index, 1)
}
},
MOVE_ITEM(state, { index, oldIndex }) {
state.integrations.splice(
MOVE_ITEM(state, { application, index, oldIndex }) {
application.integrations.splice(
index,
0,
state.integrations.splice(oldIndex, 1)[0]
application.integrations.splice(oldIndex, 1)[0]
)
},
CLEAR_ITEMS(state) {
state.integrations = []
},
}
const actions = {
forceCreate({ commit }, { integration, beforeId = null }) {
commit('ADD_ITEM', { integration, beforeId })
forceCreate({ commit }, { application, integration, beforeId = null }) {
commit('ADD_ITEM', { application, integration, beforeId })
},
forceUpdate({ commit }, { integration, values }) {
commit('UPDATE_ITEM', { integration, values })
forceUpdate({ commit }, { application, integration, values }) {
commit('UPDATE_ITEM', { application, integration, values })
},
forceDelete({ commit, getters }, { integrationId }) {
commit('DELETE_ITEM', { integrationId })
forceDelete({ commit, getters }, { application, integrationId }) {
commit('DELETE_ITEM', { application, integrationId })
},
forceMove({ commit, getters }, { integrationId, beforeIntegrationId }) {
const currentOrder = getters.getIntegrations.map(
(integration) => integration.id
)
forceMove(
{ commit, getters },
{ application, integrationId, beforeIntegrationId }
) {
const currentOrder = getters
.getIntegrations(application)
.map((integration) => integration.id)
const oldIndex = currentOrder.findIndex((id) => id === integrationId)
const index = beforeIntegrationId
? currentOrder.findIndex((id) => id === beforeIntegrationId)
@ -71,29 +71,29 @@ const actions = {
// If the integration is before the beforeIntegration we must decrease the target index by
// one to compensate the removed integration.
if (oldIndex < index) {
commit('MOVE_ITEM', { index: index - 1, oldIndex })
commit('MOVE_ITEM', { application, index: index - 1, oldIndex })
} else {
commit('MOVE_ITEM', { index, oldIndex })
commit('MOVE_ITEM', { application, index, oldIndex })
}
},
async create(
{ dispatch },
{ applicationId, integrationType, values, beforeId = null }
{ application, integrationType, values, beforeId = null }
) {
const { data: integration } = await IntegrationService(this.$client).create(
applicationId,
application.id,
integrationType,
values,
beforeId
)
await dispatch('forceCreate', { integration, beforeId })
await dispatch('forceCreate', { application, integration, beforeId })
return integration
},
async update({ dispatch, getters }, { integrationId, values }) {
const integrationsOfPage = getters.getIntegrations
const integration = integrationsOfPage.find(
async update({ dispatch, getters }, { application, integrationId, values }) {
const integrationsOfApplication = getters.getIntegrations(application)
const integration = integrationsOfApplication.find(
({ id }) => id === integrationId
)
const oldValues = {}
@ -105,20 +105,31 @@ const actions = {
}
})
await dispatch('forceUpdate', { integration, values: newValues })
await dispatch('forceUpdate', {
application,
integration,
values: newValues,
})
try {
await IntegrationService(this.$client).update(integration.id, values)
} catch (error) {
await dispatch('forceUpdate', { integration, values: oldValues })
await dispatch('forceUpdate', {
application,
integration,
values: oldValues,
})
throw error
}
},
async debouncedUpdate({ dispatch, getters }, { integrationId, values }) {
const integration = getters.getIntegrations.find(
({ id }) => id === integrationId
)
async debouncedUpdate(
{ dispatch, getters },
{ application, integrationId, values }
) {
const integration = getters
.getIntegrations(application)
.find(({ id }) => id === integrationId)
const oldValues = {}
const newValues = {}
Object.keys(values).forEach((name) => {
@ -128,7 +139,11 @@ const actions = {
}
})
await dispatch('forceUpdate', { integration, values: newValues })
await dispatch('forceUpdate', {
application,
integration,
values: newValues,
})
return new Promise((resolve, reject) => {
const fire = async () => {
@ -139,6 +154,7 @@ const actions = {
} catch (error) {
// Revert to old values on error
await dispatch('forceUpdate', {
application,
integration,
values: updateContext.lastUpdatedValues,
})
@ -161,46 +177,36 @@ const actions = {
updateContext.promiseResolve = resolve
})
},
async delete({ dispatch, getters }, { integrationId }) {
const integrationsOfPage = getters.getIntegrations
const integrationIndex = integrationsOfPage.findIndex(
async delete({ dispatch, getters }, { application, integrationId }) {
const integrationsOfApplication = getters.getIntegrations(application)
const integrationIndex = integrationsOfApplication.findIndex(
(integration) => integration.id === integrationId
)
const integrationToDelete = integrationsOfPage[integrationIndex]
const integrationToDelete = integrationsOfApplication[integrationIndex]
const beforeId =
integrationIndex !== integrationsOfPage.length - 1
? integrationsOfPage[integrationIndex + 1].id
integrationIndex !== integrationsOfApplication.length - 1
? integrationsOfApplication[integrationIndex + 1].id
: null
await dispatch('forceDelete', { integrationId })
await dispatch('forceDelete', { application, integrationId })
try {
await IntegrationService(this.$client).delete(integrationId)
} catch (error) {
await dispatch('forceCreate', {
application,
integration: integrationToDelete,
beforeId,
})
throw error
}
},
async fetch({ dispatch, commit }, { applicationId }) {
commit('CLEAR_ITEMS')
const { data: integrations } = await IntegrationService(
this.$client
).fetchAll(applicationId)
await Promise.all(
integrations.map((integration) =>
dispatch('forceCreate', { integration })
)
)
return integrations
},
async move({ dispatch }, { integrationId, beforeIntegrationId }) {
async move(
{ dispatch },
{ application, integrationId, beforeIntegrationId }
) {
await dispatch('forceMove', {
application,
integrationId,
beforeIntegrationId,
})
@ -212,18 +218,19 @@ const actions = {
)
} catch (error) {
await dispatch('forceMove', {
application,
integrationId: beforeIntegrationId,
beforeIntegrationId: integrationId,
})
throw error
}
},
async duplicate({ getters, dispatch }, { integrationId, applicationId }) {
async duplicate({ getters, dispatch }, { application, integrationId }) {
const integration = getters.getIntegrations.find(
(e) => e.id === integrationId
)
await dispatch('create', {
applicationId,
application,
integrationType: integration.type,
beforeId: integration.id,
})
@ -231,11 +238,11 @@ const actions = {
}
const getters = {
getIntegrations: (state) => {
return state.integrations
getIntegrations: (state) => (application) => {
return application.integrations
},
getIntegrationById: (state) => (id) => {
return state.integrations.find((integration) => integration.id === id)
getIntegrationById: (state) => (application, id) => {
return application.integrations.find((integration) => integration.id === id)
},
}

View file

@ -8,7 +8,6 @@
v-model="values.integration_id"
:application="application"
:integrations="integrations"
:disabled="$fetchState.pending"
:integration-type="integrationType"
/>
</FormGroup>
@ -41,9 +40,7 @@
<script>
import LocalBaserowTableSelector from '@baserow/modules/integrations/localBaserow/components/services/LocalBaserowTableSelector'
import { mapActions, mapGetters } from 'vuex'
import { LocalBaserowIntegrationType } from '@baserow/modules/integrations/integrationTypes'
import { notifyIf } from '@baserow/modules/core/utils/error'
import FieldMappingForm from '@baserow/modules/integrations/localBaserow/components/services/FieldMappingForm'
import InjectedFormulaInputGroup from '@baserow/modules/core/components/formula/InjectedFormulaInputGroup'
import IntegrationDropdown from '@baserow/modules/core/components/integrations/IntegrationDropdown'
@ -87,21 +84,13 @@ export default {
tableLoading: false,
}
},
async fetch() {
try {
await Promise.all([
this.actionFetchIntegrations({
applicationId: this.application.id,
}),
])
} catch (error) {
notifyIf(error)
}
},
computed: {
...mapGetters({
integrations: 'integration/getIntegrations',
}),
integrations() {
return this.$store.getters['integration/getIntegrations'](
this.application
)
},
workflowActionLoading() {
return this.$store.getters['workflowAction/getLoading'](
this.workflowAction
@ -132,6 +121,7 @@ export default {
},
selectedIntegration() {
return this.$store.getters['integration/getIntegrationById'](
this.application,
this.values.integration_id
)
},
@ -162,10 +152,5 @@ export default {
},
},
},
methods: {
...mapActions({
actionFetchIntegrations: 'integration/fetch',
}),
},
}
</script>