1
0
mirror of https://gitlab.com/bramw/baserow.git synced 2024-11-21 23:37:55 +00:00
bramw_baserow/web-frontend/modules/core/pages/onboarding.vue
2024-10-24 12:26:11 +00:00

307 lines
9.8 KiB
Vue

<template>
<div class="onboarding">
<Toasts></Toasts>
<div v-if="creating && creatingFailed" class="onboarding__loading">
<div class="onboarding__loading-text">
{{ $t('onboarding.failedTitle') }}
</div>
<p>
{{ $t('onboarding.failedDescription') }}
</p>
<div>
<Button
type="secondary"
size="large"
:loading="reloading"
@click="refresh()"
>{{ $t('onboarding.failedTryAgain') }}</Button
>
<Button
type="danger"
size="large"
:loading="cancelling"
@click="cancel"
>{{ $t('onboarding.failedSkip') }}</Button
>
</div>
</div>
<div v-else-if="creating" class="onboarding__loading">
<div class="loading"></div>
<div class="onboarding__loading-text">
{{ $t('onboarding.creating') }}
</div>
<div v-if="job" class="onboarding__loading-progress">
<ProgressBar :value="job.progress_percentage" />
</div>
</div>
<template v-else>
<div class="onboarding__form">
<div class="onboarding__head">
<Logo class="onboarding__logo" />
<CircleProgressBar :value="progressPercentage"></CircleProgressBar>
</div>
<div ref="bodyWrapper" class="onboarding__body-wrapper">
<div class="onboarding__body">
<div>
<component
:is="step.getFormComponent()"
ref="form"
:data="data"
@update-data="updateData"
></component>
</div>
<div class="onboarding__actions">
<Button
:ph-autocapture="'onboarding-continue-step-' + step.getType()"
type="primary"
size="large"
full-width
:disabled="!isValid() || !data"
@click="next()"
>{{ $t('onboarding.continue') }}</Button
>
<div v-if="canSkip" class="onboarding__skip">
<ButtonText
:ph-autocapture="'onboarding-skip-step-' + step.getType()"
tag="a"
@click="skip"
>{{ $t('onboarding.skip') }}</ButtonText
>
</div>
</div>
</div>
</div>
<div v-if="stepIndex === 0" class="onboarding__cancel">
<ButtonText
:ph-autocapture="'onboarding-cancel-step-' + step.getType()"
tag="a"
:loading="cancelling"
@click="cancel"
>{{ $t('onboarding.cancel') }}</ButtonText
>
</div>
</div>
<div class="onboarding__preview">
<component
:is="step.getPreviewComponent()"
v-bind="step.getAdditionalPreviewProps()"
:data="data"
></component>
</div>
</template>
</div>
</template>
<script>
import CircleProgressBar from '@baserow/modules/core/components/CircleProgressBar.vue'
import { notifyIf } from '@baserow/modules/core/utils/error'
import Toasts from '@baserow/modules/core/components/toasts/Toasts'
import AuthService from '@baserow/modules/core/services/auth'
import WorkspaceService from '@baserow/modules/core/services/workspace'
import error from '@baserow/modules/core/mixins/error'
import jobProgress from '@baserow/modules/core/mixins/jobProgress'
export default {
components: { Toasts, CircleProgressBar },
mixins: [error, jobProgress],
middleware: ['settings', 'authenticated'],
asyncData({ store, redirect }) {
// If the user has completed the onboarding, then redirect to the on-boarding page
// so that the user can create their first one.
const user = store.getters['auth/getUserObject']
if (user.completed_onboarding) {
return redirect({ name: 'dashboard' })
}
},
data() {
return {
stepIndex: 0,
data: {},
creating: false,
creatingFailed: false,
cancelling: false,
reloading: false,
}
},
head() {
return {
title: this.$t('onboarding.title'),
}
},
computed: {
steps() {
const steps = Object.values(this.$registry.getAll('onboarding'))
return steps
.filter((step) => {
return step.condition(this.data)
})
.sort((a, b) => a.getOrder() - b.getOrder())
},
step() {
return this.steps[this.stepIndex]
},
progressPercentage() {
return Math.ceil((this.stepIndex / this.steps.length) * 100)
},
canSkip() {
return this.step.canSkip()
},
},
methods: {
/**
* Called when the user wants to go to the user step. This means that the provided
* form values must be valid. If the onboarding reached the end, it should
* automatically complete it.
*/
async next() {
if (this.stepIndex === this.steps.length - 1) {
await this.complete()
} else {
this.stepIndex++
this.$nextTick(() => {
this.$refs.bodyWrapper.scrollTop = 0
})
}
},
/**
* Called when the user wants to skip a step. It's not possible to this for every
* step.
*/
async skip() {
// If the step is skipped, we don't want to store any left over data of the form
// because that can influence what happens when completing.
delete this.data[this.step.getType()]
await this.next()
},
/**
* Called when all the steps have been filled out. It will start the process off
* completing the onboarding by collecting the data filled out by every step, and
* call the `complete` method of every step. This will make sure that the onboarding
* only creating the appropriate resources if every step has been completed
* successfully.
*/
async complete() {
this.creating = true
const responses = {}
let route = { name: 'dashboard' }
// Now that all the steps have been completed, we're looping over all of them and
// execute the `complete` method to actually create the configured workspace.
for (let i = 0; i < this.steps.length; i++) {
const step = this.steps[i]
try {
responses[step.getType()] = await step.complete(this.data, responses)
} catch (error) {
// Stop the creating process if any of the steps fail.
this.creatingFailed = true
return
}
// Check if there is a job that must be polled after completion. If so, it will
// show a progressbar to the user, and it will set the job end result as
// response for this onboarding step.
const job = step.getJobForPolling(this.data, responses)
if (job) {
try {
await this.startAndWaitForJob(job)
responses[step.getType()] = this.job
this.job = null
} catch (error) {
this.creatingFailed = true
return
}
}
// Check if the step has a route, and overwrite that one. The user will be
// redirected to the last route set.
const completedRoute = step.getCompletedRoute(this.data, responses)
if (completedRoute) {
route = completedRoute
}
}
await this.markAsComplete()
// Clear all workspaces and application so that they're fetched again when
// navigating to the dashboard. This will make sure that everything is correctly
// loaded.
await this.$store.dispatch('workspace/clearAll')
await this.$store.dispatch('application/clearAll')
this.$router.push(route)
},
/**
* Mark the onboarding as completed, and redirect the user to the dashboard so
* that they can start working with their database.
*/
async markAsComplete() {
try {
const { data } = await AuthService(this.$client).update({
completed_onboarding: true,
})
this.$store.dispatch('auth/forceUpdateUserData', { user: data })
} catch (error) {
notifyIf(error)
}
},
/**
* Called when the user clicks on the cancel button. This will stop the onboarding,
* create an initial workspace, and mark it as completed.
*/
async cancel() {
this.cancelling = true
try {
await WorkspaceService(this.$client).createInitialWorkspace()
} catch (error) {
notifyIf(error)
}
await this.markAsComplete()
// Clear all workspaces and application so that they're fetched again when
// navigating to the dashboard. This will make sure that everything is correctly
// loaded.
await this.$store.dispatch('workspace/clearAll')
await this.$store.dispatch('application/clearAll')
this.$router.push({ name: 'dashboard' })
},
updateData(data) {
this.$set(this.data, this.step.getType(), data)
},
isValid() {
const form = this.$refs?.form
// It can be that the component hasn't been rendered yet. In that case, the button
// must be disabled because we have to wait until it's rendered.
if (!form) {
return false
}
const isValid = form?.isValid
if (typeof isValid === 'function') {
return isValid()
} else {
throw new TypeError(
'The onboarding form component must contain an `isValid` function.'
)
}
},
refresh() {
this.reloading = true
location.reload()
},
startAndWaitForJob(job) {
this.startJobPoller(job)
return new Promise((resolve, reject) => {
const intervalId = setInterval(() => {
if (this.jobHasSucceeded) {
clearInterval(intervalId)
resolve(job)
} else if (this.jobHasFailed) {
clearInterval(intervalId)
reject(new Error('job failed'))
}
}, 100)
})
},
},
}
</script>