Merge branch 'master' into feature/sql-attachments

This commit is contained in:
Michael Drury 2025-03-14 10:58:17 +00:00 committed by GitHub
commit b3108cd731
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
99 changed files with 1971 additions and 697 deletions

View File

@ -5,10 +5,10 @@
export let disabled = false export let disabled = false
export let align = "left" export let align = "left"
export let portalTarget export let portalTarget = undefined
export let openOnHover = false export let openOnHover = false
export let animate export let animate = true
export let offset export let offset = undefined
const actionMenuContext = getContext("actionMenu") const actionMenuContext = getContext("actionMenu")

View File

@ -14,7 +14,7 @@
export let url = "" export let url = ""
export let disabled = false export let disabled = false
export let initials = "JD" export let initials = "JD"
export let color = null export let color = ""
const DefaultColor = "#3aab87" const DefaultColor = "#3aab87"

View File

@ -28,23 +28,7 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const categories = [ const categories = [
{ {
label: "Theme", label: "Theme colors",
colors: [
"gray-50",
"gray-75",
"gray-100",
"gray-200",
"gray-300",
"gray-400",
"gray-500",
"gray-600",
"gray-700",
"gray-800",
"gray-900",
],
},
{
label: "Colors",
colors: [ colors: [
"red-100", "red-100",
"orange-100", "orange-100",
@ -91,6 +75,49 @@
"indigo-700", "indigo-700",
"magenta-700", "magenta-700",
"gray-50",
"gray-75",
"gray-100",
"gray-200",
"gray-300",
"gray-400",
"gray-500",
"gray-600",
"gray-700",
"gray-800",
"gray-900",
],
},
{
label: "Static colors",
colors: [
"static-red-400",
"static-orange-400",
"static-yellow-400",
"static-green-400",
"static-seafoam-400",
"static-blue-400",
"static-indigo-400",
"static-magenta-400",
"static-red-800",
"static-orange-800",
"static-yellow-800",
"static-green-800",
"static-seafoam-800",
"static-blue-800",
"static-indigo-800",
"static-magenta-800",
"static-red-1200",
"static-orange-1200",
"static-yellow-1200",
"static-green-1200",
"static-seafoam-1200",
"static-blue-1200",
"static-indigo-1200",
"static-magenta-1200",
"static-white", "static-white",
"static-black", "static-black",
], ],
@ -137,10 +164,13 @@
: "var(--spectrum-global-color-gray-50)" : "var(--spectrum-global-color-gray-50)"
} }
// Use contrasating check for the dim colours // Use contrasting check for the dim colours
if (value?.includes("-100")) { if (value?.includes("-100")) {
return "var(--spectrum-global-color-gray-900)" return "var(--spectrum-global-color-gray-900)"
} }
if (value?.includes("-1200") || value?.includes("-800")) {
return "var(--spectrum-global-color-static-gray-50)"
}
// Use black check for static white // Use black check for static white
if (value?.includes("static-black")) { if (value?.includes("static-black")) {
@ -169,7 +199,7 @@
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<Popover bind:this={dropdown} anchor={preview} maxHeight={320} {offset} {align}> <Popover bind:this={dropdown} anchor={preview} maxHeight={350} {offset} {align}>
<Layout paddingX="XL" paddingY="L"> <Layout paddingX="XL" paddingY="L">
<div class="container"> <div class="container">
{#each categories as category} {#each categories as category}

View File

@ -1,24 +1,31 @@
<script lang="ts"> <script lang="ts">
import "@spectrum-css/textfield/dist/index-vars.css" import "@spectrum-css/textfield/dist/index-vars.css"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import type { FocusEventHandler } from "svelte/elements"
export let value: string | null = null export let value: string | undefined = ""
export let placeholder: string | null = null export let placeholder: string | undefined = undefined
export let disabled = false export let disabled: boolean = false
export let readonly = false export let readonly: boolean = false
export let id: string | null = null export let id: string | undefined = undefined
export let height: number | null = null export let height: string | number | undefined = undefined
export let minHeight: number | null = null export let minHeight: string | number | undefined = undefined
export let align = null export let align = null
export let updateOnChange: boolean = false
export const getCaretPosition = () => ({
start: textarea.selectionStart,
end: textarea.selectionEnd,
})
const dispatch = createEventDispatcher()
let isFocused = false let isFocused = false
let textarea: HTMLTextAreaElement let textarea: HTMLTextAreaElement
const dispatch = createEventDispatcher<{ change: string }>() let scrollable = false
const onChange: FocusEventHandler<HTMLTextAreaElement> = event => {
dispatch("change", event.currentTarget.value) $: heightString = getStyleString("height", height)
isFocused = false $: minHeightString = getStyleString("min-height", minHeight)
} $: dispatch("scrollable", scrollable)
export function focus() { export function focus() {
textarea.focus() textarea.focus()
@ -28,18 +35,38 @@
return textarea.value return textarea.value
} }
const getStyleString = (attribute: string, value: number | null) => { const onBlur = () => {
if (!attribute || value == null) { isFocused = false
updateValue()
}
const onChange = () => {
scrollable = textarea.clientHeight < textarea.scrollHeight
if (!updateOnChange) {
return
}
updateValue()
}
const updateValue = () => {
if (readonly || disabled) {
return
}
dispatch("change", textarea.value)
}
const getStyleString = (
attribute: string,
value: string | number | undefined
) => {
if (value == null) {
return "" return ""
} }
if (typeof value === "number" && isNaN(value)) { if (typeof value !== "number" || isNaN(value)) {
return `${attribute}:${value};` return `${attribute}:${value};`
} }
return `${attribute}:${value}px;` return `${attribute}:${value}px;`
} }
$: heightString = getStyleString("height", height)
$: minHeightString = getStyleString("min-height", minHeight)
</script> </script>
<div <div
@ -57,8 +84,10 @@
{disabled} {disabled}
{readonly} {readonly}
{id} {id}
on:input={onChange}
on:focus={() => (isFocused = true)} on:focus={() => (isFocused = true)}
on:blur={onChange} on:blur={onBlur}
on:blur
on:keypress on:keypress
>{value || ""}</textarea> >{value || ""}</textarea>
</div> </div>

View File

@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import SpectrumMDE from "./SpectrumMDE.svelte" import SpectrumMDE from "./SpectrumMDE.svelte"
export let value: string | null = null export let value: string | undefined = undefined
export let height: string | null = null export let height: string | undefined = undefined
let mde: any let mde: any
@ -40,6 +40,7 @@
border: none; border: none;
background: transparent; background: transparent;
padding: 0; padding: 0;
color: inherit;
} }
.markdown-viewer :global(.EasyMDEContainer) { .markdown-viewer :global(.EasyMDEContainer) {
background: transparent; background: transparent;

View File

@ -11,6 +11,16 @@
--bb-forest-green: #053835; --bb-forest-green: #053835;
--bb-beige: #f6efea; --bb-beige: #f6efea;
/* Custom spectrum additions */
--spectrum-global-color-static-red-1200: #740000;
--spectrum-global-color-static-orange-1200: #612300;
--spectrum-global-color-static-yellow-1200: #483300;
--spectrum-global-color-static-green-1200: #053f27;
--spectrum-global-color-static-seafoam-1200: #123c3a;
--spectrum-global-color-static-blue-1200: #003571;
--spectrum-global-color-static-indigo-1200: #262986;
--spectrum-global-color-static-magenta-1200: #700037;
--grey-1: #fafafa; --grey-1: #fafafa;
--grey-2: #f5f5f5; --grey-2: #f5f5f5;
--grey-3: #eeeeee; --grey-3: #eeeeee;

View File

@ -56,10 +56,7 @@
memo, memo,
fetchData, fetchData,
} from "@budibase/frontend-core" } from "@budibase/frontend-core"
import { import { getSchemaForDatasourcePlus } from "@/dataBinding"
getSchemaForDatasourcePlus,
readableToRuntimeBinding,
} from "@/dataBinding"
import { TriggerStepID, ActionStepID } from "@/constants/backend/automations" import { TriggerStepID, ActionStepID } from "@/constants/backend/automations"
import { onMount, createEventDispatcher } from "svelte" import { onMount, createEventDispatcher } from "svelte"
import { writable } from "svelte/store" import { writable } from "svelte/store"
@ -1037,10 +1034,7 @@
{bindings} {bindings}
{schema} {schema}
panel={AutomationBindingPanel} panel={AutomationBindingPanel}
on:change={e => on:change={e => onChange({ [key]: e.detail })}
onChange({
[key]: readableToRuntimeBinding(bindings, e.detail),
})}
context={$memoContext} context={$memoContext}
value={inputData[key]} value={inputData[key]}
/> />
@ -1065,6 +1059,7 @@
inputData[key] = e.detail inputData[key] = e.detail
}} }}
completions={stepCompletions} completions={stepCompletions}
{bindings}
mode={codeMode} mode={codeMode}
autocompleteEnabled={codeMode !== EditorModes.JS} autocompleteEnabled={codeMode !== EditorModes.JS}
bind:getCaretPosition bind:getCaretPosition

View File

@ -152,6 +152,7 @@
<div class="field-wrap json-field"> <div class="field-wrap json-field">
<CodeEditor <CodeEditor
value={readableValue} value={readableValue}
{bindings}
on:blur={e => { on:blur={e => {
onChange({ onChange({
row: { row: {

View File

@ -59,7 +59,11 @@
import { javascript } from "@codemirror/lang-javascript" import { javascript } from "@codemirror/lang-javascript"
import { EditorModes } from "./" import { EditorModes } from "./"
import { themeStore } from "@/stores/portal" import { themeStore } from "@/stores/portal"
import { FeatureFlag, type EditorMode } from "@budibase/types" import {
type EnrichedBinding,
FeatureFlag,
type EditorMode,
} from "@budibase/types"
import { tooltips } from "@codemirror/view" import { tooltips } from "@codemirror/view"
import type { BindingCompletion, CodeValidator } from "@/types" import type { BindingCompletion, CodeValidator } from "@/types"
import { validateHbsTemplate } from "./validator/hbs" import { validateHbsTemplate } from "./validator/hbs"
@ -80,6 +84,7 @@
export let readonly = false export let readonly = false
export let readonlyLineNumbers = false export let readonlyLineNumbers = false
export let dropdown = DropdownPosition.Relative export let dropdown = DropdownPosition.Relative
export let bindings: EnrichedBinding[] = []
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -100,7 +105,8 @@
let promptInput: TextArea let promptInput: TextArea
$: aiGenEnabled = $: aiGenEnabled =
featureFlag.isEnabled(FeatureFlag.AI_JS_GENERATION) && featureFlag.isEnabled(FeatureFlag.AI_JS_GENERATION) &&
mode.name === "javascript" mode.name === "javascript" &&
!readonly
$: { $: {
if (autofocus && isEditorInitialised) { if (autofocus && isEditorInitialised) {
@ -165,15 +171,24 @@
popoverWidth = 30 popoverWidth = 30
let code = "" let code = ""
try { try {
const resp = await API.generateJs({ prompt }) const resp = await API.generateJs({ prompt, bindings })
code = resp.code code = resp.code
if (code === "") {
throw new Error(
"we didn't understand your prompt, please phrase your request in another way"
)
}
} catch (e) { } catch (e) {
console.error(e) console.error(e)
notifications.error("Unable to generate code, please try again later.") if (e instanceof Error) {
notifications.error(`Unable to generate code: ${e.message}`)
} else {
notifications.error("Unable to generate code, please try again later.")
}
code = previousContents code = previousContents
popoverWidth = 300
promptLoading = false promptLoading = false
popover.hide() resetPopover()
return return
} }
value = code value = code
@ -189,6 +204,8 @@
suggestedCode = null suggestedCode = null
previousContents = null previousContents = null
resetPopover() resetPopover()
dispatch("change", editor.state.doc.toString())
dispatch("blur", editor.state.doc.toString())
} }
const rejectSuggestion = () => { const rejectSuggestion = () => {
@ -513,13 +530,18 @@
bind:this={popover} bind:this={popover}
minWidth={popoverWidth} minWidth={popoverWidth}
anchor={popoverAnchor} anchor={popoverAnchor}
on:close={() => {
if (suggestedCode) {
acceptSuggestion()
}
}}
align="left-outside" align="left-outside"
> >
{#if promptLoading} {#if promptLoading}
<div class="prompt-spinner"> <div class="prompt-spinner">
<Spinner size="20" color="white" /> <Spinner size="20" color="white" />
</div> </div>
{:else if suggestedCode} {:else if suggestedCode !== null}
<Button on:click={acceptSuggestion}>Accept</Button> <Button on:click={acceptSuggestion}>Accept</Button>
<Button on:click={rejectSuggestion}>Reject</Button> <Button on:click={rejectSuggestion}>Reject</Button>
{:else} {:else}

View File

@ -0,0 +1,12 @@
<script lang="ts">
import { DrawerBindableInput } from "@/components/common/bindings"
</script>
<DrawerBindableInput
on:change
on:blur
on:drawerHide
on:drawerShow
{...$$props}
multiline
/>

View File

@ -5,8 +5,8 @@
import { appsStore } from "@/stores/portal" import { appsStore } from "@/stores/portal"
import { API } from "@/api" import { API } from "@/api"
import { writable } from "svelte/store" import { writable } from "svelte/store"
import { createValidationStore } from "@/helpers/validation/yup" import { createValidationStore } from "@budibase/frontend-core/src/utils/validation/yup"
import * as appValidation from "@/helpers/validation/yup/app" import * as appValidation from "@budibase/frontend-core/src/utils/validation/yup/app"
import EditableIcon from "@/components/common/EditableIcon.svelte" import EditableIcon from "@/components/common/EditableIcon.svelte"
import { isEqual } from "lodash" import { isEqual } from "lodash"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"

View File

@ -359,6 +359,7 @@
bind:getCaretPosition bind:getCaretPosition
bind:insertAtPos bind:insertAtPos
{completions} {completions}
{bindings}
{validations} {validations}
autofocus={autofocusEditor} autofocus={autofocusEditor}
placeholder={placeholder || placeholder={placeholder ||
@ -372,6 +373,7 @@
value={jsValue ? decodeJSBinding(jsValue) : ""} value={jsValue ? decodeJSBinding(jsValue) : ""}
on:change={onChangeJSValue} on:change={onChangeJSValue}
{completions} {completions}
{bindings}
{validations} {validations}
mode={EditorModes.JS} mode={EditorModes.JS}
bind:getCaretPosition bind:getCaretPosition

View File

@ -5,7 +5,10 @@
encodeJSBinding, encodeJSBinding,
processObjectSync, processObjectSync,
} from "@budibase/string-templates" } from "@budibase/string-templates"
import { runtimeToReadableBinding } from "@/dataBinding" import {
runtimeToReadableBinding,
readableToRuntimeBinding,
} from "@/dataBinding"
import CodeEditor, { DropdownPosition } from "../CodeEditor/CodeEditor.svelte" import CodeEditor, { DropdownPosition } from "../CodeEditor/CodeEditor.svelte"
import { import {
getHelperCompletions, getHelperCompletions,
@ -123,7 +126,7 @@
} }
const updateValue = (val: any) => { const updateValue = (val: any) => {
dispatch("change", val) dispatch("change", readableToRuntimeBinding(bindings, val))
} }
const onChangeJSValue = (e: { detail: string }) => { const onChangeJSValue = (e: { detail: string }) => {
@ -144,6 +147,7 @@
on:change={onChangeJSValue} on:change={onChangeJSValue}
on:blur on:blur
completions={jsCompletions} completions={jsCompletions}
{bindings}
mode={EditorModes.JS} mode={EditorModes.JS}
bind:getCaretPosition bind:getCaretPosition
bind:insertAtPos bind:insertAtPos

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Icon, Input, Drawer, Button } from "@budibase/bbui" import { Icon, Input, Drawer, Button, CoreTextArea } from "@budibase/bbui"
import { import {
readableToRuntimeBinding, readableToRuntimeBinding,
runtimeToReadableBinding, runtimeToReadableBinding,
@ -25,11 +25,13 @@
export let forceModal: boolean = false export let forceModal: boolean = false
export let context = null export let context = null
export let autocomplete: boolean | undefined = undefined export let autocomplete: boolean | undefined = undefined
export let multiline: boolean = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let bindingDrawer: any let bindingDrawer: any
let currentVal = value let currentVal = value
let scrollable = false
$: readableValue = runtimeToReadableBinding(bindings, value) $: readableValue = runtimeToReadableBinding(bindings, value)
$: tempValue = readableValue $: tempValue = readableValue
@ -63,14 +65,16 @@
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="control" class:disabled> <div class="control" class:multiline class:disabled class:scrollable>
<Input <svelte:component
this={multiline ? CoreTextArea : Input}
{label} {label}
{disabled} {disabled}
readonly={isJS} readonly={isJS}
value={isJS ? "(JavaScript function)" : readableValue} value={isJS ? "(JavaScript function)" : readableValue}
on:change={event => onChange(event.detail)} on:change={event => onChange(event.detail)}
on:blur={onBlur} on:blur={onBlur}
on:scrollable={e => (scrollable = e.detail)}
{placeholder} {placeholder}
{updateOnChange} {updateOnChange}
{autocomplete} {autocomplete}
@ -114,36 +118,38 @@
position: relative; position: relative;
} }
.icon { /* Multiline styles */
right: 1px; .control.multiline :global(textarea) {
bottom: 1px; min-height: 0 !important;
position: absolute; field-sizing: content;
justify-content: center; max-height: 105px;
align-items: center; padding: 6px 11px 6px 11px;
display: flex; height: auto;
flex-direction: row; resize: none;
box-sizing: border-box; flex: 1 1 auto;
border-left: 1px solid var(--spectrum-alias-border-color); width: 0;
border-top-right-radius: var(--spectrum-alias-border-radius-regular);
border-bottom-right-radius: var(--spectrum-alias-border-radius-regular);
width: 31px;
color: var(--spectrum-alias-text-color);
background-color: var(--spectrum-global-color-gray-75);
transition: background-color
var(--spectrum-global-animation-duration-100, 130ms),
box-shadow var(--spectrum-global-animation-duration-100, 130ms),
border-color var(--spectrum-global-animation-duration-100, 130ms);
height: calc(var(--spectrum-alias-item-height-m) - 2px);
} }
.icon {
right: 6px;
top: 8px;
position: absolute;
display: grid;
place-items: center;
box-sizing: border-box;
border-radius: 4px;
color: var(--spectrum-alias-text-color);
}
.icon:hover { .icon:hover {
cursor: pointer; cursor: pointer;
color: var(--spectrum-alias-text-color-hover); color: var(--spectrum-global-color-blue-600);
background-color: var(--spectrum-global-color-gray-50); }
border-color: var(--spectrum-alias-border-color-hover); .control.scrollable .icon {
right: 12px;
} }
.control:not(.disabled) :global(.spectrum-Textfield-input) { .control:not(.disabled) :global(.spectrum-Textfield-input),
padding-right: 40px; .control:not(.disabled) :global(textarea) {
padding-right: 26px;
} }
</style> </style>

View File

@ -31,9 +31,11 @@ import FormStepConfiguration from "./controls/FormStepConfiguration.svelte"
import FormStepControls from "./controls/FormStepControls.svelte" import FormStepControls from "./controls/FormStepControls.svelte"
import PaywalledSetting from "./controls/PaywalledSetting.svelte" import PaywalledSetting from "./controls/PaywalledSetting.svelte"
import TableConditionEditor from "./controls/TableConditionEditor.svelte" import TableConditionEditor from "./controls/TableConditionEditor.svelte"
import MultilineDrawerBindableInput from "@/components/common/MultilineDrawerBindableInput.svelte"
const componentMap = { const componentMap = {
text: DrawerBindableInput, text: DrawerBindableInput,
"text/multiline": MultilineDrawerBindableInput,
plainText: Input, plainText: Input,
select: Select, select: Select,
radio: RadioGroup, radio: RadioGroup,

View File

@ -1,34 +0,0 @@
<script>
import { ModalContent, Body, notifications } from "@budibase/bbui"
import PasswordRepeatInput from "@/components/common/users/PasswordRepeatInput.svelte"
import { auth } from "@/stores/portal"
let password
let error
const updatePassword = async () => {
try {
await auth.updateSelf({ password })
notifications.success("Password changed successfully")
} catch (error) {
notifications.error("Failed to update password")
}
}
const handleKeydown = evt => {
if (evt.key === "Enter" && !error && password) {
updatePassword()
}
}
</script>
<svelte:window on:keydown={handleKeydown} />
<ModalContent
title="Update password"
confirmText="Update password"
onConfirm={updatePassword}
disabled={error || !password}
>
<Body size="S">Enter your new password below.</Body>
<PasswordRepeatInput bind:password bind:error />
</ModalContent>

View File

@ -1,29 +0,0 @@
<script>
import { ModalContent, Body, Input, notifications } from "@budibase/bbui"
import { writable } from "svelte/store"
import { auth } from "@/stores/portal"
const values = writable({
firstName: $auth.user.firstName,
lastName: $auth.user.lastName,
})
const updateInfo = async () => {
try {
await auth.updateSelf($values)
notifications.success("Information updated successfully")
} catch (error) {
console.error(error)
notifications.error("Failed to update information")
}
}
</script>
<ModalContent title="My profile" confirmText="Save" onConfirm={updateInfo}>
<Body size="S">
Personalise the platform by adding your first name and last name.
</Body>
<Input disabled bind:value={$auth.user.email} label="Email" />
<Input bind:value={$values.firstName} label="First name" />
<Input bind:value={$values.lastName} label="Last name" />
</ModalContent>

View File

@ -12,8 +12,8 @@
import { appsStore, admin, auth } from "@/stores/portal" import { appsStore, admin, auth } from "@/stores/portal"
import { onMount } from "svelte" import { onMount } from "svelte"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { createValidationStore } from "@/helpers/validation/yup" import { createValidationStore } from "@budibase/frontend-core/src/utils/validation/yup"
import * as appValidation from "@/helpers/validation/yup/app" import * as appValidation from "@budibase/frontend-core/src/utils/validation/yup/app"
import TemplateCard from "@/components/common/TemplateCard.svelte" import TemplateCard from "@/components/common/TemplateCard.svelte"
import { lowercase } from "@/helpers" import { lowercase } from "@/helpers"
import { sdk } from "@budibase/shared-core" import { sdk } from "@budibase/shared-core"

View File

@ -6,9 +6,9 @@
Layout, Layout,
keepOpen, keepOpen,
} from "@budibase/bbui" } from "@budibase/bbui"
import { createValidationStore } from "@/helpers/validation/yup" import { createValidationStore } from "@budibase/frontend-core/src/utils/validation/yup"
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
import * as appValidation from "@/helpers/validation/yup/app" import * as appValidation from "@budibase/frontend-core/src/utils/validation/yup/app"
import { appsStore, auth } from "@/stores/portal" import { appsStore, auth } from "@/stores/portal"
import { onMount } from "svelte" import { onMount } from "svelte"
import { API } from "@/api" import { API } from "@/api"

View File

@ -9,7 +9,7 @@
notifications, notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import { downloadFile } from "@budibase/frontend-core" import { downloadFile } from "@budibase/frontend-core"
import { createValidationStore } from "@/helpers/validation/yup" import { createValidationStore } from "@budibase/frontend-core/src/utils/validation/yup"
export let app export let app
export let published export let published

View File

@ -219,6 +219,7 @@ export const PrettyRelationshipDefinitions = {
export const BUDIBASE_INTERNAL_DB_ID = INTERNAL_TABLE_SOURCE_ID export const BUDIBASE_INTERNAL_DB_ID = INTERNAL_TABLE_SOURCE_ID
export const DEFAULT_BB_DATASOURCE_ID = "datasource_internal_bb_default" export const DEFAULT_BB_DATASOURCE_ID = "datasource_internal_bb_default"
export const DEFAULT_EMPLOYEE_TABLE_ID = "ta_bb_employee"
export const BUDIBASE_DATASOURCE_TYPE = "budibase" export const BUDIBASE_DATASOURCE_TYPE = "budibase"
export const DB_TYPE_INTERNAL = "internal" export const DB_TYPE_INTERNAL = "internal"
export const DB_TYPE_EXTERNAL = "external" export const DB_TYPE_EXTERNAL = "external"

View File

@ -41,11 +41,6 @@ export const LAYOUT_NAMES = {
}, },
} }
// one or more word characters and whitespace
export const APP_NAME_REGEX = /^[\w\s]+$/
// zero or more non-whitespace characters
export const APP_URL_REGEX = /^[0-9a-zA-Z-_]+$/
export const DefaultAppTheme = { export const DefaultAppTheme = {
primaryColor: "var(--spectrum-global-color-blue-600)", primaryColor: "var(--spectrum-global-color-blue-600)",
primaryColorHover: "var(--spectrum-global-color-blue-500)", primaryColorHover: "var(--spectrum-global-color-blue-500)",

View File

@ -28,13 +28,13 @@
Constants, Constants,
Utils, Utils,
RoleUtils, RoleUtils,
emailValidator,
} from "@budibase/frontend-core" } from "@budibase/frontend-core"
import { sdk } from "@budibase/shared-core" import { sdk } from "@budibase/shared-core"
import { API } from "@/api" import { API } from "@/api"
import GroupIcon from "../../../portal/users/groups/_components/GroupIcon.svelte" import GroupIcon from "../../../portal/users/groups/_components/GroupIcon.svelte"
import RoleSelect from "@/components/common/RoleSelect.svelte" import RoleSelect from "@/components/common/RoleSelect.svelte"
import UpgradeModal from "@/components/common/users/UpgradeModal.svelte" import UpgradeModal from "@/components/common/users/UpgradeModal.svelte"
import { emailValidator } from "@/helpers/validation"
import { fly } from "svelte/transition" import { fly } from "svelte/transition"
import InfoDisplay from "../design/[screenId]/[componentId]/_components/Component/InfoDisplay.svelte" import InfoDisplay from "../design/[screenId]/[componentId]/_components/Component/InfoDisplay.svelte"
import BuilderGroupPopover from "./BuilderGroupPopover.svelte" import BuilderGroupPopover from "./BuilderGroupPopover.svelte"

View File

@ -1,13 +1,30 @@
<script> <script lang="ts">
import { redirect } from "@roxi/routify" import { redirect } from "@roxi/routify"
import { TableNames } from "@/constants" import { TableNames } from "@/constants"
import { datasources } from "@/stores/builder" import { datasources } from "@/stores/builder"
import { onMount } from "svelte"
$: { onMount(() => {
// Get first valid table ID of first datasource
let tableId: string = TableNames.USERS
for (let ds of $datasources.list) {
if (Array.isArray(ds.entities) && ds.entities.length > 0) {
if (ds.entities[0]._id) {
tableId = ds.entities[0]._id
break
}
} else {
const keys = Object.keys(ds.entities || {})
if (keys.length > 0) {
tableId = keys[0]
break
}
}
}
if ($datasources.hasData) { if ($datasources.hasData) {
$redirect(`./table/${TableNames.USERS}`) $redirect(`./table/${tableId}`)
} else { } else {
$redirect("./new") $redirect("./new")
} }
} })
</script> </script>

View File

@ -2,6 +2,7 @@
import StyleSection from "./StyleSection.svelte" import StyleSection from "./StyleSection.svelte"
import * as ComponentStyles from "./componentStyles" import * as ComponentStyles from "./componentStyles"
import ComponentSettingsSection from "./ComponentSettingsSection.svelte" import ComponentSettingsSection from "./ComponentSettingsSection.svelte"
import ColorPicker from "@/components/design/settings/controls/ColorPicker.svelte"
export let componentDefinition export let componentDefinition
export let componentInstance export let componentInstance
@ -18,6 +19,19 @@
styles.push(ComponentStyles[style]) styles.push(ComponentStyles[style])
} }
}) })
// Add section for CSS variables if present
if (def?.cssVariables?.length) {
styles.push({
label: "Customization",
settings: def.cssVariables.map(variable => ({
label: variable.label,
key: variable.variable,
control: ColorPicker,
})),
})
}
return styles return styles
} }

View File

@ -8,6 +8,7 @@
Checkbox, Checkbox,
notifications, notifications,
Select, Select,
Stepper,
} from "@budibase/bbui" } from "@budibase/bbui"
import { import {
themeStore, themeStore,
@ -182,6 +183,16 @@
options: screenRouteOptions, options: screenRouteOptions,
}} }}
/> />
<PropertyControl
label="Logo height (px)"
control={Stepper}
value={$nav.logoHeight}
onChange={height => update("logoHeight", height)}
props={{
updateOnChange: false,
placeholder: "24",
}}
/>
<PropertyControl <PropertyControl
label="New tab" label="New tab"
control={Checkbox} control={Checkbox}

View File

@ -18,6 +18,11 @@
ghost.src = ghost.src =
"data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
// Aliases for other strings to match to when searching
const aliases = {
text: ["headline", "paragraph"],
}
let searchString let searchString
let searchRef let searchRef
let selectedIndex let selectedIndex
@ -148,11 +153,12 @@
} }
const filterStructure = (structure, allowedComponents, search) => { const filterStructure = (structure, allowedComponents, search) => {
selectedIndex = search ? 0 : null
componentList = []
if (!structure?.length) { if (!structure?.length) {
return [] return []
} }
search = search?.toLowerCase()
selectedIndex = search ? 0 : null
componentList = []
// Return only items which match the search string // Return only items which match the search string
let filteredStructure = [] let filteredStructure = []
@ -161,8 +167,12 @@
const name = child.name.toLowerCase() const name = child.name.toLowerCase()
// Check if the component matches the search string // Check if the component matches the search string
if (search && !name.includes(search.toLowerCase())) { if (search) {
return false const nameMatch = name.includes(search)
const aliasMatch = (aliases[name] || []).some(x => x.includes(search))
if (!nameMatch && !aliasMatch) {
return false
}
} }
// Check if the component is allowed as a child // Check if the component is allowed as a child

View File

@ -32,8 +32,7 @@
"name": "Basic", "name": "Basic",
"icon": "TextParagraph", "icon": "TextParagraph",
"children": [ "children": [
"heading", "textv2",
"text",
"button", "button",
"buttongroup", "buttongroup",
"tag", "tag",

View File

@ -1,5 +1,5 @@
<script> <script>
import { redirect } from "@roxi/routify" import { redirect } from "@roxi/routify"
$redirect("./data") $redirect("./design")
</script> </script>

View File

@ -24,13 +24,13 @@
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { AppStatus } from "@/constants" import { AppStatus } from "@/constants"
import { gradient } from "@/actions" import { gradient } from "@/actions"
import ProfileModal from "@/components/settings/ProfileModal.svelte" import { ProfileModal, ChangePasswordModal } from "@budibase/frontend-core"
import ChangePasswordModal from "@/components/settings/ChangePasswordModal.svelte"
import { processStringSync } from "@budibase/string-templates" import { processStringSync } from "@budibase/string-templates"
import Spaceman from "assets/bb-space-man.svg" import Spaceman from "assets/bb-space-man.svg"
import Logo from "assets/bb-emblem.svg" import Logo from "assets/bb-emblem.svg"
import { UserAvatar } from "@budibase/frontend-core" import { UserAvatar } from "@budibase/frontend-core"
import { helpers, sdk } from "@budibase/shared-core" import { helpers, sdk } from "@budibase/shared-core"
import { API } from "@/api"
let loaded = false let loaded = false
let userInfoModal let userInfoModal
@ -105,8 +105,8 @@
<img class="logo" alt="logo" src={$organisation.logoUrl || Logo} /> <img class="logo" alt="logo" src={$organisation.logoUrl || Logo} />
<ActionMenu align="right"> <ActionMenu align="right">
<div slot="control" class="avatar"> <div slot="control" class="avatar">
<UserAvatar user={$auth.user} showTooltip={false} /> <UserAvatar size="M" user={$auth.user} showTooltip={false} />
<Icon size="XL" name="ChevronDown" /> <Icon size="L" name="ChevronDown" />
</div> </div>
<MenuItem icon="UserEdit" on:click={() => userInfoModal.show()}> <MenuItem icon="UserEdit" on:click={() => userInfoModal.show()}>
My profile My profile
@ -201,10 +201,14 @@
</Page> </Page>
</div> </div>
<Modal bind:this={userInfoModal}> <Modal bind:this={userInfoModal}>
<ProfileModal /> <ProfileModal {API} user={$auth.user} on:save={() => auth.getSelf()} />
</Modal> </Modal>
<Modal bind:this={changePasswordModal}> <Modal bind:this={changePasswordModal}>
<ChangePasswordModal /> <ChangePasswordModal
{API}
passwordMinLength={$admin.passwordMinLength}
on:save={() => auth.getSelf()}
/>
</Modal> </Modal>
{/if} {/if}
@ -239,6 +243,7 @@
grid-template-columns: auto auto; grid-template-columns: auto auto;
place-items: center; place-items: center;
grid-gap: var(--spacing-xs); grid-gap: var(--spacing-xs);
transition: filter 130ms ease-out;
} }
.avatar:hover { .avatar:hover {
cursor: pointer; cursor: pointer;

View File

@ -8,11 +8,10 @@
notifications, notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import { goto, params } from "@roxi/routify" import { goto, params } from "@roxi/routify"
import { auth, organisation } from "@/stores/portal" import { auth, organisation, admin } from "@/stores/portal"
import Logo from "assets/bb-emblem.svg" import Logo from "assets/bb-emblem.svg"
import { TestimonialPage } from "@budibase/frontend-core/src/components" import { TestimonialPage, PasswordRepeatInput } from "@budibase/frontend-core"
import { onMount } from "svelte" import { onMount } from "svelte"
import PasswordRepeatInput from "../../../components/common/users/PasswordRepeatInput.svelte"
const resetCode = $params["?code"] const resetCode = $params["?code"]
let form let form
@ -80,9 +79,9 @@
<Heading size="M">Reset your password</Heading> <Heading size="M">Reset your password</Heading>
<Body size="M">Must contain at least 12 characters</Body> <Body size="M">Must contain at least 12 characters</Body>
<PasswordRepeatInput <PasswordRepeatInput
bind:passwordForm={form}
bind:password bind:password
bind:error={passwordError} bind:error={passwordError}
minLength={$admin.passwordMinLength || 12}
/> />
<Button secondary cta on:click={reset}> <Button secondary cta on:click={reset}>
{#if loading} {#if loading}

View File

@ -2,11 +2,12 @@
import { admin, auth } from "@/stores/portal" import { admin, auth } from "@/stores/portal"
import { ActionMenu, MenuItem, Icon, Modal } from "@budibase/bbui" import { ActionMenu, MenuItem, Icon, Modal } from "@budibase/bbui"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import ProfileModal from "@/components/settings/ProfileModal.svelte" import ProfileModal from "@budibase/frontend-core/src/components/ProfileModal.svelte"
import ChangePasswordModal from "@/components/settings/ChangePasswordModal.svelte" import ChangePasswordModal from "@budibase/frontend-core/src/components/ChangePasswordModal.svelte"
import ThemeModal from "@/components/settings/ThemeModal.svelte" import ThemeModal from "@/components/settings/ThemeModal.svelte"
import APIKeyModal from "@/components/settings/APIKeyModal.svelte" import APIKeyModal from "@/components/settings/APIKeyModal.svelte"
import { UserAvatar } from "@budibase/frontend-core" import { UserAvatar } from "@budibase/frontend-core"
import { API } from "@/api"
let themeModal let themeModal
let profileModal let profileModal
@ -26,8 +27,8 @@
<ActionMenu align="right"> <ActionMenu align="right">
<div slot="control" class="user-dropdown"> <div slot="control" class="user-dropdown">
<UserAvatar user={$auth.user} showTooltip={false} /> <UserAvatar size="M" user={$auth.user} showTooltip={false} />
<Icon size="XL" name="ChevronDown" /> <Icon size="L" name="ChevronDown" />
</div> </div>
<MenuItem icon="UserEdit" on:click={() => profileModal.show()}> <MenuItem icon="UserEdit" on:click={() => profileModal.show()}>
My profile My profile
@ -60,10 +61,14 @@
<ThemeModal /> <ThemeModal />
</Modal> </Modal>
<Modal bind:this={profileModal}> <Modal bind:this={profileModal}>
<ProfileModal /> <ProfileModal {API} user={$auth.user} on:save={() => auth.getSelf()} />
</Modal> </Modal>
<Modal bind:this={updatePasswordModal}> <Modal bind:this={updatePasswordModal}>
<ChangePasswordModal /> <ChangePasswordModal
{API}
passwordMinLength={$admin.passwordMinLength}
on:save={() => auth.getSelf()}
/>
</Modal> </Modal>
<Modal bind:this={apiKeyModal}> <Modal bind:this={apiKeyModal}>
<APIKeyModal /> <APIKeyModal />
@ -75,7 +80,8 @@
flex-direction: row; flex-direction: row;
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
gap: var(--spacing-s); gap: var(--spacing-xs);
transition: filter 130ms ease-out;
} }
.user-dropdown:hover { .user-dropdown:hover {
cursor: pointer; cursor: pointer;

View File

@ -1,7 +1,7 @@
<script> <script>
import { Button, FancyForm, FancyInput } from "@budibase/bbui" import { Button, FancyForm, FancyInput } from "@budibase/bbui"
import PanelHeader from "./PanelHeader.svelte" import PanelHeader from "./PanelHeader.svelte"
import { APP_URL_REGEX } from "@/constants" import { Constants } from "@budibase/frontend-core"
export let disabled export let disabled
export let name = "" export let name = ""
@ -28,7 +28,7 @@
return "URL must be provided" return "URL must be provided"
} }
if (!APP_URL_REGEX.test(url)) { if (!Constants.APP_URL_REGEX.test(url)) {
return "Invalid URL" return "Invalid URL"
} }
} }

View File

@ -33,7 +33,6 @@
}, },
edit: { edit: {
width: "auto", width: "auto",
borderLeft: true,
displayName: "", displayName: "",
}, },
} }

View File

@ -10,8 +10,7 @@
Icon, Icon,
} from "@budibase/bbui" } from "@budibase/bbui"
import { groups, licensing } from "@/stores/portal" import { groups, licensing } from "@/stores/portal"
import { Constants } from "@budibase/frontend-core" import { Constants, emailValidator } from "@budibase/frontend-core"
import { emailValidator } from "@/helpers/validation"
import { capitalise } from "@/helpers" import { capitalise } from "@/helpers"
export let showOnboardingTypeModal export let showOnboardingTypeModal

