Merge pull request #888 from Budibase/component-sdk
Component SDK & Client Library Rewrite
This commit is contained in:
commit
1f6f6ac02b
|
@ -14,7 +14,7 @@
|
||||||
"prettier-plugin-svelte": "^1.4.0",
|
"prettier-plugin-svelte": "^1.4.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rollup-plugin-replace": "^2.2.0",
|
"rollup-plugin-replace": "^2.2.0",
|
||||||
"svelte": "^3.28.0"
|
"svelte": "^3.30.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"bootstrap": "lerna bootstrap",
|
"bootstrap": "lerna bootstrap",
|
||||||
|
@ -26,7 +26,7 @@
|
||||||
"nuke": "rimraf ~/.budibase && npm run restore",
|
"nuke": "rimraf ~/.budibase && npm run restore",
|
||||||
"clean": "lerna clean",
|
"clean": "lerna clean",
|
||||||
"kill-port": "kill-port 4001",
|
"kill-port": "kill-port 4001",
|
||||||
"dev": "npm run kill-port && node ./scripts/symlinkDev.js && lerna run --parallel dev:builder --concurrency 1",
|
"dev": "yarn run kill-port && node ./scripts/symlinkDev.js && lerna run --parallel dev:builder --concurrency 1",
|
||||||
"test": "lerna run test",
|
"test": "lerna run test",
|
||||||
"lint": "eslint packages",
|
"lint": "eslint packages",
|
||||||
"lint:fix": "eslint --fix packages",
|
"lint:fix": "eslint --fix packages",
|
||||||
|
|
|
@ -16,6 +16,9 @@ process.env.BUDIBASE_API_KEY = "6BE826CB-6B30-4AEC-8777-2E90464633DE"
|
||||||
process.env.NODE_ENV = "cypress"
|
process.env.NODE_ENV = "cypress"
|
||||||
process.env.ENABLE_ANALYTICS = "false"
|
process.env.ENABLE_ANALYTICS = "false"
|
||||||
|
|
||||||
|
// Stop info logs polluting test outputs
|
||||||
|
process.env.LOG_LEVEL = "error"
|
||||||
|
|
||||||
async function run(dir) {
|
async function run(dir) {
|
||||||
process.env.BUDIBASE_DIR = resolve(dir)
|
process.env.BUDIBASE_DIR = resolve(dir)
|
||||||
require("dotenv").config({ path: resolve(dir, ".env") })
|
require("dotenv").config({ path: resolve(dir, ".env") })
|
||||||
|
|
|
@ -81,8 +81,8 @@
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
"svelte-loading-spinners": "^0.1.1",
|
"svelte-loading-spinners": "^0.1.1",
|
||||||
"svelte-portal": "^0.1.0",
|
"svelte-portal": "^0.1.0",
|
||||||
"yup": "^0.29.2",
|
"uuid": "^8.3.1",
|
||||||
"uuid": "^8.3.1"
|
"yup": "^0.29.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.5.5",
|
"@babel/core": "^7.5.5",
|
||||||
|
@ -90,6 +90,7 @@
|
||||||
"@babel/preset-env": "^7.5.5",
|
"@babel/preset-env": "^7.5.5",
|
||||||
"@babel/runtime": "^7.5.5",
|
"@babel/runtime": "^7.5.5",
|
||||||
"@rollup/plugin-alias": "^3.0.1",
|
"@rollup/plugin-alias": "^3.0.1",
|
||||||
|
"@rollup/plugin-commonjs": "^16.0.0",
|
||||||
"@rollup/plugin-json": "^4.0.3",
|
"@rollup/plugin-json": "^4.0.3",
|
||||||
"@sveltech/routify": "1.7.11",
|
"@sveltech/routify": "1.7.11",
|
||||||
"@testing-library/jest-dom": "^5.11.0",
|
"@testing-library/jest-dom": "^5.11.0",
|
||||||
|
@ -104,7 +105,6 @@
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rollup": "^2.11.2",
|
"rollup": "^2.11.2",
|
||||||
"rollup-plugin-alias": "^1.5.2",
|
"rollup-plugin-alias": "^1.5.2",
|
||||||
"rollup-plugin-commonjs": "^10.0.0",
|
|
||||||
"rollup-plugin-copy": "^3.0.0",
|
"rollup-plugin-copy": "^3.0.0",
|
||||||
"rollup-plugin-css-only": "^2.1.0",
|
"rollup-plugin-css-only": "^2.1.0",
|
||||||
"rollup-plugin-livereload": "^1.0.0",
|
"rollup-plugin-livereload": "^1.0.0",
|
||||||
|
@ -115,7 +115,7 @@
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
"rollup-plugin-url": "^2.2.2",
|
"rollup-plugin-url": "^2.2.2",
|
||||||
"start-server-and-test": "^1.11.0",
|
"start-server-and-test": "^1.11.0",
|
||||||
"svelte": "^3.29.0",
|
"svelte": "^3.30.0",
|
||||||
"svelte-jester": "^1.0.6"
|
"svelte-jester": "^1.0.6"
|
||||||
},
|
},
|
||||||
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
|
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import alias from "@rollup/plugin-alias"
|
import alias from "@rollup/plugin-alias"
|
||||||
import svelte from "rollup-plugin-svelte"
|
import svelte from "rollup-plugin-svelte"
|
||||||
import resolve from "rollup-plugin-node-resolve"
|
import resolve from "rollup-plugin-node-resolve"
|
||||||
import commonjs from "rollup-plugin-commonjs"
|
import commonjs from "@rollup/plugin-commonjs"
|
||||||
import url from "rollup-plugin-url"
|
import url from "rollup-plugin-url"
|
||||||
import livereload from "rollup-plugin-livereload"
|
import livereload from "rollup-plugin-livereload"
|
||||||
import { terser } from "rollup-plugin-terser"
|
import { terser } from "rollup-plugin-terser"
|
||||||
|
@ -15,102 +15,7 @@ import json from "@rollup/plugin-json"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
const production = !process.env.ROLLUP_WATCH
|
const production = !process.env.ROLLUP_WATCH
|
||||||
|
|
||||||
const lodash_fp_exports = [
|
|
||||||
"flow",
|
|
||||||
"pipe",
|
|
||||||
"union",
|
|
||||||
"reduce",
|
|
||||||
"isUndefined",
|
|
||||||
"cloneDeep",
|
|
||||||
"split",
|
|
||||||
"some",
|
|
||||||
"map",
|
|
||||||
"filter",
|
|
||||||
"isEmpty",
|
|
||||||
"countBy",
|
|
||||||
"includes",
|
|
||||||
"last",
|
|
||||||
"find",
|
|
||||||
"constant",
|
|
||||||
"take",
|
|
||||||
"first",
|
|
||||||
"intersection",
|
|
||||||
"mapValues",
|
|
||||||
"isNull",
|
|
||||||
"has",
|
|
||||||
"isInteger",
|
|
||||||
"isNumber",
|
|
||||||
"isString",
|
|
||||||
"isBoolean",
|
|
||||||
"isDate",
|
|
||||||
"isArray",
|
|
||||||
"isObject",
|
|
||||||
"clone",
|
|
||||||
"values",
|
|
||||||
"keyBy",
|
|
||||||
"isNaN",
|
|
||||||
"keys",
|
|
||||||
"orderBy",
|
|
||||||
"concat",
|
|
||||||
"reverse",
|
|
||||||
"difference",
|
|
||||||
"merge",
|
|
||||||
"flatten",
|
|
||||||
"each",
|
|
||||||
"pull",
|
|
||||||
"join",
|
|
||||||
"defaultCase",
|
|
||||||
"uniqBy",
|
|
||||||
"every",
|
|
||||||
"uniqWith",
|
|
||||||
"isFunction",
|
|
||||||
"groupBy",
|
|
||||||
"differenceBy",
|
|
||||||
"intersectionBy",
|
|
||||||
"isEqual",
|
|
||||||
"max",
|
|
||||||
"sortBy",
|
|
||||||
"assign",
|
|
||||||
"uniq",
|
|
||||||
"trimChars",
|
|
||||||
"trimCharsStart",
|
|
||||||
"isObjectLike",
|
|
||||||
"flattenDeep",
|
|
||||||
"indexOf",
|
|
||||||
"isPlainObject",
|
|
||||||
"toNumber",
|
|
||||||
"takeRight",
|
|
||||||
"toPairs",
|
|
||||||
"remove",
|
|
||||||
"findIndex",
|
|
||||||
"compose",
|
|
||||||
"get",
|
|
||||||
"tap",
|
|
||||||
]
|
|
||||||
|
|
||||||
const lodash_exports = [
|
|
||||||
"flow",
|
|
||||||
"join",
|
|
||||||
"replace",
|
|
||||||
"trim",
|
|
||||||
"dropRight",
|
|
||||||
"takeRight",
|
|
||||||
"head",
|
|
||||||
"reduce",
|
|
||||||
"tail",
|
|
||||||
"startsWith",
|
|
||||||
"findIndex",
|
|
||||||
"merge",
|
|
||||||
"assign",
|
|
||||||
"each",
|
|
||||||
"find",
|
|
||||||
"orderBy",
|
|
||||||
"union",
|
|
||||||
]
|
|
||||||
|
|
||||||
const outputpath = "../server/builder"
|
const outputpath = "../server/builder"
|
||||||
|
|
||||||
const coreExternal = [
|
const coreExternal = [
|
||||||
"lodash",
|
"lodash",
|
||||||
"lodash/fp",
|
"lodash/fp",
|
||||||
|
@ -224,13 +129,7 @@ export default {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
commonjs({
|
commonjs(),
|
||||||
namedExports: {
|
|
||||||
"lodash/fp": lodash_fp_exports,
|
|
||||||
lodash: lodash_exports,
|
|
||||||
shortid: ["generate"],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
url({
|
url({
|
||||||
limit: 0,
|
limit: 0,
|
||||||
include: ["**/*.woff2", "**/*.png"],
|
include: ["**/*.woff2", "**/*.png"],
|
||||||
|
|
|
@ -24,7 +24,7 @@ import { cloneDeep, difference } from "lodash/fp"
|
||||||
* @returns {Array.<BindableProperty>}
|
* @returns {Array.<BindableProperty>}
|
||||||
*/
|
*/
|
||||||
export default function({ componentInstanceId, screen, components, tables }) {
|
export default function({ componentInstanceId, screen, components, tables }) {
|
||||||
const walkResult = walk({
|
const result = walk({
|
||||||
// cloning so we are free to mutate props (e.g. by adding _contexts)
|
// cloning so we are free to mutate props (e.g. by adding _contexts)
|
||||||
instance: cloneDeep(screen.props),
|
instance: cloneDeep(screen.props),
|
||||||
targetId: componentInstanceId,
|
targetId: componentInstanceId,
|
||||||
|
@ -33,13 +33,10 @@ export default function({ componentInstanceId, screen, components, tables }) {
|
||||||
})
|
})
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...walkResult.bindableInstances
|
...result.bindableInstances
|
||||||
.filter(isInstanceInSharedContext(walkResult))
|
.filter(isInstanceInSharedContext(result))
|
||||||
.map(componentInstanceToBindable(walkResult)),
|
.map(componentInstanceToBindable),
|
||||||
|
...(result.target?._contexts.map(contextToBindables(tables)).flat() ?? []),
|
||||||
...(walkResult.target?._contexts
|
|
||||||
.map(contextToBindables(tables, walkResult))
|
|
||||||
.flat() ?? []),
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,26 +50,18 @@ const isInstanceInSharedContext = walkResult => i =>
|
||||||
|
|
||||||
// turns a component instance prop into binding expressions
|
// turns a component instance prop into binding expressions
|
||||||
// used by the UI
|
// used by the UI
|
||||||
const componentInstanceToBindable = walkResult => i => {
|
const componentInstanceToBindable = i => {
|
||||||
const lastContext =
|
|
||||||
i.instance._contexts.length &&
|
|
||||||
i.instance._contexts[i.instance._contexts.length - 1]
|
|
||||||
const contextParentPath = lastContext
|
|
||||||
? getParentPath(walkResult, lastContext)
|
|
||||||
: ""
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "instance",
|
type: "instance",
|
||||||
instance: i.instance,
|
instance: i.instance,
|
||||||
// how the binding expression persists, and is used in the app at runtime
|
// how the binding expression persists, and is used in the app at runtime
|
||||||
runtimeBinding: `${contextParentPath}${i.instance._id}.${i.prop}`,
|
runtimeBinding: `${i.instance._id}`,
|
||||||
// how the binding exressions looks to the user of the builder
|
// how the binding exressions looks to the user of the builder
|
||||||
readableBinding: `${i.instance._instanceName}`,
|
readableBinding: `${i.instance._instanceName}`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const contextToBindables = (tables, walkResult) => context => {
|
const contextToBindables = tables => context => {
|
||||||
const contextParentPath = getParentPath(walkResult, context)
|
|
||||||
const tableId = context.table?.tableId ?? context.table
|
const tableId = context.table?.tableId ?? context.table
|
||||||
const table = tables.find(table => table._id === tableId)
|
const table = tables.find(table => table._id === tableId)
|
||||||
let schema =
|
let schema =
|
||||||
|
@ -98,7 +87,7 @@ const contextToBindables = (tables, walkResult) => context => {
|
||||||
fieldSchema,
|
fieldSchema,
|
||||||
instance: context.instance,
|
instance: context.instance,
|
||||||
// how the binding expression persists, and is used in the app at runtime
|
// how the binding expression persists, and is used in the app at runtime
|
||||||
runtimeBinding: `${contextParentPath}data.${runtimeBoundKey}`,
|
runtimeBinding: `${context.instance._id}.${runtimeBoundKey}`,
|
||||||
// how the binding expressions looks to the user of the builder
|
// how the binding expressions looks to the user of the builder
|
||||||
readableBinding: `${context.instance._instanceName}.${table.name}.${key}`,
|
readableBinding: `${context.instance._instanceName}.${table.name}.${key}`,
|
||||||
// table / view info
|
// table / view info
|
||||||
|
@ -118,20 +107,6 @@ const contextToBindables = (tables, walkResult) => context => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getParentPath = (walkResult, context) => {
|
|
||||||
// describes the number of "parent" in the path
|
|
||||||
// clone array first so original array is not mtated
|
|
||||||
const contextParentNumber = [...walkResult.target._contexts]
|
|
||||||
.reverse()
|
|
||||||
.indexOf(context)
|
|
||||||
|
|
||||||
return (
|
|
||||||
new Array(contextParentNumber).fill("parent").join(".") +
|
|
||||||
// trailing . if has parents
|
|
||||||
(contextParentNumber ? "." : "")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const walk = ({ instance, targetId, components, tables, result }) => {
|
const walk = ({ instance, targetId, components, tables, result }) => {
|
||||||
if (!result) {
|
if (!result) {
|
||||||
result = {
|
result = {
|
||||||
|
|
|
@ -12,10 +12,7 @@ export function readableToRuntimeBinding(bindableProperties, textWithBindings) {
|
||||||
return boundValue === `{{ ${readableBinding} }}`
|
return boundValue === `{{ ${readableBinding} }}`
|
||||||
})
|
})
|
||||||
if (binding) {
|
if (binding) {
|
||||||
result = textWithBindings.replace(
|
result = result.replace(boundValue, `{{ ${binding.runtimeBinding} }}`)
|
||||||
boundValue,
|
|
||||||
`{{ ${binding.runtimeBinding} }}`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
|
|
|
@ -481,7 +481,7 @@ export const getFrontendStore = () => {
|
||||||
// Try to extract a nav component from the master screen
|
// Try to extract a nav component from the master screen
|
||||||
const nav = findChildComponentType(
|
const nav = findChildComponentType(
|
||||||
state.pages.main,
|
state.pages.main,
|
||||||
"@budibase/standard-components/Navigation"
|
"@budibase/standard-components/navigation"
|
||||||
)
|
)
|
||||||
if (nav) {
|
if (nav) {
|
||||||
let newLink
|
let newLink
|
||||||
|
|
|
@ -15,8 +15,6 @@ export class Component extends BaseStructure {
|
||||||
selected: {},
|
selected: {},
|
||||||
},
|
},
|
||||||
_code: "",
|
_code: "",
|
||||||
className: "",
|
|
||||||
onLoad: [],
|
|
||||||
type: "",
|
type: "",
|
||||||
_instanceName: "",
|
_instanceName: "",
|
||||||
_children: [],
|
_children: [],
|
||||||
|
|
|
@ -1,140 +1,63 @@
|
||||||
<script>
|
<script>
|
||||||
import { store, backendUiStore } from "builderStore"
|
import { onMount } from "svelte"
|
||||||
import { map, join } from "lodash/fp"
|
import { store } from "builderStore"
|
||||||
import iframeTemplate from "./iframeTemplate"
|
import iframeTemplate from "./iframeTemplate"
|
||||||
import { pipe } from "../../../helpers"
|
import { Screen } from "builderStore/store/screenTemplates/utils/Screen"
|
||||||
import { Screen } from "../../../builderStore/store/screenTemplates/utils/Screen"
|
|
||||||
import { Component } from "../../../builderStore/store/screenTemplates/utils/Component"
|
|
||||||
|
|
||||||
let iframe
|
let iframe
|
||||||
let styles = ""
|
|
||||||
|
|
||||||
function transform_component(comp) {
|
// Create screen slot placeholder for use when a page is selected rather
|
||||||
const props = comp.props || comp
|
// than a screen
|
||||||
if (props && props._children && props._children.length) {
|
|
||||||
props._children = props._children.map(transform_component)
|
|
||||||
}
|
|
||||||
|
|
||||||
return props
|
|
||||||
}
|
|
||||||
|
|
||||||
const getComponentTypeName = component => {
|
|
||||||
let [componentName] = component._component.match(/[a-z]*$/)
|
|
||||||
return componentName || "element"
|
|
||||||
}
|
|
||||||
|
|
||||||
const headingStyle = {
|
|
||||||
width: "500px",
|
|
||||||
padding: "8px",
|
|
||||||
}
|
|
||||||
const textStyle = {
|
|
||||||
...headingStyle,
|
|
||||||
"max-width": "",
|
|
||||||
"text-align": "left",
|
|
||||||
}
|
|
||||||
|
|
||||||
const heading = new Component("@budibase/standard-components/heading")
|
|
||||||
.normalStyle(headingStyle)
|
|
||||||
.type("h1")
|
|
||||||
.text("Screen Slot")
|
|
||||||
.instanceName("Heading")
|
|
||||||
const textScreenDisplay = new Component("@budibase/standard-components/text")
|
|
||||||
.normalStyle(textStyle)
|
|
||||||
.instanceName("Text")
|
|
||||||
.type("none")
|
|
||||||
.text(
|
|
||||||
"The screens that you create will be displayed inside this box. This box is just a placeholder, to show you the position of screens."
|
|
||||||
)
|
|
||||||
const container = new Component("@budibase/standard-components/container")
|
|
||||||
.normalStyle({
|
|
||||||
display: "flex",
|
|
||||||
"flex-direction": "column",
|
|
||||||
"align-items": "center",
|
|
||||||
flex: "1 1 auto",
|
|
||||||
})
|
|
||||||
.type("div")
|
|
||||||
.instanceName("Container")
|
|
||||||
.addChild(heading)
|
|
||||||
.addChild(textScreenDisplay)
|
|
||||||
const screenPlaceholder = new Screen()
|
const screenPlaceholder = new Screen()
|
||||||
.name("Screen Placeholder")
|
.name("Screen Placeholder")
|
||||||
.route("*")
|
.route("*")
|
||||||
.component("@budibase/standard-components/container")
|
.component("@budibase/standard-components/screenslotplaceholder")
|
||||||
.mainType("div")
|
|
||||||
.instanceName("Content Placeholder")
|
.instanceName("Content Placeholder")
|
||||||
.normalStyle({
|
|
||||||
flex: "1 1 auto",
|
|
||||||
})
|
|
||||||
.addChild(container)
|
|
||||||
.json()
|
.json()
|
||||||
// TODO: this ID is attached to how the screen slot is rendered, confusing, would be better a type etc
|
|
||||||
screenPlaceholder.props._id = "screenslot-placeholder"
|
|
||||||
|
|
||||||
$: hasComponent = !!$store.currentPreviewItem
|
// Extract data to pass to the iframe
|
||||||
|
$: page = $store.pages[$store.currentPageName]
|
||||||
|
$: screen =
|
||||||
|
$store.currentFrontEndType === "page"
|
||||||
|
? screenPlaceholder
|
||||||
|
: $store.currentPreviewItem
|
||||||
|
$: selectedComponentId = $store.currentComponentInfo?._id ?? ""
|
||||||
|
|
||||||
$: {
|
// Saving pages and screens to the DB causes them to have _revs.
|
||||||
styles = ""
|
// These revisions change every time a save happens and causes
|
||||||
// Apply the CSS from the currently selected page and its screens
|
// these reactive statements to fire, even though the actual
|
||||||
const currentPage = $store.pages[$store.currentPageName]
|
// definition hasn't changed.
|
||||||
styles += currentPage._css
|
// By deleting all _rev properties we can avoid this and increase
|
||||||
for (let screen of currentPage._screens) {
|
// performance.
|
||||||
styles += screen._css
|
$: json = JSON.stringify({ page, screen, selectedComponentId })
|
||||||
|
$: strippedJson = json.replaceAll(/"_rev":\s*"[^"]+"/g, `"_rev":""`)
|
||||||
|
|
||||||
|
// Update the iframe with the builder info to render the correct preview
|
||||||
|
const refreshContent = message => {
|
||||||
|
if (iframe) {
|
||||||
|
iframe.contentWindow.postMessage(message)
|
||||||
}
|
}
|
||||||
styles = styles
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$: stylesheetLinks = pipe($store.pages.stylesheets, [
|
// Refresh the preview when required
|
||||||
map(s => `<link rel="stylesheet" href="${s}"/>`),
|
$: refreshContent(strippedJson)
|
||||||
join("\n"),
|
|
||||||
])
|
|
||||||
|
|
||||||
$: screensExist =
|
// Initialise the app when mounted
|
||||||
$store.currentPreviewItem._screens &&
|
onMount(() => {
|
||||||
$store.currentPreviewItem._screens.length > 0
|
iframe.contentWindow.addEventListener(
|
||||||
|
"bb-ready",
|
||||||
$: frontendDefinition = {
|
() => {
|
||||||
appId: $store.appId,
|
refreshContent(strippedJson)
|
||||||
libraries: $store.libraries,
|
},
|
||||||
page: $store.pages[$store.currentPageName],
|
{
|
||||||
screens: [
|
once: true,
|
||||||
$store.currentFrontEndType === "page"
|
}
|
||||||
? screenPlaceholder
|
|
||||||
: $store.currentPreviewItem,
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
$: selectedComponentType = getComponentTypeName($store.currentComponentInfo)
|
|
||||||
|
|
||||||
$: selectedComponentId = $store.currentComponentInfo
|
|
||||||
? $store.currentComponentInfo._id
|
|
||||||
: ""
|
|
||||||
|
|
||||||
const refreshContent = () => {
|
|
||||||
iframe.contentWindow.postMessage(
|
|
||||||
JSON.stringify({
|
|
||||||
styles,
|
|
||||||
stylesheetLinks,
|
|
||||||
selectedComponentType,
|
|
||||||
selectedComponentId,
|
|
||||||
frontendDefinition,
|
|
||||||
appId: $store.appId,
|
|
||||||
instanceId: $backendUiStore.selectedDatabase._id,
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
$: if (iframe)
|
|
||||||
iframe.contentWindow.addEventListener("bb-ready", refreshContent, {
|
|
||||||
once: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
$: if (iframe && frontendDefinition) {
|
|
||||||
refreshContent()
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="component-container">
|
<div class="component-container">
|
||||||
{#if hasComponent && $store.currentPreviewItem}
|
{#if $store.currentPreviewItem}
|
||||||
<iframe
|
<iframe
|
||||||
style="height: 100%; width: 100%"
|
style="height: 100%; width: 100%"
|
||||||
title="componentPreview"
|
title="componentPreview"
|
||||||
|
@ -152,7 +75,6 @@
|
||||||
margin: auto;
|
margin: auto;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.component-container iframe {
|
.component-container iframe {
|
||||||
border: 0;
|
border: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
|
@ -4,72 +4,50 @@ export default `<html>
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Mono">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Mono">
|
||||||
<style>
|
<style>
|
||||||
body, html {
|
body, html {
|
||||||
height: 100%!important;
|
height: 100% !important;
|
||||||
font-family: Inter !important;
|
font-family: Inter !important;
|
||||||
margin: 0px!important;
|
margin: 0px !important;
|
||||||
}
|
}
|
||||||
*, *:before, *:after {
|
*, *:before, *:after {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.container-screenslot-placeholder {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 20px;
|
|
||||||
text-align: center;
|
|
||||||
border-style: dashed !important;
|
|
||||||
border-width: 1px;
|
|
||||||
color: #000000;
|
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
.container-screenslot-placeholder span {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
<script src='/assets/budibase-client.js'></script>
|
<script src='/assets/budibase-client.js'></script>
|
||||||
<script>
|
<script>
|
||||||
function receiveMessage(event) {
|
function receiveMessage(event) {
|
||||||
|
if (!event.data) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!event.data) return
|
// Extract data from message
|
||||||
|
const { selectedComponentId, page, screen } = JSON.parse(event.data)
|
||||||
|
|
||||||
const data = JSON.parse(event.data)
|
// Set some flags so the app knows we're in the builder
|
||||||
|
window["##BUDIBASE_IN_BUILDER##"] = true
|
||||||
|
window["##BUDIBASE_PREVIEW_PAGE##"] = page
|
||||||
|
window["##BUDIBASE_PREVIEW_SCREEN##"] = screen
|
||||||
|
window["##BUDIBASE_SELECTED_COMPONENT_ID##"] = selectedComponentId
|
||||||
|
window["##BUDIBASE_PREVIEW_ID##"] = Math.random()
|
||||||
|
|
||||||
try {
|
// Initialise app
|
||||||
if (styles) document.head.removeChild(styles)
|
|
||||||
} catch(_) { }
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (selectedComponentStyle) document.head.removeChild(selectedComponentStyle)
|
|
||||||
} catch(_) { }
|
|
||||||
|
|
||||||
selectedComponentStyle = document.createElement('style');
|
|
||||||
document.head.appendChild(selectedComponentStyle)
|
|
||||||
var selectedCss = '.' + data.selectedComponentType + '-' + data.selectedComponentId + '{ border: 2px solid #0055ff; }'
|
|
||||||
selectedComponentStyle.appendChild(document.createTextNode(selectedCss))
|
|
||||||
|
|
||||||
styles = document.createElement('style')
|
|
||||||
document.head.appendChild(styles)
|
|
||||||
styles.appendChild(document.createTextNode(data.styles))
|
|
||||||
|
|
||||||
window["##BUDIBASE_FRONTEND_DEFINITION##"] = data.frontendDefinition;
|
|
||||||
if (window.loadBudibase) {
|
if (window.loadBudibase) {
|
||||||
loadBudibase({ window, localStorage })
|
loadBudibase()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let styles
|
|
||||||
let selectedComponentStyle
|
let selectedComponentStyle
|
||||||
|
|
||||||
document.addEventListener("click", function(e) {
|
// Ignore clicks
|
||||||
e.preventDefault()
|
["click", "mousedown"].forEach(type => {
|
||||||
e.stopPropagation()
|
document.addEventListener(type, function(e) {
|
||||||
return false;
|
e.preventDefault()
|
||||||
}, true)
|
e.stopPropagation()
|
||||||
|
return false
|
||||||
window.addEventListener('message', receiveMessage)
|
}, true)
|
||||||
window.dispatchEvent(new Event('bb-ready'))
|
})
|
||||||
|
|
||||||
|
window.addEventListener("message", receiveMessage)
|
||||||
|
window.dispatchEvent(new Event("bb-ready"))
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -15,7 +15,8 @@
|
||||||
let anchor
|
let anchor
|
||||||
|
|
||||||
$: noChildrenAllowed =
|
$: noChildrenAllowed =
|
||||||
!component || !getComponentDefinition($store, component._component).children
|
!component ||
|
||||||
|
!getComponentDefinition($store, component._component)?.children
|
||||||
$: noPaste = !$store.componentToPaste
|
$: noPaste = !$store.componentToPaste
|
||||||
|
|
||||||
const lastPartOfName = c => (c ? last(c._component.split("/")) : "")
|
const lastPartOfName = c => (c ? last(c._component.split("/")) : "")
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import { TextButton, Body, DropdownMenu, ModalContent } from "@budibase/bbui"
|
import { TextButton, Body, DropdownMenu, ModalContent } from "@budibase/bbui"
|
||||||
import { AddIcon, ArrowDownIcon } from "components/common/Icons/"
|
import { AddIcon, ArrowDownIcon } from "components/common/Icons/"
|
||||||
import { EVENT_TYPE_MEMBER_NAME } from "../../../../../client/src/state/eventHandlers"
|
|
||||||
import actionTypes from "./actions"
|
import actionTypes from "./actions"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
const eventTypeKey = "##eventHandlerType"
|
||||||
|
|
||||||
export let event
|
export let event
|
||||||
|
|
||||||
|
@ -18,8 +18,7 @@
|
||||||
$: actions = event || []
|
$: actions = event || []
|
||||||
$: selectedActionComponent =
|
$: selectedActionComponent =
|
||||||
selectedAction &&
|
selectedAction &&
|
||||||
actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_MEMBER_NAME])
|
actionTypes.find(t => t.name === selectedAction[eventTypeKey]).component
|
||||||
.component
|
|
||||||
|
|
||||||
const updateEventHandler = (updatedHandler, index) => {
|
const updateEventHandler = (updatedHandler, index) => {
|
||||||
actions[index] = updatedHandler
|
actions[index] = updatedHandler
|
||||||
|
@ -33,7 +32,7 @@
|
||||||
const addAction = actionType => () => {
|
const addAction = actionType => () => {
|
||||||
const newAction = {
|
const newAction = {
|
||||||
parameters: {},
|
parameters: {},
|
||||||
[EVENT_TYPE_MEMBER_NAME]: actionType.name,
|
[eventTypeKey]: actionType.name,
|
||||||
}
|
}
|
||||||
actions.push(newAction)
|
actions.push(newAction)
|
||||||
selectedAction = newAction
|
selectedAction = newAction
|
||||||
|
@ -79,7 +78,7 @@
|
||||||
{#each actions as action, index}
|
{#each actions as action, index}
|
||||||
<div class="action-container">
|
<div class="action-container">
|
||||||
<div class="action-header" on:click={selectAction(action)}>
|
<div class="action-header" on:click={selectAction(action)}>
|
||||||
<Body small lh>{index + 1}. {action[EVENT_TYPE_MEMBER_NAME]}</Body>
|
<Body small lh>{index + 1}. {action[eventTypeKey]}</Body>
|
||||||
<div class="row-expander" class:rotate={action !== selectedAction}>
|
<div class="row-expander" class:rotate={action !== selectedAction}>
|
||||||
<ArrowDownIcon />
|
<ArrowDownIcon />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,127 +0,0 @@
|
||||||
<script>
|
|
||||||
import { keys, map, includes, filter } from "lodash/fp"
|
|
||||||
import EventEditorModal from "./EventEditorModal.svelte"
|
|
||||||
import { Modal } from "@budibase/bbui"
|
|
||||||
|
|
||||||
export const EVENT_TYPE = "event"
|
|
||||||
export let component
|
|
||||||
|
|
||||||
let events = []
|
|
||||||
let selectedEvent = null
|
|
||||||
let modal
|
|
||||||
|
|
||||||
$: {
|
|
||||||
events = Object.keys(component)
|
|
||||||
// TODO: use real events
|
|
||||||
.filter(propName => ["onChange", "onClick", "onLoad"].includes(propName))
|
|
||||||
.map(propName => ({
|
|
||||||
name: propName,
|
|
||||||
handlers: component[propName] || [],
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const openModal = event => {
|
|
||||||
selectedEvent = event
|
|
||||||
modal.show()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<button class="newevent" on:click={() => openModal()}>
|
|
||||||
<i class="icon ri-add-circle-fill" />
|
|
||||||
Create New Event
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="root">
|
|
||||||
<form on:submit|preventDefault class="form-root">
|
|
||||||
{#each events as event, index}
|
|
||||||
{#if event.handlers.length > 0}
|
|
||||||
<div
|
|
||||||
class:selected={selectedEvent && selectedEvent.index === index}
|
|
||||||
class="handler-container budibase__nav-item"
|
|
||||||
on:click={() => openModal({ ...event, index })}>
|
|
||||||
<span class="event-name">{event.name}</span>
|
|
||||||
<span class="edit-text">EDIT</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Modal bind:this={modal} width="600px">
|
|
||||||
<EventEditorModal eventOptions={events} event={selectedEvent} />
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.root {
|
|
||||||
font-size: 10pt;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.newevent {
|
|
||||||
cursor: pointer;
|
|
||||||
border: 1px solid var(--grey-4);
|
|
||||||
border-radius: 3px;
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px 16px;
|
|
||||||
margin: 0px 0px 12px 0px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background: var(--background);
|
|
||||||
color: var(--ink);
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all 2ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.newevent:hover {
|
|
||||||
background: var(--grey-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
color: var(--ink);
|
|
||||||
font-size: 16px;
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-root {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.handler-container {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
border: 2px solid var(--grey-1);
|
|
||||||
height: 80px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-name {
|
|
||||||
margin-top: 5px;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 16px;
|
|
||||||
color: rgba(22, 48, 87, 0.6);
|
|
||||||
align-self: end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-text {
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
font-weight: bold;
|
|
||||||
align-self: end;
|
|
||||||
justify-self: end;
|
|
||||||
font-size: 10px;
|
|
||||||
color: rgba(35, 65, 105, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected {
|
|
||||||
color: var(--blue);
|
|
||||||
background: var(--grey-1) !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,53 +0,0 @@
|
||||||
<script>
|
|
||||||
import { Input, DataList, Select } from "@budibase/bbui"
|
|
||||||
import { automationStore, allScreens } from "builderStore"
|
|
||||||
|
|
||||||
export let parameter
|
|
||||||
|
|
||||||
let isOpen = false
|
|
||||||
|
|
||||||
const capitalize = s => {
|
|
||||||
if (typeof s !== "string") return ""
|
|
||||||
return s.charAt(0).toUpperCase() + s.slice(1)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="handler-option">
|
|
||||||
{#if parameter.name === 'automation'}<span>{parameter.name}</span>{/if}
|
|
||||||
{#if parameter.name === 'automation'}
|
|
||||||
<Select on:change bind:value={parameter.value}>
|
|
||||||
<option value="" />
|
|
||||||
{#each $automationStore.automations.filter(wf => wf.live) as automation}
|
|
||||||
<option value={automation._id}>{automation.name}</option>
|
|
||||||
{/each}
|
|
||||||
</Select>
|
|
||||||
{:else if parameter.name === 'url'}
|
|
||||||
<DataList on:change bind:value={parameter.value}>
|
|
||||||
<option value="" />
|
|
||||||
{#each $allScreens as screen}
|
|
||||||
<option value={screen.routing.route}>
|
|
||||||
{screen.props._instanceName}
|
|
||||||
</option>
|
|
||||||
{/each}
|
|
||||||
</DataList>
|
|
||||||
{:else}
|
|
||||||
<Input
|
|
||||||
name={parameter.name}
|
|
||||||
label={capitalize(parameter.name)}
|
|
||||||
on:change
|
|
||||||
value={parameter.value} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.handler-option {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-size: 18px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,9 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import { buildStyle } from "../../helpers.js"
|
|
||||||
export let value = ""
|
export let value = ""
|
||||||
export let text = ""
|
export let text = ""
|
||||||
export let icon = ""
|
export let icon = ""
|
||||||
export let onClick = value => {}
|
export let onClick = () => {}
|
||||||
export let selected = false
|
export let selected = false
|
||||||
|
|
||||||
$: useIcon = !!icon
|
$: useIcon = !!icon
|
||||||
|
|
|
@ -40,13 +40,16 @@
|
||||||
|
|
||||||
$: links = bindableProperties
|
$: links = bindableProperties
|
||||||
.filter(x => x.fieldSchema?.type === "link")
|
.filter(x => x.fieldSchema?.type === "link")
|
||||||
.map(property => ({
|
.map(property => {
|
||||||
label: property.readableBinding,
|
return {
|
||||||
fieldName: property.fieldSchema.name,
|
providerId: property.instance._id,
|
||||||
name: `all_${property.fieldSchema.tableId}`,
|
label: property.readableBinding,
|
||||||
tableId: property.fieldSchema.tableId,
|
fieldName: property.fieldSchema.name,
|
||||||
type: "link",
|
name: `all_${property.fieldSchema.tableId}`,
|
||||||
}))
|
tableId: property.fieldSchema.tableId,
|
||||||
|
type: "link",
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -1175,7 +1175,7 @@ export default {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Nav Bar",
|
name: "Nav Bar",
|
||||||
_component: "@budibase/standard-components/Navigation",
|
_component: "@budibase/standard-components/navigation",
|
||||||
description:
|
description:
|
||||||
"A component for handling the navigation within your app.",
|
"A component for handling the navigation within your app.",
|
||||||
icon: "ri-navigation-line",
|
icon: "ri-navigation-line",
|
||||||
|
|
|
@ -11,7 +11,7 @@ describe("fetch bindable properties", () => {
|
||||||
)
|
)
|
||||||
expect(componentBinding).toBeDefined()
|
expect(componentBinding).toBeDefined()
|
||||||
expect(componentBinding.type).toBe("instance")
|
expect(componentBinding.type).toBe("instance")
|
||||||
expect(componentBinding.runtimeBinding).toBe("search-input-id.value")
|
expect(componentBinding.runtimeBinding).toBe("search-input-id")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should not return bindable components when not in their context", () => {
|
it("should not return bindable components when not in their context", () => {
|
||||||
|
@ -37,20 +37,22 @@ describe("fetch bindable properties", () => {
|
||||||
expect(contextBindings.length).toBe(4)
|
expect(contextBindings.length).toBe(4)
|
||||||
|
|
||||||
const namebinding = contextBindings.find(
|
const namebinding = contextBindings.find(
|
||||||
b => b.runtimeBinding === "data.name"
|
b => b.runtimeBinding === "list-id.name"
|
||||||
)
|
)
|
||||||
expect(namebinding).toBeDefined()
|
expect(namebinding).toBeDefined()
|
||||||
expect(namebinding.readableBinding).toBe("list-name.Test Table.name")
|
expect(namebinding.readableBinding).toBe("list-name.Test Table.name")
|
||||||
|
|
||||||
const descriptionbinding = contextBindings.find(
|
const descriptionbinding = contextBindings.find(
|
||||||
b => b.runtimeBinding === "data.description"
|
b => b.runtimeBinding === "list-id.description"
|
||||||
)
|
)
|
||||||
expect(descriptionbinding).toBeDefined()
|
expect(descriptionbinding).toBeDefined()
|
||||||
expect(descriptionbinding.readableBinding).toBe(
|
expect(descriptionbinding.readableBinding).toBe(
|
||||||
"list-name.Test Table.description"
|
"list-name.Test Table.description"
|
||||||
)
|
)
|
||||||
|
|
||||||
const idbinding = contextBindings.find(b => b.runtimeBinding === "data._id")
|
const idbinding = contextBindings.find(
|
||||||
|
b => b.runtimeBinding === "list-id._id"
|
||||||
|
)
|
||||||
expect(idbinding).toBeDefined()
|
expect(idbinding).toBeDefined()
|
||||||
expect(idbinding.readableBinding).toBe("list-name.Test Table._id")
|
expect(idbinding.readableBinding).toBe("list-name.Test Table._id")
|
||||||
})
|
})
|
||||||
|
@ -65,13 +67,13 @@ describe("fetch bindable properties", () => {
|
||||||
expect(contextBindings.length).toBe(8)
|
expect(contextBindings.length).toBe(8)
|
||||||
|
|
||||||
const namebinding_parent = contextBindings.find(
|
const namebinding_parent = contextBindings.find(
|
||||||
b => b.runtimeBinding === "parent.data.name"
|
b => b.runtimeBinding === "list-id.name"
|
||||||
)
|
)
|
||||||
expect(namebinding_parent).toBeDefined()
|
expect(namebinding_parent).toBeDefined()
|
||||||
expect(namebinding_parent.readableBinding).toBe("list-name.Test Table.name")
|
expect(namebinding_parent.readableBinding).toBe("list-name.Test Table.name")
|
||||||
|
|
||||||
const descriptionbinding_parent = contextBindings.find(
|
const descriptionbinding_parent = contextBindings.find(
|
||||||
b => b.runtimeBinding === "parent.data.description"
|
b => b.runtimeBinding === "list-id.description"
|
||||||
)
|
)
|
||||||
expect(descriptionbinding_parent).toBeDefined()
|
expect(descriptionbinding_parent).toBeDefined()
|
||||||
expect(descriptionbinding_parent.readableBinding).toBe(
|
expect(descriptionbinding_parent.readableBinding).toBe(
|
||||||
|
@ -79,7 +81,7 @@ describe("fetch bindable properties", () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
const namebinding_own = contextBindings.find(
|
const namebinding_own = contextBindings.find(
|
||||||
b => b.runtimeBinding === "data.name"
|
b => b.runtimeBinding === "child-list-id.name"
|
||||||
)
|
)
|
||||||
expect(namebinding_own).toBeDefined()
|
expect(namebinding_own).toBeDefined()
|
||||||
expect(namebinding_own.readableBinding).toBe(
|
expect(namebinding_own.readableBinding).toBe(
|
||||||
|
@ -87,7 +89,7 @@ describe("fetch bindable properties", () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
const descriptionbinding_own = contextBindings.find(
|
const descriptionbinding_own = contextBindings.find(
|
||||||
b => b.runtimeBinding === "data.description"
|
b => b.runtimeBinding === "child-list-id.description"
|
||||||
)
|
)
|
||||||
expect(descriptionbinding_own).toBeDefined()
|
expect(descriptionbinding_own).toBeDefined()
|
||||||
expect(descriptionbinding_own.readableBinding).toBe(
|
expect(descriptionbinding_own.readableBinding).toBe(
|
||||||
|
@ -104,7 +106,7 @@ describe("fetch bindable properties", () => {
|
||||||
r => r.instance._id === "list-item-input-id" && r.type === "instance"
|
r => r.instance._id === "list-item-input-id" && r.type === "instance"
|
||||||
)
|
)
|
||||||
expect(componentBinding).toBeDefined()
|
expect(componentBinding).toBeDefined()
|
||||||
expect(componentBinding.runtimeBinding).toBe("list-item-input-id.value")
|
expect(componentBinding.runtimeBinding).toBe("list-item-input-id")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should not return components from child context", () => {
|
it("should not return components from child context", () => {
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,5 @@
|
||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
package-lock.json
|
package-lock.json
|
||||||
yarn.lock
|
|
||||||
release/
|
release/
|
||||||
dist/
|
dist/
|
|
@ -1,13 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
presets: ["@babel/preset-env"],
|
|
||||||
sourceMaps: "inline",
|
|
||||||
retainLines: true,
|
|
||||||
plugins: [
|
|
||||||
[
|
|
||||||
"@babel/plugin-transform-runtime",
|
|
||||||
{
|
|
||||||
regenerator: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
}
|
|
|
@ -6,56 +6,27 @@
|
||||||
"module": "dist/budibase-client.esm.mjs",
|
"module": "dist/budibase-client.esm.mjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rollup -c",
|
"build": "rollup -c",
|
||||||
"test": "jest",
|
|
||||||
"dev:builder": "rollup -cw"
|
"dev:builder": "rollup -cw"
|
||||||
},
|
},
|
||||||
"jest": {
|
|
||||||
"globals": {
|
|
||||||
"GLOBALS": {
|
|
||||||
"client": "web"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"testURL": "http://test.com",
|
|
||||||
"moduleNameMapper": {
|
|
||||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/internals/mocks/fileMock.js",
|
|
||||||
"\\.(css|less|sass|scss)$": "identity-obj-proxy"
|
|
||||||
},
|
|
||||||
"moduleFileExtensions": [
|
|
||||||
"js",
|
|
||||||
"svelte"
|
|
||||||
],
|
|
||||||
"moduleDirectories": [
|
|
||||||
"node_modules"
|
|
||||||
],
|
|
||||||
"transform": {
|
|
||||||
"^.+js$": "babel-jest",
|
|
||||||
"^.+.svelte$": "svelte-jester"
|
|
||||||
},
|
|
||||||
"transformIgnorePatterns": [
|
|
||||||
"/node_modules/(?!svelte).+\\.js$"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"deep-equal": "^2.0.1",
|
"deep-equal": "^2.0.1",
|
||||||
"mustache": "^4.0.1",
|
"mustache": "^4.0.1",
|
||||||
"regexparam": "^1.3.0"
|
"regexparam": "^1.3.0",
|
||||||
|
"svelte-spa-router": "^3.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.5.5",
|
"@budibase/standard-components": "^0.3.8",
|
||||||
"@babel/plugin-transform-runtime": "^7.5.5",
|
"@rollup/plugin-commonjs": "^16.0.0",
|
||||||
"@babel/preset-env": "^7.5.5",
|
"@rollup/plugin-node-resolve": "^10.0.0",
|
||||||
"@babel/runtime": "^7.5.5",
|
|
||||||
"babel-jest": "^24.8.0",
|
|
||||||
"fs-extra": "^8.1.0",
|
"fs-extra": "^8.1.0",
|
||||||
"jest": "^24.8.0",
|
|
||||||
"jsdom": "^16.0.1",
|
"jsdom": "^16.0.1",
|
||||||
"rollup": "^1.12.0",
|
"rollup": "^2.33.2",
|
||||||
"rollup-plugin-commonjs": "^10.0.0",
|
|
||||||
"rollup-plugin-node-builtins": "^2.1.2",
|
"rollup-plugin-node-builtins": "^2.1.2",
|
||||||
"rollup-plugin-node-globals": "^1.4.0",
|
"rollup-plugin-node-globals": "^1.4.0",
|
||||||
|
"rollup-plugin-svelte": "^6.1.1",
|
||||||
"rollup-plugin-node-resolve": "^5.2.0",
|
"rollup-plugin-node-resolve": "^5.2.0",
|
||||||
"rollup-plugin-terser": "^4.0.4",
|
"rollup-plugin-terser": "^4.0.4",
|
||||||
"svelte": "^3.29.7",
|
"svelte": "^3.30.0",
|
||||||
"svelte-jester": "^1.0.6"
|
"svelte-jester": "^1.0.6"
|
||||||
},
|
},
|
||||||
"gitHead": "e4e053cb6ff9a0ddc7115b44ccaa24b8ec41fb9a"
|
"gitHead": "e4e053cb6ff9a0ddc7115b44ccaa24b8ec41fb9a"
|
||||||
|
|
|
@ -1,31 +1,30 @@
|
||||||
import resolve from "rollup-plugin-node-resolve"
|
import commonjs from "@rollup/plugin-commonjs"
|
||||||
import commonjs from "rollup-plugin-commonjs"
|
import resolve from "@rollup/plugin-node-resolve"
|
||||||
import builtins from "rollup-plugin-node-builtins"
|
import builtins from "rollup-plugin-node-builtins"
|
||||||
import nodeglobals from "rollup-plugin-node-globals"
|
import svelte from "rollup-plugin-svelte"
|
||||||
|
|
||||||
|
const production = !process.env.ROLLUP_WATCH
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
input: "src/index.js",
|
input: "src/index.js",
|
||||||
output: [
|
output: [
|
||||||
{
|
{
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
format: "iife",
|
|
||||||
name: "app",
|
|
||||||
file: `./dist/budibase-client.js`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
file: "dist/budibase-client.esm.mjs",
|
|
||||||
format: "esm",
|
format: "esm",
|
||||||
sourcemap: "inline",
|
file: `./dist/budibase-client.js`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
|
svelte({
|
||||||
|
dev: !production,
|
||||||
|
}),
|
||||||
resolve({
|
resolve({
|
||||||
preferBuiltins: true,
|
preferBuiltins: true,
|
||||||
browser: true,
|
browser: true,
|
||||||
|
dedupe: ["svelte", "svelte/internal"],
|
||||||
}),
|
}),
|
||||||
commonjs(),
|
commonjs(),
|
||||||
builtins(),
|
builtins(),
|
||||||
nodeglobals(),
|
|
||||||
],
|
],
|
||||||
watch: {
|
watch: {
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
import { getAppId } from "../utils/getAppId"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API cache for cached request responses.
|
||||||
|
*/
|
||||||
|
let cache = {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a fully formatted URL based on the SDK configuration.
|
||||||
|
*/
|
||||||
|
const makeFullURL = path => {
|
||||||
|
return `/${path}`.replace("//", "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for API errors.
|
||||||
|
*/
|
||||||
|
const handleError = error => {
|
||||||
|
return { error }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs an API call to the server.
|
||||||
|
* App ID header is always correctly set.
|
||||||
|
*/
|
||||||
|
const makeApiCall = async ({ method, url, body, json = true }) => {
|
||||||
|
try {
|
||||||
|
const requestBody = json ? JSON.stringify(body) : body
|
||||||
|
let headers = {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"x-budibase-app-id": getAppId(),
|
||||||
|
}
|
||||||
|
if (!window["##BUDIBASE_IN_BUILDER##"]) {
|
||||||
|
headers["x-budibase-type"] = "client"
|
||||||
|
}
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: requestBody,
|
||||||
|
credentials: "same-origin",
|
||||||
|
})
|
||||||
|
switch (response.status) {
|
||||||
|
case 200:
|
||||||
|
return response.json()
|
||||||
|
case 404:
|
||||||
|
return handleError(`${url}: Not Found`)
|
||||||
|
case 400:
|
||||||
|
return handleError(`${url}: Bad Request`)
|
||||||
|
case 403:
|
||||||
|
return handleError(`${url}: Forbidden`)
|
||||||
|
default:
|
||||||
|
if (response.status >= 200 && response.status < 400) {
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
return handleError(`${url} - ${response.statusText}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return handleError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs an API call to the server and caches the response.
|
||||||
|
* Future invocation for this URL will return the cached result instead of
|
||||||
|
* hitting the server again.
|
||||||
|
*/
|
||||||
|
const makeCachedApiCall = async params => {
|
||||||
|
const identifier = params.url
|
||||||
|
if (!identifier) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (!cache[identifier]) {
|
||||||
|
cache[identifier] = makeApiCall(params)
|
||||||
|
cache[identifier] = await cache[identifier]
|
||||||
|
}
|
||||||
|
return await cache[identifier]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an API call function for a particular HTTP method.
|
||||||
|
*/
|
||||||
|
const requestApiCall = method => async params => {
|
||||||
|
const { url, cache = false } = params
|
||||||
|
const fullURL = makeFullURL(url)
|
||||||
|
const enrichedParams = { ...params, method, url: fullURL }
|
||||||
|
return await (cache ? makeCachedApiCall : makeApiCall)(enrichedParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
post: requestApiCall("POST"),
|
||||||
|
get: requestApiCall("GET"),
|
||||||
|
patch: requestApiCall("PATCH"),
|
||||||
|
del: requestApiCall("DELETE"),
|
||||||
|
error: handleError,
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import API from "./api"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches screen definition for an app.
|
||||||
|
*/
|
||||||
|
export const fetchAppDefinition = async appId => {
|
||||||
|
return await API.get({
|
||||||
|
url: `/api/applications/${appId}/definition`,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
import API from "./api"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads an attachment to the server.
|
||||||
|
*/
|
||||||
|
export const uploadAttachment = async data => {
|
||||||
|
return await API.post({
|
||||||
|
url: "/api/attachments/upload",
|
||||||
|
body: data,
|
||||||
|
json: false,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
import API from "./api"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a log in request.
|
||||||
|
*/
|
||||||
|
export const logIn = async ({ username, password }) => {
|
||||||
|
if (!username) {
|
||||||
|
return API.error("Please enter your username")
|
||||||
|
}
|
||||||
|
if (!password) {
|
||||||
|
return API.error("Please enter your password")
|
||||||
|
}
|
||||||
|
return await API.post({
|
||||||
|
url: "/api/authenticate",
|
||||||
|
body: { username, password },
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,28 +0,0 @@
|
||||||
import appStore from "../state/store"
|
|
||||||
|
|
||||||
export const USER_STATE_PATH = "_bbuser"
|
|
||||||
|
|
||||||
export const authenticate = api => async ({ username, password }) => {
|
|
||||||
if (!username) {
|
|
||||||
api.error("Authenticate: username not set")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!password) {
|
|
||||||
api.error("Authenticate: password not set")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await api.post({
|
|
||||||
url: "/api/authenticate",
|
|
||||||
body: { username, password },
|
|
||||||
})
|
|
||||||
|
|
||||||
// set user even if error - so it is defined at least
|
|
||||||
appStore.update(s => {
|
|
||||||
s[USER_STATE_PATH] = user
|
|
||||||
return s
|
|
||||||
})
|
|
||||||
|
|
||||||
localStorage.setItem("budibase:user", JSON.stringify(user))
|
|
||||||
}
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { fetchTableData } from "./tables"
|
||||||
|
import { fetchViewData } from "./views"
|
||||||
|
import { fetchRelationshipData } from "./relationships"
|
||||||
|
import { enrichRows } from "./rows"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all rows for a particular Budibase data source.
|
||||||
|
*/
|
||||||
|
export const fetchDatasource = async (datasource, dataContext) => {
|
||||||
|
if (!datasource || !datasource.type) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch all rows in data source
|
||||||
|
const { type, tableId, fieldName } = datasource
|
||||||
|
let rows = []
|
||||||
|
if (type === "table") {
|
||||||
|
rows = await fetchTableData(tableId)
|
||||||
|
} else if (type === "view") {
|
||||||
|
rows = await fetchViewData(datasource)
|
||||||
|
} else if (type === "link") {
|
||||||
|
const row = dataContext[datasource.providerId]
|
||||||
|
rows = await fetchRelationshipData({
|
||||||
|
rowId: row?._id,
|
||||||
|
tableId: row?.tableId,
|
||||||
|
fieldName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enrich rows
|
||||||
|
return await enrichRows(rows, tableId)
|
||||||
|
}
|
|
@ -1,120 +1,9 @@
|
||||||
import { authenticate } from "./authenticate"
|
export * from "./rows"
|
||||||
import { getAppId } from "../render/getAppId"
|
export * from "./auth"
|
||||||
|
export * from "./datasources"
|
||||||
export async function baseApiCall(method, url, body) {
|
export * from "./tables"
|
||||||
return await fetch(url, {
|
export * from "./attachments"
|
||||||
method: method,
|
export * from "./views"
|
||||||
headers: {
|
export * from "./relationships"
|
||||||
"Content-Type": "application/json",
|
export * from "./routes"
|
||||||
"x-budibase-app-id": getAppId(window.document.cookie),
|
export * from "./app"
|
||||||
"x-budibase-type": "client",
|
|
||||||
},
|
|
||||||
body: body && JSON.stringify(body),
|
|
||||||
credentials: "same-origin",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiCall = method => async ({ url, body }) => {
|
|
||||||
const response = await baseApiCall(method, url, body)
|
|
||||||
|
|
||||||
switch (response.status) {
|
|
||||||
case 200:
|
|
||||||
return response.json()
|
|
||||||
case 404:
|
|
||||||
return error(`${url} Not found`)
|
|
||||||
case 400:
|
|
||||||
return error(`${url} Bad Request`)
|
|
||||||
case 403:
|
|
||||||
return error(`${url} Forbidden`)
|
|
||||||
default:
|
|
||||||
if (response.status >= 200 && response.status < 400) {
|
|
||||||
return response.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
return error(`${url} - ${response.statusText}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const post = apiCall("POST")
|
|
||||||
const get = apiCall("GET")
|
|
||||||
const patch = apiCall("PATCH")
|
|
||||||
const del = apiCall("DELETE")
|
|
||||||
|
|
||||||
const ERROR_MEMBER = "##error"
|
|
||||||
const error = message => {
|
|
||||||
// appStore.update(s => s["##error_message"], message)
|
|
||||||
return { [ERROR_MEMBER]: message }
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSuccess = obj => !obj || !obj[ERROR_MEMBER]
|
|
||||||
|
|
||||||
const apiOpts = {
|
|
||||||
isSuccess,
|
|
||||||
error,
|
|
||||||
post,
|
|
||||||
get,
|
|
||||||
patch,
|
|
||||||
delete: del,
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveRow = async (params, state) =>
|
|
||||||
await post({
|
|
||||||
url: `/api/${params.tableId}/rows`,
|
|
||||||
body: makeRowRequestBody(params, state),
|
|
||||||
})
|
|
||||||
|
|
||||||
const updateRow = async (params, state) => {
|
|
||||||
const row = makeRowRequestBody(params, state)
|
|
||||||
row._id = params._id
|
|
||||||
await patch({
|
|
||||||
url: `/api/${params.tableId}/rows/${params._id}`,
|
|
||||||
body: row,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteRow = async params =>
|
|
||||||
await del({
|
|
||||||
url: `/api/${params.tableId}/rows/${params.rowId}/${params.revId}`,
|
|
||||||
})
|
|
||||||
|
|
||||||
const makeRowRequestBody = (parameters, state) => {
|
|
||||||
// start with the row thats currently in context
|
|
||||||
const body = { ...(state.data || {}) }
|
|
||||||
|
|
||||||
// dont send the table
|
|
||||||
if (body._table) delete body._table
|
|
||||||
|
|
||||||
// then override with supplied parameters
|
|
||||||
if (parameters.fields) {
|
|
||||||
for (let fieldName of Object.keys(parameters.fields)) {
|
|
||||||
const field = parameters.fields[fieldName]
|
|
||||||
|
|
||||||
// ensure fields sent are of the correct type
|
|
||||||
if (field.type === "boolean") {
|
|
||||||
if (field.value === "true") body[fieldName] = true
|
|
||||||
if (field.value === "false") body[fieldName] = false
|
|
||||||
} else if (field.type === "number") {
|
|
||||||
const val = parseFloat(field.value)
|
|
||||||
if (!isNaN(val)) {
|
|
||||||
body[fieldName] = val
|
|
||||||
}
|
|
||||||
} else if (field.type === "datetime") {
|
|
||||||
const date = new Date(field.value)
|
|
||||||
if (!isNaN(date.getTime())) {
|
|
||||||
body[fieldName] = date.toISOString()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
body[fieldName] = field.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return body
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
authenticate: authenticate(apiOpts),
|
|
||||||
saveRow,
|
|
||||||
updateRow,
|
|
||||||
deleteRow,
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import API from "./api"
|
||||||
|
import { enrichRows } from "./rows"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches related rows for a certain field of a certain row.
|
||||||
|
*/
|
||||||
|
export const fetchRelationshipData = async ({ tableId, rowId, fieldName }) => {
|
||||||
|
if (!tableId || !rowId || !fieldName) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const response = await API.get({ url: `/api/${tableId}/${rowId}/enrich` })
|
||||||
|
const rows = response[fieldName] || []
|
||||||
|
return await enrichRows(rows, tableId)
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import API from "./api"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches available routes for the client app.
|
||||||
|
*/
|
||||||
|
export const fetchRoutes = async () => {
|
||||||
|
return await API.get({
|
||||||
|
url: `/api/routing/client`,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
import API from "./api"
|
||||||
|
import { fetchTableDefinition } from "./tables"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches data about a certain row in a table.
|
||||||
|
*/
|
||||||
|
export const fetchRow = async ({ tableId, rowId }) => {
|
||||||
|
const row = await API.get({
|
||||||
|
url: `/api/${tableId}/rows/${rowId}`,
|
||||||
|
})
|
||||||
|
return (await enrichRows([row], tableId))[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a row in a table.
|
||||||
|
*/
|
||||||
|
export const saveRow = async row => {
|
||||||
|
return await API.post({
|
||||||
|
url: `/api/${row.tableId}/rows`,
|
||||||
|
body: row,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a row in a table.
|
||||||
|
*/
|
||||||
|
export const updateRow = async row => {
|
||||||
|
return await API.patch({
|
||||||
|
url: `/api/${row.tableId}/rows/${row._id}`,
|
||||||
|
body: row,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a row from a table.
|
||||||
|
*/
|
||||||
|
export const deleteRow = async ({ tableId, rowId, revId }) => {
|
||||||
|
return await API.del({
|
||||||
|
url: `/api/${tableId}/rows/${rowId}/${revId}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes many rows from a table.
|
||||||
|
*/
|
||||||
|
export const deleteRows = async ({ tableId, rows }) => {
|
||||||
|
return await API.post({
|
||||||
|
url: `/api/${tableId}/rows`,
|
||||||
|
body: {
|
||||||
|
rows,
|
||||||
|
type: "delete",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enriches rows which contain certain field types so that they can
|
||||||
|
* be properly displayed.
|
||||||
|
*/
|
||||||
|
export const enrichRows = async (rows, tableId) => {
|
||||||
|
if (rows && rows.length && tableId) {
|
||||||
|
// Fetch table schema so we can check column types
|
||||||
|
const tableDefinition = await fetchTableDefinition(tableId)
|
||||||
|
const schema = tableDefinition && tableDefinition.schema
|
||||||
|
if (schema) {
|
||||||
|
const keys = Object.keys(schema)
|
||||||
|
rows.forEach(row => {
|
||||||
|
for (let key of keys) {
|
||||||
|
const type = schema[key].type
|
||||||
|
if (type === "link") {
|
||||||
|
// Enrich row with the count of any relationship fields
|
||||||
|
row[`${key}_count`] = Array.isArray(row[key]) ? row[key].length : 0
|
||||||
|
} else if (type === "attachment") {
|
||||||
|
// Enrich row with the first image URL for any attachment fields
|
||||||
|
let url = null
|
||||||
|
if (Array.isArray(row[key]) && row[key][0] != null) {
|
||||||
|
url = row[key][0].url
|
||||||
|
}
|
||||||
|
row[`${key}_first`] = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
import API from "./api"
|
||||||
|
import { enrichRows } from "./rows"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a table definition.
|
||||||
|
* Since definitions cannot change at runtime, the result is cached.
|
||||||
|
*/
|
||||||
|
export const fetchTableDefinition = async tableId => {
|
||||||
|
return await API.get({ url: `/api/tables/${tableId}`, cache: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all rows from a table.
|
||||||
|
*/
|
||||||
|
export const fetchTableData = async tableId => {
|
||||||
|
const rows = await API.get({ url: `/api/${tableId}/rows` })
|
||||||
|
return await enrichRows(rows, tableId)
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
import API from "./api"
|
||||||
|
import { enrichRows } from "./rows"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all rows in a view.
|
||||||
|
*/
|
||||||
|
export const fetchViewData = async ({
|
||||||
|
name,
|
||||||
|
field,
|
||||||
|
groupBy,
|
||||||
|
calculation,
|
||||||
|
tableId,
|
||||||
|
}) => {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
|
||||||
|
if (calculation) {
|
||||||
|
params.set("field", field)
|
||||||
|
params.set("calculation", calculation)
|
||||||
|
}
|
||||||
|
if (groupBy) {
|
||||||
|
params.set("group", groupBy)
|
||||||
|
}
|
||||||
|
|
||||||
|
const QUERY_VIEW_URL = field
|
||||||
|
? `/api/views/${name}?${params}`
|
||||||
|
: `/api/views/${name}`
|
||||||
|
|
||||||
|
const rows = await API.get({ url: QUERY_VIEW_URL })
|
||||||
|
return await enrichRows(rows, tableId)
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
<script>
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
import { setContext, onMount } from "svelte"
|
||||||
|
import Component from "./Component.svelte"
|
||||||
|
import SDK from "../sdk"
|
||||||
|
import { createDataStore, routeStore, screenStore } from "../store"
|
||||||
|
|
||||||
|
// Provide contexts
|
||||||
|
setContext("sdk", SDK)
|
||||||
|
setContext("component", writable({}))
|
||||||
|
setContext("data", createDataStore())
|
||||||
|
|
||||||
|
let loaded = false
|
||||||
|
|
||||||
|
// Load app config
|
||||||
|
onMount(async () => {
|
||||||
|
await routeStore.actions.fetchRoutes()
|
||||||
|
await screenStore.actions.fetchScreens()
|
||||||
|
loaded = true
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if loaded}
|
||||||
|
<Component definition={$screenStore.page.props} />
|
||||||
|
{/if}
|
|
@ -0,0 +1,52 @@
|
||||||
|
<script>
|
||||||
|
import { getContext, setContext } from "svelte"
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
import * as ComponentLibrary from "@budibase/standard-components"
|
||||||
|
import Router from "./Router.svelte"
|
||||||
|
import { enrichProps } from "../utils/componentProps"
|
||||||
|
import { bindingStore, builderStore } from "../store"
|
||||||
|
|
||||||
|
export let definition = {}
|
||||||
|
|
||||||
|
// Get local data binding context
|
||||||
|
const dataContext = getContext("data")
|
||||||
|
|
||||||
|
// Create component context
|
||||||
|
const componentStore = writable({})
|
||||||
|
setContext("component", componentStore)
|
||||||
|
|
||||||
|
// Extract component definition info
|
||||||
|
$: constructor = getComponentConstructor(definition._component)
|
||||||
|
$: children = definition._children
|
||||||
|
$: id = definition._id
|
||||||
|
$: enrichedProps = enrichProps(definition, $dataContext, $bindingStore)
|
||||||
|
$: selected = id === $builderStore.selectedComponentId
|
||||||
|
|
||||||
|
// Update component context
|
||||||
|
$: componentStore.set({ id, styles: { ...definition._styles, selected } })
|
||||||
|
|
||||||
|
// Gets the component constructor for the specified component
|
||||||
|
const getComponentConstructor = component => {
|
||||||
|
const split = component?.split("/")
|
||||||
|
const name = split?.[split.length - 1]
|
||||||
|
return name === "screenslot" ? Router : ComponentLibrary[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a unique key to let svelte know when to remount components.
|
||||||
|
// If a component is selected we want to remount it every time any props
|
||||||
|
// change.
|
||||||
|
const getChildKey = childId => {
|
||||||
|
const selected = childId === $builderStore.selectedComponentId
|
||||||
|
return selected ? `${childId}-${$builderStore.previewId}` : childId
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if constructor}
|
||||||
|
<svelte:component this={constructor} {...enrichedProps}>
|
||||||
|
{#if children && children.length}
|
||||||
|
{#each children as child (getChildKey(child._id))}
|
||||||
|
<svelte:self definition={child} />
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</svelte:component>
|
||||||
|
{/if}
|
|
@ -0,0 +1,15 @@
|
||||||
|
<script>
|
||||||
|
import { getContext, setContext } from "svelte"
|
||||||
|
import { createDataStore } from "../store"
|
||||||
|
|
||||||
|
export let row
|
||||||
|
|
||||||
|
// Clone and create new data context for this component tree
|
||||||
|
const dataContext = getContext("data")
|
||||||
|
const component = getContext("component")
|
||||||
|
const newData = createDataStore($dataContext)
|
||||||
|
setContext("data", newData)
|
||||||
|
$: newData.actions.addContext(row, $component.id)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<slot />
|
|
@ -0,0 +1,38 @@
|
||||||
|
<script>
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
import Router from "svelte-spa-router"
|
||||||
|
import { routeStore } from "../store"
|
||||||
|
import Screen from "./Screen.svelte"
|
||||||
|
|
||||||
|
const { styleable } = getContext("sdk")
|
||||||
|
const component = getContext("component")
|
||||||
|
|
||||||
|
$: routerConfig = getRouterConfig($routeStore.routes)
|
||||||
|
|
||||||
|
const getRouterConfig = routes => {
|
||||||
|
let config = {}
|
||||||
|
routes.forEach(route => {
|
||||||
|
config[route.path] = Screen
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add catch-all route so that we serve the Screen component always
|
||||||
|
config["*"] = Screen
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRouteLoading = ({ detail }) => {
|
||||||
|
routeStore.actions.setActiveRoute(detail.route)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if routerConfig}
|
||||||
|
<div use:styleable={$component.styles}>
|
||||||
|
<Router on:routeLoading={onRouteLoading} routes={routerConfig} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,35 @@
|
||||||
|
<script>
|
||||||
|
import { fade } from "svelte/transition"
|
||||||
|
import { screenStore, routeStore } from "../store"
|
||||||
|
import Component from "./Component.svelte"
|
||||||
|
|
||||||
|
// Keep route params up to date
|
||||||
|
export let params = {}
|
||||||
|
$: routeStore.actions.setRouteParams(params || {})
|
||||||
|
|
||||||
|
// Get the screen definition for the current route
|
||||||
|
$: screenDefinition = $screenStore.activeScreen?.props
|
||||||
|
|
||||||
|
// Redirect to home page if no matching route
|
||||||
|
$: screenDefinition == null && routeStore.actions.navigate("/")
|
||||||
|
|
||||||
|
// Make a screen array so we can use keying to properly re-render each screen
|
||||||
|
$: screens = screenDefinition ? [screenDefinition] : []
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#each screens as screen (screen._id)}
|
||||||
|
<div in:fade>
|
||||||
|
<Component definition={screen} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
align-self: stretch;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,91 +0,0 @@
|
||||||
import { attachChildren } from "./render/attachChildren"
|
|
||||||
import { createTreeNode } from "./render/prepareRenderComponent"
|
|
||||||
import { screenRouter } from "./render/screenRouter"
|
|
||||||
import { createStateManager } from "./state/stateManager"
|
|
||||||
import { getAppId } from "./render/getAppId"
|
|
||||||
|
|
||||||
export const createApp = ({
|
|
||||||
componentLibraries,
|
|
||||||
frontendDefinition,
|
|
||||||
window,
|
|
||||||
}) => {
|
|
||||||
let routeTo
|
|
||||||
let currentUrl
|
|
||||||
let screenStateManager
|
|
||||||
|
|
||||||
const onScreenSlotRendered = screenSlotNode => {
|
|
||||||
const onScreenSelected = (screen, url) => {
|
|
||||||
const stateManager = createStateManager({
|
|
||||||
componentLibraries,
|
|
||||||
onScreenSlotRendered: () => {},
|
|
||||||
routeTo,
|
|
||||||
})
|
|
||||||
const getAttachChildrenParams = attachChildrenParams(stateManager)
|
|
||||||
screenSlotNode.props._children = [screen.props]
|
|
||||||
const initialiseChildParams = getAttachChildrenParams(screenSlotNode)
|
|
||||||
attachChildren(initialiseChildParams)(screenSlotNode.rootElement, {
|
|
||||||
hydrate: true,
|
|
||||||
force: true,
|
|
||||||
})
|
|
||||||
if (screenStateManager) screenStateManager.destroy()
|
|
||||||
screenStateManager = stateManager
|
|
||||||
currentUrl = url
|
|
||||||
}
|
|
||||||
|
|
||||||
routeTo = screenRouter({
|
|
||||||
screens: frontendDefinition.screens,
|
|
||||||
onScreenSelected,
|
|
||||||
window,
|
|
||||||
})
|
|
||||||
const fallbackPath = window.location.pathname.replace(
|
|
||||||
getAppId(window.document.cookie),
|
|
||||||
""
|
|
||||||
)
|
|
||||||
routeTo(currentUrl || fallbackPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
const attachChildrenParams = stateManager => {
|
|
||||||
const getInitialiseParams = treeNode => ({
|
|
||||||
componentLibraries,
|
|
||||||
treeNode,
|
|
||||||
onScreenSlotRendered,
|
|
||||||
setupState: stateManager.setup,
|
|
||||||
})
|
|
||||||
|
|
||||||
return getInitialiseParams
|
|
||||||
}
|
|
||||||
|
|
||||||
let rootTreeNode
|
|
||||||
const pageStateManager = createStateManager({
|
|
||||||
componentLibraries,
|
|
||||||
onScreenSlotRendered,
|
|
||||||
// seems weird, but the routeTo variable may not be available at this point
|
|
||||||
routeTo: url => routeTo(url),
|
|
||||||
})
|
|
||||||
|
|
||||||
const initialisePage = (page, target, urlPath) => {
|
|
||||||
currentUrl = urlPath
|
|
||||||
|
|
||||||
rootTreeNode = createTreeNode()
|
|
||||||
rootTreeNode.props = {
|
|
||||||
_children: [page.props],
|
|
||||||
}
|
|
||||||
const getInitialiseParams = attachChildrenParams(pageStateManager)
|
|
||||||
const initChildParams = getInitialiseParams(rootTreeNode)
|
|
||||||
|
|
||||||
attachChildren(initChildParams)(target, {
|
|
||||||
hydrate: true,
|
|
||||||
force: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
return rootTreeNode
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
initialisePage,
|
|
||||||
screenStore: () => screenStateManager.store,
|
|
||||||
pageStore: () => pageStateManager.store,
|
|
||||||
routeTo: () => routeTo,
|
|
||||||
rootNode: () => rootTreeNode,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,59 +1,25 @@
|
||||||
import { createApp } from "./createApp"
|
import ClientApp from "./components/ClientApp.svelte"
|
||||||
import { builtins, builtinLibName } from "./render/builtinComponents"
|
import { builderStore } from "./store"
|
||||||
import { getAppId } from "./render/getAppId"
|
|
||||||
|
|
||||||
/**
|
let app
|
||||||
* create a web application from static budibase definition files.
|
|
||||||
* @param {object} opts - configuration options for budibase client libary
|
|
||||||
*/
|
|
||||||
export const loadBudibase = async opts => {
|
|
||||||
const _window = (opts && opts.window) || window
|
|
||||||
// const _localStorage = (opts && opts.localStorage) || localStorage
|
|
||||||
const appId = getAppId(window.document.cookie)
|
|
||||||
const frontendDefinition = _window["##BUDIBASE_FRONTEND_DEFINITION##"]
|
|
||||||
|
|
||||||
const user = {}
|
const loadBudibase = () => {
|
||||||
|
// Update builder store with any builder flags
|
||||||
const componentLibraryModules = (opts && opts.componentLibraries) || {}
|
builderStore.set({
|
||||||
|
inBuilder: !!window["##BUDIBASE_IN_BUILDER##"],
|
||||||
const libraries = frontendDefinition.libraries || []
|
page: window["##BUDIBASE_PREVIEW_PAGE##"],
|
||||||
|
screen: window["##BUDIBASE_PREVIEW_SCREEN##"],
|
||||||
for (let library of libraries) {
|
selectedComponentId: window["##BUDIBASE_SELECTED_COMPONENT_ID##"],
|
||||||
// fetch the JavaScript for the component libraries from the server
|
previewId: window["##BUDIBASE_PREVIEW_ID##"],
|
||||||
componentLibraryModules[library] = await import(
|
|
||||||
`/componentlibrary?library=${encodeURI(library)}&appId=${appId}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
componentLibraryModules[builtinLibName] = builtins(_window)
|
|
||||||
|
|
||||||
const {
|
|
||||||
initialisePage,
|
|
||||||
screenStore,
|
|
||||||
pageStore,
|
|
||||||
routeTo,
|
|
||||||
rootNode,
|
|
||||||
} = createApp({
|
|
||||||
componentLibraries: componentLibraryModules,
|
|
||||||
frontendDefinition,
|
|
||||||
user,
|
|
||||||
window: _window,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const route = _window.location
|
// Create app if one hasn't been created yet
|
||||||
? _window.location.pathname.replace(`${appId}/`, "").replace(appId, "")
|
if (!app) {
|
||||||
: ""
|
app = new ClientApp({
|
||||||
|
target: window.document.body,
|
||||||
initialisePage(frontendDefinition.page, _window.document.body, route)
|
})
|
||||||
|
|
||||||
return {
|
|
||||||
screenStore,
|
|
||||||
pageStore,
|
|
||||||
routeTo,
|
|
||||||
rootNode,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window) {
|
// Attach to window so the HTML template can call this when it loads
|
||||||
window.loadBudibase = loadBudibase
|
window.loadBudibase = loadBudibase
|
||||||
}
|
|
||||||
|
|
|
@ -1,138 +0,0 @@
|
||||||
import { prepareRenderComponent } from "./prepareRenderComponent"
|
|
||||||
import { isScreenSlot } from "./builtinComponents"
|
|
||||||
import deepEqual from "deep-equal"
|
|
||||||
import appStore from "../state/store"
|
|
||||||
|
|
||||||
export const attachChildren = initialiseOpts => (htmlElement, options) => {
|
|
||||||
const {
|
|
||||||
componentLibraries,
|
|
||||||
treeNode,
|
|
||||||
onScreenSlotRendered,
|
|
||||||
setupState,
|
|
||||||
} = initialiseOpts
|
|
||||||
|
|
||||||
const anchor = options && options.anchor ? options.anchor : null
|
|
||||||
const force = options ? options.force : false
|
|
||||||
const hydrate = options ? options.hydrate : true
|
|
||||||
const context = options && options.context
|
|
||||||
|
|
||||||
if (!force && treeNode.children.length > 0) return treeNode.children
|
|
||||||
|
|
||||||
for (let childNode of treeNode.children) {
|
|
||||||
childNode.destroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!htmlElement) return
|
|
||||||
|
|
||||||
if (hydrate) {
|
|
||||||
while (htmlElement.firstChild) {
|
|
||||||
htmlElement.removeChild(htmlElement.firstChild)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const contextStoreKeys = []
|
|
||||||
|
|
||||||
// create new context if supplied
|
|
||||||
if (context) {
|
|
||||||
let childIndex = 0
|
|
||||||
// if context is an array, map to new structure
|
|
||||||
const contextArray = Array.isArray(context) ? context : [context]
|
|
||||||
for (let ctx of contextArray) {
|
|
||||||
const key = appStore.create(
|
|
||||||
ctx,
|
|
||||||
treeNode.props._id,
|
|
||||||
childIndex,
|
|
||||||
treeNode.contextStoreKey
|
|
||||||
)
|
|
||||||
contextStoreKeys.push(key)
|
|
||||||
childIndex++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const childNodes = []
|
|
||||||
|
|
||||||
const createChildNodes = contextStoreKey => {
|
|
||||||
for (let childProps of treeNode.props._children) {
|
|
||||||
const { componentName, libName } = splitName(childProps._component)
|
|
||||||
|
|
||||||
if (!componentName || !libName) return
|
|
||||||
|
|
||||||
const ComponentConstructor = componentLibraries[libName][componentName]
|
|
||||||
|
|
||||||
const childNode = prepareRenderComponent({
|
|
||||||
props: childProps,
|
|
||||||
parentNode: treeNode,
|
|
||||||
ComponentConstructor,
|
|
||||||
htmlElement,
|
|
||||||
anchor,
|
|
||||||
// in same context as parent, unless a new one was supplied
|
|
||||||
contextStoreKey,
|
|
||||||
})
|
|
||||||
|
|
||||||
childNodes.push(childNode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context) {
|
|
||||||
// if new context(s) is supplied, then create nodes
|
|
||||||
// with keys to new context stores
|
|
||||||
for (let contextStoreKey of contextStoreKeys) {
|
|
||||||
createChildNodes(contextStoreKey)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// otherwise, use same context store as parent
|
|
||||||
// which maybe undefined (therfor using the root state)
|
|
||||||
createChildNodes(treeNode.contextStoreKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if everything is equal, then don't re-render
|
|
||||||
if (areTreeNodesEqual(treeNode.children, childNodes)) return treeNode.children
|
|
||||||
|
|
||||||
for (let node of childNodes) {
|
|
||||||
const initialProps = setupState(node)
|
|
||||||
node.render(initialProps)
|
|
||||||
}
|
|
||||||
|
|
||||||
const screenSlot = childNodes.find(n => isScreenSlot(n.props._component))
|
|
||||||
|
|
||||||
if (onScreenSlotRendered && screenSlot) {
|
|
||||||
// assuming there is only ever one screen slot
|
|
||||||
onScreenSlotRendered(screenSlot)
|
|
||||||
}
|
|
||||||
|
|
||||||
treeNode.children = childNodes
|
|
||||||
|
|
||||||
return childNodes
|
|
||||||
}
|
|
||||||
|
|
||||||
const splitName = fullname => {
|
|
||||||
const nameParts = fullname.split("/")
|
|
||||||
|
|
||||||
const componentName = nameParts[nameParts.length - 1]
|
|
||||||
|
|
||||||
const libName = fullname.substring(
|
|
||||||
0,
|
|
||||||
fullname.length - componentName.length - 1
|
|
||||||
)
|
|
||||||
|
|
||||||
return { libName, componentName }
|
|
||||||
}
|
|
||||||
|
|
||||||
const areTreeNodesEqual = (children1, children2) => {
|
|
||||||
if (children1.length !== children2.length) return false
|
|
||||||
if (children1 === children2) return true
|
|
||||||
|
|
||||||
let isEqual = false
|
|
||||||
for (let i = 0; i < children1.length; i++) {
|
|
||||||
// same context and same children, then nothing has changed
|
|
||||||
isEqual =
|
|
||||||
deepEqual(children1[i].context, children2[i].context) &&
|
|
||||||
areTreeNodesEqual(children1[i].children, children2[i].children)
|
|
||||||
if (!isEqual) return false
|
|
||||||
if (isScreenSlot(children1[i].parentNode.props._component)) {
|
|
||||||
isEqual = deepEqual(children1[i].props, children2[i].props)
|
|
||||||
}
|
|
||||||
if (!isEqual) return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
import { screenSlotComponent } from "./screenSlotComponent"
|
|
||||||
|
|
||||||
export const builtinLibName = "##builtin"
|
|
||||||
|
|
||||||
export const isScreenSlot = componentName =>
|
|
||||||
componentName === "##builtin/screenslot"
|
|
||||||
|
|
||||||
export const builtins = window => ({
|
|
||||||
screenslot: screenSlotComponent(window),
|
|
||||||
})
|
|
|
@ -1,88 +0,0 @@
|
||||||
import renderTemplateString from "../state/renderTemplateString"
|
|
||||||
import appStore from "../state/store"
|
|
||||||
import hasBinding from "../state/hasBinding"
|
|
||||||
|
|
||||||
export const prepareRenderComponent = ({
|
|
||||||
ComponentConstructor,
|
|
||||||
htmlElement,
|
|
||||||
anchor,
|
|
||||||
props,
|
|
||||||
parentNode,
|
|
||||||
contextStoreKey,
|
|
||||||
}) => {
|
|
||||||
const thisNode = createTreeNode()
|
|
||||||
thisNode.parentNode = parentNode
|
|
||||||
thisNode.props = props
|
|
||||||
thisNode.contextStoreKey = contextStoreKey
|
|
||||||
|
|
||||||
// the treeNode is first created (above), and then this
|
|
||||||
// render method is add. The treeNode is returned, and
|
|
||||||
// render is called later (in attachChildren)
|
|
||||||
thisNode.render = initialProps => {
|
|
||||||
thisNode.component = new ComponentConstructor({
|
|
||||||
target: htmlElement,
|
|
||||||
props: initialProps,
|
|
||||||
hydrate: false,
|
|
||||||
anchor,
|
|
||||||
})
|
|
||||||
|
|
||||||
// finds the root element of the component, which was created by the contructor above
|
|
||||||
// we use this later to attach a className to. This is how styles
|
|
||||||
// are applied by the builder
|
|
||||||
thisNode.rootElement = htmlElement.children[htmlElement.children.length - 1]
|
|
||||||
|
|
||||||
let [componentName] = props._component.match(/[a-z]*$/)
|
|
||||||
if (props._id && thisNode.rootElement) {
|
|
||||||
thisNode.rootElement.classList.add(`${componentName}-${props._id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// make this node listen to the store
|
|
||||||
if (thisNode.stateBound) {
|
|
||||||
const unsubscribe = appStore.subscribe(state => {
|
|
||||||
const storeBoundProps = Object.keys(initialProps._bb.props).filter(p =>
|
|
||||||
hasBinding(initialProps._bb.props[p])
|
|
||||||
)
|
|
||||||
if (storeBoundProps.length > 0) {
|
|
||||||
const toSet = {}
|
|
||||||
for (let prop of storeBoundProps) {
|
|
||||||
const propValue = initialProps._bb.props[prop]
|
|
||||||
toSet[prop] = renderTemplateString(propValue, state)
|
|
||||||
}
|
|
||||||
thisNode.component.$set(toSet)
|
|
||||||
}
|
|
||||||
}, thisNode.contextStoreKey)
|
|
||||||
thisNode.unsubscribe = unsubscribe
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return thisNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createTreeNode = () => ({
|
|
||||||
context: {},
|
|
||||||
props: {},
|
|
||||||
rootElement: null,
|
|
||||||
parentNode: null,
|
|
||||||
children: [],
|
|
||||||
bindings: [],
|
|
||||||
component: null,
|
|
||||||
unsubscribe: () => {},
|
|
||||||
render: () => {},
|
|
||||||
get destroy() {
|
|
||||||
const node = this
|
|
||||||
return () => {
|
|
||||||
if (node.children) {
|
|
||||||
// destroy children first - from leaf nodes up
|
|
||||||
for (let child of node.children) {
|
|
||||||
child.destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (node.unsubscribe) node.unsubscribe()
|
|
||||||
if (node.component && node.component.$destroy) node.component.$destroy()
|
|
||||||
for (let onDestroyItem of node.onDestroy) {
|
|
||||||
onDestroyItem()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDestroy: [],
|
|
||||||
})
|
|
|
@ -1,124 +0,0 @@
|
||||||
import regexparam from "regexparam"
|
|
||||||
import appStore from "../state/store"
|
|
||||||
import { getAppId } from "./getAppId"
|
|
||||||
|
|
||||||
export const screenRouter = ({ screens, onScreenSelected, window }) => {
|
|
||||||
function sanitize(url) {
|
|
||||||
if (!url) return url
|
|
||||||
return url
|
|
||||||
.split("/")
|
|
||||||
.map(part => {
|
|
||||||
// if parameter, then use as is
|
|
||||||
if (part.startsWith(":")) return part
|
|
||||||
return encodeURIComponent(part)
|
|
||||||
})
|
|
||||||
.join("/")
|
|
||||||
.toLowerCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
const isRunningLocally = () => {
|
|
||||||
const hostname = (window.location && window.location.hostname) || ""
|
|
||||||
return (
|
|
||||||
hostname === "localhost" ||
|
|
||||||
hostname === "127.0.0.1" ||
|
|
||||||
hostname.startsWith("192.168")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const makeRootedPath = url => {
|
|
||||||
if (isRunningLocally()) {
|
|
||||||
const appId = getAppId(window.document.cookie)
|
|
||||||
if (url) {
|
|
||||||
url = sanitize(url)
|
|
||||||
if (!url.startsWith("/")) {
|
|
||||||
url = `/${url}`
|
|
||||||
}
|
|
||||||
if (url.startsWith(`/${appId}`)) {
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
return `/${appId}${url}`
|
|
||||||
}
|
|
||||||
return `/${appId}`
|
|
||||||
}
|
|
||||||
return sanitize(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
const routes = screens.map(screen =>
|
|
||||||
makeRootedPath(screen.routing ? screen.routing.route : null)
|
|
||||||
)
|
|
||||||
let fallback = routes.findIndex(([p]) => p === makeRootedPath("*"))
|
|
||||||
if (fallback < 0) fallback = 0
|
|
||||||
|
|
||||||
let current
|
|
||||||
|
|
||||||
function route(url) {
|
|
||||||
const _url = makeRootedPath(url.state || url)
|
|
||||||
current = routes.findIndex(
|
|
||||||
p =>
|
|
||||||
p !== makeRootedPath("*") &&
|
|
||||||
new RegExp("^" + p.toLowerCase() + "$").test(_url.toLowerCase())
|
|
||||||
)
|
|
||||||
|
|
||||||
const params = {}
|
|
||||||
|
|
||||||
if (current === -1) {
|
|
||||||
routes.forEach((p, i) => {
|
|
||||||
// ignore home - which matched everything
|
|
||||||
if (p === makeRootedPath("*")) return
|
|
||||||
const pm = regexparam(p)
|
|
||||||
const matches = pm.pattern.exec(_url)
|
|
||||||
|
|
||||||
if (!matches) return
|
|
||||||
|
|
||||||
let j = 0
|
|
||||||
while (j < pm.keys.length) {
|
|
||||||
params[pm.keys[j]] = matches[++j] || null
|
|
||||||
}
|
|
||||||
|
|
||||||
current = i
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
appStore.update(state => {
|
|
||||||
state["##routeParams"] = params
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
|
|
||||||
const screenIndex = current !== -1 ? current : fallback
|
|
||||||
|
|
||||||
try {
|
|
||||||
!url.state && history.pushState(_url, null, _url)
|
|
||||||
} catch (_) {
|
|
||||||
// ignoring an exception here as the builder runs an iframe, which does not like this
|
|
||||||
}
|
|
||||||
|
|
||||||
onScreenSelected(screens[screenIndex], _url)
|
|
||||||
}
|
|
||||||
|
|
||||||
function click(e) {
|
|
||||||
const x = e.target.closest("a")
|
|
||||||
const y = x && x.getAttribute("href")
|
|
||||||
|
|
||||||
if (
|
|
||||||
e.ctrlKey ||
|
|
||||||
e.metaKey ||
|
|
||||||
e.altKey ||
|
|
||||||
e.shiftKey ||
|
|
||||||
e.button ||
|
|
||||||
e.defaultPrevented
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
const target = (x && x.target) || "_self"
|
|
||||||
if (!y || target !== "_self" || x.host !== location.host) return
|
|
||||||
|
|
||||||
e.preventDefault()
|
|
||||||
route(y)
|
|
||||||
}
|
|
||||||
|
|
||||||
addEventListener("popstate", route)
|
|
||||||
addEventListener("pushstate", route)
|
|
||||||
addEventListener("click", click)
|
|
||||||
|
|
||||||
return route
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
export const screenSlotComponent = window => {
|
|
||||||
return function(opts) {
|
|
||||||
const node = window.document.createElement("DIV")
|
|
||||||
const $set = props => {
|
|
||||||
props._bb.attachChildren(node)
|
|
||||||
}
|
|
||||||
const $destroy = () => {
|
|
||||||
if (opts.target && node) opts.target.removeChild(node)
|
|
||||||
}
|
|
||||||
this.$set = $set
|
|
||||||
this.$destroy = $destroy
|
|
||||||
opts.target.appendChild(node)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
import * as API from "./api"
|
||||||
|
import { authStore, routeStore, screenStore, bindingStore } from "./store"
|
||||||
|
import { styleable } from "./utils/styleable"
|
||||||
|
import { getAppId } from "./utils/getAppId"
|
||||||
|
import { link as linkable } from "svelte-spa-router"
|
||||||
|
import DataProvider from "./components/DataProvider.svelte"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
API,
|
||||||
|
authStore,
|
||||||
|
routeStore,
|
||||||
|
screenStore,
|
||||||
|
styleable,
|
||||||
|
linkable,
|
||||||
|
getAppId,
|
||||||
|
DataProvider,
|
||||||
|
setBindableValue: bindingStore.actions.setBindableValue,
|
||||||
|
}
|
|
@ -1,43 +0,0 @@
|
||||||
import setBindableComponentProp from "./setBindableComponentProp"
|
|
||||||
import { attachChildren } from "../render/attachChildren"
|
|
||||||
import store from "../state/store"
|
|
||||||
import { baseApiCall } from "../api/index"
|
|
||||||
|
|
||||||
export const bbFactory = ({
|
|
||||||
componentLibraries,
|
|
||||||
onScreenSlotRendered,
|
|
||||||
runEventActions,
|
|
||||||
}) => {
|
|
||||||
const api = {
|
|
||||||
post: (url, body) => baseApiCall("POST", url, body),
|
|
||||||
get: (url, body) => baseApiCall("GET", url, body),
|
|
||||||
patch: (url, body) => baseApiCall("PATCH", url, body),
|
|
||||||
delete: (url, body) => baseApiCall("DELETE", url, body),
|
|
||||||
}
|
|
||||||
|
|
||||||
return (treeNode, setupState) => {
|
|
||||||
const attachParams = {
|
|
||||||
componentLibraries,
|
|
||||||
treeNode,
|
|
||||||
onScreenSlotRendered,
|
|
||||||
setupState,
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
attachChildren: attachChildren(attachParams),
|
|
||||||
props: treeNode.props,
|
|
||||||
call: async eventName =>
|
|
||||||
eventName &&
|
|
||||||
(await runEventActions(
|
|
||||||
treeNode.props[eventName],
|
|
||||||
store.getState(treeNode.contextStoreKey)
|
|
||||||
)),
|
|
||||||
setBinding: setBindableComponentProp(treeNode),
|
|
||||||
api,
|
|
||||||
parent,
|
|
||||||
store: store.getStore(treeNode.contextStoreKey),
|
|
||||||
// these parameters are populated by screenRouter
|
|
||||||
routeParams: () => store.getState()["##routeParams"],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
import api from "../api"
|
|
||||||
import renderTemplateString from "./renderTemplateString"
|
|
||||||
|
|
||||||
export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
|
|
||||||
|
|
||||||
export const eventHandlers = routeTo => {
|
|
||||||
const handlers = {
|
|
||||||
"Navigate To": param => routeTo(param && param.url),
|
|
||||||
"Update Row": api.updateRow,
|
|
||||||
"Save Row": api.saveRow,
|
|
||||||
"Delete Row": api.deleteRow,
|
|
||||||
"Trigger Workflow": api.triggerWorkflow,
|
|
||||||
}
|
|
||||||
|
|
||||||
// when an event is called, this is what gets run
|
|
||||||
const runEventActions = async (actions, state) => {
|
|
||||||
if (!actions) return
|
|
||||||
// calls event handlers sequentially
|
|
||||||
for (let action of actions) {
|
|
||||||
const handler = handlers[action[EVENT_TYPE_MEMBER_NAME]]
|
|
||||||
const parameters = createParameters(action.parameters, state)
|
|
||||||
if (handler) {
|
|
||||||
await handler(parameters, state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return runEventActions
|
|
||||||
}
|
|
||||||
|
|
||||||
// this will take a parameters obj, iterate all keys, and do a mustache render
|
|
||||||
// for every string. It will work recursively if it encounnters an {}
|
|
||||||
const createParameters = (parameterTemplateObj, state) => {
|
|
||||||
const parameters = {}
|
|
||||||
for (let key in parameterTemplateObj) {
|
|
||||||
if (typeof parameterTemplateObj[key] === "string") {
|
|
||||||
parameters[key] = renderTemplateString(parameterTemplateObj[key], state)
|
|
||||||
} else if (typeof parameterTemplateObj[key] === "object") {
|
|
||||||
parameters[key] = createParameters(parameterTemplateObj[key], state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return parameters
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
export const setContext = treeNode => (key, value) =>
|
|
||||||
(treeNode.context[key] = value)
|
|
||||||
|
|
||||||
export const getContext = treeNode => key => {
|
|
||||||
if (treeNode.context && treeNode.context[key] !== undefined)
|
|
||||||
return treeNode.context[key]
|
|
||||||
|
|
||||||
if (!treeNode.context.$parent) return
|
|
||||||
|
|
||||||
return getContext(treeNode.parentNode)(key)
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export default value => typeof value === "string" && value.includes("{{")
|
|
|
@ -1,17 +0,0 @@
|
||||||
import mustache from "mustache"
|
|
||||||
|
|
||||||
// this is a much more liberal version of mustache's escape function
|
|
||||||
// ...just ignoring < and > to prevent tags from user input
|
|
||||||
// original version here https://github.com/janl/mustache.js/blob/4b7908f5c9fec469a11cfaed2f2bed23c84e1c5c/mustache.js#L78
|
|
||||||
|
|
||||||
const entityMap = {
|
|
||||||
"<": "<",
|
|
||||||
">": ">",
|
|
||||||
}
|
|
||||||
|
|
||||||
mustache.escape = text =>
|
|
||||||
String(text).replace(/[&<>"'`=/]/g, function fromEntityMap(s) {
|
|
||||||
return entityMap[s] || s
|
|
||||||
})
|
|
||||||
|
|
||||||
export default mustache.render
|
|
|
@ -1,13 +0,0 @@
|
||||||
import appStore from "./store"
|
|
||||||
|
|
||||||
export default treeNode => (propName, value) => {
|
|
||||||
if (!propName || propName.length === 0) return
|
|
||||||
if (!treeNode) return
|
|
||||||
const componentId = treeNode.props._id
|
|
||||||
|
|
||||||
appStore.update(state => {
|
|
||||||
state[componentId] = state[componentId] || {}
|
|
||||||
state[componentId][propName] = value
|
|
||||||
return state
|
|
||||||
}, treeNode.contextStoreKey)
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
import { eventHandlers } from "./eventHandlers"
|
|
||||||
import { bbFactory } from "./bbComponentApi"
|
|
||||||
import renderTemplateString from "./renderTemplateString"
|
|
||||||
import appStore from "./store"
|
|
||||||
import hasBinding from "./hasBinding"
|
|
||||||
|
|
||||||
const doNothing = () => {}
|
|
||||||
doNothing.isPlaceholder = true
|
|
||||||
|
|
||||||
const isMetaProp = propName =>
|
|
||||||
propName === "_component" ||
|
|
||||||
propName === "_children" ||
|
|
||||||
propName === "_id" ||
|
|
||||||
propName === "_style" ||
|
|
||||||
propName === "_code" ||
|
|
||||||
propName === "_codeMeta" ||
|
|
||||||
propName === "_styles"
|
|
||||||
|
|
||||||
export const createStateManager = ({
|
|
||||||
componentLibraries,
|
|
||||||
onScreenSlotRendered,
|
|
||||||
routeTo,
|
|
||||||
}) => {
|
|
||||||
let runEventActions = eventHandlers(routeTo)
|
|
||||||
|
|
||||||
const bb = bbFactory({
|
|
||||||
componentLibraries,
|
|
||||||
onScreenSlotRendered,
|
|
||||||
runEventActions,
|
|
||||||
})
|
|
||||||
|
|
||||||
const setup = _setup(bb)
|
|
||||||
|
|
||||||
return {
|
|
||||||
setup,
|
|
||||||
destroy: () => {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const _setup = bb => node => {
|
|
||||||
const props = node.props
|
|
||||||
const initialProps = { ...props }
|
|
||||||
|
|
||||||
for (let propName in props) {
|
|
||||||
if (isMetaProp(propName)) continue
|
|
||||||
|
|
||||||
const propValue = props[propName]
|
|
||||||
|
|
||||||
const isBound = hasBinding(propValue)
|
|
||||||
|
|
||||||
if (isBound) {
|
|
||||||
const state = appStore.getState(node.contextStoreKey)
|
|
||||||
initialProps[propName] = renderTemplateString(propValue, state)
|
|
||||||
|
|
||||||
if (!node.stateBound) {
|
|
||||||
node.stateBound = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const setup = _setup(bb)
|
|
||||||
initialProps._bb = bb(node, setup)
|
|
||||||
|
|
||||||
return initialProps
|
|
||||||
}
|
|
|
@ -1,108 +0,0 @@
|
||||||
import { writable } from "svelte/store"
|
|
||||||
|
|
||||||
// we assume that the reference to this state object
|
|
||||||
// will remain for the life of the application
|
|
||||||
const rootState = {}
|
|
||||||
const rootStore = writable(rootState)
|
|
||||||
const contextStores = {}
|
|
||||||
|
|
||||||
// contextProviderId is the component id that provides the data for the context
|
|
||||||
const contextStoreKey = (dataProviderId, childIndex) =>
|
|
||||||
`${dataProviderId}${childIndex >= 0 ? ":" + childIndex : ""}`
|
|
||||||
|
|
||||||
// creates a store for a datacontext (e.g. each item in a list component)
|
|
||||||
// overrides store if already exists
|
|
||||||
const create = (data, dataProviderId, childIndex, parentContextStoreId) => {
|
|
||||||
const key = contextStoreKey(dataProviderId, childIndex)
|
|
||||||
const state = { data }
|
|
||||||
|
|
||||||
// add reference to parent state object,
|
|
||||||
// so we can use bindings like state.parent.parent
|
|
||||||
// (if no parent, then parent is rootState )
|
|
||||||
state.parent = parentContextStoreId
|
|
||||||
? contextStores[parentContextStoreId].state
|
|
||||||
: rootState
|
|
||||||
|
|
||||||
contextStores[key] = {
|
|
||||||
store: writable(state),
|
|
||||||
subscriberCount: 0,
|
|
||||||
state,
|
|
||||||
parentContextStoreId,
|
|
||||||
}
|
|
||||||
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
const subscribe = (subscription, storeKey) => {
|
|
||||||
if (!storeKey) {
|
|
||||||
return rootStore.subscribe(subscription)
|
|
||||||
}
|
|
||||||
const contextStore = contextStores[storeKey]
|
|
||||||
|
|
||||||
// we are subscribing to multiple stores,
|
|
||||||
// we dont want to run our listener for every subscription, the first time
|
|
||||||
// as this could repeatedly run $set on the same component
|
|
||||||
// ... which already has its initial properties set properly
|
|
||||||
const ignoreFirstSubscription = () => {
|
|
||||||
let hasRunOnce = false
|
|
||||||
return () => {
|
|
||||||
if (hasRunOnce) subscription(contextStore.state)
|
|
||||||
hasRunOnce = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const unsubscribes = [rootStore.subscribe(ignoreFirstSubscription())]
|
|
||||||
|
|
||||||
// we subscribe to all stores in the hierarchy
|
|
||||||
const ancestorSubscribe = ctxStore => {
|
|
||||||
// unsubscribe func returned by svelte store
|
|
||||||
const svelteUnsub = ctxStore.store.subscribe(ignoreFirstSubscription())
|
|
||||||
|
|
||||||
// we wrap the svelte unsubscribe, so we can
|
|
||||||
// cleanup stores when they are no longer subscribed to
|
|
||||||
const unsub = () => {
|
|
||||||
ctxStore.subscriberCount = contextStore.subscriberCount - 1
|
|
||||||
// when no subscribers left, we delete the store
|
|
||||||
if (ctxStore.subscriberCount === 0) {
|
|
||||||
delete ctxStore[storeKey]
|
|
||||||
}
|
|
||||||
svelteUnsub()
|
|
||||||
}
|
|
||||||
unsubscribes.push(unsub)
|
|
||||||
if (ctxStore.parentContextStoreId) {
|
|
||||||
ancestorSubscribe(contextStores[ctxStore.parentContextStoreId])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ancestorSubscribe(contextStore)
|
|
||||||
|
|
||||||
// our final unsubscribe function calls unsubscribe on all stores
|
|
||||||
return () => unsubscribes.forEach(u => u())
|
|
||||||
}
|
|
||||||
|
|
||||||
const findStore = (dataProviderId, childIndex) =>
|
|
||||||
dataProviderId
|
|
||||||
? contextStores[contextStoreKey(dataProviderId, childIndex)].store
|
|
||||||
: rootStore
|
|
||||||
|
|
||||||
const update = (updatefunc, dataProviderId, childIndex) =>
|
|
||||||
findStore(dataProviderId, childIndex).update(updatefunc)
|
|
||||||
|
|
||||||
const set = (value, dataProviderId, childIndex) =>
|
|
||||||
findStore(dataProviderId, childIndex).set(value)
|
|
||||||
|
|
||||||
const getState = contextStoreKey =>
|
|
||||||
contextStoreKey ? contextStores[contextStoreKey].state : rootState
|
|
||||||
|
|
||||||
const getStore = contextStoreKey =>
|
|
||||||
contextStoreKey ? contextStores[contextStoreKey].store : rootStore
|
|
||||||
|
|
||||||
export default {
|
|
||||||
subscribe,
|
|
||||||
update,
|
|
||||||
set,
|
|
||||||
getState,
|
|
||||||
create,
|
|
||||||
contextStoreKey,
|
|
||||||
getStore,
|
|
||||||
}
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import * as API from "../api"
|
||||||
|
import { getAppId } from "../utils/getAppId"
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
|
||||||
|
const createAuthStore = () => {
|
||||||
|
const store = writable("")
|
||||||
|
|
||||||
|
const logIn = async ({ username, password }) => {
|
||||||
|
const user = await API.logIn({ username, password })
|
||||||
|
if (!user.error) {
|
||||||
|
store.set(user.token)
|
||||||
|
location.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const logOut = () => {
|
||||||
|
store.set("")
|
||||||
|
const appId = getAppId()
|
||||||
|
if (appId) {
|
||||||
|
for (let environment of ["local", "cloud"]) {
|
||||||
|
window.document.cookie = `budibase:${appId}:${environment}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
location.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
actions: { logIn, logOut },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const authStore = createAuthStore()
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
|
||||||
|
const createBindingStore = () => {
|
||||||
|
const store = writable({})
|
||||||
|
|
||||||
|
const setBindableValue = (value, componentId) => {
|
||||||
|
store.update(state => {
|
||||||
|
if (componentId) {
|
||||||
|
state[componentId] = value
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
actions: { setBindableValue },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const bindingStore = createBindingStore()
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
|
||||||
|
const createBuilderStore = () => {
|
||||||
|
const initialState = {
|
||||||
|
inBuilder: false,
|
||||||
|
page: null,
|
||||||
|
screen: null,
|
||||||
|
selectedComponentId: null,
|
||||||
|
previewId: null,
|
||||||
|
}
|
||||||
|
return writable(initialState)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const builderStore = createBuilderStore()
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
||||||
|
export const createDataStore = existingContext => {
|
||||||
|
const store = writable({ ...existingContext })
|
||||||
|
|
||||||
|
// Adds a context layer to the data context tree
|
||||||
|
const addContext = (row, componentId) => {
|
||||||
|
store.update(state => {
|
||||||
|
if (componentId) {
|
||||||
|
state[componentId] = row
|
||||||
|
state[`${componentId}_draft`] = cloneDeep(row)
|
||||||
|
state.closestComponentId = componentId
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
update: store.update,
|
||||||
|
actions: { addContext },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dataStore = createDataStore()
|
|
@ -0,0 +1,8 @@
|
||||||
|
export { authStore } from "./auth"
|
||||||
|
export { routeStore } from "./routes"
|
||||||
|
export { screenStore } from "./screens"
|
||||||
|
export { builderStore } from "./builder"
|
||||||
|
export { bindingStore } from "./binding"
|
||||||
|
|
||||||
|
// Data stores are layered and duplicated, so it is not a singleton
|
||||||
|
export { createDataStore, dataStore } from "./data"
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
import { push } from "svelte-spa-router"
|
||||||
|
import * as API from "../api"
|
||||||
|
|
||||||
|
const createRouteStore = () => {
|
||||||
|
const initialState = {
|
||||||
|
routes: [],
|
||||||
|
routeParams: {},
|
||||||
|
activeRoute: null,
|
||||||
|
}
|
||||||
|
const store = writable(initialState)
|
||||||
|
|
||||||
|
const fetchRoutes = async () => {
|
||||||
|
const routeConfig = await API.fetchRoutes()
|
||||||
|
let routes = []
|
||||||
|
Object.values(routeConfig.routes).forEach(route => {
|
||||||
|
Object.entries(route.subpaths).forEach(([path, config]) => {
|
||||||
|
routes.push({
|
||||||
|
path,
|
||||||
|
screenId: config.screenId,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
store.update(state => {
|
||||||
|
state.routes = routes
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const setRouteParams = routeParams => {
|
||||||
|
store.update(state => {
|
||||||
|
state.routeParams = routeParams
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const setActiveRoute = route => {
|
||||||
|
store.update(state => {
|
||||||
|
state.activeRoute = route
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const navigate = push
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
actions: { fetchRoutes, navigate, setRouteParams, setActiveRoute },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const routeStore = createRouteStore()
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { writable, derived } from "svelte/store"
|
||||||
|
import { routeStore } from "./routes"
|
||||||
|
import { builderStore } from "./builder"
|
||||||
|
import * as API from "../api"
|
||||||
|
import { getAppId } from "../utils/getAppId"
|
||||||
|
|
||||||
|
const createScreenStore = () => {
|
||||||
|
const config = writable({
|
||||||
|
screens: [],
|
||||||
|
page: {},
|
||||||
|
})
|
||||||
|
const store = derived(
|
||||||
|
[config, routeStore, builderStore],
|
||||||
|
([$config, $routeStore, $builderStore]) => {
|
||||||
|
let page
|
||||||
|
let activeScreen
|
||||||
|
if ($builderStore.inBuilder) {
|
||||||
|
// Use builder defined definitions if inside the builder preview
|
||||||
|
page = $builderStore.page
|
||||||
|
activeScreen = $builderStore.screen
|
||||||
|
} else {
|
||||||
|
// Otherwise find the correct screen by matching the current route
|
||||||
|
page = $config.page
|
||||||
|
const { screens } = $config
|
||||||
|
if (screens.length === 1) {
|
||||||
|
activeScreen = screens[0]
|
||||||
|
} else {
|
||||||
|
activeScreen = screens.find(
|
||||||
|
screen => screen.routing.route === $routeStore.activeRoute
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { page, activeScreen }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const fetchScreens = async () => {
|
||||||
|
const appDefinition = await API.fetchAppDefinition(getAppId())
|
||||||
|
config.set({
|
||||||
|
screens: appDefinition.screens,
|
||||||
|
page: appDefinition.page,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
actions: { fetchScreens },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const screenStore = createScreenStore()
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { enrichDataBinding } from "./enrichDataBinding"
|
||||||
|
import { routeStore } from "../store"
|
||||||
|
import { saveRow, deleteRow } from "../api"
|
||||||
|
|
||||||
|
const saveRowHandler = async (action, context) => {
|
||||||
|
let draft = context[`${action.parameters.contextPath}_draft`]
|
||||||
|
if (action.parameters.fields) {
|
||||||
|
Object.entries(action.parameters.fields).forEach(([key, entry]) => {
|
||||||
|
draft[key] = enrichDataBinding(entry.value, context)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await saveRow(draft)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteRowHandler = async (action, context) => {
|
||||||
|
const { tableId, revId, rowId } = action.parameters
|
||||||
|
await deleteRow({
|
||||||
|
tableId: enrichDataBinding(tableId, context),
|
||||||
|
rowId: enrichDataBinding(rowId, context),
|
||||||
|
revId: enrichDataBinding(revId, context),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigationHandler = action => {
|
||||||
|
routeStore.actions.navigate(action.parameters.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlerMap = {
|
||||||
|
["Save Row"]: saveRowHandler,
|
||||||
|
["Delete Row"]: deleteRowHandler,
|
||||||
|
["Navigate To"]: navigationHandler,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses an array of actions and returns a function which will execute the
|
||||||
|
* actions in the current context.
|
||||||
|
*/
|
||||||
|
export const enrichButtonActions = (actions, context) => {
|
||||||
|
const handlers = actions.map(def => handlerMap[def["##eventHandlerType"]])
|
||||||
|
return async () => {
|
||||||
|
for (let i = 0; i < handlers.length; i++) {
|
||||||
|
await handlers[i](actions[i], context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { enrichDataBindings } from "./enrichDataBinding"
|
||||||
|
import { enrichButtonActions } from "./buttonActions"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enriches component props.
|
||||||
|
* Data bindings are enriched, and button actions are enriched.
|
||||||
|
*/
|
||||||
|
export const enrichProps = (props, dataContexts, dataBindings) => {
|
||||||
|
// Exclude all private props that start with an underscore
|
||||||
|
let validProps = {}
|
||||||
|
Object.entries(props)
|
||||||
|
.filter(([name]) => !name.startsWith("_"))
|
||||||
|
.forEach(([key, value]) => {
|
||||||
|
validProps[key] = value
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create context of all bindings and data contexts
|
||||||
|
// Duplicate the closest context as "data" which the builder requires
|
||||||
|
const context = {
|
||||||
|
...dataContexts,
|
||||||
|
...dataBindings,
|
||||||
|
data: dataContexts[dataContexts.closestComponentId],
|
||||||
|
data_draft: dataContexts[`${dataContexts.closestComponentId}_draft`],
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enrich all data bindings in top level props
|
||||||
|
let enrichedProps = enrichDataBindings(validProps, context)
|
||||||
|
|
||||||
|
// Enrich button actions if they exist
|
||||||
|
if (props._component.endsWith("/button") && enrichedProps.onClick) {
|
||||||
|
enrichedProps.onClick = enrichButtonActions(enrichedProps.onClick, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
return enrichedProps
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
import mustache from "mustache"
|
||||||
|
|
||||||
|
// this is a much more liberal version of mustache's escape function
|
||||||
|
// ...just ignoring < and > to prevent tags from user input
|
||||||
|
// original version here https://github.com/janl/mustache.js/blob/4b7908f5c9fec469a11cfaed2f2bed23c84e1c5c/mustache.js#L78
|
||||||
|
const entityMap = {
|
||||||
|
"<": "<",
|
||||||
|
">": ">",
|
||||||
|
}
|
||||||
|
mustache.escape = text => {
|
||||||
|
if (text == null || typeof text !== "string") {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
return text.replace(/[<>]/g, function fromEntityMap(s) {
|
||||||
|
return entityMap[s] || s
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regex to test inputs with to see if they are likely candidates for mustache
|
||||||
|
const looksLikeMustache = /{{.*}}/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enriches a given input with a row from the database.
|
||||||
|
*/
|
||||||
|
export const enrichDataBinding = (input, context) => {
|
||||||
|
// Only accept string inputs
|
||||||
|
if (!input || typeof input !== "string") {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
// Do a fast regex check if this looks like a mustache string
|
||||||
|
if (!looksLikeMustache.test(input)) {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
return mustache.render(input, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enriches each prop in a props object
|
||||||
|
*/
|
||||||
|
export const enrichDataBindings = (props, context) => {
|
||||||
|
let enrichedProps = {}
|
||||||
|
Object.entries(props).forEach(([key, value]) => {
|
||||||
|
enrichedProps[key] = enrichDataBinding(value, context)
|
||||||
|
})
|
||||||
|
return enrichedProps
|
||||||
|
}
|
|
@ -9,6 +9,9 @@ function confirmAppId(possibleAppId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function tryGetFromCookie({ cookies }) {
|
function tryGetFromCookie({ cookies }) {
|
||||||
|
if (!cookies) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
const cookie = cookies
|
const cookie = cookies
|
||||||
.split(COOKIE_SEPARATOR)
|
.split(COOKIE_SEPARATOR)
|
||||||
.find(cookie => cookie.trim().startsWith("budibase:currentapp"))
|
.find(cookie => cookie.trim().startsWith("budibase:currentapp"))
|
||||||
|
@ -30,7 +33,7 @@ function tryGetFromSubdomain() {
|
||||||
return confirmAppId(appId)
|
return confirmAppId(appId)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAppId = cookies => {
|
export const getAppId = (cookies = window.document.cookie) => {
|
||||||
const functions = [tryGetFromSubdomain, tryGetFromPath, tryGetFromCookie]
|
const functions = [tryGetFromSubdomain, tryGetFromPath, tryGetFromCookie]
|
||||||
// try getting the app Id in order
|
// try getting the app Id in order
|
||||||
let appId
|
let appId
|
||||||
|
@ -42,5 +45,3 @@ export const getAppId = cookies => {
|
||||||
}
|
}
|
||||||
return appId
|
return appId
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAppIdFromPath = getAppId
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
/**
|
||||||
|
* Helper to build a CSS string from a style object
|
||||||
|
*/
|
||||||
|
const buildStyleString = (styles, selected) => {
|
||||||
|
let str = ""
|
||||||
|
if (selected) {
|
||||||
|
styles.border = "2px solid #0055ff !important"
|
||||||
|
}
|
||||||
|
Object.entries(styles).forEach(([style, value]) => {
|
||||||
|
if (style && value != null) {
|
||||||
|
str += `${style}: ${value}; `
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Svelte action to apply correct component styles.
|
||||||
|
*/
|
||||||
|
export const styleable = (node, styles = {}) => {
|
||||||
|
let applyNormalStyles
|
||||||
|
let applyHoverStyles
|
||||||
|
|
||||||
|
// Creates event listeners and applies initial styles
|
||||||
|
const setupStyles = newStyles => {
|
||||||
|
const selected = newStyles.selected
|
||||||
|
const normalStyles = newStyles.normal || {}
|
||||||
|
const hoverStyles = {
|
||||||
|
...normalStyles,
|
||||||
|
...newStyles.hover,
|
||||||
|
}
|
||||||
|
|
||||||
|
applyNormalStyles = () => {
|
||||||
|
node.style = buildStyleString(normalStyles, selected)
|
||||||
|
}
|
||||||
|
|
||||||
|
applyHoverStyles = () => {
|
||||||
|
node.style = buildStyleString(hoverStyles, selected)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add listeners to toggle hover styles
|
||||||
|
node.addEventListener("mouseover", applyHoverStyles)
|
||||||
|
node.addEventListener("mouseout", applyNormalStyles)
|
||||||
|
|
||||||
|
// Apply initial normal styles
|
||||||
|
applyNormalStyles()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removes the current event listeners
|
||||||
|
const removeListeners = () => {
|
||||||
|
node.removeEventListener("mouseover", applyHoverStyles)
|
||||||
|
node.removeEventListener("mouseout", applyNormalStyles)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply initial styles
|
||||||
|
setupStyles(styles)
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Clean up old listeners and apply new ones on update
|
||||||
|
update: newStyles => {
|
||||||
|
removeListeners()
|
||||||
|
setupStyles(newStyles)
|
||||||
|
},
|
||||||
|
// Clean up listeners when component is destroyed
|
||||||
|
destroy: () => {
|
||||||
|
removeListeners()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,209 +0,0 @@
|
||||||
import { load, makePage, makeScreen } from "./testAppDef"
|
|
||||||
|
|
||||||
describe("binding", () => {
|
|
||||||
|
|
||||||
|
|
||||||
it("should bind to data in context", async () => {
|
|
||||||
const { dom } = await load(
|
|
||||||
makePage({
|
|
||||||
_component: "testlib/div",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "##builtin/screenslot",
|
|
||||||
text: "header one",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
makeScreen("/", {
|
|
||||||
_component: "testlib/list",
|
|
||||||
data: dataArray,
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "testlib/h1",
|
|
||||||
text: "{{data.name}}",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
const rootDiv = dom.window.document.body.children[0]
|
|
||||||
expect(rootDiv.children.length).toBe(1)
|
|
||||||
|
|
||||||
const screenRoot = rootDiv.children[0]
|
|
||||||
|
|
||||||
expect(screenRoot.children[0].children.length).toBe(2)
|
|
||||||
expect(screenRoot.children[0].children[0].innerText).toBe(dataArray[0].name)
|
|
||||||
expect(screenRoot.children[0].children[1].innerText).toBe(dataArray[1].name)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should bind to input in root", async () => {
|
|
||||||
const { dom } = await load(
|
|
||||||
makePage({
|
|
||||||
_component: "testlib/div",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "##builtin/screenslot",
|
|
||||||
text: "header one",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
makeScreen("/", {
|
|
||||||
_component: "testlib/div",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "testlib/h1",
|
|
||||||
text: "{{inputid.value}}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_id: "inputid",
|
|
||||||
_component: "testlib/input",
|
|
||||||
value: "hello"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
const rootDiv = dom.window.document.body.children[0]
|
|
||||||
expect(rootDiv.children.length).toBe(1)
|
|
||||||
|
|
||||||
const screenRoot = rootDiv.children[0]
|
|
||||||
|
|
||||||
expect(screenRoot.children[0].children.length).toBe(2)
|
|
||||||
expect(screenRoot.children[0].children[0].innerText).toBe("hello")
|
|
||||||
|
|
||||||
// change value of input
|
|
||||||
const input = dom.window.document.getElementsByClassName("input-inputid")[0]
|
|
||||||
|
|
||||||
changeInputValue(dom, input, "new value")
|
|
||||||
expect(screenRoot.children[0].children[0].innerText).toBe("new value")
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should bind to input in context", async () => {
|
|
||||||
const { dom } = await load(
|
|
||||||
makePage({
|
|
||||||
_component: "testlib/div",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "##builtin/screenslot",
|
|
||||||
text: "header one",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
makeScreen("/", {
|
|
||||||
_component: "testlib/list",
|
|
||||||
data: dataArray,
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "testlib/h1",
|
|
||||||
text: "{{inputid.value}}",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_id: "inputid",
|
|
||||||
_component: "testlib/input",
|
|
||||||
value: "hello"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
const rootDiv = dom.window.document.body.children[0]
|
|
||||||
expect(rootDiv.children.length).toBe(1)
|
|
||||||
|
|
||||||
const screenRoot = rootDiv.children[0]
|
|
||||||
expect(screenRoot.children[0].children.length).toBe(4)
|
|
||||||
|
|
||||||
const firstHeader = screenRoot.children[0].children[0]
|
|
||||||
const firstInput = screenRoot.children[0].children[1]
|
|
||||||
const secondHeader = screenRoot.children[0].children[2]
|
|
||||||
const secondInput = screenRoot.children[0].children[3]
|
|
||||||
|
|
||||||
expect(firstHeader.innerText).toBe("hello")
|
|
||||||
expect(secondHeader.innerText).toBe("hello")
|
|
||||||
|
|
||||||
changeInputValue(dom, firstInput, "first input value")
|
|
||||||
expect(firstHeader.innerText).toBe("first input value")
|
|
||||||
|
|
||||||
changeInputValue(dom, secondInput, "second input value")
|
|
||||||
expect(secondHeader.innerText).toBe("second input value")
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should bind contextual component, to input in root context", async () => {
|
|
||||||
const { dom } = await load(
|
|
||||||
makePage({
|
|
||||||
_component: "testlib/div",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "##builtin/screenslot",
|
|
||||||
text: "header one",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
makeScreen("/", {
|
|
||||||
_component: "testlib/div",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_id: "inputid",
|
|
||||||
_component: "testlib/input",
|
|
||||||
value: "hello"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_component: "testlib/list",
|
|
||||||
data: dataArray,
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "testlib/h1",
|
|
||||||
text: "{{parent.inputid.value}}",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
const rootDiv = dom.window.document.body.children[0]
|
|
||||||
expect(rootDiv.children.length).toBe(1)
|
|
||||||
|
|
||||||
const screenRoot = rootDiv.children[0]
|
|
||||||
expect(screenRoot.children[0].children.length).toBe(2)
|
|
||||||
|
|
||||||
const input = screenRoot.children[0].children[0]
|
|
||||||
|
|
||||||
const firstHeader = screenRoot.children[0].children[1].children[0]
|
|
||||||
const secondHeader = screenRoot.children[0].children[1].children[0]
|
|
||||||
|
|
||||||
expect(firstHeader.innerText).toBe("hello")
|
|
||||||
expect(secondHeader.innerText).toBe("hello")
|
|
||||||
|
|
||||||
changeInputValue(dom, input, "new input value")
|
|
||||||
expect(firstHeader.innerText).toBe("new input value")
|
|
||||||
expect(secondHeader.innerText).toBe("new input value")
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
const changeInputValue = (dom, input, newValue) => {
|
|
||||||
var event = new dom.window.Event("change")
|
|
||||||
input.value = newValue
|
|
||||||
input.dispatchEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
const dataArray = [
|
|
||||||
{
|
|
||||||
name: "katherine",
|
|
||||||
age: 30,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "steve",
|
|
||||||
age: 41,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
|
|
@ -1,172 +0,0 @@
|
||||||
import { load, makePage, makeScreen } from "./testAppDef"
|
|
||||||
|
|
||||||
describe("initialiseApp", () => {
|
|
||||||
it("should populate simple div with initial props", async () => {
|
|
||||||
const { dom } = await load(
|
|
||||||
makePage({
|
|
||||||
_component: "testlib/div",
|
|
||||||
className: "my-test-class",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(dom.window.document.body.children.length).toBe(1)
|
|
||||||
const child = dom.window.document.body.children[0]
|
|
||||||
expect(child.className.includes("my-test-class")).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should populate child component with props", async () => {
|
|
||||||
const { dom } = await load(
|
|
||||||
makePage({
|
|
||||||
_component: "testlib/div",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "testlib/h1",
|
|
||||||
text: "header one",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_component: "testlib/h1",
|
|
||||||
text: "header two",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const rootDiv = dom.window.document.body.children[0]
|
|
||||||
|
|
||||||
expect(rootDiv.children.length).toBe(2)
|
|
||||||
expect(rootDiv.children[0].tagName).toBe("H1")
|
|
||||||
expect(rootDiv.children[0].innerText).toBe("header one")
|
|
||||||
expect(rootDiv.children[1].tagName).toBe("H1")
|
|
||||||
expect(rootDiv.children[1].innerText).toBe("header two")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should append children when told to do so", async () => {
|
|
||||||
const { dom } = await load(
|
|
||||||
makePage({
|
|
||||||
_component: "testlib/div",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "testlib/h1",
|
|
||||||
text: "header one",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_component: "testlib/h1",
|
|
||||||
text: "header two",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
append: true,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const rootDiv = dom.window.document.body.children[0]
|
|
||||||
|
|
||||||
expect(rootDiv.children.length).toBe(3)
|
|
||||||
expect(rootDiv.children[0].tagName).toBe("DIV")
|
|
||||||
expect(rootDiv.children[0].className).toBe("default-child")
|
|
||||||
expect(rootDiv.children[1].tagName).toBe("H1")
|
|
||||||
expect(rootDiv.children[1].innerText).toBe("header one")
|
|
||||||
expect(rootDiv.children[2].tagName).toBe("H1")
|
|
||||||
expect(rootDiv.children[2].innerText).toBe("header two")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should populate page with correct screen", async () => {
|
|
||||||
const { dom } = await load(
|
|
||||||
makePage({
|
|
||||||
_component: "testlib/div",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "##builtin/screenslot",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
makeScreen("/", {
|
|
||||||
_component: "testlib/div",
|
|
||||||
className: "screen-class",
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
const rootDiv = dom.window.document.body.children[0]
|
|
||||||
|
|
||||||
expect(rootDiv.children.length).toBe(1)
|
|
||||||
expect(rootDiv.children[0].children.length).toBe(1)
|
|
||||||
expect(
|
|
||||||
rootDiv.children[0].children[0].className.includes("screen-class")
|
|
||||||
).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should populate screen with children", async () => {
|
|
||||||
const { dom } = await load(
|
|
||||||
makePage({
|
|
||||||
_component: "testlib/div",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "##builtin/screenslot",
|
|
||||||
text: "header one",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
makeScreen("/", {
|
|
||||||
_component: "testlib/div",
|
|
||||||
className: "screen-class",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "testlib/h1",
|
|
||||||
text: "header one",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_component: "testlib/h1",
|
|
||||||
text: "header two",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
const rootDiv = dom.window.document.body.children[0]
|
|
||||||
expect(rootDiv.children.length).toBe(1)
|
|
||||||
|
|
||||||
const screenRoot = rootDiv.children[0]
|
|
||||||
|
|
||||||
expect(screenRoot.children.length).toBe(1)
|
|
||||||
expect(screenRoot.children[0].children.length).toBe(2)
|
|
||||||
expect(screenRoot.children[0].children[0].innerText).toBe("header one")
|
|
||||||
expect(screenRoot.children[0].children[1].innerText).toBe("header two")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should repeat elements that pass an array of contexts", async () => {
|
|
||||||
const { dom } = await load(
|
|
||||||
makePage({
|
|
||||||
_component: "testlib/div",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "##builtin/screenslot",
|
|
||||||
text: "header one",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
makeScreen("/", {
|
|
||||||
_component: "testlib/list",
|
|
||||||
data: [1,2,3,4],
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "testlib/h1",
|
|
||||||
text: "header",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
const rootDiv = dom.window.document.body.children[0]
|
|
||||||
expect(rootDiv.children.length).toBe(1)
|
|
||||||
|
|
||||||
const screenRoot = rootDiv.children[0]
|
|
||||||
|
|
||||||
expect(screenRoot.children[0].children.length).toBe(4)
|
|
||||||
expect(screenRoot.children[0].children[0].innerText).toBe("header")
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,174 +0,0 @@
|
||||||
import { load, makePage, makeScreen, walkComponentTree } from "./testAppDef"
|
|
||||||
import { isScreenSlot } from "../src/render/builtinComponents"
|
|
||||||
jest.mock("../src/render/getAppId", () => ({
|
|
||||||
getAppId: () => "TEST_APP_ID"
|
|
||||||
}))
|
|
||||||
|
|
||||||
describe("screenRouting", () => {
|
|
||||||
it("should load correct screen, for initial URL", async () => {
|
|
||||||
const { page, screens } = pageWith3Screens()
|
|
||||||
const { dom } = await load(page, screens, "/screen2")
|
|
||||||
|
|
||||||
const rootDiv = dom.window.document.body.children[0]
|
|
||||||
expect(rootDiv.children.length).toBe(1)
|
|
||||||
|
|
||||||
const screenRoot = rootDiv.children[0]
|
|
||||||
|
|
||||||
expect(screenRoot.children.length).toBe(1)
|
|
||||||
expect(screenRoot.children[0].children.length).toBe(1)
|
|
||||||
expect(screenRoot.children[0].children[0].innerText).toBe("screen 2")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should load correct screen, for initial URL, when appRootPath is something", async () => {
|
|
||||||
const { page, screens } = pageWith3Screens()
|
|
||||||
const { dom } = await load(page, screens, "/TEST_APP_ID/screen2", "127.0.0.1")
|
|
||||||
|
|
||||||
const rootDiv = dom.window.document.body.children[0]
|
|
||||||
expect(rootDiv.children.length).toBe(1)
|
|
||||||
|
|
||||||
const screenRoot = rootDiv.children[0]
|
|
||||||
|
|
||||||
expect(screenRoot.children.length).toBe(1)
|
|
||||||
expect(screenRoot.children[0].children.length).toBe(1)
|
|
||||||
expect(screenRoot.children[0].children[0].innerText).toBe("screen 2")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should be able to route to the correct screen", async () => {
|
|
||||||
const { page, screens } = pageWith3Screens()
|
|
||||||
const { dom, app } = await load(page, screens, "/screen2")
|
|
||||||
|
|
||||||
app.routeTo()("/screen3")
|
|
||||||
const rootDiv = dom.window.document.body.children[0]
|
|
||||||
expect(rootDiv.children.length).toBe(1)
|
|
||||||
|
|
||||||
const screenRoot = rootDiv.children[0]
|
|
||||||
|
|
||||||
expect(screenRoot.children.length).toBe(1)
|
|
||||||
expect(screenRoot.children[0].children.length).toBe(1)
|
|
||||||
expect(screenRoot.children[0].children[0].innerText).toBe("screen 3")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should be able to route to the correct screen, when appRootPath is something", async () => {
|
|
||||||
const { page, screens } = pageWith3Screens()
|
|
||||||
const { dom, app } = await load(
|
|
||||||
page,
|
|
||||||
screens,
|
|
||||||
"/TEST_APP_ID/screen2",
|
|
||||||
"127.0.0.1"
|
|
||||||
)
|
|
||||||
|
|
||||||
app.routeTo()("/screen3")
|
|
||||||
const rootDiv = dom.window.document.body.children[0]
|
|
||||||
expect(rootDiv.children.length).toBe(1)
|
|
||||||
|
|
||||||
const screenRoot = rootDiv.children[0]
|
|
||||||
|
|
||||||
expect(screenRoot.children.length).toBe(1)
|
|
||||||
expect(screenRoot.children[0].children.length).toBe(1)
|
|
||||||
expect(screenRoot.children[0].children[0].innerText).toBe("screen 3")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should destroy and unsubscribe all components on a screen whe screen is changed", async () => {
|
|
||||||
const { page, screens } = pageWith3Screens()
|
|
||||||
const { app } = await load(page, screens, "/screen2")
|
|
||||||
|
|
||||||
const nodes = createTrackerNodes(app)
|
|
||||||
|
|
||||||
app.routeTo()("/screen3")
|
|
||||||
|
|
||||||
expect(nodes.length > 0).toBe(true)
|
|
||||||
expect(
|
|
||||||
nodes.some(n => n.isDestroyed === false && isUnderScreenSlot(n.node))
|
|
||||||
).toBe(false)
|
|
||||||
expect(
|
|
||||||
nodes.some(n => n.isUnsubscribed === false && isUnderScreenSlot(n.node))
|
|
||||||
).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should not destroy and unsubscribe page and screenslot components when screen is changed", async () => {
|
|
||||||
const { page, screens } = pageWith3Screens()
|
|
||||||
const { app } = await load(page, screens, "/screen2")
|
|
||||||
|
|
||||||
const nodes = createTrackerNodes(app)
|
|
||||||
|
|
||||||
app.routeTo()("/screen3")
|
|
||||||
|
|
||||||
expect(nodes.length > 0).toBe(true)
|
|
||||||
expect(
|
|
||||||
nodes.some(n => n.isDestroyed === true && !isUnderScreenSlot(n.node))
|
|
||||||
).toBe(false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const createTrackerNodes = app => {
|
|
||||||
const nodes = []
|
|
||||||
walkComponentTree(app.rootNode(), n => {
|
|
||||||
if (!n.component) return
|
|
||||||
const tracker = { node: n, isDestroyed: false, isUnsubscribed: false }
|
|
||||||
const _destroy = n.component.$destroy
|
|
||||||
n.component.$destroy = () => {
|
|
||||||
_destroy()
|
|
||||||
tracker.isDestroyed = true
|
|
||||||
}
|
|
||||||
const _unsubscribe = n.unsubscribe
|
|
||||||
if (!_unsubscribe) {
|
|
||||||
tracker.isUnsubscribed = undefined
|
|
||||||
} else {
|
|
||||||
n.unsubscribe = () => {
|
|
||||||
_unsubscribe()
|
|
||||||
tracker.isUnsubscribed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nodes.push(tracker)
|
|
||||||
})
|
|
||||||
return nodes
|
|
||||||
}
|
|
||||||
|
|
||||||
const isUnderScreenSlot = node =>
|
|
||||||
node.parentNode &&
|
|
||||||
(isScreenSlot(node.parentNode.props._component) ||
|
|
||||||
isUnderScreenSlot(node.parentNode))
|
|
||||||
|
|
||||||
const pageWith3Screens = () => ({
|
|
||||||
page: makePage({
|
|
||||||
_component: "testlib/div",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "##builtin/screenslot",
|
|
||||||
text: "header one",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
screens: [
|
|
||||||
makeScreen("/", {
|
|
||||||
_component: "testlib/div",
|
|
||||||
className: "screen-class",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "testlib/h1",
|
|
||||||
text: "screen 1",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
makeScreen("/screen2", {
|
|
||||||
_component: "testlib/div",
|
|
||||||
className: "screen-class",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "testlib/h1",
|
|
||||||
text: "screen 2",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
makeScreen("/screen3", {
|
|
||||||
_component: "testlib/div",
|
|
||||||
className: "screen-class",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "testlib/h1",
|
|
||||||
text: "screen 3",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
})
|
|
|
@ -1,244 +0,0 @@
|
||||||
import jsdom, { JSDOM } from "jsdom"
|
|
||||||
import { loadBudibase } from "../src/index"
|
|
||||||
|
|
||||||
export const APP_ID = "TEST_APP_ID"
|
|
||||||
|
|
||||||
export const load = async (page, screens, url, host = "test.com") => {
|
|
||||||
screens = screens || []
|
|
||||||
url = url || "/"
|
|
||||||
|
|
||||||
const fullUrl = `http://${host}${url}`
|
|
||||||
const cookieJar = new jsdom.CookieJar()
|
|
||||||
const cookie = `${btoa("{}")}.${btoa(`{"appId":"${APP_ID}"}`)}.signature`
|
|
||||||
cookieJar.setCookie(
|
|
||||||
`budibase:${APP_ID}:local=${cookie};domain=${host};path=/`,
|
|
||||||
fullUrl,
|
|
||||||
{
|
|
||||||
looseMode: false,
|
|
||||||
},
|
|
||||||
() => {}
|
|
||||||
)
|
|
||||||
|
|
||||||
const dom = new JSDOM("<!DOCTYPE html><html><body></body><html>", {
|
|
||||||
url: fullUrl,
|
|
||||||
cookieJar,
|
|
||||||
})
|
|
||||||
|
|
||||||
autoAssignIds(page.props)
|
|
||||||
for (let s of screens) {
|
|
||||||
autoAssignIds(s.props)
|
|
||||||
}
|
|
||||||
setAppDef(dom.window, page, screens)
|
|
||||||
addWindowGlobals(dom.window, page, screens, {
|
|
||||||
hierarchy: {},
|
|
||||||
actions: [],
|
|
||||||
triggers: [],
|
|
||||||
})
|
|
||||||
setComponentCodeMeta(page, screens)
|
|
||||||
const app = await loadBudibase({
|
|
||||||
componentLibraries: allLibs(dom.window),
|
|
||||||
window: dom.window,
|
|
||||||
localStorage: createLocalStorage(),
|
|
||||||
})
|
|
||||||
return { dom, app }
|
|
||||||
}
|
|
||||||
|
|
||||||
const addWindowGlobals = (window, page, screens) => {
|
|
||||||
window["##BUDIBASE_FRONTEND_DEFINITION##"] = {
|
|
||||||
page,
|
|
||||||
screens,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makePage = props => ({ props })
|
|
||||||
export const makeScreen = (route, props) => ({
|
|
||||||
props,
|
|
||||||
routing: { route, accessLevelId: "" },
|
|
||||||
})
|
|
||||||
|
|
||||||
export const timeout = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
||||||
|
|
||||||
export const walkComponentTree = (node, action) => {
|
|
||||||
action(node)
|
|
||||||
|
|
||||||
// works for nodes or props
|
|
||||||
const children = node.children || node._children
|
|
||||||
|
|
||||||
if (children) {
|
|
||||||
for (let child of children) {
|
|
||||||
walkComponentTree(child, action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// this happens for real by the builder...
|
|
||||||
// ..this only assigns _ids when missing
|
|
||||||
const autoAssignIds = (props, count = 0) => {
|
|
||||||
if (!props._id) {
|
|
||||||
props._id = `auto_id_${count}`
|
|
||||||
}
|
|
||||||
if (props._children) {
|
|
||||||
for (let child of props._children) {
|
|
||||||
count += 1
|
|
||||||
autoAssignIds(child, count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// any component with an id that include "based_on_store" is
|
|
||||||
// assumed to have code that depends on store value
|
|
||||||
const setComponentCodeMeta = (page, screens) => {
|
|
||||||
const setComponentCodeMeta_single = props => {
|
|
||||||
walkComponentTree(props, c => {
|
|
||||||
if (c._id.indexOf("based_on_store") >= 0) {
|
|
||||||
c._codeMeta = { dependsOnStore: true }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
setComponentCodeMeta_single(page.props)
|
|
||||||
for (let s of screens || []) {
|
|
||||||
setComponentCodeMeta_single(s.props)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const setAppDef = (window, page, screens) => {
|
|
||||||
window["##BUDIBASE_FRONTEND_DEFINITION##"] = {
|
|
||||||
componentLibraries: [],
|
|
||||||
page,
|
|
||||||
screens,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const allLibs = window => ({
|
|
||||||
testlib: maketestlib(window),
|
|
||||||
})
|
|
||||||
|
|
||||||
const createLocalStorage = () => {
|
|
||||||
const data = {}
|
|
||||||
return {
|
|
||||||
getItem: key => data[key],
|
|
||||||
setItem: (key, value) => (data[key] = value),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const maketestlib = window => ({
|
|
||||||
div: function(opts) {
|
|
||||||
const node = window.document.createElement("DIV")
|
|
||||||
const defaultChild = window.document.createElement("DIV")
|
|
||||||
defaultChild.className = "default-child"
|
|
||||||
node.appendChild(defaultChild)
|
|
||||||
|
|
||||||
let currentProps = { ...opts.props }
|
|
||||||
let childNodes = []
|
|
||||||
|
|
||||||
const set = props => {
|
|
||||||
currentProps = Object.assign(currentProps, props)
|
|
||||||
node.className = currentProps.className || ""
|
|
||||||
if (currentProps._children && currentProps._children.length > 0) {
|
|
||||||
if (currentProps.append) {
|
|
||||||
for (let c of childNodes) {
|
|
||||||
node.removeChild(c)
|
|
||||||
}
|
|
||||||
const components = currentProps._bb.attachChildren(node, {
|
|
||||||
hydrate: false,
|
|
||||||
})
|
|
||||||
childNodes = components.map(c => c.component._element)
|
|
||||||
} else {
|
|
||||||
currentProps._bb.attachChildren(node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$destroy = () => opts.target.removeChild(node)
|
|
||||||
|
|
||||||
this.$set = set
|
|
||||||
this._element = node
|
|
||||||
set(opts.props)
|
|
||||||
opts.target.appendChild(node)
|
|
||||||
},
|
|
||||||
|
|
||||||
h1: function(opts) {
|
|
||||||
const node = window.document.createElement("H1")
|
|
||||||
|
|
||||||
let currentProps = { ...opts.props }
|
|
||||||
|
|
||||||
const set = props => {
|
|
||||||
currentProps = Object.assign(currentProps, props)
|
|
||||||
if (currentProps.text) {
|
|
||||||
node.innerText = currentProps.text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$destroy = () => opts.target.removeChild(node)
|
|
||||||
|
|
||||||
this.$set = set
|
|
||||||
this._element = node
|
|
||||||
set(opts.props)
|
|
||||||
opts.target.appendChild(node)
|
|
||||||
},
|
|
||||||
|
|
||||||
button: function(opts) {
|
|
||||||
const node = window.document.createElement("BUTTON")
|
|
||||||
|
|
||||||
let currentProps = { ...opts.props }
|
|
||||||
|
|
||||||
const set = props => {
|
|
||||||
currentProps = Object.assign(currentProps, props)
|
|
||||||
if (currentProps.onClick) {
|
|
||||||
node.addEventListener("click", () => {
|
|
||||||
currentProps._bb.call("onClick")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$destroy = () => opts.target.removeChild(node)
|
|
||||||
|
|
||||||
this.$set = set
|
|
||||||
this._element = node
|
|
||||||
set(opts.props)
|
|
||||||
opts.target.appendChild(node)
|
|
||||||
},
|
|
||||||
|
|
||||||
list: function(opts) {
|
|
||||||
const node = window.document.createElement("DIV")
|
|
||||||
|
|
||||||
let currentProps = { ...opts.props }
|
|
||||||
|
|
||||||
const set = props => {
|
|
||||||
currentProps = Object.assign(currentProps, props)
|
|
||||||
if (currentProps._children && currentProps._children.length > 0) {
|
|
||||||
currentProps._bb.attachChildren(node, {
|
|
||||||
context: currentProps.data || {},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$destroy = () => opts.target.removeChild(node)
|
|
||||||
|
|
||||||
this.$set = set
|
|
||||||
this._element = node
|
|
||||||
set(opts.props)
|
|
||||||
opts.target.appendChild(node)
|
|
||||||
},
|
|
||||||
|
|
||||||
input: function(opts) {
|
|
||||||
const node = window.document.createElement("INPUT")
|
|
||||||
let currentProps = { ...opts.props }
|
|
||||||
|
|
||||||
const set = props => {
|
|
||||||
currentProps = Object.assign(currentProps, props)
|
|
||||||
opts.props._bb.setBinding("value", props.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
node.addEventListener("change", e => {
|
|
||||||
opts.props._bb.setBinding("value", e.target.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.$destroy = () => opts.target.removeChild(node)
|
|
||||||
|
|
||||||
this.$set = set
|
|
||||||
this._element = node
|
|
||||||
set(opts.props)
|
|
||||||
opts.target.appendChild(node)
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -1,16 +0,0 @@
|
||||||
<script>
|
|
||||||
export let _bb
|
|
||||||
export let className = ""
|
|
||||||
|
|
||||||
let containerElement
|
|
||||||
let hasLoaded
|
|
||||||
|
|
||||||
$: {
|
|
||||||
if (containerElement) {
|
|
||||||
_bb.attachChildren(containerElement)
|
|
||||||
hasLoaded = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div bind:this={containerElement} class={className} />
|
|
File diff suppressed because it is too large
Load Diff
|
@ -87,7 +87,7 @@
|
||||||
"pouchdb-all-dbs": "^1.0.2",
|
"pouchdb-all-dbs": "^1.0.2",
|
||||||
"pouchdb-replication-stream": "^1.2.9",
|
"pouchdb-replication-stream": "^1.2.9",
|
||||||
"sanitize-s3-objectkey": "^0.0.1",
|
"sanitize-s3-objectkey": "^0.0.1",
|
||||||
"svelte": "^3.29.4",
|
"svelte": "^3.30.0",
|
||||||
"tar-fs": "^2.1.0",
|
"tar-fs": "^2.1.0",
|
||||||
"to-json-schema": "^0.2.5",
|
"to-json-schema": "^0.2.5",
|
||||||
"uuid": "^3.3.2",
|
"uuid": "^3.3.2",
|
||||||
|
|
|
@ -31,15 +31,13 @@ const MAIN = {
|
||||||
selected: {},
|
selected: {},
|
||||||
},
|
},
|
||||||
_code: "",
|
_code: "",
|
||||||
className: "",
|
|
||||||
onLoad: [],
|
|
||||||
type: "div",
|
type: "div",
|
||||||
_appId: "inst_app_80b_f158d4057d2c4bedb0042d42fda8abaf",
|
_appId: "inst_app_80b_f158d4057d2c4bedb0042d42fda8abaf",
|
||||||
_instanceName: "Header",
|
_instanceName: "Header",
|
||||||
_children: [
|
_children: [
|
||||||
{
|
{
|
||||||
_id: "49e0e519-9e5e-4127-885a-ee6a0a49e2c1",
|
_id: "49e0e519-9e5e-4127-885a-ee6a0a49e2c1",
|
||||||
_component: "@budibase/standard-components/Navigation",
|
_component: "@budibase/standard-components/navigation",
|
||||||
_styles: {
|
_styles: {
|
||||||
normal: {
|
normal: {
|
||||||
"max-width": "1400px",
|
"max-width": "1400px",
|
||||||
|
@ -58,12 +56,6 @@ const MAIN = {
|
||||||
_code: "",
|
_code: "",
|
||||||
logoUrl:
|
logoUrl:
|
||||||
"https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg",
|
"https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg",
|
||||||
title: "",
|
|
||||||
backgroundColor: "",
|
|
||||||
color: "",
|
|
||||||
borderWidth: "",
|
|
||||||
borderColor: "",
|
|
||||||
borderStyle: "",
|
|
||||||
_appId: "inst_cf8ace4_69efc0d72e6f443db2d4c902c14d9394",
|
_appId: "inst_cf8ace4_69efc0d72e6f443db2d4c902c14d9394",
|
||||||
_instanceName: "Navigation",
|
_instanceName: "Navigation",
|
||||||
_children: [
|
_children: [
|
||||||
|
@ -88,11 +80,6 @@ const MAIN = {
|
||||||
url: "/",
|
url: "/",
|
||||||
openInNewTab: false,
|
openInNewTab: false,
|
||||||
text: "Home",
|
text: "Home",
|
||||||
color: "",
|
|
||||||
hoverColor: "",
|
|
||||||
underline: false,
|
|
||||||
fontSize: "",
|
|
||||||
fontFamily: "initial",
|
|
||||||
_appId: "inst_cf8ace4_69efc0d72e6f443db2d4c902c14d9394",
|
_appId: "inst_cf8ace4_69efc0d72e6f443db2d4c902c14d9394",
|
||||||
_instanceName: "Home Link",
|
_instanceName: "Home Link",
|
||||||
_children: [],
|
_children: [],
|
||||||
|
@ -143,8 +130,6 @@ const MAIN = {
|
||||||
selected: {},
|
selected: {},
|
||||||
},
|
},
|
||||||
_code: "",
|
_code: "",
|
||||||
className: "",
|
|
||||||
onLoad: [],
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,13 +166,7 @@ const UNAUTHENTICATED = {
|
||||||
selected: {},
|
selected: {},
|
||||||
},
|
},
|
||||||
_code: "",
|
_code: "",
|
||||||
loginRedirect: "",
|
|
||||||
usernameLabel: "Username",
|
|
||||||
passwordLabel: "Password",
|
|
||||||
loginButtonLabel: "Login",
|
|
||||||
buttonClass: "",
|
|
||||||
_instanceName: "Login",
|
_instanceName: "Login",
|
||||||
inputClass: "",
|
|
||||||
_children: [],
|
_children: [],
|
||||||
title: "Log in to {{ name }}",
|
title: "Log in to {{ name }}",
|
||||||
buttonText: "Log In",
|
buttonText: "Log In",
|
||||||
|
@ -213,8 +192,6 @@ const UNAUTHENTICATED = {
|
||||||
selected: {},
|
selected: {},
|
||||||
},
|
},
|
||||||
_code: "",
|
_code: "",
|
||||||
className: "",
|
|
||||||
onLoad: [],
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,8 +19,6 @@ exports.HOME_SCREEN = {
|
||||||
selected: {},
|
selected: {},
|
||||||
},
|
},
|
||||||
_code: "",
|
_code: "",
|
||||||
className: "",
|
|
||||||
onLoad: [],
|
|
||||||
type: "div",
|
type: "div",
|
||||||
_children: [
|
_children: [
|
||||||
{
|
{
|
||||||
|
@ -35,7 +33,6 @@ exports.HOME_SCREEN = {
|
||||||
selected: {},
|
selected: {},
|
||||||
},
|
},
|
||||||
_code: "",
|
_code: "",
|
||||||
className: "",
|
|
||||||
text: "Welcome to your Budibase App 👋",
|
text: "Welcome to your Budibase App 👋",
|
||||||
type: "h2",
|
type: "h2",
|
||||||
_appId: "inst_cf8ace4_69efc0d72e6f443db2d4c902c14d9394",
|
_appId: "inst_cf8ace4_69efc0d72e6f443db2d4c902c14d9394",
|
||||||
|
@ -61,8 +58,6 @@ exports.HOME_SCREEN = {
|
||||||
selected: {},
|
selected: {},
|
||||||
},
|
},
|
||||||
_code: "",
|
_code: "",
|
||||||
className: "",
|
|
||||||
onLoad: [],
|
|
||||||
type: "div",
|
type: "div",
|
||||||
_appId: "inst_app_2cc_ca3383f896034e9295345c05f7dfca0c",
|
_appId: "inst_app_2cc_ca3383f896034e9295345c05f7dfca0c",
|
||||||
_instanceName: "Video Container",
|
_instanceName: "Video Container",
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,5 +1,4 @@
|
||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
yarn.lock
|
|
||||||
package-lock.json
|
package-lock.json
|
||||||
dist/
|
dist/
|
||||||
|
|
|
@ -13,18 +13,12 @@
|
||||||
"embed": "string"
|
"embed": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Navigation": {
|
"navigation": {
|
||||||
"name": "Navigation",
|
"name": "Navigation",
|
||||||
"description": "A basic header navigation component",
|
"description": "A basic header navigation component",
|
||||||
"children": true,
|
"children": true,
|
||||||
"props": {
|
"props": {
|
||||||
"logoUrl": "string",
|
"logoUrl": "string"
|
||||||
"title": "string",
|
|
||||||
"backgroundColor": "string",
|
|
||||||
"color": "string",
|
|
||||||
"borderWidth": "string",
|
|
||||||
"borderColor": "string",
|
|
||||||
"borderStyle": "string"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"button": {
|
"button": {
|
||||||
|
@ -32,7 +26,6 @@
|
||||||
"description": "an html <button />",
|
"description": "an html <button />",
|
||||||
"props": {
|
"props": {
|
||||||
"text": "string",
|
"text": "string",
|
||||||
"className": "string",
|
|
||||||
"disabled": "bool",
|
"disabled": "bool",
|
||||||
"onClick": "event"
|
"onClick": "event"
|
||||||
},
|
},
|
||||||
|
@ -65,23 +58,8 @@
|
||||||
"name": "Login Control",
|
"name": "Login Control",
|
||||||
"description": "A control that accepts username, password an also handles password resets",
|
"description": "A control that accepts username, password an also handles password resets",
|
||||||
"props": {
|
"props": {
|
||||||
"logo": "asset",
|
"logo": "string",
|
||||||
"loginRedirect": "string",
|
|
||||||
"title": "string",
|
"title": "string",
|
||||||
"usernameLabel": {
|
|
||||||
"type": "string",
|
|
||||||
"default": "Username"
|
|
||||||
},
|
|
||||||
"passwordLabel": {
|
|
||||||
"type": "string",
|
|
||||||
"default": "Password"
|
|
||||||
},
|
|
||||||
"loginButtonLabel": {
|
|
||||||
"type": "string",
|
|
||||||
"default": "Login"
|
|
||||||
},
|
|
||||||
"buttonClass": "string",
|
|
||||||
"inputClass": "string",
|
|
||||||
"buttonText": "string"
|
"buttonText": "string"
|
||||||
},
|
},
|
||||||
"tags": [
|
"tags": [
|
||||||
|
@ -96,7 +74,6 @@
|
||||||
"bindable": "value",
|
"bindable": "value",
|
||||||
"description": "An HTML input",
|
"description": "An HTML input",
|
||||||
"props": {
|
"props": {
|
||||||
"value": "string",
|
|
||||||
"type": {
|
"type": {
|
||||||
"type": "options",
|
"type": "options",
|
||||||
"options": [
|
"options": [
|
||||||
|
@ -122,36 +99,15 @@
|
||||||
"week"
|
"week"
|
||||||
],
|
],
|
||||||
"default": "text"
|
"default": "text"
|
||||||
},
|
}
|
||||||
"onChange": "event",
|
|
||||||
"className": "string"
|
|
||||||
},
|
},
|
||||||
"tags": [
|
"tags": [
|
||||||
"form"
|
"form"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"select": {
|
|
||||||
"name": "Select",
|
|
||||||
"bindable": "value",
|
|
||||||
"description": "An HTML <select> (dropdown)",
|
|
||||||
"props": {
|
|
||||||
"value": "string",
|
|
||||||
"className": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"option": {
|
|
||||||
"name": "Option",
|
|
||||||
"description": "An HTML <option>, to be used with <select>",
|
|
||||||
"children": false,
|
|
||||||
"props": {
|
|
||||||
"value": "string",
|
|
||||||
"text": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"text": {
|
"text": {
|
||||||
"name": "Text",
|
"name": "Text",
|
||||||
"description": "stylable block of text",
|
"description": "stylable block of text",
|
||||||
"children": false,
|
|
||||||
"props": {
|
"props": {
|
||||||
"text": "string",
|
"text": "string",
|
||||||
"type": {
|
"type": {
|
||||||
|
@ -164,16 +120,6 @@
|
||||||
"container"
|
"container"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"textfield": {
|
|
||||||
"name": "Textfield",
|
|
||||||
"description": "A component that allows the user to input text.",
|
|
||||||
"props": {
|
|
||||||
"label": "string",
|
|
||||||
"type": "string",
|
|
||||||
"value": "string",
|
|
||||||
"onchange": "event"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"richtext": {
|
"richtext": {
|
||||||
"name": "Rich Text",
|
"name": "Rich Text",
|
||||||
"description": "A component that allows the user to enter long form text.",
|
"description": "A component that allows the user to enter long form text.",
|
||||||
|
@ -181,28 +127,6 @@
|
||||||
"value": "string"
|
"value": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"checkbox": {
|
|
||||||
"name": "Checkbox",
|
|
||||||
"bindable": "value",
|
|
||||||
"description": "A selectable checkbox component",
|
|
||||||
"props": {
|
|
||||||
"label": "string",
|
|
||||||
"checked": "bool",
|
|
||||||
"value": "string",
|
|
||||||
"onchange": "event"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"radiobutton": {
|
|
||||||
"name": "Radiobutton",
|
|
||||||
"bindable": "value",
|
|
||||||
"description": "A selectable radiobutton component",
|
|
||||||
"props": {
|
|
||||||
"label": "string",
|
|
||||||
"checked": "bool",
|
|
||||||
"value": "string",
|
|
||||||
"onchange": "event"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"icon": {
|
"icon": {
|
||||||
"description": "A HTML icon tag",
|
"description": "A HTML icon tag",
|
||||||
"props": {
|
"props": {
|
||||||
|
@ -217,17 +141,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"datatable": {
|
|
||||||
"description": "an HTML table that fetches data from a table or view and displays it.",
|
|
||||||
"data": true,
|
|
||||||
"props": {
|
|
||||||
"datasource": "tables",
|
|
||||||
"stripeColor": "string",
|
|
||||||
"borderColor": "string",
|
|
||||||
"backgroundColor": "string",
|
|
||||||
"color": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"datagrid": {
|
"datagrid": {
|
||||||
"name": "Grid",
|
"name": "Grid",
|
||||||
"description": "a datagrid component with functionality to add, remove and edit rows.",
|
"description": "a datagrid component with functionality to add, remove and edit rows.",
|
||||||
|
@ -269,21 +182,6 @@
|
||||||
"data": true,
|
"data": true,
|
||||||
"props": {}
|
"props": {}
|
||||||
},
|
},
|
||||||
"datalist": {
|
|
||||||
"description": "A configurable data list that attaches to your backend tables.",
|
|
||||||
"data": true,
|
|
||||||
"props": {
|
|
||||||
"table": "tables",
|
|
||||||
"layout": {
|
|
||||||
"type": "options",
|
|
||||||
"default": "list",
|
|
||||||
"options": [
|
|
||||||
"list",
|
|
||||||
"grid"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"list": {
|
"list": {
|
||||||
"name": "Repeater",
|
"name": "Repeater",
|
||||||
"description": "A configurable data list that attaches to your backend tables.",
|
"description": "A configurable data list that attaches to your backend tables.",
|
||||||
|
@ -696,36 +594,13 @@
|
||||||
"props": {
|
"props": {
|
||||||
"url": "string",
|
"url": "string",
|
||||||
"openInNewTab": "bool",
|
"openInNewTab": "bool",
|
||||||
"text": "string",
|
"text": "string"
|
||||||
"color": "string",
|
|
||||||
"hoverColor": "string",
|
|
||||||
"underline": "bool",
|
|
||||||
"fontSize": "string",
|
|
||||||
"fontFamily": {
|
|
||||||
"type": "options",
|
|
||||||
"default": "initial",
|
|
||||||
"styleBindingProperty": "font-family",
|
|
||||||
"options": [
|
|
||||||
"initial",
|
|
||||||
"Times New Roman",
|
|
||||||
"Georgia",
|
|
||||||
"Arial",
|
|
||||||
"Arial Black",
|
|
||||||
"Comic Sans MS",
|
|
||||||
"Impact",
|
|
||||||
"Lucida Sans Unicode"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"image": {
|
"image": {
|
||||||
"description": "an HTML <img> tag",
|
"description": "an HTML <img> tag",
|
||||||
"props": {
|
"props": {
|
||||||
"url": "string",
|
"url": "string"
|
||||||
"className": "string",
|
|
||||||
"description": "string",
|
|
||||||
"height": "string",
|
|
||||||
"width": "string"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"container": {
|
"container": {
|
||||||
|
@ -733,8 +608,6 @@
|
||||||
"children": true,
|
"children": true,
|
||||||
"description": "An element that contains and lays out other elements. e.g. <div>, <header> etc",
|
"description": "An element that contains and lays out other elements. e.g. <div>, <header> etc",
|
||||||
"props": {
|
"props": {
|
||||||
"className": "string",
|
|
||||||
"onLoad": "event",
|
|
||||||
"type": {
|
"type": {
|
||||||
"type": "options",
|
"type": "options",
|
||||||
"options": [
|
"options": [
|
||||||
|
@ -766,7 +639,6 @@
|
||||||
"name": "Heading",
|
"name": "Heading",
|
||||||
"description": "An HTML H1 - H6 tag",
|
"description": "An HTML H1 - H6 tag",
|
||||||
"props": {
|
"props": {
|
||||||
"className": "string",
|
|
||||||
"text": "string",
|
"text": "string",
|
||||||
"type": {
|
"type": {
|
||||||
"type": "options",
|
"type": "options",
|
||||||
|
@ -782,19 +654,5 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tags": []
|
"tags": []
|
||||||
},
|
|
||||||
"thead": {
|
|
||||||
"name": "Table Head",
|
|
||||||
"description": "an HTML <thead> tab",
|
|
||||||
"props": {
|
|
||||||
"className": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tbody": {
|
|
||||||
"name": "Table Body",
|
|
||||||
"description": "an HTML <tbody> tab",
|
|
||||||
"props": {
|
|
||||||
"className": "string"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,32 +1,29 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/standard-components",
|
"name": "@budibase/standard-components",
|
||||||
"svelte": "src/index.svelte",
|
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.js",
|
"module": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rollup -c",
|
"build": "rollup -c",
|
||||||
"prepublishOnly": "npm run build",
|
"prepublishOnly": "npm run build",
|
||||||
"postpublish": "node scripts/deploy.js",
|
"postpublish": "node scripts/deploy.js",
|
||||||
"testbuild": "rollup -w -c rollup.testconfig.js",
|
|
||||||
"dev": "run-p start:dev testbuild",
|
"dev": "run-p start:dev testbuild",
|
||||||
"start:dev": "sirv public --single --dev",
|
"start:dev": "sirv public --single --dev",
|
||||||
"dev:builder": "rollup -cw"
|
"dev:builder": "rollup -cw"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@budibase/client": "^0.3.8",
|
"@rollup/plugin-alias": "^3.1.1",
|
||||||
"@rollup/plugin-commonjs": "^11.1.0",
|
"@rollup/plugin-commonjs": "^16.0.0",
|
||||||
|
"@rollup/plugin-json": "^4.1.0",
|
||||||
|
"@rollup/plugin-node-resolve": "^10.0.0",
|
||||||
|
"@rollup/plugin-replace": "^2.3.4",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
"rollup": "^2.11.2",
|
"rollup": "^2.11.2",
|
||||||
"rollup-plugin-commonjs": "^10.0.2",
|
|
||||||
"rollup-plugin-json": "^4.0.0",
|
|
||||||
"rollup-plugin-livereload": "^1.0.1",
|
"rollup-plugin-livereload": "^1.0.1",
|
||||||
"rollup-plugin-node-resolve": "^5.0.0",
|
|
||||||
"rollup-plugin-postcss": "^3.1.5",
|
"rollup-plugin-postcss": "^3.1.5",
|
||||||
"rollup-plugin-svelte": "^6.1.1",
|
"rollup-plugin-svelte": "^6.1.1",
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
"shortid": "^2.2.15",
|
|
||||||
"sirv-cli": "^0.4.4",
|
"sirv-cli": "^0.4.4",
|
||||||
"svelte": "^3.29.0"
|
"svelte": "^3.30.0"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"svelte"
|
"svelte"
|
||||||
|
@ -35,12 +32,10 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"gitHead": "284cceb9b703c38566c6e6363c022f79a08d5691",
|
"gitHead": "284cceb9b703c38566c6e6363c022f79a08d5691",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.50.1",
|
"@budibase/bbui": "^1.51.0",
|
||||||
"@budibase/svelte-ag-grid": "^0.0.16",
|
"@budibase/svelte-ag-grid": "^0.0.16",
|
||||||
"@fortawesome/fontawesome-free": "^5.14.0",
|
"@fortawesome/fontawesome-free": "^5.14.0",
|
||||||
"@svelteschool/svelte-forms": "^0.7.0",
|
|
||||||
"apexcharts": "^3.22.1",
|
"apexcharts": "^3.22.1",
|
||||||
"fast-sort": "^2.2.0",
|
|
||||||
"flatpickr": "^4.6.6",
|
"flatpickr": "^4.6.6",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
"quill": "^1.3.7",
|
"quill": "^1.3.7",
|
||||||
|
|
|
@ -1,38 +1,32 @@
|
||||||
import svelte from "rollup-plugin-svelte"
|
|
||||||
import resolve from "rollup-plugin-node-resolve"
|
|
||||||
import commonjs from "@rollup/plugin-commonjs"
|
import commonjs from "@rollup/plugin-commonjs"
|
||||||
|
import resolve from "@rollup/plugin-node-resolve"
|
||||||
|
import svelte from "rollup-plugin-svelte"
|
||||||
import postcss from "rollup-plugin-postcss"
|
import postcss from "rollup-plugin-postcss"
|
||||||
import { terser } from "rollup-plugin-terser"
|
import { terser } from "rollup-plugin-terser"
|
||||||
|
|
||||||
const production = !process.env.ROLLUP_WATCH
|
const production = !process.env.ROLLUP_WATCH
|
||||||
const lodash_fp_exports = ["isEmpty"]
|
const externals = ["svelte", "svelte/internal"]
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
external: externals,
|
||||||
input: "src/index.js",
|
input: "src/index.js",
|
||||||
output: [
|
output: [
|
||||||
{
|
{
|
||||||
file: "dist/index.js",
|
file: "dist/index.js",
|
||||||
format: "esm",
|
format: "esm",
|
||||||
name: "budibaseStandardComponents",
|
sourcemap: false,
|
||||||
sourcemap: true,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
// Only run terser in production environments
|
|
||||||
production && terser(),
|
production && terser(),
|
||||||
postcss({
|
postcss(),
|
||||||
plugins: [],
|
|
||||||
}),
|
|
||||||
svelte({
|
svelte({
|
||||||
hydratable: true,
|
dev: !production,
|
||||||
}),
|
}),
|
||||||
resolve({
|
resolve({
|
||||||
browser: true,
|
browser: true,
|
||||||
|
skip: externals,
|
||||||
}),
|
}),
|
||||||
commonjs({
|
commonjs(),
|
||||||
namedExports: {
|
|
||||||
"lodash/fp": lodash_fp_exports,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,138 +0,0 @@
|
||||||
import svelte from "rollup-plugin-svelte"
|
|
||||||
import resolve from "rollup-plugin-node-resolve"
|
|
||||||
import commonjs from "rollup-plugin-commonjs"
|
|
||||||
import livereload from "rollup-plugin-livereload"
|
|
||||||
import { terser } from "rollup-plugin-terser"
|
|
||||||
import json from "rollup-plugin-json"
|
|
||||||
|
|
||||||
const production = !process.env.ROLLUP_WATCH
|
|
||||||
|
|
||||||
const lodash_fp_exports = [
|
|
||||||
"find",
|
|
||||||
"isUndefined",
|
|
||||||
"split",
|
|
||||||
"max",
|
|
||||||
"last",
|
|
||||||
"union",
|
|
||||||
"reduce",
|
|
||||||
"isObject",
|
|
||||||
"cloneDeep",
|
|
||||||
"some",
|
|
||||||
"isArray",
|
|
||||||
"map",
|
|
||||||
"filter",
|
|
||||||
"keys",
|
|
||||||
"isFunction",
|
|
||||||
"isEmpty",
|
|
||||||
"countBy",
|
|
||||||
"join",
|
|
||||||
"includes",
|
|
||||||
"flatten",
|
|
||||||
"constant",
|
|
||||||
"first",
|
|
||||||
"intersection",
|
|
||||||
"take",
|
|
||||||
"has",
|
|
||||||
"mapValues",
|
|
||||||
"isString",
|
|
||||||
"isBoolean",
|
|
||||||
"isNull",
|
|
||||||
"isNumber",
|
|
||||||
"isObjectLike",
|
|
||||||
"isDate",
|
|
||||||
"clone",
|
|
||||||
"values",
|
|
||||||
"keyBy",
|
|
||||||
"isNaN",
|
|
||||||
"isInteger",
|
|
||||||
"toNumber",
|
|
||||||
]
|
|
||||||
|
|
||||||
const lodash_exports = [
|
|
||||||
"flow",
|
|
||||||
"head",
|
|
||||||
"find",
|
|
||||||
"each",
|
|
||||||
"tail",
|
|
||||||
"findIndex",
|
|
||||||
"startsWith",
|
|
||||||
"dropRight",
|
|
||||||
"takeRight",
|
|
||||||
"trim",
|
|
||||||
"split",
|
|
||||||
"replace",
|
|
||||||
"merge",
|
|
||||||
"assign",
|
|
||||||
]
|
|
||||||
|
|
||||||
const coreExternal = [
|
|
||||||
"lodash",
|
|
||||||
"lodash/fp",
|
|
||||||
"date-fns",
|
|
||||||
"lunr",
|
|
||||||
"safe-buffer",
|
|
||||||
"shortid",
|
|
||||||
"@nx-js/compiler-util",
|
|
||||||
"bcryptjs",
|
|
||||||
]
|
|
||||||
|
|
||||||
export default {
|
|
||||||
input: "src/Test/testMain.js",
|
|
||||||
output: {
|
|
||||||
sourcemap: true,
|
|
||||||
format: "iife",
|
|
||||||
name: "app",
|
|
||||||
file: "public/bundle.js",
|
|
||||||
globals: {
|
|
||||||
crypto: "crypto",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
svelte({
|
|
||||||
// enable run-time checks when not in production
|
|
||||||
dev: !production,
|
|
||||||
// we'll extract any component CSS out into
|
|
||||||
// a separate file — better for performance
|
|
||||||
css: css => {
|
|
||||||
css.write("bundle.css")
|
|
||||||
},
|
|
||||||
|
|
||||||
hydratable: true,
|
|
||||||
}),
|
|
||||||
|
|
||||||
// If you have external dependencies installed from
|
|
||||||
// npm, you'll most likely need these plugins. In
|
|
||||||
// some cases you'll need additional configuration —
|
|
||||||
// consult the documentation for details:
|
|
||||||
// https://github.com/rollup/rollup-plugin-commonjs
|
|
||||||
resolve({
|
|
||||||
browser: true,
|
|
||||||
dedupe: importee => {
|
|
||||||
return (
|
|
||||||
importee === "svelte" ||
|
|
||||||
importee.startsWith("svelte/") ||
|
|
||||||
coreExternal.includes(importee)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
commonjs({
|
|
||||||
namedExports: {
|
|
||||||
"lodash/fp": lodash_fp_exports,
|
|
||||||
lodash: lodash_exports,
|
|
||||||
shortid: ["generate"],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
json(),
|
|
||||||
|
|
||||||
// Watch the `public` directory and refresh the
|
|
||||||
// browser on changes when not in production
|
|
||||||
!production && livereload("public"),
|
|
||||||
|
|
||||||
// If we're building for production (npm run build
|
|
||||||
// instead of npm run dev), minify
|
|
||||||
production && terser(),
|
|
||||||
],
|
|
||||||
watch: {
|
|
||||||
clearScreen: false,
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -1,25 +1,21 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
const { styleable } = getContext("sdk")
|
||||||
|
const component = getContext("component")
|
||||||
|
|
||||||
export let className = "default"
|
export let className = "default"
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let text
|
export let text
|
||||||
|
export let onClick
|
||||||
export let _bb
|
|
||||||
let theButton
|
|
||||||
|
|
||||||
$: if (_bb.props._children && _bb.props._children.length > 0)
|
|
||||||
theButton && _bb.attachChildren(theButton)
|
|
||||||
|
|
||||||
const clickHandler = () => {
|
|
||||||
_bb.call("onClick")
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
bind:this={theButton}
|
|
||||||
class="default"
|
class="default"
|
||||||
disabled={disabled || false}
|
disabled={disabled || false}
|
||||||
on:click|once={clickHandler}>
|
use:styleable={$component.styles}
|
||||||
{#if !_bb.props._children || _bb.props._children.length === 0}{text}{/if}
|
on:click={onClick}>
|
||||||
|
{text}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -37,28 +33,4 @@
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.border {
|
|
||||||
border: var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.color {
|
|
||||||
color: var(--color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.background {
|
|
||||||
background: var(--background);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hoverBorder:hover {
|
|
||||||
border: var(--hoverBorder);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hoverColor:hover {
|
|
||||||
color: var(--hoverColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hoverBack:hover {
|
|
||||||
background: var(--hoverBackground);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import { cssVars, createClasses } from "./cssVars"
|
import { getContext } from "svelte"
|
||||||
|
import { cssVars } from "./helpers"
|
||||||
|
|
||||||
|
const { styleable } = getContext("sdk")
|
||||||
|
const component = getContext("component")
|
||||||
|
|
||||||
export const className = ""
|
export const className = ""
|
||||||
export let imageUrl = ""
|
export let imageUrl = ""
|
||||||
|
@ -22,7 +26,10 @@
|
||||||
$: showImage = !!imageUrl
|
$: showImage = !!imageUrl
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div use:cssVars={cssVariables} class="container">
|
<div
|
||||||
|
use:cssVars={cssVariables}
|
||||||
|
class="container"
|
||||||
|
use:styleable={$component.styles}>
|
||||||
{#if showImage}<img class="image" src={imageUrl} alt="" />{/if}
|
{#if showImage}<img class="image" src={imageUrl} alt="" />{/if}
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<h2 class="heading">{heading}</h2>
|
<h2 class="heading">{heading}</h2>
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import { cssVars, createClasses } from "./cssVars"
|
import { getContext } from "svelte"
|
||||||
|
import { cssVars } from "./helpers"
|
||||||
|
|
||||||
|
const { styleable } = getContext("sdk")
|
||||||
|
const component = getContext("component")
|
||||||
|
|
||||||
export const className = ""
|
export const className = ""
|
||||||
export let imageUrl = ""
|
export let imageUrl = ""
|
||||||
|
@ -25,7 +29,10 @@
|
||||||
$: showImage = !!imageUrl
|
$: showImage = !!imageUrl
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div use:cssVars={cssVariables} class="container">
|
<div
|
||||||
|
use:cssVars={cssVariables}
|
||||||
|
class="container"
|
||||||
|
use:styleable={$component.styles}>
|
||||||
{#if showImage}<img class="image" src={imageUrl} alt="" />{/if}
|
{#if showImage}<img class="image" src={imageUrl} alt="" />{/if}
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<main>
|
<main>
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
<script>
|
|
||||||
import Input from "./Input.svelte"
|
|
||||||
export let _bb
|
|
||||||
|
|
||||||
export let label = ""
|
|
||||||
export let checked = false
|
|
||||||
export let value = ""
|
|
||||||
export let onchange = () => {}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Input type="checkbox" {_bb} {checked} {label} {value} {onchange} />
|
|
|
@ -1,50 +1,62 @@
|
||||||
<script>
|
<script>
|
||||||
import { cssVars, createClasses } from "./cssVars"
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
const { styleable } = getContext("sdk")
|
||||||
|
const component = getContext("component")
|
||||||
|
|
||||||
export let className = ""
|
|
||||||
export let onLoad
|
|
||||||
export let type = "div"
|
export let type = "div"
|
||||||
export let _bb
|
|
||||||
|
|
||||||
let containerElement
|
|
||||||
let hasLoaded
|
|
||||||
let currentChildren
|
|
||||||
|
|
||||||
$: {
|
|
||||||
if (containerElement) {
|
|
||||||
_bb.attachChildren(containerElement)
|
|
||||||
if (!hasLoaded) {
|
|
||||||
_bb.call("onLoad")
|
|
||||||
hasLoaded = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if type === 'div'}
|
{#if type === 'div'}
|
||||||
<div bind:this={containerElement} />
|
<div use:styleable={$component.styles}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
{:else if type === 'header'}
|
{:else if type === 'header'}
|
||||||
<header bind:this={containerElement} />
|
<header use:styleable={$component.styles}>
|
||||||
|
<slot />
|
||||||
|
</header>
|
||||||
{:else if type === 'main'}
|
{:else if type === 'main'}
|
||||||
<main bind:this={containerElement} />
|
<main use:styleable={$component.styles}>
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
{:else if type === 'footer'}
|
{:else if type === 'footer'}
|
||||||
<footer bind:this={containerElement} />
|
<footer use:styleable={$component.styles}>
|
||||||
|
<slot />
|
||||||
|
</footer>
|
||||||
{:else if type === 'aside'}
|
{:else if type === 'aside'}
|
||||||
<aside bind:this={containerElement} />
|
<aside use:styleable={$component.styles}>
|
||||||
|
<slot />
|
||||||
|
</aside>
|
||||||
{:else if type === 'summary'}
|
{:else if type === 'summary'}
|
||||||
<summary bind:this={containerElement} />
|
<summary use:styleable={$component.styles}>
|
||||||
|
<slot />
|
||||||
|
</summary>
|
||||||
{:else if type === 'details'}
|
{:else if type === 'details'}
|
||||||
<details bind:this={containerElement} />
|
<details use:styleable={$component.styles}>
|
||||||
|
<slot />
|
||||||
|
</details>
|
||||||
{:else if type === 'article'}
|
{:else if type === 'article'}
|
||||||
<article bind:this={containerElement} />
|
<article use:styleable={$component.styles}>
|
||||||
|
<slot />
|
||||||
|
</article>
|
||||||
{:else if type === 'nav'}
|
{:else if type === 'nav'}
|
||||||
<nav bind:this={containerElement} />
|
<nav use:styleable={$component.styles}>
|
||||||
|
<slot />
|
||||||
|
</nav>
|
||||||
{:else if type === 'mark'}
|
{:else if type === 'mark'}
|
||||||
<mark bind:this={containerElement} />
|
<mark use:styleable={$component.styles}>
|
||||||
|
<slot />
|
||||||
|
</mark>
|
||||||
{:else if type === 'figure'}
|
{:else if type === 'figure'}
|
||||||
<figure bind:this={containerElement} />
|
<figure use:styleable={$component.styles}>
|
||||||
|
<slot />
|
||||||
|
</figure>
|
||||||
{:else if type === 'figcaption'}
|
{:else if type === 'figcaption'}
|
||||||
<figcaption bind:this={containerElement} />
|
<figcaption use:styleable={$component.styles}>
|
||||||
|
<slot />
|
||||||
|
</figcaption>
|
||||||
{:else if type === 'paragraph'}
|
{:else if type === 'paragraph'}
|
||||||
<p bind:this={containerElement} />
|
<p use:styleable={$component.styles}>
|
||||||
|
<slot />
|
||||||
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,10 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import Form from "./Form.svelte"
|
import Form from "./Form.svelte"
|
||||||
|
|
||||||
export let _bb
|
|
||||||
export let table
|
|
||||||
export let title
|
|
||||||
export let buttonText
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Form {_bb} {table} {title} {buttonText} wide={false} />
|
<Form wide={false} />
|
||||||
|
|
|
@ -1,10 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import Form from "./Form.svelte"
|
import Form from "./Form.svelte"
|
||||||
|
|
||||||
export let _bb
|
|
||||||
export let table
|
|
||||||
export let title
|
|
||||||
export let buttonText
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Form {_bb} {table} {title} {buttonText} wide={true} />
|
<Form wide />
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
import api from "../../api"
|
|
||||||
|
|
||||||
let cache = {}
|
|
||||||
|
|
||||||
async function fetchTable(id) {
|
|
||||||
const FETCH_TABLE_URL = `/api/tables/${id}`
|
|
||||||
const response = await api.get(FETCH_TABLE_URL)
|
|
||||||
return await response.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getTable(tableId) {
|
|
||||||
if (!tableId) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (!cache[tableId]) {
|
|
||||||
cache[tableId] = fetchTable(tableId)
|
|
||||||
cache[tableId] = await cache[tableId]
|
|
||||||
}
|
|
||||||
return await cache[tableId]
|
|
||||||
}
|
|
|
@ -1,82 +0,0 @@
|
||||||
<script>
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
|
|
||||||
export let _bb
|
|
||||||
export let table
|
|
||||||
export let layout = "list"
|
|
||||||
|
|
||||||
let headers = []
|
|
||||||
let store = _bb.store
|
|
||||||
|
|
||||||
async function fetchData() {
|
|
||||||
if (!table || !table.length) return
|
|
||||||
|
|
||||||
const FETCH_ROWS_URL = `/api/views/all_${table}`
|
|
||||||
const response = await _bb.api.get(FETCH_ROWS_URL)
|
|
||||||
if (response.status === 200) {
|
|
||||||
const json = await response.json()
|
|
||||||
|
|
||||||
store.update(state => {
|
|
||||||
state[table] = json
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
throw new Error("Failed to fetch rows.", response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$: data = $store[table] || []
|
|
||||||
$: if (table) fetchData()
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
await fetchData()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<section class:grid={layout === 'grid'} class:list={layout === 'list'}>
|
|
||||||
{#each data as data}
|
|
||||||
<div class="data-card">
|
|
||||||
<ul>
|
|
||||||
{#each Object.keys(data) as key}
|
|
||||||
<li>
|
|
||||||
<span class="data-key">{key}:</span>
|
|
||||||
<span class="data-value">{data[key]}</span>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
font-family: Inter;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
list-style-type: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
margin: 5px 0 5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-card {
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-key {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 20px;
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,15 +0,0 @@
|
||||||
<script>
|
|
||||||
export let _bb
|
|
||||||
export let table
|
|
||||||
|
|
||||||
let searchValue = ""
|
|
||||||
|
|
||||||
function search() {
|
|
||||||
const SEARCH_URL = _bb.api.get(``)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<input type="text" bind:value={searchValue} />
|
|
||||||
<button on:click={search}>Search</button>
|
|
||||||
</div>
|
|
|
@ -1,18 +1,21 @@
|
||||||
<script>
|
<script>
|
||||||
import Flatpickr from "svelte-flatpickr"
|
|
||||||
import { DatePicker } from "@budibase/bbui"
|
import { DatePicker } from "@budibase/bbui"
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
const { styleable, setBindableValue } = getContext("sdk")
|
||||||
|
const component = getContext("component")
|
||||||
|
|
||||||
export let placeholder
|
export let placeholder
|
||||||
export let value
|
|
||||||
|
|
||||||
export let _bb
|
let value
|
||||||
|
$: setBindableValue(value, $component.id)
|
||||||
|
|
||||||
function handleChange(event) {
|
function handleChange(event) {
|
||||||
const [fullDate, dateStr, instance] = event.detail
|
const [fullDate] = event.detail
|
||||||
if (_bb) {
|
value = fullDate
|
||||||
_bb.setBinding("value", fullDate)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DatePicker {placeholder} on:change={handleChange} {value} />
|
<div use:styleable={$component.styles}>
|
||||||
|
<DatePicker {placeholder} on:change={handleChange} {value} />
|
||||||
|
</div>
|
||||||
|
|
|
@ -1,5 +1,23 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
const { styleable } = getContext("sdk")
|
||||||
|
const component = getContext("component")
|
||||||
|
|
||||||
export let embed
|
export let embed
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{@html embed}
|
<div use:styleable={$component.styles}>
|
||||||
|
{@html embed}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
div :global(> *) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,54 +1,66 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { getContext } from "svelte"
|
||||||
import { Label, DatePicker, Input, Select, Toggle } from "@budibase/bbui"
|
import { Label, DatePicker, Input, Select, Toggle } from "@budibase/bbui"
|
||||||
import Dropzone from "./attachments/Dropzone.svelte"
|
import Dropzone from "./attachments/Dropzone.svelte"
|
||||||
import LinkedRowSelector from "./LinkedRowSelector.svelte"
|
import LinkedRowSelector from "./LinkedRowSelector.svelte"
|
||||||
import ErrorsBox from "./ErrorsBox.svelte"
|
|
||||||
import { capitalise } from "./helpers"
|
import { capitalise } from "./helpers"
|
||||||
|
|
||||||
export let _bb
|
const { styleable, API } = getContext("sdk")
|
||||||
export let table
|
const component = getContext("component")
|
||||||
|
const dataContext = getContext("data")
|
||||||
|
|
||||||
export let wide = false
|
export let wide = false
|
||||||
|
|
||||||
let store = _bb.store
|
let row
|
||||||
let schema = {}
|
let schema
|
||||||
let rowId
|
let fields = []
|
||||||
let errors = {}
|
|
||||||
|
|
||||||
$: schema = $store.data && $store.data._table && $store.data._table.schema
|
// Fetch info about the closest data context
|
||||||
$: fields = schema ? Object.keys(schema) : []
|
$: getFormData($dataContext[$dataContext.closestComponentId])
|
||||||
|
|
||||||
|
const getFormData = async context => {
|
||||||
|
if (context) {
|
||||||
|
const tableDefinition = await API.fetchTableDefinition(context.tableId)
|
||||||
|
schema = tableDefinition.schema
|
||||||
|
fields = Object.keys(schema)
|
||||||
|
|
||||||
|
// Use the draft version for editing
|
||||||
|
row = $dataContext[`${$dataContext.closestComponentId}_draft`]
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="form-content">
|
<div class="form-content" use:styleable={$component.styles}>
|
||||||
<ErrorsBox errors={$store.saveRowErrors || {}} />
|
<!-- <ErrorsBox errors={$store.saveRowErrors || {}} />-->
|
||||||
{#each fields as field}
|
{#each fields as field}
|
||||||
<div class="form-field" class:wide>
|
<div class="form-field" class:wide>
|
||||||
{#if !(schema[field].type === 'boolean' && !wide)}
|
{#if !(schema[field].type === 'boolean' && !wide)}
|
||||||
<Label extraSmall={!wide} grey>{capitalise(schema[field].name)}</Label>
|
<Label extraSmall={!wide} grey>{capitalise(schema[field].name)}</Label>
|
||||||
{/if}
|
{/if}
|
||||||
{#if schema[field].type === 'options'}
|
{#if schema[field].type === 'options'}
|
||||||
<Select secondary bind:value={$store.data[field]}>
|
<Select secondary bind:value={row[field]}>
|
||||||
<option value="">Choose an option</option>
|
<option value="">Choose an option</option>
|
||||||
{#each schema[field].constraints.inclusion as opt}
|
{#each schema[field].constraints.inclusion as opt}
|
||||||
<option>{opt}</option>
|
<option>{opt}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</Select>
|
</Select>
|
||||||
{:else if schema[field].type === 'datetime'}
|
{:else if schema[field].type === 'datetime'}
|
||||||
<DatePicker bind:value={$store.data[field]} />
|
<DatePicker bind:value={row[field]} />
|
||||||
{:else if schema[field].type === 'boolean'}
|
{:else if schema[field].type === 'boolean'}
|
||||||
<Toggle
|
<Toggle
|
||||||
text={wide ? null : capitalise(schema[field].name)}
|
text={wide ? null : capitalise(schema[field].name)}
|
||||||
bind:checked={$store.data[field]} />
|
bind:checked={row[field]} />
|
||||||
{:else if schema[field].type === 'number'}
|
{:else if schema[field].type === 'number'}
|
||||||
<Input type="number" bind:value={$store.data[field]} />
|
<Input type="number" bind:value={row[field]} />
|
||||||
{:else if schema[field].type === 'string'}
|
{:else if schema[field].type === 'string'}
|
||||||
<Input bind:value={$store.data[field]} />
|
<Input bind:value={row[field]} />
|
||||||
{:else if schema[field].type === 'attachment'}
|
{:else if schema[field].type === 'attachment'}
|
||||||
<Dropzone bind:files={$store.data[field]} />
|
<Dropzone bind:files={row[field]} />
|
||||||
{:else if schema[field].type === 'link'}
|
{:else if schema[field].type === 'link'}
|
||||||
<LinkedRowSelector
|
<LinkedRowSelector
|
||||||
secondary
|
secondary
|
||||||
showLabel={false}
|
showLabel={false}
|
||||||
bind:linkedRows={$store.data[field]}
|
bind:linkedRows={row[field]}
|
||||||
schema={schema[field]} />
|
schema={schema[field]} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,30 +1,24 @@
|
||||||
<script>
|
<script>
|
||||||
import { buildStyle } from "./buildStyle.js"
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
const { styleable } = getContext("sdk")
|
||||||
|
const component = getContext("component")
|
||||||
|
|
||||||
export let className = ""
|
export let className = ""
|
||||||
export let type
|
export let type
|
||||||
export let text = ""
|
export let text = ""
|
||||||
|
|
||||||
export let _bb
|
|
||||||
|
|
||||||
let containerElement
|
|
||||||
|
|
||||||
$: containerElement &&
|
|
||||||
!text &&
|
|
||||||
_bb.props.children &&
|
|
||||||
_bb.props.children.length &&
|
|
||||||
_bb.attachChildren(containerElement)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if type === 'h1'}
|
{#if type === 'h1'}
|
||||||
<h1 class={className} bind:this={containerElement}>{text}</h1>
|
<h1 class={className} use:styleable={$component.styles}>{text}</h1>
|
||||||
{:else if type === 'h2'}
|
{:else if type === 'h2'}
|
||||||
<h2 class={className} bind:this={containerElement}>{text}</h2>
|
<h2 class={className} use:styleable={$component.styles}>{text}</h2>
|
||||||
{:else if type === 'h3'}
|
{:else if type === 'h3'}
|
||||||
<h3 class={className} bind:this={containerElement}>{text}</h3>
|
<h3 class={className} use:styleable={$component.styles}>{text}</h3>
|
||||||
{:else if type === 'h4'}
|
{:else if type === 'h4'}
|
||||||
<h4 class={className} bind:this={containerElement}>{text}</h4>
|
<h4 class={className} use:styleable={$component.styles}>{text}</h4>
|
||||||
{:else if type === 'h5'}
|
{:else if type === 'h5'}
|
||||||
<h5 class={className} bind:this={containerElement}>{text}</h5>
|
<h5 class={className} use:styleable={$component.styles}>{text}</h5>
|
||||||
{:else if type === 'h6'}
|
{:else if type === 'h6'}
|
||||||
<h6 class={className} bind:this={containerElement}>{text}</h6>
|
<h6 class={className} use:styleable={$component.styles}>{text}</h6>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
<script>
|
<script>
|
||||||
import "@fortawesome/fontawesome-free/js/all.js"
|
import "@fortawesome/fontawesome-free/js/all.js"
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
const { styleable } = getContext("sdk")
|
||||||
|
const component = getContext("component")
|
||||||
|
|
||||||
export let icon = ""
|
export let icon = ""
|
||||||
export let size = "fa-lg"
|
export let size = "fa-lg"
|
||||||
export let color = "#000"
|
export let color = "#000"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<i style={`color: ${color};`} class={`${icon} ${size}`} />
|
<i
|
||||||
|
style={`color: ${color};`}
|
||||||
|
class={`${icon} ${size}`}
|
||||||
|
use:styleable={$component.styles} />
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
<script>
|
|
||||||
export let icon = ""
|
|
||||||
export let fontSize = "1em"
|
|
||||||
export let _bb
|
|
||||||
|
|
||||||
$: style = { fontSize }
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<i class={icon} {style} />
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue