1
0
Fork 0
mirror of synced 2024-07-04 22:11:23 +12:00

Merge branch 'develop' into feature/dependencies-image

This commit is contained in:
adrinr 2023-02-01 10:56:22 +00:00
commit 34278349c3
97 changed files with 5528 additions and 2166 deletions

View file

@ -139,6 +139,8 @@ spec:
value: {{ .Values.globals.automationMaxIterations | quote }}
- name: TENANT_FEATURE_FLAGS
value: {{ .Values.globals.tenantFeatureFlags | quote }}
- name: ENCRYPTION_KEY
value: {{ .Values.globals.bbEncryptionKey | quote }}
{{ if .Values.globals.bbAdminUserEmail }}
- name: BB_ADMIN_USER_EMAIL
value: {{ .Values.globals.bbAdminUserEmail | quote }}

View file

@ -146,6 +146,8 @@ spec:
value: {{ .Values.globals.google.secret | quote }}
- name: TENANT_FEATURE_FLAGS
value: {{ .Values.globals.tenantFeatureFlags | quote }}
- name: ENCRYPTION_KEY
value: {{ .Values.globals.bbEncryptionKey | quote }}
{{ if .Values.globals.elasticApmEnabled }}
- name: ELASTIC_APM_ENABLED
value: {{ .Values.globals.elasticApmEnabled | quote }}

View file

@ -1,5 +1,5 @@
{
"version": "2.2.12-alpha.45",
"version": "2.2.12-alpha.54",
"npmClient": "yarn",
"packages": [
"packages/*"

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/backend-core",
"version": "2.2.12-alpha.45",
"version": "2.2.12-alpha.54",
"description": "Budibase backend core libraries used in server and worker",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
@ -23,7 +23,7 @@
},
"dependencies": {
"@budibase/nano": "10.1.1",
"@budibase/types": "2.2.12-alpha.45",
"@budibase/types": "2.2.12-alpha.54",
"@shopify/jest-koa-mocks": "5.0.1",
"@techpass/passport-openidconnect": "0.3.2",
"aws-cloudfront-sign": "2.2.0",

View file

@ -13,6 +13,7 @@ import {
UserPermissionAssignedEvent,
UserPermissionRemovedEvent,
UserUpdatedEvent,
UserOnboardingEvent,
} from "@budibase/types"
async function created(user: User, timestamp?: number) {
@ -36,6 +37,13 @@ async function deleted(user: User) {
await publishEvent(Event.USER_DELETED, properties)
}
export async function onboardingComplete(user: User) {
const properties: UserOnboardingEvent = {
userId: user._id as string,
}
await publishEvent(Event.USER_ONBOARDING_COMPLETE, properties)
}
// PERMISSIONS
async function permissionAdminAssigned(user: User, timestamp?: number) {
@ -126,6 +134,7 @@ export default {
permissionAdminRemoved,
permissionBuilderAssigned,
permissionBuilderRemoved,
onboardingComplete,
invited,
inviteAccepted,
passwordForceReset,

View file

@ -1,7 +1,7 @@
{
"name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.",
"version": "2.2.12-alpha.45",
"version": "2.2.12-alpha.54",
"license": "MPL-2.0",
"svelte": "src/index.js",
"module": "dist/bbui.es.js",
@ -38,7 +38,7 @@
],
"dependencies": {
"@adobe/spectrum-css-workflow-icons": "1.2.1",
"@budibase/string-templates": "2.2.12-alpha.45",
"@budibase/string-templates": "2.2.12-alpha.54",
"@spectrum-css/accordion": "3.0.24",
"@spectrum-css/actionbutton": "1.0.1",
"@spectrum-css/actiongroup": "1.0.1",

View file

@ -86,7 +86,7 @@
margin-left: 0;
transition: color ease-out 130ms;
}
.is-selected:not(.spectrum-ActionButton--emphasized) {
.is-selected:not(.spectrum-ActionButton--emphasized):not(.spectrum-ActionButton--quiet) {
background: var(--spectrum-global-color-gray-300);
border-color: var(--spectrum-global-color-gray-700);
}

View file

@ -3,6 +3,9 @@ export default function positionDropdown(
{ anchor, align, maxWidth, useAnchorWidth }
) {
const update = () => {
if (!anchor) {
return
}
const anchorBounds = anchor.getBoundingClientRect()
const elementBounds = element.getBoundingClientRect()
let styles = {
@ -14,7 +17,9 @@ export default function positionDropdown(
}
// Determine vertical styles
if (window.innerHeight - anchorBounds.bottom < 100) {
if (align === "right-outside") {
styles.top = anchorBounds.top
} else if (window.innerHeight - anchorBounds.bottom < 100) {
styles.top = anchorBounds.top - elementBounds.height - 5
} else {
styles.top = anchorBounds.bottom + 5
@ -30,8 +35,8 @@ export default function positionDropdown(
}
if (align === "right") {
styles.left = anchorBounds.left + anchorBounds.width - elementBounds.width
} else if (align === "right-side") {
styles.left = anchorBounds.left + anchorBounds.width
} else if (align === "right-outside") {
styles.left = anchorBounds.right + 10
} else {
styles.left = anchorBounds.left
}
@ -54,8 +59,11 @@ export default function positionDropdown(
const resizeObserver = new ResizeObserver(entries => {
entries.forEach(update)
})
resizeObserver.observe(anchor)
if (anchor) {
resizeObserver.observe(anchor)
}
resizeObserver.observe(element)
resizeObserver.observe(document.body)
document.addEventListener("scroll", update, true)

View file

@ -15,11 +15,13 @@
export let tooltip = undefined
export let dataCy
export let newStyles = true
export let id
let showTooltip = false
</script>
<button
{id}
class:spectrum-Button--cta={cta}
class:spectrum-Button--primary={primary}
class:spectrum-Button--secondary={secondary}

View file

@ -134,7 +134,7 @@
on:blur={onBlur}
on:focus={onFocus}
on:input={onInput}
{type}
type={hbsValue.length ? "text" : type}
style={align ? `text-align: ${align};` : ""}
class="spectrum-Textfield-input"
inputmode={type === "number" ? "decimal" : "text"}
@ -247,10 +247,6 @@
width: 100%;
}
.no-variables-height {
height: 100px;
}
.no-variables-text {
padding: var(--spacing-m);
color: var(--spectrum-global-color-gray-600);

View file

@ -15,17 +15,10 @@
export let portalTarget
export let dataCy
export let maxWidth
export let direction = "bottom"
export let showTip = false
export let open = false
export let useAnchorWidth = false
export let dismissible = true
let tipSvg =
'<svg xmlns="http://www.w3.org/svg/2000" width="23" height="12" class="spectrum-Popover-tip" > <path class="spectrum-Popover-tip-triangle" d="M 0.7071067811865476 0 L 11.414213562373096 10.707106781186548 L 22.121320343559645 0" /> </svg>'
$: tooltipClasses = showTip
? `spectrum-Popover--withTip spectrum-Popover--${direction}`
: ""
$: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum"
export const show = () => {
@ -64,41 +57,37 @@
</script>
{#if open}
<Portal {target}>
<div
tabindex="0"
use:positionDropdown={{ anchor, align, maxWidth, useAnchorWidth }}
use:clickOutside={{
callback: handleOutsideClick,
anchor,
}}
on:keydown={handleEscape}
class={"spectrum-Popover is-open " + (tooltipClasses || "")}
role="presentation"
data-cy={dataCy}
transition:fly|local={{ y: -20, duration: 200 }}
>
{#if showTip}
{@html tipSvg}
{/if}
<slot />
</div>
</Portal>
{#key anchor}
<Portal {target}>
<div
tabindex="0"
use:positionDropdown={{
anchor,
align,
maxWidth,
useAnchorWidth,
showTip: false,
}}
use:clickOutside={{
callback: dismissible ? handleOutsideClick : () => {},
anchor,
}}
on:keydown={handleEscape}
class="spectrum-Popover is-open"
role="presentation"
data-cy={dataCy}
transition:fly|local={{ y: -20, duration: 200 }}
>
<slot />
</div>
</Portal>
{/key}
{/if}
<style>
.spectrum-Popover {
min-width: var(--spectrum-global-dimension-size-2000);
border-color: var(--spectrum-global-color-gray-300);
}
.spectrum-Popover.is-open.spectrum-Popover--withTip {
margin-top: var(--spacing-xs);
margin-left: var(--spacing-xl);
}
:global(.spectrum-Popover--bottom .spectrum-Popover-tip),
:global(.spectrum-Popover--top .spectrum-Popover-tip) {
left: 90%;
margin-left: calc(var(--spectrum-global-dimension-size-150) * -1);
overflow: auto;
}
</style>

View file

@ -3,6 +3,7 @@
import Portal from "svelte-portal"
export let title
export let icon = ""
export let id
const dispatch = createEventDispatcher()
let selected = getContext("tab")
@ -31,10 +32,7 @@
$: {
if ($selected.title === title && tab_internal) {
if ($selected.info?.left !== tab_internal.getBoundingClientRect().left) {
$selected = {
...$selected,
info: tab_internal.getBoundingClientRect(),
}
setTabInfo()
}
}
}
@ -50,6 +48,7 @@
</script>
<div
{id}
bind:this={tab_internal}
on:click={onClick}
class:is-selected={$selected.title === title}

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/builder",
"version": "2.2.12-alpha.45",
"version": "2.2.12-alpha.54",
"license": "GPL-3.0",
"private": true,
"scripts": {
@ -71,10 +71,13 @@
}
},
"dependencies": {
"@budibase/bbui": "2.2.12-alpha.45",
"@budibase/client": "2.2.12-alpha.45",
"@budibase/frontend-core": "2.2.12-alpha.45",
"@budibase/string-templates": "2.2.12-alpha.45",
"@budibase/bbui": "2.2.12-alpha.54",
"@budibase/client": "2.2.12-alpha.54",
"@budibase/frontend-core": "2.2.12-alpha.54",
"@budibase/string-templates": "2.2.12-alpha.54",
"@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/free-brands-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1",
"@sentry/browser": "5.19.1",
"@spectrum-css/accordion": "^3.0.24",
"@spectrum-css/page": "^3.0.1",

View file

@ -3,7 +3,6 @@
import { routes } from "../.routify/routes"
import { NotificationDisplay, BannerDisplay } from "@budibase/bbui"
import { parse, stringify } from "qs"
import HelpIcon from "components/common/HelpIcon.svelte"
import LicensingOverlays from "components/portal/licensing/LicensingOverlays.svelte"
const queryHandler = { parse, stringify }
@ -15,7 +14,6 @@
<LicensingOverlays />
<Router {routes} config={{ queryHandler }} />
<div class="modal-container" />
<HelpIcon />
<style>
.modal-container {

View file

@ -63,6 +63,10 @@ const INITIAL_FRONTEND_STATE = {
selectedScreenId: null,
selectedComponentId: null,
selectedLayoutId: null,
// onboarding
onboarding: false,
tourNodes: null,
}
export const getFrontendStore = () => {

View file

@ -39,6 +39,23 @@
$: showError($fetch.error)
$: id, (filters = null)
let appliedFilter
let rawFilter
let appliedSort
let selectedRows = []
$: enrichedSchema,
() => {
appliedFilter = null
rawFilter = null
appliedSort = null
selectedRows = []
}
$: if (Number.isInteger($fetch.pageNumber)) {
selectedRows = []
}
const showError = error => {
if (error) {
notifications.error(error?.message || "Unable to fetch data.")
@ -95,11 +112,15 @@
}
// Fetch data whenever sorting option changes
const onSort = e => {
fetch.update({
const onSort = async e => {
const sort = {
sortColumn: e.detail.column,
sortOrder: e.detail.order,
})
}
await fetch.update(sort)
appliedSort = { ...sort }
appliedSort.sortOrder = appliedSort.sortOrder.toLowerCase()
selectedRows = []
}
// Fetch data whenever filters change
@ -108,16 +129,19 @@
fetch.update({
filter: filters,
})
appliedFilter = e.detail
}
// Fetch data whenever schema changes
const onUpdateColumns = () => {
selectedRows = []
fetch.refresh()
}
// Fetch data whenever rows are modified. Unfortunately we have to lose
// our pagination place, as our bookmarks will have shifted.
const onUpdateRows = () => {
selectedRows = []
fetch.refresh()
}
@ -142,6 +166,9 @@
disableSorting
on:updatecolumns={onUpdateColumns}
on:updaterows={onUpdateRows}
on:selectionUpdated={e => {
selectedRows = e.detail
}}
customPlaceholder
>
<div class="buttons">
@ -183,6 +210,9 @@
<ExportButton
disabled={!hasRows || !hasCols}
view={$tables.selected?._id}
filters={appliedFilter}
sorting={appliedSort}
{selectedRows}
/>
{#key id}
<TableFilterButton

View file

@ -16,6 +16,7 @@
UNSORTABLE_TYPES,
} from "constants"
import RoleCell from "./cells/RoleCell.svelte"
import { createEventDispatcher } from "svelte"
export let schema = {}
export let data = []
@ -28,6 +29,8 @@
export let disableSorting = false
export let customPlaceholder = false
const dispatch = createEventDispatcher()
let selectedRows = []
let editableColumn
let editableRow
@ -36,6 +39,7 @@
let customRenderers = []
let confirmDelete
$: selectedRows, dispatch("selectionUpdated", selectedRows)
$: isUsersTable = tableId === TableNames.USERS
$: data && resetSelectedRows()
$: editRowComponent = isUsersTable ? CreateEditUser : CreateEditRow

View file

@ -3,7 +3,10 @@
import ExportModal from "../modals/ExportModal.svelte"
export let view
export let filters
export let sorting
export let disabled = false
export let selectedRows
let modal
</script>
@ -18,5 +21,5 @@
Export
</ActionButton>
<Modal bind:this={modal}>
<ExportModal {view} />
<ExportModal {view} {filters} {sorting} {selectedRows} />
</Modal>

View file

@ -1,7 +1,14 @@
<script>
import { Select, ModalContent, notifications } from "@budibase/bbui"
import {
Select,
ModalContent,
notifications,
Body,
Table,
} from "@budibase/bbui"
import download from "downloadjs"
import { API } from "api"
import { Constants, LuceneUtils } from "@budibase/frontend-core"
const FORMATS = [
{
@ -19,8 +26,71 @@
]
export let view
export let filters
export let sorting
export let selectedRows = []
let exportFormat = FORMATS[0].key
let filterLookup
$: luceneFilter = LuceneUtils.buildLuceneQuery(filters)
$: exportOpDisplay = buildExportOpDisplay(sorting, filterDisplay, filters)
const buildFilterLookup = () => {
return Object.keys(Constants.OperatorOptions).reduce((acc, key) => {
const op = Constants.OperatorOptions[key]
acc[op.value] = op.label
return acc
}, {})
}
filterLookup = buildFilterLookup()
const filterDisplay = () => {
if (!filters) {
return []
}
return filters.map(filter => {
let newFieldName = filter.field + ""
const parts = newFieldName.split(":")
parts.shift()
newFieldName = parts.join(":")
return {
Field: newFieldName,
Operation: filterLookup[filter.operator],
"Field Value": filter.value || "",
}
})
}
const buildExportOpDisplay = (sorting, filterDisplay) => {
let filterDisplayConfig = filterDisplay()
if (sorting) {
filterDisplayConfig = [
...filterDisplayConfig,
{
Field: sorting.sortColumn,
Operation: "Order By",
"Field Value": sorting.sortOrder,
},
]
}
return filterDisplayConfig
}
const displaySchema = {
Field: {
type: "string",
fieldName: "Field",
},
Operation: {
type: "string",
fieldName: "Operation",
},
"Field Value": {
type: "string",
fieldName: "Value",
},
}
async function exportView() {
try {
@ -33,9 +103,74 @@
notifications.error(`Unable to export ${exportFormat.toUpperCase()} data`)
}
}
async function exportRows() {
if (selectedRows?.length) {
const data = await API.exportRows({
tableId: view,
rows: selectedRows.map(row => row._id),
format: exportFormat,
})
download(data, `export.${exportFormat}`)
} else if (filters || sorting) {
const data = await API.exportRows({
tableId: view,
format: exportFormat,
search: {
query: luceneFilter,
sort: sorting?.sortColumn,
sortOrder: sorting?.sortOrder,
paginate: false,
},
})
download(data, `export.${exportFormat}`)
} else {
await exportView()
}
}
</script>
<ModalContent title="Export Data" confirmText="Export" onConfirm={exportView}>
<ModalContent
title="Export Data"
confirmText="Export"
onConfirm={exportRows}
size={filters?.length || sorting ? "M" : "S"}
>
{#if selectedRows?.length}
<Body size="S">
<strong>{selectedRows?.length}</strong>
{`row${selectedRows?.length > 1 ? "s" : ""} will be exported`}
</Body>
{:else if filters || (sorting?.sortOrder && sorting?.sortColumn)}
<Body size="S">
{#if !filters}
Exporting <strong>all</strong> rows
{:else}
Filters applied
{/if}
</Body>
<div class="table-wrap">
<Table
schema={displaySchema}
data={exportOpDisplay}
{filters}
loading={false}
rowCount={filters?.length + 1}
disableSorting={true}
allowSelectRows={false}
allowEditRows={false}
allowEditColumns={false}
quiet={true}
compact={true}
/>
</div>
{:else}
<Body size="S">
Exporting <strong>all</strong> rows
</Body>
{/if}
<Select
label="Format"
bind:value={exportFormat}
@ -45,3 +180,9 @@
getOptionValue={x => x.key}
/>
</ModalContent>
<style>
.table-wrap :global(.wrapper) {
max-width: 400px;
}
</style>

View file

@ -248,6 +248,7 @@
/>
<EnvDropdown
label="Password"
type="password"
bind:value={form.basic.password}
on:change={onFieldChange}
on:blur={() => (blurred.basic.password = true)}

View file

@ -182,3 +182,9 @@
</g>
</g>
</svg>
<style>
svg {
padding-top: 3px;
}
</style>

View file

@ -11,6 +11,6 @@
<style>
img {
padding-top: 1px;
padding-top: 5px;
}
</style>

View file

@ -29,6 +29,18 @@
: BUDIBASE_INTERNAL_DB_ID
export let name
export let beforeSave = async () => {}
export let afterSave = async table => {
notifications.success(`Table ${name} created successfully.`)
// Navigate to new table
const currentUrl = $url()
const path = currentUrl.endsWith("data")
? `./table/${table._id}`
: `../../table/${table._id}`
$goto(path)
}
let error = ""
let autoColumns = getAutoColumnInformation()
let schema = {}
@ -78,15 +90,9 @@
// Create table
let table
try {
await beforeSave()
table = await tables.save(newTable)
notifications.success(`Table ${name} created successfully.`)
// Navigate to new table
const currentUrl = $url()
const path = currentUrl.endsWith("data")
? `./table/${table._id}`
: `../../table/${table._id}`
$goto(path)
await afterSave(table)
} catch (e) {
notifications.error(e)
// reload in case the table was created

View file

@ -0,0 +1,41 @@
<script context="module">
import { dom, library } from "@fortawesome/fontawesome-svg-core"
import {
faEnvelope,
faXmark,
faBook,
faPlay,
faLock,
faFileArrowUp,
faChevronLeft,
} from "@fortawesome/free-solid-svg-icons"
import { faGithub, faDiscord } from "@fortawesome/free-brands-svg-icons"
library.add(
faXmark,
faBook,
faPlay,
faLock,
faGithub,
faDiscord,
faEnvelope,
faFileArrowUp,
faChevronLeft
)
dom.watch()
</script>
<script>
export let name
</script>
<span>
<i class={name} />
</span>
<style>
span {
display: contents;
color: var(--spectrum-global-color-gray-900);
}
</style>

View file

@ -1,42 +0,0 @@
<script>
import { Icon, Body } from "@budibase/bbui"
</script>
<a target="_blank" href="https://github.com/Budibase/budibase/discussions">
<div class="inner hoverable">
<div class="hidden hoverable">
<Body size="S">Need help? Go to our forums</Body>
</div>
<Icon name="Help" size="XXL" />
</div>
</a>
<style>
.inner {
display: flex;
align-items: center;
gap: var(--spacing-s);
}
.inner :global(*) {
pointer-events: all;
transition: color var(--spectrum-global-animation-duration-100, 130ms);
}
.inner:hover :global(*) {
color: var(--spectrum-alias-icon-color-selected-hover);
cursor: pointer;
}
a {
color: inherit;
position: absolute;
bottom: var(--spacing-m);
right: var(--spacing-m);
border-radius: 55%;
z-index: 99999;
}
.hidden {
display: none;
}
.inner:hover .hidden {
display: block;
}
</style>

View file

@ -0,0 +1,182 @@
<script>
import FontAwesomeIcon from "./FontAwesomeIcon.svelte"
import { Popover, Heading, Body } from "@budibase/bbui"
import { licensing } from "stores/portal"
import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags"
$: isPremiumUser = $licensing.license && !$licensing.isFreePlan
let show
let hide
let popoverAnchor
</script>
<div bind:this={popoverAnchor} class="help">
<button class="openMenu" on:click={show}>Help</button>
<Popover
class="helpMenuPopoverOverride"
bind:show
bind:hide
anchor={popoverAnchor}
>
<nav class="helpMenu">
<div class="header">
<Heading size="XS">Help resources</Heading>
<button on:click={hide} class="closeButton">
<FontAwesomeIcon name="fa-solid fa-xmark" />
</button>
</div>
<div class="divider" />
<a target="_blank" href="https://docs.budibase.com/docs">
<div class="icon">
<FontAwesomeIcon name="fa-solid fa-book" />
</div>
<Body size="S">Help docs</Body>
</a>
<div class="divider" />
<a
target="_blank"
href="https://github.com/Budibase/budibase/discussions"
>
<div class="icon">
<FontAwesomeIcon name="fa-brands fa-github" />
</div>
<Body size="S">Discussions</Body>
</a>
<div class="divider" />
<a target="_blank" href="https://discord.com/invite/ZepTmGbtfF">
<div class="icon">
<FontAwesomeIcon name="fa-brands fa-discord" />
</div>
<Body size="S">Discord</Body>
</a>
<div class="divider" />
<a target="_blank" href="https://vimeo.com/showcase/budibase-university">
<div class="icon">
<FontAwesomeIcon name="fa-solid fa-play" />
</div>
<Body size="S">Budibase University</Body>
</a>
<div class="divider" />
{#if isEnabled(TENANT_FEATURE_FLAGS.LICENSING)}
<a
href={isPremiumUser
? "mailto:support@budibase.com"
: "/builder/portal/account/usage"}
>
<div class="premiumLinkContent" class:disabled={!isPremiumUser}>
<div class="icon">
<FontAwesomeIcon name="fa-solid fa-envelope" />
</div>
<Body size="S">Email support</Body>
</div>
{#if !isPremiumUser}
<div class="premiumBadge">
<div class="icon">
<FontAwesomeIcon name="fa-solid fa-lock" />
</div>
<Body size="XS">Premium</Body>
</div>
{/if}
</a>
{/if}
</nav>
</Popover>
</div>
<style>
.help {
z-index: 2;
position: absolute;
bottom: var(--spacing-xl);
right: 24px;
}
.openMenu {
cursor: pointer;
background-color: #6a1dc8;
border-radius: 100px;
color: white;
border: none;
font-size: 13px;
font-weight: 600;
padding: 10px 18px;
}
.helpMenu {
background-color: var(--background-alt);
overflow: hidden;
border-radius: 4px;
}
nav {
min-width: 280px;
}
.divider {
border-bottom: 1px solid var(--spectrum-global-color-gray-300);
}
.header {
display: flex;
align-items: center;
padding: 0 0 0 16px;
}
.closeButton {
cursor: pointer;
font-size: 13px;
color: var(--grey-6);
background-color: transparent;
border: none;
padding: 18px 16px;
margin-left: auto;
}
.closeButton:hover {
color: var(--grey-8);
}
a {
text-decoration: none;
color: white;
display: flex;
padding: 12px;
align-items: center;
transition: filter 0.5s, background 0.13s ease-out;
}
a:hover {
background-color: var(--spectrum-global-color-gray-200);
}
a:last-child {
padding: 8px 12px;
}
.icon {
font-size: 13px;
margin-right: 7px;
min-width: 18px;
justify-content: center;
display: flex;
}
.premiumLinkContent {
display: flex;
align-items: center;
}
.disabled {
opacity: 60%;
}
.premiumBadge {
align-items: center;
margin-left: auto;
display: flex;
border: 1px solid var(--spectrum-global-color-gray-400);
border-radius: 4px;
padding: 4px 7px 5px 8px;
}
</style>

View file

@ -179,7 +179,7 @@
<span class="detailPopover">
<Popover
align="right-side"
align="right-outside"
bind:this={popover}
anchor={popoverAnchor}
maxWidth={300}

View file

@ -11,6 +11,8 @@
import { store } from "builderStore"
import { ProgressCircle } from "@budibase/bbui"
import CopyInput from "components/common/inputs/CopyInput.svelte"
import TourWrap from "../portal/onboarding/TourWrap.svelte"
import { TOUR_STEP_KEYS } from "../portal/onboarding/tours.js"
let publishModal
let asyncModal
@ -54,7 +56,11 @@
}
</script>
<Button cta on:click={publishModal.show}>Publish</Button>
<TourWrap tourStepKey={TOUR_STEP_KEYS.BUILDER_APP_PUBLISH}>
<Button cta on:click={publishModal.show} id={"builder-app-publish-button"}>
Publish
</Button>
</TourWrap>
<Modal bind:this={publishModal}>
<ModalContent
title="Publish to production"

View file

@ -125,7 +125,6 @@
align="right"
disabled={!isPublished}
dataCy="publish-popover-menu"
showTip={true}
anchor={publishPopoverAnchor}
>
<Layout gap="M">

View file

@ -0,0 +1,173 @@
<script>
import { Popover, Layout, Heading, Body, Button } from "@budibase/bbui"
import { store } from "builderStore"
import { TOURS } from "./tours.js"
import { goto, layout, isActive } from "@roxi/routify"
let popoverAnchor
let popover
let tourSteps = null
let tourStep
let tourStepIdx
let lastStep
$: tourNodes = { ...$store.tourNodes }
$: tourKey = $store.tourKey
$: tourStepKey = $store.tourStepKey
const initTour = targetKey => {
if (!targetKey) {
return
}
tourSteps = [...TOURS[targetKey]]
tourStepIdx = 0
tourStep = { ...tourSteps[tourStepIdx] }
}
$: initTour(tourKey)
const updateTourStep = targetStepKey => {
if (!tourSteps?.length) {
return
}
tourStepIdx = getCurrentStepIdx(tourSteps, targetStepKey)
lastStep = tourStepIdx + 1 == tourSteps.length
tourStep = { ...tourSteps[tourStepIdx] }
tourStep.onLoad()
}
$: updateTourStep(tourStepKey)
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) {
store.update(state => {
if (!state.previousTopNavPath) state.previousTopNavPath = {}
state.previousTopNavPath[activeNav.path] = window.location.pathname
$goto(state.previousTopNavPath[step.route] || step.route)
return state
})
}
}
}
const nextStep = async () => {
if (!lastStep === true) {
let target = tourSteps[tourStepIdx + 1]
if (target) {
store.update(state => ({
...state,
tourStepKey: target.id,
}))
navigateStep(target)
} else {
console.log("Could not retrieve step")
}
} else {
if (typeof tourStep.onComplete === "function") {
tourStep.onComplete()
}
popover.hide()
}
}
const previousStep = async () => {
if (tourStepIdx > 0) {
let target = tourSteps[tourStepIdx - 1]
if (target) {
store.update(state => ({
...state,
tourStepKey: target.id,
}))
navigateStep(target)
} else {
console.log("Could not retrieve step")
}
}
}
const getCurrentStepIdx = (steps, tourStepKey) => {
if (!steps?.length) {
return
}
if (steps?.length && !tourStepKey) {
return 0
}
return steps.findIndex(step => step.id === tourStepKey)
}
</script>
{#key tourStepKey}
<Popover
align={tourStep?.align}
bind:this={popover}
anchor={popoverAnchor}
dataCy="tour-popover-menu"
maxWidth={300}
dismissible={false}
>
<Layout gap="M">
<div class="tour-header">
<Heading size="XS">{tourStep?.title || "-"}</Heading>
<div>{`${tourStepIdx + 1}/${tourSteps?.length}`}</div>
</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 tourStepIdx > 0}
<Button
secondary
on:click={previousStep}
disabled={tourStepIdx == 0}
>
<div>Back</div>
</Button>
{/if}
<Button cta on:click={nextStep}>
<div>{lastStep ? "Finish" : "Next"}</div>
</Button>
</div>
</div>
</Layout>
</Popover>
{/key}
<style>
.tour-navigation {
grid-gap: var(--spectrum-alias-grid-baseline);
display: flex;
justify-content: end;
}
:global([data-cy="tour-popover-menu"]) {
padding: 10px;
margin-top: var(--spacing-l);
}
.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>

View file

@ -0,0 +1,29 @@
<script>
import { tourHandler } from "./tourHandler"
import { TOURS } from "./tours"
import { onMount, onDestroy } from "svelte"
import { store } from "builderStore"
export let tourStepKey
let currentTour
let ready = false
let handler
onMount(() => {
if (!$store.tourKey) return
currentTour = TOURS[$store.tourKey].find(step => step.id === tourStepKey)
const elem = document.querySelector(currentTour.query)
handler = tourHandler(elem, tourStepKey)
ready = true
})
onDestroy(() => {
if (handler) {
handler.destroy()
}
})
</script>
<slot />

View file

@ -0,0 +1,10 @@
<div>
In this section you can mange 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>

View file

@ -0,0 +1,10 @@
<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>

View file

@ -0,0 +1,7 @@
<div>
Once youre 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>

View file

@ -0,0 +1,3 @@
export { default as OnboardingData } from "./OnboardingData.svelte"
export { default as OnboardingDesign } from "./OnboardingDesign.svelte"
export { default as OnboardingPublish } from "./OnboardingPublish.svelte"

View file

@ -0,0 +1,47 @@
import { store } from "builderStore/index"
import { get } from "svelte/store"
const registerNode = async (node, tourStepKey) => {
if (!node) {
console.log("Tour Handler - an anchor node is required")
}
if (!get(store).tourKey) {
console.log("Tour Handler - No active tour ", tourStepKey, node)
return
}
store.update(state => {
const update = {
...state,
tourNodes: {
...state.tourNodes,
[tourStepKey]: node,
},
}
return update
})
}
export function tourHandler(node, tourStepKey) {
if (node && tourStepKey) {
registerNode(node, tourStepKey)
}
return {
destroy: () => {
const updatedTourNodes = get(store).tourNodes
if (updatedTourNodes && updatedTourNodes[tourStepKey]) {
delete updatedTourNodes[tourStepKey]
store.update(state => {
const update = {
...state,
tourNodes: {
...updatedTourNodes,
},
}
return update
})
}
},
}
}

View file

@ -0,0 +1,95 @@
import { get } from "svelte/store"
import { store } from "builderStore"
import { users, auth } from "stores/portal"
import analytics from "analytics"
import { OnboardingData, OnboardingDesign, OnboardingPublish } from "./steps"
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_AUTOMATE_SECTION: "builder-automate-section",
}
export const TOUR_KEYS = {
TOUR_BUILDER_ONBOARDING: "builder-onboarding",
}
const tourEvent = eventKey => {
analytics.captureEvent(`${ONBOARDING_EVENT_PREFIX}:${eventKey}`, {
eventSource: EventSource.PORTAL,
})
}
const getTours = () => {
return {
[TOUR_KEYS.TOUR_BUILDER_ONBOARDING]: [
{
id: TOUR_STEP_KEYS.BUILDER_DATA_SECTION,
title: "Data",
route: "/builder/app/:application/data",
layout: OnboardingData,
query: ".topcenternav .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: ".topcenternav .spectrum-Tabs-item#builder-design-tab",
onLoad: () => {
tourEvent(TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION)
},
align: "left",
},
{
id: TOUR_STEP_KEYS.BUILDER_AUTOMATE_SECTION,
title: "Automations",
route: "/builder/app/:application/automate",
query: ".topcenternav .spectrum-Tabs-item#builder-automate-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_AUTOMATE_SECTION)
},
align: "left",
},
{
id: TOUR_STEP_KEYS.BUILDER_APP_PUBLISH,
title: "Publish",
layout: OnboardingPublish,
query: ".toprightnav #builder-app-publish-button",
onLoad: () => {
tourEvent(TOUR_STEP_KEYS.BUILDER_APP_PUBLISH)
},
onComplete: async () => {
// Mark the users onboarding as complete
// Clear all tour related state
if (get(auth).user) {
await users.save({
...get(auth).user,
onboardedAt: new Date().toISOString(),
})
// Update the cached user
await auth.getSelf()
store.update(state => ({
...state,
tourNodes: undefined,
tourKey: undefined,
tourKeyStep: undefined,
onboarding: false,
}))
}
},
},
],
}
}
export const TOURS = getTours()

View file

@ -4,37 +4,45 @@
Heading,
notifications,
Layout,
Input,
Body,
ActionButton,
Modal,
} from "@budibase/bbui"
import { goto } from "@roxi/routify"
import { API } from "api"
import { admin, auth } from "stores/portal"
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
import ImportAppsModal from "./_components/ImportAppsModal.svelte"
import Logo from "assets/bb-emblem.svg"
import { onMount } from "svelte"
import { FancyForm, FancyInput, ActionButton } from "@budibase/bbui"
import { TestimonialPage } from "@budibase/frontend-core/src/components"
import { passwordsMatch, handleError } from "../auth/_components/utils"
let adminUser = {}
let error
let modal
let form
let errors = {}
let formData = {}
let submitted = false
$: tenantId = $auth.tenantId
$: multiTenancyEnabled = $admin.multiTenancy
$: cloud = $admin.cloud
$: imported = $admin.importComplete
async function save() {
form.validate()
if (Object.keys(errors).length > 0) {
return
}
submitted = true
try {
adminUser.tenantId = tenantId
let adminUser = { ...formData, tenantId }
delete adminUser.confirmationPassword
// Save the admin user
await API.createAdminUser(adminUser)
notifications.success("Admin user created")
await admin.init()
$goto("../portal")
} catch (error) {
submitted = false
notifications.error("Failed to create admin user")
}
}
@ -53,35 +61,103 @@
<Modal bind:this={modal} padding={false} width="600px">
<ImportAppsModal />
</Modal>
<section>
<div class="container">
<Layout>
<TestimonialPage>
<Layout gap="M" noPadding>
<Layout justifyItems="center" noPadding>
<img alt="logo" src={Logo} />
<Layout gap="XS" justifyItems="center" noPadding>
<Heading size="M">Create an admin user</Heading>
<Body size="M" textAlign="center">
The admin user has access to everything in Budibase.
</Body>
</Layout>
<Layout gap="XS" noPadding>
<Input label="Email" bind:value={adminUser.email} />
<PasswordRepeatInput bind:password={adminUser.password} bind:error />
</Layout>
<Layout gap="XS" noPadding>
<Button cta disabled={error} on:click={save}>
Create super admin user
</Button>
{#if multiTenancyEnabled}
<ActionButton
quiet
on:click={() => {
admin.unload()
$goto("../auth/org")
}}
>
Change organisation
</ActionButton>
{:else if !cloud && !imported}
<Heading size="M">Create an admin user</Heading>
<Body>The admin user has access to everything in Budibase.</Body>
</Layout>
<Layout gap="S" noPadding>
<FancyForm bind:this={form}>
<FancyInput
label="Email"
value={formData.email}
on:change={e => {
formData = {
...formData,
email: e.detail,
}
}}
validate={() => {
let fieldError = {
email: !formData.email ? "Please enter a valid email" : undefined,
}
errors = handleError({ ...errors, ...fieldError })
}}
disabled={submitted}
error={errors.email}
/>
<FancyInput
label="Password"
value={formData.password}
type="password"
on:change={e => {
formData = {
...formData,
password: e.detail,
}
}}
validate={() => {
let fieldError = {}
fieldError["password"] = !formData.password
? "Please enter a password"
: undefined
fieldError["confirmationPassword"] =
!passwordsMatch(
formData.password,
formData.confirmationPassword
) && formData.confirmationPassword
? "Passwords must match"
: undefined
errors = handleError({ ...errors, ...fieldError })
}}
error={errors.password}
disabled={submitted}
/>
<FancyInput
label="Repeat Password"
value={formData.confirmationPassword}
type="password"
on:change={e => {
formData = {
...formData,
confirmationPassword: e.detail,
}
}}
validate={() => {
let fieldError = {
confirmationPassword:
!passwordsMatch(
formData.password,
formData.confirmationPassword
) && formData.password
? "Passwords must match"
: undefined,
}
errors = handleError({ ...errors, ...fieldError })
}}
error={errors.confirmationPassword}
disabled={submitted}
/>
</FancyForm>
</Layout>
<Layout gap="XS" noPadding justifyItems="center">
<Button
cta
disabled={Object.keys(errors).length > 0 || submitted}
on:click={save}
>
Create super admin user
</Button>
</Layout>
<Layout gap="XS" noPadding justifyItems="center">
<div class="user-actions">
{#if !cloud && !imported}
<ActionButton
quiet
on:click={() => {
@ -91,28 +167,13 @@
Import from cloud
</ActionButton>
{/if}
</Layout>
</div>
</Layout>
</div>
</section>
</Layout>
</TestimonialPage>
<style>
section {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.container {
margin: 0 auto;
width: 260px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
}
img {
width: 48px;
margin: 0 auto;
}
</style>

View file

@ -1,6 +1,7 @@
<script>
import { store, automationStore } from "builderStore"
import { roles, flags } from "stores/backend"
import { auth } from "stores/portal"
import {
ActionMenu,
MenuItem,
@ -10,6 +11,7 @@
Heading,
notifications,
} from "@budibase/bbui"
import RevertModal from "components/deploy/RevertModal.svelte"
import VersionModal from "components/deploy/VersionModal.svelte"
import DeployNavigation from "components/deploy/DeployNavigation.svelte"
@ -17,6 +19,9 @@
import { isActive, goto, layout, redirect } from "@roxi/routify"
import { capitalise } from "helpers"
import { onMount, onDestroy } from "svelte"
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
import TourPopover from "components/portal/onboarding/TourPopover.svelte"
import { TOUR_KEYS, TOURS } from "components/portal/onboarding/tours.js"
export let application
@ -62,6 +67,23 @@
})
}
const initTour = async () => {
if (!$auth.user?.onboardedAt) {
// Determine the correct step
const activeNav = $layout.children.find(c => $isActive(c.path))
const onboardingTour = TOURS[TOUR_KEYS.TOUR_BUILDER_ONBOARDING]
const targetStep = activeNav
? onboardingTour.find(step => step.route === activeNav?.path)
: null
await store.update(state => ({
...state,
onboarding: true,
tourKey: TOUR_KEYS.TOUR_BUILDER_ONBOARDING,
tourStepKey: targetStep?.id,
}))
}
}
onMount(async () => {
if (!hasSynced && application) {
try {
@ -69,6 +91,7 @@
// check if user has beta access
// const betaResponse = await API.checkBetaAccess($auth?.user?.email)
// betaAccess = betaResponse.access
initTour()
} catch (error) {
notifications.error("Failed to sync with production database")
}
@ -88,6 +111,7 @@
<!-- This should probably be some kind of loading state? -->
<div class="loading" />
{:then _}
<TourPopover />
<div class="root">
<div class="top-nav">
<div class="topleftnav">
@ -140,12 +164,15 @@
<div class="topcenternav">
<Tabs {selected} size="M">
{#each $layout.children as { path, title }}
<Tab
quiet
selected={$isActive(path)}
on:click={topItemNavigate(path)}
title={capitalise(title)}
/>
<TourWrap tourStepKey={`builder-${title}-section`}>
<Tab
quiet
selected={$isActive(path)}
on:click={topItemNavigate(path)}
title={capitalise(title)}
id={`builder-${title}-tab`}
/>
</TourWrap>
{/each}
</Tabs>
</div>

View file

@ -1,5 +1,5 @@
<script>
import { ActionButton } from "@budibase/bbui"
import { FancyButton } from "@budibase/bbui"
import GoogleLogo from "assets/google-logo.png"
import { auth, organisation } from "stores/portal"
@ -10,31 +10,11 @@
</script>
{#if show}
<ActionButton
<FancyButton
icon={GoogleLogo}
on:click={() =>
window.open(`/api/global/auth/${tenantId}/google`, "_blank")}
>
<div class="inner">
<img src={GoogleLogo} alt="google icon" />
<p>Sign in with Google</p>
</div>
</ActionButton>
Log in with Google
</FancyButton>
{/if}
<style>
.inner {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding-top: var(--spacing-xs);
padding-bottom: var(--spacing-xs);
}
.inner img {
width: 18px;
margin: 3px 10px 3px 3px;
}
.inner p {
margin: 0;
}
</style>

View file

@ -1,5 +1,5 @@
<script>
import { ActionButton, notifications } from "@budibase/bbui"
import { notifications, FancyButton } from "@budibase/bbui"
import OidcLogo from "assets/oidc-logo.png"
import Auth0Logo from "assets/auth0-logo.png"
import MicrosoftLogo from "assets/microsoft-logo.png"
@ -33,34 +33,14 @@
</script>
{#if show}
<ActionButton
<FancyButton
icon={src}
on:click={() =>
window.open(
`/api/global/auth/${$auth.tenantId}/oidc/configs/${$oidc.uuid}`,
"_blank"
)}
>
<div class="inner">
<img {src} alt="oidc icon" />
<p>{`Sign in with ${$oidc.name || "OIDC"}`}</p>
</div>
</ActionButton>
{`Log in with ${$oidc.name || "OIDC"}`}
</FancyButton>
{/if}
<style>
.inner {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding-top: var(--spacing-xs);
padding-bottom: var(--spacing-xs);
}
.inner img {
width: 18px;
margin: 3px 10px 3px 3px;
}
.inner p {
margin: 0;
}
</style>

View file

@ -0,0 +1,17 @@
export const handleError = err => {
let update = { ...err }
return Object.keys(update).reduce((acc, key) => {
if (update[key]) {
acc[key] = update[key]
}
return acc
}, {})
}
export const passwordsMatch = (password, confirmation) => {
let confirm = confirmation?.trim()
let pwd = password?.trim()
return (
typeof confirm === "string" && typeof pwd === "string" && confirm == pwd
)
}

View file

@ -1,25 +1,35 @@
<script>
import {
notifications,
Input,
Button,
Layout,
Body,
Heading,
ActionButton,
Icon,
} from "@budibase/bbui"
import { organisation, auth } from "stores/portal"
import Logo from "assets/bb-emblem.svg"
import { onMount } from "svelte"
import { goto } from "@roxi/routify"
import { TestimonialPage } from "@budibase/frontend-core/src/components"
import { FancyForm, FancyInput } from "@budibase/bbui"
let email = ""
let form
let error
let submitted = false
async function forgot() {
form.validate()
if (error) {
return
}
submitted = true
try {
await auth.forgotPassword(email)
notifications.success("Email sent - please check your inbox")
} catch (err) {
submitted = false
notifications.error("Unable to send reset password link")
}
}
@ -33,45 +43,64 @@
})
</script>
<div class="login">
<div class="main">
<Layout>
<Layout noPadding justifyItems="center">
<img alt="logo" src={$organisation.logoUrl || Logo} />
</Layout>
<Layout gap="XS" noPadding>
<Heading textAlign="center">Forgotten your password?</Heading>
<Body size="S" textAlign="center">
No problem! Just enter your account's email address and we'll send you
a link to reset it.
</Body>
<Input label="Email" bind:value={email} />
</Layout>
<Layout gap="XS" nopadding>
<Button cta on:click={forgot} disabled={!email}>
Reset your password
</Button>
<ActionButton quiet on:click={() => $goto("../")}>Back</ActionButton>
</Layout>
<TestimonialPage>
<Layout gap="S" noPadding>
<img alt="logo" src={$organisation.logoUrl || Logo} />
<span class="heading-wrap">
<Heading size="M">
<div class="heading-content">
<span class="back-chev" on:click={() => $goto("../")}>
<Icon name="ChevronLeft" size="XL" />
</span>
Forgotten your password?
</div>
</Heading>
</span>
<Layout gap="XS" noPadding>
<Body size="M">
No problem! Just enter your account's email address and we'll send you a
link to reset it.
</Body>
</Layout>
</div>
</div>
<Layout gap="S" noPadding>
<FancyForm bind:this={form}>
<FancyInput
label="Email"
value={email}
on:change={e => {
email = e.detail
}}
validate={() => {
if (!email) {
return "Please enter your email"
}
return null
}}
{error}
disabled={submitted}
/>
</FancyForm>
</Layout>
<div>
<Button disabled={!email || error || submitted} cta on:click={forgot}>
Reset password
</Button>
</div>
</Layout>
</TestimonialPage>
<style>
.login {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.main {
width: 300px;
}
img {
width: 48px;
}
.back-chev {
display: inline-block;
cursor: pointer;
margin-left: -5px;
}
.heading-content {
display: flex;
align-items: center;
}
</style>

View file

@ -5,7 +5,6 @@
Button,
Divider,
Heading,
Input,
Layout,
notifications,
Link,
@ -14,22 +13,30 @@
import { auth, organisation, oidc, admin } from "stores/portal"
import GoogleButton from "./_components/GoogleButton.svelte"
import OIDCButton from "./_components/OIDCButton.svelte"
import { handleError } from "./_components/utils"
import Logo from "assets/bb-emblem.svg"
import { TestimonialPage } from "@budibase/frontend-core/src/components"
import { FancyForm, FancyInput } from "@budibase/bbui"
import { onMount } from "svelte"
let username = ""
let password = ""
let loaded = false
let form
let errors = {}
let formData = {}
$: company = $organisation.company || "Budibase"
$: multiTenancyEnabled = $admin.multiTenancy
$: cloud = $admin.cloud
async function login() {
form.validate()
if (Object.keys(errors).length > 0) {
console.log("errors")
return
}
try {
await auth.login({
username: username.trim(),
password,
username: formData?.username.trim(),
password: formData?.password,
})
if ($auth?.user?.forceResetPassword) {
$goto("./reset")
@ -57,75 +64,96 @@
</script>
<svelte:window on:keydown={handleKeydown} />
<div class="login">
<div class="main">
<Layout>
<Layout noPadding justifyItems="center">
<img alt="logo" src={$organisation.logoUrl || Logo} />
<Heading textAlign="center">Sign in to {company}</Heading>
</Layout>
<TestimonialPage>
<Layout gap="S" noPadding>
<Layout justifyItems="center" noPadding>
{#if loaded}
<GoogleButton />
<OIDCButton oidcIcon={$oidc.logo} oidcName={$oidc.name} />
{/if}
<Divider noGrid />
<Layout gap="XS" noPadding>
<Body size="S" textAlign="center">Sign in with email</Body>
<Input label="Email" bind:value={username} />
<Input
label="Password"
type="password"
on:change
bind:value={password}
/>
</Layout>
<Layout gap="XS" noPadding>
<Button cta disabled={!username && !password} on:click={login}
>Sign in to {company}</Button
>
<ActionButton quiet on:click={() => $goto("./forgot")}>
Forgot password?
</ActionButton>
{#if multiTenancyEnabled && !cloud}
<ActionButton
quiet
on:click={() => {
admin.unload()
$goto("./org")
}}
>
Change organisation
</ActionButton>
{/if}
</Layout>
{#if cloud}
<Body size="xs" textAlign="center">
By using Budibase Cloud
<br />
you are agreeing to our
<Link href="https://budibase.com/eula" target="_blank"
>License Agreement</Link
>
</Body>
<img alt="logo" src={$organisation.logoUrl || Logo} />
{/if}
<Heading size="M">Log in to Budibase</Heading>
</Layout>
</div>
</div>
<Layout gap="S" noPadding>
{#if loaded && ($organisation.google || $organisation.oidc)}
<FancyForm>
<OIDCButton oidcIcon={$oidc.logo} oidcName={$oidc.name} />
<GoogleButton />
</FancyForm>
<Divider />
{/if}
<FancyForm bind:this={form}>
<FancyInput
label="Your work email"
value={formData.username}
on:change={e => {
formData = {
...formData,
username: e.detail,
}
}}
validate={() => {
let fieldError = {
username: !formData.username
? "Please enter a valid email"
: undefined,
}
errors = handleError({ ...errors, ...fieldError })
}}
error={errors.username}
/>
<FancyInput
label="Password"
value={formData.password}
type="password"
on:change={e => {
formData = {
...formData,
password: e.detail,
}
}}
validate={() => {
let fieldError = {
password: !formData.password
? "Please enter your password"
: undefined,
}
errors = handleError({ ...errors, ...fieldError })
}}
error={errors.password}
/>
</FancyForm>
</Layout>
<Layout gap="XS" noPadding justifyItems="center">
<Button cta disabled={Object.keys(errors).length > 0} on:click={login}>
Log in to {company}
</Button>
</Layout>
<Layout gap="XS" noPadding justifyItems="center">
<div class="user-actions">
<ActionButton quiet on:click={() => $goto("./forgot")}>
Forgot password
</ActionButton>
</div>
</Layout>
{#if cloud}
<Body size="xs" textAlign="center">
By using Budibase Cloud
<br />
you are agreeing to our
<Link href="https://budibase.com/eula" target="_blank" secondary={true}>
License Agreement
</Link>
</Body>
{/if}
</Layout>
</TestimonialPage>
<style>
.login {
width: 100%;
height: 100%;
.user-actions {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.main {
width: 300px;
}
img {
width: 48px;
}

View file

@ -1,31 +1,43 @@
<script>
import { Body, Button, Heading, Layout, notifications } from "@budibase/bbui"
import { goto, params } from "@roxi/routify"
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
import { auth, organisation } from "stores/portal"
import Logo from "assets/bb-emblem.svg"
import { TestimonialPage } from "@budibase/frontend-core/src/components"
import { FancyForm, FancyInput } from "@budibase/bbui"
import { onMount } from "svelte"
import { handleError, passwordsMatch } from "./_components/utils"
const resetCode = $params["?code"]
let password, error
let form
let formData = {}
let errors = {}
let loaded = false
$: submitted = false
$: forceResetPassword = $auth?.user?.forceResetPassword
async function reset() {
form.validate()
if (Object.keys(errors).length > 0) {
return
}
submitted = true
try {
if (forceResetPassword) {
await auth.updateSelf({
password,
password: formData.password,
forceResetPassword: false,
})
$goto("../portal/")
} else {
await auth.resetPassword(password, resetCode)
await auth.resetPassword(formData.password, resetCode)
notifications.success("Password reset successfully")
// send them to login if reset successful
$goto("./login")
}
} catch (err) {
submitted = false
notifications.error("Unable to reset password")
}
}
@ -37,47 +49,92 @@
} catch (error) {
notifications.error("Error getting org config")
}
loaded = true
})
</script>
<div class="login">
<div class="main">
<Layout>
<Layout noPadding justifyItems="center">
<img src={$organisation.logoUrl || Logo} alt="Organisation logo" />
</Layout>
<Layout gap="XS" noPadding>
<Heading textAlign="center">Reset your password</Heading>
<Body size="S" textAlign="center">
Please enter the new password you'd like to use.
</Body>
<PasswordRepeatInput bind:password bind:error />
</Layout>
<Button
cta
on:click={reset}
disabled={error || (forceResetPassword ? false : !resetCode)}
>
Reset your password
</Button>
<TestimonialPage>
<Layout gap="S" noPadding>
{#if loaded}
<img alt="logo" src={$organisation.logoUrl || Logo} />
{/if}
<Layout gap="XS" noPadding>
<Heading size="M">Reset your password</Heading>
<Body size="M">Please enter the new password you'd like to use.</Body>
</Layout>
</div>
</div>
<Layout gap="S" noPadding>
<FancyForm bind:this={form}>
<FancyInput
label="Password"
value={formData.password}
type="password"
on:change={e => {
formData = {
...formData,
password: e.detail,
}
}}
validate={() => {
let fieldError = {}
fieldError["password"] = !formData.password
? "Please enter a password"
: undefined
fieldError["confirmationPassword"] =
!passwordsMatch(
formData.password,
formData.confirmationPassword
) && formData.confirmationPassword
? "Passwords must match"
: undefined
errors = handleError({ ...errors, ...fieldError })
}}
error={errors.password}
disabled={submitted}
/>
<FancyInput
label="Repeat Password"
value={formData.confirmationPassword}
type="password"
on:change={e => {
formData = {
...formData,
confirmationPassword: e.detail,
}
}}
validate={() => {
const isValid =
!passwordsMatch(
formData.password,
formData.confirmationPassword
) && formData.password
let fieldError = {
confirmationPassword: isValid ? "Passwords must match" : null,
}
errors = handleError({ ...errors, ...fieldError })
}}
error={errors.confirmationPassword}
disabled={submitted}
/>
</FancyForm>
</Layout>
<div>
<Button
disabled={Object.keys(errors).length > 0 ||
(forceResetPassword ? false : !resetCode)}
cta
on:click={reset}>Reset your password</Button
>
</div>
</Layout>
</TestimonialPage>
<style>
.login {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.main {
width: 260px;
}
img {
width: 48px;
}

View file

@ -1,70 +1,192 @@
<script>
import { Layout, Heading, Body, Button, notifications } from "@budibase/bbui"
import { goto, params } from "@roxi/routify"
import { users, organisation } from "stores/portal"
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
import { users, organisation, auth } from "stores/portal"
import Logo from "assets/bb-emblem.svg"
import { TestimonialPage } from "@budibase/frontend-core/src/components"
import { FancyForm, FancyInput } from "@budibase/bbui"
import { onMount } from "svelte"
import { handleError, passwordsMatch } from "../auth/_components/utils"
const inviteCode = $params["?code"]
let password, error
let form
let formData = {}
let onboarding = false
let errors = {}
$: company = $organisation.company || "Budibase"
async function acceptInvite() {
form.validate()
if (Object.keys(errors).length > 0) {
return
}
onboarding = true
try {
await users.acceptInvite(inviteCode, password)
const { password, firstName, lastName } = formData
await users.acceptInvite(inviteCode, password, firstName, lastName)
notifications.success("Invitation accepted successfully")
$goto("../auth/login")
await login()
} catch (error) {
notifications.error(error.message)
onboarding = false
}
}
async function getInvite() {
try {
const invite = await users.fetchInvite(inviteCode)
if (invite?.email) {
formData.email = invite?.email
}
} catch (error) {
notifications.error(error.message)
}
}
async function login() {
try {
await auth.login({
username: formData.email.trim(),
password: formData.password.trim(),
})
notifications.success("Logged in successfully")
$goto("../portal")
} catch (err) {
notifications.error(err.message ? err.message : "Invalid credentials") //not likely, considering.
}
}
onMount(async () => {
try {
await organisation.init()
await getInvite()
} catch (error) {
notifications.error("Error getting org config")
notifications.error("Error getting invite config")
}
})
</script>
<section>
<div class="container">
<Layout>
<img alt="logo" src={$organisation.logoUrl || Logo} />
<Layout gap="XS" justifyItems="center" noPadding>
<Heading size="M">Invitation to {company}</Heading>
<Body textAlign="center" size="M">
Please enter a password to get started.
</Body>
</Layout>
<PasswordRepeatInput bind:error bind:password />
<Button disabled={error} cta on:click={acceptInvite}>
Accept invite
</Button>
<TestimonialPage>
<Layout gap="S" noPadding>
<img alt="logo" src={$organisation.logoUrl || Logo} />
<Layout gap="XS" noPadding>
<Heading size="M">Join {company}</Heading>
<Body size="M">Create your account to access your budibase apps!</Body>
</Layout>
</div>
</section>
<Layout gap="S" noPadding>
<FancyForm bind:this={form}>
<FancyInput
label="Email"
value={formData.email}
disabled={true}
error={errors.email}
/>
<FancyInput
label="First name"
value={formData.firstName}
on:change={e => {
formData = {
...formData,
firstName: e.detail,
}
}}
validate={() => {
let fieldError = {
firstName: !formData.firstName
? "Please enter your first name"
: undefined,
}
errors = handleError({ ...errors, ...fieldError })
}}
error={errors.firstName}
disabled={onboarding}
/>
<FancyInput
label="Last name (optional)"
value={formData.lastName}
on:change={e => {
formData = {
...formData,
lastName: e.detail,
}
}}
disabled={onboarding}
/>
<FancyInput
label="Password"
value={formData.password}
type="password"
on:change={e => {
formData = {
...formData,
password: e.detail,
}
}}
validate={() => {
let fieldError = {}
fieldError["password"] = !formData.password
? "Please enter a password"
: undefined
fieldError["confirmationPassword"] =
!passwordsMatch(
formData.password,
formData.confirmationPassword
) && formData.confirmationPassword
? "Passwords must match"
: undefined
errors = handleError({ ...errors, ...fieldError })
}}
error={errors.password}
disabled={onboarding}
/>
<FancyInput
label="Repeat password"
value={formData.confirmationPassword}
type="password"
on:change={e => {
formData = {
...formData,
confirmationPassword: e.detail,
}
}}
validate={() => {
let fieldError = {
confirmationPassword:
!passwordsMatch(
formData.password,
formData.confirmationPassword
) && formData.password
? "Passwords must match"
: undefined,
}
errors = handleError({ ...errors, ...fieldError })
}}
error={errors.confirmationPassword}
disabled={onboarding}
/>
</FancyForm>
</Layout>
<div>
<Button
disabled={Object.keys(errors).length > 0 || onboarding}
cta
on:click={acceptInvite}
>
Create account
</Button>
</div>
</Layout>
</TestimonialPage>
<style>
section {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.container {
margin: 0 auto;
width: 300px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
}
img {
width: 40px;
margin: 0 auto;
}
</style>

View file

@ -2,26 +2,29 @@
import { Button } from "@budibase/bbui"
import { goto } from "@roxi/routify"
import { auth, admin } from "stores/portal"
import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags"
</script>
{#if $admin.cloud && $auth?.user?.accountPortalAccess}
<Button
cta
size="S"
on:click
on:click={() => {
window.open($admin.accountPortalUrl + "/portal/upgrade", "_blank")
}}
>
Upgrade
</Button>
{:else if !$admin.cloud && $auth.isAdmin}
<Button
cta
size="S"
on:click={() => $goto("/builder/portal/account/upgrade")}
on:click
>
Upgrade
</Button>
{#if isEnabled(TENANT_FEATURE_FLAGS.LICENSING)}
{#if $admin.cloud && $auth?.user?.accountPortalAccess}
<Button
cta
size="S"
on:click
on:click={() => {
window.open($admin.accountPortalUrl + "/portal/upgrade", "_blank")
}}
>
Upgrade
</Button>
{:else if !$admin.cloud && $auth.isAdmin}
<Button
cta
size="S"
on:click={() => $goto("/builder/portal/account/upgrade")}
on:click
>
Upgrade
</Button>
{/if}
{/if}

View file

@ -1,18 +1,20 @@
<script>
import { isActive, redirect, goto, url } from "@roxi/routify"
import { Icon, notifications, Tabs, Tab } from "@budibase/bbui"
import { organisation, auth, menu } from "stores/portal"
import { organisation, auth, menu, apps } from "stores/portal"
import { onMount } from "svelte"
import UpgradeButton from "./_components/UpgradeButton.svelte"
import MobileMenu from "./_components/MobileMenu.svelte"
import Logo from "./_components/Logo.svelte"
import UserDropdown from "./_components/UserDropdown.svelte"
import HelpMenu from "components/common/HelpMenu.svelte"
let loaded = false
let mobileMenuVisible = false
let activeTab = "Apps"
$: $url(), updateActiveTab($menu)
$: fullScreen = !$apps?.length
const updateActiveTab = menu => {
for (let entry of menu) {
@ -45,7 +47,10 @@
})
</script>
{#if $auth.user && loaded}
{#if fullScreen}
<slot />
{:else if $auth.user && loaded}
<HelpMenu />
<div class="container">
<div class="nav">
<div class="branding">

View file

@ -2,7 +2,7 @@
import { notifications } from "@budibase/bbui"
import { apps, templates, licensing, groups } from "stores/portal"
import { onMount } from "svelte"
import { goto } from "@roxi/routify"
import { redirect } from "@roxi/routify"
// Don't block loading if we've already hydrated state
let loaded = $apps.length > 0
@ -24,7 +24,7 @@
// Go to new app page if no apps exists
if (!$apps.length) {
$goto("./create")
$redirect("./onboarding")
}
} catch (error) {
notifications.error("Error loading apps and templates")

View file

@ -1,5 +1,6 @@
<script>
import { url } from "@roxi/routify"
import FirstAppOnboarding from "./onboarding/index.svelte"
import { Layout, Page, Button, Modal } from "@budibase/bbui"
import CreateAppModal from "components/start/CreateAppModal.svelte"
import TemplateDisplay from "components/common/TemplateDisplay.svelte"
@ -38,42 +39,45 @@
}
</script>
<Page>
<Layout noPadding gap="L">
<Breadcrumbs>
<Breadcrumb url={$url("./")} text="Apps" />
<Breadcrumb text="Create new app" />
</Breadcrumbs>
<Header title={$apps.length ? "Create new app" : "Create your first app"}>
<div slot="buttons">
<Button
dataCy="import-app-btn"
size="M"
secondary
on:click={initiateAppImport}
>
Import app
</Button>
<Button
dataCy="create-app-btn"
size="M"
cta
on:click={initiateAppCreation}
>
Start from scratch
</Button>
</div>
</Header>
<TemplateDisplay templates={$templates} />
</Layout>
</Page>
<Modal
bind:this={creationModal}
padding={false}
width="600px"
on:hide={stopAppCreation}
>
<CreateAppModal {template} />
</Modal>
<AppLimitModal bind:this={appLimitModal} />
{#if !$apps.length}
<FirstAppOnboarding />
{:else}
<Page>
<Layout noPadding gap="L">
<Breadcrumbs>
<Breadcrumb url={$url("./")} text="Apps" />
<Breadcrumb text="Create new app" />
</Breadcrumbs>
<Header title={"Create new app"}>
<div slot="buttons">
<Button
dataCy="import-app-btn"
size="M"
secondary
on:click={initiateAppImport}
>
Import app
</Button>
<Button
dataCy="create-app-btn"
size="M"
cta
on:click={initiateAppCreation}
>
Start from scratch
</Button>
</div>
</Header>
<TemplateDisplay templates={$templates} />
</Layout>
</Page>
<Modal
bind:this={creationModal}
padding={false}
width="600px"
on:hide={stopAppCreation}
>
<CreateAppModal {template} />
</Modal>
<AppLimitModal bind:this={appLimitModal} />
{/if}

View file

@ -0,0 +1,13 @@
<script>
import PanelHeader from "./PanelHeader.svelte"
export let onBack = () => {}
</script>
<div>
<PanelHeader
title="Give it some data"
subtitle="Not ready to add yours? Get started with sample data!"
{onBack}
/>
<slot />
</div>

View file

@ -0,0 +1,109 @@
<script>
import { Button, FancyForm, FancyInput, FancyCheckbox } from "@budibase/bbui"
import { capitalise } from "helpers/helpers"
import PanelHeader from "./PanelHeader.svelte"
export let title = ""
export let onBack = null
export let onNext = () => {}
export let fields = {}
let errors = {}
const formatName = name => {
if (name === "ca") {
return "CA"
}
if (name === "ssl") {
return "SSL"
}
if (name === "rejectUnauthorized") {
return "Reject Unauthorized"
}
return capitalise(name)
}
const getDefaultValues = fields => {
const newValues = {}
Object.entries(fields).forEach(([name, { default: defaultValue }]) => {
if (defaultValue) {
newValues[name] = defaultValue
}
})
return newValues
}
const values = getDefaultValues(fields)
const validateRequired = value => {
if (value.length < 1) {
return "Required field"
}
}
const getIsValid = (fields, errors, values) => {
for (const [name, { required }] of Object.entries(fields)) {
if (required && !values[name]) {
return false
}
}
return Object.values(errors).every(error => !error)
}
$: isValid = getIsValid(fields, errors, values)
const handleNext = () => {
const parsedValues = {}
Object.entries(values).forEach(([name, value]) => {
if (fields[name].type === "number") {
parsedValues[name] = parseInt(value, 10)
} else {
parsedValues[name] = value
}
})
return onNext(parsedValues)
}
</script>
<div>
<PanelHeader
{title}
subtitle="Fill in the required fields to fetch your tables"
{onBack}
/>
<div class="form">
<FancyForm>
{#each Object.entries(fields) as [name, { type, default: defaultValue, required }]}
{#if type !== "boolean"}
<FancyInput
bind:value={values[name]}
bind:error={errors[name]}
validate={required ? validateRequired : () => {}}
label={formatName(name)}
{type}
/>
{/if}
{/each}
{#each Object.entries(fields) as [name, { type, default: defaultValue, required }]}
{#if type === "boolean"}
<FancyCheckbox bind:value={values[name]} text={formatName(name)} />
{/if}
{/each}
</FancyForm>
</div>
<Button cta disabled={!isValid} on:click={handleNext}>Connect</Button>
</div>
<style>
.form {
margin-bottom: 36px;
}
</style>

View file

@ -0,0 +1,254 @@
<script>
export let name = ""
export let showData = false
const rows = [
{
firstName: "Julie",
lastName: "Jimenez",
email: "julie.jimenez@example.com",
address: "4250 New Street",
city: "Stevenage",
postcode: "EE32 3SE",
phone: "01754 13523",
},
{
firstName: "Mandy",
lastName: "Clark",
email: "mandy.clark@example.com",
address: "8632 North Street",
city: "Hereford",
postcode: "GT81 7DG",
phone: "016973 32814",
},
{
firstName: "Holly",
lastName: "Carroll",
email: "holly.carroll@example.com",
address: "5976 Springfield Road",
city: "Edinburgh",
postcode: "Y4 2LH",
phone: "016977 73053",
},
{
firstName: "Francis",
lastName: "Castro",
email: "francis.castro@example.com",
address: "3970 High Street",
city: "Wells",
postcode: "X12 6QA",
phone: "017684 23551",
},
]
</script>
<div tabindex="-1" class="exampleApp">
<div class="page">
<div class="header">
<img alt="Budibase Logo" src={"https://i.imgur.com/Xhdt1YP.png"} />
<h1>{name}</h1>
</div>
<div class="nav">Home</div>
<table class={`table ${showData ? "tableVisible" : ""}`}>
<thead>
<tr>
<th>FIRST NAME</th>
<th>LAST NAME</th>
<th>EMAIL</th>
<th>ADDRESS</th>
<th>CITY</th>
<th>POSTCODE</th>
<th>PHONE</th>
</tr>
</thead>
<tbody>
{#each rows as row}
<tr>
{#each Object.values(row) as value}
<td>{value}</td>
{/each}
</tr>
{/each}
</tbody>
</table>
<div class={`sidePanel ${showData ? "sidePanelVisible" : ""}`}>
<h2>{rows[0].firstName}</h2>
<div class="field">
<label for="exampleLastName">lastName</label>
<input
id="exampleLastName"
tabIndex="-1"
readonly
value={rows[0].lastName}
/>
</div>
<div class="field">
<label for="exampleEmail">Email</label>
<input id="exampleEmail" tabIndex="-1" readonly value={rows[0].email} />
</div>
<div class="field">
<label for="exampleAddress">Address</label>
<input
id="exampleAddress"
tabIndex="-1"
readonly
value={rows[0].address}
/>
</div>
<div class="field">
<label for="exampleCity">City</label>
<input id="exampleCity" tabIndex="-1" readonly value={rows[0].city} />
</div>
<div class="field">
<label for="examplePostcode">Postcode</label>
<input
id="examplePostcode"
tabIndex="-1"
readonly
value={rows[0].postcode}
/>
</div>
<div class="field">
<label for="examplePhone">Phone</label>
<input id="examplePhone" tabIndex="-1" readonly value={rows[0].phone} />
</div>
</div>
</div>
</div>
<style>
.exampleApp {
box-sizing: border-box;
height: 100vh;
padding: 100px 0 100px 100px;
--text: #191919;
--lightText: #303030;
--extraLightText: #646464;
--backgroundLight: #ffffff;
--background: #f5f5f5;
--tableBorder: 1px solid #e6e6e6;
pointer-events: none;
}
.page {
overflow: hidden;
position: relative;
height: 100%;
background-color: var(--background);
color: var(--text);
}
.header {
background-color: var(--backgroundLight);
display: flex;
padding: 32px 0 20px 32px;
align-items: center;
}
.header img {
height: 36px;
margin-right: 20px;
}
.header h1 {
margin: 0;
font-weight: 600;
font-size: 20px;
}
.nav {
background-color: var(--backgroundLight);
padding: 20px 0 20px 32px;
font-weight: 600;
font-size: 16px;
border-bottom: 1px solid #d0d0d0;
}
table {
margin: 32px;
border: var(--tableBorder);
border-collapse: collapse;
min-width: 100%;
}
thead {
border-bottom: var(--tableBorder);
}
thead th {
font-family: "Source Sans Pro";
color: var(--lightText);
white-space: nowrap;
padding: 12px;
font-weight: 600;
font-size: 12px;
text-align: left;
min-width: 70px;
}
tbody td {
border-bottom: 1px solid #e6e6e6;
background-color: var(--backgroundLight);
padding: 12px;
color: var(--extraLightText);
text-align: left;
}
.table {
opacity: 0;
}
.tableVisible {
opacity: 1;
}
.sidePanel {
position: absolute;
width: 300px;
background-color: var(--backgroundLight);
box-shadow: 0px 4px 25px 0px #00000040;
height: 100%;
top: 0;
right: -364px;
padding: 42px 32px;
}
.sidePanelVisible {
right: 0;
}
.sidePanel h2 {
margin: 0;
font-weight: 600;
font-size: 22px;
margin-bottom: 35px;
}
.field {
display: flex;
width: 100%;
align-items: center;
margin-bottom: 20px;
}
.field label {
font-weight: 500;
font-size: 12px;
color: #b0b0b0;
width: 65px;
}
.field input {
border: 1px solid #d0d0d0;
border-radius: 4px;
color: var(--lightText);
padding: 7.5px 12px;
font-size: 13px;
flex-grow: 1;
}
</style>

View file

@ -0,0 +1,60 @@
<script>
import { Button, FancyForm, FancyInput } from "@budibase/bbui"
import PanelHeader from "./PanelHeader.svelte"
export let name = ""
export let url = ""
export let onNext = () => {}
let nameError = null
let urlError = null
$: isValid = name.length && url.length && !nameError && !urlError
const validateName = name => {
if (name.length < 1) {
return "Name must be provided"
}
}
const validateUrl = url => {
if (url.length < 1) {
return "URL must be provided"
}
}
</script>
<div>
<PanelHeader
title="Build your first app"
subtitle="Name your app and set the URL"
/>
<FancyForm>
<FancyInput
bind:value={name}
bind:error={nameError}
validate={validateName}
label="Name"
/>
<FancyInput
bind:value={url}
bind:error={urlError}
validate={validateUrl}
label="URL"
/>
</FancyForm>
{#if url}
<p><span class="host">{window.location.origin}/app/</span>{url}</p>
{:else}
<p></p>
{/if}
<Button size="L" cta disabled={!isValid} on:click={onNext}>Lets go!</Button>
</div>
<style>
p {
color: white;
}
.host {
color: #b0b0b0;
}
</style>

View file

@ -0,0 +1,49 @@
<script>
import FontAwesomeIcon from "components/common/FontAwesomeIcon.svelte"
import { Heading, Body } from "@budibase/bbui"
export let onBack = null
export let title = ""
export let subtitle = ""
</script>
<div class="header">
<img
alt="Budibase Logo"
class="budibaseLogo"
src={"https://i.imgur.com/Xhdt1YP.png"}
/>
<div class="headingAndBack">
{#if onBack}
<button on:click={onBack}>
<FontAwesomeIcon name="fa-solid fa-chevron-left" />
</button>
{/if}
<Heading>{title}</Heading>
</div>
<Body>{subtitle}</Body>
</div>
<style>
.header {
margin-bottom: 20px;
}
.budibaseLogo {
width: 42px;
margin-bottom: 20px;
}
.headingAndBack {
display: flex;
margin-bottom: 13px;
}
button {
background-color: transparent;
border: none;
color: white;
padding-right: 18px;
cursor: pointer;
}
</style>

View file

@ -0,0 +1,202 @@
<script>
import { goto } from "@roxi/routify"
import NamePanel from "./_components/NamePanel.svelte"
import DataPanel from "./_components/DataPanel.svelte"
import DatasourceConfigPanel from "./_components/DatasourceConfigPanel.svelte"
import ExampleApp from "./_components/ExampleApp.svelte"
import { FancyButton, notifications, Modal } from "@budibase/bbui"
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
import { SplitPage } from "@budibase/frontend-core"
import { API } from "api"
import { store, automationStore } from "builderStore"
import { saveDatasource } from "builderStore/datasource"
import { integrations } from "stores/backend"
import { auth, admin } from "stores/portal"
import FontAwesomeIcon from "components/common/FontAwesomeIcon.svelte"
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
import createFromScratchScreen from "builderStore/store/screenTemplates/createFromScratchScreen"
import { Roles } from "constants/backend"
let name = ""
let url = ""
let stage = "name"
let appId = null
let plusIntegrations = {}
let integrationsLoading = true
$: getIntegrations()
let uploadModal
const createApp = async useSampleData => {
// Create form data to create app
// This is form based and not JSON
let data = new FormData()
data.append("name", name.trim())
data.append("url", url.trim())
data.append("useTemplate", false)
if (useSampleData) {
data.append("sampleData", true)
}
const createdApp = await API.createApp(data)
// Select Correct Application/DB in prep for creating user
const pkg = await API.fetchAppPackage(createdApp.instance._id)
await store.actions.initialise(pkg)
await automationStore.actions.fetch()
// Update checklist - in case first app
await admin.init()
// Create user
await auth.setInitInfo({})
let defaultScreenTemplate = createFromScratchScreen.create()
defaultScreenTemplate.routing.route = "/home"
defaultScreenTemplate.routing.roldId = Roles.BASIC
await store.actions.screens.save(defaultScreenTemplate)
return createdApp.instance._id
}
const getIntegrations = async () => {
try {
await integrations.init()
const newPlusIntegrations = {}
Object.entries($integrations).forEach(([integrationType, schema]) => {
if (schema?.plus) {
newPlusIntegrations[integrationType] = schema
}
})
plusIntegrations = newPlusIntegrations
} catch (e) {
notifications.error("There was a problem communicating with the server.")
} finally {
integrationsLoading = false
}
}
const goToApp = appId => {
$goto(`/builder/app/${appId}`)
notifications.success(`App created successfully`)
}
const handleCreateApp = async ({ datasourceConfig, useSampleData }) => {
try {
appId = await createApp(useSampleData)
if (datasourceConfig) {
await saveDatasource({
plus: true,
auth: undefined,
name: plusIntegrations[stage].friendlyName,
schema: plusIntegrations[stage].datasource,
config: datasourceConfig,
type: stage,
})
}
goToApp(appId)
} catch (e) {
console.log(e)
notifications.error("There was a problem creating your app")
}
}
</script>
<Modal bind:this={uploadModal}>
<CreateTableModal
name="Your Data"
beforeSave={createApp}
afterSave={() => goToApp(appId)}
/>
</Modal>
<SplitPage>
{#if stage === "name"}
<NamePanel bind:name bind:url onNext={() => (stage = "data")} />
{:else if integrationsLoading}
<p>loading...</p>
{:else if stage === "data"}
<DataPanel onBack={() => (stage = "name")}>
<div class="dataButton">
<FancyButton on:click={() => handleCreateApp({ useSampleData: true })}>
<div class="dataButtonContent">
<div class="dataButtonIcon">
<img
alt="Budibase Logo"
class="budibaseLogo"
src={"https://i.imgur.com/Xhdt1YP.png"}
/>
</div>
Budibase Sample data
</div>
</FancyButton>
</div>
<div class="dataButton">
<FancyButton on:click={uploadModal.show}>
<div class="dataButtonContent">
<div class="dataButtonIcon">
<FontAwesomeIcon name="fa-solid fa-file-arrow-up" />
</div>
Upload file
</div>
</FancyButton>
</div>
{#each Object.entries(plusIntegrations) as [integrationType, schema]}
<div class="dataButton">
<FancyButton on:click={() => (stage = integrationType)}>
<div class="dataButtonContent">
<div class="dataButtonIcon">
<IntegrationIcon {integrationType} {schema} />
</div>
{schema.friendlyName}
</div>
</FancyButton>
</div>
{/each}
</DataPanel>
{:else if stage in plusIntegrations}
<DatasourceConfigPanel
title={plusIntegrations[stage].friendlyName}
fields={plusIntegrations[stage].datasource}
onBack={() => (stage = "data")}
onNext={data => handleCreateApp({ datasourceConfig: data })}
/>
{:else}
<p>There was an problem. Please refresh the page and try again.</p>
{/if}
<div slot="right">
<ExampleApp {name} showData={stage !== "name"} />
</div>
</SplitPage>
<style>
.dataButton {
margin-bottom: 12px;
}
.dataButtonContent {
display: flex;
align-items: center;
}
.budibaseLogo {
height: 20px;
}
.dataButtonIcon {
width: 22px;
display: flex;
justify-content: center;
margin-right: 16px;
}
.dataButtonContent :global(svg) {
font-size: 18px;
color: white;
}
</style>

View file

@ -29,13 +29,19 @@ export function createUsersStore() {
async function invite(payload) {
return API.inviteUsers(payload)
}
async function acceptInvite(inviteCode, password) {
async function acceptInvite(inviteCode, password, firstName, lastName) {
return API.acceptInvite({
inviteCode,
password,
firstName,
lastName,
})
}
async function fetchInvite(inviteCode) {
return API.getUserInvite(inviteCode)
}
async function create(data) {
let mappedUsers = data.users.map(user => {
const body = {
@ -101,6 +107,7 @@ export function createUsersStore() {
fetch,
invite,
acceptInvite,
fetchInvite,
create,
save,
bulkDelete,

View file

@ -1,9 +1,16 @@
import { svelte } from "@sveltejs/vite-plugin-svelte"
import replace from "@rollup/plugin-replace"
import { defineConfig, loadEnv } from "vite"
import path from "path"
const ignoredWarnings = [
"unused-export-let",
"css-unused-selector",
"module-script-reactive-declaration",
"a11y-no-onchange",
"a11y-click-events-have-key-events",
]
export default defineConfig(({ mode }) => {
const isProduction = mode === "production"
const env = loadEnv(mode, process.cwd())
@ -29,6 +36,12 @@ export default defineConfig(({ mode }) => {
svelte({
hot: !isProduction,
emitCss: true,
onwarn: (warning, handler) => {
// Ignore some warnings
if (!ignoredWarnings.includes(warning.code)) {
handler(warning)
}
},
}),
replace({
preventAssignment: true,

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/cli",
"version": "2.2.12-alpha.45",
"version": "2.2.12-alpha.54",
"description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "src/index.js",
"bin": {
@ -26,9 +26,9 @@
"outputPath": "build"
},
"dependencies": {
"@budibase/backend-core": "2.2.12-alpha.45",
"@budibase/string-templates": "2.2.12-alpha.45",
"@budibase/types": "2.2.12-alpha.45",
"@budibase/backend-core": "2.2.12-alpha.54",
"@budibase/string-templates": "2.2.12-alpha.54",
"@budibase/types": "2.2.12-alpha.54",
"axios": "0.21.2",
"chalk": "4.1.0",
"cli-progress": "3.11.2",

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/client",
"version": "2.2.12-alpha.45",
"version": "2.2.12-alpha.54",
"license": "MPL-2.0",
"module": "dist/budibase-client.js",
"main": "dist/budibase-client.js",
@ -19,9 +19,9 @@
"dev:builder": "rollup -cw"
},
"dependencies": {
"@budibase/bbui": "2.2.12-alpha.45",
"@budibase/frontend-core": "2.2.12-alpha.45",
"@budibase/string-templates": "2.2.12-alpha.45",
"@budibase/bbui": "2.2.12-alpha.54",
"@budibase/frontend-core": "2.2.12-alpha.54",
"@budibase/string-templates": "2.2.12-alpha.54",
"@spectrum-css/button": "^3.0.3",
"@spectrum-css/card": "^3.0.3",
"@spectrum-css/divider": "^1.0.3",
@ -48,6 +48,7 @@
"devDependencies": {
"@rollup/plugin-alias": "^3.1.5",
"@rollup/plugin-commonjs": "^18.0.0",
"@rollup/plugin-image": "^3.0.2",
"@rollup/plugin-node-resolve": "^11.2.1",
"postcss": "^8.2.10",
"rollup": "^2.44.0",

View file

@ -5,6 +5,7 @@ import svelte from "rollup-plugin-svelte"
import { terser } from "rollup-plugin-terser"
import postcss from "rollup-plugin-postcss"
import svg from "rollup-plugin-svg"
import image from "@rollup/plugin-image"
import json from "rollup-plugin-json"
import nodePolyfills from "rollup-plugin-polyfill-node"
import path from "path"
@ -16,6 +17,7 @@ const ignoredWarnings = [
"css-unused-selector",
"module-script-reactive-declaration",
"a11y-no-onchange",
"a11y-click-events-have-key-events",
]
export default {
@ -87,6 +89,9 @@ export default {
dedupe: ["svelte", "svelte/internal"],
}),
svg(),
image({
exclude: "**/*.svg",
}),
json(),
production && terser(),
!production && visualizer(),

View file

@ -83,6 +83,14 @@
magic-string "^0.25.7"
resolve "^1.17.0"
"@rollup/plugin-image@^3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@rollup/plugin-image/-/plugin-image-3.0.2.tgz#8a66389510517495c5d10d392140cdefa43b27c2"
integrity sha512-eGVrD6lummWH5ENo9LWX3JY62uBb9okUNQ2htXkugrG6WjACrMUVhWvss+0wW3fwJWmFYpoEny3yL4spEdh15g==
dependencies:
"@rollup/pluginutils" "^5.0.1"
mini-svg-data-uri "^1.4.4"
"@rollup/plugin-inject@^4.0.0":
version "4.0.4"
resolved "https://registry.yarnpkg.com/@rollup/plugin-inject/-/plugin-inject-4.0.4.tgz#fbeee66e9a700782c4f65c8b0edbafe58678fbc2"
@ -113,6 +121,15 @@
estree-walker "^1.0.1"
picomatch "^2.2.2"
"@rollup/pluginutils@^5.0.1":
version "5.0.2"
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.0.2.tgz#012b8f53c71e4f6f9cb317e311df1404f56e7a33"
integrity sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==
dependencies:
"@types/estree" "^1.0.0"
estree-walker "^2.0.2"
picomatch "^2.3.1"
"@socket.io/component-emitter@~3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553"
@ -182,6 +199,11 @@
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
"@types/estree@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2"
integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==
"@types/node@*":
version "16.11.7"
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.7.tgz#36820945061326978c42a01e56b61cd223dfdc42"
@ -599,7 +621,7 @@ estree-walker@^1.0.1:
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700"
integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==
estree-walker@^2.0.1:
estree-walker@^2.0.1, estree-walker@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
@ -845,6 +867,11 @@ merge-stream@^2.0.0:
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
mini-svg-data-uri@^1.4.4:
version "1.4.4"
resolved "https://registry.yarnpkg.com/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz#8ab0aabcdf8c29ad5693ca595af19dd2ead09939"
integrity sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==
minimatch@^3.0.2, minimatch@^3.0.4:
version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
@ -955,6 +982,11 @@ picomatch@^2.2.2:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
picomatch@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
pify@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f"

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View file

@ -1,12 +1,12 @@
{
"name": "@budibase/frontend-core",
"version": "2.2.12-alpha.45",
"version": "2.2.12-alpha.54",
"description": "Budibase frontend core libraries used in builder and client",
"author": "Budibase",
"license": "MPL-2.0",
"svelte": "src/index.js",
"dependencies": {
"@budibase/bbui": "2.2.12-alpha.45",
"@budibase/bbui": "2.2.12-alpha.54",
"lodash": "^4.17.21",
"svelte": "^3.46.2"
}

View file

@ -67,12 +67,13 @@ export const buildRowEndpoints = API => ({
* @param format the format to export (csv or json)
* @param columns which columns to export (all if undefined)
*/
exportRows: async ({ tableId, rows, format, columns }) => {
exportRows: async ({ tableId, rows, format, columns, search }) => {
return await API.post({
url: `/api/${tableId}/rows/exportRows?format=${format}`,
body: {
rows,
columns,
...search,
},
parseResponse: async response => {
return await response.text()

View file

@ -146,6 +146,16 @@ export const buildUserEndpoints = API => ({
})
},
/**
* Retrieves the invitation associated with a provided code.
* @param code The unique code for the target invite
*/
getUserInvite: async code => {
return await API.get({
url: `/api/global/users/invite/${code}`,
})
},
/**
* Invites multiple users to the current tenant.
* @param users An array of users to invite
@ -168,13 +178,17 @@ export const buildUserEndpoints = API => ({
* Accepts an invite to join the platform and creates a user.
* @param inviteCode the invite code sent in the email
* @param password the password for the newly created user
* @param firstName the first name of the new user
* @param lastName the last name of the new user
*/
acceptInvite: async ({ inviteCode, password }) => {
acceptInvite: async ({ inviteCode, password, firstName, lastName }) => {
return await API.post({
url: "/api/global/users/invite/accept",
body: {
inviteCode,
password,
firstName,
lastName,
},
})
},

View file

@ -1,10 +1,16 @@
<script>
import BG from "../../assets/bg.png"
</script>
<div class="split-page">
<div class="left">
<div class="content">
<slot />
</div>
</div>
<div class="right">
<div class="right spectrum spectrum--darkest">
<!-- No alt attribute to avoid flash -->
<img src={BG} alt=" " />
<slot name="right" />
</div>
</div>
@ -25,15 +31,20 @@
overflow-y: auto;
}
.right {
background: linear-gradient(
to bottom right,
var(--spectrum-global-color-gray-300) 0%,
var(--background) 100%
);
overflow: hidden;
position: relative;
}
.right img {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
.content {
width: 100%;
max-width: 400px;
min-height: 480px;
}
@media (max-width: 740px) {

View file

@ -1,6 +1,34 @@
<script>
import SplitPage from "./SplitPage.svelte"
import { Layout } from "@budibase/bbui"
import Bulgaria from "../../assets/bulgaria.png"
import Covanta from "../../assets/covanta.png"
import Schnellecke from "../../assets/schnellecke.png"
const testimonials = [
{
text: "Budibase was the only solution that checked all the boxes for Covanta. Covanta expects to realize $3.2MM in savings due to the elimination of redundant data entry.",
name: "Charles Link",
role: "Senior Director, Data and Analytics",
image: Covanta,
imageSize: 105,
},
{
text: "Budibase was mission-critical for us and went a long way in preventing what could have become a humanitarian crisis here in Bulgaria.",
name: "Bozhidar Bozhanov",
role: "Government of Bulgaria",
image: Bulgaria,
imageSize: 49,
},
{
text: "Centralization of authentication, quick turnaround time for requests, integration with different database systems has given it the edge and its now used daily for internal development for those apps that you know you need but dont feel value in losing days of development to reinvent the wheel.",
name: "Davide Lenzarini",
role: "IT manager",
image: Schnellecke,
imageSize: 141,
},
]
const testimonial = testimonials[Math.floor(Math.random() * 3)]
</script>
<SplitPage>
@ -8,19 +36,17 @@
<div class="wrapper" slot="right">
<div class="testimonial">
<Layout noPadding gap="S">
<img
width={testimonial.imageSize}
alt="a-happy-budibase-user"
src={testimonial.image}
/>
<div class="text">
"Here is an example of how Budibase changed my life for the better and
now all I do is eat, sleep, build apps, repeat."
"{testimonial.text}"
</div>
<div class="user">
<img
alt="a-happy-budibase-user"
src="https://icon-library.com/images/male-user-icon/male-user-icon-24.jpg"
/>
<div class="author">
<div class="name">No-code Enthusiast</div>
<div class="company">Bedroom TLD</div>
</div>
<div class="author">
<div class="name">{testimonial.name}</div>
<div class="company">{testimonial.role}</div>
</div>
</Layout>
</div>
@ -35,23 +61,13 @@
place-items: center;
}
.testimonial {
width: 280px;
width: 380px;
padding: 40px;
}
.text {
font-size: var(--font-size-l);
font-style: italic;
}
img {
width: 40px;
}
.user {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-s);
}
.name {
font-weight: bold;
color: var(--spectrum-global-color-gray-900);

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/sdk",
"version": "2.2.12-alpha.45",
"version": "2.2.12-alpha.54",
"description": "Budibase Public API SDK",
"author": "Budibase",
"license": "MPL-2.0",

View file

@ -1,7 +1,7 @@
{
"name": "@budibase/server",
"email": "hi@budibase.com",
"version": "2.2.12-alpha.45",
"version": "2.2.12-alpha.54",
"description": "Budibase Web Server",
"main": "src/index.ts",
"repository": {
@ -43,11 +43,11 @@
"license": "GPL-3.0",
"dependencies": {
"@apidevtools/swagger-parser": "10.0.3",
"@budibase/backend-core": "2.2.12-alpha.45",
"@budibase/client": "2.2.12-alpha.45",
"@budibase/pro": "2.2.12-alpha.45",
"@budibase/string-templates": "2.2.12-alpha.45",
"@budibase/types": "2.2.12-alpha.45",
"@budibase/backend-core": "2.2.12-alpha.54",
"@budibase/client": "2.2.12-alpha.54",
"@budibase/pro": "2.2.12-alpha.54",
"@budibase/string-templates": "2.2.12-alpha.54",
"@budibase/types": "2.2.12-alpha.54",
"@bull-board/api": "3.7.0",
"@bull-board/koa": "3.9.4",
"@elastic/elasticsearch": "7.10.0",

View file

@ -86,7 +86,7 @@ export async function importApps(ctx: Ctx) {
if (Array.isArray(file)) {
ctx.throw(400, "Single file is required")
}
if (file.type !== "application/gzip") {
if (file.type !== "application/gzip" && file.type !== "application/x-gzip") {
ctx.throw(400, "Import file must be a gzipped tarball.")
}

View file

@ -21,6 +21,8 @@ import {
} from "@budibase/types"
import sdk from "../../../sdk"
const { cleanExportRows } = require("./utils")
export async function handleRequest(
operation: Operation,
tableId: string,
@ -100,7 +102,7 @@ export async function destroy(ctx: BBContext) {
export async function bulkDestroy(ctx: BBContext) {
const { rows } = ctx.request.body
const tableId = ctx.params.tableId
let promises = []
let promises: Promise<Row[] | { row: Row; table: Table }>[] = []
for (let row of rows) {
promises.push(
handleRequest(Operation.DELETE, tableId, {
@ -186,20 +188,24 @@ export async function exportRows(ctx: BBContext) {
if (!datasource || !datasource.entities) {
ctx.throw(400, "Datasource has not been configured for plus API.")
}
ctx.request.body = {
query: {
oneOf: {
_id: ctx.request.body.rows.map(
(row: string) => JSON.parse(decodeURI(row))[0]
),
if (ctx.request.body.rows) {
ctx.request.body = {
query: {
oneOf: {
_id: ctx.request.body.rows.map(
(row: string) => JSON.parse(decodeURI(row))[0]
),
},
},
},
}
}
let result = await search(ctx)
let rows: Row[] = []
// Filter data to only specified columns if required
if (columns && columns.length) {
for (let i = 0; i < result.rows.length; i++) {
rows[i] = {}
@ -211,14 +217,19 @@ export async function exportRows(ctx: BBContext) {
rows = result.rows
}
let headers = Object.keys(rows[0])
// @ts-ignore
let schema = datasource.entities[tableName].schema
let exportRows = cleanExportRows(rows, schema, format, columns)
let headers = Object.keys(schema)
// @ts-ignore
const exporter = exporters[format]
const filename = `export.${format}`
// send down the file
ctx.attachment(filename)
return apiFileReturn(exporter(headers, rows))
return apiFileReturn(exporter(headers, exportRows))
}
export async function fetchEnrichedRow(ctx: BBContext) {

View file

@ -27,7 +27,7 @@ import {
import { cloneDeep } from "lodash/fp"
import { context, db as dbCore } from "@budibase/backend-core"
import { finaliseRow, updateRelatedFormula } from "./staticFormula"
import { csv, json, jsonWithSchema, Format, isFormat } from "../view/exporters"
import { csv, json, jsonWithSchema, Format } from "../view/exporters"
import { apiFileReturn } from "../../../utilities/fileSystem"
import {
Ctx,
@ -38,6 +38,8 @@ import {
Table,
} from "@budibase/types"
const { cleanExportRows } = require("./utils")
const CALCULATION_TYPES = {
SUM: "sum",
COUNT: "count",
@ -357,6 +359,14 @@ export async function search(ctx: Ctx) {
params.version = ctx.version
params.tableId = tableId
let table
if (params.sort && !params.sortType) {
table = await db.get(tableId)
const schema = table.schema
const sortField = schema[params.sort]
params.sortType = sortField.type == "number" ? "number" : "string"
}
let response
if (paginate) {
response = await paginatedSearch(query, params)
@ -370,7 +380,7 @@ export async function search(ctx: Ctx) {
if (tableId === InternalTables.USER_METADATA) {
response.rows = await getGlobalUsersFromMetadata(response.rows)
}
const table = await db.get(tableId)
table = table || (await db.get(tableId))
response.rows = await outputProcessing(table, response.rows)
}
@ -389,16 +399,25 @@ export async function exportRows(ctx: Ctx) {
const table = await db.get(ctx.params.tableId)
const rowIds = ctx.request.body.rows
let format = ctx.query.format
const { columns } = ctx.request.body
let response = (
await db.allDocs({
include_docs: true,
keys: rowIds,
})
).rows.map(row => row.doc)
const { columns, query } = ctx.request.body
let result
if (rowIds) {
let response = (
await db.allDocs({
include_docs: true,
keys: rowIds,
})
).rows.map(row => row.doc)
result = await outputProcessing(table, response)
} else if (query) {
let searchResponse = await exports.search(ctx)
result = searchResponse.rows
}
let result = (await outputProcessing(table, response)) as Row[]
let rows: Row[] = []
let schema = table.schema
// Filter data to only specified columns if required
if (columns && columns.length) {
@ -412,12 +431,16 @@ export async function exportRows(ctx: Ctx) {
rows = result
}
let exportRows = cleanExportRows(rows, schema, format, columns)
if (format === Format.CSV) {
ctx.attachment("export.csv")
return apiFileReturn(csv(Object.keys(rows[0]), rows))
return apiFileReturn(csv(Object.keys(rows[0]), exportRows))
} else if (format === Format.JSON) {
ctx.attachment("export.json")
return apiFileReturn(json(rows))
return apiFileReturn(json(exportRows))
} else if (format === Format.JSON_WITH_SCHEMA) {
ctx.attachment("export.json")
return apiFileReturn(jsonWithSchema(schema, exportRows))
} else {
throw "Format not recognised"
}

View file

@ -7,6 +7,7 @@ import { BBContext, Row, Table } from "@budibase/types"
export { removeKeyNumbering } from "../../../integrations/base/utils"
const validateJs = require("validate.js")
const { cloneDeep } = require("lodash/fp")
import { Format } from "../view/exporters"
import { Ctx } from "@budibase/types"
import sdk from "../../../sdk"
@ -117,3 +118,40 @@ export async function validate({
}
return { valid: Object.keys(errors).length === 0, errors }
}
export function cleanExportRows(
rows: any[],
schema: any,
format: string,
columns: string[]
) {
let cleanRows = [...rows]
const relationships = Object.entries(schema)
.filter((entry: any[]) => entry[1].type === FieldTypes.LINK)
.map(entry => entry[0])
relationships.forEach(column => {
cleanRows.forEach(row => {
delete row[column]
})
delete schema[column]
})
// Intended to avoid 'undefined' in export
if (format === Format.CSV) {
const schemaKeys = Object.keys(schema)
for (let key of schemaKeys) {
if (columns?.length && columns.indexOf(key) > 0) {
continue
}
for (let row of cleanRows) {
if (row[key] == null) {
row[key] = ""
}
}
}
}
return cleanRows
}

View file

@ -3,7 +3,6 @@ import { apiFileReturn } from "../../../utilities/fileSystem"
import { csv, json, jsonWithSchema, Format, isFormat } from "./exporters"
import { deleteView, getView, getViews, saveView } from "./utils"
import { fetchView } from "../row"
import { FieldTypes } from "../../../constants"
import { context, events } from "@budibase/backend-core"
import { DocumentType } from "../../../db/utils"
import sdk from "../../../sdk"
@ -15,6 +14,7 @@ import {
TableSchema,
View,
} from "@budibase/types"
import { cleanExportRows } from "../row/utils"
const { cloneDeep, isEqual } = require("lodash")
@ -162,39 +162,17 @@ export async function exportView(ctx: BBContext) {
schema = table.schema
}
// remove any relationships
const relationships = Object.entries(schema)
.filter(entry => entry[1].type === FieldTypes.LINK)
.map(entry => entry[0])
// iterate relationship columns and remove from and row and schema
relationships.forEach(column => {
rows.forEach(row => {
delete row[column]
})
delete schema[column]
})
// make sure no "undefined" entries appear in the CSV
if (format === Format.CSV) {
const schemaKeys = Object.keys(schema)
for (let key of schemaKeys) {
for (let row of rows) {
if (row[key] == null) {
row[key] = ""
}
}
}
}
let exportRows = cleanExportRows(rows, schema, format, [])
if (format === Format.CSV) {
ctx.attachment(`${viewName}.csv`)
ctx.body = apiFileReturn(csv(Object.keys(schema), rows))
ctx.body = apiFileReturn(csv(Object.keys(schema), exportRows))
} else if (format === Format.JSON) {
ctx.attachment(`${viewName}.json`)
ctx.body = apiFileReturn(json(rows))
ctx.body = apiFileReturn(json(exportRows))
} else if (format === Format.JSON_WITH_SCHEMA) {
ctx.attachment(`${viewName}.json`)
ctx.body = apiFileReturn(jsonWithSchema(schema, rows))
ctx.body = apiFileReturn(jsonWithSchema(schema, exportRows))
} else {
throw "Format not recognised"
}

View file

@ -7,44 +7,3 @@
export interface QueryOptions {
disableReturning?: boolean
}
export enum AuthType {
BASIC = "basic",
BEARER = "bearer",
}
interface AuthConfig {
_id: string
name: string
type: AuthType
config: BasicAuthConfig | BearerAuthConfig
}
export interface BasicAuthConfig {
username: string
password: string
}
export interface BearerAuthConfig {
token: string
}
export interface RestConfig {
url: string
rejectUnauthorized: boolean
defaultHeaders: {
[key: string]: any
}
legacyHttpParser: boolean
authConfigs: AuthConfig[]
staticVariables: {
[key: string]: string
}
dynamicVariables: [
{
name: string
queryId: string
value: string
}
]
}

View file

@ -6,13 +6,11 @@ import {
IntegrationBase,
PaginationValues,
RestQueryFields as RestQuery,
} from "@budibase/types"
import {
RestConfig,
AuthType,
BasicAuthConfig,
BearerAuthConfig,
} from "../definitions/datasource"
RestAuthType,
RestBasicAuthConfig,
RestBearerAuthConfig,
} from "@budibase/types"
import { get } from "lodash"
import * as https from "https"
import qs from "querystring"
@ -331,14 +329,14 @@ class RestIntegration implements IntegrationBase {
if (authConfig) {
let config
switch (authConfig.type) {
case AuthType.BASIC:
config = authConfig.config as BasicAuthConfig
case RestAuthType.BASIC:
config = authConfig.config as RestBasicAuthConfig
headers.Authorization = `Basic ${Buffer.from(
`${config.username}:${config.password}`
).toString("base64")}`
break
case AuthType.BEARER:
config = authConfig.config as BearerAuthConfig
case RestAuthType.BEARER:
config = authConfig.config as RestBearerAuthConfig
headers.Authorization = `Bearer ${config.token}`
break
}
@ -428,5 +426,4 @@ class RestIntegration implements IntegrationBase {
export default {
schema: SCHEMA,
integration: RestIntegration,
AuthType,
}

View file

@ -15,6 +15,7 @@ jest.mock("node-fetch", () => {
import fetch from "node-fetch"
import { default as RestIntegration } from "../rest"
import { RestAuthType } from "@budibase/types"
const FormData = require("form-data")
const { URLSearchParams } = require("url")
@ -229,7 +230,7 @@ describe("REST Integration", () => {
const basicAuth = {
_id: "c59c14bd1898a43baa08da68959b24686",
name: "basic-1",
type: RestIntegration.AuthType.BASIC,
type: RestAuthType.BASIC,
config: {
username: "user",
password: "password",
@ -239,7 +240,7 @@ describe("REST Integration", () => {
const bearerAuth = {
_id: "0d91d732f34e4befabeff50b392a8ff3",
name: "bearer-1",
type: RestIntegration.AuthType.BEARER,
type: RestAuthType.BEARER,
config: {
token: "mytoken",
},
@ -581,6 +582,7 @@ describe("REST Integration", () => {
})
await config.integration.read({})
// @ts-ignore
const calls: any = fetch.mock.calls[0]
const url = calls[0]
expect(url).toBe(`${BASE_URL}/`)

View file

@ -1,17 +1,19 @@
import { context } from "@budibase/backend-core"
import { processObjectSync, findHBSBlocks } from "@budibase/string-templates"
import { findHBSBlocks, processObjectSync } from "@budibase/string-templates"
import {
Datasource,
DatasourceFieldType,
Integration,
PASSWORD_REPLACEMENT,
RestAuthConfig,
RestAuthType,
RestBasicAuthConfig,
SourceName,
} from "@budibase/types"
import { cloneDeep } from "lodash/fp"
import { getEnvironmentVariables } from "../../utils"
import { getDefinitions } from "../../../integrations"
const ENV_VAR_PREFIX = "env."
const USER_PREFIX = "user"
async function enrichDatasourceWithValues(datasource: Datasource) {
const cloned = cloneDeep(datasource)
@ -47,6 +49,18 @@ export async function getWithEnvVars(datasourceId: string) {
return enrichDatasourceWithValues(datasource)
}
function hasAuthConfigs(datasource: Datasource) {
return datasource.source === SourceName.REST && datasource.config?.authConfigs
}
function useEnvVars(str: any) {
if (typeof str !== "string") {
return false
}
const blocks = findHBSBlocks(str)
return blocks.find(block => block.includes(ENV_VAR_PREFIX)) != null
}
export async function removeSecrets(datasources: Datasource[]) {
const definitions = await getDefinitions()
for (let datasource of datasources) {
@ -56,17 +70,24 @@ export async function removeSecrets(datasources: Datasource[]) {
if (datasource.config.auth) {
delete datasource.config.auth
}
// remove passwords
for (let key of Object.keys(datasource.config)) {
if (typeof datasource.config[key] !== "string") {
continue
// specific to REST datasources, contains passwords
if (hasAuthConfigs(datasource)) {
const configs = datasource.config.authConfigs as RestAuthConfig[]
for (let config of configs) {
if (config.type !== RestAuthType.BASIC) {
continue
}
const basic = config.config as RestBasicAuthConfig
if (!useEnvVars(basic.password)) {
basic.password = PASSWORD_REPLACEMENT
}
}
const blocks = findHBSBlocks(datasource.config[key] as string)
const usesEnvVars =
blocks.find(block => block.includes(ENV_VAR_PREFIX)) != null
}
// remove general passwords
for (let key of Object.keys(datasource.config)) {
if (
!usesEnvVars &&
schema.datasource?.[key]?.type === DatasourceFieldType.PASSWORD
schema.datasource?.[key]?.type === DatasourceFieldType.PASSWORD &&
!useEnvVars(datasource.config[key])
) {
datasource.config[key] = PASSWORD_REPLACEMENT
}
@ -84,6 +105,23 @@ export function mergeConfigs(update: Datasource, old: Datasource) {
if (!update.config) {
return update
}
// specific to REST datasources, fix the auth configs again if required
if (hasAuthConfigs(update)) {
const configs = update.config.authConfigs as RestAuthConfig[]
const oldConfigs = old.config?.authConfigs as RestAuthConfig[]
for (let config of configs) {
if (config.type !== RestAuthType.BASIC) {
continue
}
const basic = config.config as RestBasicAuthConfig
const oldBasic = oldConfigs.find(old => old.name === config.name)
?.config as RestBasicAuthConfig
if (basic.password === PASSWORD_REPLACEMENT) {
basic.password = oldBasic.password
}
}
}
// update back to actual passwords for everything else
for (let [key, value] of Object.entries(update.config)) {
if (value !== PASSWORD_REPLACEMENT) {
continue

View file

@ -1278,13 +1278,13 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@budibase/backend-core@2.2.12-alpha.45":
version "2.2.12-alpha.45"
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.2.12-alpha.45.tgz#426102222a72fecbfa9f269b79053c7f7518d1cd"
integrity sha512-iz0iNd6hekY9qJcnOkS/Z8RNXvxcimp61eZUkCleAQcs9L2kOVYPjCNvMGROS2OhJZ05ZIp19iy6ttMt7tCwEw==
"@budibase/backend-core@2.2.12-alpha.54":
version "2.2.12-alpha.54"
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.2.12-alpha.54.tgz#c03c63c3d58cf401928104bf52cfd288f93f0fb8"
integrity sha512-BfO9UlbnbRzSPXpjqAOx2qNU0atsQejCJTyB2n8nkdJqTgy6XGobQBn7iNiEWBO/zmIbqS4sz0/8NkDj7MxXDg==
dependencies:
"@budibase/nano" "10.1.1"
"@budibase/types" "2.2.12-alpha.45"
"@budibase/types" "2.2.12-alpha.54"
"@shopify/jest-koa-mocks" "5.0.1"
"@techpass/passport-openidconnect" "0.3.2"
aws-cloudfront-sign "2.2.0"
@ -1379,13 +1379,13 @@
qs "^6.11.0"
tough-cookie "^4.1.2"
"@budibase/pro@2.2.12-alpha.45":
version "2.2.12-alpha.45"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.2.12-alpha.45.tgz#9292e14c88afdf463f17f3b296fbb1fe1a67c472"
integrity sha512-CEePkBbKZOuGLB5iQy1qrmkwGwZcF5T7JrnmQ1BejDUjRcgmRYS5JCa3kv1KqqqpXmij7MXltMAhxlmE4hYM+g==
"@budibase/pro@2.2.12-alpha.54":
version "2.2.12-alpha.54"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.2.12-alpha.54.tgz#e8c259ff88326f34eaca7ef2f5a15461a104f976"
integrity sha512-gBIbbcR1fNNhs0D6+XrWP+nn9lJAHqjP8yQh3pPQ7srVSarohvdE3UgLEguMGtmGckJkna4UgzQd8ftOzd16sQ==
dependencies:
"@budibase/backend-core" "2.2.12-alpha.45"
"@budibase/types" "2.2.12-alpha.45"
"@budibase/backend-core" "2.2.12-alpha.54"
"@budibase/types" "2.2.12-alpha.54"
"@koa/router" "8.0.8"
bull "4.10.1"
joi "17.6.0"
@ -1411,10 +1411,10 @@
svelte-apexcharts "^1.0.2"
svelte-flatpickr "^3.1.0"
"@budibase/types@2.2.12-alpha.45":
version "2.2.12-alpha.45"
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.2.12-alpha.45.tgz#1e9674aa98f4a5c234f224c4985cb67f6775ec27"
integrity sha512-lZq9Pe4H1L/fCNl/Y27TFNbCU0yGcHp349AQg0BfVq8DWmkcBvoqmtTFIkxcIbSX5GLugD5cz9nN3WFelYKU+Q==
"@budibase/types@2.2.12-alpha.54":
version "2.2.12-alpha.54"
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.2.12-alpha.54.tgz#3d7676a92c42cbe131bc98ddc84a1d7ce9518efd"
integrity sha512-t9GuvH8BIfHt4YoXM6Ztk9wmGipk9SwEdwCjcJfF2qhusgR4mJjl8LWQgzzKoCAryDgEmME3ajkaE6iw7g130A==
"@bull-board/api@3.7.0":
version "3.7.0"

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/string-templates",
"version": "2.2.12-alpha.45",
"version": "2.2.12-alpha.54",
"description": "Handlebars wrapper for Budibase templating.",
"main": "src/index.cjs",
"module": "dist/bundle.mjs",

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/types",
"version": "2.2.12-alpha.45",
"version": "2.2.12-alpha.54",
"description": "Budibase types",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View file

@ -15,3 +15,44 @@ export interface Datasource extends Document {
[key: string]: Table
}
}
export enum RestAuthType {
BASIC = "basic",
BEARER = "bearer",
}
export interface RestBasicAuthConfig {
username: string
password: string
}
export interface RestBearerAuthConfig {
token: string
}
export interface RestAuthConfig {
_id: string
name: string
type: RestAuthType
config: RestBasicAuthConfig | RestBearerAuthConfig
}
export interface RestConfig {
url: string
rejectUnauthorized: boolean
defaultHeaders: {
[key: string]: any
}
legacyHttpParser: boolean
authConfigs: RestAuthConfig[]
staticVariables: {
[key: string]: string
}
dynamicVariables: [
{
name: string
queryId: string
value: string
}
]
}

View file

@ -47,6 +47,7 @@ export interface User extends ThirdPartyUser {
account?: {
authType: string
}
onboardedAt?: string
}
export interface UserRoles {

View file

@ -6,6 +6,9 @@ export enum Event {
USER_UPDATED = "user:updated",
USER_DELETED = "user:deleted",
// USER / ONBOARDING
USER_ONBOARDING_COMPLETE = "user:onboarding:complete",
// USER / PERMISSIONS
USER_PERMISSION_ADMIN_ASSIGNED = "user:admin:assigned",
USER_PERMISSION_ADMIN_REMOVED = "user:admin:removed",

View file

@ -12,6 +12,11 @@ export interface UserDeletedEvent extends BaseEvent {
userId: string
}
export interface UserOnboardingEvent extends BaseEvent {
userId: string
step?: string
}
export interface UserPermissionAssignedEvent extends BaseEvent {
userId: string
}

View file

@ -1,7 +1,7 @@
{
"name": "@budibase/worker",
"email": "hi@budibase.com",
"version": "2.2.12-alpha.45",
"version": "2.2.12-alpha.54",
"description": "Budibase background service",
"main": "src/index.ts",
"repository": {
@ -36,10 +36,10 @@
"author": "Budibase",
"license": "GPL-3.0",
"dependencies": {
"@budibase/backend-core": "2.2.12-alpha.45",
"@budibase/pro": "2.2.12-alpha.45",
"@budibase/string-templates": "2.2.12-alpha.45",
"@budibase/types": "2.2.12-alpha.45",
"@budibase/backend-core": "2.2.12-alpha.54",
"@budibase/pro": "2.2.12-alpha.54",
"@budibase/string-templates": "2.2.12-alpha.54",
"@budibase/types": "2.2.12-alpha.54",
"@koa/router": "8.0.8",
"@sentry/node": "6.17.7",
"@techpass/passport-openidconnect": "0.3.2",

View file

@ -210,6 +210,19 @@ export const inviteMultiple = async (ctx: any) => {
ctx.body = await sdk.users.invite(request)
}
export const checkInvite = async (ctx: any) => {
const { code } = ctx.params
let invite
try {
invite = await checkInviteCode(code, false)
} catch (e) {
ctx.throw(400, "There was a problem with the invite")
}
ctx.body = {
email: invite.email,
}
}
export const inviteAccept = async (ctx: any) => {
const { inviteCode, password, firstName, lastName } = ctx.request.body
try {

View file

@ -62,6 +62,10 @@ const PUBLIC_ENDPOINTS = [
route: "/api/system/restored",
method: "POST",
},
{
route: "/api/global/users/invite",
method: "GET",
},
]
const NO_TENANCY_ENDPOINTS = [

View file

@ -38,6 +38,13 @@ function buildInviteMultipleValidation() {
))
}
function buildInviteLookupValidation() {
// prettier-ignore
return auth.joiValidator.params(Joi.object({
code: Joi.string().required()
}).unknown(true))
}
const createUserAdminOnly = (ctx: any, next: any) => {
if (!ctx.request.body._id) {
return auth.adminOnly(ctx, next)
@ -51,6 +58,8 @@ function buildInviteAcceptValidation() {
return auth.joiValidator.body(Joi.object({
inviteCode: Joi.string().required(),
password: Joi.string().required(),
firstName: Joi.string().required(),
lastName: Joi.string().optional(),
}).required().unknown(true))
}
@ -91,6 +100,11 @@ router
)
// non-global endpoints
.get(
"/api/global/users/invite/:code",
buildInviteLookupValidation(),
controller.checkInvite
)
.post(
"/api/global/users/invite/accept",
buildInviteAcceptValidation(),

View file

@ -73,6 +73,10 @@ export const handleSaveEvents = async (
await events.user.permissionAdminRemoved(user)
}
if (isOnboardingComplete(user, existingUser)) {
await events.user.onboardingComplete(user)
}
if (
!existingUser.forceResetPassword &&
user.forceResetPassword &&
@ -114,6 +118,10 @@ const isRemovingAdmin = (user: any, existingUser: any) => {
return isRemovingPermission(user, existingUser, isAdmin)
}
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.
*/

View file

@ -48,6 +48,7 @@ export class UserAPI extends TestAPI {
.send({
password: "newpassword",
inviteCode: code,
firstName: "Ted",
})
.expect("Content-Type", /json/)
.expect(200)

View file

@ -475,13 +475,13 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@budibase/backend-core@2.2.12-alpha.45":
version "2.2.12-alpha.45"
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.2.12-alpha.45.tgz#426102222a72fecbfa9f269b79053c7f7518d1cd"
integrity sha512-iz0iNd6hekY9qJcnOkS/Z8RNXvxcimp61eZUkCleAQcs9L2kOVYPjCNvMGROS2OhJZ05ZIp19iy6ttMt7tCwEw==
"@budibase/backend-core@2.2.12-alpha.54":
version "2.2.12-alpha.54"
resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.2.12-alpha.54.tgz#c03c63c3d58cf401928104bf52cfd288f93f0fb8"
integrity sha512-BfO9UlbnbRzSPXpjqAOx2qNU0atsQejCJTyB2n8nkdJqTgy6XGobQBn7iNiEWBO/zmIbqS4sz0/8NkDj7MxXDg==
dependencies:
"@budibase/nano" "10.1.1"
"@budibase/types" "2.2.12-alpha.45"
"@budibase/types" "2.2.12-alpha.54"
"@shopify/jest-koa-mocks" "5.0.1"
"@techpass/passport-openidconnect" "0.3.2"
aws-cloudfront-sign "2.2.0"
@ -526,13 +526,13 @@
qs "^6.11.0"
tough-cookie "^4.1.2"
"@budibase/pro@2.2.12-alpha.45":
version "2.2.12-alpha.45"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.2.12-alpha.45.tgz#9292e14c88afdf463f17f3b296fbb1fe1a67c472"
integrity sha512-CEePkBbKZOuGLB5iQy1qrmkwGwZcF5T7JrnmQ1BejDUjRcgmRYS5JCa3kv1KqqqpXmij7MXltMAhxlmE4hYM+g==
"@budibase/pro@2.2.12-alpha.54":
version "2.2.12-alpha.54"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.2.12-alpha.54.tgz#e8c259ff88326f34eaca7ef2f5a15461a104f976"
integrity sha512-gBIbbcR1fNNhs0D6+XrWP+nn9lJAHqjP8yQh3pPQ7srVSarohvdE3UgLEguMGtmGckJkna4UgzQd8ftOzd16sQ==
dependencies:
"@budibase/backend-core" "2.2.12-alpha.45"
"@budibase/types" "2.2.12-alpha.45"
"@budibase/backend-core" "2.2.12-alpha.54"
"@budibase/types" "2.2.12-alpha.54"
"@koa/router" "8.0.8"
bull "4.10.1"
joi "17.6.0"
@ -540,10 +540,10 @@
lru-cache "^7.14.1"
node-fetch "^2.6.1"
"@budibase/types@2.2.12-alpha.45":
version "2.2.12-alpha.45"
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.2.12-alpha.45.tgz#1e9674aa98f4a5c234f224c4985cb67f6775ec27"
integrity sha512-lZq9Pe4H1L/fCNl/Y27TFNbCU0yGcHp349AQg0BfVq8DWmkcBvoqmtTFIkxcIbSX5GLugD5cz9nN3WFelYKU+Q==
"@budibase/types@2.2.12-alpha.54":
version "2.2.12-alpha.54"
resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.2.12-alpha.54.tgz#3d7676a92c42cbe131bc98ddc84a1d7ce9518efd"
integrity sha512-t9GuvH8BIfHt4YoXM6Ztk9wmGipk9SwEdwCjcJfF2qhusgR4mJjl8LWQgzzKoCAryDgEmME3ajkaE6iw7g130A==
"@cspotcode/source-map-support@^0.8.0":
version "0.8.1"