forked from tsai/budibase
committed by
GitHub
110 changed files with 3299 additions and 828 deletions
Binary file not shown.
@ -0,0 +1,102 @@ |
|||
context("Rename an App", () => { |
|||
beforeEach(() => { |
|||
cy.login() |
|||
cy.createTestApp() |
|||
}) |
|||
|
|||
it("should rename an unpublished application", () => { |
|||
const appRename = "Cypress Renamed" |
|||
// Rename app, Search for app, Confirm name was changed
|
|||
cy.get(".home-logo").click() |
|||
renameApp(appRename) |
|||
cy.searchForApplication(appRename) |
|||
cy.get(".appGrid").find(".wrapper").should("have.length", 1) |
|||
}) |
|||
|
|||
it("Should rename a published application", () => { |
|||
// It is not possible to rename a published application
|
|||
const appRename = "Cypress Renamed" |
|||
// Publish the app
|
|||
cy.get(".toprightnav") |
|||
cy.get(".spectrum-Button").contains("Publish").click({force: true}) |
|||
cy.get(".spectrum-Dialog-grid") |
|||
.within(() => { |
|||
// Click publish again within the modal
|
|||
cy.get(".spectrum-Button").contains("Publish").click({force: true}) |
|||
}) |
|||
// Rename app, Search for app, Confirm name was changed
|
|||
cy.get(".home-logo").click() |
|||
renameApp(appRename, true) |
|||
cy.searchForApplication(appRename) |
|||
cy.get(".appGrid").find(".wrapper").should("have.length", 1) |
|||
}) |
|||
|
|||
it("Should try to rename an application to have no name", () => { |
|||
cy.get(".home-logo").click() |
|||
renameApp(" ", false, true) |
|||
// Close modal and confirm name has not been changed
|
|||
cy.get(".spectrum-Dialog-grid").contains("Cancel").click() |
|||
cy.searchForApplication("Cypress Tests") |
|||
cy.get(".appGrid").find(".wrapper").should("have.length", 1) |
|||
}) |
|||
|
|||
it("Should create two applications with the same name", () => { |
|||
// It is not possible to have applications with the same name
|
|||
const appName = "Cypress Tests" |
|||
cy.visit(`localhost:${Cypress.env("PORT")}/builder`) |
|||
cy.wait(500) |
|||
cy.get(".spectrum-Button").contains("Create app").click({force: true}) |
|||
cy.contains(/Start from scratch/).click() |
|||
cy.get(".spectrum-Modal") |
|||
.within(() => { |
|||
cy.get("input").eq(0).type(appName) |
|||
cy.get(".spectrum-ButtonGroup").contains("Create app").click({force: true}) |
|||
cy.get(".error").should("have.text", "Another app with the same name already exists") |
|||
}) |
|||
}) |
|||
|
|||
it("should validate application names", () => { |
|||
// App name must be letters, numbers and spaces only
|
|||
// This test checks numbers and special characters specifically
|
|||
const numberName = 12345 |
|||
const specialCharName = "£$%^" |
|||
cy.get(".home-logo").click() |
|||
renameApp(numberName) |
|||
cy.searchForApplication(numberName) |
|||
cy.get(".appGrid").find(".wrapper").should("have.length", 1) |
|||
renameApp(specialCharName) |
|||
cy.get(".error").should("have.text", "App name must be letters, numbers and spaces only") |
|||
}) |
|||
|
|||
const renameApp = (appName, published, noName) => { |
|||
cy.request(`localhost:${Cypress.env("PORT")}/api/applications?status=all`) |
|||
.its("body") |
|||
.then(val => { |
|||
if (val.length > 0) { |
|||
cy.get(".title > :nth-child(3) > .spectrum-Icon").click() |
|||
// Check for when an app is published
|
|||
if (published == true){ |
|||
// Should not have Edit as option, will unpublish app
|
|||
cy.should("not.have.value", "Edit") |
|||
cy.get(".spectrum-Menu").contains("Unpublish").click() |
|||
cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click() |
|||
cy.get(".title > :nth-child(3) > .spectrum-Icon").click() |
|||
} |
|||
cy.contains("Edit").click() |
|||
cy.get(".spectrum-Modal") |
|||
.within(() => { |
|||
if (noName == true){ |
|||
cy.get("input").clear() |
|||
cy.get(".spectrum-Dialog-grid").click() |
|||
.contains("App name must be letters, numbers and spaces only") |
|||
return cy |
|||
} |
|||
cy.get("input").clear() |
|||
cy.get("input").eq(0).type(appName).should("have.value", appName).blur() |
|||
cy.get(".spectrum-ButtonGroup").contains("Save").click({force: true}) |
|||
cy.wait(500) |
|||
}) |
|||
} |
|||
}) |
|||
} |
|||
}) |
|||
@ -0,0 +1,159 @@ |
|||
<script context="module"> |
|||
import { Label } from "@budibase/bbui" |
|||
|
|||
export const EditorModes = { |
|||
JS: { |
|||
name: "javascript", |
|||
json: false, |
|||
}, |
|||
JSON: { |
|||
name: "javascript", |
|||
json: true, |
|||
}, |
|||
SQL: { |
|||
name: "sql", |
|||
}, |
|||
Handlebars: { |
|||
name: "handlebars", |
|||
base: "text/html", |
|||
}, |
|||
} |
|||
</script> |
|||
|
|||
<script> |
|||
import CodeMirror from "components/integration/codemirror" |
|||
import { themeStore } from "builderStore" |
|||
import { createEventDispatcher, onMount } from "svelte" |
|||
|
|||
export let mode = EditorModes.JS |
|||
export let value = "" |
|||
export let height = 300 |
|||
export let resize = "none" |
|||
export let readonly = false |
|||
export let hints = [] |
|||
export let label |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
let textarea |
|||
let editor |
|||
|
|||
// Keep editor up to date with value |
|||
$: editor?.setValue(value || "") |
|||
|
|||
// Creates an instance of a code mirror editor |
|||
async function createEditor(mode, value) { |
|||
if (!CodeMirror || !textarea || editor) { |
|||
return |
|||
} |
|||
|
|||
// Configure CM options |
|||
const lightTheme = $themeStore.theme.includes("light") |
|||
const options = { |
|||
mode, |
|||
value: value || "", |
|||
readOnly: readonly, |
|||
theme: lightTheme ? "default" : "tomorrow-night-eighties", |
|||
|
|||
// Style |
|||
lineNumbers: true, |
|||
lineWrapping: true, |
|||
indentWithTabs: true, |
|||
indentUnit: 2, |
|||
tabSize: 2, |
|||
|
|||
// QOL addons |
|||
extraKeys: { "Ctrl-Space": "autocomplete" }, |
|||
styleActiveLine: { nonEmpty: true }, |
|||
autoCloseBrackets: true, |
|||
matchBrackets: true, |
|||
} |
|||
|
|||
// Register hints plugin if desired |
|||
if (hints?.length) { |
|||
CodeMirror.registerHelper("hint", "dictionaryHint", function (editor) { |
|||
const cursor = editor.getCursor() |
|||
return { |
|||
list: hints, |
|||
from: CodeMirror.Pos(cursor.line, cursor.ch), |
|||
to: CodeMirror.Pos(cursor.line, cursor.ch), |
|||
} |
|||
}) |
|||
CodeMirror.commands.autocomplete = function (cm) { |
|||
CodeMirror.showHint(cm, CodeMirror.hint.dictionaryHint) |
|||
} |
|||
} |
|||
|
|||
// Construct CM instance |
|||
editor = CodeMirror.fromTextArea(textarea, options) |
|||
|
|||
// Use a blur handler to update the value |
|||
editor.on("blur", instance => { |
|||
dispatch("change", instance.getValue()) |
|||
}) |
|||
} |
|||
|
|||
// Export a function to expose caret position |
|||
export const getCaretPosition = () => { |
|||
const cursor = editor.getCursor() |
|||
return { |
|||
start: cursor.ch, |
|||
end: cursor.ch, |
|||
} |
|||
} |
|||
|
|||
onMount(() => { |
|||
// Create the editor with initial value |
|||
createEditor(mode, value) |
|||
|
|||
// Clean up editor on unmount |
|||
return () => { |
|||
if (editor) { |
|||
editor.toTextArea() |
|||
} |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
{#if label} |
|||
<div style="margin-bottom: var(--spacing-s)"> |
|||
<Label small>{label}</Label> |
|||
</div> |
|||
{/if} |
|||
<div |
|||
style={`--code-mirror-height: ${height}px; --code-mirror-resize: ${resize}`} |
|||
> |
|||
<textarea tabindex="0" bind:this={textarea} readonly {value} /> |
|||
</div> |
|||
|
|||
<style> |
|||
div :global(.CodeMirror) { |
|||
height: var(--code-mirror-height); |
|||
min-height: var(--code-mirror-height); |
|||
font-family: monospace; |
|||
line-height: 1.3; |
|||
border: var(--spectrum-alias-border-size-thin) solid; |
|||
border-color: var(--spectrum-alias-border-color); |
|||
border-radius: var(--border-radius-s); |
|||
resize: var(--code-mirror-resize); |
|||
overflow: hidden; |
|||
} |
|||
|
|||
/* Override default active line highlight colour in dark theme */ |
|||
div |
|||
:global(.CodeMirror-focused.cm-s-tomorrow-night-eighties |
|||
.CodeMirror-activeline-background) { |
|||
background: rgba(255, 255, 255, 0.075); |
|||
} |
|||
|
|||
/* Remove active line styling when not focused */ |
|||
div |
|||
:global(.CodeMirror:not(.CodeMirror-focused) |
|||
.CodeMirror-activeline-background) { |
|||
background: unset; |
|||
} |
|||
|
|||
/* Add a spectrum themed border when focused */ |
|||
div :global(.CodeMirror-focused) { |
|||
border-color: var(--spectrum-alias-border-color-mouse-focus); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,15 @@ |
|||
<script> |
|||
import { Input } from "@budibase/bbui" |
|||
import { isJSBinding } from "@budibase/string-templates" |
|||
|
|||
export let value |
|||
|
|||
$: isJS = isJSBinding(value) |
|||
</script> |
|||
|
|||
<Input |
|||
{...$$props} |
|||
value={isJS ? "(JavaScript function)" : value} |
|||
readonly={isJS} |
|||
on:change |
|||
/> |
|||
@ -1,12 +1,22 @@ |
|||
import CodeMirror from "codemirror" |
|||
import "codemirror/lib/codemirror.css" |
|||
import "codemirror/theme/tomorrow-night-eighties.css" |
|||
import "codemirror/addon/hint/show-hint.css" |
|||
import "codemirror/theme/neo.css" |
|||
|
|||
// Modes
|
|||
import "codemirror/mode/javascript/javascript" |
|||
import "codemirror/mode/sql/sql" |
|||
import "codemirror/mode/css/css" |
|||
import "codemirror/mode/handlebars/handlebars" |
|||
import "codemirror/mode/javascript/javascript" |
|||
|
|||
// Hints
|
|||
import "codemirror/addon/hint/show-hint" |
|||
import "codemirror/addon/hint/show-hint.css" |
|||
|
|||
// Theming
|
|||
import "codemirror/theme/tomorrow-night-eighties.css" |
|||
|
|||
// Functional addons
|
|||
import "codemirror/addon/selection/active-line" |
|||
import "codemirror/addon/edit/closebrackets" |
|||
import "codemirror/addon/edit/matchbrackets" |
|||
|
|||
export default CodeMirror |
|||
|
|||
File diff suppressed because it is too large
@ -1,10 +1,9 @@ |
|||
const { performBackup } = require("../../utilities/fileSystem") |
|||
const { streamBackup } = require("../../utilities/fileSystem") |
|||
|
|||
exports.exportAppDump = async function (ctx) { |
|||
const { appId } = ctx.query |
|||
const appname = decodeURI(ctx.query.appname) |
|||
const backupIdentifier = `${appname}Backup${new Date().getTime()}.txt` |
|||
|
|||
const appName = decodeURI(ctx.query.appname) |
|||
const backupIdentifier = `${appName}-export-${new Date().getTime()}.txt` |
|||
ctx.attachment(backupIdentifier) |
|||
ctx.body = await performBackup(appId, backupIdentifier) |
|||
ctx.body = await streamBackup(appId) |
|||
} |
|||
|
|||
@ -0,0 +1,21 @@ |
|||
const fetch = require("node-fetch") |
|||
const { VM, VMScript } = require("vm2") |
|||
|
|||
class ScriptRunner { |
|||
constructor(script, context) { |
|||
const code = `let fn = () => {\n${script}\n}; results.out = fn();` |
|||
this.vm = new VM() |
|||
this.results = { out: "" } |
|||
this.vm.setGlobals(context) |
|||
this.vm.setGlobal("fetch", fetch) |
|||
this.vm.setGlobal("results", this.results) |
|||
this.script = new VMScript(code) |
|||
} |
|||
|
|||
execute() { |
|||
this.vm.run(this.script) |
|||
return this.results.out |
|||
} |
|||
} |
|||
|
|||
module.exports = ScriptRunner |
|||
@ -0,0 +1,49 @@ |
|||
const { atob } = require("../utilities") |
|||
|
|||
// The method of executing JS scripts depends on the bundle being built.
|
|||
// This setter is used in the entrypoint (either index.cjs or index.mjs).
|
|||
let runJS |
|||
module.exports.setJSRunner = runner => (runJS = runner) |
|||
|
|||
// Helper utility to strip square brackets from a value
|
|||
const removeSquareBrackets = value => { |
|||
if (!value || typeof value !== "string") { |
|||
return value |
|||
} |
|||
const regex = /\[+(.+)]+/ |
|||
const matches = value.match(regex) |
|||
if (matches && matches[1]) { |
|||
return matches[1] |
|||
} |
|||
return value |
|||
} |
|||
|
|||
// Our context getter function provided to JS code as $.
|
|||
// Extracts a value from context.
|
|||
const getContextValue = (path, context) => { |
|||
let data = context |
|||
path.split(".").forEach(key => { |
|||
if (data == null || typeof data !== "object") { |
|||
return null |
|||
} |
|||
data = data[removeSquareBrackets(key)] |
|||
}) |
|||
return data |
|||
} |
|||
|
|||
// Evaluates JS code against a certain context
|
|||
module.exports.processJS = (handlebars, context) => { |
|||
try { |
|||
// Wrap JS in a function and immediately invoke it.
|
|||
// This is required to allow the final `return` statement to be valid.
|
|||
const js = `function run(){${atob(handlebars)}};run();` |
|||
|
|||
// Our $ context function gets a value from context
|
|||
const sandboxContext = { $: path => getContextValue(path, context) } |
|||
|
|||
// Create a sandbox with out context and run the JS
|
|||
return runJS(js, sandboxContext) |
|||
} catch (error) { |
|||
return "Error while executing JS" |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue