mirror of
https://gitlab.com/bramw/baserow.git
synced 2024-11-21 23:37:55 +00:00
307 lines
9.8 KiB
Vue
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>
|