Merge branch 'master' into server-ts-2

This commit is contained in:
Sam Rose 2025-01-20 11:25:38 +00:00 committed by GitHub
commit e06734efe8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 572 additions and 262 deletions

View File

@ -1,6 +1,6 @@
{ {
"$schema": "node_modules/lerna/schemas/lerna-schema.json", "$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "3.2.44", "version": "3.2.46",
"npmClient": "yarn", "npmClient": "yarn",
"concurrency": 20, "concurrency": 20,
"command": { "command": {

View File

@ -3,7 +3,7 @@
"description": "A UI solution used in the different Budibase projects.", "description": "A UI solution used in the different Budibase projects.",
"version": "0.0.0", "version": "0.0.0",
"license": "MPL-2.0", "license": "MPL-2.0",
"svelte": "src/index.js", "svelte": "src/index.ts",
"module": "dist/bbui.mjs", "module": "dist/bbui.mjs",
"exports": { "exports": {
".": { ".": {
@ -14,7 +14,8 @@
"./spectrum-icons-vite.js": "./src/spectrum-icons-vite.js" "./spectrum-icons-vite.js": "./src/spectrum-icons-vite.js"
}, },
"scripts": { "scripts": {
"build": "vite build" "build": "vite build",
"dev": "vite build --watch --mode=dev"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "1.4.0", "@sveltejs/vite-plugin-svelte": "1.4.0",

View File

@ -10,12 +10,12 @@
export let size = "M" export let size = "M"
export let hoverable = false export let hoverable = false
export let disabled = false export let disabled = false
export let color export let color = undefined
export let hoverColor export let hoverColor = undefined
export let tooltip export let tooltip = undefined
export let tooltipPosition = TooltipPosition.Bottom export let tooltipPosition = TooltipPosition.Bottom
export let tooltipType = TooltipType.Default export let tooltipType = TooltipType.Default
export let tooltipColor export let tooltipColor = undefined
export let tooltipWrap = true export let tooltipWrap = true
export let newStyles = false export let newStyles = false
</script> </script>

View File

@ -4,7 +4,7 @@
export let size = "M" export let size = "M"
export let tooltip = "" export let tooltip = ""
export let muted export let muted = undefined
</script> </script>
<TooltipWrapper {tooltip} {size}> <TooltipWrapper {tooltip} {size}>

View File

@ -2,10 +2,10 @@
import "@spectrum-css/typography/dist/index-vars.css" import "@spectrum-css/typography/dist/index-vars.css"
// Sizes // Sizes
export let size = "M" export let size: "XS" | "S" | "M" | "L" = "M"
export let textAlign = undefined export let textAlign: string | undefined = undefined
export let noPadding = false export let noPadding: boolean = false
export let weight = "default" // light, heavy, default export let weight: "light" | "heavy" | "default" = "default"
</script> </script>
<h1 <h1

View File

@ -1,3 +0,0 @@
declare module "./helpers" {
export const cloneDeep: <T>(obj: T) => T
}

View File

@ -6,9 +6,8 @@ export const deepGet = helpers.deepGet
/** /**
* Generates a DOM safe UUID. * Generates a DOM safe UUID.
* Starting with a letter is important to make it DOM safe. * Starting with a letter is important to make it DOM safe.
* @return {string} a random DOM safe UUID
*/ */
export function uuid() { export function uuid(): string {
return "cxxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, c => { return "cxxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, c => {
const r = (Math.random() * 16) | 0 const r = (Math.random() * 16) | 0
const v = c === "x" ? r : (r & 0x3) | 0x8 const v = c === "x" ? r : (r & 0x3) | 0x8
@ -18,22 +17,18 @@ export function uuid() {
/** /**
* Capitalises a string * Capitalises a string
* @param string the string to capitalise
* @return {string} the capitalised string
*/ */
export const capitalise = string => { export const capitalise = (string?: string | null): string => {
if (!string) { if (!string) {
return string return ""
} }
return string.substring(0, 1).toUpperCase() + string.substring(1) return string.substring(0, 1).toUpperCase() + string.substring(1)
} }
/** /**
* Computes a short hash of a string * Computes a short hash of a string
* @param string the string to compute a hash of
* @return {string} the hash string
*/ */
export const hashString = string => { export const hashString = (string?: string | null): string => {
if (!string) { if (!string) {
return "0" return "0"
} }
@ -54,11 +49,12 @@ export const hashString = string => {
* will override the value "foo" rather than "bar". * will override the value "foo" rather than "bar".
* If a deep path is specified and the parent keys don't exist then these will * If a deep path is specified and the parent keys don't exist then these will
* be created. * be created.
* @param obj the object
* @param key the key
* @param value the value
*/ */
export const deepSet = (obj, key, value) => { export const deepSet = (
obj: Record<string, any> | null,
key: string | null,
value: any
): void => {
if (!obj || !key) { if (!obj || !key) {
return return
} }
@ -82,9 +78,8 @@ export const deepSet = (obj, key, value) => {
/** /**
* Deeply clones an object. Functions are not supported. * Deeply clones an object. Functions are not supported.
* @param obj the object to clone
*/ */
export const cloneDeep = obj => { export const cloneDeep = <T>(obj: T): T => {
if (!obj) { if (!obj) {
return obj return obj
} }
@ -93,9 +88,8 @@ export const cloneDeep = obj => {
/** /**
* Copies a value to the clipboard * Copies a value to the clipboard
* @param value the value to copy
*/ */
export const copyToClipboard = value => { export const copyToClipboard = (value: any): Promise<void> => {
return new Promise(res => { return new Promise(res => {
if (navigator.clipboard && window.isSecureContext) { if (navigator.clipboard && window.isSecureContext) {
// Try using the clipboard API first // Try using the clipboard API first
@ -117,9 +111,12 @@ export const copyToClipboard = value => {
}) })
} }
// Parsed a date value. This is usually an ISO string, but can be a // Parse a date value. This is usually an ISO string, but can be a
// bunch of different formats and shapes depending on schema flags. // bunch of different formats and shapes depending on schema flags.
export const parseDate = (value, { enableTime = true }) => { export const parseDate = (
value: string | dayjs.Dayjs | null,
{ enableTime = true }
): dayjs.Dayjs | null => {
// If empty then invalid // If empty then invalid
if (!value) { if (!value) {
return null return null
@ -128,7 +125,7 @@ export const parseDate = (value, { enableTime = true }) => {
// Certain string values need transformed // Certain string values need transformed
if (typeof value === "string") { if (typeof value === "string") {
// Check for time only values // Check for time only values
if (!isNaN(new Date(`0-${value}`))) { if (!isNaN(new Date(`0-${value}`).valueOf())) {
value = `0-${value}` value = `0-${value}`
} }
@ -153,9 +150,9 @@ export const parseDate = (value, { enableTime = true }) => {
// Stringifies a dayjs object to create an ISO string that respects the various // Stringifies a dayjs object to create an ISO string that respects the various
// schema flags // schema flags
export const stringifyDate = ( export const stringifyDate = (
value, value: null | dayjs.Dayjs,
{ enableTime = true, timeOnly = false, ignoreTimezones = false } = {} { enableTime = true, timeOnly = false, ignoreTimezones = false } = {}
) => { ): string | null => {
if (!value) { if (!value) {
return null return null
} }
@ -192,7 +189,7 @@ export const stringifyDate = (
} }
// Determine the dayjs-compatible format of the browser's default locale // Determine the dayjs-compatible format of the browser's default locale
const getPatternForPart = part => { const getPatternForPart = (part: Intl.DateTimeFormatPart): string => {
switch (part.type) { switch (part.type) {
case "day": case "day":
return "D".repeat(part.value.length) return "D".repeat(part.value.length)
@ -214,9 +211,9 @@ const localeDateFormat = new Intl.DateTimeFormat()
// Formats a dayjs date according to schema flags // Formats a dayjs date according to schema flags
export const getDateDisplayValue = ( export const getDateDisplayValue = (
value, value: dayjs.Dayjs | null,
{ enableTime = true, timeOnly = false } = {} { enableTime = true, timeOnly = false } = {}
) => { ): string => {
if (!value?.isValid()) { if (!value?.isValid()) {
return "" return ""
} }
@ -229,7 +226,7 @@ export const getDateDisplayValue = (
} }
} }
export const hexToRGBA = (color, opacity) => { export const hexToRGBA = (color: string, opacity: number): string => {
if (color.includes("#")) { if (color.includes("#")) {
color = color.replace("#", "") color = color.replace("#", "")
} }

View File

@ -0,0 +1,7 @@
const { vitePreprocess } = require("@sveltejs/vite-plugin-svelte")
const config = {
preprocess: vitePreprocess(),
}
module.exports = config

View File

@ -0,0 +1,19 @@
{
"extends": "../../tsconfig.build.json",
"compilerOptions": {
"allowJs": true,
"outDir": "./dist",
"lib": ["ESNext"],
"baseUrl": ".",
"paths": {
"@budibase/*": [
"../*/src/index.ts",
"../*/src/index.js",
"../*",
"../../node_modules/@budibase/*"
]
}
},
"include": ["./src/**/*"],
"exclude": ["node_modules", "**/*.json", "**/*.spec.ts", "**/*.spec.js"]
}

View File

@ -9,7 +9,7 @@ export default defineConfig(({ mode }) => {
build: { build: {
sourcemap: !isProduction, sourcemap: !isProduction,
lib: { lib: {
entry: "src/index.js", entry: "src/index.ts",
formats: ["es"], formats: ["es"],
}, },
}, },

View File

@ -74,7 +74,6 @@
"dayjs": "^1.10.8", "dayjs": "^1.10.8",
"downloadjs": "1.4.7", "downloadjs": "1.4.7",
"fast-json-patch": "^3.1.1", "fast-json-patch": "^3.1.1",
"json-format-highlight": "^1.0.4",
"lodash": "4.17.21", "lodash": "4.17.21",
"posthog-js": "^1.118.0", "posthog-js": "^1.118.0",
"remixicon": "2.5.0", "remixicon": "2.5.0",
@ -94,6 +93,7 @@
"@sveltejs/vite-plugin-svelte": "1.4.0", "@sveltejs/vite-plugin-svelte": "1.4.0",
"@testing-library/jest-dom": "6.4.2", "@testing-library/jest-dom": "6.4.2",
"@testing-library/svelte": "^4.1.0", "@testing-library/svelte": "^4.1.0",
"@types/sanitize-html": "^2.13.0",
"@types/shortid": "^2.2.0", "@types/shortid": "^2.2.0",
"babel-jest": "^29.6.2", "babel-jest": "^29.6.2",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",

View File

@ -9,7 +9,7 @@
} from "@budibase/bbui" } from "@budibase/bbui"
import { onMount, createEventDispatcher } from "svelte" import { onMount, createEventDispatcher } from "svelte"
import { flags } from "@/stores/builder" import { flags } from "@/stores/builder"
import { featureFlags, licensing } from "@/stores/portal" import { licensing } from "@/stores/portal"
import { API } from "@/api" import { API } from "@/api"
import MagicWand from "../../../../assets/MagicWand.svelte" import MagicWand from "../../../../assets/MagicWand.svelte"
@ -27,8 +27,7 @@
let loadingAICronExpression = false let loadingAICronExpression = false
$: aiEnabled = $: aiEnabled =
($featureFlags.AI_CUSTOM_CONFIGS && $licensing.customAIConfigsEnabled) || $licensing.customAIConfigsEnabled || $licensing.budibaseAIEnabled
($featureFlags.BUDIBASE_AI && $licensing.budibaseAIEnabled)
$: { $: {
if (cronExpression) { if (cronExpression) {
try { try {

View File

@ -26,7 +26,7 @@
import { createEventDispatcher, getContext, onMount } from "svelte" import { createEventDispatcher, getContext, onMount } from "svelte"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { tables, datasources } from "@/stores/builder" import { tables, datasources } from "@/stores/builder"
import { featureFlags } from "@/stores/portal" import { licensing } from "@/stores/portal"
import { TableNames, UNEDITABLE_USER_FIELDS } from "@/constants" import { TableNames, UNEDITABLE_USER_FIELDS } from "@/constants"
import { import {
FIELDS, FIELDS,
@ -100,7 +100,8 @@
let optionsValid = true let optionsValid = true
$: rowGoldenSample = RowUtils.generateGoldenSample($rows) $: rowGoldenSample = RowUtils.generateGoldenSample($rows)
$: aiEnabled = $featureFlags.BUDIBASE_AI || $featureFlags.AI_CUSTOM_CONFIGS $: aiEnabled =
$licensing.customAIConfigsEnabled || $licensing.budibaseAiEnabled
$: if (primaryDisplay) { $: if (primaryDisplay) {
editableColumn.constraints.presence = { allowEmpty: false } editableColumn.constraints.presence = { allowEmpty: false }
} }

View File

@ -1,4 +1,4 @@
<script> <script lang="ts">
import { Label } from "@budibase/bbui" import { Label } from "@budibase/bbui"
import { onMount, createEventDispatcher, onDestroy } from "svelte" import { onMount, createEventDispatcher, onDestroy } from "svelte"
import { FIND_ANY_HBS_REGEX } from "@budibase/string-templates" import { FIND_ANY_HBS_REGEX } from "@budibase/string-templates"
@ -12,7 +12,6 @@
completionStatus, completionStatus,
} from "@codemirror/autocomplete" } from "@codemirror/autocomplete"
import { import {
EditorView,
lineNumbers, lineNumbers,
keymap, keymap,
highlightSpecialChars, highlightSpecialChars,
@ -25,6 +24,7 @@
MatchDecorator, MatchDecorator,
ViewPlugin, ViewPlugin,
Decoration, Decoration,
EditorView,
} from "@codemirror/view" } from "@codemirror/view"
import { import {
bracketMatching, bracketMatching,
@ -44,12 +44,14 @@
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 type { EditorMode } from "@budibase/types"
export let label export let label: string | undefined = undefined
export let completions = [] // TODO: work out what best type fits this
export let mode = EditorModes.Handlebars export let completions: any[] = []
export let value = "" export let mode: EditorMode = EditorModes.Handlebars
export let placeholder = null export let value: string | null = ""
export let placeholder: string | null = null
export let autocompleteEnabled = true export let autocompleteEnabled = true
export let autofocus = false export let autofocus = false
export let jsBindingWrapping = true export let jsBindingWrapping = true
@ -58,8 +60,8 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let textarea let textarea: HTMLDivElement
let editor let editor: EditorView
let mounted = false let mounted = false
let isEditorInitialised = false let isEditorInitialised = false
let queuedRefresh = false let queuedRefresh = false
@ -100,15 +102,22 @@
/** /**
* Will refresh the editor contents only after * Will refresh the editor contents only after
* it has been fully initialised * it has been fully initialised
* @param value {string} the editor value
*/ */
const refresh = (value, initialised, mounted) => { const refresh = (
value: string | null,
initialised?: boolean,
mounted?: boolean
) => {
if (!initialised || !mounted) { if (!initialised || !mounted) {
queuedRefresh = true queuedRefresh = true
return return
} }
if (editor.state.doc.toString() !== value || queuedRefresh) { if (
editor &&
value &&
(editor.state.doc.toString() !== value || queuedRefresh)
) {
editor.dispatch({ editor.dispatch({
changes: { from: 0, to: editor.state.doc.length, insert: value }, changes: { from: 0, to: editor.state.doc.length, insert: value },
}) })
@ -120,12 +129,17 @@
export const getCaretPosition = () => { export const getCaretPosition = () => {
const selection_range = editor.state.selection.ranges[0] const selection_range = editor.state.selection.ranges[0]
return { return {
start: selection_range.from, start: selection_range?.from,
end: selection_range.to, end: selection_range?.to,
} }
} }
export const insertAtPos = opts => { export const insertAtPos = (opts: {
start: number
end?: number
value: string
cursor: { anchor: number }
}) => {
// Updating the value inside. // Updating the value inside.
// Retain focus // Retain focus
editor.dispatch({ editor.dispatch({
@ -192,7 +206,7 @@
const indentWithTabCustom = { const indentWithTabCustom = {
key: "Tab", key: "Tab",
run: view => { run: (view: EditorView) => {
if (completionStatus(view.state) === "active") { if (completionStatus(view.state) === "active") {
acceptCompletion(view) acceptCompletion(view)
return true return true
@ -200,7 +214,7 @@
indentMore(view) indentMore(view)
return true return true
}, },
shift: view => { shift: (view: EditorView) => {
indentLess(view) indentLess(view)
return true return true
}, },
@ -232,7 +246,8 @@
// None of this is reactive, but it never has been, so we just assume most // None of this is reactive, but it never has been, so we just assume most
// config flags aren't changed at runtime // config flags aren't changed at runtime
const buildExtensions = base => { // TODO: work out type for base
const buildExtensions = (base: any[]) => {
let complete = [...base] let complete = [...base]
if (autocompleteEnabled) { if (autocompleteEnabled) {
@ -242,7 +257,7 @@
closeOnBlur: true, closeOnBlur: true,
icons: false, icons: false,
optionClass: completion => optionClass: completion =>
completion.simple "simple" in completion && completion.simple
? "autocomplete-option-simple" ? "autocomplete-option-simple"
: "autocomplete-option", : "autocomplete-option",
}) })
@ -347,7 +362,7 @@
{#if label} {#if label}
<div> <div>
<Label small>{label}</Label> <Label size="S">{label}</Label>
</div> </div>
{/if} {/if}

View File

@ -1,8 +1,15 @@
import { getManifest } from "@budibase/string-templates" import { getManifest } from "@budibase/string-templates"
import sanitizeHtml from "sanitize-html" import sanitizeHtml from "sanitize-html"
import { groupBy } from "lodash" import { groupBy } from "lodash"
import {
BindingCompletion,
EditorModesMap,
Helper,
Snippet,
} from "@budibase/types"
import { CompletionContext } from "@codemirror/autocomplete"
export const EditorModes = { export const EditorModes: EditorModesMap = {
JS: { JS: {
name: "javascript", name: "javascript",
json: false, json: false,
@ -26,7 +33,7 @@ export const SECTIONS = {
}, },
} }
export const buildHelperInfoNode = (completion, helper) => { export const buildHelperInfoNode = (completion: any, helper: Helper) => {
const ele = document.createElement("div") const ele = document.createElement("div")
ele.classList.add("info-bubble") ele.classList.add("info-bubble")
@ -46,7 +53,7 @@ export const buildHelperInfoNode = (completion, helper) => {
return ele return ele
} }
const toSpectrumIcon = name => { const toSpectrumIcon = (name: string) => {
return `<svg return `<svg
class="spectrum-Icon spectrum-Icon--sizeS" class="spectrum-Icon spectrum-Icon--sizeS"
focusable="false" focusable="false"
@ -58,7 +65,12 @@ const toSpectrumIcon = name => {
</svg>` </svg>`
} }
export const buildSectionHeader = (type, sectionName, icon, rank) => { export const buildSectionHeader = (
type: string,
sectionName: string,
icon: string,
rank: number
) => {
const ele = document.createElement("div") const ele = document.createElement("div")
ele.classList.add("info-section") ele.classList.add("info-section")
if (type) { if (type) {
@ -72,43 +84,52 @@ export const buildSectionHeader = (type, sectionName, icon, rank) => {
} }
} }
export const helpersToCompletion = (helpers, mode) => { export const helpersToCompletion = (
helpers: Record<string, Helper>,
mode: { name: "javascript" | "handlebars" }
) => {
const { type, name: sectionName, icon } = SECTIONS.HB_HELPER const { type, name: sectionName, icon } = SECTIONS.HB_HELPER
const helperSection = buildSectionHeader(type, sectionName, icon, 99) const helperSection = buildSectionHeader(type, sectionName, icon, 99)
return Object.keys(helpers).reduce((acc, key) => { return Object.keys(helpers).flatMap(helperName => {
let helper = helpers[key] let helper = helpers[helperName]
acc.push({ return {
label: key, label: helperName,
info: completion => { info: (completion: BindingCompletion) => {
return buildHelperInfoNode(completion, helper) return buildHelperInfoNode(completion, helper)
}, },
type: "helper", type: "helper",
section: helperSection, section: helperSection,
detail: "Function", detail: "Function",
apply: (view, completion, from, to) => { apply: (
insertBinding(view, from, to, key, mode) view: any,
completion: BindingCompletion,
from: number,
to: number
) => {
insertBinding(view, from, to, helperName, mode)
}, },
}
}) })
return acc
}, [])
} }
export const getHelperCompletions = mode => { export const getHelperCompletions = (mode: {
const manifest = getManifest() name: "javascript" | "handlebars"
return Object.keys(manifest).reduce((acc, key) => { }) => {
acc = acc || [] // TODO: manifest needs to be properly typed
return [...acc, ...helpersToCompletion(manifest[key], mode)] const manifest: any = getManifest()
}, []) return Object.keys(manifest).flatMap(key => {
return helpersToCompletion(manifest[key], mode)
})
} }
export const snippetAutoComplete = snippets => { export const snippetAutoComplete = (snippets: Snippet[]) => {
return function myCompletions(context) { return function myCompletions(context: CompletionContext) {
if (!snippets?.length) { if (!snippets?.length) {
return null return null
} }
const word = context.matchBefore(/\w*/) const word = context.matchBefore(/\w*/)
if (word.from == word.to && !context.explicit) { if (!word || (word.from == word.to && !context.explicit)) {
return null return null
} }
return { return {
@ -117,7 +138,12 @@ export const snippetAutoComplete = snippets => {
label: `snippets.${snippet.name}`, label: `snippets.${snippet.name}`,
type: "text", type: "text",
simple: true, simple: true,
apply: (view, completion, from, to) => { apply: (
view: any,
completion: BindingCompletion,
from: number,
to: number
) => {
insertSnippet(view, from, to, completion.label) insertSnippet(view, from, to, completion.label)
}, },
})), })),
@ -125,7 +151,7 @@ export const snippetAutoComplete = snippets => {
} }
} }
const bindingFilter = (options, query) => { const bindingFilter = (options: BindingCompletion[], query: string) => {
return options.filter(completion => { return options.filter(completion => {
const section_parsed = completion.section.name.toLowerCase() const section_parsed = completion.section.name.toLowerCase()
const label_parsed = completion.label.toLowerCase() const label_parsed = completion.label.toLowerCase()
@ -138,8 +164,8 @@ const bindingFilter = (options, query) => {
}) })
} }
export const hbAutocomplete = baseCompletions => { export const hbAutocomplete = (baseCompletions: BindingCompletion[]) => {
async function coreCompletion(context) { async function coreCompletion(context: CompletionContext) {
let bindingStart = context.matchBefore(EditorModes.Handlebars.match) let bindingStart = context.matchBefore(EditorModes.Handlebars.match)
let options = baseCompletions || [] let options = baseCompletions || []
@ -149,6 +175,9 @@ export const hbAutocomplete = baseCompletions => {
} }
// Accommodate spaces // Accommodate spaces
const match = bindingStart.text.match(/{{[\s]*/) const match = bindingStart.text.match(/{{[\s]*/)
if (!match) {
return null
}
const query = bindingStart.text.replace(match[0], "") const query = bindingStart.text.replace(match[0], "")
let filtered = bindingFilter(options, query) let filtered = bindingFilter(options, query)
@ -162,14 +191,17 @@ export const hbAutocomplete = baseCompletions => {
return coreCompletion return coreCompletion
} }
export const jsAutocomplete = baseCompletions => { export const jsAutocomplete = (baseCompletions: BindingCompletion[]) => {
async function coreCompletion(context) { async function coreCompletion(context: CompletionContext) {
let jsBinding = context.matchBefore(/\$\("[\s\w]*/) let jsBinding = context.matchBefore(/\$\("[\s\w]*/)
let options = baseCompletions || [] let options = baseCompletions || []
if (jsBinding) { if (jsBinding) {
// Accommodate spaces // Accommodate spaces
const match = jsBinding.text.match(/\$\("[\s]*/) const match = jsBinding.text.match(/\$\("[\s]*/)
if (!match) {
return null
}
const query = jsBinding.text.replace(match[0], "") const query = jsBinding.text.replace(match[0], "")
let filtered = bindingFilter(options, query) let filtered = bindingFilter(options, query)
return { return {
@ -185,7 +217,10 @@ export const jsAutocomplete = baseCompletions => {
return coreCompletion return coreCompletion
} }
export const buildBindingInfoNode = (completion, binding) => { export const buildBindingInfoNode = (
completion: BindingCompletion,
binding: any
) => {
if (!binding.valueHTML || binding.value == null) { if (!binding.valueHTML || binding.value == null) {
return null return null
} }
@ -196,7 +231,12 @@ export const buildBindingInfoNode = (completion, binding) => {
} }
// Readdress these methods. They shouldn't be used // Readdress these methods. They shouldn't be used
export const hbInsert = (value, from, to, text) => { export const hbInsert = (
value: string,
from: number,
to: number,
text: string
) => {
let parsedInsert = "" let parsedInsert = ""
const left = from ? value.substring(0, from) : "" const left = from ? value.substring(0, from) : ""
@ -212,11 +252,14 @@ export const hbInsert = (value, from, to, text) => {
} }
export function jsInsert( export function jsInsert(
value, value: string,
from, from: number,
to, to: number,
text, text: string,
{ helper, disableWrapping } = {} {
helper,
disableWrapping,
}: { helper?: boolean; disableWrapping?: boolean } = {}
) { ) {
let parsedInsert = "" let parsedInsert = ""
@ -236,7 +279,13 @@ export function jsInsert(
} }
// Autocomplete apply behaviour // Autocomplete apply behaviour
export const insertBinding = (view, from, to, text, mode) => { export const insertBinding = (
view: any,
from: number,
to: number,
text: string,
mode: { name: "javascript" | "handlebars" }
) => {
let parsedInsert let parsedInsert
if (mode.name == "javascript") { if (mode.name == "javascript") {
@ -270,7 +319,12 @@ export const insertBinding = (view, from, to, text, mode) => {
}) })
} }
export const insertSnippet = (view, from, to, text) => { export const insertSnippet = (
view: any,
from: number,
to: number,
text: string
) => {
let cursorPos = from + text.length let cursorPos = from + text.length
view.dispatch({ view.dispatch({
changes: { changes: {
@ -284,9 +338,13 @@ export const insertSnippet = (view, from, to, text) => {
}) })
} }
export const bindingsToCompletions = (bindings, mode) => { // TODO: typing in this function isn't great
export const bindingsToCompletions = (
bindings: any,
mode: { name: "javascript" | "handlebars" }
) => {
const bindingByCategory = groupBy(bindings, "category") const bindingByCategory = groupBy(bindings, "category")
const categoryMeta = bindings?.reduce((acc, ele) => { const categoryMeta = bindings?.reduce((acc: any, ele: any) => {
acc[ele.category] = acc[ele.category] || {} acc[ele.category] = acc[ele.category] || {}
if (ele.icon) { if (ele.icon) {
@ -298,10 +356,12 @@ export const bindingsToCompletions = (bindings, mode) => {
return acc return acc
}, {}) }, {})
const completions = Object.keys(bindingByCategory).reduce((comps, catKey) => { const completions = Object.keys(bindingByCategory).reduce(
(comps: any, catKey: string) => {
const { icon, rank } = categoryMeta[catKey] || {} const { icon, rank } = categoryMeta[catKey] || {}
const bindindSectionHeader = buildSectionHeader( const bindingSectionHeader = buildSectionHeader(
// @ts-ignore something wrong with this - logically this should be dictionary
bindingByCategory.type, bindingByCategory.type,
catKey, catKey,
icon || "", icon || "",
@ -313,21 +373,29 @@ export const bindingsToCompletions = (bindings, mode) => {
...bindingByCategory[catKey].reduce((acc, binding) => { ...bindingByCategory[catKey].reduce((acc, binding) => {
let displayType = binding.fieldSchema?.type || binding.display?.type let displayType = binding.fieldSchema?.type || binding.display?.type
acc.push({ acc.push({
label: binding.display?.name || binding.readableBinding || "NO NAME", label:
info: completion => { binding.display?.name || binding.readableBinding || "NO NAME",
info: (completion: BindingCompletion) => {
return buildBindingInfoNode(completion, binding) return buildBindingInfoNode(completion, binding)
}, },
type: "binding", type: "binding",
detail: displayType, detail: displayType,
section: bindindSectionHeader, section: bindingSectionHeader,
apply: (view, completion, from, to) => { apply: (
view: any,
completion: BindingCompletion,
from: number,
to: number
) => {
insertBinding(view, from, to, binding.readableBinding, mode) insertBinding(view, from, to, binding.readableBinding, mode)
}, },
}) })
return acc return acc
}, []), }, []),
] ]
}, []) },
[]
)
return completions return completions
} }

View File

@ -1,4 +1,4 @@
<script> <script lang="ts">
import { import {
DrawerContent, DrawerContent,
ActionButton, ActionButton,
@ -28,45 +28,45 @@
import EvaluationSidePanel from "./EvaluationSidePanel.svelte" import EvaluationSidePanel from "./EvaluationSidePanel.svelte"
import SnippetSidePanel from "./SnippetSidePanel.svelte" import SnippetSidePanel from "./SnippetSidePanel.svelte"
import { BindingHelpers } from "./utils" import { BindingHelpers } from "./utils"
import formatHighlight from "json-format-highlight"
import { capitalise } from "@/helpers" import { capitalise } from "@/helpers"
import { Utils } from "@budibase/frontend-core" import { Utils, JsonFormatter } from "@budibase/frontend-core"
import { licensing } from "@/stores/portal" import { licensing } from "@/stores/portal"
import { BindingMode, SidePanel } from "@budibase/types"
import type {
EnrichedBinding,
BindingCompletion,
Snippet,
Helper,
CaretPositionFn,
InsertAtPositionFn,
JSONValue,
} from "@budibase/types"
import type { CompletionContext } from "@codemirror/autocomplete"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export let bindings = [] export let bindings: EnrichedBinding[] = []
export let value = "" export let value: string = ""
export let allowHBS = true export let allowHBS = true
export let allowJS = false export let allowJS = false
export let allowHelpers = true export let allowHelpers = true
export let allowSnippets = true export let allowSnippets = true
export let context = null export let context = null
export let snippets = null export let snippets: Snippet[] | null = null
export let autofocusEditor = false export let autofocusEditor = false
export let placeholder = null export let placeholder = null
export let showTabBar = true export let showTabBar = true
const Modes = { let mode: BindingMode | null
Text: "Text", let sidePanel: SidePanel | null
JavaScript: "JavaScript",
}
const SidePanels = {
Bindings: "FlashOn",
Evaluation: "Play",
Snippets: "Code",
}
let mode
let sidePanel
let initialValueJS = value?.startsWith?.("{{ js ") let initialValueJS = value?.startsWith?.("{{ js ")
let jsValue = initialValueJS ? value : null let jsValue: string | null = initialValueJS ? value : null
let hbsValue = initialValueJS ? null : value let hbsValue: string | null = initialValueJS ? null : value
let getCaretPosition let getCaretPosition: CaretPositionFn | undefined
let insertAtPos let insertAtPos: InsertAtPositionFn | undefined
let targetMode = null let targetMode: BindingMode | null = null
let expressionResult let expressionResult: string | undefined
let expressionError let expressionError: string | undefined
let evaluating = false let evaluating = false
$: useSnippets = allowSnippets && !$licensing.isFreePlan $: useSnippets = allowSnippets && !$licensing.isFreePlan
@ -78,10 +78,12 @@
mode mode
) )
$: enrichedBindings = enrichBindings(bindings, context, snippets) $: enrichedBindings = enrichBindings(bindings, context, snippets)
$: usingJS = mode === Modes.JavaScript $: usingJS = mode === BindingMode.JavaScript
$: editorMode = $: editorMode =
mode === Modes.JavaScript ? EditorModes.JS : EditorModes.Handlebars mode === BindingMode.JavaScript ? EditorModes.JS : EditorModes.Handlebars
$: editorValue = editorMode === EditorModes.JS ? jsValue : hbsValue $: editorValue = (editorMode === EditorModes.JS ? jsValue : hbsValue) as
| string
| null
$: runtimeExpression = readableToRuntimeBinding(enrichedBindings, value) $: runtimeExpression = readableToRuntimeBinding(enrichedBindings, value)
$: requestEval(runtimeExpression, context, snippets) $: requestEval(runtimeExpression, context, snippets)
$: bindingCompletions = bindingsToCompletions(enrichedBindings, editorMode) $: bindingCompletions = bindingsToCompletions(enrichedBindings, editorMode)
@ -95,7 +97,7 @@
} }
} }
const getHBSCompletions = bindingCompletions => { const getHBSCompletions = (bindingCompletions: BindingCompletion[]) => {
return [ return [
hbAutocomplete([ hbAutocomplete([
...bindingCompletions, ...bindingCompletions,
@ -104,47 +106,57 @@
] ]
} }
const getJSCompletions = (bindingCompletions, snippets, useSnippets) => { const getJSCompletions = (
const completions = [ bindingCompletions: BindingCompletion[],
snippets: Snippet[] | null,
useSnippets?: boolean
) => {
const completions: ((_: CompletionContext) => any)[] = [
jsAutocomplete([ jsAutocomplete([
...bindingCompletions, ...bindingCompletions,
...getHelperCompletions(EditorModes.JS), ...getHelperCompletions(EditorModes.JS),
]), ]),
] ]
if (useSnippets) { if (useSnippets && snippets) {
completions.push(snippetAutoComplete(snippets)) completions.push(snippetAutoComplete(snippets))
} }
return completions return completions
} }
const getModeOptions = (allowHBS, allowJS) => { const getModeOptions = (allowHBS: boolean, allowJS: boolean) => {
let options = [] let options = []
if (allowHBS) { if (allowHBS) {
options.push(Modes.Text) options.push(BindingMode.Text)
} }
if (allowJS) { if (allowJS) {
options.push(Modes.JavaScript) options.push(BindingMode.JavaScript)
} }
return options return options
} }
const getSidePanelOptions = (bindings, context, useSnippets, mode) => { const getSidePanelOptions = (
bindings: EnrichedBinding[],
context: any,
useSnippets: boolean,
mode: BindingMode | null
) => {
let options = [] let options = []
if (bindings?.length) { if (bindings?.length) {
options.push(SidePanels.Bindings) options.push(SidePanel.Bindings)
} }
if (context && Object.keys(context).length > 0) { if (context && Object.keys(context).length > 0) {
options.push(SidePanels.Evaluation) options.push(SidePanel.Evaluation)
} }
if (useSnippets && mode === Modes.JavaScript) { if (useSnippets && mode === BindingMode.JavaScript) {
options.push(SidePanels.Snippets) options.push(SidePanel.Snippets)
} }
return options return options
} }
const debouncedEval = Utils.debounce((expression, context, snippets) => { const debouncedEval = Utils.debounce(
(expression: string | null, context: any, snippets: Snippet[]) => {
try { try {
expressionError = null expressionError = undefined
expressionResult = processStringSync( expressionResult = processStringSync(
expression || "", expression || "",
{ {
@ -155,20 +167,26 @@
noThrow: false, noThrow: false,
} }
) )
} catch (err) { } catch (err: any) {
expressionResult = null expressionResult = undefined
expressionError = err expressionError = err
} }
evaluating = false evaluating = false
}, 260) },
260
)
const requestEval = (expression, context, snippets) => { const requestEval = (
expression: string | null,
context: any,
snippets: Snippet[] | null
) => {
evaluating = true evaluating = true
debouncedEval(expression, context, snippets) debouncedEval(expression, context, snippets)
} }
const highlightJSON = json => { const highlightJSON = (json: JSONValue) => {
return formatHighlight(json, { return JsonFormatter.format(json, {
keyColor: "#e06c75", keyColor: "#e06c75",
numberColor: "#e5c07b", numberColor: "#e5c07b",
stringColor: "#98c379", stringColor: "#98c379",
@ -178,7 +196,11 @@
}) })
} }
const enrichBindings = (bindings, context, snippets) => { const enrichBindings = (
bindings: EnrichedBinding[],
context: any,
snippets: Snippet[] | null
) => {
// Create a single big array to enrich in one go // Create a single big array to enrich in one go
const bindingStrings = bindings.map(binding => { const bindingStrings = bindings.map(binding => {
if (binding.runtimeBinding.startsWith('trim "')) { if (binding.runtimeBinding.startsWith('trim "')) {
@ -189,17 +211,18 @@
return `{{ literal ${binding.runtimeBinding} }}` return `{{ literal ${binding.runtimeBinding} }}`
} }
}) })
const bindingEvauations = processObjectSync(bindingStrings, { const bindingEvaluations = processObjectSync(bindingStrings, {
...context, ...context,
snippets, snippets,
}) })
// Enrich bindings with evaluations and highlighted HTML // Enrich bindings with evaluations and highlighted HTML
return bindings.map((binding, idx) => { return bindings.map((binding, idx) => {
if (!context) { if (!context || typeof bindingEvaluations !== "object") {
return binding return binding
} }
const value = JSON.stringify(bindingEvauations[idx], null, 2) const evalObj: Record<any, any> = bindingEvaluations
const value = JSON.stringify(evalObj[idx], null, 2)
return { return {
...binding, ...binding,
value, value,
@ -208,29 +231,38 @@
}) })
} }
const updateValue = val => { const updateValue = (val: any) => {
const runtimeExpression = readableToRuntimeBinding(enrichedBindings, val) const runtimeExpression = readableToRuntimeBinding(enrichedBindings, val)
dispatch("change", val) dispatch("change", val)
requestEval(runtimeExpression, context, snippets) requestEval(runtimeExpression, context, snippets)
} }
const onSelectHelper = (helper, js) => { const onSelectHelper = (helper: Helper, js?: boolean) => {
bindingHelpers.onSelectHelper(js ? jsValue : hbsValue, helper, { js }) bindingHelpers.onSelectHelper(js ? jsValue : hbsValue, helper, {
js,
dontDecode: undefined,
})
} }
const onSelectBinding = (binding, { forceJS } = {}) => { const onSelectBinding = (
binding: EnrichedBinding,
{ forceJS }: { forceJS?: boolean } = {}
) => {
const js = usingJS || forceJS const js = usingJS || forceJS
bindingHelpers.onSelectBinding(js ? jsValue : hbsValue, binding, { js }) bindingHelpers.onSelectBinding(js ? jsValue : hbsValue, binding, {
js,
dontDecode: undefined,
})
} }
const changeMode = newMode => { const changeMode = (newMode: BindingMode) => {
if (targetMode || newMode === mode) { if (targetMode || newMode === mode) {
return return
} }
// Get the raw editor value to see if we are abandoning changes // Get the raw editor value to see if we are abandoning changes
let rawValue = editorValue let rawValue = editorValue
if (mode === Modes.JavaScript) { if (mode === BindingMode.JavaScript && rawValue) {
rawValue = decodeJSBinding(rawValue) rawValue = decodeJSBinding(rawValue)
} }
@ -249,16 +281,16 @@
targetMode = null targetMode = null
} }
const changeSidePanel = newSidePanel => { const changeSidePanel = (newSidePanel: SidePanel) => {
sidePanel = newSidePanel === sidePanel ? null : newSidePanel sidePanel = newSidePanel === sidePanel ? null : newSidePanel
} }
const onChangeHBSValue = e => { const onChangeHBSValue = (e: { detail: string }) => {
hbsValue = e.detail hbsValue = e.detail
updateValue(hbsValue) updateValue(hbsValue)
} }
const onChangeJSValue = e => { const onChangeJSValue = (e: { detail: string }) => {
jsValue = encodeJSBinding(e.detail) jsValue = encodeJSBinding(e.detail)
if (!e.detail?.trim()) { if (!e.detail?.trim()) {
// Don't bother saving empty values as JS // Don't bother saving empty values as JS
@ -268,9 +300,14 @@
} }
} }
const addSnippet = (snippet: Snippet) =>
bindingHelpers.onSelectSnippet(snippet)
onMount(() => { onMount(() => {
// Set the initial mode appropriately // Set the initial mode appropriately
const initialValueMode = initialValueJS ? Modes.JavaScript : Modes.Text const initialValueMode = initialValueJS
? BindingMode.JavaScript
: BindingMode.Text
if (editorModeOptions.includes(initialValueMode)) { if (editorModeOptions.includes(initialValueMode)) {
mode = initialValueMode mode = initialValueMode
} else { } else {
@ -314,7 +351,7 @@
</div> </div>
{/if} {/if}
<div class="editor"> <div class="editor">
{#if mode === Modes.Text} {#if mode === BindingMode.Text}
{#key hbsCompletions} {#key hbsCompletions}
<CodeEditor <CodeEditor
value={hbsValue} value={hbsValue}
@ -328,10 +365,10 @@
jsBindingWrapping={false} jsBindingWrapping={false}
/> />
{/key} {/key}
{:else if mode === Modes.JavaScript} {:else if mode === BindingMode.JavaScript}
{#key jsCompletions} {#key jsCompletions}
<CodeEditor <CodeEditor
value={decodeJSBinding(jsValue)} value={jsValue ? decodeJSBinding(jsValue) : jsValue}
on:change={onChangeJSValue} on:change={onChangeJSValue}
completions={jsCompletions} completions={jsCompletions}
mode={EditorModes.JS} mode={EditorModes.JS}
@ -371,7 +408,7 @@
</div> </div>
</div> </div>
<div class="side" class:visible={!!sidePanel}> <div class="side" class:visible={!!sidePanel}>
{#if sidePanel === SidePanels.Bindings} {#if sidePanel === SidePanel.Bindings}
<BindingSidePanel <BindingSidePanel
bindings={enrichedBindings} bindings={enrichedBindings}
{allowHelpers} {allowHelpers}
@ -380,18 +417,15 @@
addBinding={onSelectBinding} addBinding={onSelectBinding}
mode={editorMode} mode={editorMode}
/> />
{:else if sidePanel === SidePanels.Evaluation} {:else if sidePanel === SidePanel.Evaluation}
<EvaluationSidePanel <EvaluationSidePanel
{expressionResult} {expressionResult}
{expressionError} {expressionError}
{evaluating} {evaluating}
expression={editorValue} expression={editorValue ? editorValue : ""}
/>
{:else if sidePanel === SidePanels.Snippets}
<SnippetSidePanel
addSnippet={snippet => bindingHelpers.onSelectSnippet(snippet)}
{snippets}
/> />
{:else if sidePanel === SidePanel.Snippets}
<SnippetSidePanel {addSnippet} {snippets} />
{/if} {/if}
</div> </div>
</div> </div>

View File

@ -1,28 +1,31 @@
<script> <script lang="ts">
import formatHighlight from "json-format-highlight" import { JsonFormatter } from "@budibase/frontend-core"
import { Icon, ProgressCircle, notifications } from "@budibase/bbui" import { Icon, ProgressCircle, notifications } from "@budibase/bbui"
import { copyToClipboard } from "@budibase/bbui/helpers" import { Helpers } from "@budibase/bbui"
import { fade } from "svelte/transition" import { fade } from "svelte/transition"
import { UserScriptError } from "@budibase/string-templates" import { UserScriptError } from "@budibase/string-templates"
import type { JSONValue } from "@budibase/types"
export let expressionResult // this can be essentially any primitive response from the JS function
export let expressionError export let expressionResult: JSONValue | undefined = undefined
export let expressionError: string | undefined = undefined
export let evaluating = false export let evaluating = false
export let expression = null export let expression: string | null = null
$: error = expressionError != null $: error = expressionError != null
$: empty = expression == null || expression?.trim() === "" $: empty = expression == null || expression?.trim() === ""
$: success = !error && !empty $: success = !error && !empty
$: highlightedResult = highlight(expressionResult) $: highlightedResult = highlight(expressionResult)
const formatError = err => { const formatError = (err: any) => {
if (err.code === UserScriptError.code) { if (err.code === UserScriptError.code) {
return err.userScriptError.toString() return err.userScriptError.toString()
} }
return err.toString() return err.toString()
} }
const highlight = json => { // json can be any primitive type
const highlight = (json?: any | null) => {
if (json == null) { if (json == null) {
return "" return ""
} }
@ -31,10 +34,10 @@
try { try {
json = JSON.stringify(JSON.parse(json), null, 2) json = JSON.stringify(JSON.parse(json), null, 2)
} catch (err) { } catch (err) {
// Ignore // couldn't parse/stringify, just treat it as the raw input
} }
return formatHighlight(json, { return JsonFormatter.format(json, {
keyColor: "#e06c75", keyColor: "#e06c75",
numberColor: "#e5c07b", numberColor: "#e5c07b",
stringColor: "#98c379", stringColor: "#98c379",
@ -45,11 +48,11 @@
} }
const copy = () => { const copy = () => {
let clipboardVal = expressionResult.result let clipboardVal = expressionResult
if (typeof clipboardVal === "object") { if (typeof clipboardVal === "object") {
clipboardVal = JSON.stringify(clipboardVal, null, 2) clipboardVal = JSON.stringify(clipboardVal, null, 2)
} }
copyToClipboard(clipboardVal) Helpers.copyToClipboard(clipboardVal)
notifications.success("Value copied to clipboard") notifications.success("Value copied to clipboard")
} }
</script> </script>

View File

@ -1,6 +1,6 @@
<script> <script>
import { viewsV2, rowActions } from "@/stores/builder" import { viewsV2, rowActions } from "@/stores/builder"
import { admin, themeStore, featureFlags } from "@/stores/portal" import { admin, themeStore, licensing } from "@/stores/portal"
import { Grid } from "@budibase/frontend-core" import { Grid } from "@budibase/frontend-core"
import { API } from "@/api" import { API } from "@/api"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
@ -53,7 +53,7 @@
{buttons} {buttons}
allowAddRows allowAddRows
allowDeleteRows allowDeleteRows
aiEnabled={$featureFlags.BUDIBASE_AI || $featureFlags.AI_CUSTOM_CONFIGS} aiEnabled={$licensing.customAIConfigsEnabled || $licensing.budibaseAiEnabled}
showAvatars={false} showAvatars={false}
on:updatedatasource={handleGridViewUpdate} on:updatedatasource={handleGridViewUpdate}
isCloud={$admin.cloud} isCloud={$admin.cloud}

View File

@ -8,7 +8,7 @@
rowActions, rowActions,
roles, roles,
} from "@/stores/builder" } from "@/stores/builder"
import { themeStore, admin, featureFlags } from "@/stores/portal" import { themeStore, admin, licensing } from "@/stores/portal"
import { TableNames } from "@/constants" import { TableNames } from "@/constants"
import { Grid } from "@budibase/frontend-core" import { Grid } from "@budibase/frontend-core"
import { API } from "@/api" import { API } from "@/api"
@ -130,7 +130,8 @@
schemaOverrides={isUsersTable ? userSchemaOverrides : null} schemaOverrides={isUsersTable ? userSchemaOverrides : null}
showAvatars={false} showAvatars={false}
isCloud={$admin.cloud} isCloud={$admin.cloud}
aiEnabled={$featureFlags.BUDIBASE_AI || $featureFlags.AI_CUSTOM_CONFIGS} aiEnabled={$licensing.customAIConfigsEnabled ||
$licensing.budibaseAIEnabled}
{buttons} {buttons}
buttonsCollapsed buttonsCollapsed
on:updatedatasource={handleGridTableUpdate} on:updatedatasource={handleGridTableUpdate}

View File

@ -12,7 +12,7 @@
Tags, Tags,
Tag, Tag,
} from "@budibase/bbui" } from "@budibase/bbui"
import { admin, licensing, featureFlags } from "@/stores/portal" import { admin, licensing } from "@/stores/portal"
import { API } from "@/api" import { API } from "@/api"
import AIConfigModal from "./ConfigModal.svelte" import AIConfigModal from "./ConfigModal.svelte"
import AIConfigTile from "./AIConfigTile.svelte" import AIConfigTile from "./AIConfigTile.svelte"
@ -27,8 +27,7 @@
let editingUuid let editingUuid
$: isCloud = $admin.cloud $: isCloud = $admin.cloud
$: customAIConfigsEnabled = $: customAIConfigsEnabled = $licensing.customAIConfigsEnabled
$featureFlags.AI_CUSTOM_CONFIGS && $licensing.customAIConfigsEnabled
async function fetchAIConfig() { async function fetchAIConfig() {
try { try {

View File

@ -1,10 +1,5 @@
<script> <script>
import { redirect } from "@roxi/routify" import { redirect } from "@roxi/routify"
import { featureFlags } from "@/stores/portal"
if ($featureFlags.AI_CUSTOM_CONFIGS) {
$redirect("./ai") $redirect("./ai")
} else {
$redirect("./auth")
}
</script> </script>

View File

@ -8,6 +8,7 @@ export * as search from "./searchFields"
export * as SchemaUtils from "./schema" export * as SchemaUtils from "./schema"
export { memo, derivedMemo } from "./memo" export { memo, derivedMemo } from "./memo"
export { createWebsocket } from "./websocket" export { createWebsocket } from "./websocket"
export * as JsonFormatter from "./jsonFormatter"
export * from "./download" export * from "./download"
export * from "./settings" export * from "./settings"
export * from "./relatedColumns" export * from "./relatedColumns"

View File

@ -0,0 +1,71 @@
import { JSONValue } from "@budibase/types"
export type ColorsOptions = {
keyColor?: string
numberColor?: string
stringColor?: string
trueColor?: string
falseColor?: string
nullColor?: string
}
const defaultColors: ColorsOptions = {
keyColor: "dimgray",
numberColor: "lightskyblue",
stringColor: "lightcoral",
trueColor: "lightseagreen",
falseColor: "#f66578",
nullColor: "cornflowerblue",
}
const entityMap = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
"`": "&#x60;",
"=": "&#x3D;",
}
function escapeHtml(html: string) {
return String(html).replace(/[&<>"'`=]/g, function (s) {
return entityMap[s as keyof typeof entityMap]
})
}
export function format(json: JSONValue, colorOptions: ColorsOptions = {}) {
const valueType = typeof json
let jsonString =
typeof json === "string" ? json : JSON.stringify(json, null, 2) || valueType
let colors = Object.assign({}, defaultColors, colorOptions)
jsonString = jsonString
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
return jsonString.replace(
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+]?\d+)?)/g,
(match: string) => {
let color = colors.numberColor
let style = ""
if (/^"/.test(match)) {
if (/:$/.test(match)) {
color = colors.keyColor
} else {
color = colors.stringColor
match = '"' + escapeHtml(match.substr(1, match.length - 2)) + '"'
style = "word-wrap:break-word;white-space:pre-wrap;"
}
} else {
color = /true/.test(match)
? colors.trueColor
: /false/.test(match)
? colors.falseColor
: /null/.test(match)
? colors.nullColor
: color
}
return `<span style="${style}color:${color}">${match}</span>`
}
)
}

View File

@ -43,7 +43,7 @@ export const sequential = fn => {
* invocations is enforced. * invocations is enforced.
* @param callback an async function to run * @param callback an async function to run
* @param minDelay the minimum delay between invocations * @param minDelay the minimum delay between invocations
* @returns {Promise} a debounced version of the callback * @returns a debounced version of the callback
*/ */
export const debounce = (callback, minDelay = 1000) => { export const debounce = (callback, minDelay = 1000) => {
let timeout let timeout

View File

@ -0,0 +1,7 @@
export type JSONValue =
| string
| number
| boolean
| null
| { [key: string]: JSONValue }
| JSONValue[]

View File

@ -1,2 +1,3 @@
export * from "./installation" export * from "./installation"
export * from "./events" export * from "./events"
export * from "./common"

View File

@ -0,0 +1,26 @@
export interface BindingCompletion {
section: {
name: string
}
label: string
}
export interface EnrichedBinding {
runtimeBinding: string
readableBinding: string
type?: null | string
}
export enum BindingMode {
Text = "Text",
JavaScript = "JavaScript",
}
export type CaretPositionFn = () => { start: number; end: number }
export type InsertAtPositionFn = (_: {
start: number
end?: number
value: string
cursor?: { anchor: number }
}) => void

View File

@ -0,0 +1,4 @@
export interface Helper {
example: string
description: string
}

View File

@ -0,0 +1,2 @@
export * from "./binding"
export * from "./helper"

View File

@ -0,0 +1,26 @@
interface JSEditorMode {
name: "javascript"
json: boolean
match: RegExp
}
interface HBSEditorMode {
name: "handlebars"
base: "text/html"
match: RegExp
}
interface HTMLEditorMode {
name: "text/html"
}
export type EditorMode = JSEditorMode | HBSEditorMode | HTMLEditorMode
type EditorModeMapBase =
| (JSEditorMode & { key: "JS" })
| (HBSEditorMode & { key: "Handlebars" })
| (HTMLEditorMode & { key: "Text" })
export type EditorModesMap = {
[M in EditorModeMapBase as M["key"]]: Omit<M, "key">
}

View File

@ -0,0 +1,2 @@
export * from "./sidepanel"
export * from "./codeEditor"

View File

@ -0,0 +1,5 @@
export enum SidePanel {
Bindings = "FlashOn",
Evaluation = "Play",
Snippets = "Code",
}

View File

@ -1,2 +1,4 @@
export * from "./stores" export * from "./stores"
export * from "./bindings"
export * from "./components"
export * from "./dataFetch" export * from "./dataFetch"

View File

@ -2131,9 +2131,9 @@
through2 "^2.0.0" through2 "^2.0.0"
"@budibase/pro@npm:@budibase/pro@latest": "@budibase/pro@npm:@budibase/pro@latest":
version "3.2.32" version "3.2.44"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-3.2.32.tgz#f6abcd5a5524e7f33d958acb6e610e29995427bb" resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-3.2.44.tgz#90367bb2167aafd8c809e000a57d349e5dc4bb78"
integrity sha512-bF0pd17IjYugjll2yKYmb0RM+tfKZcCmRBc4XG2NZ4f/I47QaOovm9RqSw6tfqCFuzRewxR3SWmtmSseUc/e0w== integrity sha512-Zv2PBVUZUS6/psOpIRIDlW3jrOHWWPhpQXzCk00kIQJaqjkdcvuTXSedQ70u537sQmLu8JsSWbui9MdfF8ksVw==
dependencies: dependencies:
"@anthropic-ai/sdk" "^0.27.3" "@anthropic-ai/sdk" "^0.27.3"
"@budibase/backend-core" "*" "@budibase/backend-core" "*"
@ -5926,6 +5926,13 @@
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.5.tgz#f090ff4bd8d2e5b940ff270ab39fd5ca1834a07e" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.5.tgz#f090ff4bd8d2e5b940ff270ab39fd5ca1834a07e"
integrity sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw== integrity sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==
"@types/sanitize-html@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-2.13.0.tgz#ac3620e867b7c68deab79c72bd117e2049cdd98e"
integrity sha512-X31WxbvW9TjIhZZNyNBZ/p5ax4ti7qsNDBDEnH4zAgmEh35YnFD1UiS6z9Cd34kKm0LslFW0KPmTQzu/oGtsqQ==
dependencies:
htmlparser2 "^8.0.0"
"@types/semver@7.3.7": "@types/semver@7.3.7":
version "7.3.7" version "7.3.7"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.7.tgz#b9eb89d7dfa70d5d1ce525bc1411a35347f533a3" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.7.tgz#b9eb89d7dfa70d5d1ce525bc1411a35347f533a3"
@ -13272,11 +13279,6 @@ json-buffer@3.0.1:
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==
json-format-highlight@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/json-format-highlight/-/json-format-highlight-1.0.4.tgz#2e44277edabcec79a3d2c84e984c62e2258037b9"
integrity sha512-RqenIjKr1I99XfXPAml9G7YlEZg/GnsH7emWyWJh2yuGXqHW8spN7qx6/ME+MoIBb35/fxrMC9Jauj6nvGe4Mg==
json-parse-better-errors@^1.0.1: json-parse-better-errors@^1.0.1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
@ -18644,7 +18646,16 @@ string-length@^4.0.1:
char-regex "^1.0.2" char-regex "^1.0.2"
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: "string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
version "4.2.3" version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -18736,7 +18747,7 @@ stringify-object@^3.2.1:
is-obj "^1.0.1" is-obj "^1.0.1"
is-regexp "^1.0.0" is-regexp "^1.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: "strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1" version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -18750,6 +18761,13 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
dependencies: dependencies:
ansi-regex "^4.1.0" ansi-regex "^4.1.0"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.1: strip-ansi@^7.0.1:
version "7.0.1" version "7.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2"
@ -20497,7 +20515,7 @@ worker-farm@1.7.0:
dependencies: dependencies:
errno "~0.1.7" errno "~0.1.7"
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0" version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@ -20515,6 +20533,15 @@ wrap-ansi@^5.1.0:
string-width "^3.0.0" string-width "^3.0.0"
strip-ansi "^5.0.0" strip-ansi "^5.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0: wrap-ansi@^8.1.0:
version "8.1.0" version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"