Merge branch 'component-sdk' of github.com:Budibase/budibase into feature/self-hosting
This commit is contained in:
commit
de26870303
|
@ -186,7 +186,7 @@ Or if you are in the builder you can run `yarn cy:test`.
|
|||
|
||||
### Other Useful Information
|
||||
|
||||
* The contributors are listed in [AUTHORS.md](https://github.com/budibase/server/blob/master/AUTHORS.md) (add yourself).
|
||||
* The contributors are listed in [AUTHORS.md](https://github.com/Budibase/budibase/blob/master/.github/AUTHORS.md) (add yourself).
|
||||
|
||||
* This project uses a modified version of the MPLv2 license, see [LICENSE](https://github.com/budibase/server/blob/master/LICENSE).
|
||||
|
|
@ -116,7 +116,7 @@ You can also follow a quick tutorial on [how to build a CRM with Budibase](https
|
|||
|
||||
## ❗ Code of Conduct
|
||||
|
||||
Budibase is dedicated to providing a welcoming, diverse, and harrassment-free experience for everyone. We expect everyone in the Budibase community to abide by our [**Code of Conduct**](https://github.com/Budibase/budibase/blob/master/CODE_OF_CONDUCT.md). Please read it.
|
||||
Budibase is dedicated to providing a welcoming, diverse, and harrassment-free experience for everyone. We expect everyone in the Budibase community to abide by our [**Code of Conduct**](https://github.com/Budibase/budibase/blob/master/.github/CODE_OF_CONDUCT.md). Please read it.
|
||||
|
||||
## 🙌 Contributing to Budibase
|
||||
|
||||
|
@ -134,7 +134,7 @@ Budibase is a monorepo managed by lerna. Lerna manages the building and publishi
|
|||
|
||||
- [packages/server](https://github.com/Budibase/budibase/tree/master/packages/server) - The budibase server. This Koa app is responsible for serving the JS for the builder and budibase apps, as well as providing the API for interaction with the database and file system.
|
||||
|
||||
For more information, see [CONTRIBUTING.md](./CONTRIBUTING.md)
|
||||
For more information, see [CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/master/.github/CONTRIBUTING.md)
|
||||
|
||||
## 📝 License
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
"prettier-plugin-svelte": "^1.4.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"rollup-plugin-replace": "^2.2.0",
|
||||
"svelte": "^3.28.0"
|
||||
"svelte": "^3.30.0"
|
||||
},
|
||||
"scripts": {
|
||||
"bootstrap": "lerna bootstrap",
|
||||
|
@ -26,7 +26,7 @@
|
|||
"nuke": "rimraf ~/.budibase && npm run restore",
|
||||
"clean": "lerna clean",
|
||||
"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",
|
||||
"lint": "eslint 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.ENABLE_ANALYTICS = "false"
|
||||
|
||||
// Stop info logs polluting test outputs
|
||||
process.env.LOG_LEVEL = "error"
|
||||
|
||||
async function run(dir) {
|
||||
process.env.BUDIBASE_DIR = resolve(dir)
|
||||
require("dotenv").config({ path: resolve(dir, ".env") })
|
||||
|
|
|
@ -81,8 +81,8 @@
|
|||
"shortid": "^2.2.15",
|
||||
"svelte-loading-spinners": "^0.1.1",
|
||||
"svelte-portal": "^0.1.0",
|
||||
"yup": "^0.29.2",
|
||||
"uuid": "^8.3.1"
|
||||
"uuid": "^8.3.1",
|
||||
"yup": "^0.29.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.5.5",
|
||||
|
@ -90,6 +90,7 @@
|
|||
"@babel/preset-env": "^7.5.5",
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"@rollup/plugin-alias": "^3.0.1",
|
||||
"@rollup/plugin-commonjs": "^16.0.0",
|
||||
"@rollup/plugin-json": "^4.0.3",
|
||||
"@sveltech/routify": "1.7.11",
|
||||
"@testing-library/jest-dom": "^5.11.0",
|
||||
|
@ -104,7 +105,6 @@
|
|||
"rimraf": "^3.0.2",
|
||||
"rollup": "^2.11.2",
|
||||
"rollup-plugin-alias": "^1.5.2",
|
||||
"rollup-plugin-commonjs": "^10.0.0",
|
||||
"rollup-plugin-copy": "^3.0.0",
|
||||
"rollup-plugin-css-only": "^2.1.0",
|
||||
"rollup-plugin-livereload": "^1.0.0",
|
||||
|
@ -115,7 +115,7 @@
|
|||
"rollup-plugin-terser": "^7.0.2",
|
||||
"rollup-plugin-url": "^2.2.2",
|
||||
"start-server-and-test": "^1.11.0",
|
||||
"svelte": "^3.29.0",
|
||||
"svelte": "^3.30.0",
|
||||
"svelte-jester": "^1.0.6"
|
||||
},
|
||||
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import alias from "@rollup/plugin-alias"
|
||||
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 url from "rollup-plugin-url"
|
||||
import livereload from "rollup-plugin-livereload"
|
||||
import { terser } from "rollup-plugin-terser"
|
||||
|
@ -15,102 +15,7 @@ import json from "@rollup/plugin-json"
|
|||
import path from "path"
|
||||
|
||||
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 coreExternal = [
|
||||
"lodash",
|
||||
"lodash/fp",
|
||||
|
@ -224,13 +129,7 @@ export default {
|
|||
)
|
||||
},
|
||||
}),
|
||||
commonjs({
|
||||
namedExports: {
|
||||
"lodash/fp": lodash_fp_exports,
|
||||
lodash: lodash_exports,
|
||||
shortid: ["generate"],
|
||||
},
|
||||
}),
|
||||
commonjs(),
|
||||
url({
|
||||
limit: 0,
|
||||
include: ["**/*.woff2", "**/*.png"],
|
||||
|
|
|
@ -24,7 +24,7 @@ import { cloneDeep, difference } from "lodash/fp"
|
|||
* @returns {Array.<BindableProperty>}
|
||||
*/
|
||||
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)
|
||||
instance: cloneDeep(screen.props),
|
||||
targetId: componentInstanceId,
|
||||
|
@ -33,13 +33,10 @@ export default function({ componentInstanceId, screen, components, tables }) {
|
|||
})
|
||||
|
||||
return [
|
||||
...walkResult.bindableInstances
|
||||
.filter(isInstanceInSharedContext(walkResult))
|
||||
.map(componentInstanceToBindable(walkResult)),
|
||||
|
||||
...(walkResult.target?._contexts
|
||||
.map(contextToBindables(tables, walkResult))
|
||||
.flat() ?? []),
|
||||
...result.bindableInstances
|
||||
.filter(isInstanceInSharedContext(result))
|
||||
.map(componentInstanceToBindable),
|
||||
...(result.target?._contexts.map(contextToBindables(tables)).flat() ?? []),
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -53,26 +50,18 @@ const isInstanceInSharedContext = walkResult => i =>
|
|||
|
||||
// turns a component instance prop into binding expressions
|
||||
// used by the UI
|
||||
const componentInstanceToBindable = walkResult => i => {
|
||||
const lastContext =
|
||||
i.instance._contexts.length &&
|
||||
i.instance._contexts[i.instance._contexts.length - 1]
|
||||
const contextParentPath = lastContext
|
||||
? getParentPath(walkResult, lastContext)
|
||||
: ""
|
||||
|
||||
const componentInstanceToBindable = i => {
|
||||
return {
|
||||
type: "instance",
|
||||
instance: i.instance,
|
||||
// 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
|
||||
readableBinding: `${i.instance._instanceName}`,
|
||||
}
|
||||
}
|
||||
|
||||
const contextToBindables = (tables, walkResult) => context => {
|
||||
const contextParentPath = getParentPath(walkResult, context)
|
||||
const contextToBindables = tables => context => {
|
||||
const tableId = context.table?.tableId ?? context.table
|
||||
const table = tables.find(table => table._id === tableId)
|
||||
let schema =
|
||||
|
@ -98,7 +87,7 @@ const contextToBindables = (tables, walkResult) => context => {
|
|||
fieldSchema,
|
||||
instance: context.instance,
|
||||
// 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
|
||||
readableBinding: `${context.instance._instanceName}.${table.name}.${key}`,
|
||||
// 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 }) => {
|
||||
if (!result) {
|
||||
result = {
|
||||
|
|
|
@ -12,10 +12,7 @@ export function readableToRuntimeBinding(bindableProperties, textWithBindings) {
|
|||
return boundValue === `{{ ${readableBinding} }}`
|
||||
})
|
||||
if (binding) {
|
||||
result = textWithBindings.replace(
|
||||
boundValue,
|
||||
`{{ ${binding.runtimeBinding} }}`
|
||||
)
|
||||
result = result.replace(boundValue, `{{ ${binding.runtimeBinding} }}`)
|
||||
}
|
||||
})
|
||||
return result
|
||||
|
|
|
@ -481,7 +481,7 @@ export const getFrontendStore = () => {
|
|||
// Try to extract a nav component from the master screen
|
||||
const nav = findChildComponentType(
|
||||
state.pages.main,
|
||||
"@budibase/standard-components/Navigation"
|
||||
"@budibase/standard-components/navigation"
|
||||
)
|
||||
if (nav) {
|
||||
let newLink
|
||||
|
|
|
@ -15,8 +15,6 @@ export class Component extends BaseStructure {
|
|||
selected: {},
|
||||
},
|
||||
_code: "",
|
||||
className: "",
|
||||
onLoad: [],
|
||||
type: "",
|
||||
_instanceName: "",
|
||||
_children: [],
|
||||
|
|
|
@ -1,140 +1,63 @@
|
|||
<script>
|
||||
import { store, backendUiStore } from "builderStore"
|
||||
import { map, join } from "lodash/fp"
|
||||
import { onMount } from "svelte"
|
||||
import { store } from "builderStore"
|
||||
import iframeTemplate from "./iframeTemplate"
|
||||
import { pipe } from "../../../helpers"
|
||||
import { Screen } from "../../../builderStore/store/screenTemplates/utils/Screen"
|
||||
import { Component } from "../../../builderStore/store/screenTemplates/utils/Component"
|
||||
import { Screen } from "builderStore/store/screenTemplates/utils/Screen"
|
||||
|
||||
let iframe
|
||||
let styles = ""
|
||||
|
||||
function transform_component(comp) {
|
||||
const props = comp.props || comp
|
||||
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)
|
||||
// Create screen slot placeholder for use when a page is selected rather
|
||||
// than a screen
|
||||
const screenPlaceholder = new Screen()
|
||||
.name("Screen Placeholder")
|
||||
.route("*")
|
||||
.component("@budibase/standard-components/container")
|
||||
.mainType("div")
|
||||
.component("@budibase/standard-components/screenslotplaceholder")
|
||||
.instanceName("Content Placeholder")
|
||||
.normalStyle({
|
||||
flex: "1 1 auto",
|
||||
})
|
||||
.addChild(container)
|
||||
.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 ?? ""
|
||||
|
||||
$: {
|
||||
styles = ""
|
||||
// Apply the CSS from the currently selected page and its screens
|
||||
const currentPage = $store.pages[$store.currentPageName]
|
||||
styles += currentPage._css
|
||||
for (let screen of currentPage._screens) {
|
||||
styles += screen._css
|
||||
// Saving pages and screens to the DB causes them to have _revs.
|
||||
// These revisions change every time a save happens and causes
|
||||
// these reactive statements to fire, even though the actual
|
||||
// definition hasn't changed.
|
||||
// By deleting all _rev properties we can avoid this and increase
|
||||
// performance.
|
||||
$: 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, [
|
||||
map(s => `<link rel="stylesheet" href="${s}"/>`),
|
||||
join("\n"),
|
||||
])
|
||||
// Refresh the preview when required
|
||||
$: refreshContent(strippedJson)
|
||||
|
||||
$: screensExist =
|
||||
$store.currentPreviewItem._screens &&
|
||||
$store.currentPreviewItem._screens.length > 0
|
||||
|
||||
$: frontendDefinition = {
|
||||
appId: $store.appId,
|
||||
libraries: $store.libraries,
|
||||
page: $store.pages[$store.currentPageName],
|
||||
screens: [
|
||||
$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,
|
||||
})
|
||||
// Initialise the app when mounted
|
||||
onMount(() => {
|
||||
iframe.contentWindow.addEventListener(
|
||||
"bb-ready",
|
||||
() => {
|
||||
refreshContent(strippedJson)
|
||||
},
|
||||
{
|
||||
once: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
$: if (iframe)
|
||||
iframe.contentWindow.addEventListener("bb-ready", refreshContent, {
|
||||
once: true,
|
||||
})
|
||||
|
||||
$: if (iframe && frontendDefinition) {
|
||||
refreshContent()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="component-container">
|
||||
{#if hasComponent && $store.currentPreviewItem}
|
||||
{#if $store.currentPreviewItem}
|
||||
<iframe
|
||||
style="height: 100%; width: 100%"
|
||||
title="componentPreview"
|
||||
|
@ -152,7 +75,6 @@
|
|||
margin: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.component-container iframe {
|
||||
border: 0;
|
||||
left: 0;
|
||||
|
|
|
@ -4,72 +4,59 @@ export default `<html>
|
|||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Mono">
|
||||
<style>
|
||||
body, html {
|
||||
height: 100%!important;
|
||||
height: 100% !important;
|
||||
font-family: Inter !important;
|
||||
margin: 0px!important;
|
||||
margin: 0px !important;
|
||||
}
|
||||
*, *:before, *:after {
|
||||
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>
|
||||
<script src='/assets/budibase-client.js'></script>
|
||||
<script>
|
||||
function receiveMessage(event) {
|
||||
if (!event.data) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!event.data) return
|
||||
|
||||
const data = JSON.parse(event.data)
|
||||
|
||||
try {
|
||||
if (styles) document.head.removeChild(styles)
|
||||
} catch(_) { }
|
||||
|
||||
try {
|
||||
if (selectedComponentStyle) document.head.removeChild(selectedComponentStyle)
|
||||
} catch(_) { }
|
||||
|
||||
selectedComponentStyle = document.createElement('style');
|
||||
// Extract data from message
|
||||
const { selectedComponentId, page, screen } = JSON.parse(event.data)
|
||||
|
||||
// Update selected component style
|
||||
if (selectedComponentStyle) {
|
||||
document.head.removeChild(selectedComponentStyle)
|
||||
}
|
||||
selectedComponentStyle = document.createElement("style");
|
||||
document.head.appendChild(selectedComponentStyle)
|
||||
var selectedCss = '.' + data.selectedComponentType + '-' + data.selectedComponentId + '{ border: 2px solid #0055ff; }'
|
||||
var selectedCss = '[data-bb-id="' + selectedComponentId + '"]' + '{border:2px solid #0055ff !important;}'
|
||||
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;
|
||||
// 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()
|
||||
|
||||
// Initialise app
|
||||
if (window.loadBudibase) {
|
||||
loadBudibase({ window, localStorage })
|
||||
loadBudibase()
|
||||
}
|
||||
}
|
||||
let styles
|
||||
|
||||
let selectedComponentStyle
|
||||
|
||||
document.addEventListener("click", function(e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
return false;
|
||||
}, true)
|
||||
// Ignore clicks
|
||||
["click", "mousedown"].forEach(type => {
|
||||
document.addEventListener(type, function(e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
return false
|
||||
}, true)
|
||||
})
|
||||
|
||||
window.addEventListener('message', receiveMessage)
|
||||
window.dispatchEvent(new Event('bb-ready'))
|
||||
|
||||
window.addEventListener("message", receiveMessage)
|
||||
window.dispatchEvent(new Event("bb-ready"))
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
@ -15,7 +15,8 @@
|
|||
let anchor
|
||||
|
||||
$: noChildrenAllowed =
|
||||
!component || !getComponentDefinition($store, component._component).children
|
||||
!component ||
|
||||
!getComponentDefinition($store, component._component)?.children
|
||||
$: noPaste = !$store.componentToPaste
|
||||
|
||||
const lastPartOfName = c => (c ? last(c._component.split("/")) : "")
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<script>
|
||||
import { TextButton, Body, DropdownMenu, ModalContent } from "@budibase/bbui"
|
||||
import { AddIcon, ArrowDownIcon } from "components/common/Icons/"
|
||||
import { EVENT_TYPE_MEMBER_NAME } from "../../../../../client/src/state/eventHandlers"
|
||||
import actionTypes from "./actions"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const eventTypeKey = "##eventHandlerType"
|
||||
|
||||
export let event
|
||||
|
||||
|
@ -18,8 +18,7 @@
|
|||
$: actions = event || []
|
||||
$: selectedActionComponent =
|
||||
selectedAction &&
|
||||
actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_MEMBER_NAME])
|
||||
.component
|
||||
actionTypes.find(t => t.name === selectedAction[eventTypeKey]).component
|
||||
|
||||
const updateEventHandler = (updatedHandler, index) => {
|
||||
actions[index] = updatedHandler
|
||||
|
@ -33,7 +32,7 @@
|
|||
const addAction = actionType => () => {
|
||||
const newAction = {
|
||||
parameters: {},
|
||||
[EVENT_TYPE_MEMBER_NAME]: actionType.name,
|
||||
[eventTypeKey]: actionType.name,
|
||||
}
|
||||
actions.push(newAction)
|
||||
selectedAction = newAction
|
||||
|
@ -79,7 +78,7 @@
|
|||
{#each actions as action, index}
|
||||
<div class="action-container">
|
||||
<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}>
|
||||
<ArrowDownIcon />
|
||||
</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>
|
||||
import { buildStyle } from "../../helpers.js"
|
||||
export let value = ""
|
||||
export let text = ""
|
||||
export let icon = ""
|
||||
export let onClick = value => {}
|
||||
export let onClick = () => {}
|
||||
export let selected = false
|
||||
|
||||
$: useIcon = !!icon
|
||||
|
|
|
@ -40,13 +40,16 @@
|
|||
|
||||
$: links = bindableProperties
|
||||
.filter(x => x.fieldSchema?.type === "link")
|
||||
.map(property => ({
|
||||
label: property.readableBinding,
|
||||
fieldName: property.fieldSchema.name,
|
||||
name: `all_${property.fieldSchema.tableId}`,
|
||||
tableId: property.fieldSchema.tableId,
|
||||
type: "link",
|
||||
}))
|
||||
.map(property => {
|
||||
return {
|
||||
providerId: property.instance._id,
|
||||
label: property.readableBinding,
|
||||
fieldName: property.fieldSchema.name,
|
||||
name: `all_${property.fieldSchema.tableId}`,
|
||||
tableId: property.fieldSchema.tableId,
|
||||
type: "link",
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
|
|
@ -1175,7 +1175,7 @@ export default {
|
|||
},
|
||||
{
|
||||
name: "Nav Bar",
|
||||
_component: "@budibase/standard-components/Navigation",
|
||||
_component: "@budibase/standard-components/navigation",
|
||||
description:
|
||||
"A component for handling the navigation within your app.",
|
||||
icon: "ri-navigation-line",
|
||||
|
|
|
@ -11,7 +11,7 @@ describe("fetch bindable properties", () => {
|
|||
)
|
||||
expect(componentBinding).toBeDefined()
|
||||
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", () => {
|
||||
|
@ -37,20 +37,22 @@ describe("fetch bindable properties", () => {
|
|||
expect(contextBindings.length).toBe(4)
|
||||
|
||||
const namebinding = contextBindings.find(
|
||||
b => b.runtimeBinding === "data.name"
|
||||
b => b.runtimeBinding === "list-id.name"
|
||||
)
|
||||
expect(namebinding).toBeDefined()
|
||||
expect(namebinding.readableBinding).toBe("list-name.Test Table.name")
|
||||
|
||||
const descriptionbinding = contextBindings.find(
|
||||
b => b.runtimeBinding === "data.description"
|
||||
b => b.runtimeBinding === "list-id.description"
|
||||
)
|
||||
expect(descriptionbinding).toBeDefined()
|
||||
expect(descriptionbinding.readableBinding).toBe(
|
||||
"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.readableBinding).toBe("list-name.Test Table._id")
|
||||
})
|
||||
|
@ -65,13 +67,13 @@ describe("fetch bindable properties", () => {
|
|||
expect(contextBindings.length).toBe(8)
|
||||
|
||||
const namebinding_parent = contextBindings.find(
|
||||
b => b.runtimeBinding === "parent.data.name"
|
||||
b => b.runtimeBinding === "list-id.name"
|
||||
)
|
||||
expect(namebinding_parent).toBeDefined()
|
||||
expect(namebinding_parent.readableBinding).toBe("list-name.Test Table.name")
|
||||
|
||||
const descriptionbinding_parent = contextBindings.find(
|
||||
b => b.runtimeBinding === "parent.data.description"
|
||||
b => b.runtimeBinding === "list-id.description"
|
||||
)
|
||||
expect(descriptionbinding_parent).toBeDefined()
|
||||
expect(descriptionbinding_parent.readableBinding).toBe(
|
||||
|
@ -79,7 +81,7 @@ describe("fetch bindable properties", () => {
|
|||
)
|
||||
|
||||
const namebinding_own = contextBindings.find(
|
||||
b => b.runtimeBinding === "data.name"
|
||||
b => b.runtimeBinding === "child-list-id.name"
|
||||
)
|
||||
expect(namebinding_own).toBeDefined()
|
||||
expect(namebinding_own.readableBinding).toBe(
|
||||
|
@ -87,7 +89,7 @@ describe("fetch bindable properties", () => {
|
|||
)
|
||||
|
||||
const descriptionbinding_own = contextBindings.find(
|
||||
b => b.runtimeBinding === "data.description"
|
||||
b => b.runtimeBinding === "child-list-id.description"
|
||||
)
|
||||
expect(descriptionbinding_own).toBeDefined()
|
||||
expect(descriptionbinding_own.readableBinding).toBe(
|
||||
|
@ -104,7 +106,7 @@ describe("fetch bindable properties", () => {
|
|||
r => r.instance._id === "list-item-input-id" && r.type === "instance"
|
||||
)
|
||||
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", () => {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,5 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
release/
|
||||
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",
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"test": "jest",
|
||||
"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": {
|
||||
"deep-equal": "^2.0.1",
|
||||
"mustache": "^4.0.1",
|
||||
"regexparam": "^1.3.0"
|
||||
"regexparam": "^1.3.0",
|
||||
"svelte-spa-router": "^3.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.5.5",
|
||||
"@babel/plugin-transform-runtime": "^7.5.5",
|
||||
"@babel/preset-env": "^7.5.5",
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"babel-jest": "^24.8.0",
|
||||
"@budibase/standard-components": "^0.3.8",
|
||||
"@rollup/plugin-commonjs": "^16.0.0",
|
||||
"@rollup/plugin-node-resolve": "^10.0.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"jest": "^24.8.0",
|
||||
"jsdom": "^16.0.1",
|
||||
"rollup": "^1.12.0",
|
||||
"rollup-plugin-commonjs": "^10.0.0",
|
||||
"rollup": "^2.33.2",
|
||||
"rollup-plugin-node-builtins": "^2.1.2",
|
||||
"rollup-plugin-node-globals": "^1.4.0",
|
||||
"rollup-plugin-svelte": "^6.1.1",
|
||||
"rollup-plugin-node-resolve": "^5.2.0",
|
||||
"rollup-plugin-terser": "^4.0.4",
|
||||
"svelte": "^3.29.7",
|
||||
"svelte": "^3.30.0",
|
||||
"svelte-jester": "^1.0.6"
|
||||
},
|
||||
"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 nodeglobals from "rollup-plugin-node-globals"
|
||||
import svelte from "rollup-plugin-svelte"
|
||||
|
||||
const production = !process.env.ROLLUP_WATCH
|
||||
|
||||
export default {
|
||||
input: "src/index.js",
|
||||
output: [
|
||||
{
|
||||
sourcemap: true,
|
||||
format: "iife",
|
||||
name: "app",
|
||||
file: `./dist/budibase-client.js`,
|
||||
},
|
||||
{
|
||||
file: "dist/budibase-client.esm.mjs",
|
||||
format: "esm",
|
||||
sourcemap: "inline",
|
||||
file: `./dist/budibase-client.js`,
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
svelte({
|
||||
dev: !production,
|
||||
}),
|
||||
resolve({
|
||||
preferBuiltins: true,
|
||||
browser: true,
|
||||
dedupe: ["svelte", "svelte/internal"],
|
||||
}),
|
||||
commonjs(),
|
||||
builtins(),
|
||||
nodeglobals(),
|
||||
],
|
||||
watch: {
|
||||
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"
|
||||
import { getAppId } from "../render/getAppId"
|
||||
|
||||
export async function baseApiCall(method, url, body) {
|
||||
return await fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-budibase-app-id": getAppId(window.document.cookie),
|
||||
"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,
|
||||
}
|
||||
export * from "./rows"
|
||||
export * from "./auth"
|
||||
export * from "./datasources"
|
||||
export * from "./tables"
|
||||
export * from "./attachments"
|
||||
export * from "./views"
|
||||
export * from "./relationships"
|
||||
export * from "./routes"
|
||||
export * from "./app"
|
||||
|
|
|
@ -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,53 @@
|
|||
<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)
|
||||
|
||||
// Update component context
|
||||
// ID is duplicated inside style so that the "styleable" helper can set
|
||||
// an ID data tag for unique reference to components
|
||||
$: componentStore.set({ id, styles: { ...definition._styles, id } })
|
||||
|
||||
// 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 { builtins, builtinLibName } from "./render/builtinComponents"
|
||||
import { getAppId } from "./render/getAppId"
|
||||
import ClientApp from "./components/ClientApp.svelte"
|
||||
import { builderStore } from "./store"
|
||||
|
||||
/**
|
||||
* 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##"]
|
||||
let app
|
||||
|
||||
const user = {}
|
||||
|
||||
const componentLibraryModules = (opts && opts.componentLibraries) || {}
|
||||
|
||||
const libraries = frontendDefinition.libraries || []
|
||||
|
||||
for (let library of libraries) {
|
||||
// fetch the JavaScript for the component libraries from the server
|
||||
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 loadBudibase = () => {
|
||||
// Update builder store with any builder flags
|
||||
builderStore.set({
|
||||
inBuilder: !!window["##BUDIBASE_IN_BUILDER##"],
|
||||
page: window["##BUDIBASE_PREVIEW_PAGE##"],
|
||||
screen: window["##BUDIBASE_PREVIEW_SCREEN##"],
|
||||
selectedComponentId: window["##BUDIBASE_SELECTED_COMPONENT_ID##"],
|
||||
previewId: window["##BUDIBASE_PREVIEW_ID##"],
|
||||
})
|
||||
|
||||
const route = _window.location
|
||||
? _window.location.pathname.replace(`${appId}/`, "").replace(appId, "")
|
||||
: ""
|
||||
|
||||
initialisePage(frontendDefinition.page, _window.document.body, route)
|
||||
|
||||
return {
|
||||
screenStore,
|
||||
pageStore,
|
||||
routeTo,
|
||||
rootNode,
|
||||
// Create app if one hasn't been created yet
|
||||
if (!app) {
|
||||
app = new ClientApp({
|
||||
target: window.document.body,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (window) {
|
||||
window.loadBudibase = loadBudibase
|
||||
}
|
||||
// Attach to window so the HTML template can call this when it loads
|
||||
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 }) {
|
||||
if (!cookies) {
|
||||
return undefined
|
||||
}
|
||||
const cookie = cookies
|
||||
.split(COOKIE_SEPARATOR)
|
||||
.find(cookie => cookie.trim().startsWith("budibase:currentapp"))
|
||||
|
@ -30,7 +33,7 @@ function tryGetFromSubdomain() {
|
|||
return confirmAppId(appId)
|
||||
}
|
||||
|
||||
export const getAppId = cookies => {
|
||||
export const getAppId = (cookies = window.document.cookie) => {
|
||||
const functions = [tryGetFromSubdomain, tryGetFromPath, tryGetFromCookie]
|
||||
// try getting the app Id in order
|
||||
let appId
|
||||
|
@ -42,5 +45,3 @@ export const getAppId = cookies => {
|
|||
}
|
||||
return appId
|
||||
}
|
||||
|
||||
export const getAppIdFromPath = getAppId
|
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* Helper to build a CSS string from a style object
|
||||
*/
|
||||
const buildStyleString = styles => {
|
||||
let str = ""
|
||||
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 normalStyles = newStyles.normal || {}
|
||||
const hoverStyles = {
|
||||
...normalStyles,
|
||||
...newStyles.hover,
|
||||
}
|
||||
|
||||
applyNormalStyles = () => {
|
||||
node.style = buildStyleString(normalStyles)
|
||||
}
|
||||
|
||||
applyHoverStyles = () => {
|
||||
node.style = buildStyleString(hoverStyles)
|
||||
}
|
||||
|
||||
// Add listeners to toggle hover styles
|
||||
node.addEventListener("mouseover", applyHoverStyles)
|
||||
node.addEventListener("mouseout", applyNormalStyles)
|
||||
node.setAttribute("data-bb-id", newStyles.id)
|
||||
|
||||
// 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-replication-stream": "^1.2.9",
|
||||
"sanitize-s3-objectkey": "^0.0.1",
|
||||
"svelte": "^3.29.4",
|
||||
"svelte": "^3.30.0",
|
||||
"tar-fs": "^2.1.0",
|
||||
"to-json-schema": "^0.2.5",
|
||||
"uuid": "^3.3.2",
|
||||
|
|
|
@ -31,15 +31,13 @@ const MAIN = {
|
|||
selected: {},
|
||||
},
|
||||
_code: "",
|
||||
className: "",
|
||||
onLoad: [],
|
||||
type: "div",
|
||||
_appId: "inst_app_80b_f158d4057d2c4bedb0042d42fda8abaf",
|
||||
_instanceName: "Header",
|
||||
_children: [
|
||||
{
|
||||
_id: "49e0e519-9e5e-4127-885a-ee6a0a49e2c1",
|
||||
_component: "@budibase/standard-components/Navigation",
|
||||
_component: "@budibase/standard-components/navigation",
|
||||
_styles: {
|
||||
normal: {
|
||||
"max-width": "1400px",
|
||||
|
@ -58,12 +56,6 @@ const MAIN = {
|
|||
_code: "",
|
||||
logoUrl:
|
||||
"https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg",
|
||||
title: "",
|
||||
backgroundColor: "",
|
||||
color: "",
|
||||
borderWidth: "",
|
||||
borderColor: "",
|
||||
borderStyle: "",
|
||||
_appId: "inst_cf8ace4_69efc0d72e6f443db2d4c902c14d9394",
|
||||
_instanceName: "Navigation",
|
||||
_children: [
|
||||
|
@ -88,11 +80,6 @@ const MAIN = {
|
|||
url: "/",
|
||||
openInNewTab: false,
|
||||
text: "Home",
|
||||
color: "",
|
||||
hoverColor: "",
|
||||
underline: false,
|
||||
fontSize: "",
|
||||
fontFamily: "initial",
|
||||
_appId: "inst_cf8ace4_69efc0d72e6f443db2d4c902c14d9394",
|
||||
_instanceName: "Home Link",
|
||||
_children: [],
|
||||
|
@ -143,8 +130,6 @@ const MAIN = {
|
|||
selected: {},
|
||||
},
|
||||
_code: "",
|
||||
className: "",
|
||||
onLoad: [],
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -181,13 +166,7 @@ const UNAUTHENTICATED = {
|
|||
selected: {},
|
||||
},
|
||||
_code: "",
|
||||
loginRedirect: "",
|
||||
usernameLabel: "Username",
|
||||
passwordLabel: "Password",
|
||||
loginButtonLabel: "Login",
|
||||
buttonClass: "",
|
||||
_instanceName: "Login",
|
||||
inputClass: "",
|
||||
_children: [],
|
||||
title: "Log in to {{ name }}",
|
||||
buttonText: "Log In",
|
||||
|
@ -213,8 +192,6 @@ const UNAUTHENTICATED = {
|
|||
selected: {},
|
||||
},
|
||||
_code: "",
|
||||
className: "",
|
||||
onLoad: [],
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -19,8 +19,6 @@ exports.HOME_SCREEN = {
|
|||
selected: {},
|
||||
},
|
||||
_code: "",
|
||||
className: "",
|
||||
onLoad: [],
|
||||
type: "div",
|
||||
_children: [
|
||||
{
|
||||
|
@ -35,7 +33,6 @@ exports.HOME_SCREEN = {
|
|||
selected: {},
|
||||
},
|
||||
_code: "",
|
||||
className: "",
|
||||
text: "Welcome to your Budibase App 👋",
|
||||
type: "h2",
|
||||
_appId: "inst_cf8ace4_69efc0d72e6f443db2d4c902c14d9394",
|
||||
|
@ -61,8 +58,6 @@ exports.HOME_SCREEN = {
|
|||
selected: {},
|
||||
},
|
||||
_code: "",
|
||||
className: "",
|
||||
onLoad: [],
|
||||
type: "div",
|
||||
_appId: "inst_app_2cc_ca3383f896034e9295345c05f7dfca0c",
|
||||
_instanceName: "Video Container",
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
dist/
|
||||
|
|
|
@ -13,18 +13,12 @@
|
|||
"embed": "string"
|
||||
}
|
||||
},
|
||||
"Navigation": {
|
||||
"navigation": {
|
||||
"name": "Navigation",
|
||||
"description": "A basic header navigation component",
|
||||
"children": true,
|
||||
"props": {
|
||||
"logoUrl": "string",
|
||||
"title": "string",
|
||||
"backgroundColor": "string",
|
||||
"color": "string",
|
||||
"borderWidth": "string",
|
||||
"borderColor": "string",
|
||||
"borderStyle": "string"
|
||||
"logoUrl": "string"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
|
@ -32,7 +26,6 @@
|
|||
"description": "an html <button />",
|
||||
"props": {
|
||||
"text": "string",
|
||||
"className": "string",
|
||||
"disabled": "bool",
|
||||
"onClick": "event"
|
||||
},
|
||||
|
@ -65,23 +58,8 @@
|
|||
"name": "Login Control",
|
||||
"description": "A control that accepts username, password an also handles password resets",
|
||||
"props": {
|
||||
"logo": "asset",
|
||||
"loginRedirect": "string",
|
||||
"logo": "string",
|
||||
"title": "string",
|
||||
"usernameLabel": {
|
||||
"type": "string",
|
||||
"default": "Username"
|
||||
},
|
||||
"passwordLabel": {
|
||||
"type": "string",
|
||||
"default": "Password"
|
||||
},
|
||||
"loginButtonLabel": {
|
||||
"type": "string",
|
||||
"default": "Login"
|
||||
},
|
||||
"buttonClass": "string",
|
||||
"inputClass": "string",
|
||||
"buttonText": "string"
|
||||
},
|
||||
"tags": [
|
||||
|
@ -96,7 +74,6 @@
|
|||
"bindable": "value",
|
||||
"description": "An HTML input",
|
||||
"props": {
|
||||
"value": "string",
|
||||
"type": {
|
||||
"type": "options",
|
||||
"options": [
|
||||
|
@ -122,36 +99,15 @@
|
|||
"week"
|
||||
],
|
||||
"default": "text"
|
||||
},
|
||||
"onChange": "event",
|
||||
"className": "string"
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"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": {
|
||||
"name": "Text",
|
||||
"description": "stylable block of text",
|
||||
"children": false,
|
||||
"props": {
|
||||
"text": "string",
|
||||
"type": {
|
||||
|
@ -164,16 +120,6 @@
|
|||
"container"
|
||||
]
|
||||
},
|
||||
"textfield": {
|
||||
"name": "Textfield",
|
||||
"description": "A component that allows the user to input text.",
|
||||
"props": {
|
||||
"label": "string",
|
||||
"type": "string",
|
||||
"value": "string",
|
||||
"onchange": "event"
|
||||
}
|
||||
},
|
||||
"richtext": {
|
||||
"name": "Rich Text",
|
||||
"description": "A component that allows the user to enter long form text.",
|
||||
|
@ -181,28 +127,6 @@
|
|||
"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": {
|
||||
"description": "A HTML icon tag",
|
||||
"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": {
|
||||
"name": "Grid",
|
||||
"description": "a datagrid component with functionality to add, remove and edit rows.",
|
||||
|
@ -269,21 +182,6 @@
|
|||
"data": true,
|
||||
"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": {
|
||||
"name": "Repeater",
|
||||
"description": "A configurable data list that attaches to your backend tables.",
|
||||
|
@ -696,36 +594,13 @@
|
|||
"props": {
|
||||
"url": "string",
|
||||
"openInNewTab": "bool",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
"text": "string"
|
||||
}
|
||||
},
|
||||
"image": {
|
||||
"description": "an HTML <img> tag",
|
||||
"props": {
|
||||
"url": "string",
|
||||
"className": "string",
|
||||
"description": "string",
|
||||
"height": "string",
|
||||
"width": "string"
|
||||
"url": "string"
|
||||
}
|
||||
},
|
||||
"container": {
|
||||
|
@ -733,8 +608,6 @@
|
|||
"children": true,
|
||||
"description": "An element that contains and lays out other elements. e.g. <div>, <header> etc",
|
||||
"props": {
|
||||
"className": "string",
|
||||
"onLoad": "event",
|
||||
"type": {
|
||||
"type": "options",
|
||||
"options": [
|
||||
|
@ -766,7 +639,6 @@
|
|||
"name": "Heading",
|
||||
"description": "An HTML H1 - H6 tag",
|
||||
"props": {
|
||||
"className": "string",
|
||||
"text": "string",
|
||||
"type": {
|
||||
"type": "options",
|
||||
|
@ -782,19 +654,5 @@
|
|||
}
|
||||
},
|
||||
"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",
|
||||
"svelte": "src/index.svelte",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"prepublishOnly": "npm run build",
|
||||
"postpublish": "node scripts/deploy.js",
|
||||
"testbuild": "rollup -w -c rollup.testconfig.js",
|
||||
"dev": "run-p start:dev testbuild",
|
||||
"start:dev": "sirv public --single --dev",
|
||||
"dev:builder": "rollup -cw"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@budibase/client": "^0.3.8",
|
||||
"@rollup/plugin-commonjs": "^11.1.0",
|
||||
"@rollup/plugin-alias": "^3.1.1",
|
||||
"@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",
|
||||
"rollup": "^2.11.2",
|
||||
"rollup-plugin-commonjs": "^10.0.2",
|
||||
"rollup-plugin-json": "^4.0.0",
|
||||
"rollup-plugin-livereload": "^1.0.1",
|
||||
"rollup-plugin-node-resolve": "^5.0.0",
|
||||
"rollup-plugin-postcss": "^3.1.5",
|
||||
"rollup-plugin-svelte": "^6.1.1",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"shortid": "^2.2.15",
|
||||
"sirv-cli": "^0.4.4",
|
||||
"svelte": "^3.29.0"
|
||||
"svelte": "^3.30.0"
|
||||
},
|
||||
"keywords": [
|
||||
"svelte"
|
||||
|
@ -35,12 +32,10 @@
|
|||
"license": "MIT",
|
||||
"gitHead": "284cceb9b703c38566c6e6363c022f79a08d5691",
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.50.1",
|
||||
"@budibase/bbui": "^1.51.0",
|
||||
"@budibase/svelte-ag-grid": "^0.0.16",
|
||||
"@fortawesome/fontawesome-free": "^5.14.0",
|
||||
"@svelteschool/svelte-forms": "^0.7.0",
|
||||
"apexcharts": "^3.22.1",
|
||||
"fast-sort": "^2.2.0",
|
||||
"flatpickr": "^4.6.6",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"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 resolve from "@rollup/plugin-node-resolve"
|
||||
import svelte from "rollup-plugin-svelte"
|
||||
import postcss from "rollup-plugin-postcss"
|
||||
import { terser } from "rollup-plugin-terser"
|
||||
|
||||
const production = !process.env.ROLLUP_WATCH
|
||||
const lodash_fp_exports = ["isEmpty"]
|
||||
const externals = ["svelte", "svelte/internal"]
|
||||
|
||||
export default {
|
||||
external: externals,
|
||||
input: "src/index.js",
|
||||
output: [
|
||||
{
|
||||
file: "dist/index.js",
|
||||
format: "esm",
|
||||
name: "budibaseStandardComponents",
|
||||
sourcemap: true,
|
||||
sourcemap: false,
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
// Only run terser in production environments
|
||||
production && terser(),
|
||||
postcss({
|
||||
plugins: [],
|
||||
}),
|
||||
postcss(),
|
||||
svelte({
|
||||
hydratable: true,
|
||||
dev: !production,
|
||||
}),
|
||||
resolve({
|
||||
browser: true,
|
||||
skip: externals,
|
||||
}),
|
||||
commonjs({
|
||||
namedExports: {
|
||||
"lodash/fp": lodash_fp_exports,
|
||||
},
|
||||
}),
|
||||
commonjs(),
|
||||
],
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
import { getContext } from "svelte"
|
||||
|
||||
const { styleable } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
|
||||
export let className = "default"
|
||||
export let disabled = false
|
||||
export let text
|
||||
|
||||
export let _bb
|
||||
let theButton
|
||||
|
||||
$: if (_bb.props._children && _bb.props._children.length > 0)
|
||||
theButton && _bb.attachChildren(theButton)
|
||||
|
||||
const clickHandler = () => {
|
||||
_bb.call("onClick")
|
||||
}
|
||||
export let onClick
|
||||
</script>
|
||||
|
||||
<button
|
||||
bind:this={theButton}
|
||||
class="default"
|
||||
disabled={disabled || false}
|
||||
on:click|once={clickHandler}>
|
||||
{#if !_bb.props._children || _bb.props._children.length === 0}{text}{/if}
|
||||
use:styleable={$component.styles}
|
||||
on:click={onClick}>
|
||||
{text}
|
||||
</button>
|
||||
|
||||
<style>
|
||||
|
@ -37,28 +33,4 @@
|
|||
white-space: nowrap;
|
||||
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>
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
<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 let imageUrl = ""
|
||||
|
@ -22,7 +26,10 @@
|
|||
$: showImage = !!imageUrl
|
||||
</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}
|
||||
<div class="content">
|
||||
<h2 class="heading">{heading}</h2>
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
<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 let imageUrl = ""
|
||||
|
@ -25,7 +29,10 @@
|
|||
$: showImage = !!imageUrl
|
||||
</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}
|
||||
<div class="content">
|
||||
<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>
|
||||
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 _bb
|
||||
|
||||
let containerElement
|
||||
let hasLoaded
|
||||
let currentChildren
|
||||
|
||||
$: {
|
||||
if (containerElement) {
|
||||
_bb.attachChildren(containerElement)
|
||||
if (!hasLoaded) {
|
||||
_bb.call("onLoad")
|
||||
hasLoaded = true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if type === 'div'}
|
||||
<div bind:this={containerElement} />
|
||||
<div use:styleable={$component.styles}>
|
||||
<slot />
|
||||
</div>
|
||||
{:else if type === 'header'}
|
||||
<header bind:this={containerElement} />
|
||||
<header use:styleable={$component.styles}>
|
||||
<slot />
|
||||
</header>
|
||||
{:else if type === 'main'}
|
||||
<main bind:this={containerElement} />
|
||||
<main use:styleable={$component.styles}>
|
||||
<slot />
|
||||
</main>
|
||||
{:else if type === 'footer'}
|
||||
<footer bind:this={containerElement} />
|
||||
<footer use:styleable={$component.styles}>
|
||||
<slot />
|
||||
</footer>
|
||||
{:else if type === 'aside'}
|
||||
<aside bind:this={containerElement} />
|
||||
<aside use:styleable={$component.styles}>
|
||||
<slot />
|
||||
</aside>
|
||||
{:else if type === 'summary'}
|
||||
<summary bind:this={containerElement} />
|
||||
<summary use:styleable={$component.styles}>
|
||||
<slot />
|
||||
</summary>
|
||||
{:else if type === 'details'}
|
||||
<details bind:this={containerElement} />
|
||||
<details use:styleable={$component.styles}>
|
||||
<slot />
|
||||
</details>
|
||||
{:else if type === 'article'}
|
||||
<article bind:this={containerElement} />
|
||||
<article use:styleable={$component.styles}>
|
||||
<slot />
|
||||
</article>
|
||||
{:else if type === 'nav'}
|
||||
<nav bind:this={containerElement} />
|
||||
<nav use:styleable={$component.styles}>
|
||||
<slot />
|
||||
</nav>
|
||||
{:else if type === 'mark'}
|
||||
<mark bind:this={containerElement} />
|
||||
<mark use:styleable={$component.styles}>
|
||||
<slot />
|
||||
</mark>
|
||||
{:else if type === 'figure'}
|
||||
<figure bind:this={containerElement} />
|
||||
<figure use:styleable={$component.styles}>
|
||||
<slot />
|
||||
</figure>
|
||||
{:else if type === 'figcaption'}
|
||||
<figcaption bind:this={containerElement} />
|
||||
<figcaption use:styleable={$component.styles}>
|
||||
<slot />
|
||||
</figcaption>
|
||||
{:else if type === 'paragraph'}
|
||||
<p bind:this={containerElement} />
|
||||
<p use:styleable={$component.styles}>
|
||||
<slot />
|
||||
</p>
|
||||
{/if}
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
<script>
|
||||
import Form from "./Form.svelte"
|
||||
|
||||
export let _bb
|
||||
export let table
|
||||
export let title
|
||||
export let buttonText
|
||||
</script>
|
||||
|
||||
<Form {_bb} {table} {title} {buttonText} wide={false} />
|
||||
<Form wide={false} />
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
<script>
|
||||
import Form from "./Form.svelte"
|
||||
|
||||
export let _bb
|
||||
export let table
|
||||
export let title
|
||||
export let buttonText
|
||||
</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>
|
||||
import Flatpickr from "svelte-flatpickr"
|
||||
import { DatePicker } from "@budibase/bbui"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
const { styleable, setBindableValue } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
|
||||
export let placeholder
|
||||
export let value
|
||||
|
||||
export let _bb
|
||||
let value
|
||||
$: setBindableValue(value, $component.id)
|
||||
|
||||
function handleChange(event) {
|
||||
const [fullDate, dateStr, instance] = event.detail
|
||||
if (_bb) {
|
||||
_bb.setBinding("value", fullDate)
|
||||
}
|
||||
const [fullDate] = event.detail
|
||||
value = fullDate
|
||||
}
|
||||
</script>
|
||||
|
||||
<DatePicker {placeholder} on:change={handleChange} {value} />
|
||||
<div use:styleable={$component.styles}>
|
||||
<DatePicker {placeholder} on:change={handleChange} {value} />
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,23 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
|
||||
const { styleable } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
|
||||
export let embed
|
||||
</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>
|
||||
import { getContext } from "svelte"
|
||||
import { Label, DatePicker, Input, Select, Toggle } from "@budibase/bbui"
|
||||
import Dropzone from "./attachments/Dropzone.svelte"
|
||||
import LinkedRowSelector from "./LinkedRowSelector.svelte"
|
||||
import ErrorsBox from "./ErrorsBox.svelte"
|
||||
import { capitalise } from "./helpers"
|
||||
|
||||
export let _bb
|
||||
export let table
|
||||
const { styleable, API } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
const dataContext = getContext("data")
|
||||
|
||||
export let wide = false
|
||||
|
||||
let store = _bb.store
|
||||
let schema = {}
|
||||
let rowId
|
||||
let errors = {}
|
||||
let row
|
||||
let schema
|
||||
let fields = []
|
||||
|
||||
$: schema = $store.data && $store.data._table && $store.data._table.schema
|
||||
$: fields = schema ? Object.keys(schema) : []
|
||||
// Fetch info about the closest data context
|
||||
$: 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>
|
||||
|
||||
<div class="form-content">
|
||||
<ErrorsBox errors={$store.saveRowErrors || {}} />
|
||||
<div class="form-content" use:styleable={$component.styles}>
|
||||
<!-- <ErrorsBox errors={$store.saveRowErrors || {}} />-->
|
||||
{#each fields as field}
|
||||
<div class="form-field" class:wide>
|
||||
{#if !(schema[field].type === 'boolean' && !wide)}
|
||||
<Label extraSmall={!wide} grey>{capitalise(schema[field].name)}</Label>
|
||||
{/if}
|
||||
{#if schema[field].type === 'options'}
|
||||
<Select secondary bind:value={$store.data[field]}>
|
||||
<Select secondary bind:value={row[field]}>
|
||||
<option value="">Choose an option</option>
|
||||
{#each schema[field].constraints.inclusion as opt}
|
||||
<option>{opt}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{:else if schema[field].type === 'datetime'}
|
||||
<DatePicker bind:value={$store.data[field]} />
|
||||
<DatePicker bind:value={row[field]} />
|
||||
{:else if schema[field].type === 'boolean'}
|
||||
<Toggle
|
||||
text={wide ? null : capitalise(schema[field].name)}
|
||||
bind:checked={$store.data[field]} />
|
||||
bind:checked={row[field]} />
|
||||
{: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'}
|
||||
<Input bind:value={$store.data[field]} />
|
||||
<Input bind:value={row[field]} />
|
||||
{:else if schema[field].type === 'attachment'}
|
||||
<Dropzone bind:files={$store.data[field]} />
|
||||
<Dropzone bind:files={row[field]} />
|
||||
{:else if schema[field].type === 'link'}
|
||||
<LinkedRowSelector
|
||||
secondary
|
||||
showLabel={false}
|
||||
bind:linkedRows={$store.data[field]}
|
||||
bind:linkedRows={row[field]}
|
||||
schema={schema[field]} />
|
||||
{/if}
|
||||
</div>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue