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" |
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 |
||||
} |
|
||||
|
|||||
@ -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 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) |
||||
|
} |
||||
@ -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 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,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,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,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,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> |
<script> |
||||
import { onMount } from "svelte" |
import { getContext } from "svelte" |
||||
|
|
||||
export let _bb |
const { DataProvider } = getContext("sdk") |
||||
export let table |
|
||||
|
|
||||
let row = {} |
|
||||
|
|
||||
$: { |
|
||||
row.tableId = table |
|
||||
} |
|
||||
|
|
||||
let target |
export let table |
||||
|
|
||||
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,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("/") |
||||
// if srcdoc, then we assume this is the builder preview |
const routeParamId = $routeStore.routeParams.id |
||||
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 |
|
||||
|
|
||||
_bb.attachChildren(target, { |
// if srcdoc, then we assume this is the builder preview |
||||
context: row, |
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 { |
} else { |
||||
_bb.attachChildren(target) |
throw new Error("Row ID was not supplied to RowDetail") |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
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,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