Merge branch 'state-and-bindings-panels' of github.com:Budibase/budibase into bindings-panel
This commit is contained in:
commit
e3fd84da4d
|
@ -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": {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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", () => {
|
|
@ -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({
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
const _ = require("lodash/fp")
|
import { range } from "lodash/fp"
|
||||||
const { structures } = require("../../../tests")
|
import { structures } from "../.."
|
||||||
|
|
||||||
jest.mock("../../../src/context")
|
jest.mock("../../../src/context")
|
||||||
jest.mock("../../../src/db")
|
jest.mock("../../../src/db")
|
||||||
|
|
||||||
const context = require("../../../src/context")
|
import * as context from "../../../src/context"
|
||||||
const db = require("../../../src/db")
|
import * as db from "../../../src/db"
|
||||||
|
|
||||||
const { getCreatorCount } = require("../../../src/users/users")
|
import { getCreatorCount } from "../../../src/users/users"
|
||||||
|
|
||||||
describe("Users", () => {
|
describe("Users", () => {
|
||||||
let getGlobalDBMock
|
let getGlobalDBMock: jest.SpyInstance
|
||||||
let paginationMock
|
let paginationMock: jest.SpyInstance
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.resetAllMocks()
|
jest.resetAllMocks()
|
||||||
|
@ -22,11 +22,10 @@ describe("Users", () => {
|
||||||
jest.spyOn(db, "getGlobalUserParams")
|
jest.spyOn(db, "getGlobalUserParams")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Retrieves the number of creators", async () => {
|
it("retrieves the number of creators", async () => {
|
||||||
const getUsers = (offset, limit, creators = false) => {
|
const getUsers = (offset: number, limit: number, creators = false) => {
|
||||||
const range = _.range(offset, limit)
|
|
||||||
const opts = creators ? { builder: { global: true } } : undefined
|
const opts = creators ? { builder: { global: true } } : undefined
|
||||||
return range.map(() => structures.users.user(opts))
|
return range(offset, limit).map(() => structures.users.user(opts))
|
||||||
}
|
}
|
||||||
const page1Data = getUsers(0, 8)
|
const page1Data = getUsers(0, 8)
|
||||||
const page2Data = getUsers(8, 12, true)
|
const page2Data = getUsers(8, 12, true)
|
|
@ -81,17 +81,6 @@
|
||||||
color: var(--spectrum-global-color-gray-500) !important;
|
color: var(--spectrum-global-color-gray-500) !important;
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip {
|
|
||||||
position: absolute;
|
|
||||||
pointer-events: none;
|
|
||||||
left: 50%;
|
|
||||||
bottom: calc(100% + 4px);
|
|
||||||
transform: translateX(-50%);
|
|
||||||
text-align: center;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spectrum-Icon--sizeXS {
|
.spectrum-Icon--sizeXS {
|
||||||
width: var(--spectrum-global-dimension-size-150);
|
width: var(--spectrum-global-dimension-size-150);
|
||||||
height: var(--spectrum-global-dimension-size-150);
|
height: var(--spectrum-global-dimension-size-150);
|
||||||
|
|
|
@ -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}>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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("#", "")
|
||||||
}
|
}
|
|
@ -5,6 +5,14 @@
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"lib": ["ESNext"],
|
"lib": ["ESNext"],
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@budibase/*": [
|
||||||
|
"../*/src/index.ts",
|
||||||
|
"../*/src/index.js",
|
||||||
|
"../*",
|
||||||
|
"../../node_modules/@budibase/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": ["./src/**/*"],
|
"include": ["./src/**/*"],
|
||||||
"exclude": ["node_modules", "**/*.json", "**/*.spec.ts", "**/*.spec.js"]
|
"exclude": ["node_modules", "**/*.json", "**/*.spec.ts", "**/*.spec.js"]
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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.
|
* 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
|
||||||
|
|
|
@ -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)
|
|
@ -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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -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)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
export type JSONValue =
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null
|
||||||
|
| { [key: string]: JSONValue }
|
||||||
|
| JSONValue[]
|
|
@ -1,2 +1,3 @@
|
||||||
export * from "./installation"
|
export * from "./installation"
|
||||||
export * from "./events"
|
export * from "./events"
|
||||||
|
export * from "./common"
|
||||||
|
|
|
@ -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 "./stores"
|
||||||
|
export * from "./bindings"
|
||||||
|
export * from "./components"
|
||||||
export * from "./dataFetch"
|
export * from "./dataFetch"
|
||||||
|
|
49
yarn.lock
49
yarn.lock
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue