1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-07 14:25:37 +00:00

Merge branch 'from-template-onboarding-step' into 'develop'

"From template" onboarding step

See merge request 
This commit is contained in:
Bram Wiepjes 2025-04-01 20:23:41 +00:00
commit 1d099f3c91
20 changed files with 292 additions and 34 deletions

View file

@ -12,7 +12,8 @@
"publisher",
"planner",
"publish",
"social media"
"social media",
"onboarding"
],
"categories": [
"Marketing"
@ -4891,4 +4892,4 @@
]
}
]
}
}

View file

@ -10,7 +10,8 @@
"teams",
"employee list",
"staff catalog",
"staff records"
"staff records",
"onboarding"
],
"categories": [
"HR and Recruiting"
@ -3777,4 +3778,4 @@
]
}
]
}
}

View file

@ -5,7 +5,8 @@
"keywords": [
"CRM",
"Sales",
"Local Business"
"Local Business",
"onboarding"
],
"categories": [
"Sales and CRM"
@ -25570,4 +25571,4 @@
"type": "builder"
}
]
}
}

View file

@ -12,7 +12,8 @@
"recruits",
"hiring",
"hr",
"human resources"
"human resources",
"onboarding"
],
"categories": [
"HR and Recruiting"
@ -11144,4 +11145,4 @@
]
}
]
}
}

View file

@ -9,7 +9,8 @@
"goal tracker",
"business goal tracker",
"objectives tracker",
"progress tracker"
"progress tracker",
"onboarding"
],
"categories": [
"Business Strategy"
@ -6312,4 +6313,4 @@
"type": "builder"
}
]
}
}

View file

@ -16,7 +16,8 @@
"customer survey",
"client feedback",
"UAT",
"user acceptance testing"
"user acceptance testing",
"onboarding"
],
"categories": [
"Product Management",
@ -3716,4 +3717,4 @@
]
}
]
}
}

View file

@ -0,0 +1,8 @@
{
"type": "feature",
"message": "Install template during onboarding",
"domain": "database",
"issue_number": 2638,
"bullet_points": [],
"created_at": "2025-03-24"
}

View file

@ -184,3 +184,4 @@
@import 'admin_dashboard';
@import 'user_admin';
@import 'group_admin';
@import 'template_import_item';

View file

@ -5,6 +5,7 @@
overflow: hidden;
pointer-events: none;
transition: left 0.5s ease;
z-index: $z-index-layout-col-1;
@include absolute(10%, 0, 0, 10%);
}
@ -15,6 +16,10 @@
.onboarding-tool-preview__inner {
@include absolute(0, 0, 0, 0);
&--reserve-space-right {
right: -370px;
}
}
.onboarding-tool-preview__highlight {

View file

@ -0,0 +1,51 @@
.template-import-items {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
}
.template-import-item {
&:hover {
text-decoration: none;
}
}
.template-import-item__head {
border: 1px solid $palette-neutral-200;
background: $palette-neutral-25;
height: 76px;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 14px;
@include rounded($rounded-md);
.template-import-item--active & {
border: 2px solid $palette-blue-500;
}
}
.template-import-item__icon {
display: flex;
justify-content: center;
align-items: center;
flex: 0 0 40px;
height: 40px;
font-size: 16px;
border: solid 1px $palette-neutral-200;
border-radius: 100%;
color: $palette-neutral-700;
background-color: $white;
@include rounded($rounded-md);
@include elevation($elevation-low);
}
.template-import-item__name {
@extend %ellipsis;
font-weight: 500;
line-height: 20px;
color: $palette-neutral-1200;
}

View file

@ -218,6 +218,10 @@
justify-content: space-between;
}
.justify-content-center {
justify-content: center;
}
.justify-content-end {
justify-content: end;
}

View file

@ -34,7 +34,6 @@ $base-font-family: $text-font-stack !default;
$radius-none: 0 !default;
$rounded: 4px !default;
$rounded-left: 4px 0 0 4px !default;
$rounded-md: 6px !default;
$rounded-xl: 12px !default;
$rounded-2xl: 16px !default;

View file

@ -15,6 +15,7 @@
:class="{
'segment-control__button--active': index === activeIndex,
}"
:title="segment.label"
class="segment-control__button"
@click="setActiveIndex(index)"
>

View file

