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 baserow/baserow!3297
This commit is contained in:
commit
1d099f3c91
20 changed files with 292 additions and 34 deletions
backend/templates
content-scheduling-manager.jsonemployee-directory.jsonlightweight-crm.jsonnew-hire-onboarding.jsonobjectives-key-results.jsonuser-feedback.json
changelog/entries/unreleased/feature
web-frontend/modules
core
database
|
@ -12,7 +12,8 @@
|
|||
"publisher",
|
||||
"planner",
|
||||
"publish",
|
||||
"social media"
|
||||
"social media",
|
||||
"onboarding"
|
||||
],
|
||||
"categories": [
|
||||
"Marketing"
|
||||
|
@ -4891,4 +4892,4 @@
|
|||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,8 @@
|
|||
"teams",
|
||||
"employee list",
|
||||
"staff catalog",
|
||||
"staff records"
|
||||
"staff records",
|
||||
"onboarding"
|
||||
],
|
||||
"categories": [
|
||||
"HR and Recruiting"
|
||||
|
@ -3777,4 +3778,4 @@
|
|||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
"keywords": [
|
||||
"CRM",
|
||||
"Sales",
|
||||
"Local Business"
|
||||
"Local Business",
|
||||
"onboarding"
|
||||
],
|
||||
"categories": [
|
||||
"Sales and CRM"
|
||||
|
@ -25570,4 +25571,4 @@
|
|||
"type": "builder"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,8 @@
|
|||
"recruits",
|
||||
"hiring",
|
||||
"hr",
|
||||
"human resources"
|
||||
"human resources",
|
||||
"onboarding"
|
||||
],
|
||||
"categories": [
|
||||
"HR and Recruiting"
|
||||
|
@ -11144,4 +11145,4 @@
|
|||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,8 @@
|
|||
"customer survey",
|
||||
"client feedback",
|
||||
"UAT",
|
||||
"user acceptance testing"
|
||||
"user acceptance testing",
|
||||
"onboarding"
|
||||
],
|
||||
"categories": [
|
||||
"Product Management",
|
||||
|
@ -3716,4 +3717,4 @@
|
|||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"type": "feature",
|
||||
"message": "Install template during onboarding",
|
||||
"domain": "database",
|
||||
"issue_number": 2638,
|
||||
"bullet_points": [],
|
||||
"created_at": "2025-03-24"
|
||||
}
|
|
@ -184,3 +184,4 @@
|
|||
@import 'admin_dashboard';
|
||||
@import 'user_admin';
|
||||
@import 'group_admin';
|
||||
@import 'template_import_item';
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -218,6 +218,10 @@
|
|||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.justify-content-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.justify-content-end {
|
||||
justify-content: end;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
:class="{
|
||||
'segment-control__button--active': index === activeIndex,
|
||||
}"
|
||||
:title="segment.label"
|
||||
class="segment-control__button"
|
||||
@click="setActiveIndex(index)"
|
||||
>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -83,7 +83,7 @@
|
|||
</div>
|
||||
<div class="onboarding__preview">
|
||||
<component
|
||||
:is="step.getPreviewComponent()"
|
||||
:is="step.getPreviewComponent(data)"
|
||||
v-bind="step.getAdditionalPreviewProps()"
|
||||
:data="data"
|
||||
></component>
|
||||
|
|
|
@ -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 = {}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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",
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Add table
Reference in a new issue