View File

@ -8,8 +8,7 @@
Icon, Icon,
} from "@budibase/bbui" } from "@budibase/bbui"
import { groups, licensing, admin } from "@/stores/portal" import { groups, licensing, admin } from "@/stores/portal"
import { emailValidator } from "@/helpers/validation" import { emailValidator, Constants } from "@budibase/frontend-core"
import { Constants } from "@budibase/frontend-core"
import { capitalise } from "@/helpers" import { capitalise } from "@/helpers"
const BYTES_IN_MB = 1000000 const BYTES_IN_MB = 1000000

View File

@ -5,10 +5,10 @@ import getValidRoute from "../getValidRoute"
import { getRowActionButtonTemplates } from "@/templates/rowActions" import { getRowActionButtonTemplates } from "@/templates/rowActions"
const inline = async ({ tableOrView, permissions, screens }) => { const inline = async ({ tableOrView, permissions, screens }) => {
const heading = new Component("@budibase/standard-components/heading") const heading = new Component("@budibase/standard-components/textv2")
.instanceName("Table heading") .instanceName("Table heading")
.customProps({ .customProps({
text: tableOrView.name, text: `## ${tableOrView.name}`,
}) })
.gridDesktopColSpan(1, 13) .gridDesktopColSpan(1, 13)
.gridDesktopRowSpan(1, 3) .gridDesktopRowSpan(1, 3)

View File

@ -43,10 +43,10 @@ const modal = async ({ tableOrView, permissions, screens }) => {
.gridDesktopColSpan(7, 13) .gridDesktopColSpan(7, 13)
.gridDesktopRowSpan(1, 3) .gridDesktopRowSpan(1, 3)
const heading = new Component("@budibase/standard-components/heading") const heading = new Component("@budibase/standard-components/textv2")
.instanceName("Table heading") .instanceName("Table heading")
.customProps({ .customProps({
text: tableOrView.name, text: `## ${tableOrView.name}`,
}) })
.gridDesktopColSpan(1, 7) .gridDesktopColSpan(1, 7)
.gridDesktopRowSpan(1, 3) .gridDesktopRowSpan(1, 3)

View File

@ -40,10 +40,10 @@ const getTableScreenTemplate = ({
.gridDesktopColSpan(7, 13) .gridDesktopColSpan(7, 13)
.gridDesktopRowSpan(1, 3) .gridDesktopRowSpan(1, 3)
const heading = new Component("@budibase/standard-components/heading") const heading = new Component("@budibase/standard-components/textv2")
.instanceName("Table heading") .instanceName("Table heading")
.customProps({ .customProps({
text: tableOrView.name, text: `## ${tableOrView.name}`,
}) })
.gridDesktopColSpan(1, 7) .gridDesktopColSpan(1, 7)
.gridDesktopRowSpan(1, 3) .gridDesktopRowSpan(1, 3)

View File

@ -41,10 +41,10 @@ const sidePanel = async ({ tableOrView, permissions, screens }) => {
.gridDesktopColSpan(7, 13) .gridDesktopColSpan(7, 13)
.gridDesktopRowSpan(1, 3) .gridDesktopRowSpan(1, 3)
const heading = new Component("@budibase/standard-components/heading") const heading = new Component("@budibase/standard-components/textv2")
.instanceName("Table heading") .instanceName("Table heading")
.customProps({ .customProps({
text: tableOrView.name, text: `## ${tableOrView.name}`,
}) })
.gridDesktopColSpan(1, 7) .gridDesktopColSpan(1, 7)
.gridDesktopRowSpan(1, 3) .gridDesktopRowSpan(1, 3)

View File

@ -1048,6 +1048,7 @@
}, },
"text": { "text": {
"name": "Paragraph", "name": "Paragraph",
"deprecated": true,
"description": "A component for displaying paragraph text.", "description": "A component for displaying paragraph text.",
"icon": "TextParagraph", "icon": "TextParagraph",
"illegalChildren": ["section"], "illegalChildren": ["section"],
@ -1171,6 +1172,7 @@
}, },
"heading": { "heading": {
"name": "Headline", "name": "Headline",
"deprecated": true,
"icon": "TextBold", "icon": "TextBold",
"description": "A component for displaying heading text", "description": "A component for displaying heading text",
"illegalChildren": ["section"], "illegalChildren": ["section"],
@ -7582,6 +7584,15 @@
"name": "Table", "name": "Table",
"icon": "Table", "icon": "Table",
"styles": ["size"], "styles": ["size"],
"cssVariables": [{
"label": "Header color",
"variable": "--custom-header-cell-background",
"type": "color"
}, {
"label": "Stripe color",
"variable": "--custom-stripe-cell-background",
"type": "color"
}],
"size": { "size": {
"width": 600, "width": 600,
"height": 400 "height": 400
@ -7689,7 +7700,7 @@
}, },
{ {
"type": "boolean", "type": "boolean",
"label": "High contrast", "label": "Striped rows",
"key": "stripeRows", "key": "stripeRows",
"defaultValue": false "defaultValue": false
}, },
@ -7990,5 +8001,62 @@
} }
] ]
} }
},
"textv2": {
"name": "Text",
"description": "A component for displaying text",
"icon": "Text",
"size": {
"width": 400,
"height": 24
},
"settings": [
{
"type": "text/multiline",
"label": "Text (Markdown supported)",
"key": "text",
"wide": true
},
{
"type": "select",
"label": "Alignment",
"key": "align",
"defaultValue": "left",
"showInBar": true,
"barStyle": "buttons",
"options": [
{
"label": "Left",
"value": "left",
"barIcon": "TextAlignLeft",
"barTitle": "Align left"
},
{
"label": "Center",
"value": "center",
"barIcon": "TextAlignCenter",
"barTitle": "Align center"
},
{
"label": "Right",
"value": "right",
"barIcon": "TextAlignRight",
"barTitle": "Align right"
},
{
"label": "Justify",
"value": "justify",
"barIcon": "TextAlignJustify",
"barTitle": "Justify text"
}
]
},
{
"type": "color",
"label": "Color",
"key": "color",
"showInBar": true
}
]
} }
} }

View File

@ -4,6 +4,7 @@
import { Heading, Icon, clickOutside } from "@budibase/bbui" import { Heading, Icon, clickOutside } from "@budibase/bbui"
import { Constants } from "@budibase/frontend-core" import { Constants } from "@budibase/frontend-core"
import NavItem from "./NavItem.svelte" import NavItem from "./NavItem.svelte"
import UserMenu from "./UserMenu.svelte"
const sdk = getContext("sdk") const sdk = getContext("sdk")
const { const {
@ -13,7 +14,6 @@
builderStore, builderStore,
sidePanelStore, sidePanelStore,
modalStore, modalStore,
appStore,
} = sdk } = sdk
const context = getContext("context") const context = getContext("context")
const navStateStore = writable({}) const navStateStore = writable({})
@ -34,6 +34,7 @@
export let navWidth export let navWidth
export let pageWidth export let pageWidth
export let logoLinkUrl export let logoLinkUrl
export let logoHeight
export let openLogoLinkInNewTab export let openLogoLinkInNewTab
export let textAlign export let textAlign
export let embedded = false export let embedded = false
@ -70,6 +71,7 @@
$: navStyle = getNavStyle( $: navStyle = getNavStyle(
navBackground, navBackground,
navTextColor, navTextColor,
logoHeight,
$context.device.width, $context.device.width,
$context.device.height $context.device.height
) )
@ -156,11 +158,6 @@
return !url.startsWith("http") ? `http://${url}` : url return !url.startsWith("http") ? `http://${url}` : url
} }
const navigateToPortal = () => {
if ($builderStore.inBuilder) return
window.location.href = "/builder/apps"
}
const getScreenXOffset = (navigation, mobile) => { const getScreenXOffset = (navigation, mobile) => {
if (navigation !== "Left") { if (navigation !== "Left") {
return 0 return 0
@ -175,7 +172,13 @@
} }
} }
const getNavStyle = (backgroundColor, textColor, width, height) => { const getNavStyle = (
backgroundColor,
textColor,
logoHeight,
width,
height
) => {
let style = `--width:${width}px; --height:${height}px;` let style = `--width:${width}px; --height:${height}px;`
if (backgroundColor) { if (backgroundColor) {
style += `--navBackground:${backgroundColor};` style += `--navBackground:${backgroundColor};`
@ -183,6 +186,7 @@
if (textColor) { if (textColor) {
style += `--navTextColor:${textColor};` style += `--navTextColor:${textColor};`
} }
style += `--logoHeight:${logoHeight || 24}px;`
return style return style
} }
@ -267,13 +271,8 @@
{/if} {/if}
</div> </div>
{#if !embedded} {#if !embedded}
<div class="portal"> <div class="user top">
<Icon <UserMenu compact />
hoverable
name="Apps"
on:click={navigateToPortal}
disabled={$appStore.isDevApp}
/>
</div> </div>
{/if} {/if}
</div> </div>
@ -297,13 +296,11 @@
{navStateStore} {navStateStore}
/> />
{/each} {/each}
<div class="close"> </div>
<Icon {/if}
hoverable {#if !embedded}
name="Close" <div class="user left">
on:click={() => (mobileOpen = false)} <UserMenu />
/>
</div>
</div> </div>
{/if} {/if}
</div> </div>
@ -394,21 +391,15 @@
top: 0; top: 0;
left: 0; left: 0;
} }
.layout--top .nav-wrapper {
border-bottom: 1px solid var(--spectrum-global-color-gray-300);
}
.layout--left .nav-wrapper {
border-right: 1px solid var(--spectrum-global-color-gray-300);
}
.nav { .nav {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
align-items: stretch; align-items: stretch;
padding: 24px 32px 20px 32px; padding: 18px 32px 18px 32px;
max-width: 100%; max-width: 100%;
gap: var(--spacing-xl); gap: var(--spacing-xs);
} }
.nav :global(.spectrum-Icon) { .nav :global(.spectrum-Icon) {
color: var(--navTextColor); color: var(--navTextColor);
@ -522,7 +513,7 @@
flex: 1 1 auto; flex: 1 1 auto;
} }
.logo img { .logo img {
height: 36px; height: var(--logoHeight);
} }
.logo :global(h1) { .logo :global(h1) {
font-weight: 600; font-weight: 600;
@ -532,11 +523,8 @@
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.portal {
display: grid;
place-items: center;
}
.links { .links {
flex: 1 0 auto;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: flex-start;
@ -544,45 +532,38 @@
gap: var(--spacing-xl); gap: var(--spacing-xl);
margin-top: var(--spacing-xl); margin-top: var(--spacing-xl);
} }
.close {
display: none;
position: absolute;
top: var(--spacing-xl);
right: var(--spacing-xl);
}
.mobile-click-handler { .mobile-click-handler {
display: none; display: none;
} }
/* Left overrides for both desktop and mobile */
.nav--left {
overflow-y: auto;
}
/* Desktop nav overrides */ /* Desktop nav overrides */
.desktop.layout--left .layout-body { .desktop.layout--left .layout-body {
flex-direction: row; flex-direction: row;
overflow: hidden; overflow: hidden;
} }
.desktop.layout--left .nav-wrapper {
border-bottom: none;
}
.desktop.layout--left .main-wrapper { .desktop.layout--left .main-wrapper {
height: 100%; height: 100%;
overflow: auto; overflow: auto;
} }
.desktop.layout--left .links {
overflow-y: auto;
}
.desktop .nav--left { .desktop .nav--left {
width: 250px; width: 250px;
padding: var(--spacing-xl); padding: var(--spacing-xl);
} }
.desktop .nav--left .links { .desktop .nav--left .links {
margin-top: var(--spacing-m); margin-top: var(--spacing-m);
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
align-items: stretch; align-items: stretch;
gap: var(--spacing-xs);
} }
.desktop .nav--left .link { .desktop .nav--left .user.top,
font-size: var(--spectrum-global-dimension-font-size-150); .desktop .nav--top .user.left {
display: none;
} }
/* Mobile nav overrides */ /* Mobile nav overrides */
@ -591,13 +572,9 @@
top: 0; top: 0;
left: 0; left: 0;
box-shadow: 0 0 8px -1px rgba(0, 0, 0, 0.075); box-shadow: 0 0 8px -1px rgba(0, 0, 0, 0.075);
border-bottom: 1px solid var(--spectrum-global-color-gray-300);
border-right: none;
} }
.mobile .user.left {
/* Show close button in drawer */ display: none;
.mobile .close {
display: block;
} }
/* Force standard top bar */ /* Force standard top bar */
@ -635,6 +612,7 @@
left: -250px; left: -250px;
transform: translateX(0); transform: translateX(0);
width: 250px; width: 250px;
max-width: 75%;
transition: transform 0.26s ease-in-out, opacity 0.26s ease-in-out; transition: transform 0.26s ease-in-out, opacity 0.26s ease-in-out;
height: var(--height); height: var(--height);
opacity: 0; opacity: 0;
@ -645,10 +623,10 @@
align-items: stretch; align-items: stretch;
padding: var(--spacing-xl); padding: var(--spacing-xl);
overflow-y: auto; overflow-y: auto;
gap: var(--spacing-xs);
} }
.mobile .link { .mobile .links :global(a) {
width: calc(100% - 30px); flex: 0 0 auto;
font-size: 120%;
} }
.mobile .links.visible { .mobile .links.visible {
opacity: 1; opacity: 1;

View File

@ -2,6 +2,7 @@
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import active from "svelte-spa-router/active" import active from "svelte-spa-router/active"
import { Icon } from "@budibase/bbui" import { Icon } from "@budibase/bbui"
import { builderStore, screenStore } from "@/stores"
export let type export let type
export let url export let url
@ -16,7 +17,16 @@
let renderKey let renderKey
$: expanded = !!$navStateStore[text] $: isBuilderActive = testUrl => {
return (
$builderStore.inBuilder &&
testUrl &&
testUrl === $screenStore.activeScreen?.routing?.route
)
}
$: builderActive = isBuilderActive(url)
$: containsActiveLink = (subLinks || []).some(x => isBuilderActive(x.url))
$: expanded = !!$navStateStore[text] || containsActiveLink
$: renderLeftNav = leftNav || mobile $: renderLeftNav = leftNav || mobile
$: icon = !renderLeftNav || expanded ? "ChevronDown" : "ChevronRight" $: icon = !renderLeftNav || expanded ? "ChevronDown" : "ChevronRight"
@ -47,7 +57,7 @@
href="#{url}" href="#{url}"
on:click={onClickLink} on:click={onClickLink}
use:active={url} use:active={url}
class:active={false} class:builderActive
> >
{text} {text}
</a> </a>
@ -73,6 +83,9 @@
href="#{subLink.url}" href="#{subLink.url}"
on:click={onClickLink} on:click={onClickLink}
use:active={subLink.url} use:active={subLink.url}
class:active={false}
class:builderActive={isBuilderActive(subLink.url)}
class="sublink"
> >
{subLink.text} {subLink.text}
</a> </a>
@ -91,22 +104,29 @@
<style> <style>
/* Generic styles */ /* Generic styles */
a, a,
.dropdown .text {
padding: 4px 8px;
border-radius: 4px;
}
a,
.text span { .text span {
opacity: 0.75; opacity: 0.75;
color: var(--navTextColor); color: var(--navTextColor);
font-size: var(--spectrum-global-dimension-font-size-200); font-size: var(--spectrum-global-dimension-font-size-150);
transition: opacity 130ms ease-out; transition: opacity 130ms ease-out;
font-weight: 600;
user-select: none; user-select: none;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
a.active { a.active:not(.sublink),
a.builderActive:not(.sublink),
.dropdown.left a.sublink.active,
.dropdown.left a.sublink.builderActive {
background: rgba(0, 0, 0, 0.15);
opacity: 1; opacity: 1;
} }
a:hover, a:hover,
.dropdown:not(.left.expanded):hover .text, .text:hover span {
.text:hover {
cursor: pointer; cursor: pointer;
opacity: 1; opacity: 1;
} }
@ -167,8 +187,4 @@
.dropdown.dropdown.left.expanded .sublinks { .dropdown.dropdown.left.expanded .sublinks {
display: contents; display: contents;
} }
.dropdown.left a {
padding-top: 0;
padding-bottom: 0;
}
</style> </style>

View File

@ -1,105 +1,44 @@
<script> <script lang="ts">
import { getContext } from "svelte" import { getContext } from "svelte"
import { MarkdownViewer } from "@budibase/bbui"
export let text: string = ""
export let color: string | undefined = undefined
export let align: "left" | "center" | "right" | "justify" = "left"
const { styleable, builderStore } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
const { styleable } = getContext("sdk")
export let text // Add in certain settings to styles
export let color $: styles = enrichStyles($component.styles, color, align)
export let align
export let bold
export let italic
export let underline
export let size
let node const enrichStyles = (
let touched = false styles: any,
colorStyle: typeof color,
$: $component.editing && node?.focus() alignStyle: typeof align
$: placeholder = $builderStore.inBuilder && !text && !$component.editing ) => {
$: componentText = getComponentText(text, $builderStore, $component) let additions: Record<string, string> = {
$: sizeClass = `spectrum-Body--size${size || "M"}` "text-align": alignStyle,
$: alignClass = `align--${align || "left"}`
// Add color styles to main styles object, otherwise the styleable helper
// overrides the color when it's passed as inline style.
$: styles = enrichStyles($component.styles, color)
const getComponentText = (text, builderState, componentState) => {
if (!builderState.inBuilder || componentState.editing) {
return text || ""
} }
return text || componentState.name || "Placeholder text" if (colorStyle) {
} additions.color = colorStyle
const enrichStyles = (styles, color) => {
if (!color) {
return styles
} }
return { return {
...styles, ...styles,
normal: { normal: {
...styles?.normal, ...styles.normal,
color, ...additions,
}, },
} }
} }
// Convert contenteditable HTML to text and save
const updateText = e => {
if (touched) {
builderStore.actions.updateProp("text", e.target.textContent)
}
touched = false
}
</script> </script>
{#key $component.editing} <div use:styleable={styles}>
<p <MarkdownViewer value={text} />
bind:this={node} </div>
contenteditable={$component.editing}
use:styleable={styles}
class:placeholder
class:bold
class:italic
class:underline
class="spectrum-Body {sizeClass} {alignClass}"
on:blur={$component.editing ? updateText : null}
on:input={() => (touched = true)}
>
{componentText}
</p>
{/key}
<style> <style>
p { div :global(img) {
display: inline-block; max-width: 100%;
white-space: pre-wrap;
margin: 0;
}
.placeholder {
font-style: italic;
color: var(--spectrum-global-color-gray-600);
}
.bold {
font-weight: 600;
}
.italic {
font-style: italic;
}
.underline {
text-decoration: underline;
}
.align--left {
text-align: left;
}
.align--center {
text-align: center;
}
.align--right {
text-align: right;
}
.align--justify {
text-align: justify;
} }
</style> </style>

View File

@ -0,0 +1,144 @@
<script lang="ts">
import { ActionMenu, Icon, MenuItem, Modal } from "@budibase/bbui"
import {
UserAvatar,
ProfileModal,
ChangePasswordModal,
} from "@budibase/frontend-core"
import { getContext } from "svelte"
import { type User, type ContextUser, isSSOUser } from "@budibase/types"
import { sdk } from "@budibase/shared-core"
import { API } from "@/api"
export let compact: boolean = false
const { authStore, environmentStore, notificationStore, appStore } =
getContext("sdk")
let profileModal: any
let changePasswordModal: any
$: text = getText($authStore)
$: isBuilder = sdk.users.hasBuilderPermissions($authStore)
$: isSSO = $authStore != null && isSSOUser($authStore)
$: isOwner = $authStore?.accountPortalAccess && $environmentStore.cloud
$: embedded = $appStore.embedded || $appStore.inIframe
const getText = (user?: User | ContextUser): string => {
if (!user) {
return ""
}
if (user.firstName) {
let text = user.firstName
if (user.lastName) {
text += ` ${user.lastName}`
}
return text
} else {
return user.email
}
}
const goToPortal = () => {
window.location.href = isBuilder ? "/builder/portal/apps" : "/builder/apps"
}
</script>
{#if $authStore}
<ActionMenu align={compact ? "right" : "left"}>
<svelte:fragment slot="control">
<div class="container">
<UserAvatar user={$authStore} size="M" showTooltip={false} />
{#if !compact}
<div class="text">
<div class="name">
{text}
</div>
</div>
{/if}
<Icon size="L" name="ChevronDown" />
</div>
</svelte:fragment>
<MenuItem icon="UserEdit" on:click={() => profileModal?.show()}>
My profile
</MenuItem>
{#if !isSSO}
<MenuItem
icon="LockClosed"
on:click={() => {
if (isOwner) {
window.location.href = `${$environmentStore.accountPortalUrl}/portal/account`
} else {
changePasswordModal?.show()
}
}}
>
Update password
</MenuItem>
{/if}
<MenuItem icon="Apps" on:click={goToPortal} disabled={embedded}>
Go to portal
</MenuItem>
<MenuItem
icon="LogOut"
on:click={authStore.actions.logOut}
disabled={embedded}
>
Log out
</MenuItem>
</ActionMenu>
<Modal bind:this={profileModal}>
<ProfileModal
{API}
user={$authStore}
on:save={() => authStore.actions.fetchUser()}
notifySuccess={notificationStore.actions.success}
notifyError={notificationStore.actions.error}
/>
</Modal>
<Modal bind:this={changePasswordModal}>
<ChangePasswordModal
{API}
passwordMinLength={$environmentStore.passwordMinLength}
on:save={() => authStore.actions.logOut()}
notifySuccess={notificationStore.actions.success}
notifyError={notificationStore.actions.error}
/>
</Modal>
{/if}
<style>
.container {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: var(--spacing-xs);
transition: filter 130ms ease-out;
overflow: hidden;
}
.text {
flex-direction: column;
align-items: flex-start;
justify-content: center;
color: var(--navTextColor);
display: flex;
margin-left: var(--spacing-xs);
overflow: hidden;
}
.name {
font-weight: 600;
font-size: 14px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
width: 100%;
}
.container:hover {
cursor: pointer;
filter: brightness(110%);
}
</style>

View File

@ -125,9 +125,9 @@
order={0} order={0}
> >
<BlockComponent <BlockComponent
type="heading" type="textv2"
props={{ props={{
text: title, text: title ? `## ${title}` : "",
}} }}
order={0} order={0}
/> />

View File

@ -148,7 +148,10 @@
}} }}
order={0} order={0}
> >
<BlockComponent type="heading" props={{ text: step.title }} /> <BlockComponent
type="textv2"
props={{ text: `## ${step.title}` }}
/>
{#if buttonPosition === "top"} {#if buttonPosition === "top"}
<BlockComponent <BlockComponent
type="buttongroup" type="buttongroup"
@ -157,7 +160,7 @@
{/if} {/if}
</BlockComponent> </BlockComponent>
</BlockComponent> </BlockComponent>
<BlockComponent type="text" props={{ text: step.desc }} order={1} /> <BlockComponent type="textv2" props={{ text: step.desc }} order={1} />
<BlockComponent type="container" order={2}> <BlockComponent type="container" order={2}>
<div <div

View File

@ -190,7 +190,7 @@
}} }}
/> />
<BlockComponent <BlockComponent
type="text" type="textv2"
order={1} order={1}
props={{ props={{
text: "Select a row to view its fields", text: "Select a row to view its fields",

View File

@ -74,11 +74,11 @@
order={0} order={0}
> >
<BlockComponent <BlockComponent
type="heading" type="textv2"
props={{ text: title || "" }} props={{ text: title ? `## ${title}` : "" }}
order={0} order={0}
/> />
{#if buttonPosition == "top"} {#if buttonPosition === "top"}
<BlockComponent <BlockComponent
type="buttongroup" type="buttongroup"
props={{ props={{
@ -93,7 +93,7 @@
</BlockComponent> </BlockComponent>
{/if} {/if}
{#if description} {#if description}
<BlockComponent type="text" props={{ text: description }} order={1} /> <BlockComponent type="textv2" props={{ text: description }} order={1} />
{/if} {/if}
<BlockComponent type="container"> <BlockComponent type="container">
<div class="form-block fields" class:mobile={$context.device.mobile}> <div class="form-block fields" class:mobile={$context.device.mobile}>

View File

@ -184,9 +184,9 @@
order={0} order={0}
> >
<BlockComponent <BlockComponent
type="heading" type="textv2"
props={{ props={{
text: title, text: title ? `## ${title}` : "",
}} }}
order={0} order={0}
/> />

View File

@ -0,0 +1,105 @@
<script>
import { getContext } from "svelte"
const { styleable, builderStore } = getContext("sdk")
const component = getContext("component")
export let text
export let color
export let align
export let bold
export let italic
export let underline
export let size
let node
let touched = false
$: $component.editing && node?.focus()
$: placeholder = $builderStore.inBuilder && !text && !$component.editing
$: componentText = getComponentText(text, $builderStore, $component)
$: sizeClass = `spectrum-Body--size${size || "M"}`
$: alignClass = `align--${align || "left"}`
// Add color styles to main styles object, otherwise the styleable helper
// overrides the color when it's passed as inline style.
$: styles = enrichStyles($component.styles, color)
const getComponentText = (text, builderState, componentState) => {
if (!builderState.inBuilder || componentState.editing) {
return text || ""
}
return text || componentState.name || "Placeholder text"
}
const enrichStyles = (styles, color) => {
if (!color) {
return styles
}
return {
...styles,
normal: {
...styles?.normal,
color,
},
}
}
// Convert contenteditable HTML to text and save
const updateText = e => {
if (touched) {
builderStore.actions.updateProp("text", e.target.textContent)
}
touched = false
}
</script>
{#key $component.editing}
<p
bind:this={node}
contenteditable={$component.editing}
use:styleable={styles}
class:placeholder
class:bold
class:italic
class:underline
class="spectrum-Body {sizeClass} {alignClass}"
on:blur={$component.editing ? updateText : null}
on:input={() => (touched = true)}
>
{componentText}
</p>
{/key}
<style>
p {
display: inline-block;
white-space: pre-wrap;
margin: 0;
}
.placeholder {
font-style: italic;
color: var(--spectrum-global-color-gray-600);
}
.bold {
font-weight: 600;
}
.italic {
font-style: italic;
}
.underline {
text-decoration: underline;
}
.align--left {
text-align: left;
}
.align--center {
text-align: center;
}
.align--right {
text-align: right;
}
.align--justify {
text-align: justify;
}
</style>

View File

@ -20,10 +20,8 @@ export { default as screenslot } from "./ScreenSlot.svelte"
export { default as button } from "./Button.svelte" export { default as button } from "./Button.svelte"
export { default as buttongroup } from "./ButtonGroup.svelte" export { default as buttongroup } from "./ButtonGroup.svelte"
export { default as repeater } from "./Repeater.svelte" export { default as repeater } from "./Repeater.svelte"
export { default as text } from "./Text.svelte"
export { default as layout } from "./Layout.svelte" export { default as layout } from "./Layout.svelte"
export { default as link } from "./Link.svelte" export { default as link } from "./Link.svelte"
export { default as heading } from "./Heading.svelte"
export { default as image } from "./Image.svelte" export { default as image } from "./Image.svelte"
export { default as embed } from "./Embed.svelte" export { default as embed } from "./Embed.svelte"
export { default as icon } from "./Icon.svelte" export { default as icon } from "./Icon.svelte"
@ -37,6 +35,7 @@ export { default as embeddedmap } from "./embedded-map/EmbeddedMap.svelte"
export { default as sidepanel } from "./SidePanel.svelte" export { default as sidepanel } from "./SidePanel.svelte"
export { default as modal } from "./Modal.svelte" export { default as modal } from "./Modal.svelte"
export { default as gridblock } from "./GridBlock.svelte" export { default as gridblock } from "./GridBlock.svelte"
export { default as textv2 } from "./Text.svelte"
export * from "./charts" export * from "./charts"
export * from "./forms" export * from "./forms"
export * from "./blocks" export * from "./blocks"
@ -50,3 +49,5 @@ export { default as cardhorizontal } from "./deprecated/CardHorizontal.svelte"
export { default as stackedlist } from "./deprecated/StackedList.svelte" export { default as stackedlist } from "./deprecated/StackedList.svelte"
export { default as card } from "./deprecated/Card.svelte" export { default as card } from "./deprecated/Card.svelte"
export { default as section } from "./deprecated/Section.svelte" export { default as section } from "./deprecated/Section.svelte"
export { default as text } from "./deprecated/Text.svelte"
export { default as heading } from "./deprecated/Heading.svelte"

View File

@ -1,6 +1,7 @@
import { Writable } from "svelte" import { Writable } from "svelte"
import { Component, FieldGroupContext, FormContext, SDK } from "@/types" import { Component, FieldGroupContext, FormContext } from "@/types"
import { Readable } from "svelte/store" import { Readable } from "svelte/store"
import { SDK } from "@/index.ts"
declare module "svelte" { declare module "svelte" {
export function getContext(key: "sdk"): SDK export function getContext(key: "sdk"): SDK

View File

@ -1,6 +1,7 @@
import ClientApp from "./components/ClientApp.svelte" import ClientApp from "./components/ClientApp.svelte"
import UpdatingApp from "./components/UpdatingApp.svelte" import UpdatingApp from "./components/UpdatingApp.svelte"
import { import {
authStore,
builderStore, builderStore,
appStore, appStore,
blockStore, blockStore,
@ -11,6 +12,7 @@ import {
hoverStore, hoverStore,
stateStore, stateStore,
routeStore, routeStore,
notificationStore,
} from "@/stores" } from "@/stores"
import { get } from "svelte/store" import { get } from "svelte/store"
import { initWebsocket } from "@/websocket" import { initWebsocket } from "@/websocket"
@ -26,6 +28,8 @@ import {
UIComponentError, UIComponentError,
CustomComponent, CustomComponent,
} from "@budibase/types" } from "@budibase/types"
import { ActionTypes } from "@/constants"
import { APIClient } from "@budibase/frontend-core"
// Provide svelte and svelte/internal as globals for custom components // Provide svelte and svelte/internal as globals for custom components
import * as svelte from "svelte" import * as svelte from "svelte"
@ -37,7 +41,6 @@ window.svelte = svelte
// Initialise spectrum icons // Initialise spectrum icons
// eslint-disable-next-line local-rules/no-budibase-imports // eslint-disable-next-line local-rules/no-budibase-imports
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-vite.js" import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-vite.js"
loadSpectrumIcons() loadSpectrumIcons()
// Extend global window scope // Extend global window scope
@ -74,6 +77,20 @@ declare global {
export type Context = Readable<Record<string, any>> export type Context = Readable<Record<string, any>>
export interface SDK {
API: APIClient
styleable: any
Provider: any
ActionTypes: typeof ActionTypes
fetchDatasourceSchema: any
generateGoldenSample: any
builderStore: typeof builderStore
authStore: typeof authStore
notificationStore: typeof notificationStore
environmentStore: typeof environmentStore
appStore: typeof appStore
}
let app: ClientApp let app: ClientApp
const loadBudibase = async () => { const loadBudibase = async () => {

View File

@ -6,6 +6,7 @@ const initialState = {
isDevApp: false, isDevApp: false,
clientLoadTime: window.INIT_TIME ? Date.now() - window.INIT_TIME : null, clientLoadTime: window.INIT_TIME ? Date.now() - window.INIT_TIME : null,
embedded: false, embedded: false,
inIframe: window.self !== window.top,
} }
const createAppStore = () => { const createAppStore = () => {

View File

@ -1,38 +1,52 @@
import { API } from "@/api" import { API } from "@/api"
import { writable } from "svelte/store" import { writable } from "svelte/store"
import {
AppSelfResponse,
ContextUserMetadata,
GetGlobalSelfResponse,
} from "@budibase/types"
type AuthState = ContextUserMetadata | GetGlobalSelfResponse | undefined
const createAuthStore = () => { const createAuthStore = () => {
const store = writable<{ const store = writable<AuthState>()
csrfToken?: string
} | null>(null) const hasAppSelfUser = (
user: AppSelfResponse | null
): user is ContextUserMetadata => {
return user != null && "_id" in user
}
// Fetches the user object if someone is logged in and has reloaded the page // Fetches the user object if someone is logged in and has reloaded the page
const fetchUser = async () => { const fetchUser = async () => {
let globalSelf = null let globalSelf, appSelf
let appSelf = null
// First try and get the global user, to see if we are logged in at all // First try and get the global user, to see if we are logged in at all
try { try {
globalSelf = await API.fetchBuilderSelf() globalSelf = await API.fetchBuilderSelf()
} catch (error) { } catch (error) {
store.set(null) store.set(undefined)
return return
} }
// Then try and get the user for this app to provide via context // Then try and get the user for this app to provide via context
try { try {
appSelf = await API.fetchSelf() const res = await API.fetchSelf()
if (hasAppSelfUser(res)) {
appSelf = res
}
} catch (error) { } catch (error) {
// Swallow // Swallow
} }
// Use the app self if present, otherwise fallback to the global self // Use the app self if present, otherwise fallback to the global self
store.set(appSelf || globalSelf || null) store.set(appSelf || globalSelf)
} }
const logOut = async () => { const logOut = async () => {
try { try {
await API.logOut() await API.logOut()
window.location.href = "/"
} catch (error) { } catch (error) {
// Do nothing // Do nothing
} }

View File

@ -1,13 +1,23 @@
import { API } from "@/api" import { API } from "@/api"
import { writable } from "svelte/store" import { writable } from "svelte/store"
import type { GetEnvironmentResponse } from "@budibase/types"
const initialState = { interface EnvironmentState extends GetEnvironmentResponse {
loaded: false, loaded: boolean
}
const initialState: EnvironmentState = {
multiTenancy: false,
offlineMode: false,
cloud: false, cloud: false,
disableAccountPortal: false,
isDev: false,
maintenance: [],
loaded: false,
} }
const createEnvironmentStore = () => { const createEnvironmentStore = () => {
const store = writable(initialState) const store = writable<EnvironmentState>(initialState)
const actions = { const actions = {
fetchEnvironment: async () => { fetchEnvironment: async () => {

View File

@ -1,4 +1,3 @@
export * from "./components" export * from "./components"
export * from "./fields" export * from "./fields"
export * from "./forms" export * from "./forms"
export * from "./sdk"

View File

@ -1,24 +0,0 @@
import { ActionTypes } from "@/constants"
import { APIClient } from "@budibase/frontend-core"
import { Readable } from "svelte/store"
export interface SDK {
API: APIClient
styleable: any
Provider: any
ActionTypes: typeof ActionTypes
fetchDatasourceSchema: any
generateGoldenSample: any
builderStore: Readable<{
inBuilder: boolean
}> & {
actions: {
highlightSetting: (key: string) => void
addParentComponent: (
componentId: string,
fullAncestorType: string
) => void
updateProp: (key: string, value: any) => void
}
}
}

View File

@ -0,0 +1,43 @@
<script lang="ts">
import { ModalContent, Body, notifications } from "@budibase/bbui"
import PasswordRepeatInput from "./PasswordRepeatInput.svelte"
import type { APIClient } from "@budibase/frontend-core"
import { createEventDispatcher } from "svelte"
export let API: APIClient
export let passwordMinLength: string | undefined = undefined
export let notifySuccess = notifications.success
export let notifyError = notifications.error
const dispatch = createEventDispatcher()
let password: string = ""
let error: string = ""
const updatePassword = async () => {
try {
await API.updateSelf({ password })
notifySuccess("Password changed successfully")
dispatch("save")
} catch (error) {
notifyError("Failed to update password")
}
}
const handleKeydown = (evt: KeyboardEvent) => {
if (evt.key === "Enter" && !error && password) {
updatePassword()
}
}
</script>
<svelte:window on:keydown={handleKeydown} />
<ModalContent
title="Update password"
confirmText="Update password"
onConfirm={updatePassword}
disabled={!!error || !password}
>
<Body size="S">Enter your new password below.</Body>
<PasswordRepeatInput bind:password bind:error minLength={passwordMinLength} />
</ModalContent>

View File

@ -1,20 +1,14 @@
<script> <script>
import { FancyForm, FancyInput } from "@budibase/bbui" import { FancyForm, FancyInput } from "@budibase/bbui"
import { import { createValidationStore, requiredValidator } from "../utils/validation"
createValidationStore,
requiredValidator,
} from "@/helpers/validation"
import { admin } from "@/stores/portal"
export let password export let password
export let passwordForm
export let error export let error
export let minLength = "12"
$: passwordMinLength = $admin.passwordMinLength ?? 12
const validatePassword = value => { const validatePassword = value => {
if (!value || value.length < passwordMinLength) { if (!value || value.length < minLength) {
return `Please enter at least ${passwordMinLength} characters. We recommend using machine generated or random passwords.` return `Please enter at least ${minLength} characters. We recommend using machine generated or random passwords.`
} }
return null return null
} }
@ -41,7 +35,7 @@
firstPasswordError firstPasswordError
</script> </script>
<FancyForm bind:this={passwordForm}> <FancyForm>
<FancyInput <FancyInput
label="Password" label="Password"
type="password" type="password"

View File

@ -0,0 +1,39 @@
<script lang="ts">
import { writable } from "svelte/store"
import { ModalContent, Body, Input, notifications } from "@budibase/bbui"
import type { User, ContextUser } from "@budibase/types"
import type { APIClient } from "@budibase/frontend-core"
import { createEventDispatcher } from "svelte"
export let user: User | ContextUser | undefined = undefined
export let API: APIClient
export let notifySuccess = notifications.success
export let notifyError = notifications.error
const dispatch = createEventDispatcher()
const values = writable({
firstName: user?.firstName,
lastName: user?.lastName,
})
const updateInfo = async () => {
try {
await API.updateSelf($values)
notifySuccess("Information updated successfully")
dispatch("save")
} catch (error) {
console.error(error)
notifyError("Failed to update information")
}
}
</script>
<ModalContent title="My profile" confirmText="Save" onConfirm={updateInfo}>
<Body size="S">
Personalise the platform by adding your first name and last name.
</Body>
<Input disabled value={user?.email || ""} label="Email" />
<Input bind:value={$values.firstName} label="First name" />
<Input bind:value={$values.lastName} label="Last name" />
</ModalContent>

View File

@ -56,7 +56,7 @@
rowIdx={row?.__idx} rowIdx={row?.__idx}
metadata={row?.__metadata?.row} metadata={row?.__metadata?.row}
> >
<div class="gutter"> <div class="gutter" class:selectable={$config.canSelectRows}>
{#if $$slots.default} {#if $$slots.default}
<slot /> <slot />
{:else} {:else}
@ -116,12 +116,9 @@
margin: 3px 0 0 0; margin: 3px 0 0 0;
} }
.number { .number {
color: val(--cell-font-color, var(--spectrum-global-color-gray-500)); color: var(--spectrum-global-color-gray-500);
}
.checkbox.visible,
.number.visible {
display: flex;
} }
.delete, .delete,
.expand { .expand {
margin-right: 4px; margin-right: 4px;
@ -137,4 +134,11 @@
.delete:hover :global(.spectrum-Icon) { .delete:hover :global(.spectrum-Icon) {
color: var(--spectrum-global-color-red-600) !important; color: var(--spectrum-global-color-red-600) !important;
} }
/* Visibility of checkbox and number */
.gutter.selectable .checkbox.visible,
.number.visible,
.gutter:not(.selectable) .number {
display: flex;
}
</style> </style>

View File

@ -303,9 +303,11 @@
/> />
{/if} {/if}
<div class="column-icon"> {#if !$config.quiet}
<Icon size="S" name={getColumnIcon(column)} /> <div class="column-icon">
</div> <Icon size="S" name={getColumnIcon(column)} />
</div>
{/if}
<div class="search-icon" on:click={startSearching}> <div class="search-icon" on:click={startSearching}>
<Icon hoverable size="S" name="Search" /> <Icon hoverable size="S" name="Search" />
</div> </div>
@ -431,7 +433,7 @@
.header-cell :global(.cell) { .header-cell :global(.cell) {
padding: 0 var(--cell-padding); padding: 0 var(--cell-padding);
gap: calc(2 * var(--cell-spacing)); gap: calc(2 * var(--cell-spacing));
background: var(--grid-background-alt); background: var(--header-cell-background);
} }
/* Icon colors */ /* Icon colors */
@ -463,6 +465,7 @@
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
font-weight: bold;
} }
.header-cell.searching .name { .header-cell.searching .name {
opacity: 0; opacity: 0;

View File

@ -217,6 +217,10 @@
--accent-color: var(--primaryColor, var(--spectrum-global-color-blue-400)); --accent-color: var(--primaryColor, var(--spectrum-global-color-blue-400));
--grid-background: var(--spectrum-global-color-gray-50); --grid-background: var(--spectrum-global-color-gray-50);
--grid-background-alt: var(--spectrum-global-color-gray-100); --grid-background-alt: var(--spectrum-global-color-gray-100);
--header-cell-background: var(
--custom-header-cell-background,
var(--grid-background-alt)
);
--cell-background: var(--grid-background); --cell-background: var(--grid-background);
--cell-background-hover: var(--grid-background-alt); --cell-background-hover: var(--grid-background-alt);
--cell-background-alt: var(--cell-background); --cell-background-alt: var(--cell-background);
@ -246,7 +250,10 @@
cursor: grabbing !important; cursor: grabbing !important;
} }
.grid.stripe { .grid.stripe {
--cell-background-alt: var(--spectrum-global-color-gray-75); --cell-background-alt: var(
--custom-stripe-cell-background,
var(--spectrum-global-color-gray-75)
);
} }
/* Data layers */ /* Data layers */
@ -352,11 +359,18 @@
/* Overrides for quiet */ /* Overrides for quiet */
.grid.quiet :global(.grid-data-content .row > .cell:not(:last-child)), .grid.quiet :global(.grid-data-content .row > .cell:not(:last-child)),
.grid.quiet :global(.sticky-column .row > .cell), .grid.quiet :global(.sticky-column .row .cell),
.grid.quiet :global(.new-row .row > .cell:not(:last-child)) { .grid.quiet :global(.new-row .row > .cell:not(:last-child)),
.grid.quiet :global(.header-cell:not(:last-child) .cell) {
border-right: none; border-right: none;
} }
.grid.quiet :global(.sticky-column:before) { .grid.quiet :global(.sticky-column:before) {
display: none; display: none;
} }
.grid.quiet:not(.stripe) {
--header-cell-background: var(
--custom-header-cell-background,
var(--grid-background)
);
}
</style> </style>

View File

@ -36,7 +36,7 @@
<style> <style>
.header { .header {
background: var(--grid-background-alt); background: var(--header-cell-background);
border-bottom: var(--cell-border); border-bottom: var(--cell-border);
position: relative; position: relative;
height: var(--default-row-height); height: var(--default-row-height);

View File

@ -4,7 +4,7 @@
import GridScrollWrapper from "./GridScrollWrapper.svelte" import GridScrollWrapper from "./GridScrollWrapper.svelte"
import DataCell from "../cells/DataCell.svelte" import DataCell from "../cells/DataCell.svelte"
import { fade } from "svelte/transition" import { fade } from "svelte/transition"
import { GutterWidth, NewRowID } from "../lib/constants" import { DefaultRowHeight, GutterWidth, NewRowID } from "../lib/constants"
import GutterCell from "../cells/GutterCell.svelte" import GutterCell from "../cells/GutterCell.svelte"
import KeyboardShortcut from "./KeyboardShortcut.svelte" import KeyboardShortcut from "./KeyboardShortcut.svelte"
import { getCellID } from "../lib/utils" import { getCellID } from "../lib/utils"
@ -33,6 +33,7 @@
columnRenderMap, columnRenderMap,
visibleColumns, visibleColumns,
scrollTop, scrollTop,
height,
} = getContext("grid") } = getContext("grid")
let visible = false let visible = false
@ -47,6 +48,8 @@
$: hasNoRows = !$rows.length $: hasNoRows = !$rows.length
$: renderedRowCount = $renderedRows.length $: renderedRowCount = $renderedRows.length
$: offset = getOffset($hasNextPage, renderedRowCount, $rowHeight, $scrollTop) $: offset = getOffset($hasNextPage, renderedRowCount, $rowHeight, $scrollTop)
$: spaceBelow = $height - offset - $rowHeight
$: flipButtons = spaceBelow < 36 + DefaultRowHeight
const getOffset = (hasNextPage, rowCount, rowHeight, scrollTop) => { const getOffset = (hasNextPage, rowCount, rowHeight, scrollTop) => {
// If we have a next page of data then we aren't truly at the bottom, so we // If we have a next page of data then we aren't truly at the bottom, so we
@ -244,7 +247,11 @@
</div> </div>
</GridScrollWrapper> </GridScrollWrapper>
</div> </div>
<div class="buttons" transition:fade|local={{ duration: 130 }}> <div
class="buttons"
class:flip={flipButtons}
transition:fade|local={{ duration: 130 }}
>
<Button size="M" cta on:click={addRow} disabled={isAdding}> <Button size="M" cta on:click={addRow} disabled={isAdding}>
<div class="button-with-keys"> <div class="button-with-keys">
Save Save
@ -337,6 +344,9 @@
.button-with-keys :global(> div) { .button-with-keys :global(> div) {
padding-top: 2px; padding-top: 2px;
} }
.buttons.flip {
top: calc(var(--offset) - 36px - var(--default-row-height) / 2);
}
/* Sticky column styles */ /* Sticky column styles */
.sticky-column { .sticky-column {

View File

@ -169,7 +169,7 @@
z-index: 1; z-index: 1;
} }
.header :global(.cell) { .header :global(.cell) {
background: var(--grid-background-alt); background: var(--header-cell-background);
} }
.header :global(.cell::before) { .header :global(.cell::before) {
display: none; display: none;

View File

@ -2,8 +2,8 @@ export const SmallRowHeight = 36
export const MediumRowHeight = 64 export const MediumRowHeight = 64
export const LargeRowHeight = 92 export const LargeRowHeight = 92
export const DefaultRowHeight = SmallRowHeight export const DefaultRowHeight = SmallRowHeight
export const VPadding = SmallRowHeight * 2 export const VPadding = 0
export const HPadding = 40 export const HPadding = 80
export const ScrollBarSize = 8 export const ScrollBarSize = 8
export const GutterWidth = 72 export const GutterWidth = 72
export const DefaultColumnWidth = 200 export const DefaultColumnWidth = 200

View File

@ -140,13 +140,13 @@
div { div {
position: absolute; position: absolute;
background: var(--spectrum-global-color-gray-500); background: var(--spectrum-global-color-gray-500);
opacity: 0.5; opacity: 0.35;
border-radius: 4px; border-radius: 4px;
transition: opacity 130ms ease-out; transition: opacity 130ms ease-out;
} }
div:hover, div:hover,
div.dragging { div.dragging {
opacity: 1; opacity: 0.8;
} }
.v-scrollbar { .v-scrollbar {
width: var(--scroll-bar-size); width: var(--scroll-bar-size);

View File

@ -7,8 +7,12 @@ type ConfigStore = {
[key in keyof BaseStoreProps]: Readable<BaseStoreProps[key]> [key in keyof BaseStoreProps]: Readable<BaseStoreProps[key]>
} }
interface ConfigState extends BaseStoreProps {
canSelectRows: boolean
}
interface ConfigDerivedStore { interface ConfigDerivedStore {
config: Readable<BaseStoreProps> config: Readable<ConfigState>
} }
export type Store = ConfigStore & ConfigDerivedStore export type Store = ConfigStore & ConfigDerivedStore
@ -47,7 +51,7 @@ export const deriveStores = (context: StoreContext): ConfigDerivedStore => {
const config = derived( const config = derived(
[props, definition, hasNonAutoColumn], [props, definition, hasNonAutoColumn],
([$props, $definition, $hasNonAutoColumn]) => { ([$props, $definition, $hasNonAutoColumn]) => {
let config = { ...$props } let config: ConfigState = { ...$props, canSelectRows: false }
const type = $props.datasource?.type const type = $props.datasource?.type
// Disable some features if we're editing a view // Disable some features if we're editing a view
@ -78,6 +82,9 @@ export const deriveStores = (context: StoreContext): ConfigDerivedStore => {
config.canEditColumns = false config.canEditColumns = false
} }
// Determine if we can select rows
config.canSelectRows = !!config.canDeleteRows || !!config.canAddRows
return config return config
} }
) )

View File

@ -36,6 +36,7 @@ const DependencyOrderedStores = [
NonPlus, NonPlus,
Datasource, Datasource,
Columns, Columns,
Config as any,
Scroll, Scroll,
Validation, Validation,
Rows, Rows,
@ -47,7 +48,6 @@ const DependencyOrderedStores = [
Users, Users,
Menu, Menu,
Pagination, Pagination,
Config as any,
Clipboard, Clipboard,
Notifications, Notifications,
Cache, Cache,

View File

@ -58,6 +58,7 @@ export const deriveStores = (context: StoreContext) => {
width, width,
height, height,
buttonColumnWidth, buttonColumnWidth,
config,
} = context } = context
// Memoize store primitives // Memoize store primitives
@ -97,11 +98,14 @@ export const deriveStores = (context: StoreContext) => {
// Derive vertical limits // Derive vertical limits
const contentHeight = derived( const contentHeight = derived(
[rows, rowHeight, showHScrollbar], [rows, rowHeight, showHScrollbar, config],
([$rows, $rowHeight, $showHScrollbar]) => { ([$rows, $rowHeight, $showHScrollbar, $config]) => {
let height = ($rows.length + 1) * $rowHeight + VPadding let height = $rows.length * $rowHeight + VPadding
if ($showHScrollbar) { if ($showHScrollbar) {
height += ScrollBarSize * 2 height += ScrollBarSize * 3
}
if ($config.canAddRows) {
height += $rowHeight
} }
return height return height
} }

View File

@ -9,3 +9,6 @@ export { Grid } from "./grid"
export { default as ClientAppSkeleton } from "./ClientAppSkeleton.svelte" export { default as ClientAppSkeleton } from "./ClientAppSkeleton.svelte"
export { default as CoreFilterBuilder } from "./CoreFilterBuilder.svelte" export { default as CoreFilterBuilder } from "./CoreFilterBuilder.svelte"
export { default as FilterUsers } from "./FilterUsers.svelte" export { default as FilterUsers } from "./FilterUsers.svelte"
export { default as ChangePasswordModal } from "./ChangePasswordModal.svelte"
export { default as ProfileModal } from "./ProfileModal.svelte"
export { default as PasswordRepeatInput } from "./PasswordRepeatInput.svelte"

View File

@ -166,3 +166,9 @@ export const FieldPermissions = {
READONLY: "readonly", READONLY: "readonly",
HIDDEN: "hidden", HIDDEN: "hidden",
} }
// one or more word characters and whitespace
export const APP_NAME_REGEX = /^[\w\s]+$/
// zero or more non-whitespace characters
export const APP_URL_REGEX = /^[0-9a-zA-Z-_]+$/

View File

@ -14,3 +14,4 @@ export * from "./settings"
export * from "./relatedColumns" export * from "./relatedColumns"
export * from "./table" export * from "./table"
export * from "./components" export * from "./components"
export * from "./validation"

View File

@ -2,7 +2,9 @@ import { helpers } from "@budibase/shared-core"
import { TypeIconMap } from "../constants" import { TypeIconMap } from "../constants"
export const getColumnIcon = column => { export const getColumnIcon = column => {
if (column.schema.icon) { // For some reason we have remix icons saved under this property sometimes,
// so we must ignore those as they are invalid spectrum icons
if (column.schema.icon && !column.schema.icon.startsWith("ri-")) {
return column.schema.icon return column.schema.icon
} }
if (column.calculationType) { if (column.calculationType) {

View File

@ -1,5 +1,5 @@
import { string, mixed } from "yup" import { string, mixed } from "yup"
import { APP_NAME_REGEX, APP_URL_REGEX } from "@/constants" import { APP_NAME_REGEX, APP_URL_REGEX } from "../../../constants"
export const name = (validation, { apps, currentApp } = { apps: [] }) => { export const name = (validation, { apps, currentApp } = { apps: [] }) => {
validation.addValidator( validation.addValidator(

View File

@ -1,7 +1,6 @@
import { capitalise } from "@/helpers"
import { object, string, number } from "yup" import { object, string, number } from "yup"
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
import { notifications } from "@budibase/bbui" import { Helpers, notifications } from "@budibase/bbui"
export const createValidationStore = () => { export const createValidationStore = () => {
const DEFAULT = { const DEFAULT = {
@ -77,7 +76,7 @@ export const createValidationStore = () => {
const [fieldError] = error.errors const [fieldError] = error.errors
if (fieldError) { if (fieldError) {
validation.update(store => { validation.update(store => {
store.errors[propertyName] = capitalise(fieldError) store.errors[propertyName] = Helpers.capitalise(fieldError)
store.valid = false store.valid = false
return store return store
}) })
@ -120,7 +119,7 @@ export const createValidationStore = () => {
} else { } else {
error.inner.forEach(err => { error.inner.forEach(err => {
validation.update(store => { validation.update(store => {
store.errors[err.path] = capitalise(err.message) store.errors[err.path] = Helpers.capitalise(err.message)
return store return store
}) })
}) })

@ -1 +1 @@
Subproject commit bb1ed6fa96ebed30e30659e47b0712567601f3c0 Subproject commit f709bb6a07483785c32ebb6f186709450d735ec3

View File

@ -842,17 +842,29 @@
"description": "A constraint can be applied to the column which will be validated against when a row is saved.", "description": "A constraint can be applied to the column which will be validated against when a row is saved.",
"properties": { "properties": {
"type": { "type": {
"type": "string", "type": "string"
"enum": [
"string",
"number",
"object",
"boolean"
]
}, },
"presence": { "presence": {
"type": "boolean", "oneOf": [
"description": "Defines whether the column is required or not." {
"type": "boolean",
"description": "Defines whether the column is required or not."
},
{
"type": "object",
"description": "Defines whether the column is required or not.",
"properties": {
"allowEmpty": {
"type": "boolean",
"description": "Defines whether the value is allowed to be empty or not."
}
}
}
]
},
"inclusion": {
"type": "array",
"description": "Defines the valid values for this column."
} }
} }
}, },
@ -864,6 +876,10 @@
"type": "boolean", "type": "boolean",
"description": "Defines whether the column is automatically generated." "description": "Defines whether the column is automatically generated."
}, },
"width": {
"type": "number",
"description": "Defines the width of the column in the data UI."
},
"fieldName": { "fieldName": {
"type": "string", "type": "string",
"description": "The name of the column which a relationship column is related to in another table." "description": "The name of the column which a relationship column is related to in another table."
@ -914,17 +930,29 @@
"description": "A constraint can be applied to the column which will be validated against when a row is saved.", "description": "A constraint can be applied to the column which will be validated against when a row is saved.",
"properties": { "properties": {
"type": { "type": {
"type": "string", "type": "string"
"enum": [
"string",
"number",
"object",
"boolean"
]
}, },
"presence": { "presence": {
"type": "boolean", "oneOf": [
"description": "Defines whether the column is required or not." {
"type": "boolean",
"description": "Defines whether the column is required or not."
},
{
"type": "object",
"description": "Defines whether the column is required or not.",
"properties": {
"allowEmpty": {
"type": "boolean",
"description": "Defines whether the value is allowed to be empty or not."
}
}
}
]
},
"inclusion": {
"type": "array",
"description": "Defines the valid values for this column."
} }
} }
}, },
@ -936,6 +964,10 @@
"type": "boolean", "type": "boolean",
"description": "Defines whether the column is automatically generated." "description": "Defines whether the column is automatically generated."
}, },
"width": {
"type": "number",
"description": "Defines the width of the column in the data UI."
},
"formula": { "formula": {
"type": "string", "type": "string",
"description": "Defines a Handlebars or JavaScript formula to use, note that Javascript formulas are expected to be provided in the base64 format." "description": "Defines a Handlebars or JavaScript formula to use, note that Javascript formulas are expected to be provided in the base64 format."
@ -965,8 +997,6 @@
"datetime", "datetime",
"attachment", "attachment",
"attachment_single", "attachment_single",
"link",
"formula",
"auto", "auto",
"ai", "ai",
"json", "json",
@ -984,17 +1014,29 @@
"description": "A constraint can be applied to the column which will be validated against when a row is saved.", "description": "A constraint can be applied to the column which will be validated against when a row is saved.",
"properties": { "properties": {
"type": { "type": {
"type": "string", "type": "string"
"enum": [
"string",
"number",
"object",
"boolean"
]
}, },
"presence": { "presence": {
"type": "boolean", "oneOf": [
"description": "Defines whether the column is required or not." {
"type": "boolean",
"description": "Defines whether the column is required or not."
},
{
"type": "object",
"description": "Defines whether the column is required or not.",
"properties": {
"allowEmpty": {
"type": "boolean",
"description": "Defines whether the value is allowed to be empty or not."
}
}
}
]
},
"inclusion": {
"type": "array",
"description": "Defines the valid values for this column."
} }
} }
}, },
@ -1005,6 +1047,10 @@
"autocolumn": { "autocolumn": {
"type": "boolean", "type": "boolean",
"description": "Defines whether the column is automatically generated." "description": "Defines whether the column is automatically generated."
},
"width": {
"type": "number",
"description": "Defines the width of the column in the data UI."
} }
} }
} }
@ -1052,17 +1098,29 @@
"description": "A constraint can be applied to the column which will be validated against when a row is saved.", "description": "A constraint can be applied to the column which will be validated against when a row is saved.",
"properties": { "properties": {
"type": { "type": {
"type": "string", "type": "string"
"enum": [
"string",
"number",
"object",
"boolean"
]
}, },
"presence": { "presence": {
"type": "boolean", "oneOf": [
"description": "Defines whether the column is required or not." {
"type": "boolean",
"description": "Defines whether the column is required or not."
},
{
"type": "object",
"description": "Defines whether the column is required or not.",
"properties": {
"allowEmpty": {
"type": "boolean",
"description": "Defines whether the value is allowed to be empty or not."
}
}
}
]
},
"inclusion": {
"type": "array",
"description": "Defines the valid values for this column."
} }
} }
}, },
@ -1074,6 +1132,10 @@
"type": "boolean", "type": "boolean",
"description": "Defines whether the column is automatically generated." "description": "Defines whether the column is automatically generated."
}, },
"width": {
"type": "number",
"description": "Defines the width of the column in the data UI."
},
"fieldName": { "fieldName": {
"type": "string", "type": "string",
"description": "The name of the column which a relationship column is related to in another table." "description": "The name of the column which a relationship column is related to in another table."
@ -1124,17 +1186,29 @@
"description": "A constraint can be applied to the column which will be validated against when a row is saved.", "description": "A constraint can be applied to the column which will be validated against when a row is saved.",
"properties": { "properties": {
"type": { "type": {
"type": "string", "type": "string"
"enum": [
"string",
"number",
"object",
"boolean"
]
}, },
"presence": { "presence": {
"type": "boolean", "oneOf": [
"description": "Defines whether the column is required or not." {
"type": "boolean",
"description": "Defines whether the column is required or not."
},
{
"type": "object",
"description": "Defines whether the column is required or not.",
"properties": {
"allowEmpty": {
"type": "boolean",
"description": "Defines whether the value is allowed to be empty or not."
}
}
}
]
},
"inclusion": {
"type": "array",
"description": "Defines the valid values for this column."
} }
} }
}, },
@ -1146,6 +1220,10 @@
"type": "boolean", "type": "boolean",
"description": "Defines whether the column is automatically generated." "description": "Defines whether the column is automatically generated."
}, },
"width": {
"type": "number",
"description": "Defines the width of the column in the data UI."
},
"formula": { "formula": {
"type": "string", "type": "string",
"description": "Defines a Handlebars or JavaScript formula to use, note that Javascript formulas are expected to be provided in the base64 format." "description": "Defines a Handlebars or JavaScript formula to use, note that Javascript formulas are expected to be provided in the base64 format."
@ -1175,8 +1253,6 @@
"datetime", "datetime",
"attachment", "attachment",
"attachment_single", "attachment_single",
"link",
"formula",
"auto", "auto",
"ai", "ai",
"json", "json",
@ -1194,17 +1270,29 @@
"description": "A constraint can be applied to the column which will be validated against when a row is saved.", "description": "A constraint can be applied to the column which will be validated against when a row is saved.",
"properties": { "properties": {
"type": { "type": {
"type": "string", "type": "string"
"enum": [
"string",
"number",
"object",
"boolean"
]
}, },
"presence": { "presence": {
"type": "boolean", "oneOf": [
"description": "Defines whether the column is required or not." {
"type": "boolean",
"description": "Defines whether the column is required or not."
},
{
"type": "object",
"description": "Defines whether the column is required or not.",
"properties": {
"allowEmpty": {
"type": "boolean",
"description": "Defines whether the value is allowed to be empty or not."
}
}
}
]
},
"inclusion": {
"type": "array",
"description": "Defines the valid values for this column."
} }
} }
}, },
@ -1215,6 +1303,10 @@
"autocolumn": { "autocolumn": {
"type": "boolean", "type": "boolean",
"description": "Defines whether the column is automatically generated." "description": "Defines whether the column is automatically generated."
},
"width": {
"type": "number",
"description": "Defines the width of the column in the data UI."
} }
} }
} }
@ -1273,17 +1365,29 @@
"description": "A constraint can be applied to the column which will be validated against when a row is saved.", "description": "A constraint can be applied to the column which will be validated against when a row is saved.",
"properties": { "properties": {
"type": { "type": {
"type": "string", "type": "string"
"enum": [
"string",
"number",
"object",
"boolean"
]
}, },
"presence": { "presence": {
"type": "boolean", "oneOf": [
"description": "Defines whether the column is required or not." {
"type": "boolean",
"description": "Defines whether the column is required or not."
},
{
"type": "object",
"description": "Defines whether the column is required or not.",
"properties": {
"allowEmpty": {
"type": "boolean",
"description": "Defines whether the value is allowed to be empty or not."
}
}
}
]
},
"inclusion": {
"type": "array",
"description": "Defines the valid values for this column."
} }
} }
}, },
@ -1295,6 +1399,10 @@
"type": "boolean", "type": "boolean",
"description": "Defines whether the column is automatically generated." "description": "Defines whether the column is automatically generated."
}, },
"width": {
"type": "number",
"description": "Defines the width of the column in the data UI."
},
"fieldName": { "fieldName": {
"type": "string", "type": "string",
"description": "The name of the column which a relationship column is related to in another table." "description": "The name of the column which a relationship column is related to in another table."
@ -1345,17 +1453,29 @@
"description": "A constraint can be applied to the column which will be validated against when a row is saved.", "description": "A constraint can be applied to the column which will be validated against when a row is saved.",
"properties": { "properties": {
"type": { "type": {
"type": "string", "type": "string"
"enum": [
"string",
"number",
"object",
"boolean"
]
}, },
"presence": { "presence": {
"type": "boolean", "oneOf": [
"description": "Defines whether the column is required or not." {
"type": "boolean",
"description": "Defines whether the column is required or not."
},
{
"type": "object",
"description": "Defines whether the column is required or not.",
"properties": {
"allowEmpty": {
"type": "boolean",
"description": "Defines whether the value is allowed to be empty or not."
}
}
}
]
},
"inclusion": {
"type": "array",
"description": "Defines the valid values for this column."
} }
} }
}, },
@ -1367,6 +1487,10 @@
"type": "boolean", "type": "boolean",
"description": "Defines whether the column is automatically generated." "description": "Defines whether the column is automatically generated."
}, },
"width": {
"type": "number",
"description": "Defines the width of the column in the data UI."
},
"formula": { "formula": {
"type": "string", "type": "string",
"description": "Defines a Handlebars or JavaScript formula to use, note that Javascript formulas are expected to be provided in the base64 format." "description": "Defines a Handlebars or JavaScript formula to use, note that Javascript formulas are expected to be provided in the base64 format."
@ -1396,8 +1520,6 @@
"datetime", "datetime",
"attachment", "attachment",
"attachment_single", "attachment_single",
"link",
"formula",
"auto", "auto",
"ai", "ai",
"json", "json",
@ -1415,17 +1537,29 @@
"description": "A constraint can be applied to the column which will be validated against when a row is saved.", "description": "A constraint can be applied to the column which will be validated against when a row is saved.",
"properties": { "properties": {
"type": { "type": {
"type": "string", "type": "string"
"enum": [
"string",
"number",
"object",
"boolean"
]
}, },
"presence": { "presence": {
"type": "boolean", "oneOf": [
"description": "Defines whether the column is required or not." {
"type": "boolean",
"description": "Defines whether the column is required or not."
},
{
"type": "object",
"description": "Defines whether the column is required or not.",
"properties": {
"allowEmpty": {
"type": "boolean",
"description": "Defines whether the value is allowed to be empty or not."
}
}
}
]
},
"inclusion": {
"type": "array",
"description": "Defines the valid values for this column."
} }
} }
}, },
@ -1436,6 +1570,10 @@
"autocolumn": { "autocolumn": {
"type": "boolean", "type": "boolean",
"description": "Defines whether the column is automatically generated." "description": "Defines whether the column is automatically generated."
},
"width": {
"type": "number",
"description": "Defines the width of the column in the data UI."
} }
} }
} }

View File

@ -752,20 +752,28 @@ components:
properties: properties:
type: type:
type: string type: string
enum:
- string
- number
- object
- boolean
presence: presence:
type: boolean oneOf:
description: Defines whether the column is required or not. - type: boolean
description: Defines whether the column is required or not.
- type: object
description: Defines whether the column is required or not.
properties:
allowEmpty:
type: boolean
description: Defines whether the value is allowed to be empty or not.
inclusion:
type: array
description: Defines the valid values for this column.
name: name:
type: string type: string
description: The name of the column. description: The name of the column.
autocolumn: autocolumn:
type: boolean type: boolean
description: Defines whether the column is automatically generated. description: Defines whether the column is automatically generated.
width:
type: number
description: Defines the width of the column in the data UI.
fieldName: fieldName:
type: string type: string
description: The name of the column which a relationship column is related to in description: The name of the column which a relationship column is related to in
@ -812,20 +820,28 @@ components:
properties: properties:
type: type:
type: string type: string
enum:
- string
- number
- object
- boolean
presence: presence:
type: boolean oneOf:
description: Defines whether the column is required or not. - type: boolean
description: Defines whether the column is required or not.
- type: object
description: Defines whether the column is required or not.
properties:
allowEmpty:
type: boolean
description: Defines whether the value is allowed to be empty or not.
inclusion:
type: array
description: Defines the valid values for this column.
name: name:
type: string type: string
description: The name of the column. description: The name of the column.
autocolumn: autocolumn:
type: boolean type: boolean
description: Defines whether the column is automatically generated. description: Defines whether the column is automatically generated.
width:
type: number
description: Defines the width of the column in the data UI.
formula: formula:
type: string type: string
description: Defines a Handlebars or JavaScript formula to use, note that description: Defines a Handlebars or JavaScript formula to use, note that
@ -851,8 +867,6 @@ components:
- datetime - datetime
- attachment - attachment
- attachment_single - attachment_single
- link
- formula
- auto - auto
- ai - ai
- json - json
@ -871,20 +885,28 @@ components:
properties: properties:
type: type:
type: string type: string
enum:
- string
- number
- object
- boolean
presence: presence:
type: boolean oneOf:
description: Defines whether the column is required or not. - type: boolean
description: Defines whether the column is required or not.
- type: object
description: Defines whether the column is required or not.
properties:
allowEmpty:
type: boolean
description: Defines whether the value is allowed to be empty or not.
inclusion:
type: array
description: Defines the valid values for this column.
name: name:
type: string type: string
description: The name of the column. description: The name of the column.
autocolumn: autocolumn:
type: boolean type: boolean
description: Defines whether the column is automatically generated. description: Defines whether the column is automatically generated.
width:
type: number
description: Defines the width of the column in the data UI.
tableOutput: tableOutput:
type: object type: object
properties: properties:
@ -921,20 +943,28 @@ components:
properties: properties:
type: type:
type: string type: string
enum:
- string
- number
- object
- boolean
presence: presence:
type: boolean oneOf:
description: Defines whether the column is required or not. - type: boolean
description: Defines whether the column is required or not.
- type: object
description: Defines whether the column is required or not.
properties:
allowEmpty:
type: boolean
description: Defines whether the value is allowed to be empty or not.
inclusion:
type: array
description: Defines the valid values for this column.
name: name:
type: string type: string
description: The name of the column. description: The name of the column.
autocolumn: autocolumn:
type: boolean type: boolean
description: Defines whether the column is automatically generated. description: Defines whether the column is automatically generated.
width:
type: number
description: Defines the width of the column in the data UI.
fieldName: fieldName:
type: string type: string
description: The name of the column which a relationship column is related to in description: The name of the column which a relationship column is related to in
@ -981,20 +1011,28 @@ components:
properties: properties:
type: type:
type: string type: string
enum:
- string
- number
- object
- boolean
presence: presence:
type: boolean oneOf:
description: Defines whether the column is required or not. - type: boolean
description: Defines whether the column is required or not.
- type: object
description: Defines whether the column is required or not.
properties:
allowEmpty:
type: boolean
description: Defines whether the value is allowed to be empty or not.
inclusion:
type: array
description: Defines the valid values for this column.
name: name:
type: string type: string
description: The name of the column. description: The name of the column.
autocolumn: autocolumn:
type: boolean type: boolean
description: Defines whether the column is automatically generated. description: Defines whether the column is automatically generated.
width:
type: number
description: Defines the width of the column in the data UI.
formula: formula:
type: string type: string
description: Defines a Handlebars or JavaScript formula to use, note that description: Defines a Handlebars or JavaScript formula to use, note that
@ -1020,8 +1058,6 @@ components:
- datetime - datetime
- attachment - attachment
- attachment_single - attachment_single
- link
- formula
- auto - auto
- ai - ai
- json - json
@ -1040,20 +1076,28 @@ components:
properties: properties:
type: type:
type: string type: string
enum:
- string
- number
- object
- boolean
presence: presence:
type: boolean oneOf:
description: Defines whether the column is required or not. - type: boolean
description: Defines whether the column is required or not.
- type: object
description: Defines whether the column is required or not.
properties:
allowEmpty:
type: boolean
description: Defines whether the value is allowed to be empty or not.
inclusion:
type: array
description: Defines the valid values for this column.
name: name:
type: string type: string
description: The name of the column. description: The name of the column.
autocolumn: autocolumn:
type: boolean type: boolean
description: Defines whether the column is automatically generated. description: Defines whether the column is automatically generated.
width:
type: number
description: Defines the width of the column in the data UI.
_id: _id:
description: The ID of the table. description: The ID of the table.
type: string type: string
@ -1097,20 +1141,28 @@ components:
properties: properties:
type: type:
type: string type: string
enum:
- string
- number
- object
- boolean
presence: presence:
type: boolean oneOf:
description: Defines whether the column is required or not. - type: boolean
description: Defines whether the column is required or not.
- type: object
description: Defines whether the column is required or not.
properties:
allowEmpty:
type: boolean
description: Defines whether the value is allowed to be empty or not.
inclusion:
type: array
description: Defines the valid values for this column.
name: name:
type: string type: string
description: The name of the column. description: The name of the column.
autocolumn: autocolumn:
type: boolean type: boolean
description: Defines whether the column is automatically generated. description: Defines whether the column is automatically generated.
width:
type: number
description: Defines the width of the column in the data UI.
fieldName: fieldName:
type: string type: string
description: The name of the column which a relationship column is related to in description: The name of the column which a relationship column is related to in
@ -1157,20 +1209,28 @@ components:
properties: properties:
type: type:
type: string type: string
enum:
- string
- number
- object
- boolean
presence: presence:
type: boolean oneOf:
description: Defines whether the column is required or not. - type: boolean
description: Defines whether the column is required or not.
- type: object
description: Defines whether the column is required or not.
properties:
allowEmpty:
type: boolean
description: Defines whether the value is allowed to be empty or not.
inclusion:
type: array
description: Defines the valid values for this column.
name: name:
type: string type: string
description: The name of the column. description: The name of the column.
autocolumn: autocolumn:
type: boolean type: boolean
description: Defines whether the column is automatically generated. description: Defines whether the column is automatically generated.
width:
type: number
description: Defines the width of the column in the data UI.
formula: formula:
type: string type: string
description: Defines a Handlebars or JavaScript formula to use, note that description: Defines a Handlebars or JavaScript formula to use, note that
@ -1196,8 +1256,6 @@ components:
- datetime - datetime
- attachment - attachment
- attachment_single - attachment_single
- link
- formula
- auto - auto
- ai - ai
- json - json
@ -1216,20 +1274,28 @@ components:
properties: properties:
type: type:
type: string type: string
enum:
- string
- number
- object
- boolean
presence: presence:
type: boolean oneOf:
description: Defines whether the column is required or not. - type: boolean
description: Defines whether the column is required or not.
- type: object
description: Defines whether the column is required or not.
properties:
allowEmpty:
type: boolean
description: Defines whether the value is allowed to be empty or not.
inclusion:
type: array
description: Defines the valid values for this column.
name: name:
type: string type: string
description: The name of the column. description: The name of the column.
autocolumn: autocolumn:
type: boolean type: boolean
description: Defines whether the column is automatically generated. description: Defines whether the column is automatically generated.
width:
type: number
description: Defines the width of the column in the data UI.
_id: _id:
description: The ID of the table. description: The ID of the table.
type: string type: string

View File

@ -27,7 +27,9 @@ const table = {
const baseColumnDef = { const baseColumnDef = {
type: { type: {
type: "string", type: "string",
enum: Object.values(FieldType), enum: Object.values(FieldType).filter(
type => ![FieldType.LINK, FieldType.FORMULA].includes(type)
),
description: description:
"Defines the type of the column, most explain themselves, a link column is a relationship.", "Defines the type of the column, most explain themselves, a link column is a relationship.",
}, },
@ -38,11 +40,29 @@ const baseColumnDef = {
properties: { properties: {
type: { type: {
type: "string", type: "string",
enum: ["string", "number", "object", "boolean"],
}, },
presence: { presence: {
type: "boolean", oneOf: [
description: "Defines whether the column is required or not.", {
type: "boolean",
description: "Defines whether the column is required or not.",
},
{
type: "object",
description: "Defines whether the column is required or not.",
properties: {
allowEmpty: {
type: "boolean",
description:
"Defines whether the value is allowed to be empty or not.",
},
},
},
],
},
inclusion: {
type: "array",
description: "Defines the valid values for this column.",
}, },
}, },
}, },
@ -54,6 +74,10 @@ const baseColumnDef = {
type: "boolean", type: "boolean",
description: "Defines whether the column is automatically generated.", description: "Defines whether the column is automatically generated.",
}, },
width: {
type: "number",
description: "Defines the width of the column in the data UI.",
},
} }
const tableSchema = { const tableSchema = {

View File

@ -16,6 +16,7 @@ import {
DocumentType, DocumentType,
generateAppID, generateAppID,
generateDevAppID, generateDevAppID,
generateScreenID,
getLayoutParams, getLayoutParams,
getScreenParams, getScreenParams,
} from "../../db/utils" } from "../../db/utils"
@ -75,6 +76,7 @@ import sdk from "../../sdk"
import { builderSocket } from "../../websockets" import { builderSocket } from "../../websockets"
import { DefaultAppTheme, sdk as sharedCoreSDK } from "@budibase/shared-core" import { DefaultAppTheme, sdk as sharedCoreSDK } from "@budibase/shared-core"
import * as appMigrations from "../../appMigrations" import * as appMigrations from "../../appMigrations"
import { createSampleDataTableScreen } from "../../constants/screens"
// utility function, need to do away with this // utility function, need to do away with this
async function getLayouts() { async function getLayouts() {
@ -176,11 +178,8 @@ async function createInstance(appId: string, template: AppTemplate) {
return { _id: appId } return { _id: appId }
} }
export const addSampleData = async ( async function addSampleDataDocs() {
ctx: UserCtx<void, AddAppSampleDataResponse>
) => {
const db = context.getAppDB() const db = context.getAppDB()
try { try {
// Check if default datasource exists before creating it // Check if default datasource exists before creating it
await sdk.datasources.get(DEFAULT_BB_DATASOURCE_ID) await sdk.datasources.get(DEFAULT_BB_DATASOURCE_ID)
@ -190,7 +189,41 @@ export const addSampleData = async (
// add in the default db data docs - tables, datasource, rows and links // add in the default db data docs - tables, datasource, rows and links
await db.bulkDocs([...defaultDbDocs]) await db.bulkDocs([...defaultDbDocs])
} }
}
async function addSampleDataScreen() {
const db = context.getAppDB()
let screen = createSampleDataTableScreen()
screen._id = generateScreenID()
await db.put(screen)
}
async function addSampleDataNavLinks() {
const db = context.getAppDB()
let app = await sdk.applications.metadata.get()
if (!app.navigation) {
return
}
if (!app.navigation.links) {
app.navigation.links = []
}
app.navigation.links.push({
text: "Inventory",
url: "/inventory",
type: "link",
roleId: roles.BUILTIN_ROLE_IDS.BASIC,
})
await db.put(app)
// remove any cached metadata, so that it will be updated
await cache.app.invalidateAppMetadata(app.appId)
}
export const addSampleData = async (
ctx: UserCtx<void, AddAppSampleDataResponse>
) => {
await addSampleDataDocs()
ctx.body = { message: "Sample tables added." } ctx.body = { message: "Sample tables added." }
} }
@ -261,7 +294,7 @@ async function performAppCreate(
const { body } = ctx.request const { body } = ctx.request
const { name, url, encryptionPassword, templateKey } = body const { name, url, encryptionPassword, templateKey } = body
let useTemplate let useTemplate = false
if (typeof body.useTemplate === "string") { if (typeof body.useTemplate === "string") {
useTemplate = body.useTemplate === "true" useTemplate = body.useTemplate === "true"
} else if (typeof body.useTemplate === "boolean") { } else if (typeof body.useTemplate === "boolean") {
@ -293,12 +326,14 @@ async function performAppCreate(
return await context.doInAppContext(appId, async () => { return await context.doInAppContext(appId, async () => {
const instance = await createInstance(appId, instanceConfig) const instance = await createInstance(appId, instanceConfig)
const db = context.getAppDB() const db = context.getAppDB()
const isImport = !!instanceConfig.file
const addSampleData = !isImport && !useTemplate
if (instanceConfig.useTemplate && !instanceConfig.file) { if (instanceConfig.useTemplate && !instanceConfig.file) {
await updateUserColumns(appId, db, ctx.user._id!) await updateUserColumns(appId, db, ctx.user._id!)
} }
const newApplication: App = { let newApplication: App = {
_id: DocumentType.APP_METADATA, _id: DocumentType.APP_METADATA,
_rev: undefined, _rev: undefined,
appId, appId,
@ -317,11 +352,14 @@ async function performAppCreate(
navigation: "Top", navigation: "Top",
title: name, title: name,
navWidth: "Large", navWidth: "Large",
navBackground: "var(--spectrum-global-color-gray-100)", navBackground: "var(--spectrum-global-color-static-blue-1200)",
navTextColor: "var(--spectrum-global-color-static-white)",
links: [], links: [],
}, },
theme: DefaultAppTheme, theme: DefaultAppTheme,
customTheme: { customTheme: {
primaryColor: "var(--spectrum-global-color-static-blue-1200)",
primaryColorHover: "var(--spectrum-global-color-static-blue-800)",
buttonBorderRadius: "16px", buttonBorderRadius: "16px",
}, },
features: { features: {
@ -332,7 +370,6 @@ async function performAppCreate(
creationVersion: undefined, creationVersion: undefined,
} }
const isImport = !!instanceConfig.file
if (!isImport) { if (!isImport) {
newApplication.creationVersion = envCore.VERSION newApplication.creationVersion = envCore.VERSION
} }
@ -379,6 +416,20 @@ async function performAppCreate(
await uploadAppFiles(appId) await uploadAppFiles(appId)
} }
// Add sample datasource and example screen for non-templates/non-imports
if (addSampleData) {
try {
await addSampleDataDocs()
await addSampleDataScreen()
await addSampleDataNavLinks()
// Fetch the latest version of the app after these changes
newApplication = await sdk.applications.metadata.get()
} catch (err) {
ctx.throw(400, "App created, but failed to add sample data")
}
}
const latestMigrationId = appMigrations.getLatestEnabledMigrationId() const latestMigrationId = appMigrations.getLatestEnabledMigrationId()
if (latestMigrationId) { if (latestMigrationId) {
// Initialise the app migration version as the latest one // Initialise the app migration version as the latest one

View File

@ -127,10 +127,26 @@ describe("/applications", () => {
}) })
describe("create", () => { describe("create", () => {
it("creates empty app", async () => { const checkScreenCount = async (expectedCount: number) => {
const res = await config.api.application.getDefinition(
config.getProdAppId()
)
expect(res.screens.length).toEqual(expectedCount)
}
const checkTableCount = async (expectedCount: number) => {
const tables = await config.api.table.fetch()
expect(tables.length).toEqual(expectedCount)
}
it("creates empty app with sample data", async () => {
const app = await config.api.application.create({ name: utils.newid() }) const app = await config.api.application.create({ name: utils.newid() })
expect(app._id).toBeDefined() expect(app._id).toBeDefined()
expect(events.app.created).toHaveBeenCalledTimes(1) expect(events.app.created).toHaveBeenCalledTimes(1)
// Ensure we created sample resources
await checkScreenCount(1)
await checkTableCount(5)
}) })
it("creates app from template", async () => { it("creates app from template", async () => {
@ -149,6 +165,11 @@ describe("/applications", () => {
expect(app._id).toBeDefined() expect(app._id).toBeDefined()
expect(events.app.created).toHaveBeenCalledTimes(1) expect(events.app.created).toHaveBeenCalledTimes(1)
expect(events.app.templateImported).toHaveBeenCalledTimes(1) expect(events.app.templateImported).toHaveBeenCalledTimes(1)
// Ensure we did not create sample data. This template includes exactly
// this many of each resource.
await checkScreenCount(1)
await checkTableCount(5)
}) })
it("creates app from file", async () => { it("creates app from file", async () => {
@ -160,6 +181,11 @@ describe("/applications", () => {
expect(app._id).toBeDefined() expect(app._id).toBeDefined()
expect(events.app.created).toHaveBeenCalledTimes(1) expect(events.app.created).toHaveBeenCalledTimes(1)
expect(events.app.fileImported).toHaveBeenCalledTimes(1) expect(events.app.fileImported).toHaveBeenCalledTimes(1)
// Ensure we did not create sample data. This file includes exactly
// this many of each resource.
await checkScreenCount(1)
await checkTableCount(5)
}) })
it("should apply authorization to endpoint", async () => { it("should apply authorization to endpoint", async () => {

View File

@ -66,8 +66,8 @@ describe("/backups", () => {
await config.createScreen() await config.createScreen()
let res = await sdk.backups.calculateBackupStats(config.getAppId()!) let res = await sdk.backups.calculateBackupStats(config.getAppId()!)
expect(res.automations).toEqual(1) expect(res.automations).toEqual(1)
expect(res.datasources).toEqual(1) expect(res.datasources).toEqual(6)
expect(res.screens).toEqual(1) expect(res.screens).toEqual(2)
}) })
}) })
}) })

View File

@ -9,6 +9,7 @@ import {
UsageInScreensResponse, UsageInScreensResponse,
} from "@budibase/types" } from "@budibase/types"
import { basicDatasourcePlus } from "../../../tests/utilities/structures" import { basicDatasourcePlus } from "../../../tests/utilities/structures"
import { SAMPLE_DATA_SCREEN_NAME } from "../../../constants/screens"
const { const {
basicScreen, basicScreen,
@ -21,8 +22,8 @@ const {
} = setup.structures } = setup.structures
describe("/screens", () => { describe("/screens", () => {
let config = setup.getConfig()
let screen: Screen let screen: Screen
let config = setup.getConfig()
afterAll(setup.afterAll) afterAll(setup.afterAll)
@ -32,9 +33,16 @@ describe("/screens", () => {
}) })
describe("fetch", () => { describe("fetch", () => {
it("should be able to create a layout", async () => { it("should create the sample data screen", async () => {
const screens = await config.api.screen.list() const screens = await config.api.screen.list()
expect(screens.length).toEqual(1) expect(screens.some(s => s.name === SAMPLE_DATA_SCREEN_NAME)).toEqual(
true
)
})
it("should be able to create a screen", async () => {
const screens = await config.api.screen.list()
expect(screens.length).toEqual(2)
expect(screens.some(s => s._id === screen._id)).toEqual(true) expect(screens.some(s => s._id === screen._id)).toEqual(true)
}) })
@ -92,8 +100,14 @@ describe("/screens", () => {
const res = await config.api.application.getDefinition( const res = await config.api.application.getDefinition(
config.getProdAppId() config.getProdAppId()
) )
expect(res.screens.length).toEqual(screenIds.length)
expect(res.screens.map(s => s._id).sort()).toEqual(screenIds.sort()) // Filter out sample screen
const screens = res.screens.filter(
s => s.name !== SAMPLE_DATA_SCREEN_NAME
)
expect(screens.length).toEqual(screenIds.length)
expect(screens.map(s => s._id).sort()).toEqual(screenIds.sort())
}) })
} }
@ -122,9 +136,15 @@ describe("/screens", () => {
const res = await config.api.application.getDefinition( const res = await config.api.application.getDefinition(
config.prodAppId! config.prodAppId!
) )
// Filter out sample screen
const screens = res.screens.filter(
s => s.name !== SAMPLE_DATA_SCREEN_NAME
)
const screenIds = [screen._id!, screen1._id!] const screenIds = [screen._id!, screen1._id!]
expect(res.screens.length).toEqual(screenIds.length) expect(screens.length).toEqual(screenIds.length)
expect(res.screens.map(s => s._id).sort()).toEqual(screenIds.sort()) expect(screens.map(s => s._id).sort()).toEqual(screenIds.sort())
} }
) )
}) })

View File

@ -2,6 +2,8 @@ import { roles } from "@budibase/backend-core"
import { BASE_LAYOUT_PROP_IDS } from "./layouts" import { BASE_LAYOUT_PROP_IDS } from "./layouts"
import { Screen, Table, Query, ViewV2, Component } from "@budibase/types" import { Screen, Table, Query, ViewV2, Component } from "@budibase/types"
export const SAMPLE_DATA_SCREEN_NAME = "sample-data-inventory-screen"
export function createHomeScreen( export function createHomeScreen(
config: { config: {
roleId: string roleId: string
@ -227,3 +229,365 @@ export function createQueryScreen(datasourceId: string, query: Query): Screen {
name: "screen-id", name: "screen-id",
} }
} }
export function createSampleDataTableScreen(): Screen {
return {
showNavigation: true,
width: "Large",
routing: { route: "/inventory", roleId: "BASIC", homeScreen: false },
name: SAMPLE_DATA_SCREEN_NAME,
props: {
_id: "c38f2b9f250fb4c33965ce47e12c02a80",
_component: "@budibase/standard-components/container",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_children: [
{
_id: "cf600445f0b0048c79c0c81606b30d542",
_component: "@budibase/standard-components/gridblock",
_styles: {
normal: {
"--grid-desktop-col-start": 1,
"--grid-desktop-col-end": 13,
"--grid-desktop-row-start": 3,
"--grid-desktop-row-end": 19,
},
hover: {},
active: {},
selected: {},
},
_instanceName: "Inventory table",
_children: [],
table: {
label: "Inventory",
tableId: "ta_bb_inventory",
type: "table",
datasourceName: "Sample Data",
},
columns: [
{
label: "Item Tags",
field: "Item Tags",
active: true,
},
{
label: "Purchase Date",
field: "Purchase Date",
active: true,
},
{
label: "Purchase Price",
field: "Purchase Price",
active: true,
format:
// eslint-disable-next-line no-template-curly-in-string
"${{ [cf600445f0b0048c79c0c81606b30d542].[Purchase Price] }}",
},
{
label: "Notes",
field: "Notes",
active: true,
},
{
label: "Status",
field: "Status",
active: true,
conditions: [
{
id: Math.random(),
target: "row",
metadataKey: "backgroundColor",
operator: "contains",
valueType: "array",
metadataValue: "var(--spectrum-global-color-red-100)",
noValue: false,
referenceValue: "Repair",
},
],
},
{
label: "SKU",
field: "SKU",
active: true,
},
{
label: "Item ID",
field: "Item ID",
active: true,
},
{
label: "Created At",
field: "Created At",
active: false,
},
{
label: "Updated At",
field: "Updated At",
active: false,
},
{
label: "Item Name",
field: "Item Name",
active: true,
},
],
initialSortColumn: "Item ID",
allowAddRows: false,
allowEditRows: false,
allowDeleteRows: false,
stripeRows: false,
quiet: true,
onRowClick: [
{
parameters: {
key: "inventoryID",
type: "set",
value: "{{ [eventContext].[row]._id }}",
},
"##eventHandlerType": "Update State",
id: "fgVuxCvjL",
},
{
parameters: {
id: "c73fd03209dd44dd3937a33c6205b031d",
},
"##eventHandlerType": "Open Side Panel",
id: "hwnlhdSUb",
},
],
},
{
_id: "c09edf7de69be44ce8f0215c3f62e43a5",
_component: "@budibase/standard-components/textv2",
_styles: {
normal: {
"--grid-desktop-col-end": 3,
},
hover: {},
active: {},
},
_instanceName: "Table title",
align: "left",
text: "## Inventory",
},
{
_id: "c5879d3daffbd47619a833d3f88f07526",
_component: "@budibase/standard-components/button",
_styles: {
normal: {
"--grid-desktop-col-start": 11,
"--grid-desktop-col-end": 13,
"--grid-desktop-row-start": 1,
"--grid-desktop-row-end": 3,
"--grid-desktop-h-align": "end",
},
hover: {},
active: {},
},
_instanceName: "New row button",
text: "Create row",
type: "cta",
size: "M",
gap: "M",
onClick: [
{
parameters: {
id: "c34d2b7c480144f3c800be15a62111d24",
},
"##eventHandlerType": "Open Side Panel",
id: "rYTWHu7k0",
},
],
},
{
_id: "c73fd03209dd44dd3937a33c6205b031d",
_component: "@budibase/standard-components/sidepanel",
_styles: {
normal: {},
hover: {},
active: {},
},
_instanceName: "Edit row side panel",
ignoreClicksOutside: false,
_children: [
{
_id: "c3a5c8d0caf35410f8b75d6cb493ac693",
_component: "@budibase/standard-components/formblock",
_styles: {
normal: {},
hover: {},
active: {},
},
_instanceName: "Edit row form block",
dataSource: {
label: "Inventory",
tableId: "ta_bb_inventory",
type: "table",
datasourceName: "Sample Data",
resourceId: "ta_bb_inventory",
},
actionType: "Update",
buttonPosition: "bottom",
size: "spectrum--medium",
noRowsMessage: "We couldn't find a row to display",
disabled: false,
buttons: [
{
text: "Save",
_id: "cc8c5d82717a54e68a610fe7204e25392",
_component: "@budibase/standard-components/button",
onClick: [
{
"##eventHandlerType": "Validate Form",
parameters: {
componentId: "c3a5c8d0caf35410f8b75d6cb493ac693-form",
},
},
{
"##eventHandlerType": "Save Row",
parameters: {
providerId: "c3a5c8d0caf35410f8b75d6cb493ac693-form",
tableId: "ta_bb_inventory",
confirm: null,
},
},
{
"##eventHandlerType": "Close Screen Modal",
},
{
"##eventHandlerType": "Close Side Panel",
},
{
"##eventHandlerType": "Close Modal",
},
],
type: "cta",
},
{
text: "Delete",
_id: "cf20dbe1df3d648599932b04f7e630376",
_component: "@budibase/standard-components/button",
onClick: [
{
"##eventHandlerType": "Delete Row",
parameters: {
confirm: true,
tableId: "ta_bb_inventory",
rowId:
"{{ [c3a5c8d0caf35410f8b75d6cb493ac693-repeater].[_id] }}",
revId:
"{{ [c3a5c8d0caf35410f8b75d6cb493ac693-repeater].[_rev] }}",
},
},
{
"##eventHandlerType": "Close Screen Modal",
},
{
"##eventHandlerType": "Close Side Panel",
},
{
"##eventHandlerType": "Close Modal",
},
],
quiet: true,
type: "warning",
},
],
fields: null,
rowId: "{{ [state].[inventoryID] }}",
title:
"{{ [c3a5c8d0caf35410f8b75d6cb493ac693-repeater].[Item Name] }}",
},
],
},
{
_id: "c34d2b7c480144f3c800be15a62111d24",
_component: "@budibase/standard-components/sidepanel",
_styles: {
normal: {},
hover: {},
active: {},
},
_instanceName: "New row side panel",
ignoreClicksOutside: false,
_children: [
{
_id: "c61a1690c6ba0448db504eda38f766db1",
_component: "@budibase/standard-components/formblock",
_styles: {
normal: {},
hover: {},
active: {},
},
_instanceName: "New Form Block",
dataSource: {
label: "Inventory",
tableId: "ta_bb_inventory",
type: "table",
datasourceName: "Sample Data",
resourceId: "ta_bb_inventory",
},
actionType: "Create",
buttonPosition: "bottom",
size: "spectrum--medium",
noRowsMessage: "We couldn't find a row to display",
disabled: false,
buttons: [
{
text: "Save",
_id: "ced8cf5175b9c40aabd216f23f072a44c",
_component: "@budibase/standard-components/button",
onClick: [
{
"##eventHandlerType": "Validate Form",
parameters: {
componentId: "c61a1690c6ba0448db504eda38f766db1-form",
},
},
{
"##eventHandlerType": "Save Row",
parameters: {
providerId: "c61a1690c6ba0448db504eda38f766db1-form",
tableId: "ta_bb_inventory",
confirm: null,
},
},
{
"##eventHandlerType": "Close Screen Modal",
},
{
"##eventHandlerType": "Close Side Panel",
},
{
"##eventHandlerType": "Close Modal",
},
{
"##eventHandlerType": "Clear Form",
parameters: {
componentId: "c61a1690c6ba0448db504eda38f766db1-form",
},
},
],
type: "cta",
},
],
fields: null,
title: "Add to inventory",
},
],
},
],
_instanceName: "Inventory - List",
layout: "grid",
direction: "column",
hAlign: "stretch",
vAlign: "top",
size: "grow",
gap: "M",
},
}
}

View File

@ -106,6 +106,7 @@ export const DEFAULT_INVENTORY_TABLE_SCHEMA: Table = {
type: FieldType.NUMBER, type: FieldType.NUMBER,
subtype: AutoFieldSubType.AUTO_ID, subtype: AutoFieldSubType.AUTO_ID,
icon: "ri-magic-line", icon: "ri-magic-line",
width: 120,
autocolumn: true, autocolumn: true,
constraints: { constraints: {
type: FieldType.NUMBER, type: FieldType.NUMBER,
@ -128,6 +129,7 @@ export const DEFAULT_INVENTORY_TABLE_SCHEMA: Table = {
}, },
}, },
name: "Item Name", name: "Item Name",
width: 160,
}, },
"Item Tags": { "Item Tags": {
type: FieldType.ARRAY, type: FieldType.ARRAY,
@ -150,6 +152,7 @@ export const DEFAULT_INVENTORY_TABLE_SCHEMA: Table = {
}, },
name: "Notes", name: "Notes",
useRichText: null, useRichText: null,
width: 220,
}, },
Status: { Status: {
type: FieldType.ARRAY, type: FieldType.ARRAY,
@ -161,6 +164,7 @@ export const DEFAULT_INVENTORY_TABLE_SCHEMA: Table = {
inclusion: ["Available", "Repair", "Broken"], inclusion: ["Available", "Repair", "Broken"],
}, },
name: "Status", name: "Status",
width: 110,
sortable: false, sortable: false,
}, },
SKU: { SKU: {
@ -171,6 +175,7 @@ export const DEFAULT_INVENTORY_TABLE_SCHEMA: Table = {
presence: false, presence: false,
}, },
name: "SKU", name: "SKU",
width: 130,
}, },
"Purchase Date": { "Purchase Date": {
type: FieldType.DATETIME, type: FieldType.DATETIME,
@ -197,6 +202,7 @@ export const DEFAULT_INVENTORY_TABLE_SCHEMA: Table = {
}, },
}, },
name: "Purchase Price", name: "Purchase Price",
width: 160,
}, },
...AUTO_COLUMNS, ...AUTO_COLUMNS,
}, },

View File

@ -2,8 +2,8 @@ export const inventoryImport = [
{ {
Status: ["Available"], Status: ["Available"],
"Item Name": "Little Blue Van", "Item Name": "Little Blue Van",
SKU: "", SKU: "LBV-101",
Notes: "MAX PAYLOAD 595 kg \nMAX LOAD LENGTH 1620 mm", Notes: "Max payload 595 kg \nMax load length 1620 mm",
tableId: "ta_bb_inventory", tableId: "ta_bb_inventory",
"Created At": "2022-11-10T19:11:40.141Z", "Created At": "2022-11-10T19:11:40.141Z",
"Updated At": "2022-11-10T20:05:04.608Z", "Updated At": "2022-11-10T20:05:04.608Z",
@ -29,7 +29,7 @@ export const inventoryImport = [
Status: ["Repair"], Status: ["Repair"],
"Item Name": "Circular saw", "Item Name": "Circular saw",
SKU: "AB2-100", SKU: "AB2-100",
Notes: "", Notes: "Won't start",
tableId: "ta_bb_inventory", tableId: "ta_bb_inventory",
"Created At": "2022-11-10T19:04:38.805Z", "Created At": "2022-11-10T19:04:38.805Z",
"Updated At": "2022-11-10T20:20:24.000Z", "Updated At": "2022-11-10T20:20:24.000Z",
@ -58,7 +58,7 @@ export const inventoryImport = [
Status: ["Available"], Status: ["Available"],
"Item Name": "Power Screwdriver", "Item Name": "Power Screwdriver",
SKU: "TKIT-002-A", SKU: "TKIT-002-A",
Notes: "", Notes: "Requires micro USB charger",
tableId: "ta_bb_inventory", tableId: "ta_bb_inventory",
"Created At": "2022-11-10T20:10:51.129Z", "Created At": "2022-11-10T20:10:51.129Z",
"Updated At": "2022-11-10T20:13:37.821Z", "Updated At": "2022-11-10T20:13:37.821Z",
@ -67,8 +67,8 @@ export const inventoryImport = [
{ {
Status: ["Available"], Status: ["Available"],
"Item Name": "Large Blue Van", "Item Name": "Large Blue Van",
SKU: "", SKU: "LBV-102",
Notes: "MAX LOAD LENGTH 4256 mm", Notes: "Max load length 4256 mm",
tableId: "ta_bb_inventory", tableId: "ta_bb_inventory",
"Created At": "2022-11-10T19:03:41.698Z", "Created At": "2022-11-10T19:03:41.698Z",
"Updated At": "2022-11-10T20:04:57.932Z", "Updated At": "2022-11-10T20:04:57.932Z",
@ -81,7 +81,7 @@ export const inventoryImport = [
"Purchase Price": 2500, "Purchase Price": 2500,
"Purchase Date": "2022-11-09T12:00:00.000", "Purchase Date": "2022-11-09T12:00:00.000",
Status: ["Available"], Status: ["Available"],
"Item Name": "Office Laptop", "Item Name": "Office laptop",
SKU: "PC-123-ABC", SKU: "PC-123-ABC",
Notes: "Office Laptop \n", Notes: "Office Laptop \n",
tableId: "ta_bb_inventory", tableId: "ta_bb_inventory",
@ -93,8 +93,8 @@ export const inventoryImport = [
{ {
Status: ["Available"], Status: ["Available"],
"Item Name": "Little Red Van", "Item Name": "Little Red Van",
SKU: "", SKU: "LRV-904-VNQ",
Notes: "MAX PAYLOAD 595 kg \nMAX LOAD LENGTH 1620 mm", Notes: "Max payload 595 kg \nMax load length 1620 mm",
tableId: "ta_bb_inventory", tableId: "ta_bb_inventory",
"Created At": "2022-11-10T19:55:02.367Z", "Created At": "2022-11-10T19:55:02.367Z",
"Updated At": "2022-11-10T20:05:13.504Z", "Updated At": "2022-11-10T20:05:13.504Z",

View File

@ -226,15 +226,22 @@ export interface components {
type?: "link"; type?: "link";
/** @description A constraint can be applied to the column which will be validated against when a row is saved. */ /** @description A constraint can be applied to the column which will be validated against when a row is saved. */
constraints?: { constraints?: {
/** @enum {string} */ type?: string;
type?: "string" | "number" | "object" | "boolean"; presence?:
/** @description Defines whether the column is required or not. */ | boolean
presence?: boolean; | {
/** @description Defines whether the value is allowed to be empty or not. */
allowEmpty?: boolean;
};
/** @description Defines the valid values for this column. */
inclusion?: unknown[];
}; };
/** @description The name of the column. */ /** @description The name of the column. */
name?: string; name?: string;
/** @description Defines whether the column is automatically generated. */ /** @description Defines whether the column is automatically generated. */
autocolumn?: boolean; autocolumn?: boolean;
/** @description Defines the width of the column in the data UI. */
width?: number;
/** @description The name of the column which a relationship column is related to in another table. */ /** @description The name of the column which a relationship column is related to in another table. */
fieldName?: string; fieldName?: string;
/** @description The ID of the table which a relationship column is related to. */ /** @description The ID of the table which a relationship column is related to. */
@ -261,15 +268,22 @@ export interface components {
type?: "formula"; type?: "formula";
/** @description A constraint can be applied to the column which will be validated against when a row is saved. */ /** @description A constraint can be applied to the column which will be validated against when a row is saved. */
constraints?: { constraints?: {
/** @enum {string} */ type?: string;
type?: "string" | "number" | "object" | "boolean"; presence?:
/** @description Defines whether the column is required or not. */ | boolean
presence?: boolean; | {
/** @description Defines whether the value is allowed to be empty or not. */
allowEmpty?: boolean;
};
/** @description Defines the valid values for this column. */
inclusion?: unknown[];
}; };
/** @description The name of the column. */ /** @description The name of the column. */
name?: string; name?: string;
/** @description Defines whether the column is automatically generated. */ /** @description Defines whether the column is automatically generated. */
autocolumn?: boolean; autocolumn?: boolean;
/** @description Defines the width of the column in the data UI. */
width?: number;
/** @description Defines a Handlebars or JavaScript formula to use, note that Javascript formulas are expected to be provided in the base64 format. */ /** @description Defines a Handlebars or JavaScript formula to use, note that Javascript formulas are expected to be provided in the base64 format. */
formula?: string; formula?: string;
/** /**
@ -280,7 +294,7 @@ export interface components {
} }
| { | {
/** /**
* @description Defines the type of the column, most explain themselves, a link column is a relationship. * @description Defines the type of the column
* @enum {string} * @enum {string}
*/ */
type?: type?:
@ -293,8 +307,6 @@ export interface components {
| "datetime" | "datetime"
| "attachment" | "attachment"
| "attachment_single" | "attachment_single"
| "link"
| "formula"
| "auto" | "auto"
| "ai" | "ai"
| "json" | "json"
@ -306,15 +318,22 @@ export interface components {
| "bb_reference_single"; | "bb_reference_single";
/** @description A constraint can be applied to the column which will be validated against when a row is saved. */ /** @description A constraint can be applied to the column which will be validated against when a row is saved. */
constraints?: { constraints?: {
/** @enum {string} */ type?: string;
type?: "string" | "number" | "object" | "boolean"; presence?:
/** @description Defines whether the column is required or not. */ | boolean
presence?: boolean; | {
/** @description Defines whether the value is allowed to be empty or not. */
allowEmpty?: boolean;
};
/** @description Defines the valid values for this column. */
inclusion?: unknown[];
}; };
/** @description The name of the column. */ /** @description The name of the column. */
name?: string; name?: string;
/** @description Defines whether the column is automatically generated. */ /** @description Defines whether the column is automatically generated. */
autocolumn?: boolean; autocolumn?: boolean;
/** @description Defines the width of the column in the data UI. */
width?: number;
}; };
}; };
}; };
@ -335,15 +354,22 @@ export interface components {
type?: "link"; type?: "link";
/** @description A constraint can be applied to the column which will be validated against when a row is saved. */ /** @description A constraint can be applied to the column which will be validated against when a row is saved. */
constraints?: { constraints?: {
/** @enum {string} */ type?: string;
type?: "string" | "number" | "object" | "boolean"; presence?:
/** @description Defines whether the column is required or not. */ | boolean
presence?: boolean; | {
/** @description Defines whether the value is allowed to be empty or not. */
allowEmpty?: boolean;
};
/** @description Defines the valid values for this column. */
inclusion?: unknown[];
}; };
/** @description The name of the column. */ /** @description The name of the column. */
name?: string; name?: string;
/** @description Defines whether the column is automatically generated. */ /** @description Defines whether the column is automatically generated. */
autocolumn?: boolean; autocolumn?: boolean;
/** @description Defines the width of the column in the data UI. */
width?: number;
/** @description The name of the column which a relationship column is related to in another table. */ /** @description The name of the column which a relationship column is related to in another table. */
fieldName?: string; fieldName?: string;
/** @description The ID of the table which a relationship column is related to. */ /** @description The ID of the table which a relationship column is related to. */
@ -373,15 +399,22 @@ export interface components {
type?: "formula"; type?: "formula";
/** @description A constraint can be applied to the column which will be validated against when a row is saved. */ /** @description A constraint can be applied to the column which will be validated against when a row is saved. */
constraints?: { constraints?: {
/** @enum {string} */ type?: string;
type?: "string" | "number" | "object" | "boolean"; presence?:
/** @description Defines whether the column is required or not. */ | boolean
presence?: boolean; | {
/** @description Defines whether the value is allowed to be empty or not. */
allowEmpty?: boolean;
};
/** @description Defines the valid values for this column. */
inclusion?: unknown[];
}; };
/** @description The name of the column. */ /** @description The name of the column. */
name?: string; name?: string;
/** @description Defines whether the column is automatically generated. */ /** @description Defines whether the column is automatically generated. */
autocolumn?: boolean; autocolumn?: boolean;
/** @description Defines the width of the column in the data UI. */
width?: number;
/** @description Defines a Handlebars or JavaScript formula to use, note that Javascript formulas are expected to be provided in the base64 format. */ /** @description Defines a Handlebars or JavaScript formula to use, note that Javascript formulas are expected to be provided in the base64 format. */
formula?: string; formula?: string;
/** /**
@ -392,7 +425,7 @@ export interface components {
} }
| { | {
/** /**
* @description Defines the type of the column, most explain themselves, a link column is a relationship. * @description Defines the type of the column
* @enum {string} * @enum {string}
*/ */
type?: type?:
@ -405,8 +438,6 @@ export interface components {
| "datetime" | "datetime"
| "attachment" | "attachment"
| "attachment_single" | "attachment_single"
| "link"
| "formula"
| "auto" | "auto"
| "ai" | "ai"
| "json" | "json"
@ -418,15 +449,22 @@ export interface components {
| "bb_reference_single"; | "bb_reference_single";
/** @description A constraint can be applied to the column which will be validated against when a row is saved. */ /** @description A constraint can be applied to the column which will be validated against when a row is saved. */
constraints?: { constraints?: {
/** @enum {string} */ type?: string;
type?: "string" | "number" | "object" | "boolean"; presence?:
/** @description Defines whether the column is required or not. */ | boolean
presence?: boolean; | {
/** @description Defines whether the value is allowed to be empty or not. */
allowEmpty?: boolean;
};
/** @description Defines the valid values for this column. */
inclusion?: unknown[];
}; };
/** @description The name of the column. */ /** @description The name of the column. */
name?: string; name?: string;
/** @description Defines whether the column is automatically generated. */ /** @description Defines whether the column is automatically generated. */
autocolumn?: boolean; autocolumn?: boolean;
/** @description Defines the width of the column in the data UI. */
width?: number;
}; };
}; };
/** @description The ID of the table. */ /** @description The ID of the table. */
@ -449,15 +487,22 @@ export interface components {
type?: "link"; type?: "link";
/** @description A constraint can be applied to the column which will be validated against when a row is saved. */ /** @description A constraint can be applied to the column which will be validated against when a row is saved. */
constraints?: { constraints?: {
/** @enum {string} */ type?: string;
type?: "string" | "number" | "object" | "boolean"; presence?:
/** @description Defines whether the column is required or not. */ | boolean
presence?: boolean; | {
/** @description Defines whether the value is allowed to be empty or not. */
allowEmpty?: boolean;
};
/** @description Defines the valid values for this column. */
inclusion?: unknown[];
}; };
/** @description The name of the column. */ /** @description The name of the column. */
name?: string; name?: string;
/** @description Defines whether the column is automatically generated. */ /** @description Defines whether the column is automatically generated. */
autocolumn?: boolean; autocolumn?: boolean;
/** @description Defines the width of the column in the data UI. */
width?: number;
/** @description The name of the column which a relationship column is related to in another table. */ /** @description The name of the column which a relationship column is related to in another table. */
fieldName?: string; fieldName?: string;
/** @description The ID of the table which a relationship column is related to. */ /** @description The ID of the table which a relationship column is related to. */
@ -487,15 +532,22 @@ export interface components {
type?: "formula"; type?: "formula";
/** @description A constraint can be applied to the column which will be validated against when a row is saved. */ /** @description A constraint can be applied to the column which will be validated against when a row is saved. */
constraints?: { constraints?: {
/** @enum {string} */ type?: string;
type?: "string" | "number" | "object" | "boolean"; presence?:
/** @description Defines whether the column is required or not. */ | boolean
presence?: boolean; | {
/** @description Defines whether the value is allowed to be empty or not. */
allowEmpty?: boolean;
};
/** @description Defines the valid values for this column. */
inclusion?: unknown[];
}; };
/** @description The name of the column. */ /** @description The name of the column. */
name?: string; name?: string;
/** @description Defines whether the column is automatically generated. */ /** @description Defines whether the column is automatically generated. */
autocolumn?: boolean; autocolumn?: boolean;
/** @description Defines the width of the column in the data UI. */
width?: number;
/** @description Defines a Handlebars or JavaScript formula to use, note that Javascript formulas are expected to be provided in the base64 format. */ /** @description Defines a Handlebars or JavaScript formula to use, note that Javascript formulas are expected to be provided in the base64 format. */
formula?: string; formula?: string;
/** /**
@ -506,7 +558,7 @@ export interface components {
} }
| { | {
/** /**
* @description Defines the type of the column, most explain themselves, a link column is a relationship. * @description Defines the type of the column
* @enum {string} * @enum {string}
*/ */
type?: type?:
@ -519,8 +571,6 @@ export interface components {
| "datetime" | "datetime"
| "attachment" | "attachment"
| "attachment_single" | "attachment_single"
| "link"
| "formula"
| "auto" | "auto"
| "ai" | "ai"
| "json" | "json"
@ -532,15 +582,22 @@ export interface components {
| "bb_reference_single"; | "bb_reference_single";
/** @description A constraint can be applied to the column which will be validated against when a row is saved. */ /** @description A constraint can be applied to the column which will be validated against when a row is saved. */
constraints?: { constraints?: {
/** @enum {string} */ type?: string;
type?: "string" | "number" | "object" | "boolean"; presence?:
/** @description Defines whether the column is required or not. */ | boolean
presence?: boolean; | {
/** @description Defines whether the value is allowed to be empty or not. */
allowEmpty?: boolean;
};
/** @description Defines the valid values for this column. */
inclusion?: unknown[];
}; };
/** @description The name of the column. */ /** @description The name of the column. */
name?: string; name?: string;
/** @description Defines whether the column is automatically generated. */ /** @description Defines whether the column is automatically generated. */
autocolumn?: boolean; autocolumn?: boolean;
/** @description Defines the width of the column in the data UI. */
width?: number;
}; };
}; };
/** @description The ID of the table. */ /** @description The ID of the table. */

View File

@ -5,6 +5,8 @@ import {
EnrichedQueryJson, EnrichedQueryJson,
FieldType, FieldType,
isLogicalSearchOperator, isLogicalSearchOperator,
LockName,
LockType,
Operation, Operation,
QueryJson, QueryJson,
RelationshipFieldMetadata, RelationshipFieldMetadata,
@ -30,6 +32,7 @@ import {
} from "../../../tables/internal/sqs" } from "../../../tables/internal/sqs"
import { import {
context, context,
locks,
sql, sql,
SQLITE_DESIGN_DOC_ID, SQLITE_DESIGN_DOC_ID,
SQS_DATASOURCE_INTERNAL, SQS_DATASOURCE_INTERNAL,
@ -509,7 +512,14 @@ export async function search(
} catch (err: any) { } catch (err: any) {
const msg = typeof err === "string" ? err : err.message const msg = typeof err === "string" ? err : err.message
if (!opts?.retrying && resyncDefinitionsRequired(err.status, msg)) { if (!opts?.retrying && resyncDefinitionsRequired(err.status, msg)) {
await sdk.tables.sqs.syncDefinition() await locks.doWithLock(
{
type: LockType.AUTO_EXTEND,
name: LockName.SQS_SYNC_DEFINITIONS,
resource: context.getAppId(),
},
sdk.tables.sqs.syncDefinition
)
return search(options, source, { retrying: true }) return search(options, source, { retrying: true })
} }
// previously the internal table didn't error when a column didn't exist in search // previously the internal table didn't error when a column didn't exist in search

View File

@ -1,6 +1,10 @@
import { EnrichedBinding } from "../../ui"
export interface GenerateJsRequest { export interface GenerateJsRequest {
prompt: string prompt: string
bindings?: EnrichedBinding[]
} }
export interface GenerateJsResponse { export interface GenerateJsResponse {
code: string code: string
} }

View File

@ -1,4 +1,5 @@
import { Document } from "../document" import { Document } from "../document"
import { ContextUser } from "../../sdk"
// SSO // SSO
@ -32,7 +33,7 @@ export interface UserSSO {
export type SSOUser = User & UserSSO export type SSOUser = User & UserSSO
export function isSSOUser(user: User): user is SSOUser { export function isSSOUser(user: User | ContextUser): user is SSOUser {
return !!(user as SSOUser).providerType return !!(user as SSOUser).providerType
} }

View File

@ -22,6 +22,7 @@ export enum LockName {
QUOTA_USAGE_EVENT = "quota_usage_event", QUOTA_USAGE_EVENT = "quota_usage_event",
APP_MIGRATION = "app_migrations", APP_MIGRATION = "app_migrations",
PROCESS_USER_INVITE = "process_user_invite", PROCESS_USER_INVITE = "process_user_invite",
SQS_SYNC_DEFINITIONS = "sys_sync_definitions",
} }
export type LockOptions = { export type LockOptions = {