@ -35,7 +35,7 @@ export class OnboardingType extends Registerable {
* so it should just be for demo purposes. It can accept the data property
* containing the data of all the steps.
*/
getPreviewComponent() {
getPreviewComponent(data) {
return null
}

View file

@ -83,7 +83,7 @@
</div>
<div class="onboarding__preview">
<component
:is="step.getPreviewComponent()"
:is="step.getPreviewComponent(data)"
v-bind="step.getAdditionalPreviewProps()"
:data="data"
></component>

View file

@ -4,7 +4,7 @@
<p>
{{ $t('databaseStep.description') }}
</p>
<div class="margin-bottom-3">
<div class="margin-bottom-2">
<SegmentControl
:active-index.sync="selectedTypeIndex"
:segments="types"
@ -31,17 +31,29 @@
ref="airtable"
@input="updateValue($event)"
></AirtableImportForm>
<TemplateImportForm
v-if="selectedType === 'template'"
@selected-template="selectedTemplate"
></TemplateImportForm>
</div>
</template>
<script>
import { useVuelidate } from '@vuelidate/core'
import { required, helpers } from '@vuelidate/validators'
import AirtableImportForm from '@baserow/modules/database/components/airtable/AirtableImportForm.vue'
import AirtableImportForm from '@baserow/modules/database/components/airtable/AirtableImportForm'
import TemplateImportForm from '@baserow/modules/database/components/onboarding/TemplateImportForm'
import { DatabaseOnboardingType } from '@baserow/modules/database/onboardingTypes'
export default {
name: 'DatabaseStep',
components: { AirtableImportForm },
components: { AirtableImportForm, TemplateImportForm },
props: {
data: {
required: true,
type: Object,
},
},
setup() {
return { v$: useVuelidate({ $lazy: true }) }
},
@ -60,6 +72,10 @@ export default {
type: 'airtable',
label: this.$t('databaseStep.airtable'),
},
{
type: 'template',
label: this.$t('databaseStep.template'),
},
],
selectedTypeIndex: 0,
name: '',
@ -82,6 +98,9 @@ export default {
if (this.selectedType === 'airtable') {
const airtable = this.$refs.airtable
return !!airtable && !airtable.v$.$invalid && airtable.v$.$dirty
} else if (this.selectedType === 'template') {
const template = this.data[DatabaseOnboardingType.getType()].template
return !!template
} else {
return !this.v$.$invalid && this.v$.$dirty
}
@ -93,6 +112,12 @@ export default {
...airtable,
})
},
selectedTemplate(template) {
this.$emit('update-data', {
type: this.selectedType,
template,
})
},
},
validations() {
const rules = {}

View file

@ -0,0 +1,31 @@
<template>
<div :class="{ 'onboarding-tool-preview': true }">
<div
ref="inner"
class="onboarding-tool-preview__inner onboarding-tool-preview__inner--reserve-space-right"
>
<TemplatePreview :template="template"></TemplatePreview>
</div>
</div>
</template>
<script>
import TemplatePreview from '@baserow/modules/core/components/template/TemplatePreview'
import { DatabaseOnboardingType } from '@baserow/modules/database/onboardingTypes'
export default {
name: 'DatabaseTemplatePreview',
components: { TemplatePreview },
props: {
data: {
type: Object,
required: true,
},
},
computed: {
template() {
return this.data[DatabaseOnboardingType.getType()].template
},
},
}
</script>

View file

@ -0,0 +1,97 @@
<template>
<div>
<div v-if="loading" class="flex justify-content-center">
<div class="loading"></div>
</div>
<div v-else>
<FormInput
v-model="search"
icon-left="iconoir-search"
:placeholder="$t('templateCategories.search')"
class="margin-bottom-2"
></FormInput>
<div class="template-import-items">
<a
v-for="template in templates"
:key="template.id"
class="template-import-item"
:class="{
'template-import-item--active': template.id === selectedTemplate,
}"
@click="selectTemplate(template)"
>
<div class="template-import-item__head">
<i class="template-import-item__icon" :class="template.icon"></i>
</div>
<div class="template-import-item__name">{{ template.name }}</div>
</a>
</div>
</div>
</div>
</template>
<script>
import TemplateService from '@baserow/modules/core/services/template'
import { notifyIf } from '@baserow/modules/core/utils/error'
import { escapeRegExp } from '@baserow/modules/core/utils/string'
export default {
name: 'TemplateImportForm',
data() {
return {
loading: true,
categories: [],
search: '',
selectedTemplate: 0,
}
},
computed: {
templates() {
let allTemplates = []
this.categories.forEach((category) => {
category.templates.forEach((template) => {
// Categories can have the same templates. We should not have duplicates.
if (allTemplates.findIndex((t) => t.id === template.id) === -1) {
allTemplates.push(template)
}
})
})
// A few selected templates have the keyword `onboarding`. These are shown when
// no search query is provided.
const search = this.search || 'onboarding'
allTemplates = allTemplates
.filter((template) => {
// If `open_application == null`, then it falls back on the normal behavior,
// which is opening the first database. An `open_application` is typically
// set, if an application must be opened first. The onboarding experience
// works best by starting with a database, so we're filtering those out.
return template.open_application === null
})
.filter((template) => {
const keywords = template.keywords.split(',')
keywords.push(template.name)
const regex = new RegExp('(' + escapeRegExp(search) + ')', 'i')
return keywords.some((value) => value.match(regex))
})
return allTemplates.slice(0, 6)
},
},
async mounted() {
try {
const { data } = await TemplateService(this.$client).fetchAll()
this.categories = data
} catch (error) {
notifyIf(error, 'templates')
}
this.loading = false
},
methods: {
selectTemplate(template) {
this.selectedTemplate = template.id
this.$emit('selected-template', template)
},
},
}
</script>

View file

@ -1001,11 +1001,12 @@
},
"databaseStep": {
"title": "Create your first database",
"description": "Let us know what you're working on.",
"description": "Select what where you'd like to start from:",
"databaseNameLabel": "Database name",
"scratch": "From scratch",
"import": "From file",
"airtable": "From Airtable"
"scratch": "Scratch",
"import": "File",
"airtable": "Airtable",
"template": "Template"
},
"ViewFilterTypeDateUpgradeToMultiStep": {
"migrateButtonText": "Migrate to multi-step date filter",

View file

@ -14,6 +14,8 @@ import FieldService from '@baserow/modules/database/services/field'
import RowService from '@baserow/modules/database/services/row'
import AirtableService from '@baserow/modules/database/services/airtable'
import DatabaseScratchTrackFieldsStep from '@baserow/modules/database/components/onboarding/DatabaseScratchTrackFieldsStep.vue'
import DatabaseTemplatePreview from '@baserow/modules/database/components/onboarding/DatabaseTemplatePreview'
import TemplateService from '@baserow/modules/core/services/template'
const databaseTypeCondition = (data, type) => {
const dependingType = DatabaseOnboardingType.getType()
@ -52,8 +54,14 @@ export class DatabaseOnboardingType extends OnboardingType {
return DatabaseStep
}
getPreviewComponent() {
return DatabaseAppLayoutPreview
getPreviewComponent(data) {
const type = data[this.getType()]?.type
const template = data[this.getType()]?.template
if (type === 'template' && template) {
return DatabaseTemplatePreview
} else {
return DatabaseAppLayoutPreview
}
}
getAdditionalPreviewProps() {
@ -61,14 +69,15 @@ export class DatabaseOnboardingType extends OnboardingType {
}
async complete(data, responses) {
const type = data[this.getType()].type
if (type === 'airtable') {
const workspace = responses[WorkspaceOnboardingType.getType()]
const airtableUrl = data[this.getType()].airtableUrl
const skipFiles = data[this.getType()].skipFiles
const useSession = data[this.getType()].useSession
const session = data[this.getType()].session
const sessionSignature = data[this.getType()].sessionSignature
const workspace = responses[WorkspaceOnboardingType.getType()]
const stepData = data[this.getType()]
const fromType = stepData.type
if (fromType === 'airtable') {
const airtableUrl = stepData.airtableUrl
const skipFiles = stepData.skipFiles
const useSession = stepData.useSession
const session = stepData.session
const sessionSignature = stepData.sessionSignature
const { data: job } = await AirtableService(this.app.$client).create(
workspace.id,
airtableUrl,
@ -77,6 +86,15 @@ export class DatabaseOnboardingType extends OnboardingType {
useSession ? sessionSignature : null
)
// Responds with the newly created job, so that the `getJobForPolling` can use
// the response to mark the onboarding as an async job.
return job
} else if (fromType === 'template') {
const template = stepData.template
const { data: job } = await TemplateService(
this.app.$client
).asyncInstall(workspace.id, template.id)
// Responds with the newly created job, so that the `getJobForPolling` can use
// the response to mark the onboarding as an async job.
return job
@ -85,15 +103,26 @@ export class DatabaseOnboardingType extends OnboardingType {
getJobForPolling(data, responses) {
const type = data[this.getType()].type
if (type === 'airtable') {
if (type === 'airtable' || type === 'template') {
return responses[this.getType()]
}
}
getCompletedRoute(data, responses) {
const type = data[this.getType()].type
let database = null
if (type === 'airtable') {
const database = responses[this.getType()].database
database = responses[this.getType()].database
} else if (type === 'template') {
database = responses[this.getType()].installed_applications.find(
(application) => application.type === DatabaseApplicationType.getType()
)
}
// Deliberately open the database first because that's where the user must start
// their journey. If no database exist, return nothing, so that the dashboard
// is opened.
if (database) {
const firstTableId = database.tables[0]?.id || 0
return {
name: 'database-table',