mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-14 17:18:33 +00:00
Merge branch '1567-i-can-create-a-builder-application' into 'develop'
Resolve "I can create a Builder application" Closes #1567 See merge request bramw/baserow!1256
This commit is contained in:
commit
1a80d51ffb
19 changed files with 404 additions and 1 deletions
backend
src/baserow
tests/baserow
api/applications
contrib/builder
changelog/entries/unreleased/feature
web-frontend/modules
builder
core
assets/scss/components
components/application
|
@ -1,7 +1,14 @@
|
|||
from django.db import transaction
|
||||
from django.db.transaction import Atomic
|
||||
|
||||
from baserow.contrib.builder.models import Builder
|
||||
from baserow.core.models import Application
|
||||
from baserow.core.registries import ApplicationType
|
||||
|
||||
|
||||
class BuilderApplicationType(ApplicationType):
|
||||
type = "builder"
|
||||
model_class = Builder
|
||||
|
||||
def export_safe_transaction_context(self, application: Application) -> Atomic:
|
||||
return transaction.atomic()
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from baserow.contrib.builder.models import Builder
|
||||
from baserow.contrib.database.models import Database
|
||||
|
||||
|
||||
|
@ -13,3 +14,15 @@ class ApplicationFixtures:
|
|||
kwargs["order"] = 0
|
||||
|
||||
return Database.objects.create(**kwargs)
|
||||
|
||||
def create_builder_application(self, user=None, **kwargs):
|
||||
if "group" not in kwargs:
|
||||
kwargs["group"] = self.create_group(user=user)
|
||||
|
||||
if "name" not in kwargs:
|
||||
kwargs["name"] = self.fake.name()
|
||||
|
||||
if "order" not in kwargs:
|
||||
kwargs["order"] = 0
|
||||
|
||||
return Builder.objects.create(**kwargs)
|
||||
|
|
|
@ -20,12 +20,38 @@ from baserow.core.job_types import DuplicateApplicationJobType
|
|||
from baserow.core.jobs.handler import JobHandler
|
||||
from baserow.core.models import Template
|
||||
from baserow.core.operations import ListApplicationsGroupOperationType
|
||||
from baserow.core.registries import application_type_registry
|
||||
|
||||
|
||||
def stub_filter_queryset(u, o, q, **kwargs):
|
||||
return q
|
||||
|
||||
|
||||
@pytest.mark.django_db()
|
||||
@pytest.mark.parametrize("application_type", application_type_registry.get_all())
|
||||
def test_can_create_different_application_types(
|
||||
application_type, api_client, data_fixture
|
||||
):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
group = data_fixture.create_group(user=user)
|
||||
|
||||
response = api_client.post(
|
||||
reverse("api:applications:list", kwargs={"group_id": group.id}),
|
||||
{"name": "Test 1", "type": application_type.type},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
|
||||
application = application_type.model_class.objects.all()[0]
|
||||
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert response_json["type"] == application_type.type
|
||||
assert response_json["id"] == application.id
|
||||
assert response_json["name"] == application.name
|
||||
assert response_json["order"] == application.order
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_list_applications(api_client, data_fixture, django_assert_num_queries):
|
||||
user, token = data_fixture.create_user_and_token(
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import pytest
|
||||
|
||||
from baserow.contrib.builder.models import Builder
|
||||
from baserow.core.handler import CoreHandler
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_can_duplicate_builder_application(data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
builder = data_fixture.create_builder_application(user=user)
|
||||
|
||||
builder_clone = CoreHandler().duplicate_application(user, builder)
|
||||
|
||||
assert builder.id != builder_clone.id
|
||||
assert builder.name in builder_clone.name
|
||||
|
||||
assert Builder.objects.count() == 2
|
|
@ -0,0 +1,52 @@
|
|||
import pytest
|
||||
|
||||
from baserow.contrib.builder.models import Builder
|
||||
from baserow.core.models import Snapshot
|
||||
from baserow.core.snapshots.handler import SnapshotHandler
|
||||
from baserow.core.utils import Progress
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_can_create_a_snapshot_for_builder_application(data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
builder = data_fixture.create_builder_application(user)
|
||||
|
||||
snapshot = SnapshotHandler().create(builder.id, performed_by=user, name="test")[
|
||||
"snapshot"
|
||||
]
|
||||
|
||||
assert snapshot is not None
|
||||
assert snapshot.name == "test"
|
||||
assert Snapshot.objects.count() == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_can_delete_a_snapshot_for_builder_application(data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
builder = data_fixture.create_builder_application(user)
|
||||
|
||||
snapshot = SnapshotHandler().create(builder.id, performed_by=user, name="test")[
|
||||
"snapshot"
|
||||
]
|
||||
|
||||
SnapshotHandler().delete(snapshot.id, user)
|
||||
|
||||
snapshot.refresh_from_db()
|
||||
assert snapshot.mark_for_deletion is True
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_can_restore_a_snapshot_for_builder_application(data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
builder = data_fixture.create_builder_application(user)
|
||||
|
||||
snapshot = data_fixture.create_snapshot(
|
||||
snapshot_from_application=builder,
|
||||
snapshot_to_application=builder,
|
||||
user=user,
|
||||
)
|
||||
|
||||
snapshot_restored = SnapshotHandler().perform_restore(snapshot, Progress(total=100))
|
||||
|
||||
assert snapshot_restored is not None
|
||||
assert Builder.objects.count() == 2
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "feature",
|
||||
"message": "Add ability to create application builder",
|
||||
"issue_number": 1567,
|
||||
"bullet_points": [],
|
||||
"created_at": "2023-02-10"
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
import { ApplicationType } from '@baserow/modules/core/applicationTypes'
|
||||
import BuilderForm from '@baserow/modules/builder/components/form/BuilderForm'
|
||||
import SidebarComponentBuilder from '@baserow/modules/builder/components/sidebar/SidebarComponentBuilder'
|
||||
|
||||
export class BuilderApplicationType extends ApplicationType {
|
||||
static getType() {
|
||||
|
@ -18,4 +20,12 @@ export class BuilderApplicationType extends ApplicationType {
|
|||
const { i18n } = this.app
|
||||
return i18n.t('applicationType.builderDefaultName')
|
||||
}
|
||||
|
||||
getApplicationFormComponent() {
|
||||
return BuilderForm
|
||||
}
|
||||
|
||||
getSidebarComponent() {
|
||||
return SidebarComponentBuilder
|
||||
}
|
||||
}
|
||||
|
|
65
web-frontend/modules/builder/builderSettingTypes.js
Normal file
65
web-frontend/modules/builder/builderSettingTypes.js
Normal file
|
@ -0,0 +1,65 @@
|
|||
import { Registerable } from '@baserow/modules/core/registry'
|
||||
import IntegrationSettings from '@baserow/modules/builder/components/settings/IntegrationSettings'
|
||||
import ThemeSettings from '@baserow/modules/builder/components/settings/ThemeSettings'
|
||||
|
||||
class BuilderSettingType extends Registerable {
|
||||
getType() {
|
||||
return null
|
||||
}
|
||||
|
||||
getName() {
|
||||
return null
|
||||
}
|
||||
|
||||
getIconClass() {
|
||||
return null
|
||||
}
|
||||
|
||||
getComponent() {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export class IntegrationsBuilderSettingsType extends BuilderSettingType {
|
||||
getType() {
|
||||
return 'integrations'
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.app.i18n.t('builderSettingTypes.integrationsName')
|
||||
}
|
||||
|
||||
getIconClass() {
|
||||
return 'plug'
|
||||
}
|
||||
|
||||
getOrder() {
|
||||
return 1
|
||||
}
|
||||
|
||||
getComponent() {
|
||||
return IntegrationSettings
|
||||
}
|
||||
}
|
||||
|
||||
export class ThemeBuilderSettingsType extends BuilderSettingType {
|
||||
getType() {
|
||||
return 'theme'
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.app.i18n.t('builderSettingTypes.themeName')
|
||||
}
|
||||
|
||||
getIconClass() {
|
||||
return 'tint'
|
||||
}
|
||||
|
||||
getOrder() {
|
||||
return 2
|
||||
}
|
||||
|
||||
getComponent() {
|
||||
return ThemeSettings
|
||||
}
|
||||
}
|
35
web-frontend/modules/builder/components/form/BuilderForm.vue
Normal file
35
web-frontend/modules/builder/components/form/BuilderForm.vue
Normal file
|
@ -0,0 +1,35 @@
|
|||
<template>
|
||||
<ApplicationForm
|
||||
:default-values="{ name: defaultName }"
|
||||
@submitted="$emit('submitted', $event)"
|
||||
>
|
||||
<FormElement class="builder-form__controls">
|
||||
<button
|
||||
class="button button--large"
|
||||
:class="{ 'button--loading': loading }"
|
||||
:disabled="loading"
|
||||
type="submit"
|
||||
>
|
||||
{{ $t('builderForm.submit') }}
|
||||
</button>
|
||||
</FormElement>
|
||||
</ApplicationForm>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ApplicationForm from '@baserow/modules/core/components/application/ApplicationForm'
|
||||
export default {
|
||||
name: 'BuilderForm',
|
||||
components: { ApplicationForm },
|
||||
props: {
|
||||
defaultName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,59 @@
|
|||
<template>
|
||||
<Modal left-sidebar left-sidebar-scrollable>
|
||||
<template #sidebar>
|
||||
<div class="modal-sidebar__head">
|
||||
<div class="modal-sidebar__head-name">
|
||||
{{ $t('builderSettingsModal.title') }}
|
||||
</div>
|
||||
</div>
|
||||
<ul class="modal-sidebar__nav">
|
||||
<li v-for="setting in registeredSettings" :key="setting.getType()">
|
||||
<a
|
||||
class="modal-sidebar__nav-link"
|
||||
:class="{ active: setting === settingSelected }"
|
||||
@click="settingSelected = setting"
|
||||
>
|
||||
<i
|
||||
class="fas modal-sidebar__nav-icon"
|
||||
:class="'fa-' + setting.getIconClass()"
|
||||
></i>
|
||||
{{ setting.getName() }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<template v-if="settingSelected" #content>
|
||||
<component :is="settingSelected.getComponent()"></component>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import modal from '@baserow/modules/core/mixins/modal'
|
||||
|
||||
export default {
|
||||
name: 'BuilderSettingsModal',
|
||||
mixins: [modal],
|
||||
data() {
|
||||
return {
|
||||
settingSelected: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
registeredSettings() {
|
||||
return this.$registry.getOrderedList('builderSettings')
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setPage(page) {
|
||||
this.page = page
|
||||
},
|
||||
show(...args) {
|
||||
if (!this.settingSelected) {
|
||||
this.settingSelected = this.registeredSettings[0]
|
||||
}
|
||||
return modal.methods.show.call(this, ...args)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<div>INTEGRATIONS</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'IntegrationSettings',
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<div>THEME</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ThemeSettings',
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<div>
|
||||
<SidebarApplication
|
||||
ref="sidebarApplication"
|
||||
:group="group"
|
||||
:application="application"
|
||||
@selected="selected"
|
||||
>
|
||||
<template #context>
|
||||
<li
|
||||
v-if="
|
||||
$hasPermission(
|
||||
'application.update',
|
||||
application,
|
||||
application.group.id
|
||||
)
|
||||
"
|
||||
>
|
||||
<a @click="settingsClicked">
|
||||
<i class="context__menu-icon fas fa-fw fa-cog"></i>
|
||||
{{ $t('sidebarComponentBuilder.settings') }}
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
</SidebarApplication>
|
||||
<BuilderSettingsModal ref="builderSettingsModal"></BuilderSettingsModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SidebarApplication from '@baserow/modules/core/components/sidebar/SidebarApplication'
|
||||
import BuilderSettingsModal from '@baserow/modules/builder/components/settings/BuilderSettingsModal'
|
||||
|
||||
export default {
|
||||
name: 'TemplateSidebar',
|
||||
components: { BuilderSettingsModal, SidebarApplication },
|
||||
props: {
|
||||
application: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
group: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
selected() {
|
||||
console.log('TODO')
|
||||
},
|
||||
settingsClicked() {
|
||||
this.$refs.sidebarApplication.$refs.context.hide()
|
||||
this.$refs.builderSettingsModal.show()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -1 +1,15 @@
|
|||
{}
|
||||
{
|
||||
"builderForm": {
|
||||
"submit": "Add application"
|
||||
},
|
||||
"sidebarComponentBuilder": {
|
||||
"settings": "Settings"
|
||||
},
|
||||
"builderSettingsModal": {
|
||||
"title": "Application"
|
||||
},
|
||||
"builderSettingTypes": {
|
||||
"integrationsName": "Integrations",
|
||||
"themeName": "Theme"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,10 @@ import de from '@baserow/modules/builder/locales/de.json'
|
|||
import es from '@baserow/modules/builder/locales/es.json'
|
||||
import it from '@baserow/modules/builder/locales/it.json'
|
||||
import pl from '@baserow/modules/builder/locales/pl.json'
|
||||
import {
|
||||
IntegrationsBuilderSettingsType,
|
||||
ThemeBuilderSettingsType,
|
||||
} from '@baserow/modules/builder/builderSettingTypes'
|
||||
|
||||
export default (context) => {
|
||||
const { app, isDev } = context
|
||||
|
@ -22,5 +26,16 @@ export default (context) => {
|
|||
i18n.mergeLocaleMessage('pl', pl)
|
||||
}
|
||||
|
||||
app.$registry.registerNamespace('builderSettings')
|
||||
|
||||
app.$registry.register('application', new BuilderApplicationType(context))
|
||||
|
||||
app.$registry.register(
|
||||
'builderSettings',
|
||||
new IntegrationsBuilderSettingsType(context)
|
||||
)
|
||||
app.$registry.register(
|
||||
'builderSettings',
|
||||
new ThemeBuilderSettingsType(context)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -113,3 +113,4 @@
|
|||
@import 'group_invite_form';
|
||||
@import 'settings/members/edit_role_context';
|
||||
@import 'badge';
|
||||
@import 'builder/all';
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
@import 'builder_form';
|
|
@ -0,0 +1,4 @@
|
|||
.builder-form__controls {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
|
@ -67,6 +67,8 @@ export default {
|
|||
})
|
||||
} catch (error) {
|
||||
this.handleError(error, 'application')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Add table
Reference in a new issue