Merge branch 'master' of github.com:budibase/budibase into pro-279-pull-automation-definitions-into-shared-core
This commit is contained in:
commit
68c7a6a9ca
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||
"version": "3.2.44",
|
||||
"version": "3.2.46",
|
||||
"npmClient": "yarn",
|
||||
"concurrency": 20,
|
||||
"command": {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"description": "A UI solution used in the different Budibase projects.",
|
||||
"version": "0.0.0",
|
||||
"license": "MPL-2.0",
|
||||
"svelte": "src/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"module": "dist/bbui.mjs",
|
||||
"exports": {
|
||||
".": {
|
||||
|
@ -14,7 +14,8 @@
|
|||
"./spectrum-icons-vite.js": "./src/spectrum-icons-vite.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build"
|
||||
"build": "vite build",
|
||||
"dev": "vite build --watch --mode=dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "1.4.0",
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import {
|
||||
default as AbsTooltip,
|
||||
TooltipPosition,
|
||||
TooltipType,
|
||||
} from "../Tooltip/AbsTooltip.svelte"
|
||||
|
||||
export let name = "Add"
|
||||
export let hidden = false
|
||||
export let name: string = "Add"
|
||||
export let hidden: boolean = false
|
||||
export let size = "M"
|
||||
export let hoverable = false
|
||||
export let disabled = false
|
||||
export let color
|
||||
export let hoverColor
|
||||
export let tooltip
|
||||
export let hoverable: boolean = false
|
||||
export let disabled: boolean = false
|
||||
export let color: string | undefined = undefined
|
||||
export let hoverColor: string | undefined = undefined
|
||||
export let tooltip: string | undefined = undefined
|
||||
export let tooltipPosition = TooltipPosition.Bottom
|
||||
export let tooltipType = TooltipType.Default
|
||||
export let tooltipColor
|
||||
export let tooltipWrap = true
|
||||
export let newStyles = false
|
||||
export let tooltipColor: string | undefined = undefined
|
||||
export let tooltipWrap: boolean = true
|
||||
export let newStyles: boolean = false
|
||||
</script>
|
||||
|
||||
<AbsTooltip
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
export let size = "M"
|
||||
export let tooltip = ""
|
||||
export let muted
|
||||
export let muted = undefined
|
||||
</script>
|
||||
|
||||
<TooltipWrapper {tooltip} {size}>
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
export let type = TooltipType.Default
|
||||
export let text = ""
|
||||
export let fixed = false
|
||||
export let color = null
|
||||
export let color = ""
|
||||
export let noWrap = false
|
||||
|
||||
let wrapper
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
import "@spectrum-css/typography/dist/index-vars.css"
|
||||
|
||||
// Sizes
|
||||
export let size = "M"
|
||||
export let textAlign = undefined
|
||||
export let noPadding = false
|
||||
export let weight = "default" // light, heavy, default
|
||||
export let size: "XS" | "S" | "M" | "L" = "M"
|
||||
export let textAlign: string | undefined = undefined
|
||||
export let noPadding: boolean = false
|
||||
export let weight: "light" | "heavy" | "default" = "default"
|
||||
</script>
|
||||
|
||||
<h1
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
declare module "./helpers" {
|
||||
export const cloneDeep: <T>(obj: T) => T
|
||||
}
|
|
@ -6,9 +6,8 @@ export const deepGet = helpers.deepGet
|
|||
/**
|
||||
* Generates a DOM safe UUID.
|
||||
* 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 => {
|
||||
const r = (Math.random() * 16) | 0
|
||||
const v = c === "x" ? r : (r & 0x3) | 0x8
|
||||
|
@ -18,22 +17,18 @@ export function uuid() {
|
|||
|
||||
/**
|
||||
* 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) {
|
||||
return string
|
||||
return ""
|
||||
}
|
||||
return string.substring(0, 1).toUpperCase() + string.substring(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return "0"
|
||||
}
|
||||
|
@ -54,11 +49,12 @@ export const hashString = string => {
|
|||
* will override the value "foo" rather than "bar".
|
||||
* If a deep path is specified and the parent keys don't exist then these will
|
||||
* 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) {
|
||||
return
|
||||
}
|
||||
|
@ -82,9 +78,8 @@ export const deepSet = (obj, key, value) => {
|
|||
|
||||
/**
|
||||
* 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) {
|
||||
return obj
|
||||
}
|
||||
|
@ -93,9 +88,8 @@ export const cloneDeep = obj => {
|
|||
|
||||
/**
|
||||
* 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 => {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
// 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.
|
||||
export const parseDate = (value, { enableTime = true }) => {
|
||||
export const parseDate = (
|
||||
value: string | dayjs.Dayjs | null,
|
||||
{ enableTime = true }
|
||||
): dayjs.Dayjs | null => {
|
||||
// If empty then invalid
|
||||
if (!value) {
|
||||
return null
|
||||
|
@ -128,7 +125,7 @@ export const parseDate = (value, { enableTime = true }) => {
|
|||
// Certain string values need transformed
|
||||
if (typeof value === "string") {
|
||||
// Check for time only values
|
||||
if (!isNaN(new Date(`0-${value}`))) {
|
||||
if (!isNaN(new Date(`0-${value}`).valueOf())) {
|
||||
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
|
||||
// schema flags
|
||||
export const stringifyDate = (
|
||||
value,
|
||||
value: null | dayjs.Dayjs,
|
||||
{ enableTime = true, timeOnly = false, ignoreTimezones = false } = {}
|
||||
) => {
|
||||
): string | null => {
|
||||
if (!value) {
|
||||
return null
|
||||
}
|
||||
|
@ -192,7 +189,7 @@ export const stringifyDate = (
|
|||
}
|
||||
|
||||
// Determine the dayjs-compatible format of the browser's default locale
|
||||
const getPatternForPart = part => {
|
||||
const getPatternForPart = (part: Intl.DateTimeFormatPart): string => {
|
||||
switch (part.type) {
|
||||
case "day":
|
||||
return "D".repeat(part.value.length)
|
||||
|
@ -214,9 +211,9 @@ const localeDateFormat = new Intl.DateTimeFormat()
|
|||
|
||||
// Formats a dayjs date according to schema flags
|
||||
export const getDateDisplayValue = (
|
||||
value,
|
||||
value: dayjs.Dayjs | null,
|
||||
{ enableTime = true, timeOnly = false } = {}
|
||||
) => {
|
||||
): string => {
|
||||
if (!value?.isValid()) {
|
||||
return ""
|
||||
}
|
||||
|
@ -229,7 +226,7 @@ export const getDateDisplayValue = (
|
|||
}
|
||||
}
|
||||
|
||||
export const hexToRGBA = (color, opacity) => {
|
||||
export const hexToRGBA = (color: string, opacity: number): string => {
|
||||
if (color.includes("#")) {
|
||||
color = color.replace("#", "")
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
const { vitePreprocess } = require("@sveltejs/vite-plugin-svelte")
|
||||
|
||||
const config = {
|
||||
preprocess: vitePreprocess(),
|
||||
}
|
||||
|
||||
module.exports = config
|
|
@ -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"]
|
||||
}
|
|
@ -9,7 +9,7 @@ export default defineConfig(({ mode }) => {
|
|||
build: {
|
||||
sourcemap: !isProduction,
|
||||
lib: {
|
||||
entry: "src/index.js",
|
||||
entry: "src/index.ts",
|
||||
formats: ["es"],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -74,7 +74,6 @@
|
|||
"dayjs": "^1.10.8",
|
||||
"downloadjs": "1.4.7",
|
||||
"fast-json-patch": "^3.1.1",
|
||||
"json-format-highlight": "^1.0.4",
|
||||
"lodash": "4.17.21",
|
||||
"posthog-js": "^1.118.0",
|
||||
"remixicon": "2.5.0",
|
||||
|
@ -94,6 +93,7 @@
|
|||
"@sveltejs/vite-plugin-svelte": "1.4.0",
|
||||
"@testing-library/jest-dom": "6.4.2",
|
||||
"@testing-library/svelte": "^4.1.0",
|
||||
"@types/sanitize-html": "^2.13.0",
|
||||
"@types/shortid": "^2.2.0",
|
||||
"babel-jest": "^29.6.2",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
} from "@budibase/bbui"
|
||||
import { onMount, createEventDispatcher } from "svelte"
|
||||
import { flags } from "@/stores/builder"
|
||||
import { featureFlags, licensing } from "@/stores/portal"
|
||||
import { licensing } from "@/stores/portal"
|
||||
import { API } from "@/api"
|
||||
import MagicWand from "../../../../assets/MagicWand.svelte"
|
||||
|
||||
|
@ -27,8 +27,7 @@
|
|||
let loadingAICronExpression = false
|
||||
|
||||
$: aiEnabled =
|
||||
($featureFlags.AI_CUSTOM_CONFIGS && $licensing.customAIConfigsEnabled) ||
|
||||
($featureFlags.BUDIBASE_AI && $licensing.budibaseAIEnabled)
|
||||
$licensing.customAIConfigsEnabled || $licensing.budibaseAIEnabled
|
||||
$: {
|
||||
if (cronExpression) {
|
||||
try {
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
import { createEventDispatcher, getContext, onMount } from "svelte"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { tables, datasources } from "@/stores/builder"
|
||||
import { featureFlags } from "@/stores/portal"
|
||||
import { licensing } from "@/stores/portal"
|
||||
import { TableNames, UNEDITABLE_USER_FIELDS } from "@/constants"
|
||||
import {
|
||||
FIELDS,
|
||||
|
@ -100,7 +100,8 @@
|
|||
let optionsValid = true
|
||||
|
||||
$: rowGoldenSample = RowUtils.generateGoldenSample($rows)
|
||||
$: aiEnabled = $featureFlags.BUDIBASE_AI || $featureFlags.AI_CUSTOM_CONFIGS
|
||||
$: aiEnabled =
|
||||
$licensing.customAIConfigsEnabled || $licensing.budibaseAiEnabled
|
||||
$: if (primaryDisplay) {
|
||||
editableColumn.constraints.presence = { allowEmpty: false }
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import { Label } from "@budibase/bbui"
|
||||
import { onMount, createEventDispatcher, onDestroy } from "svelte"
|
||||
import { FIND_ANY_HBS_REGEX } from "@budibase/string-templates"
|
||||
|
@ -12,7 +12,6 @@
|
|||
completionStatus,
|
||||
} from "@codemirror/autocomplete"
|
||||
import {
|
||||
EditorView,
|
||||
lineNumbers,
|
||||
keymap,
|
||||
highlightSpecialChars,
|
||||
|
@ -25,6 +24,7 @@
|
|||
MatchDecorator,
|
||||
ViewPlugin,
|
||||
Decoration,
|
||||
EditorView,
|
||||
} from "@codemirror/view"
|
||||
import {
|
||||
bracketMatching,
|
||||
|
@ -44,12 +44,14 @@
|
|||
import { javascript } from "@codemirror/lang-javascript"
|
||||
import { EditorModes } from "./"
|
||||
import { themeStore } from "@/stores/portal"
|
||||
import type { EditorMode } from "@budibase/types"
|
||||
|
||||
export let label
|
||||
export let completions = []
|
||||
export let mode = EditorModes.Handlebars
|
||||
export let value = ""
|
||||
export let placeholder = null
|
||||
export let label: string | undefined = undefined
|
||||
// TODO: work out what best type fits this
|
||||
export let completions: any[] = []
|
||||
export let mode: EditorMode = EditorModes.Handlebars
|
||||
export let value: string | null = ""
|
||||
export let placeholder: string | null = null
|
||||
export let autocompleteEnabled = true
|
||||
export let autofocus = false
|
||||
export let jsBindingWrapping = true
|
||||
|
@ -58,8 +60,8 @@
|
|||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let textarea
|
||||
let editor
|
||||
let textarea: HTMLDivElement
|
||||
let editor: EditorView
|
||||
let mounted = false
|
||||
let isEditorInitialised = false
|
||||
let queuedRefresh = false
|
||||
|
@ -100,15 +102,22 @@
|
|||
/**
|
||||
* Will refresh the editor contents only after
|
||||
* 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) {
|
||||
queuedRefresh = true
|
||||
return
|
||||
}
|
||||
|
||||
if (editor.state.doc.toString() !== value || queuedRefresh) {
|
||||
if (
|
||||
editor &&
|
||||
value &&
|
||||
(editor.state.doc.toString() !== value || queuedRefresh)
|
||||
) {
|
||||
editor.dispatch({
|
||||
changes: { from: 0, to: editor.state.doc.length, insert: value },
|
||||
})
|
||||
|
@ -120,12 +129,17 @@
|
|||
export const getCaretPosition = () => {
|
||||
const selection_range = editor.state.selection.ranges[0]
|
||||
return {
|
||||
start: selection_range.from,
|
||||
end: selection_range.to,
|
||||
start: selection_range?.from,
|
||||
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.
|
||||
// Retain focus
|
||||
editor.dispatch({
|
||||
|
@ -192,7 +206,7 @@
|
|||
|
||||
const indentWithTabCustom = {
|
||||
key: "Tab",
|
||||
run: view => {
|
||||
run: (view: EditorView) => {
|
||||
if (completionStatus(view.state) === "active") {
|
||||
acceptCompletion(view)
|
||||
return true
|
||||
|
@ -200,7 +214,7 @@
|
|||
indentMore(view)
|
||||
return true
|
||||
},
|
||||
shift: view => {
|
||||
shift: (view: EditorView) => {
|
||||
indentLess(view)
|
||||
return true
|
||||
},
|
||||
|
@ -232,7 +246,8 @@
|
|||
|
||||
// None of this is reactive, but it never has been, so we just assume most
|
||||
// config flags aren't changed at runtime
|
||||
const buildExtensions = base => {
|
||||
// TODO: work out type for base
|
||||
const buildExtensions = (base: any[]) => {
|
||||
let complete = [...base]
|
||||
|
||||
if (autocompleteEnabled) {
|
||||
|
@ -242,7 +257,7 @@
|
|||
closeOnBlur: true,
|
||||
icons: false,
|
||||
optionClass: completion =>
|
||||
completion.simple
|
||||
"simple" in completion && completion.simple
|
||||
? "autocomplete-option-simple"
|
||||
: "autocomplete-option",
|
||||
})
|
||||
|
@ -347,7 +362,7 @@
|
|||
|
||||
{#if label}
|
||||
<div>
|
||||
<Label small>{label}</Label>
|
||||
<Label size="S">{label}</Label>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
import { getManifest } from "@budibase/string-templates"
|
||||
import sanitizeHtml from "sanitize-html"
|
||||
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: {
|
||||
name: "javascript",
|
||||
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")
|
||||
ele.classList.add("info-bubble")
|
||||
|
||||
|
@ -46,7 +53,7 @@ export const buildHelperInfoNode = (completion, helper) => {
|
|||
return ele
|
||||
}
|
||||
|
||||
const toSpectrumIcon = name => {
|
||||
const toSpectrumIcon = (name: string) => {
|
||||
return `<svg
|
||||
class="spectrum-Icon spectrum-Icon--sizeS"
|
||||
focusable="false"
|
||||
|
@ -58,7 +65,12 @@ const toSpectrumIcon = name => {
|
|||
</svg>`
|
||||
}
|
||||
|
||||
export const buildSectionHeader = (type, sectionName, icon, rank) => {
|
||||
export const buildSectionHeader = (
|
||||
type: string,
|
||||
sectionName: string,
|
||||
icon: string,
|
||||
rank: number
|
||||
) => {
|
||||
const ele = document.createElement("div")
|
||||
ele.classList.add("info-section")
|
||||
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 helperSection = buildSectionHeader(type, sectionName, icon, 99)
|
||||
|
||||
return Object.keys(helpers).reduce((acc, key) => {
|
||||
let helper = helpers[key]
|
||||
acc.push({
|
||||
label: key,
|
||||
info: completion => {
|
||||
return Object.keys(helpers).flatMap(helperName => {
|
||||
let helper = helpers[helperName]
|
||||
return {
|
||||
label: helperName,
|
||||
info: (completion: BindingCompletion) => {
|
||||
return buildHelperInfoNode(completion, helper)
|
||||
},
|
||||
type: "helper",
|
||||
section: helperSection,
|
||||
detail: "Function",
|
||||
apply: (view, completion, from, to) => {
|
||||
insertBinding(view, from, to, key, mode)
|
||||
apply: (
|
||||
view: any,
|
||||
completion: BindingCompletion,
|
||||
from: number,
|
||||
to: number
|
||||
) => {
|
||||
insertBinding(view, from, to, helperName, mode)
|
||||
},
|
||||
})
|
||||
return acc
|
||||
}, [])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const getHelperCompletions = mode => {
|
||||
const manifest = getManifest()
|
||||
return Object.keys(manifest).reduce((acc, key) => {
|
||||
acc = acc || []
|
||||
return [...acc, ...helpersToCompletion(manifest[key], mode)]
|
||||
}, [])
|
||||
export const getHelperCompletions = (mode: {
|
||||
name: "javascript" | "handlebars"
|
||||
}) => {
|
||||
// TODO: manifest needs to be properly typed
|
||||
const manifest: any = getManifest()
|
||||
return Object.keys(manifest).flatMap(key => {
|
||||
return helpersToCompletion(manifest[key], mode)
|
||||
})
|
||||
}
|
||||
|
||||
export const snippetAutoComplete = snippets => {
|
||||
return function myCompletions(context) {
|
||||
export const snippetAutoComplete = (snippets: Snippet[]) => {
|
||||
return function myCompletions(context: CompletionContext) {
|
||||
if (!snippets?.length) {
|
||||
return null
|
||||
}
|
||||
const word = context.matchBefore(/\w*/)
|
||||
if (word.from == word.to && !context.explicit) {
|
||||
if (!word || (word.from == word.to && !context.explicit)) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
|
@ -117,7 +138,12 @@ export const snippetAutoComplete = snippets => {
|
|||
label: `snippets.${snippet.name}`,
|
||||
type: "text",
|
||||
simple: true,
|
||||
apply: (view, completion, from, to) => {
|
||||
apply: (
|
||||
view: any,
|
||||
completion: BindingCompletion,
|
||||
from: number,
|
||||
to: number
|
||||
) => {
|
||||
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 => {
|
||||
const section_parsed = completion.section.name.toLowerCase()
|
||||
const label_parsed = completion.label.toLowerCase()
|
||||
|
@ -138,8 +164,8 @@ const bindingFilter = (options, query) => {
|
|||
})
|
||||
}
|
||||
|
||||
export const hbAutocomplete = baseCompletions => {
|
||||
async function coreCompletion(context) {
|
||||
export const hbAutocomplete = (baseCompletions: BindingCompletion[]) => {
|
||||
async function coreCompletion(context: CompletionContext) {
|
||||
let bindingStart = context.matchBefore(EditorModes.Handlebars.match)
|
||||
|
||||
let options = baseCompletions || []
|
||||
|
@ -149,6 +175,9 @@ export const hbAutocomplete = baseCompletions => {
|
|||
}
|
||||
// Accommodate spaces
|
||||
const match = bindingStart.text.match(/{{[\s]*/)
|
||||
if (!match) {
|
||||
return null
|
||||
}
|
||||
const query = bindingStart.text.replace(match[0], "")
|
||||
let filtered = bindingFilter(options, query)
|
||||
|
||||
|
@ -162,14 +191,17 @@ export const hbAutocomplete = baseCompletions => {
|
|||
return coreCompletion
|
||||
}
|
||||
|
||||
export const jsAutocomplete = baseCompletions => {
|
||||
async function coreCompletion(context) {
|
||||
export const jsAutocomplete = (baseCompletions: BindingCompletion[]) => {
|
||||
async function coreCompletion(context: CompletionContext) {
|
||||
let jsBinding = context.matchBefore(/\$\("[\s\w]*/)
|
||||
let options = baseCompletions || []
|
||||
|
||||
if (jsBinding) {
|
||||
// Accommodate spaces
|
||||
const match = jsBinding.text.match(/\$\("[\s]*/)
|
||||
if (!match) {
|
||||
return null
|
||||
}
|
||||
const query = jsBinding.text.replace(match[0], "")
|
||||
let filtered = bindingFilter(options, query)
|
||||
return {
|
||||
|
@ -185,7 +217,10 @@ export const jsAutocomplete = baseCompletions => {
|
|||
return coreCompletion
|
||||
}
|
||||
|
||||
export const buildBindingInfoNode = (completion, binding) => {
|
||||
export const buildBindingInfoNode = (
|
||||
completion: BindingCompletion,
|
||||
binding: any
|
||||
) => {
|
||||
if (!binding.valueHTML || binding.value == null) {
|
||||
return null
|
||||
}
|
||||
|
@ -196,7 +231,12 @@ export const buildBindingInfoNode = (completion, binding) => {
|
|||
}
|
||||
|
||||
// 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 = ""
|
||||
|
||||
const left = from ? value.substring(0, from) : ""
|
||||
|
@ -212,11 +252,14 @@ export const hbInsert = (value, from, to, text) => {
|
|||
}
|
||||
|
||||
export function jsInsert(
|
||||
value,
|
||||
from,
|
||||
to,
|
||||
text,
|
||||
{ helper, disableWrapping } = {}
|
||||
value: string,
|
||||
from: number,
|
||||
to: number,
|
||||
text: string,
|
||||
{
|
||||
helper,
|
||||
disableWrapping,
|
||||
}: { helper?: boolean; disableWrapping?: boolean } = {}
|
||||
) {
|
||||
let parsedInsert = ""
|
||||
|
||||
|
@ -236,7 +279,13 @@ export function jsInsert(
|
|||
}
|
||||
|
||||
// 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
|
||||
|
||||
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
|
||||
view.dispatch({
|
||||
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 categoryMeta = bindings?.reduce((acc, ele) => {
|
||||
const categoryMeta = bindings?.reduce((acc: any, ele: any) => {
|
||||
acc[ele.category] = acc[ele.category] || {}
|
||||
|
||||
if (ele.icon) {
|
||||
|
@ -298,36 +356,46 @@ export const bindingsToCompletions = (bindings, mode) => {
|
|||
return acc
|
||||
}, {})
|
||||
|
||||
const completions = Object.keys(bindingByCategory).reduce((comps, catKey) => {
|
||||
const { icon, rank } = categoryMeta[catKey] || {}
|
||||
const completions = Object.keys(bindingByCategory).reduce(
|
||||
(comps: any, catKey: string) => {
|
||||
const { icon, rank } = categoryMeta[catKey] || {}
|
||||
|
||||
const bindindSectionHeader = buildSectionHeader(
|
||||
bindingByCategory.type,
|
||||
catKey,
|
||||
icon || "",
|
||||
typeof rank == "number" ? rank : 1
|
||||
)
|
||||
const bindingSectionHeader = buildSectionHeader(
|
||||
// @ts-ignore something wrong with this - logically this should be dictionary
|
||||
bindingByCategory.type,
|
||||
catKey,
|
||||
icon || "",
|
||||
typeof rank == "number" ? rank : 1
|
||||
)
|
||||
|
||||
return [
|
||||
...comps,
|
||||
...bindingByCategory[catKey].reduce((acc, binding) => {
|
||||
let displayType = binding.fieldSchema?.type || binding.display?.type
|
||||
acc.push({
|
||||
label: binding.display?.name || binding.readableBinding || "NO NAME",
|
||||
info: completion => {
|
||||
return buildBindingInfoNode(completion, binding)
|
||||
},
|
||||
type: "binding",
|
||||
detail: displayType,
|
||||
section: bindindSectionHeader,
|
||||
apply: (view, completion, from, to) => {
|
||||
insertBinding(view, from, to, binding.readableBinding, mode)
|
||||
},
|
||||
})
|
||||
return acc
|
||||
}, []),
|
||||
]
|
||||
}, [])
|
||||
return [
|
||||
...comps,
|
||||
...bindingByCategory[catKey].reduce((acc, binding) => {
|
||||
let displayType = binding.fieldSchema?.type || binding.display?.type
|
||||
acc.push({
|
||||
label:
|
||||
binding.display?.name || binding.readableBinding || "NO NAME",
|
||||
info: (completion: BindingCompletion) => {
|
||||
return buildBindingInfoNode(completion, binding)
|
||||
},
|
||||
type: "binding",
|
||||
detail: displayType,
|
||||
section: bindingSectionHeader,
|
||||
apply: (
|
||||
view: any,
|
||||
completion: BindingCompletion,
|
||||
from: number,
|
||||
to: number
|
||||
) => {
|
||||
insertBinding(view, from, to, binding.readableBinding, mode)
|
||||
},
|
||||
})
|
||||
return acc
|
||||
}, []),
|
||||
]
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
return completions
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import {
|
||||
DrawerContent,
|
||||
ActionButton,
|
||||
|
@ -28,45 +28,45 @@
|
|||
import EvaluationSidePanel from "./EvaluationSidePanel.svelte"
|
||||
import SnippetSidePanel from "./SnippetSidePanel.svelte"
|
||||
import { BindingHelpers } from "./utils"
|
||||
import formatHighlight from "json-format-highlight"
|
||||
import { capitalise } from "@/helpers"
|
||||
import { Utils } from "@budibase/frontend-core"
|
||||
import { Utils, JsonFormatter } from "@budibase/frontend-core"
|
||||
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()
|
||||
|
||||
export let bindings = []
|
||||
export let value = ""
|
||||
export let bindings: EnrichedBinding[] = []
|
||||
export let value: string = ""
|
||||
export let allowHBS = true
|
||||
export let allowJS = false
|
||||
export let allowHelpers = true
|
||||
export let allowSnippets = true
|
||||
export let context = null
|
||||
export let snippets = null
|
||||
export let snippets: Snippet[] | null = null
|
||||
export let autofocusEditor = false
|
||||
export let placeholder = null
|
||||
export let showTabBar = true
|
||||
|
||||
const Modes = {
|
||||
Text: "Text",
|
||||
JavaScript: "JavaScript",
|
||||
}
|
||||
const SidePanels = {
|
||||
Bindings: "FlashOn",
|
||||
Evaluation: "Play",
|
||||
Snippets: "Code",
|
||||
}
|
||||
|
||||
let mode
|
||||
let sidePanel
|
||||
let mode: BindingMode | null
|
||||
let sidePanel: SidePanel | null
|
||||
let initialValueJS = value?.startsWith?.("{{ js ")
|
||||
let jsValue = initialValueJS ? value : null
|
||||
let hbsValue = initialValueJS ? null : value
|
||||
let getCaretPosition
|
||||
let insertAtPos
|
||||
let targetMode = null
|
||||
let expressionResult
|
||||
let expressionError
|
||||
let jsValue: string | null = initialValueJS ? value : null
|
||||
let hbsValue: string | null = initialValueJS ? null : value
|
||||
let getCaretPosition: CaretPositionFn | undefined
|
||||
let insertAtPos: InsertAtPositionFn | undefined
|
||||
let targetMode: BindingMode | null = null
|
||||
let expressionResult: string | undefined
|
||||
let expressionError: string | undefined
|
||||
let evaluating = false
|
||||
|
||||
$: useSnippets = allowSnippets && !$licensing.isFreePlan
|
||||
|
@ -78,10 +78,12 @@
|
|||
mode
|
||||
)
|
||||
$: enrichedBindings = enrichBindings(bindings, context, snippets)
|
||||
$: usingJS = mode === Modes.JavaScript
|
||||
$: usingJS = mode === BindingMode.JavaScript
|
||||
$: editorMode =
|
||||
mode === Modes.JavaScript ? EditorModes.JS : EditorModes.Handlebars
|
||||
$: editorValue = editorMode === EditorModes.JS ? jsValue : hbsValue
|
||||
mode === BindingMode.JavaScript ? EditorModes.JS : EditorModes.Handlebars
|
||||
$: editorValue = (editorMode === EditorModes.JS ? jsValue : hbsValue) as
|
||||
| string
|
||||
| null
|
||||
$: runtimeExpression = readableToRuntimeBinding(enrichedBindings, value)
|
||||
$: requestEval(runtimeExpression, context, snippets)
|
||||
$: bindingCompletions = bindingsToCompletions(enrichedBindings, editorMode)
|
||||
|
@ -95,7 +97,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
const getHBSCompletions = bindingCompletions => {
|
||||
const getHBSCompletions = (bindingCompletions: BindingCompletion[]) => {
|
||||
return [
|
||||
hbAutocomplete([
|
||||
...bindingCompletions,
|
||||
|
@ -104,71 +106,87 @@
|
|||
]
|
||||
}
|
||||
|
||||
const getJSCompletions = (bindingCompletions, snippets, useSnippets) => {
|
||||
const completions = [
|
||||
const getJSCompletions = (
|
||||
bindingCompletions: BindingCompletion[],
|
||||
snippets: Snippet[] | null,
|
||||
useSnippets?: boolean
|
||||
) => {
|
||||
const completions: ((_: CompletionContext) => any)[] = [
|
||||
jsAutocomplete([
|
||||
...bindingCompletions,
|
||||
...getHelperCompletions(EditorModes.JS),
|
||||
]),
|
||||
]
|
||||
if (useSnippets) {
|
||||
if (useSnippets && snippets) {
|
||||
completions.push(snippetAutoComplete(snippets))
|
||||
}
|
||||
return completions
|
||||
}
|
||||
|
||||
const getModeOptions = (allowHBS, allowJS) => {
|
||||
const getModeOptions = (allowHBS: boolean, allowJS: boolean) => {
|
||||
let options = []
|
||||
if (allowHBS) {
|
||||
options.push(Modes.Text)
|
||||
options.push(BindingMode.Text)
|
||||
}
|
||||
if (allowJS) {
|
||||
options.push(Modes.JavaScript)
|
||||
options.push(BindingMode.JavaScript)
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
const getSidePanelOptions = (bindings, context, useSnippets, mode) => {
|
||||
const getSidePanelOptions = (
|
||||
bindings: EnrichedBinding[],
|
||||
context: any,
|
||||
useSnippets: boolean,
|
||||
mode: BindingMode | null
|
||||
) => {
|
||||
let options = []
|
||||
if (bindings?.length) {
|
||||
options.push(SidePanels.Bindings)
|
||||
options.push(SidePanel.Bindings)
|
||||
}
|
||||
if (context && Object.keys(context).length > 0) {
|
||||
options.push(SidePanels.Evaluation)
|
||||
options.push(SidePanel.Evaluation)
|
||||
}
|
||||
if (useSnippets && mode === Modes.JavaScript) {
|
||||
options.push(SidePanels.Snippets)
|
||||
if (useSnippets && mode === BindingMode.JavaScript) {
|
||||
options.push(SidePanel.Snippets)
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
const debouncedEval = Utils.debounce((expression, context, snippets) => {
|
||||
try {
|
||||
expressionError = null
|
||||
expressionResult = processStringSync(
|
||||
expression || "",
|
||||
{
|
||||
...context,
|
||||
snippets,
|
||||
},
|
||||
{
|
||||
noThrow: false,
|
||||
}
|
||||
)
|
||||
} catch (err) {
|
||||
expressionResult = null
|
||||
expressionError = err
|
||||
}
|
||||
evaluating = false
|
||||
}, 260)
|
||||
const debouncedEval = Utils.debounce(
|
||||
(expression: string | null, context: any, snippets: Snippet[]) => {
|
||||
try {
|
||||
expressionError = undefined
|
||||
expressionResult = processStringSync(
|
||||
expression || "",
|
||||
{
|
||||
...context,
|
||||
snippets,
|
||||
},
|
||||
{
|
||||
noThrow: false,
|
||||
}
|
||||
)
|
||||
} catch (err: any) {
|
||||
expressionResult = undefined
|
||||
expressionError = err
|
||||
}
|
||||
evaluating = false
|
||||
},
|
||||
260
|
||||
)
|
||||
|
||||
const requestEval = (expression, context, snippets) => {
|
||||
const requestEval = (
|
||||
expression: string | null,
|
||||
context: any,
|
||||
snippets: Snippet[] | null
|
||||
) => {
|
||||
evaluating = true
|
||||
debouncedEval(expression, context, snippets)
|
||||
}
|
||||
|
||||
const highlightJSON = json => {
|
||||
return formatHighlight(json, {
|
||||
const highlightJSON = (json: JSONValue) => {
|
||||
return JsonFormatter.format(json, {
|
||||
keyColor: "#e06c75",
|
||||
numberColor: "#e5c07b",
|
||||
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
|
||||
const bindingStrings = bindings.map(binding => {
|
||||
if (binding.runtimeBinding.startsWith('trim "')) {
|
||||
|
@ -189,17 +211,18 @@
|
|||
return `{{ literal ${binding.runtimeBinding} }}`
|
||||
}
|
||||
})
|
||||
const bindingEvauations = processObjectSync(bindingStrings, {
|
||||
const bindingEvaluations = processObjectSync(bindingStrings, {
|
||||
...context,
|
||||
snippets,
|
||||
})
|
||||
|
||||
// Enrich bindings with evaluations and highlighted HTML
|
||||
return bindings.map((binding, idx) => {
|
||||
if (!context) {
|
||||
if (!context || typeof bindingEvaluations !== "object") {
|
||||
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 {
|
||||
...binding,
|
||||
value,
|
||||
|
@ -208,29 +231,38 @@
|
|||
})
|
||||
}
|
||||
|
||||
const updateValue = val => {
|
||||
const updateValue = (val: any) => {
|
||||
const runtimeExpression = readableToRuntimeBinding(enrichedBindings, val)
|
||||
dispatch("change", val)
|
||||
requestEval(runtimeExpression, context, snippets)
|
||||
}
|
||||
|
||||
const onSelectHelper = (helper, js) => {
|
||||
bindingHelpers.onSelectHelper(js ? jsValue : hbsValue, helper, { js })
|
||||
const onSelectHelper = (helper: Helper, js?: boolean) => {
|
||||
bindingHelpers.onSelectHelper(js ? jsValue : hbsValue, helper, {
|
||||
js,
|
||||
dontDecode: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
const onSelectBinding = (binding, { forceJS } = {}) => {
|
||||
const onSelectBinding = (
|
||||
binding: EnrichedBinding,
|
||||
{ forceJS }: { forceJS?: boolean } = {}
|
||||
) => {
|
||||
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) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get the raw editor value to see if we are abandoning changes
|
||||
let rawValue = editorValue
|
||||
if (mode === Modes.JavaScript) {
|
||||
if (mode === BindingMode.JavaScript && rawValue) {
|
||||
rawValue = decodeJSBinding(rawValue)
|
||||
}
|
||||
|
||||
|
@ -249,16 +281,16 @@
|
|||
targetMode = null
|
||||
}
|
||||
|
||||
const changeSidePanel = newSidePanel => {
|
||||
const changeSidePanel = (newSidePanel: SidePanel) => {
|
||||
sidePanel = newSidePanel === sidePanel ? null : newSidePanel
|
||||
}
|
||||
|
||||
const onChangeHBSValue = e => {
|
||||
const onChangeHBSValue = (e: { detail: string }) => {
|
||||
hbsValue = e.detail
|
||||
updateValue(hbsValue)
|
||||
}
|
||||
|
||||
const onChangeJSValue = e => {
|
||||
const onChangeJSValue = (e: { detail: string }) => {
|
||||
jsValue = encodeJSBinding(e.detail)
|
||||
if (!e.detail?.trim()) {
|
||||
// Don't bother saving empty values as JS
|
||||
|
@ -268,9 +300,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
const addSnippet = (snippet: Snippet) =>
|
||||
bindingHelpers.onSelectSnippet(snippet)
|
||||
|
||||
onMount(() => {
|
||||
// Set the initial mode appropriately
|
||||
const initialValueMode = initialValueJS ? Modes.JavaScript : Modes.Text
|
||||
const initialValueMode = initialValueJS
|
||||
? BindingMode.JavaScript
|
||||
: BindingMode.Text
|
||||
if (editorModeOptions.includes(initialValueMode)) {
|
||||
mode = initialValueMode
|
||||
} else {
|
||||
|
@ -314,7 +351,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
<div class="editor">
|
||||
{#if mode === Modes.Text}
|
||||
{#if mode === BindingMode.Text}
|
||||
{#key hbsCompletions}
|
||||
<CodeEditor
|
||||
value={hbsValue}
|
||||
|
@ -328,10 +365,10 @@
|
|||
jsBindingWrapping={false}
|
||||
/>
|
||||
{/key}
|
||||
{:else if mode === Modes.JavaScript}
|
||||
{:else if mode === BindingMode.JavaScript}
|
||||
{#key jsCompletions}
|
||||
<CodeEditor
|
||||
value={decodeJSBinding(jsValue)}
|
||||
value={jsValue ? decodeJSBinding(jsValue) : jsValue}
|
||||
on:change={onChangeJSValue}
|
||||
completions={jsCompletions}
|
||||
mode={EditorModes.JS}
|
||||
|
@ -371,7 +408,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="side" class:visible={!!sidePanel}>
|
||||
{#if sidePanel === SidePanels.Bindings}
|
||||
{#if sidePanel === SidePanel.Bindings}
|
||||
<BindingSidePanel
|
||||
bindings={enrichedBindings}
|
||||
{allowHelpers}
|
||||
|
@ -380,18 +417,15 @@
|
|||
addBinding={onSelectBinding}
|
||||
mode={editorMode}
|
||||
/>
|
||||
{:else if sidePanel === SidePanels.Evaluation}
|
||||
{:else if sidePanel === SidePanel.Evaluation}
|
||||
<EvaluationSidePanel
|
||||
{expressionResult}
|
||||
{expressionError}
|
||||
{evaluating}
|
||||
expression={editorValue}
|
||||
/>
|
||||
{:else if sidePanel === SidePanels.Snippets}
|
||||
<SnippetSidePanel
|
||||
addSnippet={snippet => bindingHelpers.onSelectSnippet(snippet)}
|
||||
{snippets}
|
||||
expression={editorValue ? editorValue : ""}
|
||||
/>
|
||||
{:else if sidePanel === SidePanel.Snippets}
|
||||
<SnippetSidePanel {addSnippet} {snippets} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,28 +1,31 @@
|
|||
<script>
|
||||
import formatHighlight from "json-format-highlight"
|
||||
<script lang="ts">
|
||||
import { JsonFormatter } from "@budibase/frontend-core"
|
||||
import { Icon, ProgressCircle, notifications } from "@budibase/bbui"
|
||||
import { copyToClipboard } from "@budibase/bbui/helpers"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
import { fade } from "svelte/transition"
|
||||
import { UserScriptError } from "@budibase/string-templates"
|
||||
import type { JSONValue } from "@budibase/types"
|
||||
|
||||
export let expressionResult
|
||||
export let expressionError
|
||||
// this can be essentially any primitive response from the JS function
|
||||
export let expressionResult: JSONValue | undefined = undefined
|
||||
export let expressionError: string | undefined = undefined
|
||||
export let evaluating = false
|
||||
export let expression = null
|
||||
export let expression: string | null = null
|
||||
|
||||
$: error = expressionError != null
|
||||
$: empty = expression == null || expression?.trim() === ""
|
||||
$: success = !error && !empty
|
||||
$: highlightedResult = highlight(expressionResult)
|
||||
|
||||
const formatError = err => {
|
||||
const formatError = (err: any) => {
|
||||
if (err.code === UserScriptError.code) {
|
||||
return err.userScriptError.toString()
|
||||
}
|
||||
return err.toString()
|
||||
}
|
||||
|
||||
const highlight = json => {
|
||||
// json can be any primitive type
|
||||
const highlight = (json?: any | null) => {
|
||||
if (json == null) {
|
||||
return ""
|
||||
}
|
||||
|
@ -31,10 +34,10 @@
|
|||
try {
|
||||
json = JSON.stringify(JSON.parse(json), null, 2)
|
||||
} catch (err) {
|
||||
// Ignore
|
||||
// couldn't parse/stringify, just treat it as the raw input
|
||||
}
|
||||
|
||||
return formatHighlight(json, {
|
||||
return JsonFormatter.format(json, {
|
||||
keyColor: "#e06c75",
|
||||
numberColor: "#e5c07b",
|
||||
stringColor: "#98c379",
|
||||
|
@ -45,11 +48,11 @@
|
|||
}
|
||||
|
||||
const copy = () => {
|
||||
let clipboardVal = expressionResult.result
|
||||
let clipboardVal = expressionResult
|
||||
if (typeof clipboardVal === "object") {
|
||||
clipboardVal = JSON.stringify(clipboardVal, null, 2)
|
||||
}
|
||||
copyToClipboard(clipboardVal)
|
||||
Helpers.copyToClipboard(clipboardVal)
|
||||
notifications.success("Value copied to clipboard")
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
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 { API } from "@/api"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
|
@ -53,7 +53,7 @@
|
|||
{buttons}
|
||||
allowAddRows
|
||||
allowDeleteRows
|
||||
aiEnabled={$featureFlags.BUDIBASE_AI || $featureFlags.AI_CUSTOM_CONFIGS}
|
||||
aiEnabled={$licensing.customAIConfigsEnabled || $licensing.budibaseAiEnabled}
|
||||
showAvatars={false}
|
||||
on:updatedatasource={handleGridViewUpdate}
|
||||
isCloud={$admin.cloud}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
rowActions,
|
||||
roles,
|
||||
} from "@/stores/builder"
|
||||
import { themeStore, admin, featureFlags } from "@/stores/portal"
|
||||
import { themeStore, admin, licensing } from "@/stores/portal"
|
||||
import { TableNames } from "@/constants"
|
||||
import { Grid } from "@budibase/frontend-core"
|
||||
import { API } from "@/api"
|
||||
|
@ -130,7 +130,8 @@
|
|||
schemaOverrides={isUsersTable ? userSchemaOverrides : null}
|
||||
showAvatars={false}
|
||||
isCloud={$admin.cloud}
|
||||
aiEnabled={$featureFlags.BUDIBASE_AI || $featureFlags.AI_CUSTOM_CONFIGS}
|
||||
aiEnabled={$licensing.customAIConfigsEnabled ||
|
||||
$licensing.budibaseAIEnabled}
|
||||
{buttons}
|
||||
buttonsCollapsed
|
||||
on:updatedatasource={handleGridTableUpdate}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
Tags,
|
||||
Tag,
|
||||
} from "@budibase/bbui"
|
||||
import { admin, licensing, featureFlags } from "@/stores/portal"
|
||||
import { admin, licensing } from "@/stores/portal"
|
||||
import { API } from "@/api"
|
||||
import AIConfigModal from "./ConfigModal.svelte"
|
||||
import AIConfigTile from "./AIConfigTile.svelte"
|
||||
|
@ -27,8 +27,7 @@
|
|||
let editingUuid
|
||||
|
||||
$: isCloud = $admin.cloud
|
||||
$: customAIConfigsEnabled =
|
||||
$featureFlags.AI_CUSTOM_CONFIGS && $licensing.customAIConfigsEnabled
|
||||
$: customAIConfigsEnabled = $licensing.customAIConfigsEnabled
|
||||
|
||||
async function fetchAIConfig() {
|
||||
try {
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
<script>
|
||||
import { redirect } from "@roxi/routify"
|
||||
import { featureFlags } from "@/stores/portal"
|
||||
|
||||
if ($featureFlags.AI_CUSTOM_CONFIGS) {
|
||||
$redirect("./ai")
|
||||
} else {
|
||||
$redirect("./auth")
|
||||
}
|
||||
$redirect("./ai")
|
||||
</script>
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
GroupUserDatasource,
|
||||
DataFetchOptions,
|
||||
} from "@budibase/types"
|
||||
import { SDK, Component } from "../../index"
|
||||
|
||||
type ProviderDatasource = Exclude<
|
||||
DataFetchDatasource,
|
||||
|
@ -29,8 +28,8 @@
|
|||
export let paginate: boolean
|
||||
export let autoRefresh: number
|
||||
|
||||
const { styleable, Provider, ActionTypes, API } = getContext<SDK>("sdk")
|
||||
const component = getContext<Component>("component")
|
||||
const { styleable, Provider, ActionTypes, API } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
|
||||
let interval: ReturnType<typeof setInterval>
|
||||
let queryExtensions: Record<string, any> = {}
|
||||
|
|
|
@ -1,37 +1,43 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import { getContext } from "svelte"
|
||||
import InnerFormBlock from "./InnerFormBlock.svelte"
|
||||
import { Utils } from "@budibase/frontend-core"
|
||||
import FormBlockWrapper from "./FormBlockWrapper.svelte"
|
||||
import { get } from "svelte/store"
|
||||
import { TableSchema, UIDatasource } from "@budibase/types"
|
||||
|
||||
export let actionType
|
||||
export let dataSource
|
||||
export let size
|
||||
export let disabled
|
||||
export let fields
|
||||
export let buttons
|
||||
export let buttonPosition
|
||||
export let title
|
||||
export let description
|
||||
export let rowId
|
||||
export let actionUrl
|
||||
export let noRowsMessage
|
||||
export let notificationOverride
|
||||
export let buttonsCollapsed
|
||||
export let buttonsCollapsedText
|
||||
type Field = { name: string; active: boolean }
|
||||
|
||||
export let actionType: string
|
||||
export let dataSource: UIDatasource
|
||||
export let size: string
|
||||
export let disabled: boolean
|
||||
export let fields: (Field | string)[]
|
||||
export let buttons: {
|
||||
"##eventHandlerType": string
|
||||
parameters: Record<string, string>
|
||||
}[]
|
||||
export let buttonPosition: "top" | "bottom"
|
||||
export let title: string
|
||||
export let description: string
|
||||
export let rowId: string
|
||||
export let actionUrl: string
|
||||
export let noRowsMessage: string
|
||||
export let notificationOverride: boolean
|
||||
export let buttonsCollapsed: boolean
|
||||
export let buttonsCollapsedText: string
|
||||
|
||||
// Legacy
|
||||
export let showDeleteButton
|
||||
export let showSaveButton
|
||||
export let saveButtonLabel
|
||||
export let deleteButtonLabel
|
||||
export let showDeleteButton: boolean
|
||||
export let showSaveButton: boolean
|
||||
export let saveButtonLabel: boolean
|
||||
export let deleteButtonLabel: boolean
|
||||
|
||||
const { fetchDatasourceSchema, generateGoldenSample } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
const context = getContext("context")
|
||||
|
||||
let schema
|
||||
let schema: TableSchema
|
||||
|
||||
$: fetchSchema(dataSource)
|
||||
$: id = $component.id
|
||||
|
@ -61,7 +67,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
const convertOldFieldFormat = fields => {
|
||||
const convertOldFieldFormat = (fields: (Field | string)[]): Field[] => {
|
||||
if (!fields) {
|
||||
return []
|
||||
}
|
||||
|
@ -82,11 +88,11 @@
|
|||
})
|
||||
}
|
||||
|
||||
const getDefaultFields = (fields, schema) => {
|
||||
const getDefaultFields = (fields: Field[], schema: TableSchema) => {
|
||||
if (!schema) {
|
||||
return []
|
||||
}
|
||||
let defaultFields = []
|
||||
let defaultFields: Field[] = []
|
||||
|
||||
if (!fields || fields.length === 0) {
|
||||
Object.values(schema)
|
||||
|
@ -101,15 +107,14 @@
|
|||
return [...fields, ...defaultFields].filter(field => field.active)
|
||||
}
|
||||
|
||||
const fetchSchema = async () => {
|
||||
schema = (await fetchDatasourceSchema(dataSource)) || {}
|
||||
const fetchSchema = async (datasource: UIDatasource) => {
|
||||
schema = (await fetchDatasourceSchema(datasource)) || {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<FormBlockWrapper {actionType} {dataSource} {rowId} {noRowsMessage}>
|
||||
<InnerFormBlock
|
||||
{dataSource}
|
||||
{actionUrl}
|
||||
{actionType}
|
||||
{size}
|
||||
{disabled}
|
||||
|
@ -117,7 +122,6 @@
|
|||
{title}
|
||||
{description}
|
||||
{schema}
|
||||
{notificationOverride}
|
||||
buttons={buttonsOrDefault}
|
||||
buttonPosition={buttons ? buttonPosition : "top"}
|
||||
{buttonsCollapsed}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import { getContext } from "svelte"
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import MissingRequiredSetting from "./MissingRequiredSetting.svelte"
|
||||
import MissingRequiredAncestor from "./MissingRequiredAncestor.svelte"
|
||||
|
||||
export let missingRequiredSettings
|
||||
export let missingRequiredAncestors
|
||||
export let missingRequiredSettings:
|
||||
| { key: string; label: string }[]
|
||||
| undefined
|
||||
export let missingRequiredAncestors: string[] | undefined
|
||||
|
||||
const component = getContext("component")
|
||||
const { styleable, builderStore } = getContext("sdk")
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import { Component, Context, SDK } from "."
|
||||
|
||||
declare module "svelte" {
|
||||
export function getContext(key: "sdk"): SDK
|
||||
export function getContext(key: "component"): Component
|
||||
export function getContext(key: "context"): Context
|
||||
}
|
|
@ -7,9 +7,17 @@ export interface SDK {
|
|||
styleable: any
|
||||
Provider: any
|
||||
ActionTypes: typeof ActionTypes
|
||||
fetchDatasourceSchema: any
|
||||
generateGoldenSample: any
|
||||
builderStore: Readable<{
|
||||
inBuilder: boolean
|
||||
}>
|
||||
}
|
||||
|
||||
export type Component = Readable<{
|
||||
id: string
|
||||
styles: any
|
||||
errorState: boolean
|
||||
}>
|
||||
|
||||
export type Context = Readable<{}>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { API } from "api"
|
||||
import { DataFetchMap, DataFetchType } from "@budibase/frontend-core"
|
||||
import { FieldType, TableSchema } from "@budibase/types"
|
||||
|
||||
/**
|
||||
* Constructs a fetch instance for a given datasource.
|
||||
|
@ -42,14 +43,14 @@ export const fetchDatasourceSchema = async <
|
|||
}
|
||||
|
||||
// Get the normal schema as long as we aren't wanting a form schema
|
||||
let schema: any
|
||||
let schema: TableSchema | undefined
|
||||
if (datasource?.type !== "query" || !options?.formSchema) {
|
||||
schema = instance.getSchema(definition as any)
|
||||
schema = instance.getSchema(definition as any) as TableSchema
|
||||
} else if ("parameters" in definition && definition.parameters?.length) {
|
||||
schema = {}
|
||||
definition.parameters.forEach(param => {
|
||||
schema[param.name] = { ...param, type: "string" }
|
||||
})
|
||||
for (const param of definition.parameters) {
|
||||
schema[param.name] = { ...param, type: FieldType.STRING }
|
||||
}
|
||||
}
|
||||
if (!schema) {
|
||||
return null
|
||||
|
@ -57,11 +58,11 @@ export const fetchDatasourceSchema = async <
|
|||
|
||||
// Strip hidden fields from views
|
||||
if (datasource.type === "viewV2") {
|
||||
Object.keys(schema).forEach(field => {
|
||||
for (const field of Object.keys(schema)) {
|
||||
if (!schema[field].visible) {
|
||||
delete schema[field]
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Enrich schema with relationships if required
|
||||
|
|
|
@ -8,6 +8,7 @@ export * as search from "./searchFields"
|
|||
export * as SchemaUtils from "./schema"
|
||||
export { memo, derivedMemo } from "./memo"
|
||||
export { createWebsocket } from "./websocket"
|
||||
export * as JsonFormatter from "./jsonFormatter"
|
||||
export * from "./download"
|
||||
export * from "./settings"
|
||||
export * from "./relatedColumns"
|
||||
|
|
|
@ -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 = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
"`": "`",
|
||||
"=": "=",
|
||||
}
|
||||
|
||||
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>`
|
||||
}
|
||||
)
|
||||
}
|
|
@ -43,7 +43,7 @@ export const sequential = fn => {
|
|||
* invocations is enforced.
|
||||
* @param callback an async function to run
|
||||
* @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) => {
|
||||
let timeout
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
`;
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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",
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,4 +1,4 @@
|
|||
import { ViewFilter, ViewTemplateOpts, DBView } from "@budibase/types"
|
||||
import { ViewFilter, DBView } from "@budibase/types"
|
||||
|
||||
const TOKEN_MAP: Record<string, string> = {
|
||||
EQUALS: "===",
|
||||
|
@ -120,7 +120,7 @@ function parseFilterExpression(filters: ViewFilter[]) {
|
|||
* @param groupBy - field to group calculation results on, if any
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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
|
||||
): DBView {
|
||||
// first filter can't have a conjunction
|
||||
|
@ -168,7 +180,7 @@ export default function (
|
|||
const parsedFilters = parseFilterExpression(filters)
|
||||
const filterExpression = parsedFilters ? `&& (${parsedFilters})` : ""
|
||||
|
||||
const emitExpression = parseEmitExpression(field, groupBy)
|
||||
const emitExpression = parseEmitExpression(field, groupBy || "_id")
|
||||
const tableExpression = `doc.tableId === "${tableId}"`
|
||||
const coreExpression = statFilter
|
||||
? `(${tableExpression} && ${statFilter})`
|
||||
|
|
|
@ -123,9 +123,11 @@ async function parseSchema(view: CreateViewRequest) {
|
|||
}
|
||||
|
||||
export async function get(ctx: Ctx<void, ViewResponseEnriched>) {
|
||||
ctx.body = {
|
||||
data: await sdk.views.getEnriched(ctx.params.viewId),
|
||||
const view = await sdk.views.getEnriched(ctx.params.viewId)
|
||||
if (!view) {
|
||||
ctx.throw(404)
|
||||
}
|
||||
ctx.body = { data: view }
|
||||
}
|
||||
|
||||
export async function fetch(ctx: Ctx<void, ViewFetchResponseEnriched>) {
|
||||
|
|
|
@ -22,7 +22,9 @@ export async function get(viewId: string): Promise<ViewV2> {
|
|||
return ensureQueryUISet(found)
|
||||
}
|
||||
|
||||
export async function getEnriched(viewId: string): Promise<ViewV2Enriched> {
|
||||
export async function getEnriched(
|
||||
viewId: string
|
||||
): Promise<ViewV2Enriched | undefined> {
|
||||
const { tableId } = utils.extractViewInfoFromID(viewId)
|
||||
|
||||
const { datasourceId, tableName } = breakExternalTableId(tableId)
|
||||
|
@ -32,7 +34,7 @@ export async function getEnriched(viewId: string): Promise<ViewV2Enriched> {
|
|||
const views = Object.values(table.views!).filter(isV2)
|
||||
const found = views.find(v => v.id === viewId)
|
||||
if (!found) {
|
||||
throw new Error("No view found")
|
||||
return
|
||||
}
|
||||
return await enrichSchema(ensureQueryUISet(found), table.schema)
|
||||
}
|
||||
|
|
|
@ -40,7 +40,9 @@ export async function get(viewId: string): Promise<ViewV2> {
|
|||
return pickApi(tableId).get(viewId)
|
||||
}
|
||||
|
||||
export async function getEnriched(viewId: string): Promise<ViewV2Enriched> {
|
||||
export async function getEnriched(
|
||||
viewId: string
|
||||
): Promise<ViewV2Enriched | undefined> {
|
||||
const { tableId } = utils.extractViewInfoFromID(viewId)
|
||||
return pickApi(tableId).getEnriched(viewId)
|
||||
}
|
||||
|
|
|
@ -17,13 +17,15 @@ export async function get(viewId: string): Promise<ViewV2> {
|
|||
return ensureQueryUISet(found)
|
||||
}
|
||||
|
||||
export async function getEnriched(viewId: string): Promise<ViewV2Enriched> {
|
||||
export async function getEnriched(
|
||||
viewId: string
|
||||
): Promise<ViewV2Enriched | undefined> {
|
||||
const { tableId } = utils.extractViewInfoFromID(viewId)
|
||||
const table = await sdk.tables.getTable(tableId)
|
||||
const views = Object.values(table.views!).filter(isV2)
|
||||
const found = views.find(v => v.id === viewId)
|
||||
if (!found) {
|
||||
throw new Error("No view found")
|
||||
return
|
||||
}
|
||||
return await enrichSchema(ensureQueryUISet(found), table.schema)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
export type JSONValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| { [key: string]: JSONValue }
|
||||
| JSONValue[]
|
|
@ -1,2 +1,3 @@
|
|||
export * from "./installation"
|
||||
export * from "./events"
|
||||
export * from "./common"
|
||||
|
|
|
@ -6,10 +6,10 @@ import { DBView, SearchFilters } from "../../sdk"
|
|||
export type ViewTemplateOpts = {
|
||||
field: string
|
||||
tableId: string
|
||||
groupBy: string
|
||||
groupBy?: string
|
||||
filters: ViewFilter[]
|
||||
schema: any
|
||||
calculation: string
|
||||
calculation?: string
|
||||
groupByMulti?: boolean
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
|||
export interface Helper {
|
||||
example: string
|
||||
description: string
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./binding"
|
||||
export * from "./helper"
|
|
@ -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">
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./sidepanel"
|
||||
export * from "./codeEditor"
|
|
@ -0,0 +1,5 @@
|
|||
export enum SidePanel {
|
||||
Bindings = "FlashOn",
|
||||
Evaluation = "Play",
|
||||
Snippets = "Code",
|
||||
}
|
|
@ -1,2 +1,4 @@
|
|||
export * from "./stores"
|
||||
export * from "./bindings"
|
||||
export * from "./components"
|
||||
export * from "./dataFetch"
|
||||
|
|
49
yarn.lock
49
yarn.lock
|
@ -2131,9 +2131,9 @@
|
|||
through2 "^2.0.0"
|
||||
|
||||
"@budibase/pro@npm:@budibase/pro@latest":
|
||||
version "3.2.32"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-3.2.32.tgz#f6abcd5a5524e7f33d958acb6e610e29995427bb"
|
||||
integrity sha512-bF0pd17IjYugjll2yKYmb0RM+tfKZcCmRBc4XG2NZ4f/I47QaOovm9RqSw6tfqCFuzRewxR3SWmtmSseUc/e0w==
|
||||
version "3.2.44"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-3.2.44.tgz#90367bb2167aafd8c809e000a57d349e5dc4bb78"
|
||||
integrity sha512-Zv2PBVUZUS6/psOpIRIDlW3jrOHWWPhpQXzCk00kIQJaqjkdcvuTXSedQ70u537sQmLu8JsSWbui9MdfF8ksVw==
|
||||
dependencies:
|
||||
"@anthropic-ai/sdk" "^0.27.3"
|
||||
"@budibase/backend-core" "*"
|
||||
|
@ -5926,6 +5926,13 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.5.tgz#f090ff4bd8d2e5b940ff270ab39fd5ca1834a07e"
|
||||
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":
|
||||
version "7.3.7"
|
||||
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"
|
||||
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:
|
||||
version "1.0.2"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
|
@ -18736,7 +18747,7 @@ stringify-object@^3.2.1:
|
|||
is-obj "^1.0.1"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
|
@ -18750,6 +18761,13 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
|
|||
dependencies:
|
||||
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:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2"
|
||||
|
@ -20497,7 +20515,7 @@ worker-farm@1.7.0:
|
|||
dependencies:
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
|
@ -20515,6 +20533,15 @@ wrap-ansi@^5.1.0:
|
|||
string-width "^3.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:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
||||
|
|
Loading…
Reference in New Issue