Merge branch 'component-sdk' of github.com:Budibase/budibase into feature/page-refactor
This commit is contained in:
commit
6aa2e53a40
|
@ -26,7 +26,7 @@
|
||||||
"nuke": "rimraf ~/.budibase && npm run restore",
|
"nuke": "rimraf ~/.budibase && npm run restore",
|
||||||
"clean": "lerna clean",
|
"clean": "lerna clean",
|
||||||
"kill-port": "kill-port 4001",
|
"kill-port": "kill-port 4001",
|
||||||
"dev": "npm run kill-port && node ./scripts/symlinkDev.js && lerna run --parallel dev:builder --concurrency 1",
|
"dev": "yarn run kill-port && node ./scripts/symlinkDev.js && lerna run --parallel dev:builder --concurrency 1",
|
||||||
"test": "lerna run test",
|
"test": "lerna run test",
|
||||||
"lint": "eslint packages",
|
"lint": "eslint packages",
|
||||||
"lint:fix": "eslint --fix packages",
|
"lint:fix": "eslint --fix packages",
|
||||||
|
|
|
@ -90,6 +90,7 @@
|
||||||
"@babel/preset-env": "^7.5.5",
|
"@babel/preset-env": "^7.5.5",
|
||||||
"@babel/runtime": "^7.5.5",
|
"@babel/runtime": "^7.5.5",
|
||||||
"@rollup/plugin-alias": "^3.0.1",
|
"@rollup/plugin-alias": "^3.0.1",
|
||||||
|
"@rollup/plugin-commonjs": "^16.0.0",
|
||||||
"@rollup/plugin-json": "^4.0.3",
|
"@rollup/plugin-json": "^4.0.3",
|
||||||
"@sveltech/routify": "1.7.11",
|
"@sveltech/routify": "1.7.11",
|
||||||
"@testing-library/jest-dom": "^5.11.0",
|
"@testing-library/jest-dom": "^5.11.0",
|
||||||
|
@ -104,7 +105,6 @@
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rollup": "^2.11.2",
|
"rollup": "^2.11.2",
|
||||||
"rollup-plugin-alias": "^1.5.2",
|
"rollup-plugin-alias": "^1.5.2",
|
||||||
"rollup-plugin-commonjs": "^10.0.0",
|
|
||||||
"rollup-plugin-copy": "^3.0.0",
|
"rollup-plugin-copy": "^3.0.0",
|
||||||
"rollup-plugin-css-only": "^2.1.0",
|
"rollup-plugin-css-only": "^2.1.0",
|
||||||
"rollup-plugin-livereload": "^1.0.0",
|
"rollup-plugin-livereload": "^1.0.0",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import alias from "@rollup/plugin-alias"
|
import alias from "@rollup/plugin-alias"
|
||||||
import svelte from "rollup-plugin-svelte"
|
import svelte from "rollup-plugin-svelte"
|
||||||
import resolve from "rollup-plugin-node-resolve"
|
import resolve from "rollup-plugin-node-resolve"
|
||||||
import commonjs from "rollup-plugin-commonjs"
|
import commonjs from "@rollup/plugin-commonjs"
|
||||||
import url from "rollup-plugin-url"
|
import url from "rollup-plugin-url"
|
||||||
import livereload from "rollup-plugin-livereload"
|
import livereload from "rollup-plugin-livereload"
|
||||||
import { terser } from "rollup-plugin-terser"
|
import { terser } from "rollup-plugin-terser"
|
||||||
|
@ -15,102 +15,7 @@ import json from "@rollup/plugin-json"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
const production = !process.env.ROLLUP_WATCH
|
const production = !process.env.ROLLUP_WATCH
|
||||||
|
|
||||||
const lodash_fp_exports = [
|
|
||||||
"flow",
|
|
||||||
"pipe",
|
|
||||||
"union",
|
|
||||||
"reduce",
|
|
||||||
"isUndefined",
|
|
||||||
"cloneDeep",
|
|
||||||
"split",
|
|
||||||
"some",
|
|
||||||
"map",
|
|
||||||
"filter",
|
|
||||||
"isEmpty",
|
|
||||||
"countBy",
|
|
||||||
"includes",
|
|
||||||
"last",
|
|
||||||
"find",
|
|
||||||
"constant",
|
|
||||||
"take",
|
|
||||||
"first",
|
|
||||||
"intersection",
|
|
||||||
"mapValues",
|
|
||||||
"isNull",
|
|
||||||
"has",
|
|
||||||
"isInteger",
|
|
||||||
"isNumber",
|
|
||||||
"isString",
|
|
||||||
"isBoolean",
|
|
||||||
"isDate",
|
|
||||||
"isArray",
|
|
||||||
"isObject",
|
|
||||||
"clone",
|
|
||||||
"values",
|
|
||||||
"keyBy",
|
|
||||||
"isNaN",
|
|
||||||
"keys",
|
|
||||||
"orderBy",
|
|
||||||
"concat",
|
|
||||||
"reverse",
|
|
||||||
"difference",
|
|
||||||
"merge",
|
|
||||||
"flatten",
|
|
||||||
"each",
|
|
||||||
"pull",
|
|
||||||
"join",
|
|
||||||
"defaultCase",
|
|
||||||
"uniqBy",
|
|
||||||
"every",
|
|
||||||
"uniqWith",
|
|
||||||
"isFunction",
|
|
||||||
"groupBy",
|
|
||||||
"differenceBy",
|
|
||||||
"intersectionBy",
|
|
||||||
"isEqual",
|
|
||||||
"max",
|
|
||||||
"sortBy",
|
|
||||||
"assign",
|
|
||||||
"uniq",
|
|
||||||
"trimChars",
|
|
||||||
"trimCharsStart",
|
|
||||||
"isObjectLike",
|
|
||||||
"flattenDeep",
|
|
||||||
"indexOf",
|
|
||||||
"isPlainObject",
|
|
||||||
"toNumber",
|
|
||||||
"takeRight",
|
|
||||||
"toPairs",
|
|
||||||
"remove",
|
|
||||||
"findIndex",
|
|
||||||
"compose",
|
|
||||||
"get",
|
|
||||||
"tap",
|
|
||||||
]
|
|
||||||
|
|
||||||
const lodash_exports = [
|
|
||||||
"flow",
|
|
||||||
"join",
|
|
||||||
"replace",
|
|
||||||
"trim",
|
|
||||||
"dropRight",
|
|
||||||
"takeRight",
|
|
||||||
"head",
|
|
||||||
"reduce",
|
|
||||||
"tail",
|
|
||||||
"startsWith",
|
|
||||||
"findIndex",
|
|
||||||
"merge",
|
|
||||||
"assign",
|
|
||||||
"each",
|
|
||||||
"find",
|
|
||||||
"orderBy",
|
|
||||||
"union",
|
|
||||||
]
|
|
||||||
|
|
||||||
const outputpath = "../server/builder"
|
const outputpath = "../server/builder"
|
||||||
|
|
||||||
const coreExternal = [
|
const coreExternal = [
|
||||||
"lodash",
|
"lodash",
|
||||||
"lodash/fp",
|
"lodash/fp",
|
||||||
|
@ -224,13 +129,7 @@ export default {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
commonjs({
|
commonjs(),
|
||||||
namedExports: {
|
|
||||||
"lodash/fp": lodash_fp_exports,
|
|
||||||
lodash: lodash_exports,
|
|
||||||
shortid: ["generate"],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
url({
|
url({
|
||||||
limit: 0,
|
limit: 0,
|
||||||
include: ["**/*.woff2", "**/*.png"],
|
include: ["**/*.woff2", "**/*.png"],
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const getComponentTypeName = component => {
|
const getComponentTypeName = component => {
|
||||||
let [componentName] = component._component.match(/[a-z]*$/)
|
let [componentName] = component._component.match(/[a-zA-Z]*$/)
|
||||||
return componentName || "element"
|
return componentName || "element"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ export default `<html>
|
||||||
*, *:before, *:after {
|
*, *:before, *:after {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.container-screenslot-placeholder {
|
[data-bb-id="container-screenslot-placeholder"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -23,7 +23,7 @@ export default `<html>
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
.container-screenslot-placeholder span {
|
[data-bb-id="container-screenslot-placeholder"] span {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
@ -46,16 +46,16 @@ export default `<html>
|
||||||
|
|
||||||
selectedComponentStyle = document.createElement('style');
|
selectedComponentStyle = document.createElement('style');
|
||||||
document.head.appendChild(selectedComponentStyle)
|
document.head.appendChild(selectedComponentStyle)
|
||||||
var selectedCss = '.' + data.selectedComponentType + '-' + data.selectedComponentId + '{ border: 2px solid #0055ff; }'
|
var selectedCss = '[data-bb-id="' + data.selectedComponentType + '-' + data.selectedComponentId + '"]' + '{border:2px solid #0055ff !important;}'
|
||||||
selectedComponentStyle.appendChild(document.createTextNode(selectedCss))
|
selectedComponentStyle.appendChild(document.createTextNode(selectedCss))
|
||||||
|
|
||||||
styles = document.createElement('style')
|
styles = document.createElement('style')
|
||||||
document.head.appendChild(styles)
|
document.head.appendChild(styles)
|
||||||
styles.appendChild(document.createTextNode(data.styles))
|
styles.appendChild(document.createTextNode(data.styles))
|
||||||
|
|
||||||
window["##BUDIBASE_FRONTEND_DEFINITION##"] = data.frontendDefinition;
|
window["##BUDIBASE_IN_BUILDER##"] = true;
|
||||||
if (window.loadBudibase) {
|
if (window.loadBudibase) {
|
||||||
loadBudibase({ window, localStorage })
|
loadBudibase()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let styles
|
let styles
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { TextButton, Body, DropdownMenu, ModalContent } from "@budibase/bbui"
|
import { TextButton, Body, DropdownMenu, ModalContent } from "@budibase/bbui"
|
||||||
import { AddIcon, ArrowDownIcon } from "components/common/Icons/"
|
import { AddIcon, ArrowDownIcon } from "components/common/Icons/"
|
||||||
import { EVENT_TYPE_MEMBER_NAME } from "../../../../../client/src/state/eventHandlers"
|
import { EVENT_TYPE_MEMBER_NAME } from "../../../../../client/src/old/state/eventHandlers"
|
||||||
import actionTypes from "./actions"
|
import actionTypes from "./actions"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import { buildStyle } from "../../helpers.js"
|
|
||||||
export let value = ""
|
export let value = ""
|
||||||
export let text = ""
|
export let text = ""
|
||||||
export let icon = ""
|
export let icon = ""
|
||||||
export let onClick = value => {}
|
export let onClick = () => {}
|
||||||
export let selected = false
|
export let selected = false
|
||||||
|
|
||||||
$: useIcon = !!icon
|
$: useIcon = !!icon
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,13 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
presets: ["@babel/preset-env"],
|
|
||||||
sourceMaps: "inline",
|
|
||||||
retainLines: true,
|
|
||||||
plugins: [
|
|
||||||
[
|
|
||||||
"@babel/plugin-transform-runtime",
|
|
||||||
{
|
|
||||||
regenerator: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
}
|
|
|
@ -6,57 +6,28 @@
|
||||||
"module": "dist/budibase-client.esm.mjs",
|
"module": "dist/budibase-client.esm.mjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rollup -c",
|
"build": "rollup -c",
|
||||||
"test": "jest",
|
|
||||||
"dev:builder": "rollup -cw"
|
"dev:builder": "rollup -cw"
|
||||||
},
|
},
|
||||||
"jest": {
|
|
||||||
"globals": {
|
|
||||||
"GLOBALS": {
|
|
||||||
"client": "web"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"testURL": "http://test.com",
|
|
||||||
"moduleNameMapper": {
|
|
||||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/internals/mocks/fileMock.js",
|
|
||||||
"\\.(css|less|sass|scss)$": "identity-obj-proxy"
|
|
||||||
},
|
|
||||||
"moduleFileExtensions": [
|
|
||||||
"js",
|
|
||||||
"svelte"
|
|
||||||
],
|
|
||||||
"moduleDirectories": [
|
|
||||||
"node_modules"
|
|
||||||
],
|
|
||||||
"transform": {
|
|
||||||
"^.+js$": "babel-jest",
|
|
||||||
"^.+.svelte$": "svelte-jester"
|
|
||||||
},
|
|
||||||
"transformIgnorePatterns": [
|
|
||||||
"/node_modules/(?!svelte).+\\.js$"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"deep-equal": "^2.0.1",
|
"deep-equal": "^2.0.1",
|
||||||
"mustache": "^4.0.1",
|
"mustache": "^4.0.1",
|
||||||
"regexparam": "^1.3.0"
|
"regexparam": "^1.3.0",
|
||||||
|
"svelte-spa-router": "^3.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.5.5",
|
"@budibase/standard-components": "^0.3.8",
|
||||||
"@babel/plugin-transform-runtime": "^7.5.5",
|
"@rollup/plugin-commonjs": "^16.0.0",
|
||||||
"@babel/preset-env": "^7.5.5",
|
"@rollup/plugin-node-resolve": "^10.0.0",
|
||||||
"@babel/runtime": "^7.5.5",
|
|
||||||
"babel-jest": "^24.8.0",
|
|
||||||
"fs-extra": "^8.1.0",
|
"fs-extra": "^8.1.0",
|
||||||
"jest": "^24.8.0",
|
|
||||||
"jsdom": "^16.0.1",
|
"jsdom": "^16.0.1",
|
||||||
"rollup": "^1.12.0",
|
"rollup": "^2.33.2",
|
||||||
"rollup-plugin-commonjs": "^10.0.0",
|
|
||||||
"rollup-plugin-node-builtins": "^2.1.2",
|
"rollup-plugin-node-builtins": "^2.1.2",
|
||||||
"rollup-plugin-node-globals": "^1.4.0",
|
"rollup-plugin-node-globals": "^1.4.0",
|
||||||
"rollup-plugin-node-resolve": "^5.2.0",
|
"rollup-plugin-node-resolve": "^5.2.0",
|
||||||
"rollup-plugin-terser": "^4.0.4",
|
"rollup-plugin-terser": "^4.0.4",
|
||||||
"svelte": "^3.29.7",
|
"svelte": "^3.29.7",
|
||||||
"svelte-jester": "^1.0.6"
|
"svelte-jester": "^1.0.6",
|
||||||
|
"rollup-plugin-svelte": "^6.1.1"
|
||||||
},
|
},
|
||||||
"gitHead": "e4e053cb6ff9a0ddc7115b44ccaa24b8ec41fb9a"
|
"gitHead": "e4e053cb6ff9a0ddc7115b44ccaa24b8ec41fb9a"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import resolve from "rollup-plugin-node-resolve"
|
import commonjs from "@rollup/plugin-commonjs"
|
||||||
import commonjs from "rollup-plugin-commonjs"
|
import resolve from "@rollup/plugin-node-resolve"
|
||||||
import builtins from "rollup-plugin-node-builtins"
|
import builtins from "rollup-plugin-node-builtins"
|
||||||
import nodeglobals from "rollup-plugin-node-globals"
|
import svelte from "rollup-plugin-svelte"
|
||||||
|
|
||||||
|
const production = !process.env.ROLLUP_WATCH
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
input: "src/index.js",
|
input: "src/index.js",
|
||||||
|
@ -19,13 +21,16 @@ export default {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
|
svelte({
|
||||||
|
dev: !production,
|
||||||
|
}),
|
||||||
resolve({
|
resolve({
|
||||||
preferBuiltins: true,
|
preferBuiltins: true,
|
||||||
browser: true,
|
browser: true,
|
||||||
|
dedupe: ["svelte", "svelte/internal"],
|
||||||
}),
|
}),
|
||||||
commonjs(),
|
commonjs(),
|
||||||
builtins(),
|
builtins(),
|
||||||
nodeglobals(),
|
|
||||||
],
|
],
|
||||||
watch: {
|
watch: {
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
import { getAppId } from "../utils"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,31 @@
|
||||||
|
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") {
|
||||||
|
rows = await fetchRelationshipData({
|
||||||
|
rowId: dataContext?.data?._id,
|
||||||
|
tableId: dataContext?.data?.tableId,
|
||||||
|
fieldName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enrich rows
|
||||||
|
return await enrichRows(rows, tableId)
|
||||||
|
}
|
|
@ -1,120 +1,9 @@
|
||||||
import { authenticate } from "./authenticate"
|
export * from "./rows"
|
||||||
import { getAppId } from "../render/getAppId"
|
export * from "./auth"
|
||||||
|
export * from "./datasources"
|
||||||
export async function baseApiCall(method, url, body) {
|
export * from "./tables"
|
||||||
return await fetch(url, {
|
export * from "./attachments"
|
||||||
method: method,
|
export * from "./views"
|
||||||
headers: {
|
export * from "./relationships"
|
||||||
"Content-Type": "application/json",
|
export * from "./routes"
|
||||||
"x-budibase-app-id": getAppId(window.document.cookie),
|
export * from "./app"
|
||||||
"x-budibase-type": "client",
|
|
||||||
},
|
|
||||||
body: body && JSON.stringify(body),
|
|
||||||
credentials: "same-origin",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiCall = method => async ({ url, body }) => {
|
|
||||||
const response = await baseApiCall(method, url, body)
|
|
||||||
|
|
||||||
switch (response.status) {
|
|
||||||
case 200:
|
|
||||||
return response.json()
|
|
||||||
case 404:
|
|
||||||
return error(`${url} Not found`)
|
|
||||||
case 400:
|
|
||||||
return error(`${url} Bad Request`)
|
|
||||||
case 403:
|
|
||||||
return error(`${url} Forbidden`)
|
|
||||||
default:
|
|
||||||
if (response.status >= 200 && response.status < 400) {
|
|
||||||
return response.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
return error(`${url} - ${response.statusText}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const post = apiCall("POST")
|
|
||||||
const get = apiCall("GET")
|
|
||||||
const patch = apiCall("PATCH")
|
|
||||||
const del = apiCall("DELETE")
|
|
||||||
|
|
||||||
const ERROR_MEMBER = "##error"
|
|
||||||
const error = message => {
|
|
||||||
// appStore.update(s => s["##error_message"], message)
|
|
||||||
return { [ERROR_MEMBER]: message }
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSuccess = obj => !obj || !obj[ERROR_MEMBER]
|
|
||||||
|
|
||||||
const apiOpts = {
|
|
||||||
isSuccess,
|
|
||||||
error,
|
|
||||||
post,
|
|
||||||
get,
|
|
||||||
patch,
|
|
||||||
delete: del,
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveRow = async (params, state) =>
|
|
||||||
await post({
|
|
||||||
url: `/api/${params.tableId}/rows`,
|
|
||||||
body: makeRowRequestBody(params, state),
|
|
||||||
})
|
|
||||||
|
|
||||||
const updateRow = async (params, state) => {
|
|
||||||
const row = makeRowRequestBody(params, state)
|
|
||||||
row._id = params._id
|
|
||||||
await patch({
|
|
||||||
url: `/api/${params.tableId}/rows/${params._id}`,
|
|
||||||
body: row,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteRow = async params =>
|
|
||||||
await del({
|
|
||||||
url: `/api/${params.tableId}/rows/${params.rowId}/${params.revId}`,
|
|
||||||
})
|
|
||||||
|
|
||||||
const makeRowRequestBody = (parameters, state) => {
|
|
||||||
// start with the row thats currently in context
|
|
||||||
const body = { ...(state.data || {}) }
|
|
||||||
|
|
||||||
// dont send the table
|
|
||||||
if (body._table) delete body._table
|
|
||||||
|
|
||||||
// then override with supplied parameters
|
|
||||||
if (parameters.fields) {
|
|
||||||
for (let fieldName of Object.keys(parameters.fields)) {
|
|
||||||
const field = parameters.fields[fieldName]
|
|
||||||
|
|
||||||
// ensure fields sent are of the correct type
|
|
||||||
if (field.type === "boolean") {
|
|
||||||
if (field.value === "true") body[fieldName] = true
|
|
||||||
if (field.value === "false") body[fieldName] = false
|
|
||||||
} else if (field.type === "number") {
|
|
||||||
const val = parseFloat(field.value)
|
|
||||||
if (!isNaN(val)) {
|
|
||||||
body[fieldName] = val
|
|
||||||
}
|
|
||||||
} else if (field.type === "datetime") {
|
|
||||||
const date = new Date(field.value)
|
|
||||||
if (!isNaN(date.getTime())) {
|
|
||||||
body[fieldName] = date.toISOString()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
body[fieldName] = field.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return body
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
authenticate: authenticate(apiOpts),
|
|
||||||
saveRow,
|
|
||||||
updateRow,
|
|
||||||
deleteRow,
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import API from "./api"
|
||||||
|
import { enrichRows } from "./rows"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches related rows for a certain field of a certain row.
|
||||||
|
*/
|
||||||
|
export const fetchRelationshipData = async ({ tableId, rowId, fieldName }) => {
|
||||||
|
if (!tableId || !rowId || !fieldName) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const response = await API.get({ url: `/api/${tableId}/${rowId}/enrich` })
|
||||||
|
const rows = response[fieldName] || []
|
||||||
|
return await enrichRows(rows, tableId)
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import API from "./api"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches available routes for the client app.
|
||||||
|
*/
|
||||||
|
export const fetchRoutes = async () => {
|
||||||
|
return await API.get({
|
||||||
|
url: `/api/routing/client`,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
import API from "./api"
|
||||||
|
import { fetchTableDefinition } from "./tables"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches data about a certain row in a table.
|
||||||
|
*/
|
||||||
|
export const fetchRow = async ({ tableId, rowId }) => {
|
||||||
|
const row = await API.get({
|
||||||
|
url: `/api/${tableId}/rows/${rowId}`,
|
||||||
|
})
|
||||||
|
return (await enrichRows([row], tableId))[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a row in a table.
|
||||||
|
*/
|
||||||
|
export const saveRow = async row => {
|
||||||
|
return await API.post({
|
||||||
|
url: `/api/${row.tableId}/rows`,
|
||||||
|
body: row,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a row in a table.
|
||||||
|
*/
|
||||||
|
export const updateRow = async row => {
|
||||||
|
return await API.patch({
|
||||||
|
url: `/api/${row.tableId}/rows/${row._id}`,
|
||||||
|
body: row,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a row from a table.
|
||||||
|
*/
|
||||||
|
export const deleteRow = async ({ tableId, rowId, revId }) => {
|
||||||
|
return await API.del({
|
||||||
|
url: `/api/${tableId}/rows/${rowId}/${revId}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes many rows from a table.
|
||||||
|
*/
|
||||||
|
export const deleteRows = async ({ tableId, rows }) => {
|
||||||
|
return await API.post({
|
||||||
|
url: `/api/${tableId}/rows`,
|
||||||
|
body: {
|
||||||
|
rows,
|
||||||
|
type: "delete",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enriches rows which contain certain field types so that they can
|
||||||
|
* be properly displayed.
|
||||||
|
*/
|
||||||
|
export const enrichRows = async (rows, tableId) => {
|
||||||
|
if (rows && rows.length && tableId) {
|
||||||
|
// Fetch table schema so we can check column types
|
||||||
|
const tableDefinition = await fetchTableDefinition(tableId)
|
||||||
|
const schema = tableDefinition && tableDefinition.schema
|
||||||
|
if (schema) {
|
||||||
|
const keys = Object.keys(schema)
|
||||||
|
rows.forEach(row => {
|
||||||
|
for (let key of keys) {
|
||||||
|
const type = schema[key].type
|
||||||
|
if (type === "link") {
|
||||||
|
// Enrich row with the count of any relationship fields
|
||||||
|
row[`${key}_count`] = Array.isArray(row[key]) ? row[key].length : 0
|
||||||
|
} else if (type === "attachment") {
|
||||||
|
// Enrich row with the first image URL for any attachment fields
|
||||||
|
let url = null
|
||||||
|
if (Array.isArray(row[key]) && row[key][0] != null) {
|
||||||
|
url = row[key][0].url
|
||||||
|
}
|
||||||
|
row[`${key}_first`] = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
import API from "./api"
|
||||||
|
import { enrichRows } from "./rows"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a table definition.
|
||||||
|
* Since definitions cannot change at runtime, the result is cached.
|
||||||
|
*/
|
||||||
|
export const fetchTableDefinition = async tableId => {
|
||||||
|
return await API.get({ url: `/api/tables/${tableId}`, cache: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all rows from a table.
|
||||||
|
*/
|
||||||
|
export const fetchTableData = async tableId => {
|
||||||
|
const rows = await API.get({ url: `/api/${tableId}/rows` })
|
||||||
|
return await enrichRows(rows, tableId)
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
import API from "./api"
|
||||||
|
import { enrichRows } from "./rows"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all rows in a view.
|
||||||
|
*/
|
||||||
|
export const fetchViewData = async ({
|
||||||
|
name,
|
||||||
|
field,
|
||||||
|
groupBy,
|
||||||
|
calculation,
|
||||||
|
tableId,
|
||||||
|
}) => {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
|
||||||
|
if (calculation) {
|
||||||
|
params.set("field", field)
|
||||||
|
params.set("calculation", calculation)
|
||||||
|
}
|
||||||
|
if (groupBy) {
|
||||||
|
params.set("group", groupBy)
|
||||||
|
}
|
||||||
|
|
||||||
|
const QUERY_VIEW_URL = field
|
||||||
|
? `/api/views/${name}?${params}`
|
||||||
|
: `/api/views/${name}`
|
||||||
|
|
||||||
|
const rows = await API.get({ url: QUERY_VIEW_URL })
|
||||||
|
return await enrichRows(rows, tableId)
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script>
|
||||||
|
import { setContext, onMount } from "svelte"
|
||||||
|
import Component from "./Component.svelte"
|
||||||
|
import SDK from "../sdk"
|
||||||
|
import { routeStore, screenStore, createDataContextStore } from "../store"
|
||||||
|
|
||||||
|
// Provide contexts
|
||||||
|
setContext("sdk", SDK)
|
||||||
|
setContext("data", createDataContextStore())
|
||||||
|
|
||||||
|
let loaded = false
|
||||||
|
|
||||||
|
// Load app config
|
||||||
|
onMount(async () => {
|
||||||
|
await routeStore.actions.fetchRoutes()
|
||||||
|
await screenStore.actions.fetchScreens()
|
||||||
|
loaded = true
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if loaded}
|
||||||
|
<Component definition={$screenStore.page.props} />
|
||||||
|
{/if}
|
|
@ -0,0 +1,52 @@
|
||||||
|
<script>
|
||||||
|
import { getContext, setContext } from "svelte"
|
||||||
|
import * as ComponentLibrary from "@budibase/standard-components"
|
||||||
|
import Router from "./Router.svelte"
|
||||||
|
|
||||||
|
export let definition = {}
|
||||||
|
|
||||||
|
// Extracts the actual component name from the library name
|
||||||
|
const extractComponentName = name => {
|
||||||
|
const split = name?.split("/")
|
||||||
|
return split?.[split.length - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extracts valid props to pass to the real svelte component
|
||||||
|
const extractValidProps = component => {
|
||||||
|
let props = {}
|
||||||
|
Object.entries(component)
|
||||||
|
.filter(([name]) => !name.startsWith("_"))
|
||||||
|
.forEach(([key, value]) => {
|
||||||
|
props[key] = value
|
||||||
|
})
|
||||||
|
return props
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets the component constructor for the specified component
|
||||||
|
const getComponentConstructor = name => {
|
||||||
|
return name === "screenslot" ? Router : ComponentLibrary[componentName]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract component definition info
|
||||||
|
const componentName = extractComponentName(definition._component)
|
||||||
|
const constructor = getComponentConstructor(componentName)
|
||||||
|
const id = `${componentName}-${definition._id}`
|
||||||
|
const componentProps = extractValidProps(definition)
|
||||||
|
const dataContext = getContext("data")
|
||||||
|
const enrichedProps = dataContext.actions.enrichDataBindings(componentProps)
|
||||||
|
const children = definition._children
|
||||||
|
|
||||||
|
// Set style context to be consumed by component
|
||||||
|
setContext("style", { ...definition._styles, id })
|
||||||
|
$: console.log("Rendering: " + componentName)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if constructor}
|
||||||
|
<svelte:component this={constructor} {...enrichedProps}>
|
||||||
|
{#if children && children.length}
|
||||||
|
{#each children as child}
|
||||||
|
<svelte:self definition={child} />
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</svelte:component>
|
||||||
|
{/if}
|
|
@ -0,0 +1,24 @@
|
||||||
|
<script>
|
||||||
|
import { onMount, getContext, setContext } from "svelte"
|
||||||
|
import { createDataContextStore } from "../store"
|
||||||
|
|
||||||
|
export let row
|
||||||
|
|
||||||
|
// Get current contexts
|
||||||
|
const dataContext = getContext("data")
|
||||||
|
|
||||||
|
// Clone current context to this context
|
||||||
|
const newDataContext = createDataContextStore($dataContext)
|
||||||
|
setContext("data", newDataContext)
|
||||||
|
|
||||||
|
// Add additional layer to context
|
||||||
|
let loaded = false
|
||||||
|
onMount(() => {
|
||||||
|
newDataContext.actions.addContext(row)
|
||||||
|
loaded = true
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if loaded}
|
||||||
|
<slot />
|
||||||
|
{/if}
|
|
@ -0,0 +1,32 @@
|
||||||
|
<script>
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
import Router from "svelte-spa-router"
|
||||||
|
import { routeStore, screenStore } from "../store"
|
||||||
|
import Screen from "./Screen.svelte"
|
||||||
|
|
||||||
|
const { styleable } = getContext("sdk")
|
||||||
|
const styles = getContext("style")
|
||||||
|
|
||||||
|
$: 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={styles}>
|
||||||
|
<Router on:routeLoading={onRouteLoading} routes={routerConfig} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
|
@ -0,0 +1,18 @@
|
||||||
|
<script>
|
||||||
|
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("/")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if screenDefinition}
|
||||||
|
<Component definition={screenDefinition} />
|
||||||
|
{/if}
|
|
@ -1,59 +1,12 @@
|
||||||
import { createApp } from "./createApp"
|
import ClientApp from "./components/ClientApp.svelte"
|
||||||
import { builtins, builtinLibName } from "./render/builtinComponents"
|
|
||||||
import { getAppId } from "./render/getAppId"
|
|
||||||
|
|
||||||
/**
|
// Initialise client app
|
||||||
* create a web application from static budibase definition files.
|
const loadBudibase = () => {
|
||||||
* @param {object} opts - configuration options for budibase client libary
|
window.document.body.innerHTML = ""
|
||||||
*/
|
new ClientApp({
|
||||||
export const loadBudibase = async opts => {
|
target: window.document.body,
|
||||||
const _window = (opts && opts.window) || window
|
|
||||||
// const _localStorage = (opts && opts.localStorage) || localStorage
|
|
||||||
const appId = getAppId(window.document.cookie)
|
|
||||||
const frontendDefinition = _window["##BUDIBASE_FRONTEND_DEFINITION##"]
|
|
||||||
|
|
||||||
const user = {}
|
|
||||||
|
|
||||||
const 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 route = _window.location
|
|
||||||
? _window.location.pathname.replace(`${appId}/`, "").replace(appId, "")
|
|
||||||
: ""
|
|
||||||
|
|
||||||
initialisePage(frontendDefinition.page, _window.document.body, route)
|
|
||||||
|
|
||||||
return {
|
|
||||||
screenStore,
|
|
||||||
pageStore,
|
|
||||||
routeTo,
|
|
||||||
rootNode,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window) {
|
// Attach to window so the HTML template can call this when it loads
|
||||||
window.loadBudibase = loadBudibase
|
window.loadBudibase = loadBudibase
|
||||||
}
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { attachChildren } from "./render/attachChildren"
|
||||||
import { createTreeNode } from "./render/prepareRenderComponent"
|
import { createTreeNode } from "./render/prepareRenderComponent"
|
||||||
import { screenRouter } from "./render/screenRouter"
|
import { screenRouter } from "./render/screenRouter"
|
||||||
import { createStateManager } from "./state/stateManager"
|
import { createStateManager } from "./state/stateManager"
|
||||||
import { getAppId } from "./render/getAppId"
|
import { getAppId } from "@budibase/component-sdk"
|
||||||
|
|
||||||
export const createApp = ({
|
export const createApp = ({
|
||||||
componentLibraries,
|
componentLibraries,
|
||||||
|
@ -37,10 +37,7 @@ export const createApp = ({
|
||||||
onScreenSelected,
|
onScreenSelected,
|
||||||
window,
|
window,
|
||||||
})
|
})
|
||||||
const fallbackPath = window.location.pathname.replace(
|
const fallbackPath = window.location.pathname.replace(getAppId(), "")
|
||||||
getAppId(window.document.cookie),
|
|
||||||
""
|
|
||||||
)
|
|
||||||
routeTo(currentUrl || fallbackPath)
|
routeTo(currentUrl || fallbackPath)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { createApp } from "./createApp"
|
||||||
|
import { builtins, builtinLibName } from "./render/builtinComponents"
|
||||||
|
import { getAppId } from "@budibase/component-sdk"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create a web application from static budibase definition files.
|
||||||
|
* @param {object} opts - configuration options for budibase client libary
|
||||||
|
*/
|
||||||
|
export const loadBudibase = async opts => {
|
||||||
|
const _window = (opts && opts.window) || window
|
||||||
|
// const _localStorage = (opts && opts.localStorage) || localStorage
|
||||||
|
const appId = getAppId(window.document.cookie)
|
||||||
|
const frontendDefinition = _window["##BUDIBASE_FRONTEND_DEFINITION##"]
|
||||||
|
|
||||||
|
const user = {}
|
||||||
|
|
||||||
|
const 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 route = _window.location
|
||||||
|
? _window.location.pathname.replace(`${appId}/`, "").replace(appId, "")
|
||||||
|
: ""
|
||||||
|
|
||||||
|
initialisePage(frontendDefinition.page, _window.document.body, route)
|
||||||
|
|
||||||
|
return {
|
||||||
|
screenStore,
|
||||||
|
pageStore,
|
||||||
|
routeTo,
|
||||||
|
rootNode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window) {
|
||||||
|
window.loadBudibase = loadBudibase
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import regexparam from "regexparam"
|
import regexparam from "regexparam"
|
||||||
import appStore from "../state/store"
|
import appStore from "../state/store"
|
||||||
import { getAppId } from "./getAppId"
|
import { getAppId } from "@budibase/component-sdk"
|
||||||
|
|
||||||
export const screenRouter = ({ screens, onScreenSelected, window }) => {
|
export const screenRouter = ({ screens, onScreenSelected, window }) => {
|
||||||
function sanitize(url) {
|
function sanitize(url) {
|
||||||
|
@ -27,7 +27,7 @@ export const screenRouter = ({ screens, onScreenSelected, window }) => {
|
||||||
|
|
||||||
const makeRootedPath = url => {
|
const makeRootedPath = url => {
|
||||||
if (isRunningLocally()) {
|
if (isRunningLocally()) {
|
||||||
const appId = getAppId(window.document.cookie)
|
const appId = getAppId()
|
||||||
if (url) {
|
if (url) {
|
||||||
url = sanitize(url)
|
url = sanitize(url)
|
||||||
if (!url.startsWith("/")) {
|
if (!url.startsWith("/")) {
|
|
@ -1,20 +1,12 @@
|
||||||
import setBindableComponentProp from "./setBindableComponentProp"
|
import setBindableComponentProp from "./setBindableComponentProp"
|
||||||
import { attachChildren } from "../render/attachChildren"
|
import { attachChildren } from "../render/attachChildren"
|
||||||
import store from "../state/store"
|
import store from "./store"
|
||||||
import { baseApiCall } from "../api/index"
|
|
||||||
|
|
||||||
export const bbFactory = ({
|
export const bbFactory = ({
|
||||||
componentLibraries,
|
componentLibraries,
|
||||||
onScreenSlotRendered,
|
onScreenSlotRendered,
|
||||||
runEventActions,
|
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) => {
|
return (treeNode, setupState) => {
|
||||||
const attachParams = {
|
const attachParams = {
|
||||||
componentLibraries,
|
componentLibraries,
|
||||||
|
@ -33,7 +25,6 @@ export const bbFactory = ({
|
||||||
store.getState(treeNode.contextStoreKey)
|
store.getState(treeNode.contextStoreKey)
|
||||||
)),
|
)),
|
||||||
setBinding: setBindableComponentProp(treeNode),
|
setBinding: setBindableComponentProp(treeNode),
|
||||||
api,
|
|
||||||
parent,
|
parent,
|
||||||
store: store.getStore(treeNode.contextStoreKey),
|
store: store.getStore(treeNode.contextStoreKey),
|
||||||
// these parameters are populated by screenRouter
|
// these parameters are populated by screenRouter
|
|
@ -1,15 +1,14 @@
|
||||||
import api from "../api"
|
|
||||||
import renderTemplateString from "./renderTemplateString"
|
import renderTemplateString from "./renderTemplateString"
|
||||||
|
import { updateRow, saveRow, deleteRow } from "@budibase/component-sdk"
|
||||||
|
|
||||||
export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
|
export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
|
||||||
|
|
||||||
export const eventHandlers = routeTo => {
|
export const eventHandlers = routeTo => {
|
||||||
const handlers = {
|
const handlers = {
|
||||||
"Navigate To": param => routeTo(param && param.url),
|
"Navigate To": param => routeTo(param && param.url),
|
||||||
"Update Row": api.updateRow,
|
"Update Row": updateRow,
|
||||||
"Save Row": api.saveRow,
|
"Save Row": saveRow,
|
||||||
"Delete Row": api.deleteRow,
|
"Delete Row": deleteRow,
|
||||||
"Trigger Workflow": api.triggerWorkflow,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// when an event is called, this is what gets run
|
// when an event is called, this is what gets run
|
|
@ -0,0 +1,16 @@
|
||||||
|
import * as API from "./api"
|
||||||
|
import { authStore, routeStore, screenStore } from "./store"
|
||||||
|
import { styleable, getAppId } from "./utils"
|
||||||
|
import { link as linkable } from "svelte-spa-router"
|
||||||
|
import DataProvider from "./components/DataProvider.svelte"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
API,
|
||||||
|
authStore,
|
||||||
|
routeStore,
|
||||||
|
screenStore,
|
||||||
|
styleable,
|
||||||
|
linkable,
|
||||||
|
getAppId,
|
||||||
|
DataProvider,
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
import * as API from "../api"
|
||||||
|
import { getAppId } from "../utils"
|
||||||
|
import { writable } from "svelte/store"
|
||||||
|
|
||||||
|
const createAuthStore = () => {
|
||||||
|
const store = writable("")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs a user in.
|
||||||
|
*/
|
||||||
|
const logIn = async ({ username, password }) => {
|
||||||
|
const user = await API.logIn({ username, password })
|
||||||
|
if (!user.error) {
|
||||||
|
store.set(user.token)
|
||||||
|
location.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs a user out.
|
||||||
|
*/
|
||||||
|
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,43 @@
|
||||||
|
import { writable, get } from "svelte/store"
|
||||||
|
import { enrichDataBinding } from "../utils"
|
||||||
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
||||||
|
const initialValue = {
|
||||||
|
parent: null,
|
||||||
|
data: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createDataContextStore = existingContext => {
|
||||||
|
const initial = existingContext ? cloneDeep(existingContext) : initialValue
|
||||||
|
const store = writable(initial)
|
||||||
|
|
||||||
|
// Adds a context layer to the data context tree
|
||||||
|
const addContext = row => {
|
||||||
|
store.update(state => {
|
||||||
|
if (state.data) {
|
||||||
|
state.parent = {
|
||||||
|
parent: state.parent,
|
||||||
|
data: state.data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.data = row
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enriches props by running mustache and filling in any data bindings present
|
||||||
|
// in the prop values
|
||||||
|
const enrichDataBindings = props => {
|
||||||
|
const state = get(store)
|
||||||
|
let enrichedProps = {}
|
||||||
|
Object.entries(props).forEach(([key, value]) => {
|
||||||
|
enrichedProps[key] = enrichDataBinding(value, state)
|
||||||
|
})
|
||||||
|
return enrichedProps
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
actions: { addContext, enrichDataBindings },
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
export { authStore } from "./auth"
|
||||||
|
export { routeStore } from "./routes"
|
||||||
|
export { screenStore } from "./screens"
|
||||||
|
export { createDataContextStore } from "./dataContext"
|
|
@ -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,40 @@
|
||||||
|
import { writable, derived } from "svelte/store"
|
||||||
|
import { routeStore } from "./routes"
|
||||||
|
import * as API from "../api"
|
||||||
|
import { getAppId } from "../utils"
|
||||||
|
|
||||||
|
const createScreenStore = () => {
|
||||||
|
const config = writable({
|
||||||
|
screens: [],
|
||||||
|
page: {},
|
||||||
|
})
|
||||||
|
const store = derived([config, routeStore], ([$config, $routeStore]) => {
|
||||||
|
const { screens, page } = $config
|
||||||
|
const activeScreen =
|
||||||
|
screens.length === 1
|
||||||
|
? screens[0]
|
||||||
|
: screens.find(
|
||||||
|
screen => screen.routing.route === $routeStore.activeRoute
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
screens,
|
||||||
|
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,35 @@
|
||||||
|
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 => {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
console.log("====================================")
|
||||||
|
console.log(input)
|
||||||
|
console.log(context)
|
||||||
|
return mustache.render(input, context)
|
||||||
|
}
|
|
@ -9,6 +9,9 @@ function confirmAppId(possibleAppId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function tryGetFromCookie({ cookies }) {
|
function tryGetFromCookie({ cookies }) {
|
||||||
|
if (!cookies) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
const cookie = cookies
|
const cookie = cookies
|
||||||
.split(COOKIE_SEPARATOR)
|
.split(COOKIE_SEPARATOR)
|
||||||
.find(cookie => cookie.trim().startsWith("budibase:currentapp"))
|
.find(cookie => cookie.trim().startsWith("budibase:currentapp"))
|
||||||
|
@ -30,7 +33,7 @@ function tryGetFromSubdomain() {
|
||||||
return confirmAppId(appId)
|
return confirmAppId(appId)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAppId = cookies => {
|
export const getAppId = (cookies = window.document.cookie) => {
|
||||||
const functions = [tryGetFromSubdomain, tryGetFromPath, tryGetFromCookie]
|
const functions = [tryGetFromSubdomain, tryGetFromPath, tryGetFromCookie]
|
||||||
// try getting the app Id in order
|
// try getting the app Id in order
|
||||||
let appId
|
let appId
|
||||||
|
@ -42,5 +45,3 @@ export const getAppId = cookies => {
|
||||||
}
|
}
|
||||||
return appId
|
return appId
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAppIdFromPath = getAppId
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
export { getAppId } from "./getAppId"
|
||||||
|
export { styleable } from "./styleable"
|
||||||
|
export { enrichDataBinding } from "./enrichDataBinding"
|
|
@ -0,0 +1,46 @@
|
||||||
|
const buildStyleString = styles => {
|
||||||
|
let str = ""
|
||||||
|
Object.entries(styles).forEach(([style, value]) => {
|
||||||
|
if (style && value) {
|
||||||
|
str += `${style}: ${value}; `
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Svelte action to apply correct component styles.
|
||||||
|
*/
|
||||||
|
export const styleable = (node, styles = {}) => {
|
||||||
|
const normalStyles = styles.normal || {}
|
||||||
|
const hoverStyles = {
|
||||||
|
...normalStyles,
|
||||||
|
...styles.hover,
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyNormalStyles() {
|
||||||
|
node.style = buildStyleString(normalStyles)
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyHoverStyles() {
|
||||||
|
node.style = buildStyleString(hoverStyles)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add listeners to toggle hover styles
|
||||||
|
node.addEventListener("mouseover", applyHoverStyles)
|
||||||
|
node.addEventListener("mouseout", applyNormalStyles)
|
||||||
|
|
||||||
|
// Apply normal styles initially
|
||||||
|
applyNormalStyles()
|
||||||
|
|
||||||
|
// Also apply data tags so we know how to reference each component
|
||||||
|
node.setAttribute("data-bb-id", styles.id)
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Clean up event listeners when component is destroyed
|
||||||
|
destroy: () => {
|
||||||
|
node.removeEventListener("mouseover", applyHoverStyles)
|
||||||
|
node.removeEventListener("mouseout", applyNormalStyles)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
@ -1,6 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/standard-components",
|
"name": "@budibase/standard-components",
|
||||||
"svelte": "src/index.svelte",
|
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.js",
|
"module": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -13,19 +12,21 @@
|
||||||
"dev:builder": "rollup -cw"
|
"dev:builder": "rollup -cw"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@budibase/client": "^0.3.8",
|
"@rollup/plugin-alias": "^3.1.1",
|
||||||
"@rollup/plugin-commonjs": "^11.1.0",
|
"@rollup/plugin-commonjs": "^16.0.0",
|
||||||
|
"@rollup/plugin-json": "^4.1.0",
|
||||||
|
"@rollup/plugin-node-resolve": "^10.0.0",
|
||||||
|
"@rollup/plugin-replace": "^2.3.4",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
"rollup": "^2.11.2",
|
"rollup": "^2.11.2",
|
||||||
"rollup-plugin-commonjs": "^10.0.2",
|
|
||||||
"rollup-plugin-json": "^4.0.0",
|
|
||||||
"rollup-plugin-livereload": "^1.0.1",
|
"rollup-plugin-livereload": "^1.0.1",
|
||||||
"rollup-plugin-node-resolve": "^5.0.0",
|
|
||||||
"rollup-plugin-postcss": "^3.1.5",
|
"rollup-plugin-postcss": "^3.1.5",
|
||||||
"rollup-plugin-svelte": "^6.1.1",
|
"rollup-plugin-svelte": "^6.1.1",
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
"sirv-cli": "^0.4.4",
|
"sirv-cli": "^0.4.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
"svelte": "^3.29.0"
|
"svelte": "^3.29.0"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|
|
@ -1,38 +1,39 @@
|
||||||
import svelte from "rollup-plugin-svelte"
|
|
||||||
import resolve from "rollup-plugin-node-resolve"
|
|
||||||
import commonjs from "@rollup/plugin-commonjs"
|
import commonjs from "@rollup/plugin-commonjs"
|
||||||
|
import resolve from "@rollup/plugin-node-resolve"
|
||||||
|
// import replace from "@rollup/plugin-replace"
|
||||||
|
import svelte from "rollup-plugin-svelte"
|
||||||
import postcss from "rollup-plugin-postcss"
|
import postcss from "rollup-plugin-postcss"
|
||||||
import { terser } from "rollup-plugin-terser"
|
import { terser } from "rollup-plugin-terser"
|
||||||
|
|
||||||
const production = !process.env.ROLLUP_WATCH
|
const production = !process.env.ROLLUP_WATCH
|
||||||
const lodash_fp_exports = ["isEmpty"]
|
const externals = ["svelte", "svelte/internal"]
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
external: externals,
|
||||||
input: "src/index.js",
|
input: "src/index.js",
|
||||||
output: [
|
output: [
|
||||||
{
|
{
|
||||||
file: "dist/index.js",
|
file: "dist/index.js",
|
||||||
format: "esm",
|
format: "esm",
|
||||||
name: "budibaseStandardComponents",
|
name: "budibaseStandardComponents",
|
||||||
sourcemap: true,
|
sourcemap: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
// Only run terser in production environments
|
|
||||||
production && terser(),
|
production && terser(),
|
||||||
postcss({
|
postcss(),
|
||||||
plugins: [],
|
|
||||||
}),
|
|
||||||
svelte({
|
svelte({
|
||||||
hydratable: true,
|
dev: !production,
|
||||||
}),
|
}),
|
||||||
resolve({
|
resolve({
|
||||||
browser: true,
|
browser: true,
|
||||||
|
skip: externals,
|
||||||
}),
|
}),
|
||||||
commonjs({
|
commonjs(),
|
||||||
namedExports: {
|
// Fix for https://github.com/sveltejs/svelte/issues/3165
|
||||||
"lodash/fp": lodash_fp_exports,
|
// replace({
|
||||||
},
|
// "outros.c.push":
|
||||||
}),
|
// "if (outros === undefined) { block.o(local); return }\noutros.c.push",
|
||||||
|
// }),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,16 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
const { styleable } = getContext("sdk")
|
||||||
|
const styles = getContext("style")
|
||||||
|
|
||||||
export let className = "default"
|
export let className = "default"
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let text
|
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")
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button
|
<button class="default" disabled={disabled || false} use:styleable={styles}>
|
||||||
bind:this={theButton}
|
{text}
|
||||||
class="default"
|
|
||||||
disabled={disabled || false}
|
|
||||||
on:click|once={clickHandler}>
|
|
||||||
{#if !_bb.props._children || _bb.props._children.length === 0}{text}{/if}
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -37,28 +28,4 @@
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.border {
|
|
||||||
border: var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.color {
|
|
||||||
color: var(--color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.background {
|
|
||||||
background: var(--background);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hoverBorder:hover {
|
|
||||||
border: var(--hoverBorder);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hoverColor:hover {
|
|
||||||
color: var(--hoverColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hoverBack:hover {
|
|
||||||
background: var(--hoverBackground);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import { cssVars, createClasses } from "./cssVars"
|
import { getContext } from "svelte"
|
||||||
|
import { cssVars } from "./helpers"
|
||||||
|
|
||||||
|
const { styleable } = getContext("sdk")
|
||||||
|
const styles = getContext("style")
|
||||||
|
|
||||||
export const className = ""
|
export const className = ""
|
||||||
export let imageUrl = ""
|
export let imageUrl = ""
|
||||||
|
@ -22,7 +26,7 @@
|
||||||
$: showImage = !!imageUrl
|
$: showImage = !!imageUrl
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div use:cssVars={cssVariables} class="container">
|
<div use:cssVars={cssVariables} class="container" use:styleable={styles}>
|
||||||
{#if showImage}<img class="image" src={imageUrl} alt="" />{/if}
|
{#if showImage}<img class="image" src={imageUrl} alt="" />{/if}
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<h2 class="heading">{heading}</h2>
|
<h2 class="heading">{heading}</h2>
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import { cssVars, createClasses } from "./cssVars"
|
import { getContext } from "svelte"
|
||||||
|
import { cssVars } from "./helpers"
|
||||||
|
|
||||||
|
const { styleable } = getContext("sdk")
|
||||||
|
const styles = getContext("style")
|
||||||
|
|
||||||
export const className = ""
|
export const className = ""
|
||||||
export let imageUrl = ""
|
export let imageUrl = ""
|
||||||
|
@ -25,7 +29,7 @@
|
||||||
$: showImage = !!imageUrl
|
$: showImage = !!imageUrl
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div use:cssVars={cssVariables} class="container">
|
<div use:cssVars={cssVariables} class="container" use:styleable={styles}>
|
||||||
{#if showImage}<img class="image" src={imageUrl} alt="" />{/if}
|
{#if showImage}<img class="image" src={imageUrl} alt="" />{/if}
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<main>
|
<main>
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
<script>
|
|
||||||
import Input from "./Input.svelte"
|
|
||||||
export let _bb
|
|
||||||
|
|
||||||
export let label = ""
|
|
||||||
export let checked = false
|
|
||||||
export let value = ""
|
|
||||||
export let onchange = () => {}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Input type="checkbox" {_bb} {checked} {label} {value} {onchange} />
|
|
|
@ -1,50 +1,63 @@
|
||||||
<script>
|
<script>
|
||||||
import { cssVars, createClasses } from "./cssVars"
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
const { styleable } = getContext("sdk")
|
||||||
|
const styles = getContext("style")
|
||||||
|
|
||||||
export let className = ""
|
export let className = ""
|
||||||
export let onLoad
|
|
||||||
export let type = "div"
|
export let type = "div"
|
||||||
export let _bb
|
|
||||||
|
|
||||||
let containerElement
|
|
||||||
let hasLoaded
|
|
||||||
let currentChildren
|
|
||||||
|
|
||||||
$: {
|
|
||||||
if (containerElement) {
|
|
||||||
_bb.attachChildren(containerElement)
|
|
||||||
if (!hasLoaded) {
|
|
||||||
_bb.call("onLoad")
|
|
||||||
hasLoaded = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if type === 'div'}
|
{#if type === 'div'}
|
||||||
<div bind:this={containerElement} />
|
<div use:styleable={styles}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
{:else if type === 'header'}
|
{:else if type === 'header'}
|
||||||
<header bind:this={containerElement} />
|
<header use:styleable={styles}>
|
||||||
|
<slot />
|
||||||
|
</header>
|
||||||
{:else if type === 'main'}
|
{:else if type === 'main'}
|
||||||
<main bind:this={containerElement} />
|
<main use:styleable={styles}>
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
{:else if type === 'footer'}
|
{:else if type === 'footer'}
|
||||||
<footer bind:this={containerElement} />
|
<footer use:styleable={styles}>
|
||||||
|
<slot />
|
||||||
|
</footer>
|
||||||
{:else if type === 'aside'}
|
{:else if type === 'aside'}
|
||||||
<aside bind:this={containerElement} />
|
<aside use:styleable={styles}>
|
||||||
|
<slot />
|
||||||
|
</aside>
|
||||||
{:else if type === 'summary'}
|
{:else if type === 'summary'}
|
||||||
<summary bind:this={containerElement} />
|
<summary use:styleable={styles}>
|
||||||
|
<slot />
|
||||||
|
</summary>
|
||||||
{:else if type === 'details'}
|
{:else if type === 'details'}
|
||||||
<details bind:this={containerElement} />
|
<details use:styleable={styles}>
|
||||||
|
<slot />
|
||||||
|
</details>
|
||||||
{:else if type === 'article'}
|
{:else if type === 'article'}
|
||||||
<article bind:this={containerElement} />
|
<article use:styleable={styles}>
|
||||||
|
<slot />
|
||||||
|
</article>
|
||||||
{:else if type === 'nav'}
|
{:else if type === 'nav'}
|
||||||
<nav bind:this={containerElement} />
|
<nav use:styleable={styles}>
|
||||||
|
<slot />
|
||||||
|
</nav>
|
||||||
{:else if type === 'mark'}
|
{:else if type === 'mark'}
|
||||||
<mark bind:this={containerElement} />
|
<mark use:styleable={styles}>
|
||||||
|
<slot />
|
||||||
|
</mark>
|
||||||
{:else if type === 'figure'}
|
{:else if type === 'figure'}
|
||||||
<figure bind:this={containerElement} />
|
<figure use:styleable={styles}>
|
||||||
|
<slot />
|
||||||
|
</figure>
|
||||||
{:else if type === 'figcaption'}
|
{:else if type === 'figcaption'}
|
||||||
<figcaption bind:this={containerElement} />
|
<figcaption use:styleable={styles}>
|
||||||
|
<slot />
|
||||||
|
</figcaption>
|
||||||
{:else if type === 'paragraph'}
|
{:else if type === 'paragraph'}
|
||||||
<p bind:this={containerElement} />
|
<p use:styleable={styles}>
|
||||||
|
<slot />
|
||||||
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,10 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import Form from "./Form.svelte"
|
import Form from "./Form.svelte"
|
||||||
|
|
||||||
export let _bb
|
|
||||||
export let table
|
|
||||||
export let title
|
|
||||||
export let buttonText
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Form {_bb} {table} {title} {buttonText} wide={false} />
|
<Form wide={false} />
|
||||||
|
|
|
@ -1,10 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import Form from "./Form.svelte"
|
import Form from "./Form.svelte"
|
||||||
|
|
||||||
export let _bb
|
|
||||||
export let table
|
|
||||||
export let title
|
|
||||||
export let buttonText
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Form {_bb} {table} {title} {buttonText} wide={true} />
|
<Form wide />
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
import api from "../../api"
|
|
||||||
|
|
||||||
let cache = {}
|
|
||||||
|
|
||||||
async function fetchTable(id) {
|
|
||||||
const FETCH_TABLE_URL = `/api/tables/${id}`
|
|
||||||
const response = await api.get(FETCH_TABLE_URL)
|
|
||||||
return await response.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getTable(tableId) {
|
|
||||||
if (!tableId) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (!cache[tableId]) {
|
|
||||||
cache[tableId] = fetchTable(tableId)
|
|
||||||
cache[tableId] = await cache[tableId]
|
|
||||||
}
|
|
||||||
return await cache[tableId]
|
|
||||||
}
|
|
|
@ -1,82 +0,0 @@
|
||||||
<script>
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
|
|
||||||
export let _bb
|
|
||||||
export let table
|
|
||||||
export let layout = "list"
|
|
||||||
|
|
||||||
let headers = []
|
|
||||||
let store = _bb.store
|
|
||||||
|
|
||||||
async function fetchData() {
|
|
||||||
if (!table || !table.length) return
|
|
||||||
|
|
||||||
const FETCH_ROWS_URL = `/api/views/all_${table}`
|
|
||||||
const response = await _bb.api.get(FETCH_ROWS_URL)
|
|
||||||
if (response.status === 200) {
|
|
||||||
const json = await response.json()
|
|
||||||
|
|
||||||
store.update(state => {
|
|
||||||
state[table] = json
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
throw new Error("Failed to fetch rows.", response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$: data = $store[table] || []
|
|
||||||
$: if (table) fetchData()
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
await fetchData()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<section class:grid={layout === 'grid'} class:list={layout === 'list'}>
|
|
||||||
{#each data as data}
|
|
||||||
<div class="data-card">
|
|
||||||
<ul>
|
|
||||||
{#each Object.keys(data) as key}
|
|
||||||
<li>
|
|
||||||
<span class="data-key">{key}:</span>
|
|
||||||
<span class="data-value">{data[key]}</span>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
font-family: Inter;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
list-style-type: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
margin: 5px 0 5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-card {
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-key {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 20px;
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,15 +0,0 @@
|
||||||
<script>
|
|
||||||
export let _bb
|
|
||||||
export let table
|
|
||||||
|
|
||||||
let searchValue = ""
|
|
||||||
|
|
||||||
function search() {
|
|
||||||
const SEARCH_URL = _bb.api.get(``)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<input type="text" bind:value={searchValue} />
|
|
||||||
<button on:click={search}>Search</button>
|
|
||||||
</div>
|
|
|
@ -1,18 +1,19 @@
|
||||||
<script>
|
<script>
|
||||||
import Flatpickr from "svelte-flatpickr"
|
|
||||||
import { DatePicker } from "@budibase/bbui"
|
import { DatePicker } from "@budibase/bbui"
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
const { styleable } = getContext("sdk")
|
||||||
|
const styles = getContext("style")
|
||||||
|
|
||||||
export let placeholder
|
export let placeholder
|
||||||
export let value
|
export let value
|
||||||
|
|
||||||
export let _bb
|
|
||||||
|
|
||||||
function handleChange(event) {
|
function handleChange(event) {
|
||||||
const [fullDate, dateStr, instance] = event.detail
|
const [fullDate] = event.detail
|
||||||
if (_bb) {
|
value = fullDate
|
||||||
_bb.setBinding("value", fullDate)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DatePicker {placeholder} on:change={handleChange} {value} />
|
<div use:styleable={styles}>
|
||||||
|
<DatePicker {placeholder} on:change={handleChange} {value} />
|
||||||
|
</div>
|
||||||
|
|
|
@ -1,5 +1,23 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
const { styleable } = getContext("sdk")
|
||||||
|
const styles = getContext("style")
|
||||||
|
|
||||||
export let embed
|
export let embed
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{@html embed}
|
<div use:styleable={styles}>
|
||||||
|
{@html embed}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
div :global(> *) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,54 +1,63 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { getContext } from "svelte"
|
||||||
import { Label, DatePicker, Input, Select, Toggle } from "@budibase/bbui"
|
import { Label, DatePicker, Input, Select, Toggle } from "@budibase/bbui"
|
||||||
import Dropzone from "./attachments/Dropzone.svelte"
|
import Dropzone from "./attachments/Dropzone.svelte"
|
||||||
import LinkedRowSelector from "./LinkedRowSelector.svelte"
|
import LinkedRowSelector from "./LinkedRowSelector.svelte"
|
||||||
import ErrorsBox from "./ErrorsBox.svelte"
|
|
||||||
import { capitalise } from "./helpers"
|
import { capitalise } from "./helpers"
|
||||||
|
|
||||||
export let _bb
|
const { styleable, screenStore, API } = getContext("sdk")
|
||||||
export let table
|
const dataContextStore = getContext("data")
|
||||||
|
const styles = getContext("style")
|
||||||
|
|
||||||
export let wide = false
|
export let wide = false
|
||||||
|
|
||||||
let store = _bb.store
|
let row
|
||||||
let schema = {}
|
let schema
|
||||||
let rowId
|
let fields = []
|
||||||
let errors = {}
|
|
||||||
|
|
||||||
$: schema = $store.data && $store.data._table && $store.data._table.schema
|
$: getContextDetails($dataContextStore)
|
||||||
$: fields = schema ? Object.keys(schema) : []
|
|
||||||
|
const getContextDetails = async dataContext => {
|
||||||
|
row = dataContext?.data
|
||||||
|
if (row) {
|
||||||
|
const tableDefinition = await API.fetchTableDefinition(row.tableId)
|
||||||
|
schema = tableDefinition.schema
|
||||||
|
fields = Object.keys(schema)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="form-content">
|
<div class="form-content" use:styleable={styles}>
|
||||||
<ErrorsBox errors={$store.saveRowErrors || {}} />
|
<!-- <ErrorsBox errors={$store.saveRowErrors || {}} />-->
|
||||||
{#each fields as field}
|
{#each fields as field}
|
||||||
<div class="form-field" class:wide>
|
<div class="form-field" class:wide>
|
||||||
{#if !(schema[field].type === 'boolean' && !wide)}
|
{#if !(schema[field].type === 'boolean' && !wide)}
|
||||||
<Label extraSmall={!wide} grey>{capitalise(schema[field].name)}</Label>
|
<Label extraSmall={!wide} grey>{capitalise(schema[field].name)}</Label>
|
||||||
{/if}
|
{/if}
|
||||||
{#if schema[field].type === 'options'}
|
{#if schema[field].type === 'options'}
|
||||||
<Select secondary bind:value={$store.data[field]}>
|
<Select secondary bind:value={row[field]}>
|
||||||
<option value="">Choose an option</option>
|
<option value="">Choose an option</option>
|
||||||
{#each schema[field].constraints.inclusion as opt}
|
{#each schema[field].constraints.inclusion as opt}
|
||||||
<option>{opt}</option>
|
<option>{opt}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</Select>
|
</Select>
|
||||||
{:else if schema[field].type === 'datetime'}
|
{:else if schema[field].type === 'datetime'}
|
||||||
<DatePicker bind:value={$store.data[field]} />
|
<DatePicker bind:value={row[field]} />
|
||||||
{:else if schema[field].type === 'boolean'}
|
{:else if schema[field].type === 'boolean'}
|
||||||
<Toggle
|
<Toggle
|
||||||
text={wide ? null : capitalise(schema[field].name)}
|
text={wide ? null : capitalise(schema[field].name)}
|
||||||
bind:checked={$store.data[field]} />
|
bind:checked={row[field]} />
|
||||||
{:else if schema[field].type === 'number'}
|
{:else if schema[field].type === 'number'}
|
||||||
<Input type="number" bind:value={$store.data[field]} />
|
<Input type="number" bind:value={row[field]} />
|
||||||
{:else if schema[field].type === 'string'}
|
{:else if schema[field].type === 'string'}
|
||||||
<Input bind:value={$store.data[field]} />
|
<Input bind:value={row[field]} />
|
||||||
{:else if schema[field].type === 'attachment'}
|
{:else if schema[field].type === 'attachment'}
|
||||||
<Dropzone bind:files={$store.data[field]} />
|
<Dropzone bind:files={row[field]} />
|
||||||
{:else if schema[field].type === 'link'}
|
{:else if schema[field].type === 'link'}
|
||||||
<LinkedRowSelector
|
<LinkedRowSelector
|
||||||
secondary
|
secondary
|
||||||
showLabel={false}
|
showLabel={false}
|
||||||
bind:linkedRows={$store.data[field]}
|
bind:linkedRows={row[field]}
|
||||||
schema={schema[field]} />
|
schema={schema[field]} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,30 +1,24 @@
|
||||||
<script>
|
<script>
|
||||||
import { buildStyle } from "./buildStyle.js"
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
const { styleable } = getContext("sdk")
|
||||||
|
const styles = getContext("style")
|
||||||
|
|
||||||
export let className = ""
|
export let className = ""
|
||||||
export let type
|
export let type
|
||||||
export let text = ""
|
export let text = ""
|
||||||
|
|
||||||
export let _bb
|
|
||||||
|
|
||||||
let containerElement
|
|
||||||
|
|
||||||
$: containerElement &&
|
|
||||||
!text &&
|
|
||||||
_bb.props.children &&
|
|
||||||
_bb.props.children.length &&
|
|
||||||
_bb.attachChildren(containerElement)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if type === 'h1'}
|
{#if type === 'h1'}
|
||||||
<h1 class={className} bind:this={containerElement}>{text}</h1>
|
<h1 class={className} use:styleable={styles}>{text}</h1>
|
||||||
{:else if type === 'h2'}
|
{:else if type === 'h2'}
|
||||||
<h2 class={className} bind:this={containerElement}>{text}</h2>
|
<h2 class={className} use:styleable={styles}>{text}</h2>
|
||||||
{:else if type === 'h3'}
|
{:else if type === 'h3'}
|
||||||
<h3 class={className} bind:this={containerElement}>{text}</h3>
|
<h3 class={className} use:styleable={styles}>{text}</h3>
|
||||||
{:else if type === 'h4'}
|
{:else if type === 'h4'}
|
||||||
<h4 class={className} bind:this={containerElement}>{text}</h4>
|
<h4 class={className} use:styleable={styles}>{text}</h4>
|
||||||
{:else if type === 'h5'}
|
{:else if type === 'h5'}
|
||||||
<h5 class={className} bind:this={containerElement}>{text}</h5>
|
<h5 class={className} use:styleable={styles}>{text}</h5>
|
||||||
{:else if type === 'h6'}
|
{:else if type === 'h6'}
|
||||||
<h6 class={className} bind:this={containerElement}>{text}</h6>
|
<h6 class={className} use:styleable={styles}>{text}</h6>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
<script>
|
<script>
|
||||||
import "@fortawesome/fontawesome-free/js/all.js"
|
import "@fortawesome/fontawesome-free/js/all.js"
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
const { styleable } = getContext("sdk")
|
||||||
|
const styles = getContext("style")
|
||||||
|
|
||||||
export let icon = ""
|
export let icon = ""
|
||||||
export let size = "fa-lg"
|
export let size = "fa-lg"
|
||||||
export let color = "#000"
|
export let color = "#000"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<i style={`color: ${color};`} class={`${icon} ${size}`} />
|
<i
|
||||||
|
style={`color: ${color};`}
|
||||||
|
class={`${icon} ${size}`}
|
||||||
|
use:styleable={styles} />
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
<script>
|
|
||||||
export let icon = ""
|
|
||||||
export let fontSize = "1em"
|
|
||||||
export let _bb
|
|
||||||
|
|
||||||
$: style = { fontSize }
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<i class={icon} {style} />
|
|
|
@ -1,15 +1,20 @@
|
||||||
<script>
|
<script>
|
||||||
import { buildStyle } from "./buildStyle"
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
const { styleable } = getContext("sdk")
|
||||||
|
const styles = getContext("style")
|
||||||
|
|
||||||
export let className = ""
|
export let className = ""
|
||||||
export let url = ""
|
export let url = ""
|
||||||
export let description = ""
|
export let description = ""
|
||||||
export let height
|
export let height
|
||||||
export let width
|
export let width
|
||||||
|
|
||||||
export let _bb
|
|
||||||
|
|
||||||
$: style = buildStyle({ height, width })
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<img class={className} {style} src={url} alt={description} />
|
<img
|
||||||
|
{height}
|
||||||
|
{width}
|
||||||
|
class={className}
|
||||||
|
src={url}
|
||||||
|
alt={description}
|
||||||
|
use:styleable={styles} />
|
||||||
|
|
|
@ -1,22 +1,21 @@
|
||||||
<script>
|
<script>
|
||||||
export let id = ""
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
const { styleable } = getContext("sdk")
|
||||||
|
const styles = getContext("style")
|
||||||
|
|
||||||
export let value = ""
|
export let value = ""
|
||||||
export let className = ""
|
export let className = ""
|
||||||
export let type = "text"
|
export let type = "text"
|
||||||
export let label = ""
|
|
||||||
export let checked = false
|
|
||||||
|
|
||||||
export let _bb
|
|
||||||
|
|
||||||
let actualValue = ""
|
|
||||||
|
|
||||||
const onchange = ev => {
|
const onchange = ev => {
|
||||||
if (_bb) {
|
value = ev.target.value
|
||||||
const val = type === "checkbox" ? ev.target.checked : ev.target.value
|
|
||||||
_bb.setBinding("value", val)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<label for={id}>{label}</label>
|
<input
|
||||||
<input {id} class={className} {type} {value} {checked} on:change={onchange} />
|
class={className}
|
||||||
|
{type}
|
||||||
|
{value}
|
||||||
|
on:change={onchange}
|
||||||
|
use:styleable={styles} />
|
||||||
|
|
|
@ -1,22 +1,17 @@
|
||||||
<script>
|
<script>
|
||||||
import { cssVars, createClasses } from "./cssVars"
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
const { linkable, styleable } = getContext("sdk")
|
||||||
|
const styles = getContext("style")
|
||||||
|
|
||||||
export let url = ""
|
export let url = ""
|
||||||
export let text = ""
|
export let text = ""
|
||||||
export let openInNewTab = false
|
export let openInNewTab = false
|
||||||
|
|
||||||
export let _bb
|
|
||||||
|
|
||||||
let anchorElement
|
|
||||||
|
|
||||||
$: anchorElement && !text && _bb.attachChildren(anchorElement)
|
|
||||||
$: target = openInNewTab ? "_blank" : "_self"
|
$: target = openInNewTab ? "_blank" : "_self"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a href={url} bind:this={anchorElement} {target}>{text}</a>
|
<a href={url} use:linkable {target} use:styleable={styles}>
|
||||||
|
{text}
|
||||||
<style>
|
<slot />
|
||||||
.textDecoration {
|
</a>
|
||||||
text-decoration: var(--textDecoration);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import { Label, Multiselect } from "@budibase/bbui"
|
import { Label, Multiselect } from "@budibase/bbui"
|
||||||
import api from "./api"
|
|
||||||
import { capitalise } from "./helpers"
|
import { capitalise } from "./helpers"
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
const { API } = getContext("sdk")
|
||||||
|
|
||||||
export let schema = {}
|
export let schema = {}
|
||||||
export let linkedRows = []
|
export let linkedRows = []
|
||||||
|
@ -16,22 +18,16 @@
|
||||||
$: fetchRows(linkedTableId)
|
$: fetchRows(linkedTableId)
|
||||||
$: fetchTable(linkedTableId)
|
$: fetchTable(linkedTableId)
|
||||||
|
|
||||||
async function fetchTable() {
|
async function fetchTable(id) {
|
||||||
if (linkedTableId == null) {
|
if (id != null) {
|
||||||
return
|
linkedTable = await API.fetchTableDefinition(id)
|
||||||
}
|
}
|
||||||
const FETCH_TABLE_URL = `/api/tables/${linkedTableId}`
|
|
||||||
const response = await api.get(FETCH_TABLE_URL)
|
|
||||||
linkedTable = await response.json()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchRows(linkedTableId) {
|
async function fetchRows(id) {
|
||||||
if (linkedTableId == null) {
|
if (id != null) {
|
||||||
return
|
allRows = await API.fetchTableData(id)
|
||||||
}
|
}
|
||||||
const FETCH_ROWS_URL = `/api/${linkedTableId}/rows`
|
|
||||||
const response = await api.get(FETCH_ROWS_URL)
|
|
||||||
allRows = await response.json()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPrettyName(row) {
|
function getPrettyName(row) {
|
||||||
|
|
|
@ -1,23 +1,26 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte"
|
import { getContext, onMount } from "svelte"
|
||||||
import fetchData from "./fetchData.js"
|
|
||||||
import { isEmpty } from "lodash/fp"
|
import { isEmpty } from "lodash/fp"
|
||||||
|
|
||||||
export let _bb
|
const { API, styleable, DataProvider } = getContext("sdk")
|
||||||
|
const dataContextStore = getContext("data")
|
||||||
|
const styles = getContext("style")
|
||||||
|
|
||||||
export let datasource = []
|
export let datasource = []
|
||||||
|
|
||||||
let target
|
let rows = []
|
||||||
let store = _bb.store
|
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (!isEmpty(datasource)) {
|
if (!isEmpty(datasource)) {
|
||||||
const data = await fetchData(datasource, $store)
|
rows = await API.fetchDatasource(datasource, $dataContextStore)
|
||||||
_bb.attachChildren(target, {
|
|
||||||
hydrate: false,
|
|
||||||
context: data,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section bind:this={target} />
|
<div use:styleable={styles}>
|
||||||
|
{#each rows as row}
|
||||||
|
<DataProvider {row}>
|
||||||
|
<slot />
|
||||||
|
</DataProvider>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import Button from "./Button.svelte"
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
const { authStore, styleable } = getContext("sdk")
|
||||||
|
const styles = getContext("style")
|
||||||
|
|
||||||
export let buttonText = "Log In"
|
export let buttonText = "Log In"
|
||||||
export let logo = ""
|
export let logo = ""
|
||||||
|
@ -7,8 +10,6 @@
|
||||||
export let buttonClass = ""
|
export let buttonClass = ""
|
||||||
export let inputClass = ""
|
export let inputClass = ""
|
||||||
|
|
||||||
export let _bb
|
|
||||||
|
|
||||||
let username = ""
|
let username = ""
|
||||||
let password = ""
|
let password = ""
|
||||||
let loading = false
|
let loading = false
|
||||||
|
@ -23,23 +24,12 @@
|
||||||
|
|
||||||
const login = async () => {
|
const login = async () => {
|
||||||
loading = true
|
loading = true
|
||||||
const response = await _bb.api.post("/api/authenticate", {
|
await authStore.actions.logIn({ username, password })
|
||||||
username,
|
loading = false
|
||||||
password,
|
|
||||||
})
|
|
||||||
if (response.status === 200) {
|
|
||||||
const json = await response.json()
|
|
||||||
localStorage.setItem("budibase:token", json.token)
|
|
||||||
// TODO: possibly do something with the user information in the response?
|
|
||||||
location.reload()
|
|
||||||
} else {
|
|
||||||
loading = false
|
|
||||||
error = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="root">
|
<div class="root" use:styleable={styles}>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{#if logo}
|
{#if logo}
|
||||||
<div class="logo-container"><img src={logo} alt="logo" /></div>
|
<div class="logo-container"><img src={logo} alt="logo" /></div>
|
||||||
|
|
|
@ -1,116 +0,0 @@
|
||||||
<script>
|
|
||||||
import cssVars from "./cssVars"
|
|
||||||
|
|
||||||
export let navBarBackground = ""
|
|
||||||
export let navBarBorder = ""
|
|
||||||
export let navBarColor = ""
|
|
||||||
export let selectedItemBackground = ""
|
|
||||||
export let selectedItemColor = ""
|
|
||||||
export let selectedItemBorder = ""
|
|
||||||
export let itemHoverBackground = ""
|
|
||||||
export let itemHoverColor = ""
|
|
||||||
export let hideNavBar = false
|
|
||||||
export let selectedItem = ""
|
|
||||||
|
|
||||||
export let _children
|
|
||||||
export let _bb
|
|
||||||
|
|
||||||
let selectedIndex = -1
|
|
||||||
let styleVars = {}
|
|
||||||
let components = {}
|
|
||||||
let componentElements = {}
|
|
||||||
|
|
||||||
const hasComponentElements = () =>
|
|
||||||
Object.getOwnPropertyNames(componentElements).length > 0
|
|
||||||
|
|
||||||
$: {
|
|
||||||
styleVars = {
|
|
||||||
navBarBackground,
|
|
||||||
navBarBorder,
|
|
||||||
navBarColor,
|
|
||||||
selectedItemBackground,
|
|
||||||
selectedItemColor,
|
|
||||||
selectedItemBorder,
|
|
||||||
itemHoverBackground,
|
|
||||||
itemHoverColor,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_children && _children.length > 0 && hasComponentElements()) {
|
|
||||||
const currentSelectedItem =
|
|
||||||
selectedIndex > 0 ? _children[selectedIndex].title : ""
|
|
||||||
if (selectedItem && currentSelectedItem !== selectedItem) {
|
|
||||||
let i = 0
|
|
||||||
for (let child of _children) {
|
|
||||||
if (child.title === selectedItem) {
|
|
||||||
onSelectItem(i)()
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
} else if (!currentSelectedItem) {
|
|
||||||
onSelectItem(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onSelectItem = index => () => {
|
|
||||||
selectedIndex = index
|
|
||||||
if (!components[index]) {
|
|
||||||
const comp = _bb.attachChildren(componentElements[index])
|
|
||||||
components[index] = comp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="root" use:cssVars={styleVars}>
|
|
||||||
{#if !hideNavBar}
|
|
||||||
<div class="navbar">
|
|
||||||
{#each _children as navItem, index}
|
|
||||||
<div
|
|
||||||
class="navitem"
|
|
||||||
on:click={onSelectItem(index)}
|
|
||||||
class:selected={selectedIndex === index}>
|
|
||||||
{navItem.title}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#each _children as navItem, index}
|
|
||||||
<div class="content" bind:this={componentElements[index]} />
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.root {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
grid-template-columns: [navbar] auto [content] 1fr;
|
|
||||||
display: grid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar {
|
|
||||||
grid-column: navbar;
|
|
||||||
background: var(--navBarBackground);
|
|
||||||
border: var(--navBarBorder);
|
|
||||||
color: var(--navBarColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
.navitem {
|
|
||||||
padding: 10px 17px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navitem:hover {
|
|
||||||
background: var(--itemHoverBackground);
|
|
||||||
color: var(--itemHoverColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
.navitem.selected {
|
|
||||||
background: var(--selectedItemBackground);
|
|
||||||
border: var(--selectedItemBorder);
|
|
||||||
color: var(--selectedItemColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
grid-column: content;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,37 +1,20 @@
|
||||||
<script>
|
<script>
|
||||||
export let onLoad
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
const { authStore, linkable, styleable } = getContext("sdk")
|
||||||
|
const styles = getContext("style")
|
||||||
|
|
||||||
export let logoUrl
|
export let logoUrl
|
||||||
export let _bb
|
|
||||||
export let title
|
export let title
|
||||||
|
|
||||||
let itemContainer
|
|
||||||
let hasLoaded
|
|
||||||
|
|
||||||
$: {
|
|
||||||
if (itemContainer) {
|
|
||||||
_bb.attachChildren(itemContainer)
|
|
||||||
if (!hasLoaded) {
|
|
||||||
_bb.call("onLoad")
|
|
||||||
hasLoaded = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const logOut = () => {
|
const logOut = () => {
|
||||||
// TODO: not the best way to clear cookie, try to find better way
|
authStore.actions.logOut()
|
||||||
const appId = location.pathname.split("/")[1]
|
|
||||||
if (appId) {
|
|
||||||
for (let environment of ["local", "cloud"]) {
|
|
||||||
document.cookie = `budibase:${appId}:${environment}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
location.href = `/${appId}`
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="nav">
|
<div class="nav" use:styleable={styles}>
|
||||||
<div class="nav__top">
|
<div class="nav__top">
|
||||||
<a href="/">
|
<a href="/" use:linkable>
|
||||||
{#if logoUrl}
|
{#if logoUrl}
|
||||||
<img class="logo" alt="logo" src={logoUrl} height="48" />
|
<img class="logo" alt="logo" src={logoUrl} height="48" />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -41,7 +24,9 @@
|
||||||
<div on:click={logOut}>Log out</div>
|
<div on:click={logOut}>Log out</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav__menu" bind:this={itemContainer} />
|
<div class="nav__menu">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -1,37 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte"
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
const { DataProvider } = getContext("sdk")
|
||||||
|
|
||||||
export let _bb
|
|
||||||
export let table
|
export let table
|
||||||
|
|
||||||
let row = {}
|
|
||||||
|
|
||||||
$: {
|
|
||||||
row.tableId = table
|
|
||||||
}
|
|
||||||
|
|
||||||
let target
|
|
||||||
|
|
||||||
async function fetchTable(id) {
|
|
||||||
const FETCH_TABLE_URL = `/api/tables/${id}`
|
|
||||||
const response = await _bb.api.get(FETCH_TABLE_URL)
|
|
||||||
return await response.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
if (table && typeof table === "string") {
|
|
||||||
const tableObj = await fetchTable(table)
|
|
||||||
row.tableId = table
|
|
||||||
row._table = tableObj
|
|
||||||
_bb.attachChildren(target, {
|
|
||||||
context: row,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
_bb.attachChildren(target, {
|
|
||||||
context: {},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section bind:this={target} />
|
<DataProvider row={{ tableId: table }}>
|
||||||
|
<slot />
|
||||||
|
</DataProvider>
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
<script>
|
|
||||||
export let value = ""
|
|
||||||
export let text = ""
|
|
||||||
export let _bb
|
|
||||||
|
|
||||||
let actualValue
|
|
||||||
let actualText
|
|
||||||
|
|
||||||
$: actualValue = value || text || ""
|
|
||||||
$: actualText = text || value || ""
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<option value={actualValue}>{actualText}</option>
|
|
|
@ -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="radio" {_bb} {checked} {label} {value} {onchange} />
|
|
|
@ -1,20 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { getContext } from "svelte"
|
||||||
import { RichText } from "@budibase/bbui"
|
import { RichText } from "@budibase/bbui"
|
||||||
|
|
||||||
export let _bb
|
const { styleable } = getContext("sdk")
|
||||||
|
|
||||||
export let content = ""
|
export let value = ""
|
||||||
|
|
||||||
const updateValue = content => {
|
|
||||||
if (_bb) {
|
|
||||||
_bb.setBinding("value", content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$: updateValue(content)
|
|
||||||
|
|
||||||
// Need to determine what options we want to expose.
|
// Need to determine what options we want to expose.
|
||||||
|
|
||||||
let options = {
|
let options = {
|
||||||
modules: {
|
modules: {
|
||||||
toolbar: [
|
toolbar: [
|
||||||
|
@ -31,4 +23,6 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<RichText bind:content {options} />
|
<div use:styleable={styles}>
|
||||||
|
<RichText bind:content={value} {options} />
|
||||||
|
</div>
|
||||||
|
|
|
@ -1,81 +1,42 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte"
|
import { onMount, getContext } from "svelte"
|
||||||
|
|
||||||
|
const { API, screenStore, routeStore, DataProvider } = getContext("sdk")
|
||||||
|
|
||||||
export let _bb
|
|
||||||
export let table
|
export let table
|
||||||
|
|
||||||
let headers = []
|
let headers = []
|
||||||
let store = _bb.store
|
let row
|
||||||
let target
|
|
||||||
|
|
||||||
async function fetchTable(id) {
|
|
||||||
const FETCH_TABLE_URL = `/api/tables/${id}`
|
|
||||||
const response = await _bb.api.get(FETCH_TABLE_URL)
|
|
||||||
return await response.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchFirstRow() {
|
async function fetchFirstRow() {
|
||||||
const FETCH_ROWS_URL = `/api/views/all_${table}`
|
const rows = await API.fetchTableData(table)
|
||||||
const response = await _bb.api.get(FETCH_ROWS_URL)
|
return Array.isArray(rows) && rows.length ? rows[0] : { tableId: table }
|
||||||
if (response.status === 200) {
|
|
||||||
const allRows = await response.json()
|
|
||||||
if (allRows.length > 0) return allRows[0]
|
|
||||||
return { tableId: table }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
const pathParts = window.location.pathname.split("/")
|
|
||||||
|
|
||||||
if (!table) {
|
if (!table) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let row
|
const pathParts = window.location.pathname.split("/")
|
||||||
|
const routeParamId = $routeStore.routeParams.id
|
||||||
|
|
||||||
// if srcdoc, then we assume this is the builder preview
|
// if srcdoc, then we assume this is the builder preview
|
||||||
if (pathParts.length === 0 || pathParts[0] === "srcdoc") {
|
if ((pathParts.length === 0 || pathParts[0] === "srcdoc") && table) {
|
||||||
if (table) row = await fetchFirstRow()
|
console.log("getting first row")
|
||||||
} else if (_bb.routeParams().id) {
|
row = await fetchFirstRow()
|
||||||
const GET_ROW_URL = `/api/${table}/rows/${_bb.routeParams().id}`
|
} else if (routeParamId) {
|
||||||
const response = await _bb.api.get(GET_ROW_URL)
|
row = await API.fetchRow({ tableId: table, rowId: routeParamId })
|
||||||
if (response.status === 200) {
|
|
||||||
row = await response.json()
|
|
||||||
} else {
|
|
||||||
throw new Error("Failed to fetch row.", response)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
throw new Exception("Row ID was not supplied to RowDetail")
|
throw new Error("Row ID was not supplied to RowDetail")
|
||||||
}
|
|
||||||
|
|
||||||
if (row) {
|
|
||||||
// Fetch table schema so we can check for linked rows
|
|
||||||
const tableObj = await fetchTable(row.tableId)
|
|
||||||
for (let key of Object.keys(tableObj.schema)) {
|
|
||||||
const type = tableObj.schema[key].type
|
|
||||||
if (type === "link") {
|
|
||||||
row[`${key}_count`] = Array.isArray(row[key]) ? row[key].length : 0
|
|
||||||
} else if (type === "attachment") {
|
|
||||||
let url = null
|
|
||||||
if (Array.isArray(row[key]) && row[key][0] != null) {
|
|
||||||
url = row[key][0].url
|
|
||||||
}
|
|
||||||
row[`${key}_first`] = url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
row._table = tableObj
|
|
||||||
|
|
||||||
_bb.attachChildren(target, {
|
|
||||||
context: row,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
_bb.attachChildren(target)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(fetchData)
|
||||||
await fetchData()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section bind:this={target} />
|
{#if row}
|
||||||
|
<DataProvider {row}>
|
||||||
|
<slot />
|
||||||
|
</DataProvider>
|
||||||
|
{/if}
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
<script>
|
|
||||||
export let value = ""
|
|
||||||
export let className = ""
|
|
||||||
|
|
||||||
export let _bb
|
|
||||||
|
|
||||||
let actualValue = ""
|
|
||||||
let selectElement
|
|
||||||
|
|
||||||
$: selectElement && _bb.attachChildren(selectElement)
|
|
||||||
|
|
||||||
const onchange = ev => {
|
|
||||||
if (_bb) {
|
|
||||||
_bb.setBinding("value", ev.target.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<select
|
|
||||||
class={className}
|
|
||||||
{value}
|
|
||||||
on:change={onchange}
|
|
||||||
bind:this={selectElement} />
|
|
|
@ -1,4 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
const { styleable } = getContext("sdk")
|
||||||
|
const styles = getContext("style")
|
||||||
|
|
||||||
export let imageUrl = ""
|
export let imageUrl = ""
|
||||||
export let heading = ""
|
export let heading = ""
|
||||||
export let text1 = ""
|
export let text1 = ""
|
||||||
|
@ -9,7 +14,7 @@
|
||||||
$: showImage = !!imageUrl
|
$: showImage = !!imageUrl
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container" use:styleable={styles}>
|
||||||
<a href={destinationUrl}>
|
<a href={destinationUrl}>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{#if showImage}
|
{#if showImage}
|
||||||
|
|
|
@ -1,77 +0,0 @@
|
||||||
<script>
|
|
||||||
export let columns = []
|
|
||||||
export let data = ""
|
|
||||||
export let tableClass = ""
|
|
||||||
export let theadClass = ""
|
|
||||||
export let tbodyClass = ""
|
|
||||||
export let trClass = ""
|
|
||||||
export let thClass = ""
|
|
||||||
export let onRowClick
|
|
||||||
|
|
||||||
export let _bb
|
|
||||||
|
|
||||||
const rowClickHandler = row => () => {
|
|
||||||
// call currently only accepts one argument, so passing row does nothing
|
|
||||||
// however, we do not expose this event anyway. I am leaving this
|
|
||||||
// in for the future, as can and probably should hande this
|
|
||||||
_bb.call("onRowClick", row)
|
|
||||||
}
|
|
||||||
|
|
||||||
const cellValue = (colIndex, row) => {
|
|
||||||
const val = _bb.getStateOrValue(_bb.props.columns[colIndex].value, row)
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<table class={tableClass}>
|
|
||||||
<thead class={theadClass}>
|
|
||||||
<tr class={trClass}>
|
|
||||||
{#each columns as col}
|
|
||||||
<th class={thClass}>{col.title}</th>
|
|
||||||
{/each}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class={tbodyClass}>
|
|
||||||
{#if data}
|
|
||||||
{#each data as row}
|
|
||||||
<tr class={trClass} on:click={rowClickHandler(row)}>
|
|
||||||
{#each columns as col, index}
|
|
||||||
<th class={thClass}>{cellValue(index, row)}</th>
|
|
||||||
{/each}
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.table-default {
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
color: #212529;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-default .thead-default .th-default {
|
|
||||||
vertical-align: bottom;
|
|
||||||
border-bottom: 2px solid #dee2e6;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-default .th-default {
|
|
||||||
padding: 0.75rem;
|
|
||||||
vertical-align: top;
|
|
||||||
border-top: 1px solid #dee2e6;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.th-default {
|
|
||||||
text-align: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-default .tbody-default .tr-default:hover {
|
|
||||||
color: #212529;
|
|
||||||
background-color: rgba(0, 0, 0, 0.075);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,10 +0,0 @@
|
||||||
<script>
|
|
||||||
export let className = ""
|
|
||||||
export let _bb
|
|
||||||
|
|
||||||
let thead
|
|
||||||
|
|
||||||
$: _bb.attachChildren(thead)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<tbody bind:this={thead} class="className" />
|
|
|
@ -1,10 +0,0 @@
|
||||||
<script>
|
|
||||||
export let className = ""
|
|
||||||
export let _bb
|
|
||||||
|
|
||||||
let thead
|
|
||||||
|
|
||||||
$: _bb.attachChildren(thead)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<thead bind:this={thead} class="className" />
|
|
|
@ -1,23 +0,0 @@
|
||||||
export default ({ rows }) =>
|
|
||||||
rows.map(r => ({
|
|
||||||
name: `Save ${r.name} Button`,
|
|
||||||
props: buttonProps(r),
|
|
||||||
}))
|
|
||||||
|
|
||||||
const buttonProps = row => ({
|
|
||||||
_component: "@budibase/standard-components/button",
|
|
||||||
_children: [
|
|
||||||
{
|
|
||||||
_component: "@budibase/standard-components/text",
|
|
||||||
text: `Save ${row.name}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
onClick: [
|
|
||||||
{
|
|
||||||
"##eventHandlerType": "Save Row",
|
|
||||||
parameters: {
|
|
||||||
statePath: `${row.name}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { createApp } from "@budibase/client/src/createApp"
|
|
||||||
import components from "./testComponents"
|
|
||||||
import packageJson from "../../package.json"
|
|
||||||
import { rootComponent } from "./rootComponent"
|
|
||||||
|
|
||||||
export default async () => {
|
|
||||||
delete components._lib
|
|
||||||
const componentLibraries = {}
|
|
||||||
componentLibraries[packageJson.name] = components
|
|
||||||
componentLibraries["testcomponents"] = {
|
|
||||||
rootComponent: rootComponent(window),
|
|
||||||
}
|
|
||||||
const appDef = { hierarchy: {}, actions: {} }
|
|
||||||
const user = { name: "yeo", permissions: [] }
|
|
||||||
const { initialisePage } = createApp(
|
|
||||||
window.document,
|
|
||||||
componentLibraries,
|
|
||||||
{},
|
|
||||||
appDef,
|
|
||||||
user,
|
|
||||||
{},
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
return initialisePage
|
|
||||||
}
|
|
|
@ -1,35 +1,39 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
|
||||||
|
const { styleable } = getContext("sdk")
|
||||||
|
const styles = getContext("style")
|
||||||
|
|
||||||
export let text = ""
|
export let text = ""
|
||||||
export let className = ""
|
export let className = ""
|
||||||
export let type = ""
|
export let type = ""
|
||||||
export let _bb
|
|
||||||
|
|
||||||
const isTag = tag => type === tag
|
const isTag = tag => type === tag
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isTag('none')}
|
{#if isTag('none')}
|
||||||
<span>{text}</span>
|
<span use:styleable={styles}>{text}</span>
|
||||||
{:else if isTag('bold')}
|
{:else if isTag('bold')}
|
||||||
<b class={className}>{text}</b>
|
<b class={className} use:styleable={styles}>{text}</b>
|
||||||
{:else if isTag('strong')}
|
{:else if isTag('strong')}
|
||||||
<strong class={className}>{text}</strong>
|
<strong class={className} use:styleable={styles}>{text}</strong>
|
||||||
{:else if isTag('italic')}
|
{:else if isTag('italic')}
|
||||||
<i class={className}>{text}</i>
|
<i class={className} use:styleable={styles}>{text}</i>
|
||||||
{:else if isTag('emphasis')}
|
{:else if isTag('emphasis')}
|
||||||
<em class={className}>{text}</em>
|
<em class={className} use:styleable={styles}>{text}</em>
|
||||||
{:else if isTag('mark')}
|
{:else if isTag('mark')}
|
||||||
<mark class={className}>{text}</mark>
|
<mark class={className} use:styleable={styles}>{text}</mark>
|
||||||
{:else if isTag('small')}
|
{:else if isTag('small')}
|
||||||
<small class={className}>{text}</small>
|
<small class={className} use:styleable={styles}>{text}</small>
|
||||||
{:else if isTag('del')}
|
{:else if isTag('del')}
|
||||||
<del class={className}>{text}</del>
|
<del class={className} use:styleable={styles}>{text}</del>
|
||||||
{:else if isTag('ins')}
|
{:else if isTag('ins')}
|
||||||
<ins class={className}>{text}</ins>
|
<ins class={className} use:styleable={styles}>{text}</ins>
|
||||||
{:else if isTag('sub')}
|
{:else if isTag('sub')}
|
||||||
<sub class={className}>{text}</sub>
|
<sub class={className} use:styleable={styles}>{text}</sub>
|
||||||
{:else if isTag('sup')}
|
{:else if isTag('sup')}
|
||||||
<sup class={className}>{text}</sup>
|
<sup class={className} use:styleable={styles}>{text}</sup>
|
||||||
{:else}<span>{text}</span>{/if}
|
{:else}<span use:styleable={styles}>{text}</span>{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
span {
|
span {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue