Merge branch 'master' into ts/type-create-screen-modal
This commit is contained in:
commit
ff1ac05d3b
|
@ -30,6 +30,7 @@ spec:
|
||||||
{{- toYaml .Values.services.apps.templateLabels | indent 8 -}}
|
{{- toYaml .Values.services.apps.templateLabels | indent 8 -}}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
spec:
|
spec:
|
||||||
|
terminationGracePeriodSeconds: 60
|
||||||
containers:
|
containers:
|
||||||
- env:
|
- env:
|
||||||
- name: BUDIBASE_ENVIRONMENT
|
- name: BUDIBASE_ENVIRONMENT
|
||||||
|
|
|
@ -30,6 +30,7 @@ spec:
|
||||||
{{- toYaml .Values.services.worker.templateLabels | indent 8 -}}
|
{{- toYaml .Values.services.worker.templateLabels | indent 8 -}}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
spec:
|
spec:
|
||||||
|
terminationGracePeriodSeconds: 60
|
||||||
containers:
|
containers:
|
||||||
- env:
|
- env:
|
||||||
- name: BUDIBASE_ENVIRONMENT
|
- name: BUDIBASE_ENVIRONMENT
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||||
"version": "3.10.0",
|
"version": "3.10.2",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"concurrency": 20,
|
"concurrency": 20,
|
||||||
"command": {
|
"command": {
|
||||||
|
|
|
@ -14,7 +14,6 @@ import {
|
||||||
UserPermissionAssignedEvent,
|
UserPermissionAssignedEvent,
|
||||||
UserPermissionRemovedEvent,
|
UserPermissionRemovedEvent,
|
||||||
UserUpdatedEvent,
|
UserUpdatedEvent,
|
||||||
UserOnboardingEvent,
|
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { isScim } from "../../context"
|
import { isScim } from "../../context"
|
||||||
|
|
||||||
|
@ -51,16 +50,6 @@ async function deleted(user: User) {
|
||||||
await publishEvent(Event.USER_DELETED, properties)
|
await publishEvent(Event.USER_DELETED, properties)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function onboardingComplete(user: User) {
|
|
||||||
const properties: UserOnboardingEvent = {
|
|
||||||
userId: user._id as string,
|
|
||||||
audited: {
|
|
||||||
email: user.email,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
await publishEvent(Event.USER_ONBOARDING_COMPLETE, properties)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PERMISSIONS
|
// PERMISSIONS
|
||||||
|
|
||||||
async function permissionAdminAssigned(user: User, timestamp?: number) {
|
async function permissionAdminAssigned(user: User, timestamp?: number) {
|
||||||
|
@ -191,7 +180,6 @@ export default {
|
||||||
permissionAdminRemoved,
|
permissionAdminRemoved,
|
||||||
permissionBuilderAssigned,
|
permissionBuilderAssigned,
|
||||||
permissionBuilderRemoved,
|
permissionBuilderRemoved,
|
||||||
onboardingComplete,
|
|
||||||
invited,
|
invited,
|
||||||
inviteAccepted,
|
inviteAccepted,
|
||||||
passwordForceReset,
|
passwordForceReset,
|
||||||
|
|
|
@ -76,10 +76,6 @@ export const handleSaveEvents = async (
|
||||||
await events.user.permissionAdminRemoved(user)
|
await events.user.permissionAdminRemoved(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isOnboardingComplete(user, existingUser)) {
|
|
||||||
await events.user.onboardingComplete(user)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!existingUser.forceResetPassword &&
|
!existingUser.forceResetPassword &&
|
||||||
user.forceResetPassword &&
|
user.forceResetPassword &&
|
||||||
|
@ -122,10 +118,6 @@ const isRemovingAdmin = (user: any, existingUser: any) => {
|
||||||
return isRemovingPermission(user, existingUser, hasAdminPermissions)
|
return isRemovingPermission(user, existingUser, hasAdminPermissions)
|
||||||
}
|
}
|
||||||
|
|
||||||
const isOnboardingComplete = (user: any, existingUser: any) => {
|
|
||||||
return !existingUser?.onboardedAt && typeof user.onboardedAt === "string"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a permission is being added to a new or existing user.
|
* Check if a permission is being added to a new or existing user.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { Icon, Modal } from "@budibase/bbui"
|
|
||||||
import ChooseIconModal from "@/components/start/ChooseIconModal.svelte"
|
import ChooseIconModal from "@/components/start/ChooseIconModal.svelte"
|
||||||
|
import { Icon, Modal } from "@budibase/bbui"
|
||||||
|
|
||||||
export let name
|
export let name: string
|
||||||
export let size = "M"
|
export let size: "M" = "M"
|
||||||
export let app
|
export let color: string
|
||||||
export let color
|
export let disabled: boolean = false
|
||||||
export let autoSave = false
|
|
||||||
export let disabled = false
|
|
||||||
|
|
||||||
let modal
|
let modal: Modal
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
@ -28,7 +26,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
<ChooseIconModal {name} {color} {app} {autoSave} on:change />
|
<ChooseIconModal {name} {color} on:change />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -27,8 +27,6 @@
|
||||||
sortedScreens,
|
sortedScreens,
|
||||||
appPublished,
|
appPublished,
|
||||||
} from "@/stores/builder"
|
} from "@/stores/builder"
|
||||||
import TourWrap from "@/components/portal/onboarding/TourWrap.svelte"
|
|
||||||
import { TOUR_STEP_KEYS } from "@/components/portal/onboarding/tours.js"
|
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
|
|
||||||
export let application
|
export let application
|
||||||
|
@ -153,12 +151,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<TourWrap
|
|
||||||
stepKeys={[
|
|
||||||
TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT,
|
|
||||||
TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<div class="app-action-button users">
|
<div class="app-action-button users">
|
||||||
<div class="app-action" id="builder-app-users-button">
|
<div class="app-action" id="builder-app-users-button">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
@ -172,7 +164,6 @@
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TourWrap>
|
|
||||||
|
|
||||||
<div class="app-action-button preview">
|
<div class="app-action-button preview">
|
||||||
<div class="app-action">
|
<div class="app-action">
|
||||||
|
@ -201,7 +192,6 @@
|
||||||
<div bind:this={appActionPopoverAnchor}>
|
<div bind:this={appActionPopoverAnchor}>
|
||||||
<div class="app-action">
|
<div class="app-action">
|
||||||
<Icon name={$appPublished ? "GlobeCheck" : "GlobeStrike"} />
|
<Icon name={$appPublished ? "GlobeCheck" : "GlobeStrike"} />
|
||||||
<TourWrap stepKeys={[TOUR_STEP_KEYS.BUILDER_APP_PUBLISH]}>
|
|
||||||
<span class="publish-open" id="builder-app-publish-button">
|
<span class="publish-open" id="builder-app-publish-button">
|
||||||
Publish
|
Publish
|
||||||
<Icon
|
<Icon
|
||||||
|
@ -209,7 +199,6 @@
|
||||||
size="M"
|
size="M"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</TourWrap>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Popover
|
<Popover
|
||||||
|
|
|
@ -1,167 +0,0 @@
|
||||||
<script>
|
|
||||||
import { Popover, Layout, Heading, Body, Button, Link } from "@budibase/bbui"
|
|
||||||
import { TOURS, getCurrentStepIdx } from "./tours.js"
|
|
||||||
import { goto, layout, isActive } from "@roxi/routify"
|
|
||||||
import { builderStore } from "@/stores/builder"
|
|
||||||
|
|
||||||
let popoverAnchor
|
|
||||||
let popover
|
|
||||||
let tourSteps = null
|
|
||||||
let tourStep
|
|
||||||
let tourStepIdx
|
|
||||||
let lastStep
|
|
||||||
let skipping = false
|
|
||||||
|
|
||||||
$: tourNodes = { ...$builderStore.tourNodes }
|
|
||||||
$: tourKey = $builderStore.tourKey
|
|
||||||
$: tourStepKey = $builderStore.tourStepKey
|
|
||||||
$: tour = TOURS[tourKey]
|
|
||||||
$: tourOnSkip = tour?.onSkip
|
|
||||||
|
|
||||||
const updateTourStep = (targetStepKey, tourKey) => {
|
|
||||||
if (!tourKey) {
|
|
||||||
tourSteps = null
|
|
||||||
tourStepIdx = null
|
|
||||||
lastStep = null
|
|
||||||
tourStep = null
|
|
||||||
popoverAnchor = null
|
|
||||||
popover = null
|
|
||||||
skipping = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!tourSteps?.length) {
|
|
||||||
tourSteps = [...tour.steps]
|
|
||||||
}
|
|
||||||
tourStepIdx = getCurrentStepIdx(tourSteps, targetStepKey)
|
|
||||||
lastStep = tourStepIdx + 1 == tourSteps.length
|
|
||||||
tourStep = { ...tourSteps[tourStepIdx] }
|
|
||||||
tourStep.onLoad()
|
|
||||||
}
|
|
||||||
|
|
||||||
$: updateTourStep(tourStepKey, tourKey)
|
|
||||||
|
|
||||||
const showPopover = (tourStep, tourNodes, popover) => {
|
|
||||||
if (!tourStep) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
popoverAnchor = tourNodes[tourStep.id]
|
|
||||||
popover?.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
$: showPopover(tourStep, tourNodes, popover)
|
|
||||||
|
|
||||||
const navigateStep = step => {
|
|
||||||
if (step.route) {
|
|
||||||
const activeNav = $layout.children.find(c => $isActive(c.path))
|
|
||||||
if (activeNav) {
|
|
||||||
builderStore.setPreviousTopNavPath(
|
|
||||||
activeNav.path,
|
|
||||||
window.location.pathname
|
|
||||||
)
|
|
||||||
$goto($builderStore.previousTopNavPath[step.route] || step.route)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextStep = async () => {
|
|
||||||
if (!lastStep === true) {
|
|
||||||
let target = tourSteps[tourStepIdx + 1]
|
|
||||||
if (target) {
|
|
||||||
builderStore.update(state => ({
|
|
||||||
...state,
|
|
||||||
tourStepKey: target.id,
|
|
||||||
}))
|
|
||||||
navigateStep(target)
|
|
||||||
} else {
|
|
||||||
console.warn("Could not retrieve step")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (typeof tourStep.onComplete === "function") {
|
|
||||||
tourStep.onComplete()
|
|
||||||
}
|
|
||||||
popover.hide()
|
|
||||||
if (tour.endRoute) {
|
|
||||||
$goto(tour.endRoute)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if tourKey}
|
|
||||||
{#key tourStepKey}
|
|
||||||
<Popover
|
|
||||||
align={tourStep?.align}
|
|
||||||
bind:this={popover}
|
|
||||||
anchor={popoverAnchor}
|
|
||||||
maxWidth={300}
|
|
||||||
dismissible={false}
|
|
||||||
offset={12}
|
|
||||||
handlePositionUpdate={tourStep?.positionHandler}
|
|
||||||
customZIndex={3}
|
|
||||||
>
|
|
||||||
<div class="tour-content">
|
|
||||||
<Layout noPadding gap="M">
|
|
||||||
<div class="tour-header">
|
|
||||||
<Heading size="XS">{tourStep?.title || "-"}</Heading>
|
|
||||||
{#if tourSteps?.length > 1}
|
|
||||||
<div>{`${tourStepIdx + 1}/${tourSteps?.length}`}</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<Body size="S">
|
|
||||||
<span class="tour-body">
|
|
||||||
{#if tourStep.layout}
|
|
||||||
<svelte:component this={tourStep.layout} />
|
|
||||||
{:else}
|
|
||||||
{tourStep?.body || ""}
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
</Body>
|
|
||||||
<div class="tour-footer">
|
|
||||||
<div class="tour-navigation">
|
|
||||||
{#if typeof tourOnSkip === "function" && !lastStep}
|
|
||||||
<Link
|
|
||||||
secondary
|
|
||||||
quiet
|
|
||||||
on:click={() => {
|
|
||||||
skipping = true
|
|
||||||
tourOnSkip()
|
|
||||||
if (tour.endRoute) {
|
|
||||||
$goto(tour.endRoute)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={skipping}
|
|
||||||
>
|
|
||||||
Skip
|
|
||||||
</Link>
|
|
||||||
{/if}
|
|
||||||
<Button cta on:click={nextStep} disabled={skipping}>
|
|
||||||
<div>{lastStep ? "Finish" : "Next"}</div>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
{/key}
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.tour-content {
|
|
||||||
padding: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
.tour-navigation {
|
|
||||||
grid-gap: var(--spacing-xl);
|
|
||||||
display: flex;
|
|
||||||
justify-content: end;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.tour-body :global(.feature-list) {
|
|
||||||
margin-bottom: 0px;
|
|
||||||
padding-left: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
.tour-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,64 +0,0 @@
|
||||||
<script>
|
|
||||||
import { tourHandler } from "./tourHandler"
|
|
||||||
import { TOURSBYSTEP, TOURS, getCurrentStepIdx } from "./tours"
|
|
||||||
import { onMount, onDestroy } from "svelte"
|
|
||||||
import { builderStore } from "@/stores/builder"
|
|
||||||
|
|
||||||
export let stepKeys = []
|
|
||||||
|
|
||||||
let ready = false
|
|
||||||
let registered = {}
|
|
||||||
|
|
||||||
const registerTourNode = (tourKey, stepKey) => {
|
|
||||||
const step = TOURSBYSTEP[stepKey]
|
|
||||||
if (ready && step && !registered[stepKey] && step?.tour === tourKey) {
|
|
||||||
const elem = document.querySelector(step.query)
|
|
||||||
registered[stepKey] = tourHandler(elem, stepKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const scrollToStep = () => {
|
|
||||||
let tourStepIdx = getCurrentStepIdx(
|
|
||||||
TOURS[tourKeyWatch]?.steps,
|
|
||||||
tourStepKeyWatch
|
|
||||||
)
|
|
||||||
let currentStep = TOURS[tourKeyWatch]?.steps?.[tourStepIdx]
|
|
||||||
if (currentStep?.scrollIntoView) {
|
|
||||||
let currentNode = $builderStore.tourNodes?.[currentStep.id]
|
|
||||||
if (currentNode) {
|
|
||||||
currentNode.scrollIntoView({ behavior: "smooth", block: "center" })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$: tourKeyWatch = $builderStore.tourKey
|
|
||||||
$: tourStepKeyWatch = $builderStore.tourStepKey
|
|
||||||
$: if (tourKeyWatch || stepKeys || ready) {
|
|
||||||
stepKeys.forEach(tourStepKey => {
|
|
||||||
registerTourNode(tourKeyWatch, tourStepKey)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
$: scrollToStep(tourKeyWatch, tourStepKeyWatch)
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
ready = true
|
|
||||||
})
|
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
Object.entries(registered).forEach(entry => {
|
|
||||||
const handler = entry[1]
|
|
||||||
const stepKey = entry[0]
|
|
||||||
// Run step destroy, de-register nodes in the builderStore and local cache
|
|
||||||
handler.destroy()
|
|
||||||
delete registered[stepKey]
|
|
||||||
|
|
||||||
// Check if the step is part of an active tour. End the tour if that is the case
|
|
||||||
const step = TOURSBYSTEP[stepKey]
|
|
||||||
if (step.tour === tourKeyWatch) {
|
|
||||||
builderStore.setTour()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<slot />
|
|
|
@ -1,9 +0,0 @@
|
||||||
<div>
|
|
||||||
When faced with a sizable form, consider implementing a multi-step approach to
|
|
||||||
enhance user experience.
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Breaking the form into multiple steps can significantly improve usability by
|
|
||||||
making the process more digestible for your users.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
|
@ -1,17 +0,0 @@
|
||||||
<div>
|
|
||||||
You can use bindings to set the Row ID on your form.
|
|
||||||
<p>
|
|
||||||
This will allow you to pull the correct information into your form and allow
|
|
||||||
you to update!
|
|
||||||
</p>
|
|
||||||
<a href="https://docs.budibase.com/docs/form-block" target="_blank">
|
|
||||||
How to pass a row ID using bindings
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
a {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,10 +0,0 @@
|
||||||
<div>
|
|
||||||
In this section you can manage the data for your app:
|
|
||||||
<ul class="feature-list">
|
|
||||||
<li>Connect data sources</li>
|
|
||||||
<li>Edit data</li>
|
|
||||||
<li>Manage read & write access</li>
|
|
||||||
<li>Create views</li>
|
|
||||||
<li>Add bindings</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
|
@ -1,10 +0,0 @@
|
||||||
<div>
|
|
||||||
After setting up your data, Design is where you build the screens for your
|
|
||||||
app:
|
|
||||||
<ul class="feature-list">
|
|
||||||
<li>Add screens</li>
|
|
||||||
<li>Add components</li>
|
|
||||||
<li>Choose your theme</li>
|
|
||||||
<li>Edit navigation</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
|
@ -1,7 +0,0 @@
|
||||||
<div>
|
|
||||||
Once you’re happy with your app you can publish it to production!
|
|
||||||
<p>
|
|
||||||
After publishing, any changes you make will not take affect until you next
|
|
||||||
publish.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
|
@ -1,5 +0,0 @@
|
||||||
export { default as OnboardingData } from "./OnboardingData.svelte"
|
|
||||||
export { default as OnboardingDesign } from "./OnboardingDesign.svelte"
|
|
||||||
export { default as OnboardingPublish } from "./OnboardingPublish.svelte"
|
|
||||||
export { default as NewViewUpdateFormRowId } from "./NewViewUpdateFormRowId.svelte"
|
|
||||||
export { default as NewFormSteps } from "./NewFormSteps.svelte"
|
|
|
@ -1,26 +0,0 @@
|
||||||
import { builderStore } from "@/stores/builder"
|
|
||||||
import { get } from "svelte/store"
|
|
||||||
|
|
||||||
const registerNode = async (node, tourStepKey) => {
|
|
||||||
if (!node) {
|
|
||||||
console.warn("Tour Handler - an anchor node is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!get(builderStore).tourKey) {
|
|
||||||
console.warn("Tour Handler - No active tour ", tourStepKey, node)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
builderStore.registerTourNode(tourStepKey, node)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function tourHandler(node, tourStepKey) {
|
|
||||||
if (node && tourStepKey) {
|
|
||||||
registerNode(node, tourStepKey)
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
destroy: () => {
|
|
||||||
builderStore.destroyTourNode(tourStepKey)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,238 +0,0 @@
|
||||||
import { get } from "svelte/store"
|
|
||||||
import { builderStore } from "@/stores/builder"
|
|
||||||
import { auth } from "@/stores/portal"
|
|
||||||
import analytics from "@/analytics"
|
|
||||||
import {
|
|
||||||
OnboardingData,
|
|
||||||
OnboardingDesign,
|
|
||||||
OnboardingPublish,
|
|
||||||
NewViewUpdateFormRowId,
|
|
||||||
NewFormSteps,
|
|
||||||
} from "./steps"
|
|
||||||
import { API } from "@/api"
|
|
||||||
|
|
||||||
const ONBOARDING_EVENT_PREFIX = "onboarding"
|
|
||||||
|
|
||||||
export const TOUR_STEP_KEYS = {
|
|
||||||
BUILDER_APP_PUBLISH: "builder-app-publish",
|
|
||||||
BUILDER_DATA_SECTION: "builder-data-section",
|
|
||||||
BUILDER_DESIGN_SECTION: "builder-design-section",
|
|
||||||
BUILDER_USER_MANAGEMENT: "builder-user-management",
|
|
||||||
BUILDER_AUTOMATION_SECTION: "builder-automation-section",
|
|
||||||
FEATURE_USER_MANAGEMENT: "feature-user-management",
|
|
||||||
BUILDER_FORM_CREATE_STEPS: "builder-form-create-steps",
|
|
||||||
BUILDER_FORM_VIEW_UPDATE_STEPS: "builder-form-view-update-steps",
|
|
||||||
BUILDER_FORM_ROW_ID: "builder-form-row-id",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TOUR_KEYS = {
|
|
||||||
TOUR_BUILDER_ONBOARDING: "builder-onboarding",
|
|
||||||
FEATURE_ONBOARDING: "feature-onboarding",
|
|
||||||
BUILDER_FORM_CREATE: "builder-form-create",
|
|
||||||
BUILDER_FORM_VIEW_UPDATE: "builder-form-view-update",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getCurrentStepIdx = (steps, tourStepKey) => {
|
|
||||||
if (!steps?.length) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (steps?.length && !tourStepKey) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return steps.findIndex(step => step.id === tourStepKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
const endUserOnboarding = async ({ skipped = false } = {}) => {
|
|
||||||
// Mark the users onboarding as complete
|
|
||||||
// Clear all tour related state
|
|
||||||
if (get(auth).user) {
|
|
||||||
try {
|
|
||||||
await API.updateSelf({
|
|
||||||
onboardedAt: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (skipped) {
|
|
||||||
tourEvent("skipped")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the cached user
|
|
||||||
await auth.getSelf()
|
|
||||||
|
|
||||||
builderStore.endBuilderOnboarding()
|
|
||||||
builderStore.setTour()
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Onboarding failed", e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const endTour = async ({ key, skipped = false } = {}) => {
|
|
||||||
const { tours = {} } = get(auth).user
|
|
||||||
tours[key] = new Date().toISOString()
|
|
||||||
|
|
||||||
await API.updateSelf({
|
|
||||||
tours,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (skipped) {
|
|
||||||
tourEvent(key, skipped)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the cached user
|
|
||||||
await auth.getSelf()
|
|
||||||
|
|
||||||
// Reset tour state
|
|
||||||
builderStore.setTour()
|
|
||||||
}
|
|
||||||
|
|
||||||
const tourEvent = (eventKey, skipped) => {
|
|
||||||
analytics.captureEvent(`${ONBOARDING_EVENT_PREFIX}:${eventKey}`, {
|
|
||||||
eventSource: EventSource.PORTAL,
|
|
||||||
skipped,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTours = () => {
|
|
||||||
return {
|
|
||||||
[TOUR_KEYS.TOUR_BUILDER_ONBOARDING]: {
|
|
||||||
steps: [
|
|
||||||
{
|
|
||||||
id: TOUR_STEP_KEYS.BUILDER_DATA_SECTION,
|
|
||||||
title: "Data",
|
|
||||||
route: "/builder/app/:application/data",
|
|
||||||
layout: OnboardingData,
|
|
||||||
query: ".topleftnav .spectrum-Tabs-item#builder-data-tab",
|
|
||||||
onLoad: async () => {
|
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_DATA_SECTION)
|
|
||||||
},
|
|
||||||
align: "left",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION,
|
|
||||||
title: "Design",
|
|
||||||
route: "/builder/app/:application/design",
|
|
||||||
layout: OnboardingDesign,
|
|
||||||
query: ".topleftnav .spectrum-Tabs-item#builder-design-tab",
|
|
||||||
onLoad: () => {
|
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION)
|
|
||||||
},
|
|
||||||
align: "left",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: TOUR_STEP_KEYS.BUILDER_AUTOMATION_SECTION,
|
|
||||||
title: "Automations",
|
|
||||||
route: "/builder/app/:application/automation",
|
|
||||||
query: ".topleftnav .spectrum-Tabs-item#builder-automation-tab",
|
|
||||||
body: "Once you have your app screens made, you can set up automations to fit in with your current workflow",
|
|
||||||
onLoad: () => {
|
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_AUTOMATION_SECTION)
|
|
||||||
},
|
|
||||||
align: "left",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT,
|
|
||||||
title: "Users",
|
|
||||||
query: ".toprightnav #builder-app-users-button",
|
|
||||||
body: "Add users to your app and control what level of access they have.",
|
|
||||||
onLoad: () => {
|
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: TOUR_STEP_KEYS.BUILDER_APP_PUBLISH,
|
|
||||||
title: "Publish",
|
|
||||||
layout: OnboardingPublish,
|
|
||||||
route: "/builder/app/:application/design",
|
|
||||||
query: ".toprightnav #builder-app-publish-button",
|
|
||||||
onLoad: () => {
|
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_APP_PUBLISH)
|
|
||||||
},
|
|
||||||
onComplete: endUserOnboarding,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
onSkip: async () => {
|
|
||||||
await endUserOnboarding({ skipped: true })
|
|
||||||
},
|
|
||||||
endRoute: "/builder/app/:application/data",
|
|
||||||
},
|
|
||||||
[TOUR_KEYS.FEATURE_ONBOARDING]: {
|
|
||||||
steps: [
|
|
||||||
{
|
|
||||||
id: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT,
|
|
||||||
title: "Users",
|
|
||||||
query: ".toprightnav #builder-app-users-button",
|
|
||||||
body: "Add users to your app and control what level of access they have.",
|
|
||||||
onLoad: () => {
|
|
||||||
tourEvent(TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT)
|
|
||||||
},
|
|
||||||
onComplete: endUserOnboarding,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
[TOUR_KEYS.BUILDER_FORM_CREATE]: {
|
|
||||||
steps: [
|
|
||||||
{
|
|
||||||
id: TOUR_STEP_KEYS.BUILDER_FORM_CREATE_STEPS,
|
|
||||||
title: "Add multiple steps",
|
|
||||||
layout: NewFormSteps,
|
|
||||||
query: "#steps-prop-control-wrap",
|
|
||||||
onComplete: () => {
|
|
||||||
builderStore.highlightSetting()
|
|
||||||
endTour({ key: TOUR_KEYS.BUILDER_FORM_CREATE })
|
|
||||||
},
|
|
||||||
onLoad: () => {
|
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_FORM_CREATE_STEPS)
|
|
||||||
builderStore.highlightSetting("steps", "info")
|
|
||||||
},
|
|
||||||
align: "left-outside",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
[TOUR_KEYS.BUILDER_FORM_VIEW_UPDATE]: {
|
|
||||||
steps: [
|
|
||||||
{
|
|
||||||
id: TOUR_STEP_KEYS.BUILDER_FORM_ROW_ID,
|
|
||||||
title: "Add row ID to update a row",
|
|
||||||
layout: NewViewUpdateFormRowId,
|
|
||||||
query: "#rowId-prop-control-wrap",
|
|
||||||
onLoad: () => {
|
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_FORM_ROW_ID)
|
|
||||||
builderStore.highlightSetting("rowId", "info")
|
|
||||||
},
|
|
||||||
align: "left-outside",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: TOUR_STEP_KEYS.BUILDER_FORM_VIEW_UPDATE_STEPS,
|
|
||||||
title: "Add multiple steps",
|
|
||||||
layout: NewFormSteps,
|
|
||||||
query: "#steps-prop-control-wrap",
|
|
||||||
onComplete: () => {
|
|
||||||
builderStore.highlightSetting()
|
|
||||||
endTour({ key: TOUR_KEYS.BUILDER_FORM_VIEW_UPDATE })
|
|
||||||
},
|
|
||||||
onLoad: () => {
|
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_FORM_VIEW_UPDATE_STEPS)
|
|
||||||
builderStore.highlightSetting("steps", "info")
|
|
||||||
},
|
|
||||||
align: "left-outside",
|
|
||||||
scrollIntoView: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
onSkip: async () => {
|
|
||||||
builderStore.highlightSetting()
|
|
||||||
endTour({ key: TOUR_KEYS.BUILDER_FORM_VIEW_UPDATE, skipped: true })
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TOURS = getTours()
|
|
||||||
export const TOURSBYSTEP = Object.keys(TOURS).reduce((acc, tour) => {
|
|
||||||
TOURS[tour].steps.forEach(element => {
|
|
||||||
acc[element.id] = element
|
|
||||||
acc[element.id]["tour"] = tour
|
|
||||||
})
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
|
@ -1,18 +1,9 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import {
|
import { ColorPicker, Icon, Label, ModalContent } from "@budibase/bbui"
|
||||||
ModalContent,
|
|
||||||
Icon,
|
|
||||||
ColorPicker,
|
|
||||||
Label,
|
|
||||||
notifications,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { appsStore } from "@/stores/portal"
|
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
export let app
|
export let name: string
|
||||||
export let name
|
export let color: string
|
||||||
export let color
|
|
||||||
export let autoSave = false
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -44,18 +35,9 @@
|
||||||
]
|
]
|
||||||
|
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
if (!autoSave) {
|
|
||||||
dispatch("change", { color, name })
|
dispatch("change", { color, name })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
await appsStore.save(app.instance._id, {
|
|
||||||
icon: { name, color },
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error updating app")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
userStore,
|
userStore,
|
||||||
deploymentStore,
|
deploymentStore,
|
||||||
} from "@/stores/builder"
|
} from "@/stores/builder"
|
||||||
import { auth, appsStore } from "@/stores/portal"
|
import { appsStore } from "@/stores/portal"
|
||||||
import {
|
import {
|
||||||
Icon,
|
Icon,
|
||||||
Tabs,
|
Tabs,
|
||||||
|
@ -22,11 +22,8 @@
|
||||||
import { isActive, url, goto, layout, redirect } from "@roxi/routify"
|
import { isActive, url, goto, layout, redirect } from "@roxi/routify"
|
||||||
import { capitalise } from "@/helpers"
|
import { capitalise } from "@/helpers"
|
||||||
import { onMount, onDestroy } from "svelte"
|
import { onMount, onDestroy } from "svelte"
|
||||||
import TourWrap from "@/components/portal/onboarding/TourWrap.svelte"
|
|
||||||
import TourPopover from "@/components/portal/onboarding/TourPopover.svelte"
|
|
||||||
import BuilderSidePanel from "./_components/BuilderSidePanel.svelte"
|
import BuilderSidePanel from "./_components/BuilderSidePanel.svelte"
|
||||||
import { UserAvatars } from "@budibase/frontend-core"
|
import { UserAvatars } from "@budibase/frontend-core"
|
||||||
import { TOUR_KEYS } from "@/components/portal/onboarding/tours.js"
|
|
||||||
import PreviewOverlay from "./_components/PreviewOverlay.svelte"
|
import PreviewOverlay from "./_components/PreviewOverlay.svelte"
|
||||||
import EnterpriseBasicTrialModal from "@/components/portal/onboarding/EnterpriseBasicTrialModal.svelte"
|
import EnterpriseBasicTrialModal from "@/components/portal/onboarding/EnterpriseBasicTrialModal.svelte"
|
||||||
import UpdateAppTopNav from "@/components/common/UpdateAppTopNav.svelte"
|
import UpdateAppTopNav from "@/components/common/UpdateAppTopNav.svelte"
|
||||||
|
@ -37,7 +34,6 @@
|
||||||
let hasSynced = false
|
let hasSynced = false
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
|
||||||
$: loaded && initTour()
|
|
||||||
$: selected = capitalise(
|
$: selected = capitalise(
|
||||||
$layout.children.find(layout => $isActive(layout.path))?.title ?? "data"
|
$layout.children.find(layout => $isActive(layout.path))?.title ?? "data"
|
||||||
)
|
)
|
||||||
|
@ -75,20 +71,6 @@
|
||||||
$goto($builderStore.previousTopNavPath[path] || path)
|
$goto($builderStore.previousTopNavPath[path] || path)
|
||||||
}
|
}
|
||||||
|
|
||||||
const initTour = async () => {
|
|
||||||
// Check if onboarding is enabled.
|
|
||||||
if (!$auth.user?.onboardedAt) {
|
|
||||||
builderStore.startBuilderOnboarding()
|
|
||||||
} else {
|
|
||||||
// Feature tour date
|
|
||||||
const release_date = new Date("2023-03-01T00:00:00.000Z")
|
|
||||||
const onboarded = new Date($auth.user?.onboardedAt)
|
|
||||||
if (onboarded < release_date) {
|
|
||||||
builderStore.setTour(TOUR_KEYS.FEATURE_ONBOARDING)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (!hasSynced && application) {
|
if (!hasSynced && application) {
|
||||||
try {
|
try {
|
||||||
|
@ -107,8 +89,6 @@
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<TourPopover />
|
|
||||||
|
|
||||||
{#if $builderStore.builderSidePanel}
|
{#if $builderStore.builderSidePanel}
|
||||||
<BuilderSidePanel />
|
<BuilderSidePanel />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -122,7 +102,6 @@
|
||||||
</a>
|
</a>
|
||||||
<Tabs {selected} size="M">
|
<Tabs {selected} size="M">
|
||||||
{#each $layout.children as { path, title }}
|
{#each $layout.children as { path, title }}
|
||||||
<TourWrap stepKeys={[`builder-${title}-section`]}>
|
|
||||||
<Tab
|
<Tab
|
||||||
link
|
link
|
||||||
href={$url(path)}
|
href={$url(path)}
|
||||||
|
@ -132,7 +111,6 @@
|
||||||
title={capitalise(title)}
|
title={capitalise(title)}
|
||||||
id={`builder-${title}-tab`}
|
id={`builder-${title}-tab`}
|
||||||
/>
|
/>
|
||||||
</TourWrap>
|
|
||||||
{/each}
|
{/each}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -17,14 +17,6 @@
|
||||||
import { ActionButton, notifications } from "@budibase/bbui"
|
import { ActionButton, notifications } from "@budibase/bbui"
|
||||||
import { capitalise } from "@/helpers"
|
import { capitalise } from "@/helpers"
|
||||||
import { builderStore } from "@/stores/builder"
|
import { builderStore } from "@/stores/builder"
|
||||||
import TourWrap from "@/components/portal/onboarding/TourWrap.svelte"
|
|
||||||
import { TOUR_STEP_KEYS } from "@/components/portal/onboarding/tours.js"
|
|
||||||
|
|
||||||
const {
|
|
||||||
BUILDER_FORM_CREATE_STEPS,
|
|
||||||
BUILDER_FORM_VIEW_UPDATE_STEPS,
|
|
||||||
BUILDER_FORM_ROW_ID,
|
|
||||||
} = TOUR_STEP_KEYS
|
|
||||||
|
|
||||||
const onUpdateName = async value => {
|
const onUpdateName = async value => {
|
||||||
try {
|
try {
|
||||||
|
@ -111,13 +103,6 @@
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
{#if section === "settings"}
|
{#if section === "settings"}
|
||||||
<TourWrap
|
|
||||||
stepKeys={[
|
|
||||||
BUILDER_FORM_CREATE_STEPS,
|
|
||||||
BUILDER_FORM_VIEW_UPDATE_STEPS,
|
|
||||||
BUILDER_FORM_ROW_ID,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<ComponentSettingsSection
|
<ComponentSettingsSection
|
||||||
{componentInstance}
|
{componentInstance}
|
||||||
{componentDefinition}
|
{componentDefinition}
|
||||||
|
@ -125,7 +110,6 @@
|
||||||
{componentBindings}
|
{componentBindings}
|
||||||
{isScreen}
|
{isScreen}
|
||||||
/>
|
/>
|
||||||
</TourWrap>
|
|
||||||
{/if}
|
{/if}
|
||||||
{#if section === "styles"}
|
{#if section === "styles"}
|
||||||
<DesignSection
|
<DesignSection
|
||||||
|
|
|
@ -9,13 +9,10 @@
|
||||||
screenStore,
|
screenStore,
|
||||||
navigationStore,
|
navigationStore,
|
||||||
permissions as permissionsStore,
|
permissions as permissionsStore,
|
||||||
builderStore,
|
|
||||||
datasources,
|
datasources,
|
||||||
appStore,
|
appStore,
|
||||||
} from "@/stores/builder"
|
} from "@/stores/builder"
|
||||||
import { auth } from "@/stores/portal"
|
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { TOUR_KEYS } from "@/components/portal/onboarding/tours.js"
|
|
||||||
import * as screenTemplating from "@/templates/screenTemplating"
|
import * as screenTemplating from "@/templates/screenTemplating"
|
||||||
import { Roles } from "@/constants/backend"
|
import { Roles } from "@/constants/backend"
|
||||||
import { AutoScreenTypes } from "@/constants"
|
import { AutoScreenTypes } from "@/constants"
|
||||||
|
@ -159,19 +156,6 @@
|
||||||
)
|
)
|
||||||
).flat()
|
).flat()
|
||||||
const newScreens = await createScreens(screenTemplates)
|
const newScreens = await createScreens(screenTemplates)
|
||||||
|
|
||||||
if (type === "update" || type === "create") {
|
|
||||||
const associatedTour =
|
|
||||||
type === "update"
|
|
||||||
? TOUR_KEYS.BUILDER_FORM_VIEW_UPDATE
|
|
||||||
: TOUR_KEYS.BUILDER_FORM_CREATE
|
|
||||||
|
|
||||||
const tourRequired = !$auth?.user?.tours?.[associatedTour]
|
|
||||||
if (tourRequired) {
|
|
||||||
builderStore.setTour(associatedTour)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadNewScreen(newScreens[0])
|
loadNewScreen(newScreens[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -143,7 +143,7 @@
|
||||||
return Constants.Roles.CREATOR
|
return Constants.Roles.CREATOR
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user?.roles[prodAppId]) {
|
if (user?.roles?.[prodAppId]) {
|
||||||
return user.roles[prodAppId]
|
return user.roles[prodAppId]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { createBuilderWebsocket } from "./websocket.js"
|
||||||
import { Socket } from "socket.io-client"
|
import { Socket } from "socket.io-client"
|
||||||
import { BuilderSocketEvent } from "@budibase/shared-core"
|
import { BuilderSocketEvent } from "@budibase/shared-core"
|
||||||
import { BudiStore } from "../BudiStore.js"
|
import { BudiStore } from "../BudiStore.js"
|
||||||
import { TOUR_KEYS } from "@/components/portal/onboarding/tours.js"
|
|
||||||
import { App } from "@budibase/types"
|
import { App } from "@budibase/types"
|
||||||
|
|
||||||
interface BuilderState {
|
interface BuilderState {
|
||||||
|
@ -14,10 +13,6 @@ interface BuilderState {
|
||||||
} | null
|
} | null
|
||||||
propertyFocus: string | null
|
propertyFocus: string | null
|
||||||
builderSidePanel: boolean
|
builderSidePanel: boolean
|
||||||
onboarding: boolean
|
|
||||||
tourNodes: Record<string, HTMLElement> | null
|
|
||||||
tourKey: string | null
|
|
||||||
tourStepKey: string | null
|
|
||||||
hoveredComponentId: string | null
|
hoveredComponentId: string | null
|
||||||
websocket?: Socket
|
websocket?: Socket
|
||||||
}
|
}
|
||||||
|
@ -27,10 +22,6 @@ export const INITIAL_BUILDER_STATE: BuilderState = {
|
||||||
highlightedSetting: null,
|
highlightedSetting: null,
|
||||||
propertyFocus: null,
|
propertyFocus: null,
|
||||||
builderSidePanel: false,
|
builderSidePanel: false,
|
||||||
onboarding: false,
|
|
||||||
tourNodes: null,
|
|
||||||
tourKey: null,
|
|
||||||
tourStepKey: null,
|
|
||||||
hoveredComponentId: null,
|
hoveredComponentId: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,9 +40,6 @@ export class BuilderStore extends BudiStore<BuilderState> {
|
||||||
this.showBuilderSidePanel = this.showBuilderSidePanel.bind(this)
|
this.showBuilderSidePanel = this.showBuilderSidePanel.bind(this)
|
||||||
this.setPreviousTopNavPath = this.setPreviousTopNavPath.bind(this)
|
this.setPreviousTopNavPath = this.setPreviousTopNavPath.bind(this)
|
||||||
this.selectResource = this.selectResource.bind(this)
|
this.selectResource = this.selectResource.bind(this)
|
||||||
this.registerTourNode = this.registerTourNode.bind(this)
|
|
||||||
this.destroyTourNode = this.destroyTourNode.bind(this)
|
|
||||||
this.startBuilderOnboarding = this.startBuilderOnboarding.bind(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init(app: App) {
|
init(app: App) {
|
||||||
|
@ -118,55 +106,6 @@ export class BuilderStore extends BudiStore<BuilderState> {
|
||||||
resourceId: id,
|
resourceId: id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
registerTourNode(tourStepKey: string, node: HTMLElement) {
|
|
||||||
this.update(state => {
|
|
||||||
const update = {
|
|
||||||
...state,
|
|
||||||
tourNodes: {
|
|
||||||
...state.tourNodes,
|
|
||||||
[tourStepKey]: node,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return update
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
destroyTourNode(tourStepKey: string) {
|
|
||||||
const store = get(this.store)
|
|
||||||
if (store.tourNodes?.[tourStepKey]) {
|
|
||||||
const nodes = { ...store.tourNodes }
|
|
||||||
delete nodes[tourStepKey]
|
|
||||||
this.update(state => ({
|
|
||||||
...state,
|
|
||||||
tourNodes: nodes,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
startBuilderOnboarding() {
|
|
||||||
this.update(state => ({
|
|
||||||
...state,
|
|
||||||
onboarding: true,
|
|
||||||
tourKey: TOUR_KEYS.TOUR_BUILDER_ONBOARDING,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
endBuilderOnboarding() {
|
|
||||||
this.update(state => ({
|
|
||||||
...state,
|
|
||||||
onboarding: false,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
setTour(tourKey?: string | null) {
|
|
||||||
this.update(state => ({
|
|
||||||
...state,
|
|
||||||
tourStepKey: null,
|
|
||||||
tourNodes: null,
|
|
||||||
tourKey: tourKey || null,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const builderStore = new BuilderStore()
|
export const builderStore = new BuilderStore()
|
||||||
|
|
|
@ -209,26 +209,4 @@ describe("Builder store", () => {
|
||||||
[dataRoute]: updatedURL,
|
[dataRoute]: updatedURL,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Register a builder tour node", ctx => {
|
|
||||||
const fakeNode = { name: "node" }
|
|
||||||
ctx.test.builderStore.registerTourNode("sampleKey", fakeNode)
|
|
||||||
|
|
||||||
const registeredNodes = ctx.test.store.tourNodes
|
|
||||||
|
|
||||||
expect(registeredNodes).not.toBeNull()
|
|
||||||
expect(Object.keys(registeredNodes).length).toBe(1)
|
|
||||||
expect(registeredNodes["sampleKey"]).toStrictEqual(fakeNode)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("Clear a destroyed tour node", ctx => {
|
|
||||||
const fakeNode = { name: "node" }
|
|
||||||
ctx.test.builderStore.registerTourNode("sampleKey", fakeNode)
|
|
||||||
|
|
||||||
expect(ctx.test.store.tourNodes).not.toBeNull()
|
|
||||||
expect(Object.keys(ctx.test.store.tourNodes).length).toBe(1)
|
|
||||||
|
|
||||||
ctx.test.builderStore.destroyTourNode("sampleKey")
|
|
||||||
expect(ctx.test.store.tourNodes).toStrictEqual({})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -135,7 +135,8 @@
|
||||||
"validate.js": "0.13.1",
|
"validate.js": "0.13.1",
|
||||||
"worker-farm": "1.7.0",
|
"worker-farm": "1.7.0",
|
||||||
"xml2js": "0.6.2",
|
"xml2js": "0.6.2",
|
||||||
"zod-validation-error": "^3.4.0"
|
"zod-validation-error": "^3.4.0",
|
||||||
|
"http-graceful-shutdown": "^3.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.22.5",
|
"@babel/core": "^7.22.5",
|
||||||
|
|
|
@ -7,8 +7,8 @@ import * as automations from "./automations"
|
||||||
import { Thread } from "./threads"
|
import { Thread } from "./threads"
|
||||||
import * as redis from "./utilities/redis"
|
import * as redis from "./utilities/redis"
|
||||||
import { events, logging, middleware, timers } from "@budibase/backend-core"
|
import { events, logging, middleware, timers } from "@budibase/backend-core"
|
||||||
import destroyable from "server-destroy"
|
|
||||||
import { userAgent } from "koa-useragent"
|
import { userAgent } from "koa-useragent"
|
||||||
|
import gracefulShutdown from "http-graceful-shutdown"
|
||||||
|
|
||||||
export default function createKoaApp() {
|
export default function createKoaApp() {
|
||||||
const app = new Koa()
|
const app = new Koa()
|
||||||
|
@ -40,55 +40,49 @@ export default function createKoaApp() {
|
||||||
app.use(userAgent)
|
app.use(userAgent)
|
||||||
|
|
||||||
const server = http.createServer(app.callback())
|
const server = http.createServer(app.callback())
|
||||||
destroyable(server)
|
|
||||||
|
|
||||||
let shuttingDown = false,
|
const shutdown = async () => {
|
||||||
errCode = 0
|
console.log("Server shutting down gracefully...")
|
||||||
|
|
||||||
server.on("close", async () => {
|
|
||||||
// already in process
|
|
||||||
if (shuttingDown) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
shuttingDown = true
|
|
||||||
console.log("Server Closed")
|
|
||||||
timers.cleanup()
|
timers.cleanup()
|
||||||
await automations.shutdown()
|
await automations.shutdown()
|
||||||
await redis.shutdown()
|
await redis.shutdown()
|
||||||
events.shutdown()
|
events.shutdown()
|
||||||
await Thread.shutdown()
|
await Thread.shutdown()
|
||||||
api.shutdown()
|
api.shutdown()
|
||||||
if (!env.isTest()) {
|
}
|
||||||
process.exit(errCode)
|
|
||||||
|
gracefulShutdown(server, {
|
||||||
|
signals: "SIGINT SIGTERM",
|
||||||
|
timeout: 30000, // in ms
|
||||||
|
onShutdown: shutdown,
|
||||||
|
forceExit: !env.isTest,
|
||||||
|
finally: () => {
|
||||||
|
console.log("Server shutdown complete")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
process.on("uncaughtException", async err => {
|
||||||
|
// @ts-ignore
|
||||||
|
// don't worry about this error, comes from zlib isn't important
|
||||||
|
if (err?.["code"] === "ERR_INVALID_CHAR") {
|
||||||
|
logging.logAlert("Uncaught exception.", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await shutdown()
|
||||||
|
if (!env.isTest) {
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
process.on("unhandledRejection", async reason => {
|
||||||
|
logging.logAlert("Unhandled Promise Rejection", reason as Error)
|
||||||
|
await shutdown()
|
||||||
|
if (!env.isTest) {
|
||||||
|
process.exit(1)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const listener = server.listen(env.PORT || 0)
|
const listener = server.listen(env.PORT || 0)
|
||||||
|
|
||||||
const shutdown = () => {
|
|
||||||
server.close()
|
|
||||||
// @ts-ignore
|
|
||||||
server.destroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
process.on("uncaughtException", err => {
|
|
||||||
// @ts-ignore
|
|
||||||
// don't worry about this error, comes from zlib isn't important
|
|
||||||
if (err && err["code"] === "ERR_INVALID_CHAR") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
errCode = -1
|
|
||||||
logging.logAlert("Uncaught exception.", err)
|
|
||||||
shutdown()
|
|
||||||
})
|
|
||||||
|
|
||||||
process.on("SIGTERM", () => {
|
|
||||||
shutdown()
|
|
||||||
})
|
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
|
||||||
shutdown()
|
|
||||||
})
|
|
||||||
|
|
||||||
return { app, server: listener }
|
return { app, server: listener }
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,14 +17,19 @@ import datasources from "../datasources"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import { ensureQueryUISet } from "../views/utils"
|
import { ensureQueryUISet } from "../views/utils"
|
||||||
import { isV2 } from "../views"
|
import { isV2 } from "../views"
|
||||||
|
import { tracer } from "dd-trace"
|
||||||
|
|
||||||
export async function processTable(table: Table): Promise<Table> {
|
export async function processTable(table: Table): Promise<Table> {
|
||||||
|
return await tracer.trace("processTable", async span => {
|
||||||
if (!table) {
|
if (!table) {
|
||||||
return table
|
return table
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span.addTags({ tableId: table._id })
|
||||||
|
|
||||||
table = { ...table }
|
table = { ...table }
|
||||||
if (table.views) {
|
if (table.views) {
|
||||||
|
span.addTags({ numViews: Object.keys(table.views).length })
|
||||||
for (const [key, view] of Object.entries(table.views)) {
|
for (const [key, view] of Object.entries(table.views)) {
|
||||||
if (!isV2(view)) {
|
if (!isV2(view)) {
|
||||||
continue
|
continue
|
||||||
|
@ -33,6 +38,7 @@ export async function processTable(table: Table): Promise<Table> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (table._id && isExternalTableID(table._id)) {
|
if (table._id && isExternalTableID(table._id)) {
|
||||||
|
span.addTags({ isExternal: true })
|
||||||
// Old created external tables via Budibase might have a missing field name breaking some UI such as filters
|
// Old created external tables via Budibase might have a missing field name breaking some UI such as filters
|
||||||
if (table.schema["id"] && !table.schema["id"].name) {
|
if (table.schema["id"] && !table.schema["id"].name) {
|
||||||
table.schema["id"].name = "id"
|
table.schema["id"].name = "id"
|
||||||
|
@ -43,6 +49,7 @@ export async function processTable(table: Table): Promise<Table> {
|
||||||
sourceType: TableSourceType.EXTERNAL,
|
sourceType: TableSourceType.EXTERNAL,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
span.addTags({ isExternal: false })
|
||||||
const processed: Table = {
|
const processed: Table = {
|
||||||
...table,
|
...table,
|
||||||
type: "table",
|
type: "table",
|
||||||
|
@ -53,50 +60,70 @@ export async function processTable(table: Table): Promise<Table> {
|
||||||
}
|
}
|
||||||
return processed
|
return processed
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processTables(tables: Table[]): Promise<Table[]> {
|
export async function processTables(tables: Table[]): Promise<Table[]> {
|
||||||
|
return await tracer.trace("processTables", async span => {
|
||||||
|
span.addTags({ numTables: tables.length })
|
||||||
return await Promise.all(tables.map(table => processTable(table)))
|
return await Promise.all(tables.map(table => processTable(table)))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processEntities(tables: Record<string, Table>) {
|
async function processEntities(tables: Record<string, Table>) {
|
||||||
|
return await tracer.trace("processEntities", async span => {
|
||||||
|
span.addTags({ numTables: Object.keys(tables).length })
|
||||||
for (let key of Object.keys(tables)) {
|
for (let key of Object.keys(tables)) {
|
||||||
tables[key] = await processTable(tables[key])
|
tables[key] = await processTable(tables[key])
|
||||||
}
|
}
|
||||||
return tables
|
return tables
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllInternalTables(db?: Database): Promise<Table[]> {
|
export async function getAllInternalTables(db?: Database): Promise<Table[]> {
|
||||||
|
return await tracer.trace("getAllInternalTables", async span => {
|
||||||
if (!db) {
|
if (!db) {
|
||||||
db = context.getAppDB()
|
db = context.getAppDB()
|
||||||
}
|
}
|
||||||
|
span.addTags({ db: db.name })
|
||||||
const internalTables = await db.allDocs<Table>(
|
const internalTables = await db.allDocs<Table>(
|
||||||
getTableParams(null, {
|
getTableParams(null, {
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
span.addTags({ numTables: internalTables.rows.length })
|
||||||
return await processTables(internalTables.rows.map(row => row.doc!))
|
return await processTables(internalTables.rows.map(row => row.doc!))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAllExternalTables(): Promise<Table[]> {
|
async function getAllExternalTables(): Promise<Table[]> {
|
||||||
|
return await tracer.trace("getAllExternalTables", async span => {
|
||||||
// this is all datasources, we'll need to filter out internal
|
// this is all datasources, we'll need to filter out internal
|
||||||
const datasources = await sdk.datasources.fetch({ enriched: true })
|
const datasources = await sdk.datasources.fetch({ enriched: true })
|
||||||
|
span.addTags({ numDatasources: datasources.length })
|
||||||
|
|
||||||
const allEntities = datasources
|
const allEntities = datasources
|
||||||
.filter(datasource => datasource._id !== INTERNAL_TABLE_SOURCE_ID)
|
.filter(datasource => datasource._id !== INTERNAL_TABLE_SOURCE_ID)
|
||||||
.map(datasource => datasource.entities)
|
.map(datasource => datasource.entities)
|
||||||
|
span.addTags({ numEntities: allEntities.length })
|
||||||
|
|
||||||
let final: Table[] = []
|
let final: Table[] = []
|
||||||
for (let entities of allEntities) {
|
for (let entities of allEntities) {
|
||||||
if (entities) {
|
if (entities) {
|
||||||
final = final.concat(Object.values(entities))
|
final = final.concat(Object.values(entities))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
span.addTags({ numTables: final.length })
|
||||||
return await processTables(final)
|
return await processTables(final)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getExternalTable(
|
export async function getExternalTable(
|
||||||
datasourceId: string,
|
datasourceId: string,
|
||||||
tableName: string
|
tableName: string
|
||||||
): Promise<Table> {
|
): Promise<Table> {
|
||||||
|
return await tracer.trace("getExternalTable", async span => {
|
||||||
|
span.addTags({ datasourceId, tableName })
|
||||||
const entities = await getExternalTablesInDatasource(datasourceId)
|
const entities = await getExternalTablesInDatasource(datasourceId)
|
||||||
if (!entities[tableName]) {
|
if (!entities[tableName]) {
|
||||||
throw new Error(`Unable to find table named "${tableName}"`)
|
throw new Error(`Unable to find table named "${tableName}"`)
|
||||||
|
@ -106,50 +133,75 @@ export async function getExternalTable(
|
||||||
table.sourceId = datasourceId
|
table.sourceId = datasourceId
|
||||||
}
|
}
|
||||||
return table
|
return table
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTable(tableId: string): Promise<Table> {
|
export async function getTable(tableId: string): Promise<Table> {
|
||||||
|
return await tracer.trace("getTable", async span => {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
|
span.addTags({ tableId, db: db.name })
|
||||||
let output: Table
|
let output: Table
|
||||||
if (tableId && isExternalTableID(tableId)) {
|
if (tableId && isExternalTableID(tableId)) {
|
||||||
let { datasourceId, tableName } = breakExternalTableId(tableId)
|
let { datasourceId, tableName } = breakExternalTableId(tableId)
|
||||||
|
span.addTags({ isExternal: true, datasourceId, tableName })
|
||||||
const datasource = await datasources.get(datasourceId)
|
const datasource = await datasources.get(datasourceId)
|
||||||
const table = await getExternalTable(datasourceId, tableName)
|
const table = await getExternalTable(datasourceId, tableName)
|
||||||
output = { ...table, sql: isSQL(datasource) }
|
output = { ...table, sql: isSQL(datasource) }
|
||||||
|
span.addTags({ isSQL: isSQL(datasource) })
|
||||||
} else {
|
} else {
|
||||||
output = await db.get<Table>(tableId)
|
output = await db.get<Table>(tableId)
|
||||||
}
|
}
|
||||||
return await processTable(output)
|
return await processTable(output)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function doesTableExist(tableId: string): Promise<boolean> {
|
export async function doesTableExist(tableId: string): Promise<boolean> {
|
||||||
|
return await tracer.trace("doesTableExist", async span => {
|
||||||
|
span.addTags({ tableId })
|
||||||
try {
|
try {
|
||||||
const table = await getTable(tableId)
|
const table = await getTable(tableId)
|
||||||
|
span.addTags({ tableExists: !!table })
|
||||||
return !!table
|
return !!table
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
span.addTags({ tableExists: false })
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllTables() {
|
export async function getAllTables() {
|
||||||
|
return await tracer.trace("getAllTables", async span => {
|
||||||
const [internal, external] = await Promise.all([
|
const [internal, external] = await Promise.all([
|
||||||
getAllInternalTables(),
|
getAllInternalTables(),
|
||||||
getAllExternalTables(),
|
getAllExternalTables(),
|
||||||
])
|
])
|
||||||
|
span.addTags({
|
||||||
|
numInternalTables: internal.length,
|
||||||
|
numExternalTables: external.length,
|
||||||
|
})
|
||||||
return await processTables([...internal, ...external])
|
return await processTables([...internal, ...external])
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getExternalTablesInDatasource(
|
export async function getExternalTablesInDatasource(
|
||||||
datasourceId: string
|
datasourceId: string
|
||||||
): Promise<Record<string, Table>> {
|
): Promise<Record<string, Table>> {
|
||||||
|
return await tracer.trace("getExternalTablesInDatasource", async span => {
|
||||||
const datasource = await datasources.get(datasourceId, { enriched: true })
|
const datasource = await datasources.get(datasourceId, { enriched: true })
|
||||||
if (!datasource || !datasource.entities) {
|
if (!datasource || !datasource.entities) {
|
||||||
throw new Error("Datasource is not configured fully.")
|
throw new Error("Datasource is not configured fully.")
|
||||||
}
|
}
|
||||||
|
span.addTags({
|
||||||
|
datasourceId,
|
||||||
|
numEntities: Object.keys(datasource.entities).length,
|
||||||
|
})
|
||||||
return await processEntities(datasource.entities)
|
return await processEntities(datasource.entities)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTables(tableIds: string[]): Promise<Table[]> {
|
export async function getTables(tableIds: string[]): Promise<Table[]> {
|
||||||
|
return tracer.trace("getTables", async span => {
|
||||||
|
span.addTags({ numTableIds: tableIds.length })
|
||||||
const externalTableIds = tableIds.filter(tableId =>
|
const externalTableIds = tableIds.filter(tableId =>
|
||||||
isExternalTableID(tableId)
|
isExternalTableID(tableId)
|
||||||
),
|
),
|
||||||
|
@ -170,12 +222,16 @@ export async function getTables(tableIds: string[]): Promise<Table[]> {
|
||||||
})
|
})
|
||||||
tables = tables.concat(internalTables)
|
tables = tables.concat(internalTables)
|
||||||
}
|
}
|
||||||
|
span.addTags({ numTables: tables.length })
|
||||||
return await processTables(tables)
|
return await processTables(tables)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function enrichViewSchemas(
|
export async function enrichViewSchemas(
|
||||||
table: Table
|
table: Table
|
||||||
): Promise<FindTableResponse> {
|
): Promise<FindTableResponse> {
|
||||||
|
return await tracer.trace("enrichViewSchemas", async span => {
|
||||||
|
span.addTags({ tableId: table._id })
|
||||||
const views = []
|
const views = []
|
||||||
for (const view of Object.values(table.views ?? [])) {
|
for (const view of Object.values(table.views ?? [])) {
|
||||||
if (sdk.views.isV2(view)) {
|
if (sdk.views.isV2(view)) {
|
||||||
|
@ -190,4 +246,5 @@ export async function enrichViewSchemas(
|
||||||
return p
|
return p
|
||||||
}, {} as TableViewsResponse),
|
}, {} as TableViewsResponse),
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,10 +38,8 @@ export interface UpdateSelfRequest {
|
||||||
lastName?: string
|
lastName?: string
|
||||||
password?: string
|
password?: string
|
||||||
forceResetPassword?: boolean
|
forceResetPassword?: boolean
|
||||||
onboardedAt?: string
|
|
||||||
freeTrialConfirmedAt?: string
|
freeTrialConfirmedAt?: string
|
||||||
appFavourites?: string[]
|
appFavourites?: string[]
|
||||||
tours?: Record<string, Date>
|
|
||||||
appSort?: string
|
appSort?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -64,15 +64,15 @@ export interface User extends Document {
|
||||||
status?: UserStatus
|
status?: UserStatus
|
||||||
createdAt?: number // override the default createdAt behaviour - users sdk historically set this to Date.now()
|
createdAt?: number // override the default createdAt behaviour - users sdk historically set this to Date.now()
|
||||||
userGroups?: string[]
|
userGroups?: string[]
|
||||||
onboardedAt?: string
|
|
||||||
freeTrialConfirmedAt?: string
|
freeTrialConfirmedAt?: string
|
||||||
tours?: Record<string, Date>
|
|
||||||
scimInfo?: { isSync: true } & Record<string, any>
|
scimInfo?: { isSync: true } & Record<string, any>
|
||||||
appFavourites?: string[]
|
appFavourites?: string[]
|
||||||
ssoId?: string
|
ssoId?: string
|
||||||
appSort?: string
|
appSort?: string
|
||||||
budibaseAccess?: boolean
|
budibaseAccess?: boolean
|
||||||
accountPortalAccess?: boolean
|
accountPortalAccess?: boolean
|
||||||
|
onboardedAt?: string // deprecated and no longer saved
|
||||||
|
tours?: Record<string, Date> // deprecated and no longer saved
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserBindings extends Document {
|
export interface UserBindings extends Document {
|
||||||
|
|
|
@ -24,14 +24,6 @@ export interface UserDeletedEvent extends BaseEvent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserOnboardingEvent extends BaseEvent {
|
|
||||||
userId: string
|
|
||||||
step?: string
|
|
||||||
audited: {
|
|
||||||
email: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UserPermissionAssignedEvent extends BaseEvent {
|
export interface UserPermissionAssignedEvent extends BaseEvent {
|
||||||
userId: string
|
userId: string
|
||||||
audited: {
|
audited: {
|
||||||
|
|
|
@ -75,7 +75,8 @@
|
||||||
"pouchdb": "7.3.0",
|
"pouchdb": "7.3.0",
|
||||||
"pouchdb-all-dbs": "1.1.1",
|
"pouchdb-all-dbs": "1.1.1",
|
||||||
"server-destroy": "1.0.1",
|
"server-destroy": "1.0.1",
|
||||||
"uuid": "^8.3.2"
|
"uuid": "^8.3.2",
|
||||||
|
"http-graceful-shutdown": "^3.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@jest/types": "^29.6.3",
|
"@jest/types": "^29.6.3",
|
||||||
|
|
|
@ -47,13 +47,12 @@ describe("/api/global/self", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should update onboarding", async () => {
|
it("should update free trial confirmation date", async () => {
|
||||||
const user = await config.createUser()
|
const user = await config.createUser()
|
||||||
await config.createSession(user)
|
await config.createSession(user)
|
||||||
|
|
||||||
const res = await config.api.self
|
const res = await config.api.self
|
||||||
.updateSelf(user, {
|
.updateSelf(user, {
|
||||||
onboardedAt: "2023-03-07T14:10:54.869Z",
|
|
||||||
freeTrialConfirmedAt: "2024-03-17T14:10:54.869Z",
|
freeTrialConfirmedAt: "2024-03-17T14:10:54.869Z",
|
||||||
})
|
})
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
@ -61,7 +60,6 @@ describe("/api/global/self", () => {
|
||||||
const dbUser = (await config.getUser(user.email))!
|
const dbUser = (await config.getUser(user.email))!
|
||||||
|
|
||||||
user._rev = dbUser._rev
|
user._rev = dbUser._rev
|
||||||
expect(dbUser.onboardedAt).toBe("2023-03-07T14:10:54.869Z")
|
|
||||||
expect(dbUser.freeTrialConfirmedAt).toBe("2024-03-17T14:10:54.869Z")
|
expect(dbUser.freeTrialConfirmedAt).toBe("2024-03-17T14:10:54.869Z")
|
||||||
expect(res.body._id).toBe(user._id)
|
expect(res.body._id).toBe(user._id)
|
||||||
})
|
})
|
||||||
|
|
|
@ -25,10 +25,8 @@ export const buildSelfSaveValidation = () => {
|
||||||
forceResetPassword: Joi.boolean().optional(),
|
forceResetPassword: Joi.boolean().optional(),
|
||||||
firstName: OPTIONAL_STRING,
|
firstName: OPTIONAL_STRING,
|
||||||
lastName: OPTIONAL_STRING,
|
lastName: OPTIONAL_STRING,
|
||||||
onboardedAt: Joi.string().optional(),
|
|
||||||
freeTrialConfirmedAt: Joi.string().optional(),
|
freeTrialConfirmedAt: Joi.string().optional(),
|
||||||
appFavourites: Joi.array().optional(),
|
appFavourites: Joi.array().optional(),
|
||||||
tours: Joi.object().optional(),
|
|
||||||
appSort: Joi.string().optional(),
|
appSort: Joi.string().optional(),
|
||||||
}
|
}
|
||||||
return auth.joiValidator.body(Joi.object(schema).required().unknown(false))
|
return auth.joiValidator.body(Joi.object(schema).required().unknown(false))
|
||||||
|
|
|
@ -26,12 +26,12 @@ db.init()
|
||||||
import koaBody from "koa-body"
|
import koaBody from "koa-body"
|
||||||
import http from "http"
|
import http from "http"
|
||||||
import api from "./api"
|
import api from "./api"
|
||||||
|
import gracefulShutdown from "http-graceful-shutdown"
|
||||||
|
|
||||||
const koaSession = require("koa-session")
|
const koaSession = require("koa-session")
|
||||||
|
|
||||||
import { userAgent } from "koa-useragent"
|
import { userAgent } from "koa-useragent"
|
||||||
|
|
||||||
import destroyable from "server-destroy"
|
|
||||||
import { initPro } from "./initPro"
|
import { initPro } from "./initPro"
|
||||||
import { handleScimBody } from "./middleware/handleScimBody"
|
import { handleScimBody } from "./middleware/handleScimBody"
|
||||||
|
|
||||||
|
@ -86,29 +86,40 @@ app.use(auth.passport.session())
|
||||||
app.use(api.routes())
|
app.use(api.routes())
|
||||||
|
|
||||||
const server = http.createServer(app.callback())
|
const server = http.createServer(app.callback())
|
||||||
destroyable(server)
|
|
||||||
|
|
||||||
let shuttingDown = false,
|
const shutdown = async () => {
|
||||||
errCode = 0
|
console.log("Worker service shutting down gracefully...")
|
||||||
server.on("close", async () => {
|
|
||||||
if (shuttingDown) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
shuttingDown = true
|
|
||||||
console.log("Server Closed")
|
|
||||||
timers.cleanup()
|
timers.cleanup()
|
||||||
events.shutdown()
|
events.shutdown()
|
||||||
await redis.clients.shutdown()
|
await redis.clients.shutdown()
|
||||||
await queue.shutdown()
|
await queue.shutdown()
|
||||||
if (!env.isTest()) {
|
}
|
||||||
process.exit(errCode)
|
|
||||||
|
gracefulShutdown(server, {
|
||||||
|
signals: "SIGINT SIGTERM",
|
||||||
|
timeout: 30000,
|
||||||
|
onShutdown: shutdown,
|
||||||
|
forceExit: !env.isTest,
|
||||||
|
finally: () => {
|
||||||
|
console.log("Worker service shutdown complete")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
process.on("uncaughtException", async err => {
|
||||||
|
logging.logAlert("Uncaught exception.", err)
|
||||||
|
await shutdown()
|
||||||
|
if (!env.isTest) {
|
||||||
|
process.exit(1)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const shutdown = () => {
|
process.on("unhandledRejection", async reason => {
|
||||||
server.close()
|
logging.logAlert("Unhandled Promise Rejection", reason as Error)
|
||||||
server.destroy()
|
await shutdown()
|
||||||
}
|
if (!env.isTest) {
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export default server.listen(parseInt(env.PORT || "4002"), async () => {
|
export default server.listen(parseInt(env.PORT || "4002"), async () => {
|
||||||
let startupLog = `Worker running on ${JSON.stringify(server.address())}`
|
let startupLog = `Worker running on ${JSON.stringify(server.address())}`
|
||||||
|
@ -125,17 +136,3 @@ export default server.listen(parseInt(env.PORT || "4002"), async () => {
|
||||||
// can't integrate directly into backend-core due to cyclic issues
|
// can't integrate directly into backend-core due to cyclic issues
|
||||||
await events.processors.init(proSdk.auditLogs.write)
|
await events.processors.init(proSdk.auditLogs.write)
|
||||||
})
|
})
|
||||||
|
|
||||||
process.on("uncaughtException", err => {
|
|
||||||
errCode = -1
|
|
||||||
logging.logAlert("Uncaught exception.", err)
|
|
||||||
shutdown()
|
|
||||||
})
|
|
||||||
|
|
||||||
process.on("SIGTERM", () => {
|
|
||||||
shutdown()
|
|
||||||
})
|
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
|
||||||
shutdown()
|
|
||||||
})
|
|
||||||
|
|
|
@ -13161,6 +13161,13 @@ http-errors@~1.6.2:
|
||||||
setprototypeof "1.1.0"
|
setprototypeof "1.1.0"
|
||||||
statuses ">= 1.4.0 < 2"
|
statuses ">= 1.4.0 < 2"
|
||||||
|
|
||||||
|
http-graceful-shutdown@^3.1.12:
|
||||||
|
version "3.1.14"
|
||||||
|
resolved "https://registry.yarnpkg.com/http-graceful-shutdown/-/http-graceful-shutdown-3.1.14.tgz#a4d48ac5d985da18b4d35c050acd3ef10f02113d"
|
||||||
|
integrity sha512-aTbGAZDUtRt7gRmU+li7rt5WbJeemULZHLNrycJ1dRBU80Giut6NvzG8h5u1TW1zGHXkPGpEtoEKhPKogIRKdA==
|
||||||
|
dependencies:
|
||||||
|
debug "^4.3.4"
|
||||||
|
|
||||||
http-proxy-agent@^4.0.1:
|
http-proxy-agent@^4.0.1:
|
||||||
version "4.0.1"
|
version "4.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a"
|
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a"
|
||||||
|
|
Loading…
Reference in New Issue