forked from tsai/budibase
137 changed files with 3472 additions and 11676 deletions
File diff suppressed because it is too large
@ -1,13 +0,0 @@ |
|||
module.exports = { |
|||
presets: ["@babel/preset-env"], |
|||
sourceMaps: "inline", |
|||
retainLines: true, |
|||
plugins: [ |
|||
[ |
|||
"@babel/plugin-transform-runtime", |
|||
{ |
|||
regenerator: true, |
|||
}, |
|||
], |
|||
], |
|||
} |
|||
@ -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" |
|||
import { getAppId } from "../render/getAppId" |
|||
|
|||
export async function baseApiCall(method, url, body) { |
|||
return await fetch(url, { |
|||
method: method, |
|||
headers: { |
|||
"Content-Type": "application/json", |
|||
"x-budibase-app-id": getAppId(window.document.cookie), |
|||
"x-budibase-type": "client", |
|||
}, |
|||
body: body && JSON.stringify(body), |
|||
credentials: "same-origin", |
|||
}) |
|||
} |
|||
|
|||
const apiCall = method => async ({ url, body }) => { |
|||
const response = await baseApiCall(method, url, body) |
|||
|
|||
switch (response.status) { |
|||
case 200: |
|||
return response.json() |
|||
case 404: |
|||
return error(`${url} Not found`) |
|||
case 400: |
|||
return error(`${url} Bad Request`) |
|||
case 403: |
|||
return error(`${url} Forbidden`) |
|||
default: |
|||
if (response.status >= 200 && response.status < 400) { |
|||
return response.json() |
|||
} |
|||
|
|||
return error(`${url} - ${response.statusText}`) |
|||
} |
|||
} |
|||
|
|||
const post = apiCall("POST") |
|||
const get = apiCall("GET") |
|||
const patch = apiCall("PATCH") |
|||
const del = apiCall("DELETE") |
|||
|
|||
const ERROR_MEMBER = "##error" |
|||
const error = message => { |
|||
// appStore.update(s => s["##error_message"], message)
|
|||
return { [ERROR_MEMBER]: message } |
|||
} |
|||
|
|||
const isSuccess = obj => !obj || !obj[ERROR_MEMBER] |
|||
|
|||
const apiOpts = { |
|||
isSuccess, |
|||
error, |
|||
post, |
|||
get, |
|||
patch, |
|||
delete: del, |
|||
} |
|||
|
|||
const saveRow = async (params, state) => |
|||
await post({ |
|||
url: `/api/${params.tableId}/rows`, |
|||
body: makeRowRequestBody(params, state), |
|||
}) |
|||
|
|||
const updateRow = async (params, state) => { |
|||
const row = makeRowRequestBody(params, state) |
|||
row._id = params._id |
|||
await patch({ |
|||
url: `/api/${params.tableId}/rows/${params._id}`, |
|||
body: row, |
|||
}) |
|||
} |
|||
|
|||
const deleteRow = async params => |
|||
await del({ |
|||
url: `/api/${params.tableId}/rows/${params.rowId}/${params.revId}`, |
|||
}) |
|||
|
|||
const makeRowRequestBody = (parameters, state) => { |
|||
// start with the row thats currently in context
|
|||
const body = { ...(state.data || {}) } |
|||
|
|||
// dont send the table
|
|||
if (body._table) delete body._table |
|||
|
|||
// then override with supplied parameters
|
|||
if (parameters.fields) { |
|||
for (let fieldName of Object.keys(parameters.fields)) { |
|||
const field = parameters.fields[fieldName] |
|||
|
|||
// ensure fields sent are of the correct type
|
|||
if (field.type === "boolean") { |
|||
if (field.value === "true") body[fieldName] = true |
|||
if (field.value === "false") body[fieldName] = false |
|||
} else if (field.type === "number") { |
|||
const val = parseFloat(field.value) |
|||
if (!isNaN(val)) { |
|||
body[fieldName] = val |
|||
} |
|||
} else if (field.type === "datetime") { |
|||
const date = new Date(field.value) |
|||
if (!isNaN(date.getTime())) { |
|||
body[fieldName] = date.toISOString() |
|||
} |
|||
} else { |
|||
body[fieldName] = field.value |
|||
} |
|||
} |
|||
} |
|||
|
|||
return body |
|||
} |
|||
|
|||
export default { |
|||
authenticate: authenticate(apiOpts), |
|||
saveRow, |
|||
updateRow, |
|||
deleteRow, |
|||
} |
|||
export * from "./rows" |
|||
export * from "./auth" |
|||
export * from "./datasources" |
|||
export * from "./tables" |
|||
export * from "./attachments" |
|||
export * from "./views" |
|||
export * from "./relationships" |
|||
export * from "./routes" |
|||
export * from "./app" |
|||
|
|||
@ -0,0 +1,14 @@ |
|||
import API from "./api" |
|||
import { enrichRows } from "./rows" |
|||
|
|||
/** |
|||
* Fetches related rows for a certain field of a certain row. |
|||
*/ |
|||
export const fetchRelationshipData = async ({ tableId, rowId, fieldName }) => { |
|||
if (!tableId || !rowId || !fieldName) { |
|||
return [] |
|||
} |
|||
const response = await API.get({ url: `/api/${tableId}/${rowId}/enrich` }) |
|||
const rows = response[fieldName] || [] |
|||
return await enrichRows(rows, tableId) |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
import API from "./api" |
|||
|
|||
/** |
|||
* Fetches available routes for the client app. |
|||
*/ |
|||
export const fetchRoutes = async () => { |
|||
return await API.get({ |
|||
url: `/api/routing/client`, |
|||
}) |
|||
} |
|||
@ -0,0 +1,86 @@ |
|||
import API from "./api" |
|||
import { fetchTableDefinition } from "./tables" |
|||
|
|||
/** |
|||
* Fetches data about a certain row in a table. |
|||
*/ |
|||
export const fetchRow = async ({ tableId, rowId }) => { |
|||
const row = await API.get({ |
|||
url: `/api/${tableId}/rows/${rowId}`, |
|||
}) |
|||
return (await enrichRows([row], tableId))[0] |
|||
} |
|||
|
|||
/** |
|||
* Creates a row in a table. |
|||
*/ |
|||
export const saveRow = async row => { |
|||
return await API.post({ |
|||
url: `/api/${row.tableId}/rows`, |
|||
body: row, |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* Updates a row in a table. |
|||
*/ |
|||
export const updateRow = async row => { |
|||
return await API.patch({ |
|||
url: `/api/${row.tableId}/rows/${row._id}`, |
|||
body: row, |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* Deletes a row from a table. |
|||
*/ |
|||
export const deleteRow = async ({ tableId, rowId, revId }) => { |
|||
return await API.del({ |
|||
url: `/api/${tableId}/rows/${rowId}/${revId}`, |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* Deletes many rows from a table. |
|||
*/ |
|||
export const deleteRows = async ({ tableId, rows }) => { |
|||
return await API.post({ |
|||
url: `/api/${tableId}/rows`, |
|||
body: { |
|||
rows, |
|||
type: "delete", |
|||
}, |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* Enriches rows which contain certain field types so that they can |
|||
* be properly displayed. |
|||
*/ |
|||
export const enrichRows = async (rows, tableId) => { |
|||
if (rows && rows.length && tableId) { |
|||
// Fetch table schema so we can check column types
|
|||
const tableDefinition = await fetchTableDefinition(tableId) |
|||
const schema = tableDefinition && tableDefinition.schema |
|||
if (schema) { |
|||
const keys = Object.keys(schema) |
|||
rows.forEach(row => { |
|||
for (let key of keys) { |
|||
const type = schema[key].type |
|||
if (type === "link") { |
|||
// Enrich row with the count of any relationship fields
|
|||
row[`${key}_count`] = Array.isArray(row[key]) ? row[key].length : 0 |
|||
} else if (type === "attachment") { |
|||
// Enrich row with the first image URL for any attachment fields
|
|||
let url = null |
|||
if (Array.isArray(row[key]) && row[key][0] != null) { |
|||
url = row[key][0].url |
|||
} |
|||
row[`${key}_first`] = url |
|||
} |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
return rows |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
import API from "./api" |
|||
import { enrichRows } from "./rows" |
|||
|
|||
/** |
|||
* Fetches a table definition. |
|||
* Since definitions cannot change at runtime, the result is cached. |
|||
*/ |
|||
export const fetchTableDefinition = async tableId => { |
|||
return await API.get({ url: `/api/tables/${tableId}`, cache: true }) |
|||
} |
|||
|
|||
/** |
|||
* Fetches all rows from a table. |
|||
*/ |
|||
export const fetchTableData = async tableId => { |
|||
const rows = await API.get({ url: `/api/${tableId}/rows` }) |
|||
return await enrichRows(rows, tableId) |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
import API from "./api" |
|||
import { enrichRows } from "./rows" |
|||
|
|||
/** |
|||
* Fetches all rows in a view. |
|||
*/ |
|||
export const fetchViewData = async ({ |
|||
name, |
|||
field, |
|||
groupBy, |
|||
calculation, |
|||
tableId, |
|||
}) => { |
|||
const params = new URLSearchParams() |
|||
|
|||
if (calculation) { |
|||
params.set("field", field) |
|||
params.set("calculation", calculation) |
|||
} |
|||
if (groupBy) { |
|||
params.set("group", groupBy) |
|||
} |
|||
|
|||
const QUERY_VIEW_URL = field |
|||
? `/api/views/${name}?${params}` |
|||
: `/api/views/${name}` |
|||
|
|||
const rows = await API.get({ url: QUERY_VIEW_URL }) |
|||
return await enrichRows(rows, tableId) |
|||
} |
|||
@ -0,0 +1,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 { builtins, builtinLibName } from "./render/builtinComponents" |
|||
import { getAppId } from "./render/getAppId" |
|||
import ClientApp from "./components/ClientApp.svelte" |
|||
|
|||
/** |
|||
* 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, |
|||
// Initialise client app
|
|||
const loadBudibase = () => { |
|||
window.document.body.innerHTML = "" |
|||
new ClientApp({ |
|||
target: window.document.body, |
|||
}) |
|||
|
|||
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 |
|||
} |
|||
// Attach to window so the HTML template can call this when it loads
|
|||
window.loadBudibase = loadBudibase |
|||
|
|||
@ -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,15 +1,14 @@ |
|||
import api from "../api" |
|||
import renderTemplateString from "./renderTemplateString" |
|||
import { updateRow, saveRow, deleteRow } from "@budibase/component-sdk" |
|||
|
|||
export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType" |
|||
|
|||
export const eventHandlers = routeTo => { |
|||
const handlers = { |
|||
"Navigate To": param => routeTo(param && param.url), |
|||
"Update Row": api.updateRow, |
|||
"Save Row": api.saveRow, |
|||
"Delete Row": api.deleteRow, |
|||
"Trigger Workflow": api.triggerWorkflow, |
|||
"Update Row": updateRow, |
|||
"Save Row": saveRow, |
|||
"Delete Row": deleteRow, |
|||
} |
|||
|
|||
// 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) |
|||
} |
|||
@ -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
@ -1,38 +1,39 @@ |
|||
import svelte from "rollup-plugin-svelte" |
|||
import resolve from "rollup-plugin-node-resolve" |
|||
import commonjs from "@rollup/plugin-commonjs" |
|||
import resolve from "@rollup/plugin-node-resolve" |
|||
// import replace from "@rollup/plugin-replace"
|
|||
import svelte from "rollup-plugin-svelte" |
|||
import postcss from "rollup-plugin-postcss" |
|||
import { terser } from "rollup-plugin-terser" |
|||
|
|||
const production = !process.env.ROLLUP_WATCH |
|||
const lodash_fp_exports = ["isEmpty"] |
|||
const externals = ["svelte", "svelte/internal"] |
|||
|
|||
export default { |
|||
external: externals, |
|||
input: "src/index.js", |
|||
output: [ |
|||
{ |
|||
file: "dist/index.js", |
|||
format: "esm", |
|||
name: "budibaseStandardComponents", |
|||
sourcemap: true, |
|||
sourcemap: false, |
|||
}, |
|||
], |
|||
plugins: [ |
|||
// Only run terser in production environments
|
|||
production && terser(), |
|||
postcss({ |
|||
plugins: [], |
|||
}), |
|||
postcss(), |
|||
svelte({ |
|||
hydratable: true, |
|||
dev: !production, |
|||
}), |
|||
resolve({ |
|||
browser: true, |
|||
skip: externals, |
|||
}), |
|||
commonjs({ |
|||
namedExports: { |
|||
"lodash/fp": lodash_fp_exports, |
|||
}, |
|||
}), |
|||
commonjs(), |
|||
// Fix for https://github.com/sveltejs/svelte/issues/3165
|
|||
// replace({
|
|||
// "outros.c.push":
|
|||
// "if (outros === undefined) { block.o(local); return }\noutros.c.push",
|
|||
// }),
|
|||
], |
|||
} |
|||
|
|||
@ -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> |
|||
import { cssVars, createClasses } from "./cssVars" |
|||
import { getContext } from "svelte" |
|||
|
|||
const { styleable } = getContext("sdk") |
|||
const styles = getContext("style") |
|||
|
|||
export let className = "" |
|||
export let onLoad |
|||
export let type = "div" |
|||
export let _bb |
|||
|
|||
let containerElement |
|||
let hasLoaded |
|||
let currentChildren |
|||
|
|||
$: { |
|||
if (containerElement) { |
|||
_bb.attachChildren(containerElement) |
|||
if (!hasLoaded) { |
|||
_bb.call("onLoad") |
|||
hasLoaded = true |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
{#if type === 'div'} |
|||
<div bind:this={containerElement} /> |
|||
<div use:styleable={styles}> |
|||
<slot /> |
|||
</div> |
|||
{:else if type === 'header'} |
|||
<header bind:this={containerElement} /> |
|||
<header use:styleable={styles}> |
|||
<slot /> |
|||
</header> |
|||
{:else if type === 'main'} |
|||
<main bind:this={containerElement} /> |
|||
<main use:styleable={styles}> |
|||
<slot /> |
|||
</main> |
|||
{:else if type === 'footer'} |
|||
<footer bind:this={containerElement} /> |
|||
<footer use:styleable={styles}> |
|||
<slot /> |
|||
</footer> |
|||
{:else if type === 'aside'} |
|||
<aside bind:this={containerElement} /> |
|||
<aside use:styleable={styles}> |
|||
<slot /> |
|||
</aside> |
|||
{:else if type === 'summary'} |
|||
<summary bind:this={containerElement} /> |
|||
<summary use:styleable={styles}> |
|||
<slot /> |
|||
</summary> |
|||
{:else if type === 'details'} |
|||
<details bind:this={containerElement} /> |
|||
<details use:styleable={styles}> |
|||
<slot /> |
|||
</details> |
|||
{:else if type === 'article'} |
|||
<article bind:this={containerElement} /> |
|||
<article use:styleable={styles}> |
|||
<slot /> |
|||
</article> |
|||
{:else if type === 'nav'} |
|||
<nav bind:this={containerElement} /> |
|||
<nav use:styleable={styles}> |
|||
<slot /> |
|||
</nav> |
|||
{:else if type === 'mark'} |
|||
<mark bind:this={containerElement} /> |
|||
<mark use:styleable={styles}> |
|||
<slot /> |
|||
</mark> |
|||
{:else if type === 'figure'} |
|||
<figure bind:this={containerElement} /> |
|||
<figure use:styleable={styles}> |
|||
<slot /> |
|||
</figure> |
|||
{:else if type === 'figcaption'} |
|||
<figcaption bind:this={containerElement} /> |
|||
<figcaption use:styleable={styles}> |
|||
<slot /> |
|||
</figcaption> |
|||
{:else if type === 'paragraph'} |
|||
<p bind:this={containerElement} /> |
|||
<p use:styleable={styles}> |
|||
<slot /> |
|||
</p> |
|||
{/if} |
|||
|
|||
@ -1,10 +1,5 @@ |
|||
<script> |
|||
import Form from "./Form.svelte" |
|||
|
|||
export let _bb |
|||
export let table |
|||
export let title |
|||
export let buttonText |
|||
</script> |
|||
|
|||
<Form {_bb} {table} {title} {buttonText} wide={false} /> |
|||
<Form wide={false} /> |
|||
|
|||
@ -1,10 +1,5 @@ |
|||
<script> |
|||
import Form from "./Form.svelte" |
|||
|
|||
export let _bb |
|||
export let table |
|||
export let title |
|||
export let buttonText |
|||
</script> |
|||
|
|||
<Form {_bb} {table} {title} {buttonText} wide={true} /> |
|||
<Form wide /> |
|||
|
|||
@ -1,20 +0,0 @@ |
|||
import api from "../../api" |
|||
|
|||
let cache = {} |
|||
|
|||
async function fetchTable(id) { |
|||
const FETCH_TABLE_URL = `/api/tables/${id}` |
|||
const response = await api.get(FETCH_TABLE_URL) |
|||
return await response.json() |
|||
} |
|||
|
|||
export async function getTable(tableId) { |
|||
if (!tableId) { |
|||
return null |
|||
} |
|||
if (!cache[tableId]) { |
|||
cache[tableId] = fetchTable(tableId) |
|||
cache[tableId] = await cache[tableId] |
|||
} |
|||
return await cache[tableId] |
|||
} |
|||
@ -1,82 +0,0 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
|
|||
export let _bb |
|||
export let table |
|||
export let layout = "list" |
|||
|
|||
let headers = [] |
|||
let store = _bb.store |
|||
|
|||
async function fetchData() { |
|||
if (!table || !table.length) return |
|||
|
|||
const FETCH_ROWS_URL = `/api/views/all_${table}` |
|||
const response = await _bb.api.get(FETCH_ROWS_URL) |
|||
if (response.status === 200) { |
|||
const json = await response.json() |
|||
|
|||
store.update(state => { |
|||
state[table] = json |
|||
return state |
|||
}) |
|||
} else { |
|||
throw new Error("Failed to fetch rows.", response) |
|||
} |
|||
} |
|||
|
|||
$: data = $store[table] || [] |
|||
$: if (table) fetchData() |
|||
|
|||
onMount(async () => { |
|||
await fetchData() |
|||
}) |
|||
</script> |
|||
|
|||
<section class:grid={layout === 'grid'} class:list={layout === 'list'}> |
|||
{#each data as data} |
|||
<div class="data-card"> |
|||
<ul> |
|||
{#each Object.keys(data) as key} |
|||
<li> |
|||
<span class="data-key">{key}:</span> |
|||
<span class="data-value">{data[key]}</span> |
|||
</li> |
|||
{/each} |
|||
</ul> |
|||
</div> |
|||
{/each} |
|||
</section> |
|||
|
|||
<style> |
|||
.list { |
|||
display: flex; |
|||
flex-direction: column; |
|||
font-family: Inter; |
|||
} |
|||
|
|||
.grid { |
|||
display: grid; |
|||
grid-template-columns: 1fr 1fr 1fr; |
|||
} |
|||
|
|||
ul { |
|||
list-style-type: none; |
|||
} |
|||
|
|||
li { |
|||
margin: 5px 0 5px 0; |
|||
} |
|||
|
|||
.data-card { |
|||
border: 1px solid #ccc; |
|||
border-radius: 5px; |
|||
padding: 10px; |
|||
} |
|||
|
|||
.data-key { |
|||
font-weight: bold; |
|||
font-size: 20px; |
|||
text-transform: capitalize; |
|||
} |
|||
</style> |
|||
@ -1,15 +0,0 @@ |
|||
<script> |
|||
export let _bb |
|||
export let table |
|||
|
|||
let searchValue = "" |
|||
|
|||
function search() { |
|||
const SEARCH_URL = _bb.api.get(``) |
|||
} |
|||
</script> |
|||
|
|||
<div> |
|||
<input type="text" bind:value={searchValue} /> |
|||
<button on:click={search}>Search</button> |
|||
</div> |
|||
@ -1,18 +1,19 @@ |
|||
<script> |
|||
import Flatpickr from "svelte-flatpickr" |
|||
import { DatePicker } from "@budibase/bbui" |
|||
import { getContext } from "svelte" |
|||
|
|||
const { styleable } = getContext("sdk") |
|||
const styles = getContext("style") |
|||
|
|||
export let placeholder |
|||
export let value |
|||
|
|||
export let _bb |
|||
|
|||
function handleChange(event) { |
|||
const [fullDate, dateStr, instance] = event.detail |
|||
if (_bb) { |
|||
_bb.setBinding("value", fullDate) |
|||
} |
|||
const [fullDate] = event.detail |
|||
value = fullDate |
|||
} |
|||
</script> |
|||
|
|||
<DatePicker {placeholder} on:change={handleChange} {value} /> |
|||
<div use:styleable={styles}> |
|||
<DatePicker {placeholder} on:change={handleChange} {value} /> |
|||
</div> |
|||
|
|||
@ -1,5 +1,23 @@ |
|||
<script> |
|||
import { getContext } from "svelte" |
|||
|
|||
const { styleable } = getContext("sdk") |
|||
const styles = getContext("style") |
|||
|
|||
export let embed |
|||
</script> |
|||
|
|||
{@html embed} |
|||
<div use:styleable={styles}> |
|||
{@html embed} |
|||
</div> |
|||
|
|||
<style> |
|||
div { |
|||
position: relative; |
|||
} |
|||
div :global(> *) { |
|||
width: 100%; |
|||
height: 100%; |
|||
position: absolute; |
|||
} |
|||
</style> |
|||
|
|||
@ -1,30 +1,24 @@ |
|||
<script> |
|||
import { buildStyle } from "./buildStyle.js" |
|||
import { getContext } from "svelte" |
|||
|
|||
const { styleable } = getContext("sdk") |
|||
const styles = getContext("style") |
|||
|
|||
export let className = "" |
|||
export let type |
|||
export let text = "" |
|||
|
|||
export let _bb |
|||
|
|||
let containerElement |
|||
|
|||
$: containerElement && |
|||
!text && |
|||
_bb.props.children && |
|||
_bb.props.children.length && |
|||
_bb.attachChildren(containerElement) |
|||
</script> |
|||
|
|||
{#if type === 'h1'} |
|||
<h1 class={className} bind:this={containerElement}>{text}</h1> |
|||
<h1 class={className} use:styleable={styles}>{text}</h1> |
|||
{:else if type === 'h2'} |
|||
<h2 class={className} bind:this={containerElement}>{text}</h2> |
|||
<h2 class={className} use:styleable={styles}>{text}</h2> |
|||
{:else if type === 'h3'} |
|||
<h3 class={className} bind:this={containerElement}>{text}</h3> |
|||
<h3 class={className} use:styleable={styles}>{text}</h3> |
|||
{:else if type === 'h4'} |
|||
<h4 class={className} bind:this={containerElement}>{text}</h4> |
|||
<h4 class={className} use:styleable={styles}>{text}</h4> |
|||
{:else if type === 'h5'} |
|||
<h5 class={className} bind:this={containerElement}>{text}</h5> |
|||
<h5 class={className} use:styleable={styles}>{text}</h5> |
|||
{:else if type === 'h6'} |
|||
<h6 class={className} bind:this={containerElement}>{text}</h6> |
|||
<h6 class={className} use:styleable={styles}>{text}</h6> |
|||
{/if} |
|||
|
|||
@ -1,9 +1,16 @@ |
|||
<script> |
|||
import "@fortawesome/fontawesome-free/js/all.js" |
|||
import { getContext } from "svelte" |
|||
|
|||
const { styleable } = getContext("sdk") |
|||
const styles = getContext("style") |
|||
|
|||
export let icon = "" |
|||
export let size = "fa-lg" |
|||
export let color = "#000" |
|||
</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> |
|||
import { buildStyle } from "./buildStyle" |
|||
import { getContext } from "svelte" |
|||
|
|||
const { styleable } = getContext("sdk") |
|||
const styles = getContext("style") |
|||
|
|||
export let className = "" |
|||
export let url = "" |
|||
export let description = "" |
|||
export let height |
|||
export let width |
|||
|
|||
export let _bb |
|||
|
|||
$: style = buildStyle({ height, width }) |
|||
</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> |
|||
export let id = "" |
|||
import { getContext } from "svelte" |
|||
|
|||
const { styleable } = getContext("sdk") |
|||
const styles = getContext("style") |
|||
|
|||
export let value = "" |
|||
export let className = "" |
|||
export let type = "text" |
|||
export let label = "" |
|||
export let checked = false |
|||
|
|||
export let _bb |
|||
|
|||
let actualValue = "" |
|||
|
|||
const onchange = ev => { |
|||
if (_bb) { |
|||
const val = type === "checkbox" ? ev.target.checked : ev.target.value |
|||
_bb.setBinding("value", val) |
|||
} |
|||
value = ev.target.value |
|||
} |
|||
</script> |
|||
|
|||
<label for={id}>{label}</label> |
|||
<input {id} class={className} {type} {value} {checked} on:change={onchange} /> |
|||
<input |
|||
class={className} |
|||
{type} |
|||
{value} |
|||
on:change={onchange} |
|||
use:styleable={styles} /> |
|||
|
|||
@ -1,22 +1,17 @@ |
|||
<script> |
|||
import { cssVars, createClasses } from "./cssVars" |
|||
import { getContext } from "svelte" |
|||
|
|||
const { linkable, styleable } = getContext("sdk") |
|||
const styles = getContext("style") |
|||
|
|||
export let url = "" |
|||
export let text = "" |
|||
export let openInNewTab = false |
|||
|
|||
export let _bb |
|||
|
|||
let anchorElement |
|||
|
|||
$: anchorElement && !text && _bb.attachChildren(anchorElement) |
|||
$: target = openInNewTab ? "_blank" : "_self" |
|||
</script> |
|||
|
|||
<a href={url} bind:this={anchorElement} {target}>{text}</a> |
|||
|
|||
<style> |
|||
.textDecoration { |
|||
text-decoration: var(--textDecoration); |
|||
} |
|||
</style> |
|||
<a href={url} use:linkable {target} use:styleable={styles}> |
|||
{text} |
|||
<slot /> |
|||
</a> |
|||
|
|||
@ -1,23 +1,26 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
import fetchData from "./fetchData.js" |
|||
import { getContext, onMount } from "svelte" |
|||
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 = [] |
|||
|
|||
let target |
|||
let store = _bb.store |
|||
let rows = [] |
|||
|
|||
onMount(async () => { |
|||
if (!isEmpty(datasource)) { |
|||
const data = await fetchData(datasource, $store) |
|||
_bb.attachChildren(target, { |
|||
hydrate: false, |
|||
context: data, |
|||
}) |
|||
rows = await API.fetchDatasource(datasource, $dataContextStore) |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
<section bind:this={target} /> |
|||
<div use:styleable={styles}> |
|||
{#each rows as row} |
|||
<DataProvider {row}> |
|||
<slot /> |
|||
</DataProvider> |
|||
{/each} |
|||
</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,11 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
import { getContext } from "svelte" |
|||
|
|||
export let _bb |
|||
export let table |
|||
|
|||
let row = {} |
|||
|
|||
$: { |
|||
row.tableId = table |
|||
} |
|||
const { DataProvider } = getContext("sdk") |
|||
|
|||
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: {}, |
|||
}) |
|||
} |
|||
}) |
|||
export let table |
|||
</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,81 +1,42 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
import { onMount, getContext } from "svelte" |
|||
|
|||
const { API, screenStore, routeStore, DataProvider } = getContext("sdk") |
|||
|
|||
export let _bb |
|||
export let table |
|||
|
|||
let headers = [] |
|||
let store = _bb.store |
|||
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() |
|||
} |
|||
let row |
|||
|
|||
async function fetchFirstRow() { |
|||
const FETCH_ROWS_URL = `/api/views/all_${table}` |
|||
const response = await _bb.api.get(FETCH_ROWS_URL) |
|||
if (response.status === 200) { |
|||
const allRows = await response.json() |
|||
if (allRows.length > 0) return allRows[0] |
|||
return { tableId: table } |
|||
} |
|||
const rows = await API.fetchTableData(table) |
|||
return Array.isArray(rows) && rows.length ? rows[0] : { tableId: table } |
|||
} |
|||
|
|||
async function fetchData() { |
|||
const pathParts = window.location.pathname.split("/") |
|||
|
|||
if (!table) { |
|||
return |
|||
} |
|||
|
|||
let row |
|||
// if srcdoc, then we assume this is the builder preview |
|||
if (pathParts.length === 0 || pathParts[0] === "srcdoc") { |
|||
if (table) row = await fetchFirstRow() |
|||
} else if (_bb.routeParams().id) { |
|||
const GET_ROW_URL = `/api/${table}/rows/${_bb.routeParams().id}` |
|||
const response = await _bb.api.get(GET_ROW_URL) |
|||
if (response.status === 200) { |
|||
row = await response.json() |
|||
} else { |
|||
throw new Error("Failed to fetch row.", response) |
|||
} |
|||
} else { |
|||
throw new Exception("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 |
|||
const pathParts = window.location.pathname.split("/") |
|||
const routeParamId = $routeStore.routeParams.id |
|||
|
|||
_bb.attachChildren(target, { |
|||
context: row, |
|||
}) |
|||
// if srcdoc, then we assume this is the builder preview |
|||
if ((pathParts.length === 0 || pathParts[0] === "srcdoc") && table) { |
|||
console.log("getting first row") |
|||
row = await fetchFirstRow() |
|||
} else if (routeParamId) { |
|||
row = await API.fetchRow({ tableId: table, rowId: routeParamId }) |
|||
} else { |
|||
_bb.attachChildren(target) |
|||
throw new Error("Row ID was not supplied to RowDetail") |
|||
} |
|||
} |
|||
|
|||
onMount(async () => { |
|||
await fetchData() |
|||
}) |
|||
onMount(fetchData) |
|||
</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,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 |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue