Merge branch 'develop' into feature/dependencies-image
This commit is contained in:
commit
34278349c3
|
@ -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 }}
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.2.12-alpha.45",
|
||||
"version": "2.2.12-alpha.54",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -63,6 +63,10 @@ const INITIAL_FRONTEND_STATE = {
|
|||
selectedScreenId: null,
|
||||
selectedComponentId: null,
|
||||
selectedLayoutId: null,
|
||||
|
||||
// onboarding
|
||||
onboarding: false,
|
||||
tourNodes: null,
|
||||
}
|
||||
|
||||
export const getFrontendStore = () => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -248,6 +248,7 @@
|
|||
/>
|
||||
<EnvDropdown
|
||||
label="Password"
|
||||
type="password"
|
||||
bind:value={form.basic.password}
|
||||
on:change={onFieldChange}
|
||||
on:blur={() => (blurred.basic.password = true)}
|
||||
|
|
|
@ -182,3 +182,9 @@
|
|||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<style>
|
||||
svg {
|
||||
padding-top: 3px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -11,6 +11,6 @@
|
|||
|
||||
<style>
|
||||
img {
|
||||
padding-top: 1px;
|
||||
padding-top: 5px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -179,7 +179,7 @@
|
|||
|
||||
<span class="detailPopover">
|
||||
<Popover
|
||||
align="right-side"
|
||||
align="right-outside"
|
||||
bind:this={popover}
|
||||
anchor={popoverAnchor}
|
||||
maxWidth={300}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -125,7 +125,6 @@
|
|||
align="right"
|
||||
disabled={!isPublished}
|
||||
dataCy="publish-popover-menu"
|
||||
showTip={true}
|
||||
anchor={publishPopoverAnchor}
|
||||
>
|
||||
<Layout gap="M">
|
||||
|
|
|
@ -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>
|
|
@ -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 />
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,7 @@
|
|||
<div>
|
||||
Once you’re happy with your app you can publish it to production!
|
||||
<p>
|
||||
After publishing, any changes you make will not take affect until you next
|
||||
publish.
|
||||
</p>
|
||||
</div>
|
|
@ -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"
|
|
@ -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
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
|
@ -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()
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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,
|
||||
|
|
|
@ -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
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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 |
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
},
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 it’s now used daily for internal development for those apps that you know you need but don’t 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);
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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.")
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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}/`)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ export interface User extends ThirdPartyUser {
|
|||
account?: {
|
||||
authType: string
|
||||
}
|
||||
onboardedAt?: string
|
||||
}
|
||||
|
||||
export interface UserRoles {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -62,6 +62,10 @@ const PUBLIC_ENDPOINTS = [
|
|||
route: "/api/system/restored",
|
||||
method: "POST",
|
||||
},
|
||||
{
|
||||
route: "/api/global/users/invite",
|
||||
method: "GET",
|
||||
},
|
||||
]
|
||||
|
||||
const NO_TENANCY_ENDPOINTS = [
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -48,6 +48,7 @@ export class UserAPI extends TestAPI {
|
|||
.send({
|
||||
password: "newpassword",
|
||||
inviteCode: code,
|
||||
firstName: "Ted",
|
||||
})
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue