Merge branch 'master' into remove-dead-code

This commit is contained in:
Sam Rose 2025-01-20 13:18:28 +00:00 committed by GitHub
commit 6068390d16
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
75 changed files with 1348 additions and 973 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

@ -1,6 +1,6 @@
import env from "../../environment" import env from "../../environment"
export const getCouchInfo = (connection?: string) => { export const getCouchInfo = (connection?: string | null) => {
// clean out any auth credentials // clean out any auth credentials
const urlInfo = getUrlInfo(connection) const urlInfo = getUrlInfo(connection)
let username let username
@ -45,7 +45,7 @@ export const getCouchInfo = (connection?: string) => {
} }
} }
export const getUrlInfo = (url = env.COUCH_DB_URL) => { export const getUrlInfo = (url: string | null = env.COUCH_DB_URL) => {
let cleanUrl, username, password, host let cleanUrl, username, password, host
if (url) { if (url) {
// Ensure the URL starts with a protocol // Ensure the URL starts with a protocol

View File

@ -1,5 +1,6 @@
require("../../../tests") require("../../../tests")
const getUrlInfo = require("../couch").getUrlInfo
import { getUrlInfo } from "../couch"
describe("pouch", () => { describe("pouch", () => {
describe("Couch DB URL parsing", () => { describe("Couch DB URL parsing", () => {

View File

@ -1172,20 +1172,22 @@ class InternalBuilder {
nulls = value.direction === SortOrder.ASCENDING ? "first" : "last" nulls = value.direction === SortOrder.ASCENDING ? "first" : "last"
} }
const composite = `${aliased}.${key}`
let identifier
if (this.isAggregateField(key)) { if (this.isAggregateField(key)) {
query = query.orderBy(key, direction, nulls) identifier = this.rawQuotedIdentifier(key)
} else if (this.client === SqlClient.ORACLE) {
identifier = this.convertClobs(composite)
} else { } else {
let composite = `${aliased}.${key}` identifier = this.rawQuotedIdentifier(composite)
if (this.client === SqlClient.ORACLE) {
query = query.orderByRaw(`?? ?? nulls ??`, [
this.convertClobs(composite),
this.knex.raw(direction),
this.knex.raw(nulls as string),
])
} else {
query = query.orderBy(composite, direction, nulls)
}
} }
query = query.orderByRaw(`?? ?? ${nulls ? "nulls ??" : ""}`, [
identifier,
this.knex.raw(direction),
...(nulls ? [this.knex.raw(nulls as string)] : []),
])
} }
} }
@ -1344,14 +1346,16 @@ class InternalBuilder {
// add the correlation to the overall query // add the correlation to the overall query
subQuery = subQuery.where( subQuery = subQuery.where(
correlatedTo, this.rawQuotedIdentifier(correlatedTo),
"=", "=",
this.rawQuotedIdentifier(correlatedFrom) this.rawQuotedIdentifier(correlatedFrom)
) )
const standardWrap = (select: Knex.Raw): Knex.QueryBuilder => { const standardWrap = (select: Knex.Raw): Knex.QueryBuilder => {
subQuery = subQuery subQuery = subQuery
.select(relationshipFields) .select(
relationshipFields.map(field => this.rawQuotedIdentifier(field))
)
.limit(getRelationshipLimit()) .limit(getRelationshipLimit())
// @ts-ignore - the from alias syntax isn't in Knex typing // @ts-ignore - the from alias syntax isn't in Knex typing
return knex.select(select).from({ return knex.select(select).from({

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,36 +356,46 @@ export const bindingsToCompletions = (bindings, mode) => {
return acc return acc
}, {}) }, {})
const completions = Object.keys(bindingByCategory).reduce((comps, catKey) => { const completions = Object.keys(bindingByCategory).reduce(
const { icon, rank } = categoryMeta[catKey] || {} (comps: any, catKey: string) => {
const { icon, rank } = categoryMeta[catKey] || {}
const bindindSectionHeader = buildSectionHeader( const bindingSectionHeader = buildSectionHeader(
bindingByCategory.type, // @ts-ignore something wrong with this - logically this should be dictionary
catKey, bindingByCategory.type,
icon || "", catKey,
typeof rank == "number" ? rank : 1 icon || "",
) typeof rank == "number" ? rank : 1
)
return [ return [
...comps, ...comps,
...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",
return buildBindingInfoNode(completion, binding) info: (completion: BindingCompletion) => {
}, return buildBindingInfoNode(completion, binding)
type: "binding", },
detail: displayType, type: "binding",
section: bindindSectionHeader, detail: displayType,
apply: (view, completion, from, to) => { section: bindingSectionHeader,
insertBinding(view, from, to, binding.readableBinding, mode) apply: (
}, view: any,
}) completion: BindingCompletion,
return acc from: number,
}, []), to: number
] ) => {
}, []) insertBinding(view, from, to, binding.readableBinding, mode)
},
})
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,71 +106,87 @@
] ]
} }
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(
try { (expression: string | null, context: any, snippets: Snippet[]) => {
expressionError = null try {
expressionResult = processStringSync( expressionError = undefined
expression || "", expressionResult = processStringSync(
{ expression || "",
...context, {
snippets, ...context,
}, snippets,
{ },
noThrow: false, {
} noThrow: false,
) }
} catch (err) { )
expressionResult = null } catch (err: any) {
expressionError = err expressionResult = undefined
} expressionError = err
evaluating = false }
}, 260) evaluating = false
},
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

@ -1,22 +1,39 @@
<script> <script lang="ts">
import { getContext } from "svelte" import { getContext } from "svelte"
import { Pagination, ProgressCircle } from "@budibase/bbui" import { Pagination, ProgressCircle } from "@budibase/bbui"
import { fetchData, QueryUtils } from "@budibase/frontend-core" import { fetchData, QueryUtils } from "@budibase/frontend-core"
import { LogicalOperator, EmptyFilterOption } from "@budibase/types" import {
LogicalOperator,
EmptyFilterOption,
TableSchema,
SortOrder,
SearchFilters,
UISearchFilter,
DataFetchDatasource,
UserDatasource,
GroupUserDatasource,
DataFetchOptions,
} from "@budibase/types"
import { SDK, Component } from "../../index"
export let dataSource type ProviderDatasource = Exclude<
export let filter DataFetchDatasource,
export let sortColumn UserDatasource | GroupUserDatasource
export let sortOrder >
export let limit
export let paginate
export let autoRefresh
const { styleable, Provider, ActionTypes, API } = getContext("sdk") export let dataSource: ProviderDatasource
const component = getContext("component") export let filter: UISearchFilter
export let sortColumn: string
export let sortOrder: SortOrder
export let limit: number
export let paginate: boolean
export let autoRefresh: number
let interval const { styleable, Provider, ActionTypes, API } = getContext<SDK>("sdk")
let queryExtensions = {} const component = getContext<Component>("component")
let interval: ReturnType<typeof setInterval>
let queryExtensions: Record<string, any> = {}
$: defaultQuery = QueryUtils.buildQuery(filter) $: defaultQuery = QueryUtils.buildQuery(filter)
@ -49,8 +66,14 @@
}, },
{ {
type: ActionTypes.SetDataProviderSorting, type: ActionTypes.SetDataProviderSorting,
callback: ({ column, order }) => { callback: ({
let newOptions = {} column,
order,
}: {
column: string
order: SortOrder | undefined
}) => {
let newOptions: Partial<DataFetchOptions> = {}
if (column) { if (column) {
newOptions.sortColumn = column newOptions.sortColumn = column
} }
@ -63,6 +86,7 @@
}, },
}, },
] ]
$: dataContext = { $: dataContext = {
rows: $fetch.rows, rows: $fetch.rows,
info: $fetch.info, info: $fetch.info,
@ -75,14 +99,12 @@
id: $component?.id, id: $component?.id,
state: { state: {
query: $fetch.query, query: $fetch.query,
sortColumn: $fetch.sortColumn,
sortOrder: $fetch.sortOrder,
}, },
limit, limit,
primaryDisplay: $fetch.definition?.primaryDisplay, primaryDisplay: ($fetch.definition as any)?.primaryDisplay,
} }
const createFetch = datasource => { const createFetch = (datasource: ProviderDatasource) => {
return fetchData({ return fetchData({
API, API,
datasource, datasource,
@ -96,7 +118,7 @@
}) })
} }
const sanitizeSchema = schema => { const sanitizeSchema = (schema: TableSchema | null) => {
if (!schema) { if (!schema) {
return schema return schema
} }
@ -109,14 +131,14 @@
return cloned return cloned
} }
const addQueryExtension = (key, extension) => { const addQueryExtension = (key: string, extension: any) => {
if (!key || !extension) { if (!key || !extension) {
return return
} }
queryExtensions = { ...queryExtensions, [key]: extension } queryExtensions = { ...queryExtensions, [key]: extension }
} }
const removeQueryExtension = key => { const removeQueryExtension = (key: string) => {
if (!key) { if (!key) {
return return
} }
@ -125,11 +147,14 @@
queryExtensions = newQueryExtensions queryExtensions = newQueryExtensions
} }
const extendQuery = (defaultQuery, extensions) => { const extendQuery = (
defaultQuery: SearchFilters,
extensions: Record<string, any>
): SearchFilters => {
if (!Object.keys(extensions).length) { if (!Object.keys(extensions).length) {
return defaultQuery return defaultQuery
} }
const extended = { const extended: SearchFilters = {
[LogicalOperator.AND]: { [LogicalOperator.AND]: {
conditions: [ conditions: [
...(defaultQuery ? [defaultQuery] : []), ...(defaultQuery ? [defaultQuery] : []),
@ -140,12 +165,12 @@
} }
// If there are no conditions applied at all, clear the request. // If there are no conditions applied at all, clear the request.
return extended[LogicalOperator.AND]?.conditions?.length > 0 return (extended[LogicalOperator.AND]?.conditions?.length ?? 0) > 0
? extended ? extended
: null : {}
} }
const setUpAutoRefresh = autoRefresh => { const setUpAutoRefresh = (autoRefresh: number) => {
clearInterval(interval) clearInterval(interval)
if (autoRefresh) { if (autoRefresh) {
interval = setInterval(fetch.refresh, Math.max(10000, autoRefresh * 1000)) interval = setInterval(fetch.refresh, Math.max(10000, autoRefresh * 1000))

View File

@ -0,0 +1,15 @@
import { APIClient } from "@budibase/frontend-core"
import type { ActionTypes } from "./constants"
import { Readable } from "svelte/store"
export interface SDK {
API: APIClient
styleable: any
Provider: any
ActionTypes: typeof ActionTypes
}
export type Component = Readable<{
id: string
styles: any
}>

View File

@ -6,7 +6,7 @@ import { screenStore } from "./screens"
import { builderStore } from "./builder" import { builderStore } from "./builder"
import Router from "../components/Router.svelte" import Router from "../components/Router.svelte"
import * as AppComponents from "../components/app/index.js" import * as AppComponents from "../components/app/index.js"
import { ScreenslotType } from "../constants.js" import { ScreenslotType } from "../constants"
export const BudibasePrefix = "@budibase/standard-components/" export const BudibasePrefix = "@budibase/standard-components/"

View File

@ -1,7 +1,11 @@
// TODO: datasource and defitions are unions of the different implementations. At this point, the datasource does not know what type is being used, and the assignations will cause TS exceptions. Casting it "as any" for now. This should be fixed improving the type usages. // TODO: datasource and defitions are unions of the different implementations. At this point, the datasource does not know what type is being used, and the assignations will cause TS exceptions. Casting it "as any" for now. This should be fixed improving the type usages.
import { derived, get, Readable, Writable } from "svelte/store" import { derived, get, Readable, Writable } from "svelte/store"
import { getDatasourceDefinition, getDatasourceSchema } from "../../../fetch" import {
DataFetchDefinition,
getDatasourceDefinition,
getDatasourceSchema,
} from "../../../fetch"
import { enrichSchemaWithRelColumns, memo } from "../../../utils" import { enrichSchemaWithRelColumns, memo } from "../../../utils"
import { cloneDeep } from "lodash" import { cloneDeep } from "lodash"
import { import {
@ -18,7 +22,7 @@ import { Store as StoreContext, BaseStoreProps } from "."
import { DatasourceActions } from "./datasources" import { DatasourceActions } from "./datasources"
interface DatasourceStore { interface DatasourceStore {
definition: Writable<UIDatasource | null> definition: Writable<DataFetchDefinition | null>
schemaMutations: Writable<Record<string, UIFieldMutation>> schemaMutations: Writable<Record<string, UIFieldMutation>>
subSchemaMutations: Writable<Record<string, Record<string, UIFieldMutation>>> subSchemaMutations: Writable<Record<string, Record<string, UIFieldMutation>>>
} }
@ -131,11 +135,17 @@ export const deriveStores = (context: StoreContext): DerivedDatasourceStore => {
[datasource, definition], [datasource, definition],
([$datasource, $definition]) => { ([$datasource, $definition]) => {
let type = $datasource?.type let type = $datasource?.type
// @ts-expect-error
if (type === "provider") { if (type === "provider") {
type = ($datasource as any).value?.datasource?.type // TODO: see line 1 type = ($datasource as any).value?.datasource?.type // TODO: see line 1
} }
// Handle calculation views // Handle calculation views
if (type === "viewV2" && $definition?.type === ViewV2Type.CALCULATION) { if (
type === "viewV2" &&
$definition &&
"type" in $definition &&
$definition.type === ViewV2Type.CALCULATION
) {
return false return false
} }
return !!type && ["table", "viewV2", "link"].includes(type) return !!type && ["table", "viewV2", "link"].includes(type)
@ -197,7 +207,7 @@ export const createActions = (context: StoreContext): ActionDatasourceStore => {
) => { ) => {
// Update local state // Update local state
const originalDefinition = get(definition) const originalDefinition = get(definition)
definition.set(newDefinition as UIDatasource) definition.set(newDefinition)
// Update server // Update server
if (get(config).canSaveSchema) { if (get(config).canSaveSchema) {
@ -225,13 +235,15 @@ export const createActions = (context: StoreContext): ActionDatasourceStore => {
// Update primary display // Update primary display
newDefinition.primaryDisplay = column newDefinition.primaryDisplay = column
// Sanitise schema to ensure field is required and has no default value if (newDefinition.schema) {
if (!newDefinition.schema[column].constraints) { // Sanitise schema to ensure field is required and has no default value
newDefinition.schema[column].constraints = {} if (!newDefinition.schema[column].constraints) {
} newDefinition.schema[column].constraints = {}
newDefinition.schema[column].constraints.presence = { allowEmpty: false } }
if ("default" in newDefinition.schema[column]) { newDefinition.schema[column].constraints.presence = { allowEmpty: false }
delete newDefinition.schema[column].default if ("default" in newDefinition.schema[column]) {
delete newDefinition.schema[column].default
}
} }
return await saveDefinition(newDefinition as any) // TODO: see line 1 return await saveDefinition(newDefinition as any) // TODO: see line 1
} }

View File

@ -8,6 +8,7 @@ import {
import { get } from "svelte/store" import { get } from "svelte/store"
import { Store as StoreContext } from ".." import { Store as StoreContext } from ".."
import { DatasourceTableActions } from "." import { DatasourceTableActions } from "."
import TableFetch from "../../../../fetch/TableFetch"
const SuppressErrors = true const SuppressErrors = true
@ -119,7 +120,7 @@ export const initialise = (context: StoreContext) => {
unsubscribers.push( unsubscribers.push(
allFilters.subscribe($allFilters => { allFilters.subscribe($allFilters => {
// Ensure we're updating the correct fetch // Ensure we're updating the correct fetch
const $fetch = get(fetch) const $fetch = get(fetch) as TableFetch | null
if ($fetch?.options?.datasource?.tableId !== $datasource.tableId) { if ($fetch?.options?.datasource?.tableId !== $datasource.tableId) {
return return
} }
@ -133,7 +134,7 @@ export const initialise = (context: StoreContext) => {
unsubscribers.push( unsubscribers.push(
sort.subscribe($sort => { sort.subscribe($sort => {
// Ensure we're updating the correct fetch // Ensure we're updating the correct fetch
const $fetch = get(fetch) const $fetch = get(fetch) as TableFetch | null
if ($fetch?.options?.datasource?.tableId !== $datasource.tableId) { if ($fetch?.options?.datasource?.tableId !== $datasource.tableId) {
return return
} }

View File

@ -4,11 +4,11 @@ import {
SaveRowRequest, SaveRowRequest,
SortOrder, SortOrder,
UIDatasource, UIDatasource,
UIView,
UpdateViewRequest, UpdateViewRequest,
} from "@budibase/types" } from "@budibase/types"
import { Store as StoreContext } from ".." import { Store as StoreContext } from ".."
import { DatasourceViewActions } from "." import { DatasourceViewActions } from "."
import ViewV2Fetch from "../../../../fetch/ViewV2Fetch"
const SuppressErrors = true const SuppressErrors = true
@ -134,6 +134,9 @@ export const initialise = (context: StoreContext) => {
if (!get(config).canSaveSchema) { if (!get(config).canSaveSchema) {
return return
} }
if (!$definition || !("id" in $definition)) {
return
}
if ($definition?.id !== $datasource.id) { if ($definition?.id !== $datasource.id) {
return return
} }
@ -184,7 +187,10 @@ export const initialise = (context: StoreContext) => {
unsubscribers.push( unsubscribers.push(
sort.subscribe(async $sort => { sort.subscribe(async $sort => {
// Ensure we're updating the correct view // Ensure we're updating the correct view
const $view = get(definition) as UIView const $view = get(definition)
if (!$view || !("id" in $view)) {
return
}
if ($view?.id !== $datasource.id) { if ($view?.id !== $datasource.id) {
return return
} }
@ -207,7 +213,7 @@ export const initialise = (context: StoreContext) => {
// Also update the fetch to ensure the new sort is respected. // Also update the fetch to ensure the new sort is respected.
// Ensure we're updating the correct fetch. // Ensure we're updating the correct fetch.
const $fetch = get(fetch) const $fetch = get(fetch) as ViewV2Fetch | null
if ($fetch?.options?.datasource?.id !== $datasource.id) { if ($fetch?.options?.datasource?.id !== $datasource.id) {
return return
} }
@ -225,6 +231,9 @@ export const initialise = (context: StoreContext) => {
return return
} }
const $view = get(definition) const $view = get(definition)
if (!$view || !("id" in $view)) {
return
}
if ($view?.id !== $datasource.id) { if ($view?.id !== $datasource.id) {
return return
} }
@ -246,7 +255,7 @@ export const initialise = (context: StoreContext) => {
if (!get(config).canSaveSchema) { if (!get(config).canSaveSchema) {
return return
} }
const $fetch = get(fetch) const $fetch = get(fetch) as ViewV2Fetch | null
if ($fetch?.options?.datasource?.id !== $datasource.id) { if ($fetch?.options?.datasource?.id !== $datasource.id) {
return return
} }
@ -262,7 +271,7 @@ export const initialise = (context: StoreContext) => {
if (get(config).canSaveSchema) { if (get(config).canSaveSchema) {
return return
} }
const $fetch = get(fetch) const $fetch = get(fetch) as ViewV2Fetch | null
if ($fetch?.options?.datasource?.id !== $datasource.id) { if ($fetch?.options?.datasource?.id !== $datasource.id) {
return return
} }

View File

@ -1,5 +1,5 @@
import { writable, derived, get, Writable, Readable } from "svelte/store" import { writable, derived, get, Writable, Readable } from "svelte/store"
import { fetchData } from "../../../fetch" import { DataFetch, fetchData } from "../../../fetch"
import { NewRowID, RowPageSize } from "../lib/constants" import { NewRowID, RowPageSize } from "../lib/constants"
import { import {
generateRowID, generateRowID,
@ -13,7 +13,6 @@ import { sleep } from "../../../utils/utils"
import { FieldType, Row, UIRow } from "@budibase/types" import { FieldType, Row, UIRow } from "@budibase/types"
import { getRelatedTableValues } from "../../../utils" import { getRelatedTableValues } from "../../../utils"
import { Store as StoreContext } from "." import { Store as StoreContext } from "."
import DataFetch from "../../../fetch/DataFetch"
interface IndexedUIRow extends UIRow { interface IndexedUIRow extends UIRow {
__idx: number __idx: number
@ -21,7 +20,7 @@ interface IndexedUIRow extends UIRow {
interface RowStore { interface RowStore {
rows: Writable<UIRow[]> rows: Writable<UIRow[]>
fetch: Writable<DataFetch<any, any, any> | null> // TODO: type this properly, having a union of all the possible options fetch: Writable<DataFetch | null>
loaded: Writable<boolean> loaded: Writable<boolean>
refreshing: Writable<boolean> refreshing: Writable<boolean>
loading: Writable<boolean> loading: Writable<boolean>
@ -254,7 +253,7 @@ export const createActions = (context: StoreContext): RowActionStore => {
// Reset state properties when dataset changes // Reset state properties when dataset changes
if (!$instanceLoaded || resetRows) { if (!$instanceLoaded || resetRows) {
definition.set($fetch.definition as any) // TODO: datasource and defitions are unions of the different implementations. At this point, the datasource does not know what type is being used, and the assignations will cause TS exceptions. Casting it "as any" for now. This should be fixed improving the type usages. definition.set($fetch.definition ?? null)
} }
// Reset scroll state when data changes // Reset scroll state when data changes

View File

@ -1,13 +1,9 @@
import DataFetch from "./DataFetch" import { CustomDatasource } from "@budibase/types"
import BaseDataFetch from "./DataFetch"
interface CustomDatasource {
type: "custom"
data: any
}
type CustomDefinition = Record<string, any> type CustomDefinition = Record<string, any>
export default class CustomFetch extends DataFetch< export default class CustomFetch extends BaseDataFetch<
CustomDatasource, CustomDatasource,
CustomDefinition CustomDefinition
> { > {

View File

@ -3,14 +3,13 @@ import { cloneDeep } from "lodash/fp"
import { QueryUtils } from "../utils" import { QueryUtils } from "../utils"
import { convertJSONSchemaToTableSchema } from "../utils/json" import { convertJSONSchemaToTableSchema } from "../utils/json"
import { import {
DataFetchOptions,
FieldType, FieldType,
LegacyFilter,
Row, Row,
SearchFilters, SearchFilters,
SortOrder, SortOrder,
SortType, SortType,
TableSchema, TableSchema,
UISearchFilter,
} from "@budibase/types" } from "@budibase/types"
import { APIClient } from "../api/types" import { APIClient } from "../api/types"
import { DataFetchType } from "." import { DataFetchType } from "."
@ -44,14 +43,11 @@ interface DataFetchDerivedStore<TDefinition, TQuery>
supportsPagination: boolean supportsPagination: boolean
} }
export interface DataFetchParams< export interface DataFetchParams<TDatasource, TQuery = SearchFilters> {
TDatasource,
TQuery = SearchFilters | undefined
> {
API: APIClient API: APIClient
datasource: TDatasource datasource: TDatasource
query: TQuery query: TQuery
options?: {} options?: Partial<DataFetchOptions<TQuery>>
} }
/** /**
@ -59,7 +55,7 @@ export interface DataFetchParams<
* internal table or datasource plus. * internal table or datasource plus.
* For other types of datasource, this class is overridden and extended. * For other types of datasource, this class is overridden and extended.
*/ */
export default abstract class DataFetch< export default abstract class BaseDataFetch<
TDatasource extends { type: DataFetchType }, TDatasource extends { type: DataFetchType },
TDefinition extends { TDefinition extends {
schema?: Record<string, any> | null schema?: Record<string, any> | null
@ -73,18 +69,11 @@ export default abstract class DataFetch<
supportsSort: boolean supportsSort: boolean
supportsPagination: boolean supportsPagination: boolean
} }
options: { options: DataFetchOptions<TQuery> & {
datasource: TDatasource datasource: TDatasource
limit: number
// Search config
filter: UISearchFilter | LegacyFilter[] | null
query: TQuery
// Sorting config
sortColumn: string | null
sortOrder: SortOrder
sortType: SortType | null sortType: SortType | null
// Pagination config
paginate: boolean
// Client side feature customisation // Client side feature customisation
clientSideSearching: boolean clientSideSearching: boolean
clientSideSorting: boolean clientSideSorting: boolean
@ -267,6 +256,7 @@ export default abstract class DataFetch<
// Build the query // Build the query
let query = this.options.query let query = this.options.query
if (!query) { if (!query) {
query = buildQuery(filter ?? undefined) as TQuery query = buildQuery(filter ?? undefined) as TQuery
} }
@ -430,7 +420,7 @@ export default abstract class DataFetch<
* Resets the data set and updates options * Resets the data set and updates options
* @param newOptions any new options * @param newOptions any new options
*/ */
async update(newOptions: any) { async update(newOptions: Partial<DataFetchOptions<TQuery>>) {
// Check if any settings have actually changed // Check if any settings have actually changed
let refresh = false let refresh = false
for (const [key, value] of Object.entries(newOptions || {})) { for (const [key, value] of Object.entries(newOptions || {})) {

View File

@ -1,14 +1,10 @@
import { Row } from "@budibase/types" import {
import DataFetch from "./DataFetch" FieldDatasource,
JSONArrayFieldDatasource,
type Types = "field" | "queryarray" | "jsonarray" QueryArrayFieldDatasource,
Row,
export interface FieldDatasource<TType extends Types> { } from "@budibase/types"
type: TType import BaseDataFetch from "./DataFetch"
tableId: string
fieldType: "attachment" | "array"
value: string[] | Row[]
}
export interface FieldDefinition { export interface FieldDefinition {
schema?: Record<string, { type: string }> | null schema?: Record<string, { type: string }> | null
@ -18,10 +14,12 @@ function isArrayOfStrings(value: string[] | Row[]): value is string[] {
return Array.isArray(value) && !!value[0] && typeof value[0] !== "object" return Array.isArray(value) && !!value[0] && typeof value[0] !== "object"
} }
export default class FieldFetch<TType extends Types> extends DataFetch< export default class FieldFetch<
FieldDatasource<TType>, TDatasource extends
FieldDefinition | FieldDatasource
> { | QueryArrayFieldDatasource
| JSONArrayFieldDatasource = FieldDatasource
> extends BaseDataFetch<TDatasource, FieldDefinition> {
async getDefinition(): Promise<FieldDefinition | null> { async getDefinition(): Promise<FieldDefinition | null> {
const { datasource } = this.options const { datasource } = this.options

View File

@ -1,20 +1,20 @@
import { get } from "svelte/store" import { get } from "svelte/store"
import DataFetch, { DataFetchParams } from "./DataFetch" import BaseDataFetch, { DataFetchParams } from "./DataFetch"
import { TableNames } from "../constants" import { GroupUserDatasource, InternalTable } from "@budibase/types"
interface GroupUserQuery { interface GroupUserQuery {
groupId: string groupId: string
emailSearch: string emailSearch: string
} }
interface GroupUserDatasource { interface GroupUserDefinition {
type: "groupUser" schema?: Record<string, any> | null
tableId: TableNames.USERS primaryDisplay?: string
} }
export default class GroupUserFetch extends DataFetch< export default class GroupUserFetch extends BaseDataFetch<
GroupUserDatasource, GroupUserDatasource,
{}, GroupUserDefinition,
GroupUserQuery GroupUserQuery
> { > {
constructor(opts: DataFetchParams<GroupUserDatasource, GroupUserQuery>) { constructor(opts: DataFetchParams<GroupUserDatasource, GroupUserQuery>) {
@ -22,7 +22,7 @@ export default class GroupUserFetch extends DataFetch<
...opts, ...opts,
datasource: { datasource: {
type: "groupUser", type: "groupUser",
tableId: TableNames.USERS, tableId: InternalTable.USER_METADATA,
}, },
}) })
} }

View File

@ -1,7 +1,8 @@
import FieldFetch from "./FieldFetch" import FieldFetch from "./FieldFetch"
import { getJSONArrayDatasourceSchema } from "../utils/json" import { getJSONArrayDatasourceSchema } from "../utils/json"
import { JSONArrayFieldDatasource } from "@budibase/types"
export default class JSONArrayFetch extends FieldFetch<"jsonarray"> { export default class JSONArrayFetch extends FieldFetch<JSONArrayFieldDatasource> {
async getDefinition() { async getDefinition() {
const { datasource } = this.options const { datasource } = this.options

View File

@ -1,20 +1,11 @@
import { Row, TableSchema } from "@budibase/types" import { NestedProviderDatasource, TableSchema } from "@budibase/types"
import DataFetch from "./DataFetch" import BaseDataFetch from "./DataFetch"
interface NestedProviderDatasource {
type: "provider"
value?: {
schema: TableSchema
primaryDisplay: string
rows: Row[]
}
}
interface NestedProviderDefinition { interface NestedProviderDefinition {
schema?: TableSchema schema?: TableSchema
primaryDisplay?: string primaryDisplay?: string
} }
export default class NestedProviderFetch extends DataFetch< export default class NestedProviderFetch extends BaseDataFetch<
NestedProviderDatasource, NestedProviderDatasource,
NestedProviderDefinition NestedProviderDefinition
> { > {

View File

@ -3,8 +3,9 @@ import {
getJSONArrayDatasourceSchema, getJSONArrayDatasourceSchema,
generateQueryArraySchemas, generateQueryArraySchemas,
} from "../utils/json" } from "../utils/json"
import { QueryArrayFieldDatasource } from "@budibase/types"
export default class QueryArrayFetch extends FieldFetch<"queryarray"> { export default class QueryArrayFetch extends FieldFetch<QueryArrayFieldDatasource> {
async getDefinition() { async getDefinition() {
const { datasource } = this.options const { datasource } = this.options

View File

@ -1,23 +1,9 @@
import DataFetch from "./DataFetch" import BaseDataFetch from "./DataFetch"
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"
import { ExecuteQueryRequest, Query } from "@budibase/types" import { ExecuteQueryRequest, Query, QueryDatasource } from "@budibase/types"
import { get } from "svelte/store" import { get } from "svelte/store"
interface QueryDatasource { export default class QueryFetch extends BaseDataFetch<QueryDatasource, Query> {
type: "query"
_id: string
fields: Record<string, any> & {
pagination?: {
type: string
location: string
pageParam: string
}
}
queryParams?: Record<string, string>
parameters: { name: string; default: string }[]
}
export default class QueryFetch extends DataFetch<QueryDatasource, Query> {
async determineFeatureFlags() { async determineFeatureFlags() {
const definition = await this.getDefinition() const definition = await this.getDefinition()
const supportsPagination = const supportsPagination =

View File

@ -1,15 +1,7 @@
import { Table } from "@budibase/types" import { RelationshipDatasource, Table } from "@budibase/types"
import DataFetch from "./DataFetch" import BaseDataFetch from "./DataFetch"
interface RelationshipDatasource { export default class RelationshipFetch extends BaseDataFetch<
type: "link"
tableId: string
rowId: string
rowTableId: string
fieldName: string
}
export default class RelationshipFetch extends DataFetch<
RelationshipDatasource, RelationshipDatasource,
Table Table
> { > {

View File

@ -1,13 +1,8 @@
import { get } from "svelte/store" import { get } from "svelte/store"
import DataFetch from "./DataFetch" import BaseDataFetch from "./DataFetch"
import { SortOrder, Table } from "@budibase/types" import { SortOrder, Table, TableDatasource } from "@budibase/types"
interface TableDatasource { export default class TableFetch extends BaseDataFetch<TableDatasource, Table> {
type: "table"
tableId: string
}
export default class TableFetch extends DataFetch<TableDatasource, Table> {
async determineFeatureFlags() { async determineFeatureFlags() {
return { return {
supportsSearch: true, supportsSearch: true,

View File

@ -1,22 +1,24 @@
import { get } from "svelte/store" import { get } from "svelte/store"
import DataFetch, { DataFetchParams } from "./DataFetch" import BaseDataFetch, { DataFetchParams } from "./DataFetch"
import { TableNames } from "../constants"
import { utils } from "@budibase/shared-core" import { utils } from "@budibase/shared-core"
import { SearchFilters, SearchUsersRequest } from "@budibase/types" import {
InternalTable,
SearchFilters,
SearchUsersRequest,
UserDatasource,
} from "@budibase/types"
interface UserFetchQuery { interface UserFetchQuery {
appId: string appId: string
paginated: boolean paginated: boolean
} }
interface UserDatasource { interface UserDefinition {
type: "user" schema?: Record<string, any> | null
tableId: TableNames.USERS primaryDisplay?: string
} }
interface UserDefinition {} export default class UserFetch extends BaseDataFetch<
export default class UserFetch extends DataFetch<
UserDatasource, UserDatasource,
UserDefinition, UserDefinition,
UserFetchQuery UserFetchQuery
@ -26,7 +28,7 @@ export default class UserFetch extends DataFetch<
...opts, ...opts,
datasource: { datasource: {
type: "user", type: "user",
tableId: TableNames.USERS, tableId: InternalTable.USER_METADATA,
}, },
}) })
} }

View File

@ -1,16 +1,7 @@
import { Table } from "@budibase/types" import { Table, ViewV1Datasource } from "@budibase/types"
import DataFetch from "./DataFetch" import BaseDataFetch from "./DataFetch"
type ViewV1Datasource = { export default class ViewFetch extends BaseDataFetch<ViewV1Datasource, Table> {
type: "view"
name: string
tableId: string
calculation: string
field: string
groupBy: string
}
export default class ViewFetch extends DataFetch<ViewV1Datasource, Table> {
async getDefinition() { async getDefinition() {
const { datasource } = this.options const { datasource } = this.options

View File

@ -1,14 +1,14 @@
import { SortOrder, ViewV2Enriched, ViewV2Type } from "@budibase/types" import {
import DataFetch from "./DataFetch" SortOrder,
ViewDatasource,
ViewV2Enriched,
ViewV2Type,
} from "@budibase/types"
import BaseDataFetch from "./DataFetch"
import { get } from "svelte/store" import { get } from "svelte/store"
import { helpers } from "@budibase/shared-core" import { helpers } from "@budibase/shared-core"
interface ViewDatasource { export default class ViewV2Fetch extends BaseDataFetch<
type: "viewV2"
id: string
}
export default class ViewV2Fetch extends DataFetch<
ViewDatasource, ViewDatasource,
ViewV2Enriched ViewV2Enriched
> { > {

View File

@ -11,6 +11,7 @@ import GroupUserFetch from "./GroupUserFetch"
import CustomFetch from "./CustomFetch" import CustomFetch from "./CustomFetch"
import QueryArrayFetch from "./QueryArrayFetch" import QueryArrayFetch from "./QueryArrayFetch"
import { APIClient } from "../api/types" import { APIClient } from "../api/types"
import { DataFetchDatasource, Table, ViewV2Enriched } from "@budibase/types"
export type DataFetchType = keyof typeof DataFetchMap export type DataFetchType = keyof typeof DataFetchMap
@ -26,32 +27,88 @@ export const DataFetchMap = {
// Client specific datasource types // Client specific datasource types
provider: NestedProviderFetch, provider: NestedProviderFetch,
field: FieldFetch<"field">, field: FieldFetch,
jsonarray: JSONArrayFetch, jsonarray: JSONArrayFetch,
queryarray: QueryArrayFetch, queryarray: QueryArrayFetch,
} }
export interface DataFetchClassMap {
table: TableFetch
view: ViewFetch
viewV2: ViewV2Fetch
query: QueryFetch
link: RelationshipFetch
user: UserFetch
groupUser: GroupUserFetch
custom: CustomFetch
// Client specific datasource types
provider: NestedProviderFetch
field: FieldFetch
jsonarray: JSONArrayFetch
queryarray: QueryArrayFetch
}
export type DataFetch =
| TableFetch
| ViewFetch
| ViewV2Fetch
| QueryFetch
| RelationshipFetch
| UserFetch
| GroupUserFetch
| CustomFetch
| NestedProviderFetch
| FieldFetch
| JSONArrayFetch
| QueryArrayFetch
export type DataFetchDefinition =
| Table
| ViewV2Enriched
| {
// These fields are added to allow checking these fields on definition usages without requiring constant castings
schema?: Record<string, any> | null
primaryDisplay?: string
rowHeight?: number
type?: string
name?: string
}
// Constructs a new fetch model for a certain datasource // Constructs a new fetch model for a certain datasource
export const fetchData = ({ API, datasource, options }: any) => { export const fetchData = <
const Fetch = DataFetchMap[datasource?.type as DataFetchType] || TableFetch T extends DataFetchDatasource,
Type extends T["type"] = T["type"]
>({
API,
datasource,
options,
}: {
API: APIClient
datasource: T
options: any
}): Type extends keyof DataFetchClassMap
? DataFetchClassMap[Type]
: TableFetch => {
const Fetch = DataFetchMap[datasource?.type] || TableFetch
const fetch = new Fetch({ API, datasource, ...options }) const fetch = new Fetch({ API, datasource, ...options })
// Initially fetch data but don't bother waiting for the result // Initially fetch data but don't bother waiting for the result
fetch.getInitialData() fetch.getInitialData()
return fetch return fetch as any
} }
// Creates an empty fetch instance with no datasource configured, so no data // Creates an empty fetch instance with no datasource configured, so no data
// will initially be loaded // will initially be loaded
const createEmptyFetchInstance = <TDatasource extends { type: DataFetchType }>({ const createEmptyFetchInstance = ({
API, API,
datasource, datasource,
}: { }: {
API: APIClient API: APIClient
datasource: TDatasource datasource: DataFetchDatasource
}) => { }) => {
const handler = DataFetchMap[datasource?.type as DataFetchType] const handler = DataFetchMap[datasource?.type]
if (!handler) { if (!handler) {
return null return null
} }
@ -63,29 +120,25 @@ const createEmptyFetchInstance = <TDatasource extends { type: DataFetchType }>({
} }
// Fetches the definition of any type of datasource // Fetches the definition of any type of datasource
export const getDatasourceDefinition = async < export const getDatasourceDefinition = async ({
TDatasource extends { type: DataFetchType }
>({
API, API,
datasource, datasource,
}: { }: {
API: APIClient API: APIClient
datasource: TDatasource datasource: DataFetchDatasource
}) => { }) => {
const instance = createEmptyFetchInstance({ API, datasource }) const instance = createEmptyFetchInstance({ API, datasource })
return await instance?.getDefinition() return await instance?.getDefinition()
} }
// Fetches the schema of any type of datasource // Fetches the schema of any type of datasource
export const getDatasourceSchema = < export const getDatasourceSchema = ({
TDatasource extends { type: DataFetchType }
>({
API, API,
datasource, datasource,
definition, definition,
}: { }: {
API: APIClient API: APIClient
datasource: TDatasource datasource: DataFetchDatasource
definition?: any definition?: any
}) => { }) => {
const instance = createEmptyFetchInstance({ API, datasource }) const instance = createEmptyFetchInstance({ API, datasource })

View File

@ -1,7 +1,6 @@
export { createAPIClient } from "./api" export { createAPIClient } from "./api"
export type { APIClient } from "./api" export type { APIClient } from "./api"
export { fetchData, DataFetchMap } from "./fetch" export { fetchData, DataFetchMap } from "./fetch"
export type { DataFetchType } from "./fetch"
export * as Constants from "./constants" export * as Constants from "./constants"
export * from "./stores" export * from "./stores"
export * from "./utils" export * from "./utils"

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

@ -1,34 +1,24 @@
const { Curl } = require("../../curl") import { Curl } from "../../curl"
const fs = require("fs") import { readFileSync } from "fs"
const path = require("path") import { join } from "path"
const getData = file => { const getData = (file: string) => {
return fs.readFileSync(path.join(__dirname, `./data/${file}.txt`), "utf8") return readFileSync(join(__dirname, `./data/${file}.txt`), "utf8")
} }
describe("Curl Import", () => { describe("Curl Import", () => {
let curl let curl: Curl
beforeEach(() => { beforeEach(() => {
curl = new Curl() curl = new Curl()
}) })
it("validates unsupported data", async () => { it("validates unsupported data", async () => {
let data expect(await curl.isSupported("{}")).toBe(false)
let supported expect(await curl.isSupported("")).toBe(false)
// JSON
data = "{}"
supported = await curl.isSupported(data)
expect(supported).toBe(false)
// Empty
data = ""
supported = await curl.isSupported(data)
expect(supported).toBe(false)
}) })
const init = async file => { const init = async (file: string) => {
await curl.isSupported(getData(file)) await curl.isSupported(getData(file))
} }
@ -39,14 +29,14 @@ describe("Curl Import", () => {
}) })
describe("Returns queries", () => { describe("Returns queries", () => {
const getQueries = async file => { const getQueries = async (file: string) => {
await init(file) await init(file)
const queries = await curl.getQueries() const queries = await curl.getQueries("fake_datasource_id")
expect(queries.length).toBe(1) expect(queries.length).toBe(1)
return queries return queries
} }
const testVerb = async (file, verb) => { const testVerb = async (file: string, verb: string) => {
const queries = await getQueries(file) const queries = await getQueries(file)
expect(queries[0].queryVerb).toBe(verb) expect(queries[0].queryVerb).toBe(verb)
} }
@ -59,7 +49,7 @@ describe("Curl Import", () => {
await testVerb("patch", "patch") await testVerb("patch", "patch")
}) })
const testPath = async (file, urlPath) => { const testPath = async (file: string, urlPath: string) => {
const queries = await getQueries(file) const queries = await getQueries(file)
expect(queries[0].fields.path).toBe(urlPath) expect(queries[0].fields.path).toBe(urlPath)
} }
@ -69,7 +59,10 @@ describe("Curl Import", () => {
await testPath("path", "http://example.com/paths/abc") await testPath("path", "http://example.com/paths/abc")
}) })
const testHeaders = async (file, headers) => { const testHeaders = async (
file: string,
headers: Record<string, string>
) => {
const queries = await getQueries(file) const queries = await getQueries(file)
expect(queries[0].fields.headers).toStrictEqual(headers) expect(queries[0].fields.headers).toStrictEqual(headers)
} }
@ -82,7 +75,7 @@ describe("Curl Import", () => {
}) })
}) })
const testQuery = async (file, queryString) => { const testQuery = async (file: string, queryString: string) => {
const queries = await getQueries(file) const queries = await getQueries(file)
expect(queries[0].fields.queryString).toBe(queryString) expect(queries[0].fields.queryString).toBe(queryString)
} }
@ -91,7 +84,7 @@ describe("Curl Import", () => {
await testQuery("query", "q1=v1&q1=v2") await testQuery("query", "q1=v1&q1=v2")
}) })
const testBody = async (file, body) => { const testBody = async (file: string, body?: Record<string, any>) => {
const queries = await getQueries(file) const queries = await getQueries(file)
expect(queries[0].fields.requestBody).toStrictEqual( expect(queries[0].fields.requestBody).toStrictEqual(
JSON.stringify(body, null, 2) JSON.stringify(body, null, 2)

View File

@ -1,243 +0,0 @@
const { OpenAPI2 } = require("../../openapi2")
const fs = require("fs")
const path = require("path")
const getData = (file, extension) => {
return fs.readFileSync(
path.join(__dirname, `./data/${file}/${file}.${extension}`),
"utf8"
)
}
describe("OpenAPI2 Import", () => {
let openapi2
beforeEach(() => {
openapi2 = new OpenAPI2()
})
it("validates unsupported data", async () => {
let data
let supported
// non json / yaml
data = "curl http://example.com"
supported = await openapi2.isSupported(data)
expect(supported).toBe(false)
// Empty
data = ""
supported = await openapi2.isSupported(data)
expect(supported).toBe(false)
})
const init = async (file, extension) => {
await openapi2.isSupported(getData(file, extension))
}
const runTests = async (filename, test, assertions) => {
for (let extension of ["json", "yaml"]) {
await test(filename, extension, assertions)
}
}
const testImportInfo = async (file, extension) => {
await init(file, extension)
const info = await openapi2.getInfo()
expect(info.name).toBe("Swagger Petstore")
}
it("returns import info", async () => {
await runTests("petstore", testImportInfo)
})
describe("Returns queries", () => {
const indexQueries = queries => {
return queries.reduce((acc, query) => {
acc[query.name] = query
return acc
}, {})
}
const getQueries = async (file, extension) => {
await init(file, extension)
const queries = await openapi2.getQueries()
expect(queries.length).toBe(6)
return indexQueries(queries)
}
const testVerb = async (file, extension, assertions) => {
const queries = await getQueries(file, extension)
for (let [operationId, method] of Object.entries(assertions)) {
expect(queries[operationId].queryVerb).toBe(method)
}
}
it("populates verb", async () => {
const assertions = {
createEntity: "create",
getEntities: "read",
getEntity: "read",
updateEntity: "update",
patchEntity: "patch",
deleteEntity: "delete",
}
await runTests("crud", testVerb, assertions)
})
const testPath = async (file, extension, assertions) => {
const queries = await getQueries(file, extension)
for (let [operationId, urlPath] of Object.entries(assertions)) {
expect(queries[operationId].fields.path).toBe(urlPath)
}
}
it("populates path", async () => {
const assertions = {
createEntity: "http://example.com/entities",
getEntities: "http://example.com/entities",
getEntity: "http://example.com/entities/{{entityId}}",
updateEntity: "http://example.com/entities/{{entityId}}",
patchEntity: "http://example.com/entities/{{entityId}}",
deleteEntity: "http://example.com/entities/{{entityId}}",
}
await runTests("crud", testPath, assertions)
})
const testHeaders = async (file, extension, assertions) => {
const queries = await getQueries(file, extension)
for (let [operationId, headers] of Object.entries(assertions)) {
expect(queries[operationId].fields.headers).toStrictEqual(headers)
}
}
const contentTypeHeader = {
"Content-Type": "application/json",
}
it("populates headers", async () => {
const assertions = {
createEntity: {
...contentTypeHeader,
},
getEntities: {},
getEntity: {},
updateEntity: {
...contentTypeHeader,
},
patchEntity: {
...contentTypeHeader,
},
deleteEntity: {
"x-api-key": "{{x-api-key}}",
},
}
await runTests("crud", testHeaders, assertions)
})
const testQuery = async (file, extension, assertions) => {
const queries = await getQueries(file, extension)
for (let [operationId, queryString] of Object.entries(assertions)) {
expect(queries[operationId].fields.queryString).toStrictEqual(
queryString
)
}
}
it("populates query", async () => {
const assertions = {
createEntity: "",
getEntities: "page={{page}}&size={{size}}",
getEntity: "",
updateEntity: "",
patchEntity: "",
deleteEntity: "",
}
await runTests("crud", testQuery, assertions)
})
const testParameters = async (file, extension, assertions) => {
const queries = await getQueries(file, extension)
for (let [operationId, parameters] of Object.entries(assertions)) {
expect(queries[operationId].parameters).toStrictEqual(parameters)
}
}
it("populates parameters", async () => {
const assertions = {
createEntity: [],
getEntities: [
{
name: "page",
default: "",
},
{
name: "size",
default: "",
},
],
getEntity: [
{
name: "entityId",
default: "",
},
],
updateEntity: [
{
name: "entityId",
default: "",
},
],
patchEntity: [
{
name: "entityId",
default: "",
},
],
deleteEntity: [
{
name: "entityId",
default: "",
},
{
name: "x-api-key",
default: "",
},
],
}
await runTests("crud", testParameters, assertions)
})
const testBody = async (file, extension, assertions) => {
const queries = await getQueries(file, extension)
for (let [operationId, body] of Object.entries(assertions)) {
expect(queries[operationId].fields.requestBody).toStrictEqual(
JSON.stringify(body, null, 2)
)
}
}
it("populates body", async () => {
const assertions = {
createEntity: {
name: "name",
type: "type",
},
getEntities: undefined,
getEntity: undefined,
updateEntity: {
id: 1,
name: "name",
type: "type",
},
patchEntity: {
id: 1,
name: "name",
type: "type",
},
deleteEntity: undefined,
}
await runTests("crud", testBody, assertions)
})
})
})

View File

@ -0,0 +1,135 @@
import { OpenAPI2 } from "../../openapi2"
import { readFileSync } from "fs"
import { join } from "path"
import { groupBy, mapValues } from "lodash"
import { Query } from "@budibase/types"
const getData = (file: string, extension: string) => {
return readFileSync(
join(__dirname, `./data/${file}/${file}.${extension}`),
"utf8"
)
}
describe("OpenAPI2 Import", () => {
let openapi2: OpenAPI2
beforeEach(() => {
openapi2 = new OpenAPI2()
})
it("validates unsupported data", async () => {
expect(await openapi2.isSupported("curl http://example.com")).toBe(false)
expect(await openapi2.isSupported("")).toBe(false)
})
describe.each(["json", "yaml"])("%s", extension => {
describe("petstore", () => {
beforeEach(async () => {
await openapi2.isSupported(getData("petstore", extension))
})
it("returns import info", async () => {
const { name } = await openapi2.getInfo()
expect(name).toBe("Swagger Petstore")
})
})
describe("crud", () => {
let queries: Record<string, Query>
beforeEach(async () => {
await openapi2.isSupported(getData("crud", extension))
const raw = await openapi2.getQueries("fake_datasource_id")
queries = mapValues(groupBy(raw, "name"), group => group[0])
})
it("should have 6 queries", () => {
expect(Object.keys(queries).length).toBe(6)
})
it.each([
["createEntity", "create"],
["getEntities", "read"],
["getEntity", "read"],
["updateEntity", "update"],
["patchEntity", "patch"],
["deleteEntity", "delete"],
])("should have correct verb for %s", (operationId, method) => {
expect(queries[operationId].queryVerb).toBe(method)
})
it.each([
["createEntity", "http://example.com/entities"],
["getEntities", "http://example.com/entities"],
["getEntity", "http://example.com/entities/{{entityId}}"],
["updateEntity", "http://example.com/entities/{{entityId}}"],
["patchEntity", "http://example.com/entities/{{entityId}}"],
["deleteEntity", "http://example.com/entities/{{entityId}}"],
])("should have correct path for %s", (operationId, urlPath) => {
expect(queries[operationId].fields.path).toBe(urlPath)
})
it.each([
["createEntity", { "Content-Type": "application/json" }],
["getEntities", {}],
["getEntity", {}],
["updateEntity", { "Content-Type": "application/json" }],
["patchEntity", { "Content-Type": "application/json" }],
["deleteEntity", { "x-api-key": "{{x-api-key}}" }],
])(`should have correct headers for %s`, (operationId, headers) => {
expect(queries[operationId].fields.headers).toStrictEqual(headers)
})
it.each([
["createEntity", ""],
["getEntities", "page={{page}}&size={{size}}"],
["getEntity", ""],
["updateEntity", ""],
["patchEntity", ""],
["deleteEntity", ""],
])(
`should have correct query string for %s`,
(operationId, queryString) => {
expect(queries[operationId].fields.queryString).toBe(queryString)
}
)
it.each([
["createEntity", []],
[
"getEntities",
[
{ name: "page", default: "" },
{ name: "size", default: "" },
],
],
["getEntity", [{ name: "entityId", default: "" }]],
["updateEntity", [{ name: "entityId", default: "" }]],
["patchEntity", [{ name: "entityId", default: "" }]],
[
"deleteEntity",
[
{ name: "entityId", default: "" },
{ name: "x-api-key", default: "" },
],
],
])(`should have correct parameters for %s`, (operationId, parameters) => {
expect(queries[operationId].parameters).toStrictEqual(parameters)
})
it.each([
["createEntity", { name: "name", type: "type" }],
["getEntities", undefined],
["getEntity", undefined],
["updateEntity", { id: 1, name: "name", type: "type" }],
["patchEntity", { id: 1, name: "name", type: "type" }],
["deleteEntity", undefined],
])(`should have correct body for %s`, (operationId, body) => {
expect(queries[operationId].fields.requestBody).toBe(
JSON.stringify(body, null, 2)
)
})
})
})
})

View File

@ -1,147 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`viewBuilder Calculate and filter creates a view with the calculation statistics and filter schema 1`] = `
{
"map": "function (doc) {
if ((doc.tableId === "14f1c4e94d6a47b682ce89d35d4c78b0" && !(
doc["myField"] === undefined ||
doc["myField"] === null ||
doc["myField"] === "" ||
(Array.isArray(doc["myField"]) && doc["myField"].length === 0)
)) && (doc["age"] > 17)) {
emit(doc["_id"], doc["myField"]);
}
}",
"meta": {
"calculation": "stats",
"field": "myField",
"filters": [
{
"condition": "MT",
"key": "age",
"value": 17,
},
],
"groupBy": undefined,
"schema": {
"avg": {
"type": "number",
},
"count": {
"type": "number",
},
"field": {
"type": "string",
},
"max": {
"type": "number",
},
"min": {
"type": "number",
},
"sum": {
"type": "number",
},
"sumsqr": {
"type": "number",
},
},
"tableId": "14f1c4e94d6a47b682ce89d35d4c78b0",
},
"reduce": "_stats",
}
`;
exports[`viewBuilder Calculate creates a view with the calculation statistics schema 1`] = `
{
"map": "function (doc) {
if ((doc.tableId === "14f1c4e94d6a47b682ce89d35d4c78b0" && !(
doc["myField"] === undefined ||
doc["myField"] === null ||
doc["myField"] === "" ||
(Array.isArray(doc["myField"]) && doc["myField"].length === 0)
)) ) {
emit(doc["_id"], doc["myField"]);
}
}",
"meta": {
"calculation": "stats",
"field": "myField",
"filters": [],
"groupBy": undefined,
"schema": {
"avg": {
"type": "number",
},
"count": {
"type": "number",
},
"field": {
"type": "string",
},
"max": {
"type": "number",
},
"min": {
"type": "number",
},
"sum": {
"type": "number",
},
"sumsqr": {
"type": "number",
},
},
"tableId": "14f1c4e94d6a47b682ce89d35d4c78b0",
},
"reduce": "_stats",
}
`;
exports[`viewBuilder Filter creates a view with multiple filters and conjunctions 1`] = `
{
"map": "function (doc) {
if (doc.tableId === "14f1c4e94d6a47b682ce89d35d4c78b0" && (doc["Name"] === "Test" || doc["Yes"] > "Value")) {
emit(doc["_id"], doc["undefined"]);
}
}",
"meta": {
"calculation": undefined,
"field": undefined,
"filters": [
{
"condition": "EQUALS",
"key": "Name",
"value": "Test",
},
{
"condition": "MT",
"conjunction": "OR",
"key": "Yes",
"value": "Value",
},
],
"groupBy": undefined,
"schema": null,
"tableId": "14f1c4e94d6a47b682ce89d35d4c78b0",
},
}
`;
exports[`viewBuilder Group By creates a view emitting the group by field 1`] = `
{
"map": "function (doc) {
if (doc.tableId === "14f1c4e94d6a47b682ce89d35d4c78b0" ) {
emit(doc["age"], doc["score"]);
}
}",
"meta": {
"calculation": undefined,
"field": "score",
"filters": [],
"groupBy": "age",
"schema": null,
"tableId": "14f1c4e94d6a47b682ce89d35d4c78b0",
},
}
`;

View File

@ -1,75 +0,0 @@
const viewTemplate = require("../viewBuilder").default
describe("viewBuilder", () => {
describe("Filter", () => {
it("creates a view with multiple filters and conjunctions", () => {
expect(
viewTemplate({
name: "Test View",
tableId: "14f1c4e94d6a47b682ce89d35d4c78b0",
filters: [
{
value: "Test",
condition: "EQUALS",
key: "Name",
},
{
value: "Value",
condition: "MT",
key: "Yes",
conjunction: "OR",
},
],
})
).toMatchSnapshot()
})
})
describe("Calculate", () => {
it("creates a view with the calculation statistics schema", () => {
expect(
viewTemplate({
name: "Calculate View",
field: "myField",
calculation: "stats",
tableId: "14f1c4e94d6a47b682ce89d35d4c78b0",
filters: [],
})
).toMatchSnapshot()
})
})
describe("Group By", () => {
it("creates a view emitting the group by field", () => {
expect(
viewTemplate({
name: "Test Scores Grouped By Age",
tableId: "14f1c4e94d6a47b682ce89d35d4c78b0",
groupBy: "age",
field: "score",
filters: [],
})
).toMatchSnapshot()
})
})
describe("Calculate and filter", () => {
it("creates a view with the calculation statistics and filter schema", () => {
expect(
viewTemplate({
name: "Calculate View",
field: "myField",
calculation: "stats",
tableId: "14f1c4e94d6a47b682ce89d35d4c78b0",
filters: [
{
value: 17,
condition: "MT",
key: "age",
},
],
})
).toMatchSnapshot()
})
})
})

View File

@ -0,0 +1,174 @@
import viewTemplate from "../viewBuilder"
describe("viewBuilder", () => {
describe("Filter", () => {
it("creates a view with multiple filters and conjunctions", () => {
expect(
viewTemplate({
field: "myField",
tableId: "tableId",
filters: [
{
value: "Test",
condition: "EQUALS",
key: "Name",
},
{
value: "Value",
condition: "MT",
key: "Yes",
conjunction: "OR",
},
],
})
).toEqual({
map: `function (doc) {
if (doc.tableId === "tableId" && (doc["Name"] === "Test" || doc["Yes"] > "Value")) {
emit(doc["_id"], doc["myField"]);
}
}`,
meta: {
calculation: undefined,
field: "myField",
filters: [
{
condition: "EQUALS",
key: "Name",
value: "Test",
},
{
condition: "MT",
conjunction: "OR",
key: "Yes",
value: "Value",
},
],
groupBy: undefined,
schema: null,
tableId: "tableId",
},
})
})
})
describe("Calculate", () => {
it("creates a view with the calculation statistics schema", () => {
expect(
viewTemplate({
field: "myField",
calculation: "stats",
tableId: "tableId",
filters: [],
})
).toEqual({
map: `function (doc) {
if ((doc.tableId === "tableId" && !(
doc["myField"] === undefined ||
doc["myField"] === null ||
doc["myField"] === "" ||
(Array.isArray(doc["myField"]) && doc["myField"].length === 0)
)) ) {
emit(doc["_id"], doc["myField"]);
}
}`,
meta: {
calculation: "stats",
field: "myField",
filters: [],
groupBy: undefined,
schema: {
min: { type: "number" },
max: { type: "number" },
avg: { type: "number" },
count: { type: "number" },
sumsqr: { type: "number" },
sum: { type: "number" },
field: { type: "string" },
},
tableId: "tableId",
},
reduce: "_stats",
})
})
})
describe("Group By", () => {
it("creates a view emitting the group by field", () => {
expect(
viewTemplate({
tableId: "tableId",
groupBy: "age",
field: "score",
filters: [],
})
).toEqual({
map: `function (doc) {
if (doc.tableId === "tableId" ) {
emit(doc["age"], doc["score"]);
}
}`,
meta: {
calculation: undefined,
field: "score",
filters: [],
groupBy: "age",
schema: null,
tableId: "tableId",
},
})
})
})
describe("Calculate and filter", () => {
it("creates a view with the calculation statistics and filter schema", () => {
expect(
viewTemplate({
field: "myField",
calculation: "stats",
tableId: "tableId",
filters: [
{
value: 17,
condition: "MT",
key: "age",
},
],
})
).toEqual({
map: `function (doc) {
if ((doc.tableId === "tableId" && !(
doc["myField"] === undefined ||
doc["myField"] === null ||
doc["myField"] === "" ||
(Array.isArray(doc["myField"]) && doc["myField"].length === 0)
)) && (doc["age"] > 17)) {
emit(doc["_id"], doc["myField"]);
}
}`,
meta: {
calculation: "stats",
field: "myField",
filters: [
{
condition: "MT",
key: "age",
value: 17,
},
],
groupBy: undefined,
schema: {
min: { type: "number" },
max: { type: "number" },
avg: { type: "number" },
count: { type: "number" },
sumsqr: { type: "number" },
sum: { type: "number" },
field: { type: "string" },
},
tableId: "tableId",
},
reduce: "_stats",
})
})
})
})

View File

@ -1,4 +1,4 @@
import { ViewFilter, ViewTemplateOpts, DBView } from "@budibase/types" import { ViewFilter, DBView } from "@budibase/types"
const TOKEN_MAP: Record<string, string> = { const TOKEN_MAP: Record<string, string> = {
EQUALS: "===", EQUALS: "===",
@ -120,7 +120,7 @@ function parseFilterExpression(filters: ViewFilter[]) {
* @param groupBy - field to group calculation results on, if any * @param groupBy - field to group calculation results on, if any
*/ */
function parseEmitExpression(field: string, groupBy: string) { function parseEmitExpression(field: string, groupBy: string) {
return `emit(doc["${groupBy || "_id"}"], doc["${field}"]);` return `emit(doc["${groupBy}"], doc["${field}"]);`
} }
/** /**
@ -135,7 +135,19 @@ function parseEmitExpression(field: string, groupBy: string) {
* calculation: an optional calculation to be performed over the view data. * calculation: an optional calculation to be performed over the view data.
*/ */
export default function ( export default function (
{ field, tableId, groupBy, filters = [], calculation }: ViewTemplateOpts, {
field,
tableId,
groupBy,
filters = [],
calculation,
}: {
field: string
tableId: string
groupBy?: string
filters?: ViewFilter[]
calculation?: string
},
groupByMulti?: boolean groupByMulti?: boolean
): DBView { ): DBView {
// first filter can't have a conjunction // first filter can't have a conjunction
@ -168,7 +180,7 @@ export default function (
const parsedFilters = parseFilterExpression(filters) const parsedFilters = parseFilterExpression(filters)
const filterExpression = parsedFilters ? `&& (${parsedFilters})` : "" const filterExpression = parsedFilters ? `&& (${parsedFilters})` : ""
const emitExpression = parseEmitExpression(field, groupBy) const emitExpression = parseEmitExpression(field, groupBy || "_id")
const tableExpression = `doc.tableId === "${tableId}"` const tableExpression = `doc.tableId === "${tableId}"`
const coreExpression = statFilter const coreExpression = statFilter
? `(${tableExpression} && ${statFilter})` ? `(${tableExpression} && ${statFilter})`

View File

@ -3650,6 +3650,51 @@ if (descriptions.length) {
}) })
}) })
if (isInternal || isMSSQL) {
describe("Fields with spaces", () => {
let table: Table
let otherTable: Table
let relatedRow: Row
beforeAll(async () => {
otherTable = await config.api.table.save(defaultTable())
table = await config.api.table.save(
saveTableRequest({
schema: {
links: {
name: "links",
fieldName: "links",
type: FieldType.LINK,
tableId: otherTable._id!,
relationshipType: RelationshipType.ONE_TO_MANY,
},
"nameWithSpace ": {
name: "nameWithSpace ",
type: FieldType.STRING,
},
},
})
)
relatedRow = await config.api.row.save(otherTable._id!, {
name: generator.word(),
description: generator.paragraph(),
})
await config.api.row.save(table._id!, {
"nameWithSpace ": generator.word(),
tableId: table._id!,
links: [relatedRow._id],
})
})
it("Successfully returns rows that have spaces in their field names", async () => {
const { rows } = await config.api.row.search(table._id!)
expect(rows.length).toBe(1)
const row = rows[0]
expect(row["nameWithSpace "]).toBeDefined()
})
})
}
if (!isInternal && !isOracle) { if (!isInternal && !isOracle) {
describe("bigint ids", () => { describe("bigint ids", () => {
let table1: Table, table2: Table let table1: Table, table2: Table

View File

@ -276,6 +276,7 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
encrypt, encrypt,
enableArithAbort: true, enableArithAbort: true,
requestTimeout: env.QUERY_THREAD_TIMEOUT, requestTimeout: env.QUERY_THREAD_TIMEOUT,
connectTimeout: env.QUERY_THREAD_TIMEOUT,
}, },
} }
if (encrypt) { if (encrypt) {

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

@ -6,10 +6,10 @@ import { DBView, SearchFilters } from "../../sdk"
export type ViewTemplateOpts = { export type ViewTemplateOpts = {
field: string field: string
tableId: string tableId: string
groupBy: string groupBy?: string
filters: ViewFilter[] filters: ViewFilter[]
schema: any schema: any
calculation: string calculation?: string
groupByMulti?: boolean groupByMulti?: boolean
} }

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

@ -0,0 +1,93 @@
import { InternalTable, Row, TableSchema } from "../../documents"
export type DataFetchDatasource =
| TableDatasource
| ViewV1Datasource
| ViewDatasource
| QueryDatasource
| RelationshipDatasource
| UserDatasource
| GroupUserDatasource
| CustomDatasource
| NestedProviderDatasource
| FieldDatasource
| QueryArrayFieldDatasource
| JSONArrayFieldDatasource
export interface TableDatasource {
type: "table"
tableId: string
}
export type ViewV1Datasource = {
type: "view"
name: string
tableId: string
calculation: string
field: string
groupBy: string
}
export interface ViewDatasource {
type: "viewV2"
id: string
}
export interface QueryDatasource {
type: "query"
_id: string
fields: Record<string, any> & {
pagination?: {
type: string
location: string
pageParam: string
}
}
queryParams?: Record<string, string>
parameters: { name: string; default: string }[]
}
export interface RelationshipDatasource {
type: "link"
tableId: string
rowId: string
rowTableId: string
fieldName: string
}
export interface UserDatasource {
type: "user"
tableId: InternalTable.USER_METADATA
}
export interface GroupUserDatasource {
type: "groupUser"
tableId: InternalTable.USER_METADATA
}
export interface CustomDatasource {
type: "custom"
data: any
}
export interface NestedProviderDatasource {
type: "provider"
value?: {
schema: TableSchema
primaryDisplay: string
rows: Row[]
}
}
interface BaseFieldDatasource<
TType extends "field" | "queryarray" | "jsonarray"
> {
type: TType
tableId: string
fieldType: "attachment" | "array"
value: string[] | Row[]
}
export type FieldDatasource = BaseFieldDatasource<"field">
export type QueryArrayFieldDatasource = BaseFieldDatasource<"queryarray">
export type JSONArrayFieldDatasource = BaseFieldDatasource<"jsonarray">

View File

@ -0,0 +1,16 @@
import { LegacyFilter, SortOrder, UISearchFilter } from "../../api"
import { SearchFilters } from "../../sdk"
export * from "./datasources"
export interface DataFetchOptions<TQuery = SearchFilters> {
// Search config
filter: UISearchFilter | LegacyFilter[] | null
query: TQuery
// Sorting config
sortColumn: string | null
sortOrder: SortOrder
// Pagination config
limit: number
paginate: boolean
}

View File

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

View File

@ -1,6 +1,6 @@
import { UITable, UIView } from "@budibase/types" import { UITable, UIView } from "@budibase/types"
export type UIDatasource = UITable | UIView export type UIDatasource = UITable | (Omit<UIView, "type"> & { type: "viewV2" })
export interface UIFieldMutation { export interface UIFieldMutation {
visible?: boolean visible?: boolean

View File

@ -8,10 +8,9 @@ import {
UISearchFilter, UISearchFilter,
} from "@budibase/types" } from "@budibase/types"
export interface UITable extends Omit<Table, "type"> { export interface UITable extends Table {
name: string name: string
id: string id: string
type: string
tableId: string tableId: string
primaryDisplay?: string primaryDisplay?: string
sort?: { sort?: {

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"